From b8ae345c2a84f889dc3d997200978986073ebd9e Mon Sep 17 00:00:00 2001 From: Younes El Ouarti Date: Tue, 17 Dec 2024 11:04:44 +0100 Subject: [PATCH] cleaned up and comments added --- CHANGELOG.md | 3 +- build.gradle | 19 ++ .../hateoflux/http/HalListResponse.java | 183 ++++++++--- .../http/HalMultiResourceResponse.java | 170 ++++++++-- .../hateoflux/http/HalResourceResponse.java | 178 ++++++++--- .../hateoflux/http/HalResponseConfig.java | 44 --- .../http/HalResponseHandlerResultHandler.java | 214 ------------- .../hateoflux/http/HttpHeadersModule.java | 111 +++++-- .../hateoflux/http/OldHalListResponse.java | 295 ------------------ .../http/OldHalResourceResponse.java | 284 ----------------- .../hateoflux/http/OldHalResponse.java | 236 -------------- .../http/ReactiveResponseEntity.java | 14 + .../http/ReactiveResponseEntityConfig.java | 25 ++ ...iveResponseEntityHandlerResultHandler.java | 73 ++++- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- .../dummy/controller/BookController.java | 98 +----- .../hateoflux/http/HalListResponseTest.java | 264 +++++++++++----- .../http/HalMultiResourceResponseTest.java | 273 ++++++++++++++++ .../http/HalResourceResponseTest.java | 251 ++++++++++----- .../hateoflux/http/HttpHeadersModuleTest.java | 154 +++++++++ ...seHandlerResultHandlerIntegrationTest.java | 132 +------- 21 files changed, 1448 insertions(+), 1575 deletions(-) delete mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/HalResponseConfig.java delete mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/HalResponseHandlerResultHandler.java delete mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/OldHalListResponse.java delete mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/OldHalResourceResponse.java delete mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/OldHalResponse.java create mode 100644 src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityConfig.java create mode 100644 src/test/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponseTest.java create mode 100644 src/test/java/de/kamillionlabs/hateoflux/http/HttpHeadersModuleTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e4e152..20f67a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.0-SNAPSHOT] ### Added +* New `ReactiveResponseEntity` classes with its handler and autoconfiguration ### Changed -* HalResourecWrapperis#EmbeddedOriginallyAList is now private +* `HalResourecWrapperis#EmbeddedOriginallyAList` is now private ### Fixed * Single scalars (e.g. String, Integers, Boolean, etc.) are now not allowed in Wrappers and will result in an exception diff --git a/build.gradle b/build.gradle index 0bb141b..ce3cf8c 100644 --- a/build.gradle +++ b/build.gradle @@ -22,9 +22,22 @@ repositories { } tasks.named('test') { + doFirst { + // Resolve the path to the mockito-agent JAR + def mockitoAgentPath = configurations.mockitoAgent.singleFile.absolutePath + println "Using Mockito Java Agent at: ${mockitoAgentPath}" + + // Add the Java agent JVM argument + jvmArgs "-javaagent:${mockitoAgentPath}" + } + useJUnitPlatform() } +configurations { + mockitoAgent +} + dependencies { implementation 'org.springframework.boot:spring-boot-starter-json' implementation 'org.springframework:spring-webflux' @@ -43,8 +56,14 @@ dependencies { testCompileOnly 'org.projectlombok:lombok' testAnnotationProcessor 'org.projectlombok:lombok' + + // Add mockito-core to the mockitoAgent configuration without transitive dependencies + mockitoAgent 'org.mockito:mockito-core:5.14.0', { + transitive = false + } } + jar { enabled = true archiveClassifier = '' diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HalListResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/HalListResponse.java index 61f4659..c1f353d 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HalListResponse.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/HalListResponse.java @@ -1,6 +1,7 @@ package de.kamillionlabs.hateoflux.http; import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; +import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -14,7 +15,51 @@ import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; /** + * A reactive response representation for HAL resources, wrapping a {@link HalListWrapper} + * along with an HTTP status and optional HTTP headers. + * + *

This class extends {@link HttpHeadersModule} to provide fluent methods for adding HTTP headers, + * and implements {@link ReactiveResponseEntity} to allow for automatic serialization by the + * {@link ReactiveResponseEntityHandlerResultHandler}.

+ * + *

