Added move registration and validation

This commit is contained in:
2025-12-12 16:45:12 +01:00
parent 47e812d710
commit f457911f3b
17 changed files with 269 additions and 45 deletions

View File

@@ -2,20 +2,26 @@
FROM gradle:9.2-jdk21 AS build
WORKDIR /app
# Copy only build scripts first
COPY settings.gradle.kts build.gradle.kts ./
COPY gradle.properties* ./
COPY gradle ./gradle
# Warm dependency cache
RUN gradle build -x test --no-daemon || true
# Copy source last
COPY src ./src
RUN gradle clean bootJar --no-daemon
# Build the jar
RUN gradle bootJar --no-daemon
# -------- Runtime --------
FROM eclipse-temurin:21-jdk-alpine
EXPOSE 8080
WORKDIR /app
COPY --from=build /app/build/libs/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

View File

@@ -1,6 +1,8 @@
services:
boardmate-api:
build: .
build:
context: .
dockerfile: Dockerfile
container_name: boardmate-api
ports:
- "8000:8080"
@@ -11,6 +13,9 @@ services:
depends_on:
- mongodb
- elasticsearch
volumes:
- ./.gradle:/home/gradle/.gradle # Use your existing local Gradle and build directories
- ./build:/app/.gradle # optional, only if you want project cache mapped too
elasticsearch:
image: 'docker.elastic.co/elasticsearch/elasticsearch:7.17.10'

1
api/gradle.properties Normal file
View File

@@ -0,0 +1 @@
org.gradle.caching=true

View File

@@ -0,0 +1,15 @@
package be.naaturel.boardmateapi.common.helpers;
public class Logger {
public static void displayInfo(String message){
final String BLUE = "\u001B[34m";
final String RESET = "\u001B[0m";
System.out.println(BLUE + "[Info] --- " + message + RESET);
}
public static void displayError(String message){
String GREEN = "\u001B[32m";
String RESET = "\u001B[0m";
System.out.println(GREEN + "[Error] --- " + message + RESET);
}
}

View File

