Added better error messages, and tested json API

This commit is contained in:
Samy Avrillon 2025-02-27 18:08:03 +01:00
parent be01c925a3
commit 3a7a413c69
Signed by: Mysaa
GPG Key ID: 0220AC4A3D6A328B
12 changed files with 226 additions and 58 deletions

View File

@ -40,12 +40,16 @@ dependencies {
implementation 'org.flywaydb:flyway-database-postgresql:11.3.2' implementation 'org.flywaydb:flyway-database-postgresql:11.3.2'
implementation 'org.springframework.session:spring-session-jdbc' implementation 'org.springframework.session:spring-session-jdbc'
implementation 'jakarta.validation:jakarta.validation-api:3.1.1' 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-devtools'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose' developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'org.postgresql:postgresql' runtimeOnly 'org.postgresql:postgresql'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher' 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 //Lombok
compileOnly 'org.projectlombok:lombok:1.18.36' compileOnly 'org.projectlombok:lombok:1.18.36'
annotationProcessor 'org.projectlombok:lombok:1.18.36' annotationProcessor 'org.projectlombok:lombok:1.18.36'

View File

@ -1,6 +1,12 @@
package com.bernard.misael.model; 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.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Converter;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
@ -35,6 +41,14 @@ public class Answer {
@JoinColumn(name = "form",nullable = false) @JoinColumn(name = "form",nullable = false)
private QuizzForm form; 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());
}
} }

View File

