[Added] Tests and some more functionality to the Services

This commit is contained in:
Ninjaws
2024-10-30 21:47:45 +01:00
parent bf66869ebf
commit 2505fce920
21 changed files with 535 additions and 121 deletions
@@ -1,37 +0,0 @@
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();
// }
}
@@ -1,14 +0,0 @@
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;
}
}
@@ -1,23 +0,0 @@
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;
}
}
@@ -1,4 +1,4 @@
package com.ninjaws.quiz.Config;
package com.ninjaws.quiz.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -0,0 +1,44 @@
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.ninjaws.quiz.models.Answers;
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(
@RequestParam(required = true) String amount,
@RequestParam(required = false) Integer category,
@RequestParam(required = false) String difficulty,
@RequestParam(required = false) String type
) {
return quizService.createSession(quizService.jsonToQuizSettings("todo"));
}
@GetMapping("/status/{sessionId}")
public Optional<Session> checkStatus(@PathVariable String sessionId) {
return quizService.checkSession(sessionId);
}
@PostMapping("/checkanswers")
public String checkAnswers(@RequestBody Answers answers) {
return quizService.getScore(answers);
}
}
@@ -1,4 +1,4 @@
package com.ninjaws.quiz.Exception;
package com.ninjaws.quiz.exception;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -0,0 +1,17 @@
package com.ninjaws.quiz.models;
import java.util.List;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class Answers {
private final String sessionId;
private List<String> answers;
public Answers(String sessionId) {
this.sessionId = sessionId;
}
}
@@ -1,4 +1,4 @@
package com.ninjaws.quiz.Models;
package com.ninjaws.quiz.models;
import java.util.List;
@@ -1,4 +1,4 @@
package com.ninjaws.quiz.Models;
package com.ninjaws.quiz.models;
import java.util.ArrayList;
import java.util.Collections;
@@ -9,22 +9,16 @@ 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()
@Getter
public class Question {
private String type;
private String difficulty;
private String category;
private String question;
// @JsonIgnore
private String correctAnswer;
// @JsonIgnore
//* Hidden from the user */
private String correctAnswer;
private List<String> incorrectAnswers;
@JsonProperty("answers")
@@ -0,0 +1,86 @@
package com.ninjaws.quiz.models;
import com.ninjaws.quiz.models.QuizSettings.Difficulty;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QuizSettings {
public enum Type {
ANY, BOOLEAN, MULTIPLE;
public String getApiValue() {
return this.name().toLowerCase();
}
}
public enum Difficulty {
ANY, EASY, MEDIUM, HARD;
public String getApiValue() {
return this.name().toLowerCase();
}
}
public enum Category {
ANY, GEN_KNOWLEDGE, ENT_BOOKS, ENT_FILM, ENT_MUSIC, ENT_MUSICAL_THEATRE, ENT_TELEVISION,
ENT_VIDEOGAMES, ENT_BOARDGAMES, SCIENCE_NATURE, SCIENCE_COMPUTERS, SCIENCE_MATH, MYTHOLOGY, SPORTS, GEOGRAPHY, HISTORY, POLITICS, ART, CELEBRITIES,
ANIMALS, VEHICLES, ENT_COMICS, SCIENCE_GADGETS, ENT_ANIME_MANGA, ENT_CARTOON_ANIMATIONS;
/**
* For some reason the external API starts with a value of 9 for the General Knowledge category, so we have to adjust
* ANY also gets adjusted, but it doesn't matter since that one doesn't get used as parameter anyway
* @return The corrected value
*/
public int getApiValue() {
return 8 + this.ordinal();
}
}
@Min(1)
@Max(50)
private int amount = 10;
private Type type = Type.ANY;
private Difficulty difficulty = Difficulty.ANY;
private Category category = Category.ANY;
/**
* Ensures the value is between 1 and 50
* @Valid doesn't seem to be enforcing it, so this ensures the value is legal
*/
public void setAmount(@Valid int amount) {
this.amount = Math.max(1, Math.min(amount, 50));
}
/**
* Converts the class to a format that can be provided as arguments for the api request
* @return The resulting request arguments as String
*/
public String toApiQuery() {
StringBuilder query = new StringBuilder("?amount=" + amount);
if (category != null && category != Category.ANY) {
query.append("&category=").append(category.getApiValue());
}
if (difficulty != null && difficulty != Difficulty.ANY) {
query.append("&difficulty=").append(difficulty.getApiValue());
}
if (type != null && type != Type.ANY) {
query.append("&type=").append(type.getApiValue());
}
return query.toString();
}
}
@@ -0,0 +1,14 @@
package com.ninjaws.quiz.models;
import lombok.Getter;
@Getter
public class Request {
private final String sessionId;
private final QuizSettings quizSettings;
public Request(String sessionId, QuizSettings quizSettings) {
this.sessionId = sessionId;
this.quizSettings = quizSettings;
}
}
@@ -1,4 +1,4 @@
package com.ninjaws.quiz.Models;
package com.ninjaws.quiz.models;
import java.util.List;
import java.util.UUID;
@@ -0,0 +1,74 @@
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;
import com.ninjaws.quiz.models.QuizSettings;
import jakarta.validation.Valid;
@Service
public class ApiService {
// @Autowired
// private ObjectMapper objectMapper;
private final RestTemplate restTemplate;
private final String apiUrl = "https://opentdb.com/api.php";
// private String sessionToken = "";
@Autowired
public ApiService(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
// try {
// sessionToken = requestSessionToken();
// } catch (Exception e) {
// // Failed to get a session token, setting default to none
// sessionToken = "";
// }
}
private String buildRequestString(QuizSettings quizSettings) {
return apiUrl + quizSettings.toApiQuery();
}
// private int getResponseCode(String json) throws Exception {
// JsonNode jsonNode = objectMapper.readTree(json);
// return jsonNode.get("response_code").asInt();
// }
// public String requestSessionToken() throws Exception {
// String sessionTokenRequestUrl = "https://opentdb.com/api_token.php?command=request";
// ResponseEntity<String> response = restTemplate.getForEntity(sessionTokenRequestUrl, String.class);
// JsonNode jsonNode = objectMapper.readTree(response.getBody());
// if (jsonNode.get("response_code").asInt() == 0) {
// sessionToken = jsonNode.get("token").asText();
// }else {
// // Failed to get a session token, setting default to none
// sessionToken = "";
// }
// return sessionToken;
// }
public String requestQuestions(@Valid QuizSettings quizSettings) {
String url = buildRequestString(quizSettings);
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String jsonData = response.getBody();
// try {
// int responseCode = getResponseCode(jsonData);
// /** When the code has expired or you've ran out of questions, update the token and try again */
// if(responseCode == 3 || responseCode == 4) {
// requestSessionToken();
// requestQuestions(quizSettings);
// }
// } catch (Exception e) {
// // Failed to get a responseCode, meaning the request failed completely (they always send a response_code)
// }
return jsonData;
}
}
@@ -1,22 +1,25 @@
package com.ninjaws.quiz.Services;
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 java.util.concurrent.TimeUnit;
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;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ninjaws.quiz.models.Answers;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.Question;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Request;
import com.ninjaws.quiz.models.Session;
@Service
@@ -31,9 +34,9 @@ public class QuizService {
* 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<>();
// private final Map<String,Session> sessions = new ConcurrentHashMap<>();
private final Cache<String,Session> sessions;
/**
* Stores all user requests for data
@@ -42,18 +45,34 @@ public class QuizService {
*/
private final BlockingQueue<Request> requests = new LinkedBlockingQueue<>();
public QuizService() {
sessions = (Caffeine.newBuilder()
.expireAfterWrite(1,TimeUnit.HOURS)
.build());
}
public QuizService(Cache<String, Session> cache) {
sessions = cache;
}
public QuizSettings jsonToQuizSettings(String json){
QuizSettings quizSettings = new QuizSettings();
return quizSettings;
}
/**
* Actions:
* - Creates a session with sessionId
* - Adds session to the cache
* - Creates request and adds it to the queue
* - Creates a request and adds it to the queue
* @param quizSettings The values set by the user, determining the kind of quiz they want
* @return The sessionId
*/
public String createSession() {
public String createSession(QuizSettings quizSettings) {
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"));
requests.put(new Request(session.getId(), quizSettings));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
@@ -62,14 +81,14 @@ public class QuizService {
}
/**
* @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 (?)
* @param sessionId
* @return The data if it's available, Optional.empty() if not, and an Exception if the session is invalid
*/
public Optional<Session> checkSession(String sessionId) throws IllegalArgumentException {
Session session = sessions.get(sessionId);
Session session = sessions.getIfPresent(sessionId);
if(session == null) {
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
@@ -92,12 +111,15 @@ public class QuizService {
*/
@Scheduled(fixedDelay = 5000)
protected void processQueue() {
System.out.print("Schedule!");
Request request = requests.poll();
if (request != null) {
String json = apiService.requestQuestions(request.getRequestUrl());
System.out.print("----------------An item!");
String json = apiService.requestQuestions(request.getQuizSettings());
System.out.print(json);
try {
List<Question> questions = extractQuestions(json);
Session session = sessions.get(request.getSessionId());
Session session = sessions.getIfPresent(request.getSessionId());
session.setQuestions(questions);
sessions.put(request.getSessionId(), session);
} catch (IOException e) {
@@ -112,6 +134,7 @@ public class QuizService {
/**
* Receives: sessionId and answers
*
* Actions:
* - retrieves session from cache
* - Deletes session from cache
@@ -119,8 +142,16 @@ public class QuizService {
* - Calculates score
* Returns: calculated score
* */
public void calculateScore() {
public String getScore(Answers answers) {
Session session = sessions.getIfPresent(answers.getSessionId());
sessions.invalidate(session.getId());
return calculateScore(session, answers);
}
private String calculateScore(Session session, Answers answers) {
System.out.print(session);
System.out.print(answers);
return "You did great!";
}
}