From 3a7a413c6964d58a6233795cff777d9cd5f42356 Mon Sep 17 00:00:00 2001 From: Samy Avrillon Date: Thu, 27 Feb 2025 18:08:03 +0100 Subject: [PATCH] Added better error messages, and tested json API --- build.gradle | 4 + .../java/com/bernard/misael/model/Answer.java | 16 +- .../bernard/misael/questions/DccQuestion.java | 14 +- .../misael/questions/QuestionType.java | 5 +- .../misael/service/JsonNodeConverter.java | 32 ++++ .../misael/service/QuizzManagerImpl.java | 142 ++++++++++++------ .../exception/MalformedAnswerException.java | 5 + .../MalformedClientAnswerException.java | 5 + .../exception/QuestionTypeException.java | 5 + .../ShouldNotAnswerNowException.java | 5 - .../misael/web/QuestionsController.java | 7 +- src/main/resources/templates/form.html | 44 +++++- 12 files changed, 226 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/bernard/misael/service/JsonNodeConverter.java create mode 100644 src/main/java/com/bernard/misael/service/exception/MalformedAnswerException.java create mode 100644 src/main/java/com/bernard/misael/service/exception/MalformedClientAnswerException.java create mode 100644 src/main/java/com/bernard/misael/service/exception/QuestionTypeException.java delete mode 100644 src/main/java/com/bernard/misael/service/exception/ShouldNotAnswerNowException.java diff --git a/build.gradle b/build.gradle index aac6324..d8ccdf1 100644 --- a/build.gradle +++ b/build.gradle @@ -40,12 +40,16 @@ dependencies { implementation 'org.flywaydb:flyway-database-postgresql:11.3.2' implementation 'org.springframework.session:spring-session-jdbc' implementation 'jakarta.validation:jakarta.validation-api:3.1.1' + runtimeOnly 'org.webjars:jquery:1.9.1' developmentOnly 'org.springframework.boot:spring-boot-devtools' developmentOnly 'org.springframework.boot:spring-boot-docker-compose' runtimeOnly 'org.postgresql:postgresql' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.18.2' + //Lombok compileOnly 'org.projectlombok:lombok:1.18.36' annotationProcessor 'org.projectlombok:lombok:1.18.36' diff --git a/src/main/java/com/bernard/misael/model/Answer.java b/src/main/java/com/bernard/misael/model/Answer.java index 567b3c3..0fce67d 100644 --- a/src/main/java/com/bernard/misael/model/Answer.java +++ b/src/main/java/com/bernard/misael/model/Answer.java @@ -1,6 +1,12 @@ package com.bernard.misael.model; +import com.bernard.misael.service.JsonNodeConverter; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; + import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -35,6 +41,14 @@ public class Answer { @JoinColumn(name = "form",nullable = false) private QuizzForm form; - private String value; + @Convert(converter = JsonNodeConverter.class) + private JsonNode value; + + public Answer(QuizzForm qf, Question q) { + super(); + this.setQuestion(q); + this.setForm(qf); + this.setValue(JsonNodeFactory.instance.objectNode()); + } } diff --git a/src/main/java/com/bernard/misael/questions/DccQuestion.java b/src/main/java/com/bernard/misael/questions/DccQuestion.java index 9c48658..85a9321 100644 --- a/src/main/java/com/bernard/misael/questions/DccQuestion.java +++ b/src/main/java/com/bernard/misael/questions/DccQuestion.java @@ -6,6 +6,9 @@ import java.util.List; import java.util.Random; import java.util.stream.Collectors; +import com.bernard.misael.service.exception.MalformedAnswerException; +import com.bernard.misael.service.exception.MalformedClientAnswerException; +import com.bernard.misael.service.exception.QuestionTypeException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -26,7 +29,7 @@ public class DccQuestion implements QuestionType{ } @Override - public JsonNode clientQuestionData(int step, JsonNode answer) { + public JsonNode clientQuestionData(int step, JsonNode answer) throws QuestionTypeException{ ObjectNode o = JsonNodeFactory.instance.objectNode(); switch (step) { case 0: @@ -34,6 +37,7 @@ public class DccQuestion implements QuestionType{ return o; case 1: Random r = new Random(); + if(!answer.has("type") || !answer.get("type").isInt()) throw new MalformedAnswerException(); int k = answer.get("type").asInt(); switch(k) { case 0: @@ -42,13 +46,13 @@ public class DccQuestion implements QuestionType{ case 4: List answers = new ArrayList<>(k); answers.add(goodAnswer); - for(int i = 0;i { + + private ObjectMapper om; + + public JsonNodeConverter() { + this.om = new ObjectMapper(); + } + + @Override + public String convertToDatabaseColumn(JsonNode attribute) { + return attribute.toString(); + } + + @Override + public JsonNode convertToEntityAttribute(String dbData) { + try { + return om.readTree(dbData); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException(e); + } + } + +} diff --git a/src/main/java/com/bernard/misael/service/QuizzManagerImpl.java b/src/main/java/com/bernard/misael/service/QuizzManagerImpl.java index 31a5c93..8d5ede7 100644 --- a/src/main/java/com/bernard/misael/service/QuizzManagerImpl.java +++ b/src/main/java/com/bernard/misael/service/QuizzManagerImpl.java @@ -1,5 +1,7 @@ package com.bernard.misael.service; +import java.util.Optional; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @@ -13,9 +15,10 @@ import com.bernard.misael.repository.AnswerRepository; import com.bernard.misael.repository.QuestionRepository; import com.bernard.misael.repository.QuizzFormRepository; import com.bernard.misael.repository.QuizzRepository; -import com.fasterxml.jackson.core.JsonProcessingException; +import com.bernard.misael.service.exception.MalformedAnswerException; +import com.bernard.misael.service.exception.MalformedClientAnswerException; +import com.bernard.misael.service.exception.QuestionTypeException; import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.ObjectNode; @@ -36,69 +39,115 @@ public class QuizzManagerImpl implements QuizzManager { @Override public JsonNode answer(User user, long quizzId, JsonNode data) { - //TODO replace conversions String <-> JsonNode to Answer type - Quizz quizz = qRepository.findById(quizzId).get(); + if(!data.has("index") || !data.get("index").isInt()) + return errorNode("Request should contain the question index"); + if(!data.has("step") || !data.get("step").isInt()) + return errorNode("Request should contain the answer step"); + if(!data.has("data")) + return errorNode("Request should contain the answer data"); + if(user == null) + return errorNode("You must be logged in to answer"); + Optional oquizz = qRepository.findById(quizzId); + if(!oquizz.isPresent()) + return errorNode("Could not find the quizz with id "+quizzId); + Quizz quizz = oquizz.get(); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz); - + if(qf == null) + return errorNode("The quizzform does not exist, ask the question first"); if(qf.isDone()) - throw new UnsupportedOperationException();//TODO add more precise exceptions here + return errorNode("You're done with the quizz, you cannot answer anymore"); int qindex = qf.getCurrentQuestion(); + if(qindex != data.get("index").intValue()) + return errorNode("You are not answering the right question (you answer question "+data.get("index").intValue() + +" where you should answer question "+qindex+")"); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex); + if(q == null) + return errorNode("Could not find question "+qindex); int step = qf.getAnswerStep(); + if(step != data.get("step").intValue()) + return errorNode("You are not answering the right step of the question (you answer step "+data.get("step").intValue() + +" where you should step question "+step+")"); + Answer answer = answerRepository.findByFormAndQuestion(qf, q); - - ObjectMapper om = new ObjectMapper(); - JsonNode answerData; + if(answer == null) + return errorNode("The database answer object does not exist, ask the question first"); + + JsonNode answerData = answer.getValue(); + AnswerResult result; try { - answerData = om.readTree(answer.getValue()); - AnswerResult result = q.getQT().clientAnswers(step, answerData, data); - if(result.isNextQuestion()) { - qf.setCurrentQuestion(qindex+1); - } else { - qf.setAnswerStep(result.getNextStep()); - } - answer.setValue(om.writeValueAsString(result.getNewAnswer())); - //XXX Persist QF ? - ObjectNode out = JsonNodeFactory.instance.objectNode(); - out.set("result", JsonNodeFactory.instance.textNode("ok")); - return out; - } catch (JsonProcessingException e) { - e.printStackTrace(); - //TODO autogenerated - return null; + result = q.getQT().clientAnswers(step, answerData, data.get("data")); + } catch (MalformedAnswerException e) { + return errorNode("The previous answer stored in database is invalid"); + } catch (MalformedClientAnswerException e) { + return errorNode("This answer is not valid here"); + } catch (QuestionTypeException e) { + return errorNode("Unknown error from the QuestionType"); + } catch (IllegalArgumentException e) { + return errorNode("The QuestionType did not recognize the step of the question"); } + if(result.isNextQuestion()) { + qf.setCurrentQuestion(qindex+1); + qf.setAnswerStep(0); + if(qf.getCurrentQuestion() >= quizz.getQuestionCount()){ + // The quizz is done + qf.setDone(true); + } + } else { + qf.setAnswerStep(result.getNextStep()); + } + answer.setValue(result.getNewAnswer()); + answerRepository.save(answer); + ObjectNode out = JsonNodeFactory.instance.objectNode(); + out.set("success", JsonNodeFactory.instance.booleanNode(true)); + return out; } @Override public JsonNode next(User user, long quizzId) { - Quizz quizz = qRepository.findById(quizzId).get(); + if(user == null) + return errorNode("You need to be logged in to discover the questions"); + Optional oquizz = qRepository.findById(quizzId); + if(!oquizz.isPresent()) + return errorNode("Could not find quizz with id "+quizzId); + Quizz quizz = oquizz.get(); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz); if(qf == null){ // We should create the quizzform qf = newQuizzForm(user, quizz); } if(qf.isDone()) - throw new UnsupportedOperationException();//TODO add more precise exceptions here + return errorNode("No more questions"); int qindex = qf.getCurrentQuestion(); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex); + if(q == null) + return errorNode("Could not find question "+qindex); int step = qf.getAnswerStep(); Answer answer = answerRepository.findByFormAndQuestion(qf, q); - - ObjectMapper om = new ObjectMapper(); - JsonNode answerData; - try { - answerData = om.readTree(answer.getValue()); - JsonNode qdata = q.getQT().clientQuestionData(step, answerData); - - ObjectNode out = JsonNodeFactory.instance.objectNode(); - out.set("index", JsonNodeFactory.instance.numberNode(qindex)); - out.set("step", JsonNodeFactory.instance.numberNode(step)); - out.set("index", qdata); - return out; - } catch (JsonProcessingException e){ - //XXX autogenerated - return null; + if(answer==null){ + // We construct the blank answer + answer = new Answer(qf,q); + answerRepository.save(answer); } + + JsonNode answerData; + answerData = answer.getValue(); + JsonNode qdata; + try { + qdata = q.getQT().clientQuestionData(step, answerData); + } catch (MalformedAnswerException e) { + return errorNode("The previous answer stored in database is invalid"); + } catch (QuestionTypeException e) { + return errorNode("Unknown error from the QuestionType"); + } catch (IllegalArgumentException e) { + return errorNode("The QuestionType did not recognize the step of the question"); + } + + ObjectNode out = JsonNodeFactory.instance.objectNode(); + out.set("success", JsonNodeFactory.instance.booleanNode(true)); + out.set("index", JsonNodeFactory.instance.numberNode(qindex)); + out.set("step", JsonNodeFactory.instance.numberNode(step)); + out.set("data", qdata); + return out; } public QuizzForm newQuizzForm(User user, Quizz quizz) { @@ -108,8 +157,15 @@ public class QuizzManagerImpl implements QuizzManager { qf.setDone(false); qf.setCurrentQuestion(0); qf.setAnswerStep(0); - //XXX Persist QF ? + qfRepository.save(qf); return qf; } + public static final JsonNode errorNode(String err){ + ObjectNode out = JsonNodeFactory.instance.objectNode(); + out.set("success", JsonNodeFactory.instance.booleanNode(false)); + out.set("message", JsonNodeFactory.instance.textNode(err)); + return out; + } + } diff --git a/src/main/java/com/bernard/misael/service/exception/MalformedAnswerException.java b/src/main/java/com/bernard/misael/service/exception/MalformedAnswerException.java new file mode 100644 index 0000000..416aa30 --- /dev/null +++ b/src/main/java/com/bernard/misael/service/exception/MalformedAnswerException.java @@ -0,0 +1,5 @@ +package com.bernard.misael.service.exception; + +public class MalformedAnswerException extends QuestionTypeException{ + +} diff --git a/src/main/java/com/bernard/misael/service/exception/MalformedClientAnswerException.java b/src/main/java/com/bernard/misael/service/exception/MalformedClientAnswerException.java new file mode 100644 index 0000000..eca967a --- /dev/null +++ b/src/main/java/com/bernard/misael/service/exception/MalformedClientAnswerException.java @@ -0,0 +1,5 @@ +package com.bernard.misael.service.exception; + +public class MalformedClientAnswerException extends QuestionTypeException { + +} diff --git a/src/main/java/com/bernard/misael/service/exception/QuestionTypeException.java b/src/main/java/com/bernard/misael/service/exception/QuestionTypeException.java new file mode 100644 index 0000000..8569986 --- /dev/null +++ b/src/main/java/com/bernard/misael/service/exception/QuestionTypeException.java @@ -0,0 +1,5 @@ +package com.bernard.misael.service.exception; + +public abstract class QuestionTypeException extends Exception{ + +} diff --git a/src/main/java/com/bernard/misael/service/exception/ShouldNotAnswerNowException.java b/src/main/java/com/bernard/misael/service/exception/ShouldNotAnswerNowException.java deleted file mode 100644 index 589569c..0000000 --- a/src/main/java/com/bernard/misael/service/exception/ShouldNotAnswerNowException.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.bernard.misael.service.exception; - -public class ShouldNotAnswerNowException extends Exception{ - -} diff --git a/src/main/java/com/bernard/misael/web/QuestionsController.java b/src/main/java/com/bernard/misael/web/QuestionsController.java index 73607ad..7755e5e 100644 --- a/src/main/java/com/bernard/misael/web/QuestionsController.java +++ b/src/main/java/com/bernard/misael/web/QuestionsController.java @@ -13,6 +13,7 @@ import com.bernard.misael.model.User; import com.bernard.misael.repository.UserRepository; import com.bernard.misael.service.QuizzManager; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -71,17 +72,19 @@ public class QuestionsController { User u = null; if (p!=null) u = ur.findByName(p.getName()); + if(u==null) + return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED); JsonNode out = qm.next(u, quizzId); return new ResponseEntity<>(out, HttpStatus.OK); } @PostMapping("/answer/{q}") - public JsonNode answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) { + public ResponseEntity answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) { User u = null; if (p!=null) u = ur.findByName(p.getName()); JsonNode out = qm.answer(u, quizzId, data); - return out; + return new ResponseEntity<>(out, HttpStatus.OK); } diff --git a/src/main/resources/templates/form.html b/src/main/resources/templates/form.html index ed4b17a..e65adea 100644 --- a/src/main/resources/templates/form.html +++ b/src/main/resources/templates/form.html @@ -6,8 +6,50 @@
+
- Youhou ! (Y devrait y avoir le form ici ^^) + Youhou ! (Y devrait y avoir le form ici ^^)
+ + + +
\ No newline at end of file