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.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'

View File

@ -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());
}
}

View File

@ -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<String> answers = new ArrayList<>(k);
answers.add(goodAnswer);
for(int i = 0;i<k;i++)
for(int i = 0;i<k-1;i++)
answers.add(wrongAnswers[i]);
Collections.shuffle(answers, r);
o.putArray("answers").addAll(answers.stream().map(JsonNodeFactory.instance::textNode).collect(Collectors.toList()));
return o;
default:
throw new IllegalStateException("Invalid Answer type");
throw new MalformedAnswerException();
}
default:
@ -57,16 +61,18 @@ public class DccQuestion implements QuestionType{
}
@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();
switch (step) {
case 0:
if(!clientAnswer.isInt()) throw new MalformedClientAnswerException();
int answerType = clientAnswer.asInt();
if(answerType != 0 && answerType != 2 && answerType != 4)
throw new IllegalArgumentException("Illegal answer type: "+answerType);
o.put("type", answerType);
return new AnswerResult(o, false, 1);
case 1:
if(!clientAnswer.isTextual()) throw new MalformedClientAnswerException();
String answer = clientAnswer.asText();
o = oldAnswer.deepCopy();
o.put("answer", answer);

View File

@ -1,5 +1,6 @@
package com.bernard.misael.questions;
import com.bernard.misael.service.exception.QuestionTypeException;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.AllArgsConstructor;
@ -7,9 +8,9 @@ import lombok.Getter;
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
@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;
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<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);
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<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);
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;
}
}

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.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<JsonNode> 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);
}

View File

@ -6,8 +6,50 @@
<body>
<div th:replace="~{header}"/>
<script th:src="@{/webjars/jquery/1.9.1/jquery.min.js}"></script>
<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>
</body>
</html>