The core functionality of this class is to encapsulate a reactive HAL resource, represented as a + * {@link Mono} of {@link HalListWrapper}, and to transform it into a standard {@link ResponseEntity} + * via the {@link #toResponseEntity()} method. This enables seamless integration of HAL-based hypermedia + * resources into the reactive response handling pipeline.

+ * + *

The HTTP status is also reactive, defined as a {@link Mono} that defaults to + * {@link #DEFAULT_STATUS}. Headers can be optionally provided or constructed using this builder's methods.

+ * + *

Convenience factory methods such as {@link #ok(Mono)}, {@link #created(Mono)}, and others + * produce commonly used response configurations with specific HTTP statuses.

+ * + *

Example usage:

+ *
{@code
+ * Mono> body = ...;
+ * HalListResponse response = HalListResponse.ok(body)
+ *     .withContentType("application/hal+json")
+ *     .withETag("\"123456\"");
+ * }
+ * + *

+ * Usage Guidelines: + *

+ * + * + * @param + * the type of the resource represented by the HAL wrapper + * @param + * the type of the embedded resources represented by the HAL wrapper * @author Younes El Ouarti + * @see ReactiveResponseEntity + * @see ResponseEntity + * @see HttpHeadersModule */ public class HalListResponse extends HttpHeadersModule> @@ -23,9 +68,20 @@ public class HalListResponse private final Mono> body; private final Mono status; - private HalListResponse(Mono> body, - Mono httpStatus, - MultiValueMap headers) { + /** + * Constructs a new {@link HalListResponse} with the given body, HTTP status, and headers. + * + * @param body + * a {@link Mono} of {@link HalListResponse} representing the HAL resource body + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} to be associated with the response; defaults to + * {@link #DEFAULT_STATUS} if empty + * @param headers + * an optional set of HTTP headers + */ + public HalListResponse(Mono> body, + Mono httpStatus, + MultiValueMap headers) { this.status = httpStatus.defaultIfEmpty(DEFAULT_STATUS); this.body = body; this.headers = Optional.ofNullable(headers) @@ -44,32 +100,71 @@ public Mono> toResponseEntity() { // Static Factory Method + /** + * Creates a {@link HalListResponse} with the given HAL resource body and HTTP status provided as a + * {@link Mono}. + * + * @param body + * a {@link Mono} of {@link HalListWrapper} representing the HAL resource body + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@link HalListResponse} + */ public static HalListResponse of( @NonNull Mono> body, @NonNull Mono httpStatus) { return new HalListResponse<>(body, httpStatus, null); } + /** + * Creates a {@link HalListResponse} with an empty body and the given HTTP status provided as a {@link Mono}. + * + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@link HalListResponse} + */ public static HalListResponse of( @NonNull Mono httpStatus) { return new HalListResponse<>(Mono.empty(), httpStatus, null); } + /** + * Creates a {@link HalListResponse} with an empty body and the given {@link HttpStatus}. + * + * @param httpStatus + * the {@link HttpStatus} to use + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@link HalListResponse} + */ public static HalListResponse of( @NonNull HttpStatus httpStatus) { return new HalListResponse<>(Mono.empty(), Mono.just(httpStatus), null); } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#OK} and the given body. + * Creates a {@link HalListResponse} with a body and {@link HttpStatus#OK}. * + * @param body + * the {@link Mono} of {@link HalListWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code OK} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalListResponse ok( @NonNull Mono> body) { @@ -78,15 +173,18 @@ public static HalListResponse ok( } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#CREATED} and the given body. + * Creates a {@link HalListResponse} with a body and {@link HttpStatus#CREATED}. * + * @param body + * the {@link Mono} of {@link HalListWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code CREATED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalListResponse created( @NonNull Mono> body) { @@ -95,15 +193,18 @@ public static HalListResponse creat } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#ACCEPTED} and the given body. + * Creates a {@link HalListResponse} with a body and {@link HttpStatus#ACCEPTED}. * + * @param body + * the {@link Mono} of {@link HalListWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code ACCEPTED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalListResponse accepted( @NonNull Mono> body) { @@ -112,65 +213,65 @@ public static HalListResponse accep } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#NO_CONTENT}. + * Creates a {@link HalListResponse} with no body and {@link HttpStatus#NO_CONTENT}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code NO_CONTENT} status */ public static HalListResponse noContent() { return new HalListResponse<>(Mono.empty(), Mono.just(HttpStatus.NO_CONTENT), null); } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#BAD_REQUEST}. + * Creates a {@link HalListResponse} with no body and {@link HttpStatus#BAD_REQUEST}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code BAD_REQUEST} status */ public static HalListResponse badRequest() { return new HalListResponse<>(Mono.empty(), Mono.just(HttpStatus.BAD_REQUEST), null); } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#NOT_FOUND}. + * Creates a {@link HalListResponse} with no body and {@link HttpStatus#NOT_FOUND}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code NOT_FOUND} status */ public static HalListResponse notFound() { return new HalListResponse<>(Mono.empty(), Mono.just(HttpStatus.NOT_FOUND), null); } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#FORBIDDEN}. + * Creates a {@link HalListResponse} with no body and {@link HttpStatus#FORBIDDEN}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code FORBIDDEN} status */ public static HalListResponse forbidden() { return new HalListResponse<>(Mono.empty(), Mono.just(HttpStatus.FORBIDDEN), null); } /** - * Creates a {@link HalListResponse} with {@link HttpStatus#UNAUTHORIZED}. + * Creates a {@link HalListResponse} with no body and {@link HttpStatus#UNAUTHORIZED}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalListResponse} with {@code UNAUTHORIZED} status */ public static HalListResponse unauthorized() { return new HalListResponse<>(Mono.empty(), Mono.just(HttpStatus.UNAUTHORIZED), null); diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponse.java index e23b99b..4ef4d46 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponse.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponse.java @@ -1,5 +1,6 @@ package de.kamillionlabs.hateoflux.http; +import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -15,7 +16,53 @@ import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; /** + * A reactive response representation for multiple HAL resources, encapsulating a {@link Flux} of + * {@link HalResourceWrapper} along with an HTTP status and optional HTTP headers. + * + *

This class extends {@link HttpHeadersModule} to provide fluent methods for adding HTTP headers + * and implements {@link ReactiveResponseEntity} to allow for automatic serialization by the + * {@code ReactiveResponseEntityHandlerResultHandler}.

+ * + *

The core functionality of this class is to encapsulate multiple HAL resources, represented as a + * {@link Flux} of {@link HalResourceWrapper}, and to transform them into a standard {@link ResponseEntity} + * via the {@link #toResponseEntity()} method. This enables seamless integration of multiple HAL-based hypermedia + * resources into the reactive response handling pipeline.

+ * + *

The HTTP status is defined as an {@link HttpStatus} that defaults to {@link #DEFAULT_STATUS} if not provided. + * Headers can be optionally provided or constructed using this builder's methods.

+ * + *

Convenience factory methods such as {@link #ok(Flux)}, {@link #created(Flux)}, and others + * produce commonly used response configurations with specific HTTP statuses.

+ * + *

+ * Example usage: + *

{@code
+ * Flux> body = ...;
+ * HalMultiResourceResponse response = HalMultiResourceResponse.ok(body)
+ *     .withContentType("application/hal+json")
+ *     .withETag("\"123456\"");
+ * }
+ * + * + *

+ * Usage Guidelines: + *

+ *
    + *
  • {@link HalResourceResponse}: Use when your API endpoint returns a single + * {@link HalResourceWrapper}/li> + *
  • {@link HalMultiResourceResponse}: Use when your endpoint returns multiple + * {@link HalResourceWrapper}
  • + *
  • {@link HalListResponse}: Use when your endpoint returns a single {@link HalListWrapper}
  • + *
+ * + * @param + * the type of the resource represented by the HAL wrapper + * @param + * the type of the embedded resources represented by the HAL wrapper * @author Younes El Ouarti + * @see ReactiveResponseEntity + * @see ResponseEntity + * @see HttpHeadersModule */ public class HalMultiResourceResponse extends HttpHeadersModule> @@ -24,6 +71,17 @@ public class HalMultiResourceResponse private final Flux> body; private final HttpStatus status; + /** + * Constructs a new {@link HalMultiResourceResponse} with the given body, HTTP status, and headers. + * + * @param body + * a {@link Flux} of {@link HalResourceWrapper} representing the multiple HAL resources + * @param httpStatus + * the {@link HttpStatus} to be associated with the response; defaults to {@link #DEFAULT_STATUS} if + * {@code null} + * @param headers + * an optional set of HTTP headers; may be {@code null} + */ public HalMultiResourceResponse(Flux> body, HttpStatus httpStatus, MultiValueMap headers) { @@ -44,27 +102,63 @@ public Mono> toResponseEntity() { } // Static Factory Method + + /** + * Creates a {@link HalMultiResourceResponse} with the given HAL resource body and HTTP status. + * + * @param body + * a {@link Flux} of {@link HalResourceWrapper} representing the multiple HAL resources; must not be + * {@code null} + * @param httpStatus + * the {@link HttpStatus} to associate with the response; must not be {@code null} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@link HalMultiResourceResponse} + * + * @throws IllegalArgumentException + * if {@code body} or {@code httpStatus} is {@code null} + */ public static HalMultiResourceResponse of( @NonNull Flux> body, @NonNull HttpStatus httpStatus) { return new HalMultiResourceResponse<>(body, httpStatus, null); } + /** + * Creates a {@link HalMultiResourceResponse} with an empty body and the given HTTP status. + * + * @param httpStatus + * the {@link HttpStatus} to associate with the response; must not be {@code null} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@link HalMultiResourceResponse} + * + * @throws IllegalArgumentException + * if {@code httpStatus} is {@code null} + */ public static HalMultiResourceResponse of( @NonNull HttpStatus httpStatus) { return new HalMultiResourceResponse<>(Flux.empty(), httpStatus, null); } /** - * Creates a {@link HalMultiResourceResponse} with {@link HttpStatus#OK} and the given body. + * Creates a {@link HalMultiResourceResponse} with the given HAL resource body and {@link HttpStatus#OK}. * + * @param body + * the {@link Flux} of {@link HalResourceWrapper} representing the multiple HAL resources; must not be + * {@code null} * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code OK} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is {@code null} */ public static HalMultiResourceResponse ok( @NonNull Flux> body) { @@ -73,15 +167,19 @@ public static HalMultiResourceResponse - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code CREATED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is {@code null} */ public static HalMultiResourceResponse created( @NonNull Flux> body) { @@ -90,15 +188,19 @@ public static HalMultiResourceResponse - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code ACCEPTED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is {@code null} */ public static HalMultiResourceResponse accepted( @NonNull Flux> body) { @@ -107,26 +209,26 @@ public static HalMultiResourceResponse - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code NO_CONTENT} status */ public static HalMultiResourceResponse noContent() { return new HalMultiResourceResponse<>(Flux.empty(), HttpStatus.NO_CONTENT, null); } /** - * Creates a {@link HalMultiResourceResponse} with {@link HttpStatus#BAD_REQUEST}. + * Creates a {@link HalMultiResourceResponse} with no body and {@link HttpStatus#BAD_REQUEST}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code BAD_REQUEST} status */ public static HalMultiResourceResponse badRequest() { return new HalMultiResourceResponse<>(Flux.empty(), HttpStatus.BAD_REQUEST, null); @@ -146,26 +248,26 @@ public static HalMultiResourceResponse - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code FORBIDDEN} status */ public static HalMultiResourceResponse forbidden() { return new HalMultiResourceResponse<>(Flux.empty(), HttpStatus.FORBIDDEN, null); } /** - * Creates a {@link HalMultiResourceResponse} with {@link HttpStatus#UNAUTHORIZED}. + * Creates a {@link HalMultiResourceResponse} with no body and {@link HttpStatus#UNAUTHORIZED}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@link HalMultiResourceResponse} with {@code UNAUTHORIZED} status */ public static HalMultiResourceResponse unauthorized() { return new HalMultiResourceResponse<>(Flux.empty(), HttpStatus.UNAUTHORIZED, null); diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HalResourceResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/HalResourceResponse.java index 2d197b9..b114aea 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HalResourceResponse.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/HalResourceResponse.java @@ -1,5 +1,6 @@ package de.kamillionlabs.hateoflux.http; +import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; @@ -14,7 +15,51 @@ import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; /** + * A reactive response representation for HAL resources, wrapping a {@link HalResourceWrapper} + * along with an HTTP status and optional HTTP headers. + * + *

This class extends {@link HttpHeadersModule} to provide fluent methods for adding HTTP headers, + * and implements {@link ReactiveResponseEntity} to allow for automatic serialization by the + * {@link ReactiveResponseEntityHandlerResultHandler}.

+ * + *

The core functionality of this class is to encapsulate a reactive HAL resource, represented as a + * {@link Mono} of {@link HalResourceWrapper}, and to transform it into a standard {@link ResponseEntity} + * via the {@link #toResponseEntity()} method. This enables seamless integration of HAL-based hypermedia + * resources into the reactive response handling pipeline.

+ * + *

The HTTP status is also reactive, defined as a {@link Mono} that defaults to + * {@link #DEFAULT_STATUS}. Headers can be optionally provided or constructed using this builder's methods.

+ * + *

Convenience factory methods such as {@link #ok(Mono)}, {@link #created(Mono)}, and others + * produce commonly used response configurations with specific HTTP statuses.

+ * + *

Example usage:

+ *
{@code
+ * Mono> body = ...;
+ * HalResourceResponse response = HalResourceResponse.ok(body)
+ *     .withContentType("application/hal+json")
+ *     .withETag("\"123456\"");
+ * }
+ * + *

+ * Usage Guidelines: + *

+ *
    + *
  • {@link HalResourceResponse}: Use when your API endpoint returns a single + * {@link HalResourceWrapper}/li> + *
  • {@link HalMultiResourceResponse}: Use when your endpoint returns multiple + * {@link HalResourceWrapper}
  • + *
  • {@link HalListResponse}: Use when your endpoint returns a single {@link HalListWrapper}
  • + *
+ * + * @param + * the type of the resource represented by the HAL wrapper + * @param + * the type of the embedded resources represented by the HAL wrapper * @author Younes El Ouarti + * @see ReactiveResponseEntity + * @see ResponseEntity + * @see HttpHeadersModule */ public class HalResourceResponse extends HttpHeadersModule> @@ -23,6 +68,17 @@ public class HalResourceResponse private final Mono> body; private final Mono status; + /** + * Constructs a new {@link HalResourceResponse} with the given body, HTTP status, and headers. + * + * @param body + * a {@link Mono} of {@link HalResourceWrapper} representing the HAL resource body + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} to be associated with the response; defaults to + * {@link #DEFAULT_STATUS} if empty + * @param headers + * an optional set of HTTP headers + */ public HalResourceResponse(Mono> body, Mono httpStatus, MultiValueMap headers) { @@ -33,6 +89,7 @@ public HalResourceResponse(Mono> body, .orElse(new HttpHeaders()); } + @Override public Mono> toResponseEntity() { Mono>> reactiveResponseEntity = @@ -44,32 +101,71 @@ public Mono> toResponseEntity() { // Static Factory Method + /** + * Creates a {@link HalResourceResponse} with the given HAL resource body and HTTP status provided as a + * {@link Mono}. + * + * @param body + * a {@link Mono} of {@link HalResourceWrapper} representing the HAL resource body + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@code HalResourceResponse} + */ public static HalResourceResponse of( @NonNull Mono> body, @NonNull Mono httpStatus) { return new HalResourceResponse<>(body, httpStatus, null); } + /** + * Creates a {@link HalResourceResponse} with an empty body and the given HTTP status provided as a {@link Mono}. + * + * @param httpStatus + * a {@link Mono} of {@link HttpStatus} + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@code HalResourceResponse} + */ public static HalResourceResponse of( @NonNull Mono httpStatus) { return new HalResourceResponse<>(Mono.empty(), httpStatus, null); } + /** + * Creates a {@link HalResourceResponse} with an empty body and the given {@link HttpStatus}. + * + * @param httpStatus + * the {@link HttpStatus} to use + * @param + * the resource type + * @param + * the embedded resource type + * @return the created {@code HalResourceResponse} + */ public static HalResourceResponse of( @NonNull HttpStatus httpStatus) { return new HalResourceResponse<>(Mono.empty(), Mono.just(httpStatus), null); } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#OK} and the given body. + * Creates a {@code HalResourceResponse} with a body and {@link HttpStatus#OK}. * + * @param body + * the {@link Mono} of {@link HalResourceWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code OK} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalResourceResponse ok( @NonNull Mono> body) { @@ -78,15 +174,18 @@ public static HalResourceResponse o } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#CREATED} and the given body. + * Creates a {@code HalResourceResponse} with a body and {@link HttpStatus#CREATED}. * + * @param body + * the {@link Mono} of {@link HalResourceWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code CREATED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalResourceResponse created( @NonNull Mono> body) { @@ -95,15 +194,18 @@ public static HalResourceResponse c } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#ACCEPTED} and the given body. + * Creates a {@code HalResourceResponse} with a body and {@link HttpStatus#ACCEPTED}. * + * @param body + * the {@link Mono} of {@link HalResourceWrapper} representing the body; must not be null * @param - * resource type + * the resource type * @param - * embedded type - * @param body - * the HAL wrapper - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code ACCEPTED} status and the given body + * + * @throws IllegalArgumentException + * if {@code body} is null */ public static HalResourceResponse accepted( @NonNull Mono> body) { @@ -112,65 +214,65 @@ public static HalResourceResponse a } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#NO_CONTENT}. + * Creates a {@code HalResourceResponse} with no body and {@link HttpStatus#NO_CONTENT}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code NO_CONTENT} status */ public static HalResourceResponse noContent() { return new HalResourceResponse<>(Mono.empty(), Mono.just(HttpStatus.NO_CONTENT), null); } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#BAD_REQUEST}. + * Creates a {@code HalResourceResponse} with no body and {@link HttpStatus#BAD_REQUEST}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code BAD_REQUEST} status */ public static HalResourceResponse badRequest() { return new HalResourceResponse<>(Mono.empty(), Mono.just(HttpStatus.BAD_REQUEST), null); } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#NOT_FOUND}. + * Creates a {@code HalResourceResponse} with no body and {@link HttpStatus#NOT_FOUND}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code NOT_FOUND} status */ public static HalResourceResponse notFound() { return new HalResourceResponse<>(Mono.empty(), Mono.just(HttpStatus.NOT_FOUND), null); } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#FORBIDDEN}. + * Creates a {@code HalResourceResponse} with no body and {@link HttpStatus#FORBIDDEN}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code FORBIDDEN} status */ public static HalResourceResponse forbidden() { return new HalResourceResponse<>(Mono.empty(), Mono.just(HttpStatus.FORBIDDEN), null); } /** - * Creates a {@link HalResourceResponse} with {@link HttpStatus#UNAUTHORIZED}. + * Creates a {@code HalResourceResponse} with no body and {@link HttpStatus#UNAUTHORIZED}. * * @param - * resource type + * the resource type * @param - * embedded type - * @return the created response + * the embedded resource type + * @return a {@code HalResourceResponse} with {@code UNAUTHORIZED} status */ public static HalResourceResponse unauthorized() { return new HalResourceResponse<>(Mono.empty(), Mono.just(HttpStatus.UNAUTHORIZED), null); diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseConfig.java b/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseConfig.java deleted file mode 100644 index 1902d61..0000000 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseConfig.java +++ /dev/null @@ -1,44 +0,0 @@ -package de.kamillionlabs.hateoflux.http; - -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; -import org.springframework.context.annotation.Bean; -import org.springframework.http.ResponseEntity; -import org.springframework.web.reactive.config.WebFluxConfigurer; -import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; - -/** - * Auto-configuration class for setting up {@link OldHalResponse} handling in a Spring WebFlux application. This - * configuration automatically integrates with Spring's WebFlux infrastructure by importing - * {@link WebFluxAutoConfiguration} and implementing {@link WebFluxConfigurer}. - * - * @see HalResponseHandlerResultHandler - * @see WebFluxAutoConfiguration - * @see WebFluxConfigurer - */ -@AutoConfiguration -@ImportAutoConfiguration(WebFluxAutoConfiguration.class) -public class HalResponseConfig implements WebFluxConfigurer { - - /** - * Creates a {@link HalResponseHandlerResultHandler} bean that enables proper serialization of - * {@link OldHalResponse} - * instances into {@link ResponseEntity} objects that Spring's response handling mechanism can process. - * - * @param responseEntityHandler - * the {@link ResponseEntityResultHandler} to delegate to after converting the {@link OldHalResponse} - * @return a new {@link HalResponseHandlerResultHandler} instance - */ - @Bean - public HalResponseHandlerResultHandler halResponseHandlerResultHandler( - ResponseEntityResultHandler responseEntityHandler) { - return new HalResponseHandlerResultHandler(responseEntityHandler); - } - - @Bean - public ReactiveResponseEntityHandlerResultHandler reactiveResponseEntityHandlerResultHandler( - ResponseEntityResultHandler responseEntityHandler) { - return new ReactiveResponseEntityHandlerResultHandler(responseEntityHandler); - } -} diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseHandlerResultHandler.java b/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseHandlerResultHandler.java deleted file mode 100644 index de25a3b..0000000 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HalResponseHandlerResultHandler.java +++ /dev/null @@ -1,214 +0,0 @@ -package de.kamillionlabs.hateoflux.http; - -import org.reactivestreams.Publisher; -import org.springframework.core.MethodParameter; -import org.springframework.core.Ordered; -import org.springframework.core.ResolvableType; -import org.springframework.http.ResponseEntity; -import org.springframework.web.reactive.HandlerResult; -import org.springframework.web.reactive.HandlerResultHandler; -import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; -import org.springframework.web.server.ServerWebExchange; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -import java.lang.reflect.Method; - -/** - * A specialized {@link HandlerResultHandler} implementation that processes HAL (Hypertext Application Language) - * responses - * in a Spring WebFlux application. This handler is responsible for converting {@link OldHalResponse} objects and their - * publisher variants into appropriate {@link ResponseEntity} instances that can be handled by Spring's standard - * response handling mechanism. - * - *

The handler supports both direct {@link OldHalResponse} objects and reactive types ({@link Publisher}, - * {@link Mono}, {@link Flux}) that contain {@link OldHalResponse} objects. It delegates the actual response - * handling to Spring's {@link ResponseEntityResultHandler} after performing the necessary conversions. - * - *

This handler is ordered with a priority of -99 to ensure it runs before standard Spring handlers - * (which typically have order 0) while still allowing for custom handlers to take precedence if needed. - * - * @author Younes El Ouarti - * @see HandlerResultHandler - * @see ResponseEntityResultHandler - * @see OldHalResponse - */ -public class HalResponseHandlerResultHandler implements HandlerResultHandler, Ordered { - - /** - * The order value for this handler. Set to -99 to run before standard Spring handlers (order 0) - * while still allowing custom handlers to take precedence if needed. - * - * @see Ordered - */ - int ORDER = -99; - - private final ResponseEntityResultHandler responseEntityHandler; - - private final MethodParameter responseEntityReturnType; - - /** - * Constructs a new {@link HalResponseHandlerResultHandler} with the specified {@link ResponseEntityResultHandler}. - * - * @param responseEntityHandler - * the Spring response entity handler to delegate to after conversion - */ - public HalResponseHandlerResultHandler(ResponseEntityResultHandler responseEntityHandler) { - this.responseEntityHandler = responseEntityHandler; - try { - Method methodWithResponseEntityAsReturnType = ResponseEntityTypeProvider.class - .getMethod("returnResponseEntity"); - int returnTypeIndex = -1; - this.responseEntityReturnType = new MethodParameter(methodWithResponseEntityAsReturnType, returnTypeIndex); - } catch (NoSuchMethodException e) { - throw new IllegalStateException("Failed to find dummy method for ResponseEntity return type", e); - } - } - - /** - * Determines whether this handler can process the given result. - * - *

This method returns true if: - *

    - *
  • The result is a direct {@link OldHalResponse} instance
  • - *
  • The result is a {@link Publisher} (including {@link Mono} or {@link Flux}) containing - * {@link OldHalResponse} instances
  • - *
- * - * @param result - * the handler result to check - * @return {@code true} if this handler can process the result; {@code false} otherwise - */ - @Override - public boolean supports(HandlerResult result) { - ResolvableType returnType = result.getReturnType(); - Class rawClassOfReturnType = returnType.toClass(); - - // Direct HalResponse? - if (OldHalResponse.class.isAssignableFrom(rawClassOfReturnType)) { - return true; - } - - // Check if it's a Publisher - if (Publisher.class.isAssignableFrom(rawClassOfReturnType)) { - ResolvableType genericType = returnType.getGeneric(0); - Class genericClass = genericType.toClass(); - return OldHalResponse.class.isAssignableFrom(genericClass); - } - - return false; - } - - /** - * Handles the result by converting {@link OldHalResponse} instances to {@link ResponseEntity} instances and - * delegating - * to the {@link ResponseEntityResultHandler}. - * - *

This method handles: - *

    - *
  • Direct {@link OldHalResponse} instances by converting them to {@link ResponseEntity}
  • - *
  • {@link Publisher} instances containing {@link OldHalResponse} objects by converting the stream - * to contain {@link ResponseEntity} objects
  • - *
- * - * @param exchange - * the current server exchange - * @param result - * the handler result to process - * @return a {@code Mono<@link Void>} that completes when handling is complete - * - * @throws IllegalStateException - * if the handler cannot process the given result type - */ - @Override - public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { - Object returnValue = result.getReturnValue(); - - if (returnValue instanceof OldHalResponse halResponse) { - return handleHalResponse(exchange, result, halResponse); - } - - if (returnValue instanceof Publisher publisher) { - return handlePublisher(exchange, result, publisher); - } - - return Mono.error(new IllegalStateException( - "HalResponseHandlerResultHandler cannot handle result: " + result.getReturnValue())); - } - - private Mono handleHalResponse(ServerWebExchange exchange, HandlerResult result, - OldHalResponse halResponse) { - // Unwrap to ResponseEntity - ResponseEntity responseEntity = halResponse.toResponseEntity(); - - HandlerResult delegateResult = new HandlerResult( - result.getHandler(), //Controller method that returns the HalResponse which triggered all this - responseEntity, - //we have to fake the return value type because the real one is a HalResponse - this.responseEntityReturnType - ); - - return responseEntityHandler.handleResult(exchange, delegateResult); - } - - private Mono handlePublisher(ServerWebExchange exchange, HandlerResult result, Publisher publisher) { - ResolvableType publisherReturnType = result.getReturnType(); - ResolvableType genericTypeInPublisher = publisherReturnType.getGeneric(0); - - if (OldHalResponse.class.isAssignableFrom(genericTypeInPublisher.toClass())) { - Publisher> responseEntityPublisher = Flux.from(publisher) - .cast(OldHalResponse.class) - .map(OldHalResponse::toResponseEntity); - - Object concreteResponseEntityPublisher; - Class publisherClass = publisherReturnType.toClass(); - // Create concrete publisher - if (Mono.class.isAssignableFrom(publisherClass)) { - concreteResponseEntityPublisher = Mono.from(responseEntityPublisher); - } else { - concreteResponseEntityPublisher = Flux.from(responseEntityPublisher); - } - - // Act as if the "Publisher method(...)" was "actually Publisher method(...)" - // ^^^^^^^^^^^ ^^^^^^^^^^^^^^ - HandlerResult delegateResult = new HandlerResult( - // Controller method that returns the Publisher which triggered all this - result.getHandler(), - // Mono or Flux - concreteResponseEntityPublisher, - // We have to fake the return value type because the real one is a HalResponse - this.responseEntityReturnType - ); - - return responseEntityHandler.handleResult(exchange, delegateResult); - } - - return Mono.error(new IllegalStateException( - "HalResponseHandlerResultHandler cannot handle publisher of type: " + publisher)); - } - - /** - * Returns the order value of this handler. - * - * @return -99, indicating this handler should run before standard Spring handlers - */ - @Override - public int getOrder() { - return ORDER; - } - - /** - * Internal helper class used to obtain a {@link MethodParameter} instance for {@link ResponseEntity} return type. - * This is used for type resolution when delegating to the {@link ResponseEntityResultHandler}. - */ - private static class ResponseEntityTypeProvider { - /** - * Dummy method used to obtain {@link ResponseEntity} return type information. - * - * @return null (this method is never actually called) - */ - public ResponseEntity returnResponseEntity() { - return null; - } - } -} diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/HttpHeadersModule.java b/src/main/java/de/kamillionlabs/hateoflux/http/HttpHeadersModule.java index 6c98d46..4212b77 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/HttpHeadersModule.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/HttpHeadersModule.java @@ -11,80 +11,139 @@ import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; /** + * A fluent builder for {@link HttpHeaders} that allows for the convenient addition of common HTTP headers. + * + *

This module provides methods to add headers such as {@code Content-Type}, {@code Location}, and {@code ETag}. + * It utilizes method chaining to enable a readable and concise way of constructing {@link HttpHeaders} instances.

+ * + *

The class is generic, allowing subclasses to return their own type from the builder methods, facilitating + * extensibility and customization.

+ * + *

Example usage:

+ *
{@code
+ * HttpHeaders headers = new HttpHeadersModule<>()
+ *     .withContentType(MediaType.APPLICATION_JSON)
+ *     .withLocation(new URI("http://example.com/resource"))
+ *     .withETag("\"123456\"");
+ * }
+ * + * @param + * the type of the concrete subclass extending this module * @author Younes El Ouarti + * @see HttpHeaders */ public class HttpHeadersModule> { protected HttpHeaders headers; + /** + * Adds a new header with the specified name and values. + * + *

If the header already exists, the new values are appended to the existing ones.

+ * + * @param name + * the name of the header to add; must not be {@code null} or empty + * @param values + * the values of the header + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code name} is {@code null} or empty + */ public HttpHeadersModuleT withHeader(@NonNull String name, String... values) { putNewHeader(name, values); return (HttpHeadersModuleT) this; } + /** + * Adds a {@code Content-Type} header with the specified {@link MediaType}. + * + * @param mediaType + * the media type to set; must not be {@code null} + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code mediaType} is {@code null} or empty + */ public HttpHeadersModuleT withContentType(@NonNull MediaType mediaType) { addContentType(mediaType.toString()); return (HttpHeadersModuleT) this; } + /** + * Adds a {@code Content-Type} header with the specified media type string. + * + * @param mediaType + * the media type string to set; must not be {@code null} or empty + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code mediaType} is {@code null} or empty + */ public HttpHeadersModuleT withContentType(@NonNull String mediaType) { addContentType(mediaType); return (HttpHeadersModuleT) this; } + /** + * Adds a {@code Location} header with the specified {@link URI}. + * + * @param location + * the URI to set as the location; must not be {@code null} or empty + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code location} is {@code null} or empty + */ public HttpHeadersModuleT withLocation(@NonNull URI location) { addLocation(location.toString()); return (HttpHeadersModuleT) this; } + /** + * Adds a {@code Location} header with the specified location string. + * + * @param location + * the location string to set; must not be {@code null} or empty + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code location} is {@code null} or empty + */ public HttpHeadersModuleT withLocation(@NonNull String location) { addLocation(location); return (HttpHeadersModuleT) this; } + /** + * Adds an {@code ETag} header with the specified ETag value. + * + * @param eTag + * the ETag value to set; must not be {@code null} or empty + * @return the current instance for method chaining + * + * @throws IllegalArgumentException + * if {@code eTag} is {@code null} or empty + */ public HttpHeadersModuleT withETag(@NonNull String eTag) { addETag(eTag); return (HttpHeadersModuleT) this; } - /** - * Creates a new headers map with Content-Type header added. - * - * @param mediaType - * the media type to set - * @throws IllegalArgumentException - * if mediaType is null - */ protected void addContentType(@NonNull String mediaType) { Assert.notNull(mediaType, valueNotAllowedToBeNull("MediaType")); - Assert.notNull(mediaType, valueNotAllowedToBeEmpty("MediaType")); + Assert.isTrue(!mediaType.isBlank(), valueNotAllowedToBeEmpty("MediaType")); putNewHeader(HttpHeaders.CONTENT_TYPE, mediaType); } - /** - * Creates a new headers map with Location header added. - * - * @param location - * the location URI - * @throws IllegalArgumentException - * if location is null - */ protected void addLocation(@NonNull String location) { Assert.notNull(location, valueNotAllowedToBeNull("Location URI")); - Assert.notNull(location, valueNotAllowedToBeEmpty("Location URI")); + Assert.isTrue(!location.isBlank(), valueNotAllowedToBeEmpty("Location URI")); putNewHeader(HttpHeaders.LOCATION, location); } - /** - * Creates a new headers map with ETag header added. - * - * @param eTag - * the ETag value - * @throws IllegalArgumentException - * if etag is null or empty - */ protected void addETag(@NonNull String eTag) { Assert.notNull(eTag, valueNotAllowedToBeNull("ETag")); Assert.isTrue(!eTag.isBlank(), valueNotAllowedToBeEmpty("ETag")); diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalListResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/OldHalListResponse.java deleted file mode 100644 index 2d49f86..0000000 --- a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalListResponse.java +++ /dev/null @@ -1,295 +0,0 @@ -package de.kamillionlabs.hateoflux.http; - -import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.NonNull; -import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; - -import java.net.URI; -import java.util.function.Function; - -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeEmpty; -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; - -/** - * Equivalent of a specialized {@link ResponseEntity} and a concrete implementation of {@link OldHalResponse} that is - * able - * to hold an instance of {@link HalListWrapper}. - * - * @param - * the type of the primary resource - * @param - * the type of embedded resources - */ -@Value -@EqualsAndHashCode(callSuper = true) -public class OldHalListResponse extends OldHalResponse> { - - /** - * Creates a new {@link OldHalListResponse} with the given status code. - * - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalListResponse(@NonNull HttpStatusCode status) { - super(status); - } - - /** - * Creates a new {@link OldHalListResponse} with the given body and status code. - * - * @param body - * the HAL list wrapper that holds the response content - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalListResponse(HalListWrapper body, @NonNull HttpStatusCode status) { - super(body, status); - } - - /** - * Creates a new {@link OldHalListResponse} with the given headers and status code. - * - * @param headers - * the HTTP response headers - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalListResponse(MultiValueMap headers, @NonNull HttpStatusCode status) { - super(headers, status); - } - - /** - * Creates a new {@link OldHalListResponse} with the given body, headers, and raw status code. - * - * @param body - * the HAL list wrapper that holds the response content - * @param headers - * the HTTP response headers - * @param rawStatus - * the HTTP status code as an integer - * @throws IllegalArgumentException - * if the status code is not a valid HTTP status - */ - public OldHalListResponse(HalListWrapper body, MultiValueMap headers, - int rawStatus) { - super(body, headers, rawStatus); - } - - /** - * Creates a new {@link OldHalListResponse} with the given body, headers, and status code. - * - * @param body - * the HAL list wrapper that holds the response content - * @param headers - * the HTTP response headers - * @param statusCode - * the HTTP status code - * @throws IllegalArgumentException - * if status code is null - */ - public OldHalListResponse(HalListWrapper body, MultiValueMap headers, - @NonNull HttpStatusCode statusCode) { - super(body, headers, statusCode); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#OK} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL list wrapper - * @return the created response - */ - public static OldHalListResponse ok( - @NonNull HalListWrapper body) { - Assert.notNull(body, valueNotAllowedToBeNull("Body")); - return new OldHalListResponse<>(body, createHeaders(), HttpStatus.OK); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#CREATED} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL list wrapper - * @return the created response - */ - public static OldHalListResponse created( - @NonNull HalListWrapper body) { - Assert.notNull(body, valueNotAllowedToBeNull("Body")); - return new OldHalListResponse<>(body, createHeaders(), HttpStatus.CREATED); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#ACCEPTED} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL list wrapper - * @return the created response - */ - public static OldHalListResponse accepted( - @NonNull HalListWrapper body) { - Assert.notNull(body, valueNotAllowedToBeNull("Body")); - return new OldHalListResponse<>(body, createHeaders(), HttpStatus.ACCEPTED); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#NO_CONTENT}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalListResponse noContent() { - return new OldHalListResponse<>(createHeaders(), HttpStatus.NO_CONTENT); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#BAD_REQUEST}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalListResponse badRequest() { - return new OldHalListResponse<>(createHeaders(), HttpStatus.BAD_REQUEST); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#NOT_FOUND}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalListResponse notFound() { - return new OldHalListResponse<>(createHeaders(), HttpStatus.NOT_FOUND); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#FORBIDDEN}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalListResponse forbidden() { - return new OldHalListResponse<>(createHeaders(), HttpStatus.FORBIDDEN); - } - - /** - * Creates a {@link OldHalListResponse} with {@link HttpStatus#UNAUTHORIZED}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalListResponse unauthorized() { - return new OldHalListResponse<>(createHeaders(), HttpStatus.UNAUTHORIZED); - } - - /** - * Sets the Content-Type header for this response. - * - * @param mediaType - * the media type to set - * @return a new response instance with the updated Content-Type header - * - * @throws IllegalArgumentException - * if mediaType is null - */ - public OldHalListResponse contentType(@NonNull MediaType mediaType) { - Assert.notNull(mediaType, valueNotAllowedToBeNull("MediaType")); - return new OldHalListResponse<>(this.halWrapper, withContentType(mediaType), this.httpStatusCode); - } - - /** - * Sets the Location header for this response. - * - * @param location - * the location URI - * @return a new response instance with the updated Location header - * - * @throws IllegalArgumentException - * if location is null - */ - public OldHalListResponse location(@NonNull URI location) { - Assert.notNull(location, valueNotAllowedToBeNull("Location URI")); - return new OldHalListResponse<>(this.halWrapper, withLocation(location), this.httpStatusCode); - } - - /** - * Sets the ETag header for this response. - * - * @param etag - * the ETag value - * @return a new response instance with the updated ETag header - * - * @throws IllegalArgumentException - * if etag is null or empty - */ - public OldHalListResponse eTag(@NonNull String etag) { - Assert.notNull(etag, valueNotAllowedToBeNull("ETag")); - Assert.isTrue(!etag.isBlank(), valueNotAllowedToBeEmpty("ETag")); - return new OldHalListResponse<>(this.halWrapper, withETag(etag), this.httpStatusCode); - } - - /** - * Transforms this response's body using the provided mapping function while preserving - * the HTTP status and headers. - * - * @param - * the new resource type - * @param - * the new embedded type - * @param mapper - * the function to transform the body - * @return a new response with the transformed body - * - * @throws IllegalArgumentException - * if mapper is null - */ - public OldHalListResponse map( - @NonNull Function, - HalListWrapper> mapper) { - Assert.notNull(mapper, valueNotAllowedToBeNull("Mapper function")); - return new OldHalListResponse<>( - this.halWrapper != null ? mapper.apply(this.halWrapper) : null, - this.headers, - this.httpStatusCode - ); - } -} diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResourceResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResourceResponse.java deleted file mode 100644 index 635776d..0000000 --- a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResourceResponse.java +++ /dev/null @@ -1,284 +0,0 @@ -package de.kamillionlabs.hateoflux.http; - -import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; -import lombok.EqualsAndHashCode; -import lombok.Value; -import org.springframework.http.HttpStatus; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.NonNull; -import org.springframework.util.Assert; -import org.springframework.util.MultiValueMap; - -import java.net.URI; -import java.util.function.Function; - -/** - * Equivalent of a specialized {@link ResponseEntity} and a concrete implementation of {@link OldHalResponse} that is - * able - * to hold an instance of {@link HalResourceWrapper}. - * - * @param - * the type of the primary resource - * @param - * the type of embedded resources - */ -@Value -@EqualsAndHashCode(callSuper = true) -public class OldHalResourceResponse extends OldHalResponse> { - - /** - * Creates a new {@link OldHalResourceResponse} with the given status code. - * - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResourceResponse(@NonNull HttpStatusCode status) { - super(status); - } - - /** - * Creates a new {@link OldHalResourceResponse} with the given body and status code. - * - * @param body - * the HAL resource wrapper that holds the response content - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResourceResponse(HalResourceWrapper body, @NonNull HttpStatusCode status) { - super(body, status); - } - - /** - * Creates a new {@link OldHalResourceResponse} with the given headers and status code. - * - * @param headers - * the HTTP response headers - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResourceResponse(MultiValueMap headers, @NonNull HttpStatusCode status) { - super(headers, status); - } - - /** - * Creates a new {@link OldHalResourceResponse} with the given body, headers, and raw status code. - * - * @param body - * the HAL resource wrapper that holds the response content - * @param headers - * the HTTP response headers - * @param rawStatus - * the HTTP status code as an integer - */ - public OldHalResourceResponse(HalResourceWrapper body, MultiValueMap headers, - int rawStatus) { - super(body, headers, rawStatus); - } - - /** - * Creates a new {@link OldHalResourceResponse} with the given body, headers, and status code. - * - * @param body - * the HAL resource wrapper that holds the response content - * @param headers - * the HTTP response headers - * @param statusCode - * the HTTP status code - * @throws IllegalArgumentException - * if status code is null - */ - public OldHalResourceResponse(HalResourceWrapper body, MultiValueMap headers, - @NonNull HttpStatusCode statusCode) { - super(body, headers, statusCode); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#OK} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL resource wrapper - * @return the created response - */ - public static OldHalResourceResponse ok( - HalResourceWrapper body) { - return new OldHalResourceResponse<>(body, createHeaders(), HttpStatus.OK); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#CREATED} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL resource wrapper - * @return the created response - */ - public static OldHalResourceResponse created( - HalResourceWrapper body) { - return new OldHalResourceResponse<>(body, createHeaders(), HttpStatus.CREATED); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#ACCEPTED} and the given body. - * - * @param - * resource type - * @param - * embedded type - * @param body - * the HAL resource wrapper - * @return the created response - */ - public static OldHalResourceResponse accepted( - HalResourceWrapper body) { - return new OldHalResourceResponse<>(body, createHeaders(), HttpStatus.ACCEPTED); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#NO_CONTENT}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalResourceResponse noContent() { - return new OldHalResourceResponse<>(createHeaders(), HttpStatus.NO_CONTENT); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#BAD_REQUEST}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalResourceResponse badRequest() { - return new OldHalResourceResponse<>(createHeaders(), HttpStatus.BAD_REQUEST); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#NOT_FOUND}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalResourceResponse notFound() { - return new OldHalResourceResponse<>(createHeaders(), HttpStatus.NOT_FOUND); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#FORBIDDEN}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalResourceResponse forbidden() { - return new OldHalResourceResponse<>(createHeaders(), HttpStatus.FORBIDDEN); - } - - /** - * Creates a {@link OldHalResourceResponse} with {@link HttpStatus#UNAUTHORIZED}. - * - * @param - * resource type - * @param - * embedded type - * @return the created response - */ - public static OldHalResourceResponse unauthorized() { - return new OldHalResourceResponse<>(createHeaders(), HttpStatus.UNAUTHORIZED); - } - - /** - * Sets the Content-Type header for this response. - * - * @param mediaType - * the media type to set - * @return a new response instance with the updated Content-Type header - * - * @throws IllegalArgumentException - * if mediaType is null - */ - public OldHalResourceResponse contentType(@NonNull MediaType mediaType) { - return new OldHalResourceResponse<>(this.halWrapper, withContentType(mediaType), this.httpStatusCode); - } - - /** - * Sets the Location header for this response. - * - * @param location - * the location URI - * @return a new response instance with the updated Location header - * - * @throws IllegalArgumentException - * if location is null - */ - public OldHalResourceResponse location(@NonNull URI location) { - return new OldHalResourceResponse<>(this.halWrapper, withLocation(location), this.httpStatusCode); - } - - /** - * Sets the ETag header for this response. - * - * @param etag - * the ETag value - * @return a new response instance with the updated ETag header - * - * @throws IllegalArgumentException - * if etag is null or empty - */ - public OldHalResourceResponse eTag(@NonNull String etag) { - return new OldHalResourceResponse<>(this.halWrapper, withETag(etag), this.httpStatusCode); - } - - /** - * Transforms this response's body using the provided mapping function while preserving - * the HTTP status and headers. - * - * @param - * the new resource type - * @param - * the new embedded type - * @param mapper - * the function to transform the body - * @return a new response with the transformed body - * - * @throws IllegalArgumentException - * if mapper is null - */ - public OldHalResourceResponse map( - @NonNull Function, - HalResourceWrapper> mapper) { - Assert.notNull(mapper, "Mapper function must not be null"); - return new OldHalResourceResponse<>( - this.halWrapper != null ? mapper.apply(this.halWrapper) : null, - this.headers, - this.httpStatusCode - ); - } -} diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResponse.java b/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResponse.java deleted file mode 100644 index bd41fef..0000000 --- a/src/main/java/de/kamillionlabs/hateoflux/http/OldHalResponse.java +++ /dev/null @@ -1,236 +0,0 @@ -package de.kamillionlabs.hateoflux.http; - -import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; -import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; -import de.kamillionlabs.hateoflux.model.hal.HalWrapper; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatusCode; -import org.springframework.http.MediaType; -import org.springframework.http.ResponseEntity; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; - -import java.net.URI; - -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeEmpty; -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; - -/** - * Abstract base class for HAL (Hypertext Application Language) responses in a Spring WebFlux application. - * This class serves as a specialized alternative to {@link ResponseEntity} for HAL-compliant responses, - * wrapping {@link HalWrapper} instances to provide proper HTTP response handling. - * - *

Similar to {@link ResponseEntity}, this class allows setting HTTP headers and status codes while - * ensuring proper serialization of HAL responses. The wrapped {@link HalWrapper} instance handles the - * HAL-specific structure including links and embedded resources. - * - * @param - * the type of {@link HalWrapper} being wrapped in this response - * @see HalWrapper - * @see HalResourceWrapper - * @see OldHalListResponse - * @see HalListWrapper - * @see ResponseEntity - */ -@Getter -@EqualsAndHashCode -public abstract class OldHalResponse> { - - /** - * The HAL wrapper containing the response content. - */ - protected HalWrapperT halWrapper; - - /** - * HTTP headers for the response. - */ - protected MultiValueMap headers; - - /** - * The HTTP status code for the response. - */ - protected HttpStatusCode httpStatusCode; - - /** - * Creates a new {@link OldHalResponse} with the given status code. - * - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResponse(@NonNull HttpStatusCode status) { - this(null, null, status); - } - - /** - * Creates a new {@link OldHalResponse} with the given body and status code. - * - * @param body - * the HAL wrapper body - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResponse(@Nullable HalWrapperT body, @NonNull HttpStatusCode status) { - this(body, null, status); - } - - /** - * Creates a new {@link OldHalResponse} with the given headers and status code. - * - * @param headers - * the HTTP headers - * @param status - * the HTTP status code - * @throws IllegalArgumentException - * if status is null - */ - public OldHalResponse(MultiValueMap headers, @NonNull HttpStatusCode status) { - this(null, headers, status); - } - - /** - * Creates a new {@link OldHalResponse} with the given body, headers, and raw status code. - * - * @param body - * the HAL wrapper body - * @param headers - * the HTTP headers - * @param rawStatus - * the HTTP status code as an integer - */ - public OldHalResponse(@Nullable HalWrapperT body, @Nullable MultiValueMap headers, int rawStatus) { - this(body, headers, HttpStatusCode.valueOf(rawStatus)); - } - - /** - * Creates a new {@link OldHalResponse} with the given body, headers, and status code. - * - * @param body - * the HAL wrapper body - * @param headers - * the HTTP headers - * @param statusCode - * the HTTP status code - * @throws IllegalArgumentException - * IllegalArgumentException if status code is null - */ - public OldHalResponse(@Nullable HalWrapperT body, @Nullable MultiValueMap headers, - @NonNull HttpStatusCode statusCode) { - Assert.notNull(statusCode, valueNotAllowedToBeNull("HttpStatusCode")); - this.halWrapper = body; - this.headers = headers; - this.httpStatusCode = statusCode; - } - - /** - * Converts this {@link OldHalResponse} into a {@link ResponseEntity}. - * - * @return a {@link ResponseEntity} representing this response - */ - public ResponseEntity toResponseEntity() { - return new ResponseEntity<>(halWrapper, headers, httpStatusCode); - } - - /** - * Helper method to create and validate headers for responses. - * - * @return empty headers map - */ - protected static MultiValueMap createHeaders() { - return new LinkedMultiValueMap<>(); - } - - /** - * Helper method to validate status code. - * - * @param status - * the status to validate - * @throws IllegalArgumentException - * if status is null - */ - protected static void validateStatus(@NonNull HttpStatusCode status) { - Assert.notNull(status, valueNotAllowedToBeNull("HttpStatusCode")); - } - - /** - * Helper method to validate headers map. - * - * @param headers - * the headers to validate - * @throws IllegalArgumentException - * if headers is null - */ - protected static void validateHeaders(@NonNull MultiValueMap headers) { - Assert.notNull(headers, valueNotAllowedToBeNull("Headers")); - } - - /** - * Creates a new headers map with Content-Type header added. - * - * @param mediaType - * the media type to set - * @return new headers map with the Content-Type header - * - * @throws IllegalArgumentException - * if mediaType is null - */ - protected MultiValueMap withContentType(@NonNull MediaType mediaType) { - Assert.notNull(mediaType, valueNotAllowedToBeNull("MediaType")); - MultiValueMap newHeaders = new LinkedMultiValueMap<>(); - if (this.headers != null) { - newHeaders.putAll(this.headers); - } - newHeaders.set(HttpHeaders.CONTENT_TYPE, mediaType.toString()); - return newHeaders; - } - - /** - * Creates a new headers map with Location header added. - * - * @param location - * the location URI - * @return new headers map with the Location header - * - * @throws IllegalArgumentException - * if location is null - */ - protected MultiValueMap withLocation(@NonNull URI location) { - Assert.notNull(location, valueNotAllowedToBeNull("Location URI")); - MultiValueMap newHeaders = new LinkedMultiValueMap<>(); - if (this.headers != null) { - newHeaders.putAll(this.headers); - } - newHeaders.set(HttpHeaders.LOCATION, location.toString()); - return newHeaders; - } - - /** - * Creates a new headers map with ETag header added. - * - * @param etag - * the ETag value - * @return new headers map with the ETag header - * - * @throws IllegalArgumentException - * if etag is null or empty - */ - protected MultiValueMap withETag(@NonNull String etag) { - Assert.notNull(etag, valueNotAllowedToBeNull("ETag")); - Assert.isTrue(!etag.isBlank(), valueNotAllowedToBeEmpty("ETag")); - MultiValueMap newHeaders = new LinkedMultiValueMap<>(); - if (this.headers != null) { - newHeaders.putAll(this.headers); - } - newHeaders.set(HttpHeaders.ETAG, etag); - return newHeaders; - } -} - diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntity.java b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntity.java index e75c140..0750c3b 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntity.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntity.java @@ -5,12 +5,26 @@ import reactor.core.publisher.Mono; /** + * Represents a reactive response entity that can be converted into a standard {@link ResponseEntity}. + * + *

Implementations of this interface enable custom response types to be serialized automatically + * by the {@link ReactiveResponseEntityHandlerResultHandler}. This facilitates seamless integration + * of custom reactive responses within the application's response handling pipeline.

+ * * @author Younes El Ouarti */ public interface ReactiveResponseEntity { HttpStatus DEFAULT_STATUS = HttpStatus.OK; + /** + * Converts this reactive response entity into a standard {@link ResponseEntity}. + * + *

This method encapsulates the logic required to transform the custom reactive response + * into a format that can be understood and processed by Spring's response handling mechanisms.

+ * + * @return a {@link Mono} emitting the corresponding {@link ResponseEntity} + */ Mono> toResponseEntity(); } diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityConfig.java b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityConfig.java new file mode 100644 index 0000000..ec5ed44 --- /dev/null +++ b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityConfig.java @@ -0,0 +1,25 @@ +package de.kamillionlabs.hateoflux.http; + +import org.springframework.boot.autoconfigure.AutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.config.WebFluxConfigurer; +import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler; + +/** + * Auto-configuration class for enabling serialization of {@link ReactiveResponseEntity} instances. This config adds the + * {@link ReactiveResponseEntityHandlerResultHandler} to enable the serialization of {@link ReactiveResponseEntity} + * + * @author Younes El Ouarti + */ +@AutoConfiguration +@ImportAutoConfiguration(WebFluxAutoConfiguration.class) +public class ReactiveResponseEntityConfig implements WebFluxConfigurer { + + @Bean + public ReactiveResponseEntityHandlerResultHandler reactiveResponseEntityHandlerResultHandler( + ResponseEntityResultHandler responseEntityHandler) { + return new ReactiveResponseEntityHandlerResultHandler(responseEntityHandler); + } +} diff --git a/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityHandlerResultHandler.java b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityHandlerResultHandler.java index dcfda7c..98b47cb 100644 --- a/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityHandlerResultHandler.java +++ b/src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityHandlerResultHandler.java @@ -13,6 +13,25 @@ import java.lang.reflect.Method; +/** + * A custom {@link HandlerResultHandler} that enables the serialization of {@link ReactiveResponseEntity} + * instances by delegating their handling to a standard {@link ResponseEntityResultHandler}. + * + *

This handler checks whether the return value of a controller method is an instance of + * {@link ReactiveResponseEntity} (or a publisher wrapping one). If so, it converts the + * {@link ReactiveResponseEntity} into a {@link ResponseEntity} and delegates to the provided + * {@link ResponseEntityResultHandler} to handle the actual response rendering. + * + *

This ensures that custom reactive response entity types can be seamlessly integrated into the existing Spring + * WebFlux response pipeline without requiring manual conversion by the controller developer. + * + *

If a publisher of {@link ReactiveResponseEntity} is encountered, an error is returned, as nesting a reactive type + * within another publisher is not supported by this handler.

+ * + * @author Younes El Ouarti + * @see ReactiveResponseEntity + * @see ResponseEntityResultHandler + */ public class ReactiveResponseEntityHandlerResultHandler implements HandlerResultHandler, Ordered { int ORDER = -99; @@ -21,6 +40,17 @@ public class ReactiveResponseEntityHandlerResultHandler implements HandlerResult private final MethodParameter responseEntityReturnType; + /** + * Constructs a new {@link ReactiveResponseEntityHandlerResultHandler} that delegates to the provided + * {@link ResponseEntityResultHandler} after converting {@link ReactiveResponseEntity} objects to standard + * {@link ResponseEntity} instances. + * + * @param responseEntityHandler + * the {@link ResponseEntityResultHandler} used to handle standard {@link ResponseEntity} instances + * @throws IllegalStateException + * if reflection fails to find the dummy method used to create a {@link MethodParameter} for + * {@link ResponseEntity} + */ public ReactiveResponseEntityHandlerResultHandler(ResponseEntityResultHandler responseEntityHandler) { this.responseEntityHandler = responseEntityHandler; try { @@ -33,6 +63,17 @@ public ReactiveResponseEntityHandlerResultHandler(ResponseEntityResultHandler re } } + /** + * Determines whether this handler should process the given {@link HandlerResult}. + * + *

This handler supports {@link ReactiveResponseEntity} only. {@code Publisher} are also + * caught, but will result in a {@link IllegalStateException} later during {@link #handleResult}: + * + * @param result + * the {@link HandlerResult} to inspect + * @return {@code true} if the result is a {@link ReactiveResponseEntity} or a publisher of one; + * {@code false} otherwise + */ @Override public boolean supports(HandlerResult result) { ResolvableType returnType = result.getReturnType(); @@ -57,6 +98,20 @@ public boolean supports(HandlerResult result) { return false; } + /** + * Handles the given {@link HandlerResult} by converting any {@link ReactiveResponseEntity} to a standard + * {@link ResponseEntity} and delegating to the configured {@link ResponseEntityResultHandler}. + * + *

If the return value is a direct {@link ReactiveResponseEntity}, it is converted and handled. + * If it's a {@code Publisher}, an error is returned. + * + * @param exchange + * the current server exchange + * @param result + * the handler result containing the controller method's return value + * @return a {@link Mono} that completes when the response handling is finished or with an error + * if the return value is not supported + */ @Override public Mono handleResult(ServerWebExchange exchange, HandlerResult result) { Object returnValue = result.getReturnValue(); @@ -85,10 +140,11 @@ private Mono handleReactiveResponseEntity(ServerWebExchange exchange, Hand Mono> responseEntity = reactiveResponseEntity.toResponseEntity(); HandlerResult delegateResult = new HandlerResult( - result.getHandler(), //Controller method that returns the ReactiveResponseEntity which triggered all - // this + //Controller method that returns the ReactiveResponseEntity which triggered all this + result.getHandler(), + //Unwrapped ResponsEntity from the custom Responses responseEntity, - //we have to fake the return value type because the real one is a HalResponse + //We have to fake the return value type because the real one is a descendant of ReactiveResponseEntity this.responseEntityReturnType ); @@ -97,7 +153,7 @@ private Mono handleReactiveResponseEntity(ServerWebExchange exchange, Hand /** - * Returns the order value of this handler. + * Returns the order value of this handler. A lower value means higher priority. * * @return -99, indicating this handler should run before standard Spring handlers */ @@ -107,14 +163,15 @@ public int getOrder() { } /** - * Internal helper class used to obtain a {@link MethodParameter} instance for {@link ResponseEntity} return type. - * This is used for type resolution when delegating to the {@link ResponseEntityResultHandler}. + * Internal helper class used solely to obtain a {@link MethodParameter} instance + * for {@link ResponseEntity} return type information. */ private static class ResponseEntityTypeProvider { /** - * Dummy method used to obtain {@link ResponseEntity} return type information. + * A dummy method used to retrieve a {@link Method} representing a {@link ResponseEntity} return type. + * This method is never actually called, it's just used for reflection to obtain type information. * - * @return null (this method is never actually called) + * @return null (this method is never executed) */ public ResponseEntity returnResponseEntity() { return null; diff --git a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index a63c567..551f617 100644 --- a/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1 @@ -de.kamillionlabs.hateoflux.http.HalResponseConfig \ No newline at end of file +de.kamillionlabs.hateoflux.http.ReactiveResponseEntityConfig \ No newline at end of file diff --git a/src/test/java/de/kamillionlabs/hateoflux/dummy/controller/BookController.java b/src/test/java/de/kamillionlabs/hateoflux/dummy/controller/BookController.java index 3857466..94368f6 100644 --- a/src/test/java/de/kamillionlabs/hateoflux/dummy/controller/BookController.java +++ b/src/test/java/de/kamillionlabs/hateoflux/dummy/controller/BookController.java @@ -21,17 +21,14 @@ import de.kamillionlabs.hateoflux.dummy.TestDataGenerator; import de.kamillionlabs.hateoflux.dummy.TestDataGenerator.AuthorName; import de.kamillionlabs.hateoflux.dummy.TestDataGenerator.BookTitle; -import de.kamillionlabs.hateoflux.dummy.model.Author; import de.kamillionlabs.hateoflux.dummy.model.Book; import de.kamillionlabs.hateoflux.http.HalListResponse; import de.kamillionlabs.hateoflux.http.HalMultiResourceResponse; import de.kamillionlabs.hateoflux.http.HalResourceResponse; -import de.kamillionlabs.hateoflux.model.hal.HalEmbeddedWrapper; import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; import de.kamillionlabs.hateoflux.model.link.Link; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -39,7 +36,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.net.URI; import java.util.List; /** @@ -66,46 +62,17 @@ public HalResourceResponse teapotEndpoint() { return HalResourceResponse.of(HttpStatus.I_AM_A_TEAPOT); } - @GetMapping("/get-book-found") + @GetMapping("/get-book") public HalResourceResponse getBookFound() { Book book = testData.getBookByTitle(BookTitle.EFFECTIVE_JAVA); HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) .withLinks(Link.linkAsSelfOf("/book/effective-java")); - HalResourceResponse response = HalResourceResponse.ok(Mono.just(bookWrapper)); - return response; - } - - @GetMapping("/get-book-not-found") - public HalResourceResponse getBookNotFound() { - return HalResourceResponse.notFound(); - } - - @GetMapping("/get-book-with-author") - public HalResourceResponse getBookWithAuthor() { - Book book = testData.getBookByTitle(BookTitle.EFFECTIVE_JAVA); - Author author = testData.getAuthorByName(AuthorName.JOSHUA_BLOCH); - - HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) - .withEmbeddedResource(HalEmbeddedWrapper.wrap(author)) - .withLinks( - Link.linkAsSelfOf("/book/effective-java") - ); - return HalResourceResponse.ok(Mono.just(bookWrapper)); } - @GetMapping("/get-book-created") - public HalResourceResponse getBookCreated() { - Book book = testData.getBookByTitle(BookTitle.CLEAN_CODE); - HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) - .withLinks(Link.linkAsSelfOf("/book/clean-code")); - - return HalResourceResponse.created(Mono.just(bookWrapper)) - .withLocation(URI.create("/book/clean-code")); - } - @GetMapping("/get-books-by-author") + @GetMapping("/get-list-of-books") public HalListResponse getBooksByAuthor() { List books = testData.getAllBooksByAuthorName(AuthorName.ERICH_GAMMA); @@ -122,31 +89,8 @@ public HalListResponse getBooksByAuthor() { return HalListResponse.ok(Mono.just(listWrapper)); } - @GetMapping("/get-empty-book-list") - public HalListResponse getEmptyBookList() { - return HalListResponse.noContent(); - } - - @GetMapping("/get-book-list-with-authors") - public HalListResponse getBookListWithAuthors() { - List books = testData.getAllBooksByAuthorName(AuthorName.BRIAN_GOETZ); - Author author = testData.getAuthorByName(AuthorName.BRIAN_GOETZ); - - List> bookWrappers = books.stream() - .map(book -> HalResourceWrapper.wrap(book) - .withEmbeddedResource(HalEmbeddedWrapper.wrap(author)) - .withLinks(Link.linkAsSelfOf("/book/" + book.getTitle().toLowerCase().replace(" ", "-")))) - .toList(); - HalListWrapper listWrapper = HalListWrapper.wrap(bookWrappers) - .withLinks( - Link.linkAsSelfOf("/books/brian-goetz") - ); - - return HalListResponse.ok(Mono.just(listWrapper)); - } - - @GetMapping("/get-book-accepted") + @GetMapping("/get-book-with-header") public HalResourceResponse getBookAccepted() { Book book = testData.getBookByTitle(BookTitle.REFACTORING); HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) @@ -155,39 +99,9 @@ public HalResourceResponse getBookAccepted() { return HalResourceResponse.accepted(Mono.just(bookWrapper)); } - @GetMapping("/get-book-bad-request") - public HalResourceResponse getBookBadRequest() { - return HalResourceResponse.badRequest(); - } - - @GetMapping("/get-book-forbidden") - public HalResourceResponse getBookForbidden() { - return HalResourceResponse.forbidden(); - } - - @GetMapping("/get-book-unauthorized") - public HalResourceResponse getBookUnauthorized() { - return HalResourceResponse.unauthorized(); - } - - @GetMapping("/get-book-with-etag") - public HalResourceResponse getBookWithETag() { - Book book = testData.getBookByTitle(BookTitle.CLEAN_CODE); - HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) - .withLinks(Link.linkAsSelfOf("/book/clean-code")); - - return HalResourceResponse.ok(Mono.just(bookWrapper)) - .withETag("\"1234\"") - .withContentType(MediaType.APPLICATION_JSON); - } - - @GetMapping("/get-book-non-reactive") - public HalResourceResponse getBookNonReactive() { - Book book = testData.getBookByTitle(BookTitle.EFFECTIVE_JAVA); - HalResourceWrapper bookWrapper = HalResourceWrapper.wrap(book) - .withLinks(Link.linkAsSelfOf("/book/effective-java")); - - return HalResourceResponse.ok(Mono.just(bookWrapper)); + @GetMapping("/wrapped-halresponse") + public Mono> getWrappedResponse() { + return Mono.just(HalResourceResponse.noContent()); } diff --git a/src/test/java/de/kamillionlabs/hateoflux/http/HalListResponseTest.java b/src/test/java/de/kamillionlabs/hateoflux/http/HalListResponseTest.java index 2fe2f72..a607eac 100644 --- a/src/test/java/de/kamillionlabs/hateoflux/http/HalListResponseTest.java +++ b/src/test/java/de/kamillionlabs/hateoflux/http/HalListResponseTest.java @@ -1,111 +1,231 @@ package de.kamillionlabs.hateoflux.http; -import de.kamillionlabs.hateoflux.dummy.TestDataGenerator; -import de.kamillionlabs.hateoflux.dummy.model.Author; -import de.kamillionlabs.hateoflux.dummy.model.Book; -import de.kamillionlabs.hateoflux.model.hal.HalEmbeddedWrapper; import de.kamillionlabs.hateoflux.model.hal.HalListWrapper; -import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Mono; -import java.net.URI; -import java.util.List; - -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; -/** - * @author Younes El Ouarti - */ class HalListResponseTest { - private Author author; - private List> bookWithAuthorWrappers; - private HalListWrapper listWrapper; + // GIVEN constants for testing + private final HalListWrapper mockWrapper = mock(HalListWrapper.class); + private final Mono> mockBody = Mono.just(mockWrapper); + private final Mono conflictStatus = Mono.just(HttpStatus.CONFLICT); + private final HttpHeaders headers = new HttpHeaders(); + + + @Test + void givenBodyAndStatus_whenConstructed_thenCorrectValuesAssigned() { + // GIVEN + HalListResponse response = new HalListResponse<>(mockBody, conflictStatus, null); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenBodyAndStatus_whenOfCalled_thenCorrectValuesAssigned() { + // GIVEN + HalListResponse response = HalListResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenEmptyBody_whenToResponseEntity_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalListResponse response = new HalListResponse<>(Mono.empty(), conflictStatus, headers); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenOnlyMonoStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalListResponse response = HalListResponse.of(conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenOnlyStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalListResponse response = HalListResponse.of(HttpStatus.CONFLICT); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - @BeforeEach - void setUp() { - TestDataGenerator testData = new TestDataGenerator(); - author = testData.getAuthorByName(TestDataGenerator.AuthorName.ERICH_GAMMA); - List books = testData.getAllBooksByAuthorName(TestDataGenerator.AuthorName.ERICH_GAMMA); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } - List> bookWrappers = books.stream() - .map(HalResourceWrapper::wrap) - .toList(); + @Test + void givenFactoryMethodOk_whenCalled_thenCreatesResponseWithOkStatus() { + // GIVEN + HalListResponse response = HalListResponse.ok(mockBody); - bookWithAuthorWrappers = books.stream() - .map(book -> HalResourceWrapper.wrap(book) - .withEmbeddedResource(HalEmbeddedWrapper.wrap(author))) - .toList(); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - listWrapper = HalListWrapper.wrap(bookWrappers); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test - void givenStatusOnly_whenCreatingResponse_thenStatusIsSet() { - OldHalListResponse response = new OldHalListResponse<>(HttpStatus.OK); + void givenFactoryMethodUnauthorized_whenCalled_thenCreatesResponseWithUnauthorizedStatus() { + // GIVEN + HalListResponse response = HalListResponse.unauthorized(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHalWrapper()).isNull(); - assertThat(response.getHeaders()).isNull(); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test - void givenWrapper_whenCallingOk_thenCreateOkResponse() { - OldHalListResponse response = OldHalListResponse.ok(listWrapper); - - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHalWrapper()).isEqualTo(listWrapper); - assertThat(response.getHalWrapper().getResourceList()).hasSize(3); - assertThat(response.getHalWrapper().getResourceList().get(0).getResource().getAuthor()) - .isEqualTo("Erich Gamma"); - assertThat(response.getHeaders()).isNotNull(); + void givenFactoryMethodForbidden_whenCalled_thenCreatesResponseWithForbiddenStatus() { + // GIVEN + HalListResponse response = HalListResponse.forbidden(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test - void whenCallingNoContent_thenCreateNoContentResponse() { - OldHalListResponse response = OldHalListResponse.noContent(); + void givenFactoryMethodNotFound_whenCalled_thenCreatesResponseWithNotFoundStatus() { + // GIVEN + HalListResponse response = HalListResponse.notFound(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); - assertThat(response.getHalWrapper()).isNull(); - assertThat(response.getHeaders()).isNotNull(); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); } @Test - void givenExistingResponse_whenAddingLocation_thenCreateNewResponseWithLocation() { - URI location = URI.create("/authors/erich-gamma/books"); - OldHalListResponse response = OldHalListResponse.ok(listWrapper) - .location(location); + void givenFactoryMethodBadRequest_whenCalled_thenCreatesResponseWithBadRequestStatus() { + // GIVEN + HalListResponse response = HalListResponse.badRequest(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHeaders().getFirst(HttpHeaders.LOCATION)) - .isEqualTo(location.toString()); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); } @Test - void givenResponse_whenMapping_thenCreateNewResponseWithMappedContent() { - OldHalListResponse response = OldHalListResponse.ok(listWrapper); - - OldHalListResponse mappedResponse = response.map(wrapper -> - HalListWrapper.wrap(bookWithAuthorWrappers)); - - assertThat(mappedResponse.getHttpStatusCode()).isEqualTo(response.getHttpStatusCode()); - assertThat(mappedResponse.getHeaders()).isEqualTo(response.getHeaders()); - assertThat(mappedResponse.getHalWrapper().getResourceList()).hasSize(3); - assertThat(mappedResponse.getHalWrapper().getResourceList().get(0) - .getRequiredEmbedded().get(0).getEmbeddedResource().getMainGenre()) - .isEqualTo("Software Architecture"); + void givenFactoryMethodOfWithBodyAndStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalListResponse response = HalListResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenNullMapper_whenMapping_thenThrowException() { - OldHalListResponse response = OldHalListResponse.ok(listWrapper); + void givenFactoryMethodOfWithOnlyStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalListResponse response = HalListResponse.of(conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenNoContentFactory_whenCalled_thenCreatesResponseWithNoContentStatus() { + // GIVEN + HalListResponse response = HalListResponse.noContent(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + void givenFactoryMethodCreated_whenCalled_thenCreatesResponseWithCreatedStatus() { + // GIVEN + HalListResponse response = HalListResponse.created(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED); + } + + @Test + void givenFactoryMethodAccepted_whenCalled_thenCreatesResponseWithAcceptedStatus() { + // GIVEN + HalListResponse response = HalListResponse.accepted(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThatThrownBy(() -> response.map(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining(valueNotAllowedToBeNull("Mapper function")); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); } } diff --git a/src/test/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponseTest.java b/src/test/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponseTest.java new file mode 100644 index 0000000..2eadb47 --- /dev/null +++ b/src/test/java/de/kamillionlabs/hateoflux/http/HalMultiResourceResponseTest.java @@ -0,0 +1,273 @@ +package de.kamillionlabs.hateoflux.http; + +import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; + +class HalMultiResourceResponseTest { + + // GIVEN constants for testing + private final HalResourceWrapper mockWrapper = mock(HalResourceWrapper.class); + private final Flux> mockBody = Flux.just(mockWrapper); + private final HttpStatus conflictStatus = HttpStatus.CONFLICT; + private final HttpHeaders headers = new HttpHeaders(); + + @Test + void givenBodyAndStatus_whenConstructed_thenCorrectValuesAssigned() { + // GIVEN + HalMultiResourceResponse response = new HalMultiResourceResponse<>(mockBody, conflictStatus, + null); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenBodyAndStatus_whenOfCalled_thenCorrectValuesAssigned() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenEmptyBody_whenToResponseEntity_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalMultiResourceResponse response = new HalMultiResourceResponse<>(Flux.empty(), + conflictStatus, headers); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenOnlyMonoStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.of(conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenOnlyStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.of(HttpStatus.CONFLICT); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenFactoryMethodOk_whenCalled_thenCreatesResponseWithOkStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.ok(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void givenFactoryMethodUnauthorized_whenCalled_thenCreatesResponseWithUnauthorizedStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.unauthorized(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); + } + + @Test + void givenFactoryMethodForbidden_whenCalled_thenCreatesResponseWithForbiddenStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.forbidden(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + } + + @Test + void givenFactoryMethodNotFound_whenCalled_thenCreatesResponseWithNotFoundStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.notFound(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void givenFactoryMethodBadRequest_whenCalled_thenCreatesResponseWithBadRequestStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.badRequest(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void givenFactoryMethodOfWithBodyAndStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenFactoryMethodOfWithOnlyStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.of(conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenNoContentFactory_whenCalled_thenCreatesResponseWithNoContentStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.noContent(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux body = (Flux) entity.getBody(); + Mono hasElements = body.hasElements(); + assertEquals(Boolean.FALSE, hasElements.block()); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + void givenFactoryMethodCreated_whenCalled_thenCreatesResponseWithCreatedStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.created(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED); + } + + @Test + void givenFactoryMethodAccepted_whenCalled_thenCreatesResponseWithAcceptedStatus() { + // GIVEN + HalMultiResourceResponse response = HalMultiResourceResponse.accepted(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNotNull(); + Flux> body = (Flux) entity.getBody(); + assertThat(body.blockFirst()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); + } +} diff --git a/src/test/java/de/kamillionlabs/hateoflux/http/HalResourceResponseTest.java b/src/test/java/de/kamillionlabs/hateoflux/http/HalResourceResponseTest.java index 3780dd5..9eedc60 100644 --- a/src/test/java/de/kamillionlabs/hateoflux/http/HalResourceResponseTest.java +++ b/src/test/java/de/kamillionlabs/hateoflux/http/HalResourceResponseTest.java @@ -1,124 +1,229 @@ package de.kamillionlabs.hateoflux.http; -import de.kamillionlabs.hateoflux.dummy.TestDataGenerator; -import de.kamillionlabs.hateoflux.dummy.model.Author; -import de.kamillionlabs.hateoflux.dummy.model.Book; -import de.kamillionlabs.hateoflux.model.hal.HalEmbeddedWrapper; import de.kamillionlabs.hateoflux.model.hal.HalResourceWrapper; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; +import org.springframework.http.ResponseEntity; +import reactor.core.publisher.Mono; -import java.net.URI; - -import static de.kamillionlabs.hateoflux.utility.ValidationMessageTemplates.valueNotAllowedToBeNull; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; +import static org.mockito.Mockito.mock; class HalResourceResponseTest { - private Author author; - private HalResourceWrapper bookWrapper; + private final HalResourceWrapper mockWrapper = mock(HalResourceWrapper.class); + private final Mono> mockBody = Mono.just(mockWrapper); + private final Mono conflictStatus = Mono.just(HttpStatus.CONFLICT); + private final HttpHeaders headers = new HttpHeaders(); + + @Test + void givenBodyAndStatus_whenConstructed_thenCorrectValuesAssigned() { + // GIVEN + HalResourceResponse response = new HalResourceResponse<>(mockBody, conflictStatus, null); - @BeforeEach - void setUp() { - TestDataGenerator testData = new TestDataGenerator(); - Book book = testData.getBookByTitle(TestDataGenerator.BookTitle.EFFECTIVE_JAVA); - author = testData.getAuthorByName(TestDataGenerator.AuthorName.JOSHUA_BLOCH); - bookWrapper = HalResourceWrapper.wrap(book); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenStatusOnly_whenCreatingResponse_thenStatusIsSet() { - OldHalResourceResponse response = new OldHalResourceResponse<>(HttpStatus.OK); + void givenBodyAndStatus_whenOfCalled_thenCorrectValuesAssigned() { + // GIVEN + HalResourceResponse response = HalResourceResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHalWrapper()).isNull(); - assertThat(response.getHeaders()).isNull(); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenHeadersAndStatus_whenCreatingResponse_thenBothAreSet() { - MultiValueMap headers = new LinkedMultiValueMap<>(); - headers.add("Test-Header", "test-value"); + void givenEmptyBody_whenToResponseEntity_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalResourceResponse response = new HalResourceResponse<>(Mono.empty(), conflictStatus, headers); - OldHalResourceResponse response = new OldHalResourceResponse<>(headers, HttpStatus.OK); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHeaders()).isEqualTo(headers); - assertThat(response.getHalWrapper()).isNull(); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenWrapperAndStatus_whenCreatingResponse_thenBothAreSet() { - OldHalResourceResponse response = new OldHalResourceResponse<>(bookWrapper, HttpStatus.OK); + void givenOnlyMonoStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.of(conflictStatus); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHalWrapper()).isEqualTo(bookWrapper); - assertThat(response.getHalWrapper().getResource().getTitle()).isEqualTo("Effective Java"); - assertThat(response.getHeaders()).isNull(); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenNullStatus_whenCreatingResponse_thenThrowException() { - assertThatThrownBy(() -> new OldHalResourceResponse(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining(valueNotAllowedToBeNull("HttpStatusCode")); + void givenOnlyStatus_whenOfCalled_thenReturnsResponseWithOnlyHeadersAndStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.of(HttpStatus.CONFLICT); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); } @Test - void givenWrapper_whenCallingOk_thenCreateOkResponse() { - OldHalResourceResponse response = OldHalResourceResponse.ok(bookWrapper); + void givenFactoryMethodOk_whenCalled_thenCreatesResponseWithOkStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.ok(mockBody); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getHalWrapper()).isEqualTo(bookWrapper); - assertThat(response.getHalWrapper().getResource().getAuthor()).isEqualTo("Joshua Bloch"); - assertThat(response.getHeaders()).isNotNull(); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test - void givenWrapper_whenCallingCreated_thenCreateCreatedResponse() { - OldHalResourceResponse response = OldHalResourceResponse.created(bookWrapper); + void givenFactoryMethodUnauthorized_whenCalled_thenCreatesResponseWithUnauthorizedStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.unauthorized(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.CREATED); - assertThat(response.getHalWrapper()).isEqualTo(bookWrapper); - assertThat(response.getHalWrapper().getResource().getIsbn()).isEqualTo("978-0134685991"); - assertThat(response.getHeaders()).isNotNull(); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); } @Test - void whenCallingNotFound_thenCreateNotFoundResponse() { - OldHalResourceResponse response = OldHalResourceResponse.notFound(); + void givenFactoryMethodForbidden_whenCalled_thenCreatesResponseWithForbiddenStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.forbidden(); - assertThat(response.getHttpStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(response.getHalWrapper()).isNull(); - assertThat(response.getHeaders()).isNotNull(); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test - void givenExistingResponse_whenAddingLocation_thenCreateNewResponseWithLocation() { - URI location = URI.create("/books/effective-java"); - OldHalResourceResponse response = OldHalResourceResponse.created(bookWrapper) - .location(location); + void givenFactoryMethodNotFound_whenCalled_thenCreatesResponseWithNotFoundStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.notFound(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Test + void givenFactoryMethodBadRequest_whenCalled_thenCreatesResponseWithBadRequestStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.badRequest(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + } + + @Test + void givenFactoryMethodOfWithBodyAndStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalResourceResponse response = HalResourceResponse.of(mockBody, conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenFactoryMethodOfWithOnlyStatus_whenCalled_thenCreatesResponseCorrectly() { + // GIVEN + HalResourceResponse response = HalResourceResponse.of(conflictStatus); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + } + + @Test + void givenNoContentFactory_whenCalled_thenCreatesResponseWithNoContentStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.noContent(); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); + + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isNull(); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + } + + @Test + void givenFactoryMethodCreated_whenCalled_thenCreatesResponseWithCreatedStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.created(mockBody); + + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(response.getHeaders().getFirst(HttpHeaders.LOCATION)) - .isEqualTo(location.toString()); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.CREATED); } @Test - void givenResponse_whenMapping_thenCreateNewResponseWithMappedContent() { - OldHalResourceResponse response = OldHalResourceResponse.ok(bookWrapper); + void givenFactoryMethodAccepted_whenCalled_thenCreatesResponseWithAcceptedStatus() { + // GIVEN + HalResourceResponse response = HalResourceResponse.accepted(mockBody); - OldHalResourceResponse mappedResponse = response.map(wrapper -> - wrapper.withEmbeddedResource(HalEmbeddedWrapper.wrap(author))); + // WHEN + ResponseEntity entity = response.toResponseEntity().block(); - assertThat(mappedResponse.getHttpStatusCode()).isEqualTo(response.getHttpStatusCode()); - assertThat(mappedResponse.getHeaders()).isEqualTo(response.getHeaders()); - assertThat(mappedResponse.getHalWrapper().getEmbedded()).isPresent(); - assertThat(mappedResponse.getHalWrapper().getRequiredEmbedded().get(0).getEmbeddedResource().getName()) - .isEqualTo("Joshua Bloch"); + // THEN + assertThat(entity).isNotNull(); + assertThat(entity.getBody()).isEqualTo(mockWrapper); + assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED); } } \ No newline at end of file diff --git a/src/test/java/de/kamillionlabs/hateoflux/http/HttpHeadersModuleTest.java b/src/test/java/de/kamillionlabs/hateoflux/http/HttpHeadersModuleTest.java new file mode 100644 index 0000000..403e986 --- /dev/null +++ b/src/test/java/de/kamillionlabs/hateoflux/http/HttpHeadersModuleTest.java @@ -0,0 +1,154 @@ +package de.kamillionlabs.hateoflux.http; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; + +import java.net.URI; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class HttpHeadersModuleTest { + + private static class TestHttpHeadersModule extends HttpHeadersModule { + } + + @Test + void givenValidHeader_whenWithHeader_thenHeaderAdded() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN + module.withHeader("X-Test-Header", "value1", "value2"); + + // THEN + assertThat(module.headers).isNotNull(); + assertThat(module.headers.get("X-Test-Header")) + .containsExactly("value1", "value2"); + } + + @Test + void givenValidContentType_whenWithContentType_thenContentTypeAdded() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN + module.withContentType(MediaType.APPLICATION_JSON); + + // THEN + assertThat(module.headers).isNotNull(); + assertThat(module.headers.get(HttpHeaders.CONTENT_TYPE)) + .containsExactly(MediaType.APPLICATION_JSON_VALUE); + } + + @Test + void givenValidLocation_whenWithLocation_thenLocationAdded() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + URI location = URI.create("http://example.com/resource"); + + // WHEN + module.withLocation(location); + + // THEN + assertThat(module.headers).isNotNull(); + assertThat(module.headers.get(HttpHeaders.LOCATION)) + .containsExactly(location.toString()); + } + + @Test + void givenValidETag_whenWithETag_thenETagAdded() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN + module.withETag("\"12345\""); + + // THEN + assertThat(module.headers).isNotNull(); + assertThat(module.headers.get(HttpHeaders.ETAG)).containsExactly("\"12345\""); + } + + @Test + void givenNullMediaType_whenAddContentType_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withContentType((String) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("MediaType is not allowed to be null"); + } + + @Test + void givenEmptyMediaType_whenAddContentType_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withContentType("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("MediaType is not allowed to be empty"); + } + + @Test + void givenNullLocation_whenAddLocation_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withLocation((String) null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location URI is not allowed to be null"); + } + + @Test + void givenEmptyLocation_whenAddLocation_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withLocation("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Location URI is not allowed to be empty"); + } + + @Test + void givenNullETag_whenAddETag_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withETag(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("ETag is not allowed to be null"); + } + + @Test + void givenEmptyETag_whenAddETag_thenThrowsException() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + // WHEN & THEN + assertThatThrownBy(() -> module.withETag("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("ETag is not allowed to be empty"); + } + + + @Test + void givenAllMethodsAtOnes_whenAllWithMethodsExecuted_thenChainingReturnsCorrectType() { + // GIVEN + TestHttpHeadersModule module = new TestHttpHeadersModule(); + + //THEN type is correct and doesnt result in compile errors + TestHttpHeadersModule testHttpHeadersModule = module + //WHEN + .withHeader("X-Test-Header", "value1", "value2") + .withContentType("application/json") + .withLocation("http://example.com/resource") + .withETag("\"12345\""); + } +} + diff --git a/src/test/java/de/kamillionlabs/hateoflux/integrationtest/HalResponseHandlerResultHandlerIntegrationTest.java b/src/test/java/de/kamillionlabs/hateoflux/integrationtest/HalResponseHandlerResultHandlerIntegrationTest.java index e908e79..d3508eb 100644 --- a/src/test/java/de/kamillionlabs/hateoflux/integrationtest/HalResponseHandlerResultHandlerIntegrationTest.java +++ b/src/test/java/de/kamillionlabs/hateoflux/integrationtest/HalResponseHandlerResultHandlerIntegrationTest.java @@ -1,6 +1,6 @@ package de.kamillionlabs.hateoflux.integrationtest; -import de.kamillionlabs.hateoflux.http.HalResponseConfig; +import de.kamillionlabs.hateoflux.http.ReactiveResponseEntityConfig; import org.json.JSONArray; import org.junit.jupiter.api.Test; import org.skyscreamer.jsonassert.JSONAssert; @@ -19,7 +19,7 @@ classes = IntegrationTestConfiguration.class ) @AutoConfigureWebTestClient -@Import({HalResponseConfig.class}) +@Import({ReactiveResponseEntityConfig.class}) class HalResponseHandlerResultHandlerIntegrationTest { @Autowired @@ -38,7 +38,7 @@ void givenNoContent_whenCallingTeapotEndpoint_thenReturnTeapotStatus() { @Test void givenExistingBook_whenCallingGetBookFound_thenReturnBookWithHalResponse() { webTestClient.get() - .uri("/book/get-book-found") + .uri("/book/get-book") .exchange() .expectStatus().isOk() .expectBody() @@ -47,44 +47,11 @@ void givenExistingBook_whenCallingGetBookFound_thenReturnBookWithHalResponse() { .jsonPath("$._links.self.href").isEqualTo("/book/effective-java"); } - @Test - void givenNoBook_whenCallingGetBookNotFound_thenReturnNotFoundStatus() { - webTestClient.get() - .uri("/book/get-book-not-found") - .exchange() - .expectStatus().isNotFound() - .expectBody() - .isEmpty(); - } - - @Test - void givenBookWithAuthor_whenCallingGetBookWithAuthor_thenReturnBookAndEmbeddedAuthor() { - webTestClient.get() - .uri("/book/get-book-with-author") - .exchange() - .expectStatus().isOk() - .expectBody() - .jsonPath("$.title").isEqualTo("Effective Java") - .jsonPath("$._embedded.author.name").isEqualTo("Joshua Bloch") - .jsonPath("$._links.self.href").isEqualTo("/book/effective-java"); - } - - @Test - void givenNewBook_whenCallingGetBookCreated_thenReturnCreatedStatusAndLocation() { - webTestClient.get() - .uri("/book/get-book-created") - .exchange() - .expectStatus().isCreated() - .expectHeader().valueEquals("Location", "/book/clean-code") - .expectBody() - .jsonPath("$.title").isEqualTo("Clean Code") - .jsonPath("$._links.self.href").isEqualTo("/book/clean-code"); - } @Test void givenBooksCollection_whenCallingGetBooksByAuthor_thenReturnHalListResponse() { webTestClient.get() - .uri("/book/get-books-by-author") + .uri("/book/get-list-of-books") .exchange() .expectStatus().isOk() .expectBody() @@ -95,32 +62,11 @@ void givenBooksCollection_whenCallingGetBooksByAuthor_thenReturnHalListResponse( .jsonPath("$._links.self.href").isEqualTo("/books/erich-gamma"); } - @Test - void givenNoBooks_whenCallingGetEmptyBookList_thenReturnNoContent() { - webTestClient.get() - .uri("/book/get-empty-book-list") - .exchange() - .expectStatus().isNoContent() - .expectBody() - .isEmpty(); - } - - @Test - void givenBooksWithAuthor_whenCallingGetBookListWithAuthors_thenReturnHalListWithEmbeddedAuthors() { - webTestClient.get() - .uri("/book/get-book-list-with-authors") - .exchange() - .expectStatus().isOk() - .expectBody() - .jsonPath("$._embedded.customBooks.length()").isEqualTo(2) - .jsonPath("$._embedded.customBooks[0]._embedded.author.name").isEqualTo("Brian Goetz") - .jsonPath("$._embedded.customBooks[0]._links.self.href").exists(); - } @Test - void givenPendingBook_whenCallingGetBookAccepted_thenReturnAcceptedStatus() { + void givenResponseWithHttpHeaderAccepted_whenCallingGetBookAccepted_thenReturnAcceptedStatus() { webTestClient.get() - .uri("/book/get-book-accepted") + .uri("/book/get-book-with-header") .exchange() .expectStatus().isAccepted() .expectBody() @@ -128,62 +74,6 @@ void givenPendingBook_whenCallingGetBookAccepted_thenReturnAcceptedStatus() { .jsonPath("$._links.self.href").isEqualTo("/book/refactoring"); } - @Test - void givenInvalidRequest_whenCallingGetBookBadRequest_thenReturnBadRequestStatus() { - webTestClient.get() - .uri("/book/get-book-bad-request") - .exchange() - .expectStatus().isBadRequest() - .expectBody() - .isEmpty(); - } - - @Test - void givenUnauthorizedAccess_whenCallingGetBookForbidden_thenReturnForbiddenStatus() { - webTestClient.get() - .uri("/book/get-book-forbidden") - .exchange() - .expectStatus().isForbidden() - .expectBody() - .isEmpty(); - } - - @Test - void givenUnauthenticatedAccess_whenCallingGetBookUnauthorized_thenReturnUnauthorizedStatus() { - webTestClient.get() - .uri("/book/get-book-unauthorized") - .exchange() - .expectStatus().isUnauthorized() - .expectBody() - .isEmpty(); - } - - @Test - void givenBookWithMetadata_whenCallingGetBookWithETag_thenReturnBookWithETagAndContentType() { - webTestClient.get() - .uri("/book/get-book-with-etag") - .exchange() - .expectStatus().isOk() - .expectHeader().valueEquals("ETag", "\"1234\"") - .expectHeader().contentType(MediaType.APPLICATION_JSON) - .expectBody() - .jsonPath("$.title").isEqualTo("Clean Code") - .jsonPath("$._links.self.href").isEqualTo("/book/clean-code"); - } - - @Test - void givenNonReactiveEndpoint_whenCallingGetBookNonReactive_thenReturnBookWithHalResponse() { - webTestClient.get() - .uri("/book/get-book-non-reactive") - .exchange() - .expectStatus().isOk() - .expectBody() - .jsonPath("$.title").isEqualTo("Effective Java") - .jsonPath("$.author").isEqualTo("Joshua Bloch") - .jsonPath("$.isbn").isEqualTo("978-0134685991") - .jsonPath("$._links.self.href").isEqualTo("/book/effective-java"); - } - @Test void givenFluxEndpoint_whenCallingGetBooksFlux_thenReturnBooksStream() { webTestClient.get() @@ -220,4 +110,14 @@ void givenFluxEndpoint_whenCallingGetBooksFlux_thenReturnBooksStream() { } }); } + + @Test + void givenControllerReturnsAPublisherOfReactiveResponseEntity_whenmethodCalled_thenThrowsException() { + webTestClient.get() + .uri("/book/wrapped-halresponse") + .exchange() + .expectStatus().is5xxServerError(); + } + + } \ No newline at end of file