Skip to content

Commit 4a8d22b

Browse files
committed
readme, javadocs and fix for SpringControllerLinkBuilder
1 parent c0fba74 commit 4a8d22b

File tree

8 files changed

+141
-10
lines changed

8 files changed

+141
-10
lines changed

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Building hypermedia-driven APIs in reactive Spring applications using WebFlux ca
6060

6161
- **Resource Wrappers:** `HalResourceWrapper` and `HalListWrapper` to encapsulate resources and collections.
6262
- **Type-Safe Link Building:** Easily create and manage hypermedia links.
63+
- **Specialized Response Types:** Purpose-built reactive response handling with `HalResourceResponse`, `HalMultiResourceResponse`, and `HalListResponse`.
6364
- **Pagination Support:** Simplified pagination with metadata and navigation links.
6465
- **URI Template Support:** Define dynamic URLs with placeholders.
6566
- **Seamless Spring Integration:** Works effortlessly with existing Spring configurations and annotations.
@@ -101,7 +102,7 @@ dependencies {
101102
```
102103
## Basic Usage
103104
### Creating a HalResourceWrapper
104-
Here's a simple example of how to create a `HalResourceWrapper` for an OrderDTO without any embedded resources.
105+
Here's a simple example of how to create a `HalResourceWrapper` for an `OrderDTO` without any embedded resources.
105106
```java
106107
@GetMapping("/order-no-embedded/{orderId}")
107108
public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int orderId) {
@@ -133,6 +134,35 @@ public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int order
133134
}
134135
}
135136
```
137+
### Response Types
138+
hateoflux provides specialized response types (basically reactive `ResponseEntity`s) to handle different resource scenarios in reactive applications. The following controller method is from the previous example now altered however altered to return a reactive HTTP response, while preserving the same body:
139+
140+
hateoflux provides specialized response types (essentially reactive `ResponseEntity`s) to handle different resource scenarios in reactive applications. Here's the previous controller example modified to return a reactive HTTP response while preserving the same body:
141+
142+
```java
143+
@GetMapping("/order-no-embedded/{orderId}")
144+
public HalResourceResponse<OrderDTO, Void> getOrder(@PathVariable String orderId) {
145+
146+
Mono<HalResourceWrapper<OrderDTO, Void>> order = orderService.getOrder(orderId)
147+
.map(order -> HalResourceWrapper.wrap(order)
148+
.withLinks(
149+
Link.of("orders/{orderId}/shipment")
150+
.expand(orderId)
151+
.withRel("shipment"),
152+
Link.linkAsSelfOf("orders/" + orderId)
153+
));
154+
155+
return HalResourceResponse.ok(order)
156+
.withContentType(MediaType.APPLICATION_JSON)
157+
.withHeader("Custom-Header", "value");
158+
}
159+
```
160+
The library provides three response types for different scenarios:
161+
162+
* `HalResourceResponse`: For single HAL resources (shown above)
163+
* `HalMultiResourceResponse`: For streaming multiple resources individually
164+
* `HalListResponse`: For collections as a single HAL document, including pagination
165+
136166
## Advanced Usage
137167
### Assemblers
138168
Assemblers in hateoflux reduce boilerplate by handling the wrapping and linking logic. Implement either `FlatHalWrapperAssembler` for resources without embedded entities or `EmbeddingHalWrapperAssembler` for resources with embedded entities.
@@ -186,23 +216,25 @@ Link userLink = linkTo(UserController.class, controller -> controller.getUser("1
186216
### Demos
187217
Explore practical examples and debug them in the [hateoflux-demos](https://github.com/kamillionlabs/hateoflux-demos) repository. Fork the repository and run the applications to see hateoflux in action.
188218
### Cookbook
189-
Refer to the [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook.html) for detailed and explained scenarios and code snippets demonstrating various functionalities of hateoflux.
219+
Refer to the [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/cookbook/cookbook.html) for detailed and explained scenarios and code snippets demonstrating various functionalities of hateoflux.
190220

191221
## Documentation
192222
Comprehensive documentation is available at [https://hateoflux.kamillionlabs.de (english)](https://hateoflux.kamillionlabs.de), covering:
193223
- [What is hateoflux?](https://hateoflux.kamillionlabs.de/)
194224
- [Representation Model](https://hateoflux.kamillionlabs.de/docs/core-concepts/representation-model.html)
225+
- [Response Types](https://hateoflux.kamillionlabs.de/docs/core-concepts/response-handling.html)
195226
- [Link Building](https://hateoflux.kamillionlabs.de/docs/core-concepts/linkbuilding.html)
196227
- [Assemblers](https://hateoflux.kamillionlabs.de/docs/core-concepts/assemblers.html)
197228
- [Spring HATEOAS vs. hateoflux](https://hateoflux.kamillionlabs.de/docs/spring-vs-hateoflux.html)
198-
- [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook.html)
229+
- [Cookbook: Examples & Use Cases](https://hateoflux.kamillionlabs.de/docs/cookbook/)
199230

200231
## Comparison with Spring HATEOAS
201232
hateoflux is specifically designed for reactive Spring WebFlux applications, offering a more streamlined and maintainable approach compared to Spring HATEOAS in reactive environments. Key differences include:
202233

203234
| **Aspect** | **Spring HATEOAS (WebFlux)** | **hateoflux** |
204235
|--------------------------------|--------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
205236
| **Representation Models** | Uses wrappers and inheritance-based models, requiring manual embedding of resources via inheritance or separate classes. | Uses wrappers exclusively to keep domain models clean and decoupled. |
237+
| **Response Types** | Uses standard `ResponseEntity` with manual reactive flow handling | Dedicated response types optimized for different resource scenarios |
206238
| **Assemblers and Boilerplate** | Verbose with manual resource wrapping and link addition. | Simplified with built-in methods; only links need to be specified in assemblers. |
207239
| **Pagination Handling** | Limited support in reactive environments; requires manual implementation. | Easy pagination with HalListWrapper; handles metadata and navigation links automatically. |
208240
| **Documentation Support** | Better for Spring MVC; less comprehensive for WebFlux. | Tailored for reactive Spring WebFlux with focused documentation and examples. |

src/main/java/de/kamillionlabs/hateoflux/http/HttpHeadersModule.java

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,24 @@
3434
*/
3535
public class HttpHeadersModule<HttpHeadersModuleT extends HttpHeadersModule<HttpHeadersModuleT>> {
3636

37+
/**
38+
* The underlying {@link HttpHeaders} instance being built.
39+
*
40+
* <p>This field holds the HTTP headers that are being constructed using the builder methods.
41+
*/
3742
protected HttpHeaders headers;
3843

39-
4044
/**
45+
* Constructs a new {@code HttpHeadersModule} instance with an empty {@link HttpHeaders}.
46+
*
47+
* <p>This default constructor initializes the builder with an empty set of HTTP headers,
48+
* ready to be populated using the provided builder methods.</p>
49+
* s/
50+
* public HttpHeadersModule() {
51+
* this.headers = new HttpHeaders();
52+
* }
53+
* <p>
54+
* /**
4155
* Adds a new header with the specified name and values.
4256
*
4357
* <p>If the header already exists, the new values are appended to the existing ones.</p>
@@ -131,25 +145,61 @@ public HttpHeadersModuleT withETag(@NonNull String eTag) {
131145
return (HttpHeadersModuleT) this;
132146
}
133147

134-
148+
/**
149+
* Sets the {@code Content-Type} header to the specified media type.
150+
*
151+
* @param mediaType
152+
* the media type string to set; must not be {@code null} or empty
153+
* @throws IllegalArgumentException
154+
* if {@code mediaType} is {@code null} or empty
155+
*/
135156
protected void addContentType(@NonNull String mediaType) {
136157
Assert.notNull(mediaType, valueNotAllowedToBeNull("MediaType"));
137158
Assert.isTrue(!mediaType.isBlank(), valueNotAllowedToBeEmpty("MediaType"));
138159
putNewHeader(HttpHeaders.CONTENT_TYPE, mediaType);
139160
}
140161

162+
/**
163+
* Sets the {@code Location} header to the specified location URI.
164+
*
165+
* @param location
166+
* the location URI string to set; must not be {@code null} or empty
167+
* @throws IllegalArgumentException
168+
* if {@code location} is {@code null} or empty
169+
*/
141170
protected void addLocation(@NonNull String location) {
142171
Assert.notNull(location, valueNotAllowedToBeNull("Location URI"));
143172
Assert.isTrue(!location.isBlank(), valueNotAllowedToBeEmpty("Location URI"));
144173
putNewHeader(HttpHeaders.LOCATION, location);
145174
}
146175

176+
/**
177+
* Sets the {@code ETag} header to the specified ETag value.
178+
*
179+
* @param eTag
180+
* the ETag value to set; must not be {@code null} or empty
181+
* @throws IllegalArgumentException
182+
* if {@code eTag} is {@code null} or empty
183+
*/
147184
protected void addETag(@NonNull String eTag) {
148185
Assert.notNull(eTag, valueNotAllowedToBeNull("ETag"));
149186
Assert.isTrue(!eTag.isBlank(), valueNotAllowedToBeEmpty("ETag"));
150187
putNewHeader(HttpHeaders.ETAG, eTag);
151188
}
152189

190+
/**
191+
* Inserts a new header into the {@link HttpHeaders} instance.
192+
*
193+
* <p>If headers already exist, they are preserved and the new header is added alongside them.
194+
* If the header with the specified key already exists, the provided values are appended.</p>
195+
*
196+
* @param key
197+
* the name of the header to add; must not be {@code null} or empty
198+
* @param values
199+
* the values of the header to add; must not be {@code null}
200+
* @throws IllegalArgumentException
201+
* if {@code key} is {@code null} or empty
202+
*/
153203
protected void putNewHeader(String key, String... values) {
154204
Assert.notNull(key, valueNotAllowedToBeNull("Key of attribute to put in header"));
155205
Assert.isTrue(!key.isBlank(), valueNotAllowedToBeNull("Key of attribute to put in header"));

src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntity.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
*/
1616
public interface ReactiveResponseEntity {
1717

18+
/**
19+
* The default HTTP status to be used when no specific status is provided. This constant is set to
20+
* {@link HttpStatus#OK} (200), indicating a successful request.
21+
*/
1822
HttpStatus DEFAULT_STATUS = HttpStatus.OK;
1923

2024
/**

src/main/java/de/kamillionlabs/hateoflux/http/ReactiveResponseEntityConfig.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
55
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
66
import org.springframework.context.annotation.Bean;
7+
import org.springframework.http.ResponseEntity;
78
import org.springframework.web.reactive.config.WebFluxConfigurer;
89
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
910

@@ -17,6 +18,21 @@
1718
@ImportAutoConfiguration(WebFluxAutoConfiguration.class)
1819
public class ReactiveResponseEntityConfig implements WebFluxConfigurer {
1920

21+
/**
22+
* Creates and registers a {@link ReactiveResponseEntityHandlerResultHandler} bean within the Spring application
23+
* context.
24+
*
25+
* <p>The {@code ReactiveResponseEntityHandlerResultHandler} is responsible for handling and serializing
26+
* {@link ReactiveResponseEntity} instances in reactive web responses. It leverages the provided
27+
* {@link ResponseEntityResultHandler} to delegate the processing of standard {@link ResponseEntity} objects,
28+
* ensuring consistency and integration with existing response handling mechanisms.
29+
*
30+
* @param responseEntityHandler
31+
* the existing {@link ResponseEntityResultHandler} bean provided by Spring WebFlux for handling standard
32+
* {@link ResponseEntity} instances
33+
* @return a new instance of {@link ReactiveResponseEntityHandlerResultHandler} configured with the provided
34+
* {@code responseEntityHandler}
35+
*/
2036
@Bean
2137
public ReactiveResponseEntityHandlerResultHandler reactiveResponseEntityHandlerResultHandler(
2238
ResponseEntityResultHandler responseEntityHandler) {

src/main/java/de/kamillionlabs/hateoflux/linkbuilder/ControllerMethodReference.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818

1919
package de.kamillionlabs.hateoflux.linkbuilder;
2020

21-
import org.reactivestreams.Publisher;
22-
2321
/**
2422
* Functional interface for method references to a controller method in the context of URI generation. This interface
2523
* enables type-safe referencing of specific methods within a controller when constructing links with
@@ -44,5 +42,5 @@ public interface ControllerMethodReference<ControllerT> {
4442
*
4543
* @see SpringControllerLinkBuilder#linkTo(Class, ControllerMethodReference)
4644
*/
47-
Publisher<?> invoke(ControllerT controller);
45+
Object invoke(ControllerT controller);
4846
}

src/main/java/de/kamillionlabs/hateoflux/model/hal/HalWrapper.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,22 @@ protected static String determineRelationNameForObject(Object object) {
281281
}
282282
}
283283

284+
/**
285+
* Determines whether the given class represents a scalar value type.
286+
* <p>
287+
* Scalar types are defined as:
288+
* <ul>
289+
* <li>Primitive types (int, boolean, char, etc.)</li>
290+
* <li>String</li>
291+
* <li>Number subclasses (Integer, Double, etc.)</li>
292+
* <li>Boolean wrapper class</li>
293+
* <li>Character wrapper class</li>
294+
* </ul>
295+
*
296+
* @param clazz
297+
* The class to check
298+
* @return true if the class represents a scalar type, false otherwise
299+
*/
284300
protected static boolean isScalar(Class<?> clazz) {
285301
return clazz.isPrimitive()
286302
|| clazz.equals(String.class)

src/test/java/de/kamillionlabs/hateoflux/dummy/controller/DummyController.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
package de.kamillionlabs.hateoflux.dummy.controller;
2020

21+
import de.kamillionlabs.hateoflux.dummy.model.Book;
22+
import de.kamillionlabs.hateoflux.http.HalResourceResponse;
2123
import de.kamillionlabs.hateoflux.model.hal.Composite;
2224
import org.springframework.web.bind.annotation.*;
2325
import reactor.core.publisher.Mono;
@@ -97,5 +99,8 @@ public Mono<Void> postMappingWithVoidAsReturnValue() {
9799
return Mono.empty();
98100
}
99101

100-
102+
@PostMapping("/response-type/{id}")
103+
public HalResourceResponse<Book, Void> postMappingWithHalResponseAsReturnValue(@PathVariable String id) {
104+
return HalResourceResponse.notFound();
105+
}
101106
}

src/test/java/de/kamillionlabs/hateoflux/linkbuilder/SpringControllerLinkBuilderTest.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ void givenRequestPutMappingWithQueryParameterAndCustomName_whenLinkTo_thenCustom
184184

185185

186186
@Test
187-
void givenpostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
187+
void givenPostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
188188
// GIVEN & WHEN
189189
final Link link = linkTo(DummyController.class,
190190
DummyController::postMappingWithVoidAsReturnValue);
@@ -193,5 +193,15 @@ void givenpostMappingWithVoidAsReturnValue_whenLinkTo_noExceptionIsThrownAndLink
193193
assertThat(link.getHref()).isEqualTo("/dummy/void-of-nothing");
194194
}
195195

196+
@Test
197+
void givenPostMappingWithHalResponseAsReturnValue_whenLinkTo_noExceptionIsThrownAndLinkIsCorrect() {
198+
// GIVEN & WHEN
199+
final Link link = linkTo(DummyController.class,
200+
c -> c.postMappingWithHalResponseAsReturnValue("123"));
201+
202+
//THEN
203+
assertThat(link.getHref()).isEqualTo("/dummy/response-type/123");
204+
}
205+
196206

197207
}

0 commit comments

Comments
 (0)