[Added] Functionality for getting data from the database [NOTE: swapping seems to difficult for now, might have to go for a cache approach]

This commit is contained in:
Ninjaws
2024-11-02 20:02:36 +01:00
parent ddc421b4b3
commit 121dff910d
30 changed files with 791 additions and 302 deletions
@@ -0,0 +1,27 @@
package com.ninjaws.quiz.components;
import org.springframework.boot.CommandLineRunner;
import org.springframework.jdbc.core.JdbcTemplate;
/**
* Sets up the database, and adds the categories
*
* Fills up the primary database
*
* Then, at night, creates a new database, adds categories, and then starts filling it up
* When full, swaps the databases, and wipes the old one
*/
public class StartupRunner implements CommandLineRunner {
private final JdbcTemplate primaryJdbcTemplate;
public StartupRunner(JdbcTemplate primaryJdbcTemplate) {
this.primaryJdbcTemplate = primaryJdbcTemplate;
}
@Override
public void run(String... args) {
// Initialization logic here
primaryJdbcTemplate.execute("CREATE TABLE IF NOT EXISTS test_table (id INT PRIMARY KEY, name VARCHAR(255))");
primaryJdbcTemplate.update("INSERT INTO test_table (id, name) VALUES (?, ?)", 1, "Sample Data");
}
}
@@ -0,0 +1,46 @@
package com.ninjaws.quiz.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.core.JdbcTemplate;
public class DataSourceConfig {
@Primary
@Bean(name = "primaryDataSourceProperties")
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Primary
@Bean(name = "primaryDataSource")
public DataSource primaryDataSource() {
return primaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean(name = "secondaryDataSourceProperties")
@ConfigurationProperties("spring.datasource.secondary")
public DataSourceProperties secondaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean(name = "secondaryDataSource")
public DataSource secondaryDataSource() {
return secondaryDataSourceProperties().initializeDataSourceBuilder().build();
}
@Bean
public JdbcTemplate primaryJdbcTemplate(@Qualifier("primaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
@Bean
public JdbcTemplate secondaryJdbcTemplate(@Qualifier("secondaryDataSource") DataSource ds) {
return new JdbcTemplate(ds);
}
}
@@ -0,0 +1,25 @@
package com.ninjaws.quiz.entities;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "category")
@Getter
@Setter
public class Category {
@Id
private Integer id;
private String name;
public Category() {}
public Category(int id, String name) {
this.id = id;
this.name = name;
}
}
@@ -0,0 +1,36 @@
package com.ninjaws.quiz.entities;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "question")
@Getter
@Setter
public class QuestionEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "category", nullable = false)
private Category category;
private String difficulty;
private String type;
private String question;
//* Hidden from the user */
private String correctAnswer;
private List<String> incorrectAnswers;
}
@@ -17,5 +17,5 @@ public class ApiResponse {
@JsonProperty("response_code")
private int responseCode;
@JsonProperty("results")
private List<Question> results;
private List<QuestionDTO> results;
}
@@ -0,0 +1,14 @@
package com.ninjaws.quiz.models;
import java.util.List;
import com.ninjaws.quiz.entities.Category;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CategoryResponse {
List<Category> trivia_categories;
}
@@ -7,25 +7,17 @@ import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@Entity
@Table(name = "question")
@Getter
@Setter
public class Question {
public class QuestionDTO {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
private String difficulty;
private String category;
private String difficulty;
private String type;
private String question;
//* Hidden from the user */
private String correctAnswer;
@@ -48,21 +48,25 @@ public class QuizSettings {
@Max(50)
private int amount = 10;
private Type type = Type.ANY;
// private Type type = Type.ANY;
private Difficulty difficulty = Difficulty.ANY;
private Category category = Category.ANY;
// private Difficulty difficulty = Difficulty.ANY;
private Integer category;
private String difficulty;
private String type;
public QuizSettings(){}
/** Inverse of toApiQuery */
public QuizSettings(int amount, Integer category, String difficulty, String type) {
public QuizSettings(int amount, Integer category, String difficulty, String 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;
this.category = category;
this.difficulty = difficulty;
this.type = type;
// 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;
}
/**
@@ -77,19 +81,37 @@ public class QuizSettings {
* Converts the class to a format that can be provided as arguments for the api request
* @return The resulting request arguments as String
*/
// public String toApiQuery() {
// StringBuilder query = new StringBuilder("?amount=" + amount);
// if (category != null && category != Category.ANY) {
// query.append("&category=").append(category.getApiValue());
// }
// if (difficulty != null && difficulty != Difficulty.ANY) {
// query.append("&difficulty=").append(difficulty.getApiValue());
// }
// if (type != null && type != Type.ANY) {
// query.append("&type=").append(type.getApiValue());
// }
// return query.toString();
// }
public String toApiQuery() {
StringBuilder query = new StringBuilder("?amount=" + amount);
if (category != null && category != Category.ANY) {
query.append("&category=").append(category.getApiValue());
if (category != null) {
query.append("&category=").append(category);
}
if (difficulty != null && difficulty != Difficulty.ANY) {
query.append("&difficulty=").append(difficulty.getApiValue());
if (difficulty != null ) {
query.append("&difficulty=").append(difficulty);
}
if (type != null && type != Type.ANY) {
query.append("&type=").append(type.getApiValue());
if (type != null ) {
query.append("&type=").append(type);
}
return query.toString();
@@ -11,7 +11,7 @@ import lombok.Setter;
public class Session {
private final String id;
private int statusCode = 0;
private List<Question> questions;
private List<QuestionDTO> questions;
public Session() {
this.id = UUID.randomUUID().toString();
@@ -0,0 +1,11 @@
package com.ninjaws.quiz.repositories;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import com.ninjaws.quiz.entities.Category;
public interface CategoryRepository extends JpaRepository<Category, Long>{
Optional<Category> findByName(String name);
}
@@ -1,8 +1,26 @@
package com.ninjaws.quiz.repositories;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import com.ninjaws.quiz.models.Question;
import com.ninjaws.quiz.entities.QuestionEntity;
public interface QuestionRepository extends JpaRepository<Question, Long> {
public interface QuestionRepository extends JpaRepository<QuestionEntity, Long>{//, JpaSpecificationExecutor<Question> {
@Query(value = "SELECT * FROM question AS q " +
"WHERE (?1 IS NULL OR q.category = ?1) " +
"AND (?2 IS NULL OR q.difficulty = ?2) " +
"AND (?3 IS NULL OR q.type = ?3) " +
"ORDER BY RAND() LIMIT ?4", nativeQuery = true)
List<QuestionEntity> lookupQuestions(Integer categoryId, String difficulty, String type, int limit);
// public List<Question> getQuestionsBy(String type, String category, String difficulty, int limit) {
// Specification<Product> spec = Specification.where(QuestionSpecification.hasType(type))
// .and(QuestionSpecification.hasCategory(category))
// .and(QuestionSpecification.hasDifficulty(difficulty));
// return productRepository.findAll(spec);
// }
}
@@ -9,6 +9,7 @@ import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.CategoryResponse;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Request;
@@ -26,13 +27,13 @@ public class ApiService {
private static final System.Logger logger = System.getLogger(ApiService.class.getName());
private final String apiUrl = "https://opentdb.com/api.php";
private final String apiUrl = "https://opentdb.com";
public ApiService() {
}
private String buildRequestString(QuizSettings quizSettings) {
return apiUrl + quizSettings.toApiQuery();
return apiUrl + "/api.php" + quizSettings.toApiQuery();
}
public void handleRequest(@Valid Request request) {
@@ -54,7 +55,9 @@ public class ApiService {
* Store them in the DataService
*/
public void collectBulk() {
String url = apiUrl + "?amount=50";
// TODO: Make this happen only once, when a new database is generated
getCategories();
String url = apiUrl + "/api.php?amount=50";
try {
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String json = response.getBody();
@@ -64,5 +67,21 @@ public class ApiService {
} catch (RestClientException | IOException e) {
logger.log(System.Logger.Level.ERROR, "Critical failure", e);
}
}
public void getCategories() {
String url = apiUrl + "/api_category.php";
try {
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
String json = response.getBody();
CategoryResponse categoryResponse = cleanerService.extractCategoryResponse(json);
responseHandler.handleCategoryResponse(categoryResponse);
} catch (RestClientException | IOException e) {
logger.log(System.Logger.Level.ERROR, "Critical failure", e);
}
}
}
@@ -1,22 +1,87 @@
package com.ninjaws.quiz.services;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang3.StringEscapeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ninjaws.quiz.entities.Category;
import com.ninjaws.quiz.entities.QuestionEntity;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.CategoryResponse;
import com.ninjaws.quiz.models.QuestionDTO;
import com.ninjaws.quiz.repositories.CategoryRepository;
@Service
public class CleanerService {
@Autowired
private ObjectMapper objectMapper;
@Autowired
private CategoryRepository categoryRepository;
public CleanerService() {}
public CleanerService() {
}
public ApiResponse extractApiResponse(String json) throws IOException {
return objectMapper.readValue(json, ApiResponse.class);
}
public CategoryResponse extractCategoryResponse(String json) throws IOException {
return objectMapper.readValue(json, CategoryResponse.class);
}
public List<QuestionDTO> questionListEntitytoDTO(List<QuestionEntity> entities) {
List<QuestionDTO> dtos = new ArrayList<>();
for (int i = 0; i < entities.size(); i++) {
QuestionEntity entity = entities.get(i);
dtos.add(questionEntityToDTO(entity));
}
return dtos;
}
public QuestionDTO questionEntityToDTO(QuestionEntity entity) {
QuestionDTO dto = new QuestionDTO();
dto.setQuestion(entity.getQuestion());
dto.setCorrectAnswer(entity.getCorrectAnswer());
dto.setIncorrectAnswer(entity.getIncorrectAnswers());
dto.setCategory(entity.getCategory().getName());
dto.setType(entity.getType());
dto.setDifficulty(entity.getDifficulty());
return dto;
}
public List<QuestionEntity> questionListDTOtoEntity(List<QuestionDTO> dtos) {
List<QuestionEntity> entities = new ArrayList<>();
for (int i = 0; i < dtos.size(); i++) {
QuestionDTO dto = dtos.get(i);
entities.add(questionDTOtoEntity(dto));
}
return entities;
}
public QuestionEntity questionDTOtoEntity(QuestionDTO dto) {
QuestionEntity entity = new QuestionEntity();
Category category = categoryRepository.findByName(decodeString(dto.getCategory())).orElseThrow(() -> new IllegalStateException("Category not in DB!"));
entity.setCategory(category);
entity.setDifficulty(dto.getDifficulty());
entity.setType(dto.getType());
entity.setQuestion(dto.getQuestion());
entity.setCorrectAnswer(dto.getCorrectAnswer());
entity.setIncorrectAnswers(dto.getIncorrectAnswers());
return entity;
}
/**
* For some reason, the categories come with special characters,
* But the categories from the questions dont
* So this has to be manually fixed if they are to be stored in the db (otherwise namematching doesnt work)
*/
private String decodeString(String text){
return StringEscapeUtils.unescapeHtml4(text);
}
}
@@ -5,23 +5,61 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.Question;
import com.ninjaws.quiz.entities.Category;
import com.ninjaws.quiz.entities.QuestionEntity;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.repositories.CategoryRepository;
import com.ninjaws.quiz.repositories.QuestionRepository;
@Service
public class DataService {
@Autowired
private QuestionRepository questionRepository;
public List<Question> getAllQuestions() {
@Autowired
private CategoryRepository categoryRepository;
public List<QuestionEntity> getAllQuestions() {
return questionRepository.findAll();
}
public Question saveQuestion(Question question) {
public QuestionEntity saveQuestion(QuestionEntity question) {
return questionRepository.save(question);
}
public List<Question> saveQuestions(List<Question> questions) {
public List<QuestionEntity> saveQuestions(List<QuestionEntity> questions) {
return questionRepository.saveAll(questions);
}
// public List<Question> getRandomQuestions(String type, String category, String difficulty, int limit) {
// return questionRepository.findRandomQuestions(type, limit);
// }
public List<QuestionEntity> lookupQuestions(QuizSettings quizSettings) {
// Long category = Long.valueOf(quizSettings.getCategory().getApiValue() == 1 ? null : quizSettings.getCategory().getApiValue());
// String difficulty = quizSettings.getDifficulty() == QuizSettings.Difficulty.ANY ? null : quizSettings.getDifficulty().getApiValue();
// String type = quizSettings.getType() == QuizSettings.Type.ANY ? null : quizSettings.getType().getApiValue();
// System.out.println(category);
// System.out.println(difficulty);
// System.out.println(type);
return questionRepository.lookupQuestions(quizSettings.getCategory(), quizSettings.getDifficulty(), quizSettings.getType(), quizSettings.getAmount());
}
/**
* Tries to get questions that match the specified QuizSettings
* @return The questions if the query was possible, and an empty array if not
*/
// public List<Question> getByQuizSettings(QuizSettings quizSettings) {
// Specification<Product> spec = Specification.where(QuestionSpecification.hasType(type))
// .and(QuestionSpecification.hasCategory(category))
// .and(QuestionSpecification.hasDifficulty(difficulty));
// return productRepository.findAll(spec);
// }
public List<Category> saveCategories(List<Category> categories) {
return categoryRepository.saveAll(categories);
}
}
@@ -0,0 +1,20 @@
package com.ninjaws.quiz.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class DatabaseScheduler {
@Autowired
private JdbcTemplate secondaryJdbcTemplate;
@Scheduled(fixedRate = 60000) // Runs every 60 seconds
public void createAndFillSecondaryDatabase() {
// Create tables and insert data into the secondary database
secondaryJdbcTemplate.
.execute("CREATE TABLE IF NOT EXISTS sample_table (id INT PRIMARY KEY, name VARCHAR(255))");
secondaryJdbcTemplate.update("INSERT INTO sample_table (id, name) VALUES (?, ?)", 1, "Sample Data");
}
}
@@ -1,10 +1,12 @@
package com.ninjaws.quiz.services;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.entities.QuestionEntity;
import com.ninjaws.quiz.models.Answers;
import com.ninjaws.quiz.models.QuizSettings;
import com.ninjaws.quiz.models.Request;
@@ -18,6 +20,10 @@ public class QuizService {
private CacheService cacheService;
@Autowired
private QueueService queueService;
@Autowired
private DataService dataService;
@Autowired
private CleanerService cleanerService;
/**
* Actions:
@@ -29,7 +35,22 @@ public class QuizService {
*/
public Optional<String> createSession(QuizSettings quizSettings) {
Session session = new Session();
/** First, we check if there are already enough items in our db */
List<QuestionEntity> questionsRetrieved = dataService.lookupQuestions(quizSettings);
if(!questionsRetrieved.isEmpty()) {
session.setQuestions(cleanerService.questionListEntitytoDTO(questionsRetrieved));
}
cacheService.addToCache(session.getId(), session);
/**
* If the questions were retrieved from the DB, then it doesn't need to be added to the queue
*/
if(!questionsRetrieved.isEmpty()) {
return Optional.of(session.getId());
}
boolean addedToQueue = queueService.addToQueue(new Request(session.getId(), quizSettings));
/** In this order, because doing the queue before the cache could lead to race conditions (request completion immediately calls cache, but then the item doesn't exist) */
if(!addedToQueue) {
@@ -4,6 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ninjaws.quiz.models.ApiResponse;
import com.ninjaws.quiz.models.CategoryResponse;
import com.ninjaws.quiz.models.Session;
@Service
@@ -13,6 +14,8 @@ public class ResponseHandlingService {
private CacheService cacheService;
@Autowired
private DataService dataService;
@Autowired
private CleanerService cleanerService;
/**
* If there is no sessionId, then this is a request from the scheduler
@@ -30,6 +33,7 @@ public class ResponseHandlingService {
case 0:
// Success
updateSessionInCache(session, response);
dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults()));
break;
case 1:
// Not enough questions to satisfy the request.
@@ -63,8 +67,7 @@ public class ResponseHandlingService {
public void handleResponse(ApiResponse response) {
switch (response.getResponseCode()) {
case 0:
// TODO: Store in DB
dataService.saveQuestions(response.getResults());
dataService.saveQuestions(cleanerService.questionListDTOtoEntity(response.getResults()));
break;
case 4:
// Signal DataService that we have extracted all the data. Stop gathering, switch to caching-mode
@@ -83,4 +86,7 @@ public class ResponseHandlingService {
cacheService.addToCache(session.getId(), session);
}
public void handleCategoryResponse(CategoryResponse categoryResponse) {
dataService.saveCategories(categoryResponse.getTrivia_categories());
}
}