[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",
|
||||
|
||||
"build": {
|
||||
"context": ".",
|
||||
"dockerfile": "Dockerfile",
|
||||
"context": "..",
|
||||
"dockerfile": "../Dockerfile",
|
||||
"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",
|
||||
"mounts": [
|
||||
"source=.,target=/app,type=bind"
|
||||
@@ -17,6 +23,8 @@
|
||||
"vscode": {
|
||||
"settings": {},
|
||||
"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>
|
||||
<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>
|
||||
@@ -4,5 +4,7 @@ services:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
target: production
|
||||
image: quiz-frontend-prod
|
||||
container_name: quiz-frontend-prod-cont
|
||||
ports:
|
||||
- "4200:80"
|
||||
Reference in New Issue
Block a user