From f457911f3b9ce6deb3043c346aac96c296693c44 Mon Sep 17 00:00:00 2001 From: Laurent Date: Fri, 12 Dec 2025 16:45:12 +0100 Subject: [PATCH] Added move registration and validation --- api/Dockerfile | 14 ++++-- api/compose-dev.yaml | 7 ++- api/gradle.properties | 1 + .../boardmateapi/common/helpers/Logger.java | 15 ++++++ .../boardmateapi/common/models/Game.java | 39 +++++++++++---- .../configurations/AppConfigurations.java | 4 +- .../configurations/BuildHasher.java | 30 ++++++++++++ .../configurations/MongoConfig.java | 3 -- .../boardmateapi/configurations/Startup.java | 23 +++++++++ .../controllers/GameController.java | 43 +++++++++------- .../controllers/ResponseBody.java | 49 +++++++++++++++++++ .../controllers/dtos/GameDto.java | 40 +++++++++++++++ .../controllers/mappings/GameMapper.java | 22 +++++++++ .../boardmateapi/repository/dtos/GameDto.java | 4 ++ .../repository/mappings/GameMapper.java | 9 ++-- .../boardmateapi/services/GameService.java | 3 +- api/src/main/resources/application.properties | 8 +-- 17 files changed, 269 insertions(+), 45 deletions(-) create mode 100644 api/gradle.properties create mode 100644 api/src/main/java/be/naaturel/boardmateapi/common/helpers/Logger.java create mode 100644 api/src/main/java/be/naaturel/boardmateapi/configurations/BuildHasher.java create mode 100644 api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java create mode 100644 api/src/main/java/be/naaturel/boardmateapi/controllers/ResponseBody.java create mode 100644 api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/GameDto.java create mode 100644 api/src/main/java/be/naaturel/boardmateapi/controllers/mappings/GameMapper.java diff --git a/api/Dockerfile b/api/Dockerfile index a0f75e10..951f0c84 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -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"] - +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/api/compose-dev.yaml b/api/compose-dev.yaml index 5f661afc..7854f017 100644 --- a/api/compose-dev.yaml +++ b/api/compose-dev.yaml @@ -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' diff --git a/api/gradle.properties b/api/gradle.properties new file mode 100644 index 00000000..16089002 --- /dev/null +++ b/api/gradle.properties @@ -0,0 +1 @@ +org.gradle.caching=true diff --git a/api/src/main/java/be/naaturel/boardmateapi/common/helpers/Logger.java b/api/src/main/java/be/naaturel/boardmateapi/common/helpers/Logger.java new file mode 100644 index 00000000..c2009709 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/common/helpers/Logger.java @@ -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); + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/common/models/Game.java b/api/src/main/java/be/naaturel/boardmateapi/common/models/Game.java index 6264f1bb..7ff087af 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/common/models/Game.java +++ b/api/src/main/java/be/naaturel/boardmateapi/common/models/Game.java @@ -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 moves; + private final List 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 moves){ + public Game(String id, String whiteName, String blackName, int timeValue, int increment, Collection 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 getMoves() { + public List 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); + } + } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java index a74fb049..88d113e8 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/AppConfigurations.java @@ -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; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/BuildHasher.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/BuildHasher.java new file mode 100644 index 00000000..1d57fb49 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/BuildHasher.java @@ -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(); + } + +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java index d9c4b523..4a251ac6 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/MongoConfig.java @@ -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); diff --git a/api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java b/api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java new file mode 100644 index 00000000..32b0ebcb --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/configurations/Startup.java @@ -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())); + } + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java index d9a43fff..bea3bbab 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/GameController.java @@ -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 retrieveGames(@PathVariable String id){ + public ResponseEntity> retrieveGames(@PathVariable String id){ + ResponseBody 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> CreateParty(@RequestBody GameDto game){ + ResponseBody 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(); } } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/ResponseBody.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/ResponseBody.java new file mode 100644 index 00000000..b5650b6f --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/ResponseBody.java @@ -0,0 +1,49 @@ +package be.naaturel.boardmateapi.controllers; + +import java.util.Optional; + +public class ResponseBody { + + 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 The wrapped data type + */ + public static ResponseBody 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 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; + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/GameDto.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/GameDto.java new file mode 100644 index 00000000..a395b346 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/dtos/GameDto.java @@ -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; + } +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/controllers/mappings/GameMapper.java b/api/src/main/java/be/naaturel/boardmateapi/controllers/mappings/GameMapper.java new file mode 100644 index 00000000..2b0852f8 --- /dev/null +++ b/api/src/main/java/be/naaturel/boardmateapi/controllers/mappings/GameMapper.java @@ -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; + } + +} diff --git a/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/GameDto.java b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/GameDto.java index 7307f8b3..f41948f9 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/GameDto.java +++ b/api/src/main/java/be/naaturel/boardmateapi/repository/dtos/GameDto.java @@ -36,6 +36,10 @@ public class GameDto { return players; } + public void setId(String id){ + this.id = id; + } + public void setPlayers(Map players) { this.players = players; } diff --git a/api/src/main/java/be/naaturel/boardmateapi/repository/mappings/GameMapper.java b/api/src/main/java/be/naaturel/boardmateapi/repository/mappings/GameMapper.java index 7d454e65..fa77081c 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/repository/mappings/GameMapper.java +++ b/api/src/main/java/be/naaturel/boardmateapi/repository/mappings/GameMapper.java @@ -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 players = new HashMap<>(); @@ -28,7 +30,6 @@ public class GameMapper { List 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 moves = dto.getMoves(); + return new Game(id, whiteName, blackName, timeValue, increment, moves); } } diff --git a/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java b/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java index c3c91021..94cb207c 100644 --- a/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java +++ b/api/src/main/java/be/naaturel/boardmateapi/services/GameService.java @@ -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(); } } diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties index 4839eefa..19fa78c5 100644 --- a/api/src/main/resources/application.properties +++ b/api/src/main/resources/application.properties @@ -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 \ No newline at end of file