[Added] Basis for Backend
This commit is contained in:
@@ -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"]
|
|
||||||
@@ -2,13 +2,19 @@
|
|||||||
"name": "Quiz Backend",
|
"name": "Quiz Backend",
|
||||||
|
|
||||||
"build": {
|
"build": {
|
||||||
"context": ".",
|
"context": "..",
|
||||||
"dockerfile": "Dockerfile",
|
"dockerfile": "../Dockerfile",
|
||||||
"args": {
|
"args": {
|
||||||
"WORKDIR": "/app"
|
"WORKDIR": "/app",
|
||||||
}
|
// "UID": "${localEnv:USER_UID}",
|
||||||
|
// "GID": "${localEnv:USER_GID}"
|
||||||
|
},
|
||||||
|
"target": "development"
|
||||||
},
|
},
|
||||||
"runArgs": ["--name=quiz-backend-dev-cont"],
|
"runArgs": [
|
||||||
|
"--name=quiz-backend-dev-cont",
|
||||||
|
"--net=host" // Allows it to be opened on the host network
|
||||||
|
],
|
||||||
"workspaceFolder": "/app",
|
"workspaceFolder": "/app",
|
||||||
"mounts": [
|
"mounts": [
|
||||||
"source=.,target=/app,type=bind"
|
"source=.,target=/app,type=bind"
|
||||||
@@ -17,6 +23,8 @@
|
|||||||
"vscode": {
|
"vscode": {
|
||||||
"settings": {},
|
"settings": {},
|
||||||
"extensions": [
|
"extensions": [
|
||||||
|
"vscjava.vscode-java-pack",
|
||||||
|
"Oracle.oracle-java"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -55,6 +55,17 @@
|
|||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</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>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<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.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
|
@EnableScheduling
|
||||||
public class QuizApplication {
|
public class QuizApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
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
|
spring.application.name=quiz
|
||||||
|
server.port=9090
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<h1>Quiz backend</h1>
|
||||||
@@ -4,5 +4,7 @@ services:
|
|||||||
context: ./frontend
|
context: ./frontend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
target: production
|
target: production
|
||||||
|
image: quiz-frontend-prod
|
||||||
|
container_name: quiz-frontend-prod-cont
|
||||||
ports:
|
ports:
|
||||||
- "4200:80"
|
- "4200:80"
|
||||||
Reference in New Issue
Block a user