@ -6,6 +6,9 @@ import java.util.List;
import java.util.Random; import java.util.Random;
import java.util.stream.Collectors; 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.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@ -26,7 +29,7 @@ public class DccQuestion implements QuestionType{
} }
@Override @Override
public JsonNode clientQuestionData(int step, JsonNode answer) { public JsonNode clientQuestionData(int step, JsonNode answer) throws QuestionTypeException{
ObjectNode o = JsonNodeFactory.instance.objectNode(); ObjectNode o = JsonNodeFactory.instance.objectNode();
switch (step) { switch (step) {
case 0: case 0:
@ -34,6 +37,7 @@ public class DccQuestion implements QuestionType{
return o; return o;
case 1: case 1:
Random r = new Random(); Random r = new Random();
if(!answer.has("type") || !answer.get("type").isInt()) throw new MalformedAnswerException();
int k = answer.get("type").asInt(); int k = answer.get("type").asInt();
switch(k) { switch(k) {
case 0: case 0:
@ -42,13 +46,13 @@ public class DccQuestion implements QuestionType{
case 4: case 4:
List<String> answers = new ArrayList<>(k); List<String> answers = new ArrayList<>(k);
answers.add(goodAnswer); answers.add(goodAnswer);
for(int i = 0;i<k;i++) for(int i = 0;i<k-1;i++)
answers.add(wrongAnswers[i]); answers.add(wrongAnswers[i]);
Collections.shuffle(answers, r); Collections.shuffle(answers, r);
o.putArray("answers").addAll(answers.stream().map(JsonNodeFactory.instance::textNode).collect(Collectors.toList())); o.putArray("answers").addAll(answers.stream().map(JsonNodeFactory.instance::textNode).collect(Collectors.toList()));
return o; return o;
default: default:
throw new IllegalStateException("Invalid Answer type"); throw new MalformedAnswerException();
} }
default: default:
@ -57,16 +61,18 @@ public class DccQuestion implements QuestionType{
} }
@Override @Override
public AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer) { public AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer) throws QuestionTypeException {
ObjectNode o = JsonNodeFactory.instance.objectNode(); ObjectNode o = JsonNodeFactory.instance.objectNode();
switch (step) { switch (step) {
case 0: case 0:
if(!clientAnswer.isInt()) throw new MalformedClientAnswerException();
int answerType = clientAnswer.asInt(); int answerType = clientAnswer.asInt();
if(answerType != 0 && answerType != 2 && answerType != 4) if(answerType != 0 && answerType != 2 && answerType != 4)
throw new IllegalArgumentException("Illegal answer type: "+answerType); throw new IllegalArgumentException("Illegal answer type: "+answerType);
o.put("type", answerType); o.put("type", answerType);
return new AnswerResult(o, false, 1); return new AnswerResult(o, false, 1);
case 1: case 1:
if(!clientAnswer.isTextual()) throw new MalformedClientAnswerException();
String answer = clientAnswer.asText(); String answer = clientAnswer.asText();
o = oldAnswer.deepCopy(); o = oldAnswer.deepCopy();
o.put("answer", answer); o.put("answer", answer);

View File

@ -1,5 +1,6 @@
package com.bernard.misael.questions; package com.bernard.misael.questions;
import com.bernard.misael.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
@ -7,9 +8,9 @@ import lombok.Getter;
public interface QuestionType { public interface QuestionType {
JsonNode clientQuestionData(int step, JsonNode answer); JsonNode clientQuestionData(int step, JsonNode answer) throws QuestionTypeException;
AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer); AnswerResult clientAnswers(int step, JsonNode oldAnswer, JsonNode clientAnswer) throws QuestionTypeException;
@AllArgsConstructor @AllArgsConstructor
@Getter @Getter

View File

@ -0,0 +1,32 @@
package com.bernard.misael.service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.AttributeConverter;
public class JsonNodeConverter implements AttributeConverter<JsonNode, String> {
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);
}
}
}

View File

@ -1,5 +1,7 @@
package com.bernard.misael.service; package com.bernard.misael.service;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; 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.QuestionRepository;
import com.bernard.misael.repository.QuizzFormRepository; import com.bernard.misael.repository.QuizzFormRepository;
import com.bernard.misael.repository.QuizzRepository; 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.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.JsonNodeFactory; import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.ObjectNode;
@ -36,69 +39,115 @@ public class QuizzManagerImpl implements QuizzManager {
@Override @Override
public JsonNode answer(User user, long quizzId, JsonNode data) { public JsonNode answer(User user, long quizzId, JsonNode data) {
//TODO replace conversions String <-> JsonNode to Answer type if(!data.has("index") || !data.get("index").isInt())
Quizz quizz = qRepository.findById(quizzId).get(); 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<Quizz> 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); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null)
return errorNode("The quizzform does not exist, ask the question first");
if(qf.isDone()) 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(); 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); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null)
return errorNode("Could not find question "+qindex);
int step = qf.getAnswerStep(); int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q); 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+")");
ObjectMapper om = new ObjectMapper(); Answer answer = answerRepository.findByFormAndQuestion(qf, q);
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 { try {
answerData = om.readTree(answer.getValue()); result = q.getQT().clientAnswers(step, answerData, data.get("data"));
AnswerResult result = q.getQT().clientAnswers(step, answerData, data); } catch (MalformedAnswerException e) {
if(result.isNextQuestion()) { return errorNode("The previous answer stored in database is invalid");
qf.setCurrentQuestion(qindex+1); } catch (MalformedClientAnswerException e) {
} else { return errorNode("This answer is not valid here");
qf.setAnswerStep(result.getNextStep()); } catch (QuestionTypeException e) {
} return errorNode("Unknown error from the QuestionType");
answer.setValue(om.writeValueAsString(result.getNewAnswer())); } catch (IllegalArgumentException e) {
//XXX Persist QF ? return errorNode("The QuestionType did not recognize the step of the question");
ObjectNode out = JsonNodeFactory.instance.objectNode();
out.set("result", JsonNodeFactory.instance.textNode("ok"));
return out;
} catch (JsonProcessingException e) {
e.printStackTrace();
//TODO autogenerated
return null;
} }
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 @Override
public JsonNode next(User user, long quizzId) { 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<Quizz> 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); QuizzForm qf = qfRepository.findByUserAndQuizz(user, quizz);
if(qf == null){ if(qf == null){
// We should create the quizzform // We should create the quizzform
qf = newQuizzForm(user, quizz); qf = newQuizzForm(user, quizz);
} }
if(qf.isDone()) if(qf.isDone())
throw new UnsupportedOperationException();//TODO add more precise exceptions here return errorNode("No more questions");
int qindex = qf.getCurrentQuestion(); int qindex = qf.getCurrentQuestion();
Question q = questionRepository.findByQuizzAndIndex(quizz,qindex); Question q = questionRepository.findByQuizzAndIndex(quizz,qindex);
if(q == null)
return errorNode("Could not find question "+qindex);
int step = qf.getAnswerStep(); int step = qf.getAnswerStep();
Answer answer = answerRepository.findByFormAndQuestion(qf, q); Answer answer = answerRepository.findByFormAndQuestion(qf, q);
if(answer==null){
ObjectMapper om = new ObjectMapper(); // We construct the blank answer
JsonNode answerData; answer = new Answer(qf,q);
try { answerRepository.save(answer);
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;
} }
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) { public QuizzForm newQuizzForm(User user, Quizz quizz) {
@ -108,8 +157,15 @@ public class QuizzManagerImpl implements QuizzManager {
qf.setDone(false); qf.setDone(false);
qf.setCurrentQuestion(0); qf.setCurrentQuestion(0);
qf.setAnswerStep(0); qf.setAnswerStep(0);
//XXX Persist QF ? qfRepository.save(qf);
return 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;
}
} }

View File

@ -0,0 +1,5 @@
package com.bernard.misael.service.exception;
public class MalformedAnswerException extends QuestionTypeException{
}

View File

@ -0,0 +1,5 @@
package com.bernard.misael.service.exception;
public class MalformedClientAnswerException extends QuestionTypeException {
}

View File

@ -0,0 +1,5 @@
package com.bernard.misael.service.exception;
public abstract class QuestionTypeException extends Exception{
}

View File

@ -1,5 +0,0 @@
package com.bernard.misael.service.exception;
public class ShouldNotAnswerNowException extends Exception{
}

View File

@ -13,6 +13,7 @@ import com.bernard.misael.model.User;
import com.bernard.misael.repository.UserRepository; import com.bernard.misael.repository.UserRepository;
import com.bernard.misael.service.QuizzManager; import com.bernard.misael.service.QuizzManager;
import com.fasterxml.jackson.databind.JsonNode; 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.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
@ -71,17 +72,19 @@ public class QuestionsController {
User u = null; User u = null;
if (p!=null) if (p!=null)
u = ur.findByName(p.getName()); u = ur.findByName(p.getName());
if(u==null)
return new ResponseEntity<>(JsonNodeFactory.instance.objectNode(),HttpStatus.UNAUTHORIZED);
JsonNode out = qm.next(u, quizzId); JsonNode out = qm.next(u, quizzId);
return new ResponseEntity<>(out, HttpStatus.OK); return new ResponseEntity<>(out, HttpStatus.OK);
} }
@PostMapping("/answer/{q}") @PostMapping("/answer/{q}")
public JsonNode answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) { public ResponseEntity<JsonNode> answer(@PathVariable("q") long quizzId, @RequestBody JsonNode data, Principal p) {
User u = null; User u = null;
if (p!=null) if (p!=null)
u = ur.findByName(p.getName()); u = ur.findByName(p.getName());
JsonNode out = qm.answer(u, quizzId, data); JsonNode out = qm.answer(u, quizzId, data);
return out; return new ResponseEntity<>(out, HttpStatus.OK);
} }

View File

@ -6,8 +6,50 @@
<body> <body>
<div th:replace="~{header}"/> <div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}"></script>
<main> <main>
Youhou ! (Y devrait y avoir le form ici ^^) <span th:text="${formid}"/> Youhou ! (Y devrait y avoir le form ici ^^) <span th:text="${formid}"/> <br/>
<button id="question-button">Poser la question</button>
<button id="answer1-button">Première réponse</button>
<script>
e = new RegExp("/([0-9]+)$")
e.test(window.location.href)
qid = Number(RegExp.lastMatch.substring(1))
qindex = 0
qstep = 0
function next() {
$.ajax({
url: "/questions/question/"+qid,
type: "GET",
dataType: "json",
success: function(res) {
console.log(res)
qindex=res["index"]
qstep=res["step"]
}
})
}
function answer() {
asw={
index: qindex,
step: qstep,
data: Math.floor(Math.random() * 3)*2
}
$.ajax({
contentType: 'application/json',
type: "POST",
url: "/questions/answer/"+qid,
data: JSON.stringify(asw),
dataType: "json",
success: function(res) {
console.log(res)
}
})
}
$("#question-button").on('click',next)
$("#answer1-button").on('click',answer)
</script>
</main> </main>
</body> </body>
</html> </html>