[Added] Basis for Backend

This commit is contained in:
Ninjaws
2024-10-29 21:21:14 +01:00
parent 1a992a871e
commit bf66869ebf
17 changed files with 388 additions and 20 deletions
@@ -0,0 +1,14 @@
package com.ninjaws.quiz.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
@@ -0,0 +1,37 @@
package com.ninjaws.quiz.Controllers;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ninjaws.quiz.Models.Session;
import com.ninjaws.quiz.Services.QuizService;
@RestController
@RequestMapping("/quiz")
public class QuizController {
@Autowired
private QuizService quizService;
@GetMapping("/questions")
public String startSession() {
// String sessionId = generateSessionId();
String sessionId = quizService.createSession();
return sessionId;
}
@GetMapping("/questions/{sessionId}")
public Optional<Session> checkStatus(@PathVariable String sessionId) {
return quizService.checkSession(sessionId);
}
// @PostMapping("/checkanswers")
// public Result checkAnswers(@RequestBody String answers) {
// return new Result();
// }
}
@@ -0,0 +1,20 @@
package com.ninjaws.quiz.Exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* Determines behaviour when certain types of Exceptions are encountered
*/
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException e) {
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(e.getMessage());
}
}
@@ -0,0 +1,21 @@
package com.ninjaws.quiz.Models;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
/**
* The response sent by the external API
*/
@Getter
@Setter
public class ApiResponse {
@JsonProperty("response_code")
private int responseCode;
@JsonProperty("results")
private List<Question> results;
}
@@ -0,0 +1,59 @@
package com.ninjaws.quiz.Models;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
/**
* Holds the question, answers, and correct answer
* TODO: Should not send correct answer to the user
*/
// @Setter()
@Getter()
public class Question {
private String type;
private String difficulty;
private String category;
private String question;
// @JsonIgnore
private String correctAnswer;
// @JsonIgnore
private List<String> incorrectAnswers;
@JsonProperty("answers")
public List<String> getAnswers() {
List<String> possibleAnswers = new ArrayList<>(incorrectAnswers);
possibleAnswers.add(correctAnswer);
Collections.shuffle(possibleAnswers);
return possibleAnswers;
}
@JsonProperty("correct_answer")
public void setCorrectAnswer(String correctAnswer) {
this.correctAnswer = correctAnswer;
}
@JsonProperty("incorrect_answers")
public void setIncorrectAnswer(List<String> incorrectAnswers) {
this.incorrectAnswers = incorrectAnswers;
}
@JsonProperty("correct_answer")
@JsonIgnore
public String getCorrectAnswer() {
return correctAnswer;
}
@JsonProperty("incorrect_answers")
@JsonIgnore
public List<String> getIncorrectAnswers() {
return incorrectAnswers;
}
}
@@ -0,0 +1,14 @@
package com.ninjaws.quiz.Models;
import lombok.Getter;
@Getter
public class Request {
private final String sessionId;
private final String requestUrl;
public Request(String sessionId, String requestUrl) {
this.sessionId = sessionId;
this.requestUrl = requestUrl;
}
}
@@ -0,0 +1,18 @@
package com.ninjaws.quiz.Models;
import java.util.List;
import java.util.UUID;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Session {
private final String id;
private List<Question> questions;
public Session() {
this.id = UUID.randomUUID().toString();
}
}
@@ -2,8 +2,10 @@ package com.ninjaws.quiz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class QuizApplication {
public static void main(String[] args) {
@@ -0,0 +1,23 @@
package com.ninjaws.quiz.Services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class ApiService {
private final RestTemplate restTemplate;
@Autowired
public ApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
public String requestQuestions(String url) {
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String jsonData = response.getBody();
return jsonData;
}
}
@@ -0,0 +1,126 @@
package com.ninjaws.quiz.Services;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ninjaws.quiz.Models.ApiResponse;
import com.ninjaws.quiz.Models.Question;
import com.ninjaws.quiz.Models.Request;
import com.ninjaws.quiz.Models.Session;
@Service
public class QuizService {
@Autowired
private ApiService apiService;
@Autowired
private ObjectMapper objectMapper;
/**
* Stores all active sessions
* Gets added to when a user requests data
* Gets removed from when a user requests their score (by sending their answers)
* TODO: Replace with something that automatically removes items after a certain amount of time (Caffein?)
*/
private final Map<String,Session> sessions = new ConcurrentHashMap<>();
/**
* Stores all user requests for data
* Executes them in order, one every 5 seconds (the limit of the external API)
* Removes them from the Queue when the request was succesful
*/
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
/**
* Actions:
* - Creates a session with sessionId
* - Adds session to the cache
* - Creates request and adds it to the queue
* @return The sessionId
*/
public String createSession() {
Session session = new Session();
sessions.put(session.getId(), session); // TODO: Either remove id from session, or maybe Caffein does allow them together
try {
requests.put(new Request(session.getId(), "https://opentdb.com/api.php?amount=10"));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return session.getId();
}
/**
* @param sessionId
* Actions:
* - Looks up the session in the cache
* - Checks if the data from the request is stored
* @return The data if it's available, otherwise a statuscode (?)
*/
public Optional<Session> checkSession(String sessionId) throws IllegalArgumentException {
Session session = sessions.get(sessionId);
if(session == null) {
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
}
if (session.getQuestions() == null) {
return Optional.empty();
}
System.out.print(session.getQuestions().get(0).getIncorrectAnswers().get(0));
return Optional.of(session);
}
/**
* Every 5 seconds:
* - Take the top request from the queue (removes it)
* - Perform the request
* - Wait for the data
* - Find the matching session from the cache
* - Add the data to the session in the cache
*/
@Scheduled(fixedDelay = 5000)
protected void processQueue() {
Request request = requests.poll();
if (request != null) {
String json = apiService.requestQuestions(request.getRequestUrl());
try {
List<Question> questions = extractQuestions(json);
Session session = sessions.get(request.getSessionId());
session.setQuestions(questions);
sessions.put(request.getSessionId(), session);
} catch (IOException e) {
//TODO: Log this
}
}
}
private List<Question> extractQuestions(String json) throws IOException {
return objectMapper.readValue(json, ApiResponse.class).getResults();
}
/**
* Receives: sessionId and answers
* Actions:
* - retrieves session from cache
* - Deletes session from cache
* - Matches answers answers to correctAnswers
* - Calculates score
* Returns: calculated score
* */
public void calculateScore() {
}
}