From 0ea4892c7321d46a26d90937ab56007c4cfac567 Mon Sep 17 00:00:00 2001 From: Sunny Sharma Date: Sat, 9 May 2026 16:16:48 +0530 Subject: [PATCH] feat: implement PATCH /players/{squadNumber} for partial updates (#318) --- .../boot/controllers/PlayersController.java | 38 +++++++++++++++ .../spring/boot/models/PlayerPatchDTO.java | 46 +++++++++++++++++++ .../spring/boot/services/PlayersService.java | 42 +++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java index 0563f9b..38cf8bd 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/controllers/PlayersController.java @@ -15,8 +15,10 @@ import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerPatchDTO; import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO; import ar.com.nanotaboada.java.samples.spring.boot.services.PlayersService; import io.swagger.v3.oas.annotations.Operation; @@ -219,7 +221,43 @@ public ResponseEntity put(@PathVariable Integer squadNumber, @RequestBody ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } +/* + * ----------------------------------------------------------------------------------------------------------------------- + * HTTP PATCH + * ----------------------------------------------------------------------------------------------------------------------- + */ + /** + * Partially updates an existing player resource identified by squad number. + *

+ * Only the fields present in the request body are updated; absent fields retain + * their current values. The {@code squadNumber} and {@code id} fields are immutable + * and must not be included in the request body. + *

+ * + * @param squadNumber the squad number (natural key) of the player to patch + * @param playerPatchDTO the partial player data to apply (only non-null fields are applied) + * @return 204 No Content if successful, 400 Bad Request if body contains squadNumber or id, + * or 404 Not Found if player doesn't exist + */ + @PatchMapping("/players/{squadNumber}") + @Operation(summary = "Partially updates a player by squad number") + @ApiResponses(value = { + @ApiResponse(responseCode = "204", description = "No Content", content = @Content), + @ApiResponse(responseCode = "400", description = "Bad Request - Body must not contain squadNumber or id", content = @Content), + @ApiResponse(responseCode = "404", description = "Not Found", content = @Content) + }) + public ResponseEntity patch( + @PathVariable Integer squadNumber, + @RequestBody PlayerPatchDTO playerPatchDTO) { + if (playerPatchDTO.getSquadNumber() != null || playerPatchDTO.getId() != null) { + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + boolean patched = playersService.patch(squadNumber, playerPatchDTO); + return (patched) + ? ResponseEntity.status(HttpStatus.NO_CONTENT).build() + : ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } /* * ----------------------------------------------------------------------------------------------------------------------- * HTTP DELETE diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java new file mode 100644 index 0000000..7b9a162 --- /dev/null +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/models/PlayerPatchDTO.java @@ -0,0 +1,46 @@ +package ar.com.nanotaboada.java.samples.spring.boot.models; + +import java.time.LocalDate; +import java.util.UUID; + +import com.fasterxml.jackson.annotation.JsonInclude; + +import lombok.Data; + +/** + * Data Transfer Object for partial updates to a {@link Player} resource (HTTP PATCH). + *

+ * All fields are nullable. Only non-null fields are applied to the existing entity; + * absent fields (null) are left unchanged — following RFC 7396 (JSON Merge Patch) semantics. + *

+ * + *

Immutable Fields:

+ *
    + *
  • {@code id} — surrogate UUID key, must not be present in PATCH requests
  • + *
  • {@code squadNumber} — natural key, must not be present in PATCH requests
  • + *
+ *

+ * If either immutable field is present in the request body, the controller returns + * {@code 400 Bad Request}. + *

+ * + * @see Player + * @see PlayerDTO + * @since 4.0.2025 + */ +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PlayerPatchDTO { + + private UUID id; + private Integer squadNumber; + private String firstName; + private String middleName; + private String lastName; + private LocalDate dateOfBirth; + private String position; + private String abbrPosition; + private String team; + private String league; + private Boolean starting11; +} \ No newline at end of file diff --git a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java index 33fb118..ae0f32f 100644 --- a/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java +++ b/src/main/java/ar/com/nanotaboada/java/samples/spring/boot/services/PlayersService.java @@ -12,6 +12,7 @@ import ar.com.nanotaboada.java.samples.spring.boot.models.Player; import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerDTO; +import ar.com.nanotaboada.java.samples.spring.boot.models.PlayerPatchDTO; import ar.com.nanotaboada.java.samples.spring.boot.repositories.PlayersRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -215,6 +216,47 @@ public boolean update(Integer squadNumber, PlayerDTO playerDTO) { }); } + /** + * Partially updates an existing player identified by their squad number. + *

+ * Only the non-null fields in the DTO are applied to the existing entity. + * Fields absent from the request (null) are left unchanged. + *

+ * + * @param squadNumber the squad number (natural key) of the player to patch + * @param playerPatchDTO the partial player data to apply + * @return true if the player was patched successfully, false if not found + */ + @Transactional + @CacheEvict(value = "players", allEntries = true) + public boolean patch(Integer squadNumber, PlayerPatchDTO playerPatchDTO) { + log.debug("Patching player with squad number: {}", squadNumber); + + if (squadNumber == null) { + log.warn("Cannot patch player - squad number is null"); + return false; + } + + return playersRepository.findBySquadNumber(squadNumber) + .map(existing -> { + if (playerPatchDTO.getFirstName() != null) existing.setFirstName(playerPatchDTO.getFirstName()); + if (playerPatchDTO.getMiddleName() != null) existing.setMiddleName(playerPatchDTO.getMiddleName()); + if (playerPatchDTO.getLastName() != null) existing.setLastName(playerPatchDTO.getLastName()); + if (playerPatchDTO.getDateOfBirth() != null) existing.setDateOfBirth(playerPatchDTO.getDateOfBirth()); + if (playerPatchDTO.getPosition() != null) existing.setPosition(playerPatchDTO.getPosition()); + if (playerPatchDTO.getAbbrPosition() != null) existing.setAbbrPosition(playerPatchDTO.getAbbrPosition()); + if (playerPatchDTO.getTeam() != null) existing.setTeam(playerPatchDTO.getTeam()); + if (playerPatchDTO.getLeague() != null) existing.setLeague(playerPatchDTO.getLeague()); + if (playerPatchDTO.getStarting11() != null) existing.setStarting11(playerPatchDTO.getStarting11()); + playersRepository.save(existing); + log.info("Player patched successfully - Squad Number: {}", squadNumber); + return true; + }) + .orElseGet(() -> { + log.warn("Cannot patch player - squad number {} not found", squadNumber); + return false; + }); + } /* * ----------------------------------------------------------------------------------------------------------------------- * Delete