@@ -3,20 +3,27 @@ package be.naaturel.boardmateapi.common.models;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
public class Game {
private static final Pattern MOVE_PATTERN = Pattern.compile(
"^(O-O(-O)?|([KQRBN])?([a-h])?([1-8])?x?[a-h][1-8](=[QRBN])?[+#]?)$"
);
private final String id;
private final String whiteName;
private final String blackName;
private final int timeValue;
private final int increment;
private final List<Move> moves;
private final List<String> moves;
public Game(String whiteName, String blackName, int timeValue, int increment) {
this(whiteName, blackName, timeValue, increment, new ArrayList<>());
this(null, whiteName, blackName, timeValue, increment, new ArrayList<>());
}
public Game(String whiteName, String blackName, int timeValue, int increment, Collection<Move> moves){
public Game(String id, String whiteName, String blackName, int timeValue, int increment, Collection<String> moves){
this.id = id;
this.whiteName = whiteName;
this.blackName = blackName;
this.timeValue = timeValue;
@@ -24,6 +31,10 @@ public class Game {
this.moves = new ArrayList<>(moves);
}
public String getId(){
return this.id;
}
public String getBlackName() {
return blackName;
}
@@ -40,11 +51,12 @@ public class Game {
return this.increment;
}
public List<Move> getMoves() {
public List<String > getMoves() {
return new ArrayList<>(moves);
}
public void addMove(Move m) {
public void addMove(String m) throws IllegalArgumentException {
validateMove(m);
this.moves.add(m);
}
@@ -52,12 +64,12 @@ public class Game {
StringBuilder builder = new StringBuilder();
int moveNumber = 1;
int movesInTurn = 1;
for(Move m : moves){
for(String m : moves){
if(movesInTurn == 1) {
builder.append(String.format("%d.", moveNumber));
builder.append(String.format("%d. ", moveNumber));
}
builder.append(String.format(" %s", m));
builder.append(String.format("%s ", m));
if(movesInTurn == 2){
moveNumber++;
@@ -66,7 +78,16 @@ public class Game {
movesInTurn++;
}
}
return builder.toString();
return builder.toString().trim();
}
public static void validateMove(String move) throws IllegalArgumentException {
if (move == null || move.isBlank()) {
throw new IllegalArgumentException("Move cannot be empty");
}
if (!MOVE_PATTERN.matcher(move).matches()) {
throw new IllegalArgumentException("Invalid PGN move: " + move);
}
}
}

View File

@@ -15,10 +15,10 @@ public class AppConfigurations {
@Value("${sec.cors.authorizedHeader}")
public String[] authorizedHeaders;
@Value("${spring.data.mongodb.uri}")
@Value("${spring.mongodb.uri}")
public String connectionString;
@Value("${spring.data.mongodb.database}")
@Value("${spring.mongodb.database}")
public String database;
}

View File

@@ -0,0 +1,30 @@
package be.naaturel.boardmateapi.configurations;
import java.io.BufferedInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.MessageDigest;
public class BuildHasher {
public static String get() throws Exception {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (InputStream is =
BuildHasher.class.getProtectionDomain()
.getCodeSource()
.getLocation()
.openStream()) {
digest.update(is.readAllBytes());
}
byte[] hashBytes = digest.digest();
StringBuilder hex = new StringBuilder(hashBytes.length * 2);
for (byte b : hashBytes) hex.append(String.format("%02x", b));
return hex.toString();
}
}

View File

@@ -8,17 +8,14 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.mongodb.core.MongoTemplate;
@Configuration
public class MongoConfig {
private final AppConfigurations conf;
@Autowired
public MongoConfig(AppConfigurations appConf) {
this.conf = appConf;
}
@Bean
public MongoTemplate mongoTemplate() {
System.out.println(">>> Connection string : " + conf.connectionString);
System.out.println(">>> Used database : " + conf.database);

View File

@@ -0,0 +1,23 @@
package be.naaturel.boardmateapi.configurations;
import be.naaturel.boardmateapi.common.helpers.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.*;
import java.util.Arrays;
@Configuration
public class Startup {
@Bean
public String log() {
try {
String hash = BuildHasher.get();
Logger.displayInfo("Hash for this version " + hash);
return hash;
} catch (Exception e) {
throw new RuntimeException("Unable to generate a hash for this version\n" + Arrays.toString(e.getStackTrace()));
}
}
}

View File

@@ -1,8 +1,10 @@
package be.naaturel.boardmateapi.controllers;
import be.naaturel.boardmateapi.common.models.Move;
import be.naaturel.boardmateapi.common.models.Game;
import be.naaturel.boardmateapi.controllers.dtos.GameDto;
import be.naaturel.boardmateapi.controllers.mappings.GameMapper;
import be.naaturel.boardmateapi.services.GameService;
import jakarta.websocket.server.PathParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -30,47 +32,54 @@ public class GameController {
}
@GetMapping("/games/{id}")
public ResponseEntity<Game> retrieveGames(@PathVariable String id){
public ResponseEntity<ResponseBody<GameDto>> retrieveGames(@PathVariable String id){
ResponseBody<GameDto> response = ResponseBody.createEmpty();
try{
Game g = service.retrieveGame(id);
GameDto dto = GameMapper.toDto(g);
response.setData(dto);
response.setSuccess(true);
return ResponseEntity
.status(HttpStatus.OK)
.body(g);
.body(response);
} catch (Exception e){
e.printStackTrace();
response.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.build();
.body(response);
}
}
@PostMapping("/create")
public ResponseEntity<?> CreateParty(@RequestBody Game game){
public ResponseEntity<ResponseBody<String>> CreateParty(@RequestBody GameDto game){
ResponseBody<String> response = ResponseBody.createEmpty();
try{
service.create(game);
Game model = GameMapper.toModel(game);
String result = service.create(model);
response.setData(result);
response.setSuccess(true);
return ResponseEntity.
status(HttpStatus.INTERNAL_SERVER_ERROR)
.build();
status(HttpStatus.OK)
.body(response);
} catch (Exception e){
e.printStackTrace();
response.setMessage(e.getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(e.getStackTrace());
.body(response);
}
}
@PostMapping("/moves/add")
public ResponseEntity<?> AddMove(@RequestBody String gameId, @RequestBody Move move){
@PostMapping("/moves/add/{gameId}")
public ResponseEntity<?> AddMove(@PathVariable String gameId, @RequestBody String move){
try{
service.addMove(gameId, move);
return ResponseEntity
.status(HttpStatus.OK)
.build();
} catch (Exception e){
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.build();
}
}
}

View File

@@ -0,0 +1,49 @@
package be.naaturel.boardmateapi.controllers;
import java.util.Optional;
public class ResponseBody<T> {
private T data;
private String message;
private boolean success;
/**
* Create a default empty response body containing no data, no message and flagged as failure
* @return Empty response of T
* @param <T> The wrapped data type
*/
public static <T> ResponseBody<T> createEmpty() {
return new ResponseBody<>(null, null,false);
}
private ResponseBody(T data, String messageOpt, boolean success){
this.data = data;
this.message = null;
this.success = success;
}
public T getData() {
return data;
}
public Optional<String> getMessage() {
return Optional.ofNullable(message);
}
public boolean isSuccess() {
return success;
}
public void setData(T data) {
this.data = data;
}
public void setSuccess(boolean success) {
this.success = success;
}
public void setMessage(String message) {
this.message = message;
}
}

View File

@@ -0,0 +1,40 @@
package be.naaturel.boardmateapi.controllers.dtos;
public class GameDto {
private String whiteName;
private String blackName;
private int timeValue;
private int increment;
public String getWhiteName() {
return whiteName;
}
public String getBlackName() {
return blackName;
}
public int getTimeValue() {
return timeValue;
}
public int getIncrement() {
return increment;
}
public void setWhiteName(String whiteName) {
this.whiteName = whiteName;
}
public void setBlackName(String blackName) {
this.blackName = blackName;
}
public void setTimeValue(int timeValue) {
this.timeValue = timeValue;
}
public void setIncrement(int increment){
this.increment = increment;
}
}

View File

@@ -0,0 +1,22 @@
package be.naaturel.boardmateapi.controllers.mappings;
import be.naaturel.boardmateapi.common.models.Game;
import be.naaturel.boardmateapi.controllers.dtos.GameDto;
public class GameMapper {
public static Game toModel(GameDto dto){
return new Game(dto.getWhiteName(), dto.getBlackName(), dto.getTimeValue(), dto.getIncrement());
}
public static GameDto toDto(Game model){
GameDto g = new GameDto();
g.setWhiteName(model.getWhiteName());
g.setBlackName(model.getBlackName());
g.setTimeValue(model.getTimeValue());
g.setIncrement(model.getIncrement());
return g;
}
}

View File

@@ -36,6 +36,10 @@ public class GameDto {
return players;
}
public void setId(String id){
this.id = id;
}
public void setPlayers(Map<String, PlayerDto> players) {
this.players = players;
}

View File

@@ -1,7 +1,6 @@
package be.naaturel.boardmateapi.repository.mappings;
import be.naaturel.boardmateapi.common.models.Game;
import be.naaturel.boardmateapi.common.models.Move;
import be.naaturel.boardmateapi.repository.dtos.GameDto;
import be.naaturel.boardmateapi.repository.dtos.PlayerDto;
import be.naaturel.boardmateapi.repository.dtos.TimeControlDto;
@@ -15,6 +14,9 @@ public class GameMapper {
public static GameDto toDto(Game game){
GameDto dto = new GameDto();
String id = game.getId();
dto.setId(id);
PlayerDto white = PlayerMapper.toDto(game.getWhiteName());
PlayerDto black = PlayerMapper.toDto(game.getBlackName());
Map<String, PlayerDto> players = new HashMap<>();
@@ -28,7 +30,6 @@ public class GameMapper {
List<String> moves =
game.getMoves()
.stream()
.map(Move::toString)
.toList();
dto.setMoves(moves);
@@ -36,10 +37,12 @@ public class GameMapper {
}
public static Game toModel(GameDto dto){
String id = dto.getId();
String whiteName = dto.getPlayers().get("white").getName();
String blackName = dto.getPlayers().get("black").getName();
int timeValue = dto.getTimeControl().getValue();
int increment = dto.getTimeControl().getIncrement();
return new Game(whiteName, blackName, timeValue, increment);
List<String> moves = dto.getMoves();
return new Game(id, whiteName, blackName, timeValue, increment, moves);
}
}

View File

@@ -38,10 +38,9 @@ public class GameService {
return gameDto.getId();
}
public String addMove(@RequestBody String gameId, @RequestBody Move move) throws Exception {
public void addMove(@RequestBody String gameId, @RequestBody String move) {
Game g = retrieveGame(gameId);
g.addMove(move);
save(g);
return move.toString();
}
}

View File

@@ -16,9 +16,9 @@ management.endpoint.health.show-details=always
management.endpoint.prometheus.enabled=true
#=============DOCUMENTATION=============
springdoc.swagger-ui.path=/api-docs
springdoc.api-docs.path=/v1/api-docs
springdoc.swagger-ui.path=/docs
springdoc.api-docs.path=/v1/docs
#=============DATABASE=============
spring.data.mongodb.uri=mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db?authSource=board-mate-db
spring.data.mongodb.database=board-mate-db
spring.mongodb.uri=mongodb://board-mate-user:apx820kcng@mongodb:27017/board-mate-db?authSource=board-mate-db
spring.mongodb.database=board-mate-db