diff --git a/backend/.devcontainer/Dockerfile b/backend/.devcontainer/Dockerfile
deleted file mode 100644
index deef895..0000000
--- a/backend/.devcontainer/Dockerfile
+++ /dev/null
@@ -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"]
\ No newline at end of file
diff --git a/backend/.devcontainer/devcontainer.json b/backend/.devcontainer/devcontainer.json
index 3075d71..aead60f 100644
--- a/backend/.devcontainer/devcontainer.json
+++ b/backend/.devcontainer/devcontainer.json
@@ -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"
]
}
}
diff --git a/backend/Dockerfile b/backend/Dockerfile
new file mode 100644
index 0000000..fc70432
--- /dev/null
+++ b/backend/Dockerfile
@@ -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
\ No newline at end of file
diff --git a/backend/pom.xml b/backend/pom.xml
index 80df2a1..47ec3a2 100644
--- a/backend/pom.xml
+++ b/backend/pom.xml
@@ -55,6 +55,17 @@
spring-boot-starter-test
test
+
+ com.github.ben-manes.caffeine
+ caffeine
+ 3.1.8
+
+
+ org.projectlombok
+ lombok
+ 1.18.34
+ provided
+
diff --git a/backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java b/backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java
new file mode 100644
index 0000000..a426b14
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Config/AppConfig.java
@@ -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();
+ }
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java b/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java
new file mode 100644
index 0000000..6ab69c3
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Controllers/QuizController.java
@@ -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 checkStatus(@PathVariable String sessionId) {
+ return quizService.checkSession(sessionId);
+ }
+
+ // @PostMapping("/checkanswers")
+ // public Result checkAnswers(@RequestBody String answers) {
+ // return new Result();
+ // }
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java b/backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java
new file mode 100644
index 0000000..07d7a06
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Exception/GlobalExceptionHandler.java
@@ -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 handleIllegalArgumentException(IllegalArgumentException e) {
+ return ResponseEntity
+ .status(HttpStatus.BAD_REQUEST)
+ .body(e.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java b/backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java
new file mode 100644
index 0000000..0d1da5c
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Models/ApiResponse.java
@@ -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 results;
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Question.java b/backend/src/main/java/com/ninjaws/quiz/Models/Question.java
new file mode 100644
index 0000000..4efb9a9
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Models/Question.java
@@ -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 incorrectAnswers;
+
+ @JsonProperty("answers")
+ public List getAnswers() {
+ List 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 incorrectAnswers) {
+ this.incorrectAnswers = incorrectAnswers;
+ }
+
+ @JsonProperty("correct_answer")
+ @JsonIgnore
+ public String getCorrectAnswer() {
+ return correctAnswer;
+ }
+
+ @JsonProperty("incorrect_answers")
+ @JsonIgnore
+ public List getIncorrectAnswers() {
+ return incorrectAnswers;
+ }
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Request.java b/backend/src/main/java/com/ninjaws/quiz/Models/Request.java
new file mode 100644
index 0000000..15205ef
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Models/Request.java
@@ -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;
+ }
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Models/Session.java b/backend/src/main/java/com/ninjaws/quiz/Models/Session.java
new file mode 100644
index 0000000..f0f8ab5
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Models/Session.java
@@ -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 questions;
+
+ public Session() {
+ this.id = UUID.randomUUID().toString();
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/ninjaws/quiz/QuizApplication.java b/backend/src/main/java/com/ninjaws/quiz/QuizApplication.java
index 722a567..809b4e1 100644
--- a/backend/src/main/java/com/ninjaws/quiz/QuizApplication.java
+++ b/backend/src/main/java/com/ninjaws/quiz/QuizApplication.java
@@ -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) {
diff --git a/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java b/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java
new file mode 100644
index 0000000..189f4b2
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Services/ApiService.java
@@ -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 response = restTemplate.getForEntity(url, String.class);
+ String jsonData = response.getBody();
+ return jsonData;
+ }
+}
diff --git a/backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java b/backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java
new file mode 100644
index 0000000..53924fc
--- /dev/null
+++ b/backend/src/main/java/com/ninjaws/quiz/Services/QuizService.java
@@ -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 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 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 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 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 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() {
+
+ }
+
+}
diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties
index b3b3a2f..8d23e20 100644
--- a/backend/src/main/resources/application.properties
+++ b/backend/src/main/resources/application.properties
@@ -1 +1,2 @@
spring.application.name=quiz
+server.port=9090
\ No newline at end of file
diff --git a/backend/src/main/resources/static/index.html b/backend/src/main/resources/static/index.html
new file mode 100644
index 0000000..4b255a9
--- /dev/null
+++ b/backend/src/main/resources/static/index.html
@@ -0,0 +1 @@
+Quiz backend
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
index 5765b4f..9e0016c 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -4,5 +4,7 @@ services:
context: ./frontend
dockerfile: Dockerfile
target: production
+ image: quiz-frontend-prod
+ container_name: quiz-frontend-prod-cont
ports:
- "4200:80"
\ No newline at end of file