[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
-15
View File
@@ -1,15 +0,0 @@
# Use an official OpenJDK as a parent image
FROM openjdk:17-jdk-slim
# Set the working directory
WORKDIR /app
# Install Maven
RUN apt-get update && apt-get install -y \
maven=3.3.9-4 \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
RUN mvn clean package
CMD ["tail","-f","/dev/null"]
+13 -5
View File
@@ -2,13 +2,19 @@
"name": "Quiz Backend",
"build": {
"context": ".",
"dockerfile": "Dockerfile",
"context": "..",
"dockerfile": "../Dockerfile",
"args": {
"WORKDIR": "/app"
}
"WORKDIR": "/app",
// "UID": "${localEnv:USER_UID}",
// "GID": "${localEnv:USER_GID}"
},
"runArgs": ["--name=quiz-backend-dev-cont"],
"target": "development"
},
"runArgs": [
"--name=quiz-backend-dev-cont",
"--net=host" // Allows it to be opened on the host network
],
"workspaceFolder": "/app",
"mounts": [
"source=.,target=/app,type=bind"
@@ -17,6 +23,8 @@
"vscode": {
"settings": {},
"extensions": [
"vscjava.vscode-java-pack",
"Oracle.oracle-java"
]
}
}
+26
View File
@@ -0,0 +1,26 @@
FROM openjdk:17-jdk-slim AS base
WORKDIR /app
RUN apt-get update && apt install -y \
maven \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
#---Development
FROM base AS development
# Switching user so it doesn't use root
# If root adds files, it takes ownership, making them unremovable on the host except by using sudo
# node tranfers ownership to the host user
ENV UID=1000
ENV GID=1000
ENV USER=dev
RUN groupadd -g ${GID} ${USER} \
&& useradd -m -u ${UID} -g ${USER} ${USER}
USER ${USER}
EXPOSE 8080
CMD ["tail","-f","/dev/null"]
#---Building
#---Production
+11
View File
@@ -55,6 +55,17 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
@@ -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() {
}
}
@@ -1 +1,2 @@
spring.application.name=quiz
server.port=9090
@@ -0,0 +1 @@
<h1>Quiz backend</h1>
+2
View File
@@ -4,5 +4,7 @@ services:
context: ./frontend
dockerfile: Dockerfile
target: production
image: quiz-frontend-prod
container_name: quiz-frontend-prod-cont
ports:
- "4200:80"