Repository: HomeVaultNSU/file-server
Pull request #9: task/dev-4
Author: login: kosteekX3D
Created: 2025-04-25T09:24:54Z | Merged: 2025-04-26T05:23:55Z

- removed jakarta validation dependency
- add dependency org.springframework.boot:spring-boot-starter-validation
- added to FileControllerAdivce new exception to handle: MethodArgumentNotValidException


Diff:
diff --git a/service/build.gradle b/service/build.gradle
index c887bd2..a7c7fe3 100644
--- a/service/build.gradle
+++ b/service/build.gradle
@@ -25,13 +25,12 @@ ext {
     lombokVersion = '1.18.36'
     spockFrameworkRelease = '4.0'
     spockFrameworkVersion = '2.4'
-    jakartaValidationVersion = '3.1.1'
     mapStructVersion = '1.6.2'
 }
 
 dependencies {
     implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
-    implementation "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion"
+    implementation "org.springframework.boot:spring-boot-starter-validation:$springBootVersion"
 
     implementation "org.mapstruct:mapstruct:$mapStructVersion"
 
diff --git a/service/src/main/java/ru/homevault/fileserver/controller/FileController.java b/service/src/main/java/ru/homevault/fileserver/controller/FileController.java
index c2e9d31..61d804a 100644
--- a/service/src/main/java/ru/homevault/fileserver/controller/FileController.java
+++ b/service/src/main/java/ru/homevault/fileserver/controller/FileController.java
@@ -1,6 +1,8 @@
 package ru.homevault.fileserver.controller;
 
 import jakarta.validation.constraints.Min;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import org.springframework.core.io.Resource;
 import org.springframework.http.HttpHeaders;
@@ -34,15 +36,15 @@ public class FileController {
 
     @GetMapping("/list")
     public DirectoryListing list(
-            @RequestParam("path") String path,
-            @RequestParam(value = "depth", defaultValue = "0") @Min(0) Integer depth
+            @RequestParam(value = "path", defaultValue = "") String path,
+            @RequestParam(value = "depth", defaultValue = "0") @Min(value = 0, message = "Depth must be >= 0") Integer depth
     ) {
         return fileService.getDirectoryListing(path, depth);
     }
 
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<UploadResponse> upload(
-            @RequestPart("file") MultipartFile file,
+            @RequestPart("file") @NotNull(message = "File cannot be null") MultipartFile file,
             @RequestParam(value = "path", defaultValue = "/") String path
     ) {
         String filePath = fileService.uploadFile(file, path);
@@ -51,7 +53,9 @@ public ResponseEntity<UploadResponse> upload(
     }
 
     @GetMapping("/download")
-    public ResponseEntity<Resource> download(@RequestParam("path") String filePath) {
+    public ResponseEntity<Resource> download(
+            @RequestParam("path") @NotBlank(message = "Path cannot be blank") String filePath
+    ) {
         Resource fileResource = fileService.downloadFile(filePath);
 
         String encodedFilename = Optional.ofNullable(fileResource.getFilename())
@@ -63,5 +67,4 @@ public ResponseEntity<Resource> download(@RequestParam("path") String filePath)
                 .contentType(MediaType.APPLICATION_OCTET_STREAM)
                 .body(fileResource);
     }
-
-}
+}
\ No newline at end of file
diff --git a/service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java b/service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java
index cbd53e8..77f8ced 100644
--- a/service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java
+++ b/service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java
@@ -1,6 +1,7 @@
 package ru.homevault.fileserver.controller;
 
 
+import jakarta.validation.ConstraintViolationException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -12,18 +13,20 @@
 import ru.homevault.fileserver.dto.ErrorResponse;
 import ru.homevault.fileserver.exception.HomeVaultException;
 
+import java.util.stream.Collectors;
+
 @Slf4j
 @RestControllerAdvice
 public class FileControllerAdvice {
 
-    @ExceptionHandler(exception = HomeVaultException.class)
+    @ExceptionHandler(HomeVaultException.class)
     public ResponseEntity<ErrorResponse> handleHomeVaultException(HomeVaultException ex) {
+
         ErrorResponse error = ErrorResponse.builder()
                 .error(ex.getMessage())
                 .build();
 
-        return ResponseEntity
-                .status(ex.getHttpStatus())
+        return ResponseEntity.status(ex.getHttpStatus())
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
@@ -36,34 +39,45 @@ public ResponseEntity<ErrorResponse> handleThrowable(Throwable t) {
                 .error("Unexpected server error")
                 .build();
 
-        return ResponseEntity
-                .status(HttpStatus.INTERNAL_SERVER_ERROR)
+        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
 
     @ExceptionHandler(NoResourceFoundException.class)
     public ResponseEntity<ErrorResponse> handleNotFound(NoResourceFoundException ex) {
+
         ErrorResponse error = ErrorResponse.builder()
                 .error("Resource not found: " + ex.getResourcePath())
                 .build();
 
-        return ResponseEntity
-                .status(HttpStatus.NOT_FOUND)
+        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
 
     @ExceptionHandler(MissingServletRequestParameterException.class)
-    public ResponseEntity<ErrorResponse> handleNotFound(MissingServletRequestParameterException ex) {
+    public ResponseEntity<ErrorResponse> handleMissingParameter(MissingServletRequestParameterException ex) {
+
         ErrorResponse error = ErrorResponse.builder()
                 .error("Missing required request parameter: " + ex.getParameterName())
                 .build();
 
-        return ResponseEntity
-                .status(HttpStatus.NOT_FOUND)
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
 
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<ErrorResponse> handleConstraintViolation(ConstraintViolationException ex) {
+
+        String message = ex.getConstraintViolations()
+                .stream()
+                .map(v -> "%s".formatted(v.getMessage()))
+                .collect(Collectors.joining(", "));
+
+        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
+                .body(ErrorResponse.builder().error(message).build());
+    }
+
 }


Reviews:
user: login: b4lmor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:16:54Z
user: login: kosteekX3D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T10:18:38Z
user: login: b4lmor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T21:55:32Z
user: login: b4lmor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T21:58:54Z
user: login: b4lmor
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T21:59:27Z
user: login: kosteekX3D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T03:56:28Z
user: login: kosteekX3D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T04:00:53Z
user: login: kosteekX3D
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T05:12:52Z
user: login: b4lmor
body: wp
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-26T05:21:16Z

Review comments:
diff_hunk: @@ -34,24 +36,28 @@ public class FileController {
 
     @GetMapping("/list")
     public DirectoryListing list(
-            @RequestParam("path") String path,
-            @RequestParam(value = "depth", defaultValue = "0") @Min(0) Integer depth
+            @RequestParam("path") @NotEmpty(message = "Path cannot be empty") String path,
+            @RequestParam(value = "depth", defaultValue = "0") @Min(value = 0, message = "Depth must be >= 0") Integer depth
     ) {
         return fileService.getDirectoryListing(path, depth);
     }
 
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<UploadResponse> upload(
             @RequestPart("file") MultipartFile file,
-            @RequestParam(value = "path", defaultValue = "/") String path
+            @RequestParam(value = "path", defaultValue = "/") @Size(max = 255, message = "Path must be <= 255 characters") String path
     ) {
+        if (file.isEmpty()) {
path: service/src/main/java/ru/homevault/fileserver/controller/FileController.java
user: login: b4lmor
body: не стоит добавлять бизнес логику в код метода контроллера
created_at: 2025-04-25T10:16:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,24 +36,28 @@ public class FileController {
 
     @GetMapping("/list")
     public DirectoryListing list(
-            @RequestParam("path") String path,
-            @RequestParam(value = "depth", defaultValue = "0") @Min(0) Integer depth
+            @RequestParam("path") @NotEmpty(message = "Path cannot be empty") String path,
+            @RequestParam(value = "depth", defaultValue = "0") @Min(value = 0, message = "Depth must be >= 0") Integer depth
     ) {
         return fileService.getDirectoryListing(path, depth);
     }
 
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<UploadResponse> upload(
             @RequestPart("file") MultipartFile file,
-            @RequestParam(value = "path", defaultValue = "/") String path
+            @RequestParam(value = "path", defaultValue = "/") @Size(max = 255, message = "Path must be <= 255 characters") String path
     ) {
+        if (file.isEmpty()) {
path: service/src/main/java/ru/homevault/fileserver/controller/FileController.java
user: login: kosteekX3D
body: fixed
created_at: 2025-04-25T10:18:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,24 +36,26 @@ public class FileController {
 
     @GetMapping("/list")
     public DirectoryListing list(
-            @RequestParam("path") String path,
-            @RequestParam(value = "depth", defaultValue = "0") @Min(0) Integer depth
+            @RequestParam(value = "path", defaultValue = "") String path,
+            @RequestParam(value = "depth", defaultValue = "0") @Min(value = 0, message = "Depth must be >= 0") Integer depth
     ) {
         return fileService.getDirectoryListing(path, depth);
     }
 
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<UploadResponse> upload(
-            @RequestPart("file") MultipartFile file,
-            @RequestParam(value = "path", defaultValue = "/") String path
+            @RequestPart("file") @NotNull(message = "File cannot be null") MultipartFile file,
+            @RequestParam(value = "path", defaultValue = "/") @NotBlank(message = "Path cannot be blank") String path
path: service/src/main/java/ru/homevault/fileserver/controller/FileController.java
user: login: b4lmor
body: поскольку загрузить могут и в корневую директорию, path может быть blank
created_at: 2025-04-25T21:55:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,15 +63,34 @@ public ResponseEntity<ErrorResponse> handleNotFound(NoResourceFoundException ex)
     }
 
     @ExceptionHandler(MissingServletRequestParameterException.class)
-    public ResponseEntity<ErrorResponse> handleNotFound(MissingServletRequestParameterException ex) {
+    public ResponseEntity<ErrorResponse> handleMissingParameter(MissingServletRequestParameterException ex) {
+
         ErrorResponse error = ErrorResponse.builder()
                 .error("Missing required request parameter: " + ex.getParameterName())
                 .build();
 
         return ResponseEntity
-                .status(HttpStatus.NOT_FOUND)
+                .status(HttpStatus.BAD_REQUEST)
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
 
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<Map<String, Object>> handleConstraintViolation(ConstraintViolationException ex) {
+        Map<String, String> fieldErrors = new HashMap<>();
+        ex.getConstraintViolations().forEach(violation -> {
+            String field = violation.getPropertyPath().toString();
+            fieldErrors.put(field, violation.getMessage());
+        });
+
+        Map<String, Object> errorResponse = new HashMap<>();
+        errorResponse.put("status", HttpStatus.BAD_REQUEST.value());
+        errorResponse.put("message", "Validation failed");
+        errorResponse.put("errors", fieldErrors);
+
+        return ResponseEntity
+                .status(HttpStatus.BAD_REQUEST)
+                .body(errorResponse);
path: service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java
user: login: b4lmor
body: в теле ответа должен быть ErrorResponse, а не Map<>
{
"message": "path is invalid: cannot be empty", -- что-то такое
"timestamp": ...
}
created_at: 2025-04-25T21:58:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +25,12 @@ ext {
     lombokVersion = '1.18.36'
     spockFrameworkRelease = '4.0'
     spockFrameworkVersion = '2.4'
-    jakartaValidationVersion = '3.1.1'
     mapStructVersion = '1.6.2'
 }
 
 dependencies {
     implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
-    implementation "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion"
+    implementation "org.springframework.boot:spring-boot-starter-validation:$springBootVersion" // Add this line
path: service/build.gradle
user: login: b4lmor
body: предлагаю убрать комментарий к строчке
created_at: 2025-04-25T21:59:27Z
author_association: CONTRIBUTOR
diff_hunk: @@ -34,24 +36,26 @@ public class FileController {
 
     @GetMapping("/list")
     public DirectoryListing list(
-            @RequestParam("path") String path,
-            @RequestParam(value = "depth", defaultValue = "0") @Min(0) Integer depth
+            @RequestParam(value = "path", defaultValue = "") String path,
+            @RequestParam(value = "depth", defaultValue = "0") @Min(value = 0, message = "Depth must be >= 0") Integer depth
     ) {
         return fileService.getDirectoryListing(path, depth);
     }
 
     @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
     public ResponseEntity<UploadResponse> upload(
-            @RequestPart("file") MultipartFile file,
-            @RequestParam(value = "path", defaultValue = "/") String path
+            @RequestPart("file") @NotNull(message = "File cannot be null") MultipartFile file,
+            @RequestParam(value = "path", defaultValue = "/") @NotBlank(message = "Path cannot be blank") String path
path: service/src/main/java/ru/homevault/fileserver/controller/FileController.java
user: login: kosteekX3D
body: fixed
created_at: 2025-04-26T03:56:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,13 +25,12 @@ ext {
     lombokVersion = '1.18.36'
     spockFrameworkRelease = '4.0'
     spockFrameworkVersion = '2.4'
-    jakartaValidationVersion = '3.1.1'
     mapStructVersion = '1.6.2'
 }
 
 dependencies {
     implementation "org.springframework.boot:spring-boot-starter-web:$springBootVersion"
-    implementation "jakarta.validation:jakarta.validation-api:$jakartaValidationVersion"
+    implementation "org.springframework.boot:spring-boot-starter-validation:$springBootVersion" // Add this line
path: service/build.gradle
user: login: kosteekX3D
body: fixed
created_at: 2025-04-26T04:00:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -55,15 +63,34 @@ public ResponseEntity<ErrorResponse> handleNotFound(NoResourceFoundException ex)
     }
 
     @ExceptionHandler(MissingServletRequestParameterException.class)
-    public ResponseEntity<ErrorResponse> handleNotFound(MissingServletRequestParameterException ex) {
+    public ResponseEntity<ErrorResponse> handleMissingParameter(MissingServletRequestParameterException ex) {
+
         ErrorResponse error = ErrorResponse.builder()
                 .error("Missing required request parameter: " + ex.getParameterName())
                 .build();
 
         return ResponseEntity
-                .status(HttpStatus.NOT_FOUND)
+                .status(HttpStatus.BAD_REQUEST)
                 .contentType(MediaType.APPLICATION_JSON)
                 .body(error);
     }
 
+    @ExceptionHandler(ConstraintViolationException.class)
+    public ResponseEntity<Map<String, Object>> handleConstraintViolation(ConstraintViolationException ex) {
+        Map<String, String> fieldErrors = new HashMap<>();
+        ex.getConstraintViolations().forEach(violation -> {
+            String field = violation.getPropertyPath().toString();
+            fieldErrors.put(field, violation.getMessage());
+        });
+
+        Map<String, Object> errorResponse = new HashMap<>();
+        errorResponse.put("status", HttpStatus.BAD_REQUEST.value());
+        errorResponse.put("message", "Validation failed");
+        errorResponse.put("errors", fieldErrors);
+
+        return ResponseEntity
+                .status(HttpStatus.BAD_REQUEST)
+                .body(errorResponse);
path: service/src/main/java/ru/homevault/fileserver/controller/FileControllerAdvice.java
user: login: kosteekX3D
body: fixed
created_at: 2025-04-26T05:12:52Z
author_association: CONTRIBUTOR

PR comments:
user: login: kosteekX3D
created_at: 2025-04-25T10:12:44Z
body: так, выяснилось, что по какой-то неизвестной причине MethodArgumentNotValidException не вызывается, прогнать тест на глубину например.
curl -X GET "http://localhost:8080/list?path=/&depth=-1"

скорее всего мой skill issue.

https://github.com/spring-projects/spring-boot/issues/40055
author_association: CONTRIBUTOR
user: login: b4lmor
created_at: 2025-04-25T10:21:04Z
body: > так, выяснилось, что по какой-то неизвестной причине MethodArgumentNotValidException не вызывается, прогнать тест на глубину например. curl -X GET "http://localhost:8080/list?path=/&depth=-1"
> 
> скорее всего мой skill issue.
> 
> [spring-projects/spring-boot#40055](https://github.com/spring-projects/spring-boot/issues/40055)

![{DEF61416-523F-4929-9E83-A2CC6EC50EA3}](https://github.com/user-attachments/assets/d8720d76-af42-4960-9574-4bcae99ee669)

походу, там другой эксепшен выбрасывается, давай хендлить `ConstraintViolationException` вместо `MethodArgumentNotValidException `
author_association: CONTRIBUTOR
user: login: b4lmor
created_at: 2025-04-25T10:23:10Z
body: и немного по-другому ответ ожидается на ошибку передаваемых параметров.

это всё равно будет `ErrorResponse` но в message у него будет что-то по типу `parameter % must be positive. parameter % should not be empty.` (если в ошибке возвращается информация только об одном ошибочном поле - пусть в ошибке будет детали только о нем)
author_association: CONTRIBUTOR
user: login: b4lmor
created_at: 2025-04-25T10:24:55Z
body: см. класс ConstraintViolationException. Особенно метод toString(). он соберет нужное нам сообщение об ошибке, которое просто нужно будет положить в месседж

![{D894FB93-BBDA-4666-966E-9B5BEC002925}](https://github.com/user-attachments/assets/407be2d5-efa4-43a8-af25-8752007badf4)

author_association: CONTRIBUTOR
user: login: b4lmor
created_at: 2025-04-25T10:38:30Z
body: еще в чатике решили вот так сделать

![{EF614B66-296A-44E7-9D2F-B92D9761B616}](https://github.com/user-attachments/assets/829705d3-f91b-407f-aa94-57a2e941c5b2)


![{B66558B4-FC62-4CEF-8973-7ECC7E5AAB67}](https://github.com/user-attachments/assets/c1166de4-0547-42c1-9638-659b9ffcf564)


author_association: CONTRIBUTOR
user: login: kosteekX3D
created_at: 2025-04-25T10:49:16Z
body: Всё вышесказанное поправил.
author_association: CONTRIBUTOR