diff --git a/lesson_26/api/java/api_app/build.gradle.kts b/lesson_26/api/java/api_app/build.gradle.kts index a282111f8..7aba6eb1a 100644 --- a/lesson_26/api/java/api_app/build.gradle.kts +++ b/lesson_26/api/java/api_app/build.gradle.kts @@ -3,7 +3,7 @@ plugins { application eclipse id("com.diffplug.spotless") version "6.25.0" - id("org.springframework.boot") version "3.4.0" + id("org.springframework.boot") version "3.2.3" id("com.adarshr.test-logger") version "4.0.0" } @@ -21,7 +21,6 @@ dependencies { testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.assertj:assertj-core:3.26.3") testImplementation("at.favre.lib:bcrypt:0.10.2") - testImplementation("org.springframework.boot:spring-boot-starter-test") // This dependency is used by the application. implementation("com.codedifferently.instructional:instructional-lib") diff --git a/lesson_26/api/java/api_app/src/main/java/com/codedifferently/lesson26/web/MediaItemsController.java b/lesson_26/api/java/api_app/src/main/java/com/codedifferently/lesson26/web/MediaItemsController.java index bbbc45e41..1a95c700d 100644 --- a/lesson_26/api/java/api_app/src/main/java/com/codedifferently/lesson26/web/MediaItemsController.java +++ b/lesson_26/api/java/api_app/src/main/java/com/codedifferently/lesson26/web/MediaItemsController.java @@ -4,16 +4,28 @@ import com.codedifferently.lesson26.library.Library; import com.codedifferently.lesson26.library.MediaItem; import com.codedifferently.lesson26.library.search.SearchCriteria; +import jakarta.validation.Valid; import java.io.IOException; import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.UUID; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @CrossOrigin +@RequestMapping("/api") public class MediaItemsController { private final Library library; @@ -31,4 +43,54 @@ public ResponseEntity getItems() { var response = GetMediaItemsResponse.builder().items(responseItems).build(); return ResponseEntity.ok(response); } + + @GetMapping("/items/{id}") + public ResponseEntity getItemById(@PathVariable String id) { + try { + Set items = library.search(SearchCriteria.builder().id(id).build()); + if (items.isEmpty()) { + return ResponseEntity.notFound().build(); + } + MediaItemResponse response = MediaItemResponse.from(items.iterator().next()); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + @PostMapping("/items") + public ResponseEntity createItem( + @Valid @RequestBody CreateMediaItemRequest request) { + try { + MediaItem item = MediaItemRequest.asMediaItem(request.getItem()); + library.addMediaItem(item, librarian); + var response = CreateMediaItemResponse.builder().item(MediaItemResponse.from(item)).build(); + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + return ResponseEntity.badRequest().build(); + } + } + + @DeleteMapping("/items/{id}") + public ResponseEntity deleteItemById(@PathVariable String id) { + try { + UUID uuid = UUID.fromString(id); + Set items = library.search(SearchCriteria.builder().id(id).build()); + if (items.isEmpty()) { + return ResponseEntity.notFound().build(); + } + library.removeMediaItem(uuid, librarian); + return ResponseEntity.noContent().build(); + } catch (IllegalArgumentException e) { + return ResponseEntity.notFound().build(); + } + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity>> handleValidationErrors( + MethodArgumentNotValidException ex) { + List errors = + ex.getBindingResult().getFieldErrors().stream().map(FieldError::getDefaultMessage).toList(); + return ResponseEntity.badRequest().body(Map.of("errors", errors)); + } } diff --git a/lesson_26/api/java/api_app/src/test/java/com/codedifferently/lesson26/web/MediaItemsControllerTest.java b/lesson_26/api/java/api_app/src/test/java/com/codedifferently/lesson26/web/MediaItemsControllerTest.java index 1b7757b58..9ab8750dd 100644 --- a/lesson_26/api/java/api_app/src/test/java/com/codedifferently/lesson26/web/MediaItemsControllerTest.java +++ b/lesson_26/api/java/api_app/src/test/java/com/codedifferently/lesson26/web/MediaItemsControllerTest.java @@ -37,7 +37,7 @@ static void setUp(WebApplicationContext wac) { @Test void testController_getsAllItems() throws Exception { mockMvc - .perform(get("/items").contentType(MediaType.APPLICATION_JSON)) + .perform(get("/api/items").contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$.items").isArray()) .andExpect(jsonPath("$.items.length()").value(31)); @@ -47,7 +47,7 @@ void testController_getsAllItems() throws Exception { void testController_getsAnItem() throws Exception { mockMvc .perform( - get("/items/31616162-3831-3832-2d34-3334352d3465") + get("/api/items/31616162-3831-3832-2d34-3334352d3465") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()); } @@ -56,7 +56,7 @@ void testController_getsAnItem() throws Exception { void testController_returnsNotFoundOnGetItem() throws Exception { mockMvc .perform( - get("/items/00000000-0000-0000-0000-000000000000") + get("/api/items/00000000-0000-0000-0000-000000000000") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); } @@ -66,7 +66,7 @@ void testController_reportsBadRequestOnAddItem() throws Exception { String json = "{}"; mockMvc - .perform(post("/items").contentType(MediaType.APPLICATION_JSON).content(json)) + .perform(post("/api/items").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.errors").isArray()) .andExpect(jsonPath("$.errors.length()").value(1)); @@ -89,7 +89,7 @@ void testController_addsItem() throws Exception { """; mockMvc - .perform(post("/items").contentType(MediaType.APPLICATION_JSON).content(json)) + .perform(post("/api/items").contentType(MediaType.APPLICATION_JSON).content(json)) .andExpect(status().isOk()) .andExpect(jsonPath("$.item.id").value("e27a4e0d-9664-420d-955e-c0e295d0ce02")); @@ -105,7 +105,7 @@ void testController_addsItem() throws Exception { void testController_returnsNotFoundOnDeleteItem() throws Exception { mockMvc .perform( - delete("/items/00000000-0000-0000-0000-000000000000") + delete("/api/items/00000000-0000-0000-0000-000000000000") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNotFound()); } @@ -114,7 +114,7 @@ void testController_returnsNotFoundOnDeleteItem() throws Exception { void testController_deletesItem() throws Exception { mockMvc .perform( - delete("/items/32623932-6566-3364-2d62-3232342d3435") + delete("/api/items/32623932-6566-3364-2d62-3232342d3435") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isNoContent()); diff --git a/lesson_26/api/java/gradle/wrapper/gradle-wrapper.jar b/lesson_26/api/java/gradle/wrapper/gradle-wrapper.jar index a4b76b953..1b33c55ba 100644 Binary files a/lesson_26/api/java/gradle/wrapper/gradle-wrapper.jar and b/lesson_26/api/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/lesson_26/api/java/gradle/wrapper/gradle-wrapper.properties b/lesson_26/api/java/gradle/wrapper/gradle-wrapper.properties index e2847c820..ca025c83a 100644 --- a/lesson_26/api/java/gradle/wrapper/gradle-wrapper.properties +++ b/lesson_26/api/java/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/lesson_26/api/java/gradlew b/lesson_26/api/java/gradlew index f5feea6d6..23d15a936 100755 --- a/lesson_26/api/java/gradlew +++ b/lesson_26/api/java/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/lesson_26/api/java/gradlew.bat b/lesson_26/api/java/gradlew.bat index 9d21a2183..db3a6ac20 100644 --- a/lesson_26/api/java/gradlew.bat +++ b/lesson_26/api/java/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..649f8d4bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "code-differently-25-q1", + "lockfileVersion": 3, + "requires": true, + "packages": {} +}