[Added] Most of the frontend, split up backend services

This commit is contained in:
Ninjaws
2024-10-31 23:05:22 +01:00
parent 2505fce920
commit 3188f6ce20
48 changed files with 1391 additions and 398 deletions
@@ -0,0 +1,20 @@
package com.ninjaws.quiz.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // Allow all endpoints
.allowedOrigins("http://localhost:4200") // Allow specific origin
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // Allow specific methods
.allowedHeaders("*") // Allow all headers
.allowCredentials(true); // Allow credentials
}
}
@@ -12,6 +12,7 @@ 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.QuizSettings;
import com.ninjaws.quiz.models.Session;
import com.ninjaws.quiz.services.QuizService;
@@ -24,21 +25,24 @@ public class QuizController {
@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
@RequestParam(required = true) Integer amount,
@RequestParam(required = false) Integer category,
@RequestParam(required = false) String difficulty,
@RequestParam(required = false) String type
) {
return quizService.createSession(quizService.jsonToQuizSettings("todo"));
}
QuizSettings quizSettings = new QuizSettings(amount, category, difficulty, type);
String jsonString = String.format("{\"sessionId\":\"%s\"}", quizService.createSession(quizSettings));
return jsonString;
}
@GetMapping("/status/{sessionId}")
public Optional<Session> checkStatus(@PathVariable String sessionId) {
return quizService.checkSession(sessionId);
}
@PostMapping("/checkanswers")
@PostMapping("/score")
public String checkAnswers(@RequestBody Answers answers) {
return quizService.getScore(answers);
String jsonString = String.format("{\"score\":\"%s\"}", quizService.getScore(answers));
return jsonString;
}
}
@@ -8,9 +8,11 @@ import lombok.Setter;
@Getter
@Setter
public class Answers {
private final String sessionId;
private String sessionId;
private List<String> answers;
public Answers() {}
public Answers(String sessionId) {
this.sessionId = sessionId;
}
@@ -1,5 +1,6 @@
package com.ninjaws.quiz.models;
import com.ninjaws.quiz.models.QuizSettings.Category;
import com.ninjaws.quiz.models.QuizSettings.Difficulty;
import jakarta.validation.Valid;
@@ -54,6 +55,21 @@ public class QuizSettings {
private Category category = Category.ANY;
public QuizSettings(){}
/** Inverse of toApiQuery */
public QuizSettings(int amount, Integer category, String difficulty, String type) {
System.out.println(amount);
System.out.print(category);
System.out.print(difficulty);
System.out.print(type);
this.amount = amount;
this.category = category == null ? Category.ANY : Category.values()[category];
this.difficulty = difficulty == null ? Difficulty.ANY : Difficulty.valueOf(difficulty.toUpperCase());
this.type = type == null ? Type.ANY : type.equals("boolean") ? Type.BOOLEAN : Type.MULTIPLE;
}
/**
* Ensures the value is between 1 and 50
* @Valid doesn't seem to be enforcing it, so this ensures the value is legal
@@ -10,6 +10,7 @@ import lombok.Setter;
@Setter
public class Session {
private final String id;
private int statusCode = 0;
private List<Question> questions;
public Session() {
@@ -12,48 +12,18 @@ import jakarta.validation.Valid;
@Service
public class ApiService {
// @Autowired
// private ObjectMapper objectMapper;
private final RestTemplate restTemplate;
@Autowired
private 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 = "";
// }
public ApiService() {
}
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);
@@ -71,4 +41,8 @@ public class ApiService {
// }
return jsonData;
}
public void handleStatusCode(int statusCode) {
}
}
@@ -0,0 +1,38 @@
package com.ninjaws.quiz.services;
import java.util.concurrent.TimeUnit;
import org.springframework.stereotype.Service;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.ninjaws.quiz.models.Session;
@Service
public class CacheService {
/**
* 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)
*/
private final Cache<String,Session> sessions;
public CacheService() {
sessions = (Caffeine.newBuilder()
.expireAfterWrite(1,TimeUnit.HOURS)
.build());
}
public void addToCache(String key, Session session) {
sessions.put(key, session);
}
public Session getFromCache(String sessionId) {
return sessions.getIfPresent(sessionId);
}
public void removeFromCache(String sessionId) {
sessions.invalidate(sessionId);
}
}
@@ -0,0 +1,16 @@
package com.ninjaws.quiz.services;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.QuizSettings;
@Service
public class CleanerService {
public CleanerService() {}
public QuizSettings jsonToQuizSettings(String json){
QuizSettings quizSettings = new QuizSettings();
return quizSettings;
}
}
@@ -0,0 +1,27 @@
package com.ninjaws.quiz.services;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.Request;
@Service
public class QueueService {
/**
* 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<>();
public void addToQueue(Request request) {
requests.add(request);
}
public Request popFromQueue() {
return requests.poll();
}
}
@@ -1,22 +1,11 @@
package com.ninjaws.quiz.services;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.BlockingQueue;
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.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;
@@ -26,40 +15,9 @@ import com.ninjaws.quiz.models.Session;
public class QuizService {
@Autowired
private ApiService apiService;
private CacheService cacheService;
@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)
*/
// private final Map<String,Session> sessions = new ConcurrentHashMap<>();
private final Cache<String,Session> sessions;
/**
* 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<>();
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;
}
private QueueService queueService;
/**
* Actions:
@@ -71,11 +29,8 @@ public class QuizService {
*/
public String createSession(QuizSettings quizSettings) {
Session session = new Session();
try {
requests.put(new Request(session.getId(), quizSettings));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
cacheService.addToCache(session.getId(), session);
queueService.addToQueue(new Request(session.getId(), quizSettings));
return session.getId();
}
@@ -88,7 +43,7 @@ public class QuizService {
* @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.getIfPresent(sessionId);
Session session = cacheService.getFromCache(sessionId);
if(session == null) {
throw new IllegalArgumentException("Invalid session ID: " + sessionId);
@@ -97,41 +52,9 @@ public class QuizService {
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() {
System.out.print("Schedule!");
Request request = requests.poll();
if (request != null) {
System.out.print("----------------An item!");
String json = apiService.requestQuestions(request.getQuizSettings());
System.out.print(json);
try {
List<Question> questions = extractQuestions(json);
Session session = sessions.getIfPresent(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
*
@@ -142,16 +65,25 @@ public class QuizService {
* - Calculates score
* Returns: calculated score
* */
public String getScore(Answers answers) {
Session session = sessions.getIfPresent(answers.getSessionId());
sessions.invalidate(session.getId());
public int getScore(Answers answers) {
Session session = cacheService.getFromCache(answers.getSessionId());
cacheService.removeFromCache(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!";
}
private int calculateScore(Session session, Answers answers) {
int amountCorrect = 0;
for(int i = 0; i < session.getQuestions().size(); i++) {
String correctAnswer = session.getQuestions().get(i).getCorrectAnswer();
String userAnswer = answers.getAnswers().get(i);
// for(String answer : session.getQuestions().get(i).getAnswers()) {
// System.out.print(answer);
// }
if (correctAnswer.equalsIgnoreCase(userAnswer)) {
amountCorrect++;
}
}
return amountCorrect;
}
}
@@ -0,0 +1,59 @@
package com.ninjaws.quiz.services;
import java.io.IOException;
import java.util.List;
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 SchedulerService {
@Autowired
private QueueService queueService;
@Autowired
private ApiService apiService;
@Autowired
private CacheService cacheService;
@Autowired
private ObjectMapper objectMapper;
private static final System.Logger logger = System.getLogger(SchedulerService.class.getName());
public SchedulerService() {}
/**
* 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 = queueService.popFromQueue();
if (request != null) {
String json = apiService.requestQuestions(request.getQuizSettings());
try {
List<Question> questions = extractQuestions(json);
Session session = cacheService.getFromCache(request.getSessionId());
session.setQuestions(questions);
cacheService.addToCache(request.getSessionId(), session);
} catch (IOException | NullPointerException e) {
logger.log(System.Logger.Level.ERROR, "Critical failure", e);
}
}
}
private List<Question> extractQuestions(String json) throws IOException {
return objectMapper.readValue(json, ApiResponse.class).getResults();
}
}
@@ -0,0 +1,5 @@
package com.ninjaws.quiz.services;
public class StorageService {
}
@@ -1,11 +1,10 @@
package com.ninjaws.quiz.services;
import org.junit.jupiter.api.TestInstance;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
// @EnableScheduling
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
// @TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class QuizServiceTest {
// @InjectMocks