Manually Creating Wrappers
- Creating a
HalResourceWrapperwithout an Embedded Resource - Creating a
HalResourceWrapperwith an Embedded Resource - Creating a
HalListWrapperwith Pagination
Reminder: Every example shown can be viewed and debugged in the hateoflux-demos repository. Clone or fork it and test as you explore options available! Either run the application and curl against the micro service or check the examples directly in the given unit tests.
Creating a HalResourceWrapper without an Embedded Resource
To create a HalResourceWrapper for a simple order without shipment details, the wrapper is implemented as shown below. This implementation typically resides within a controller method (or better yet, in an assembler):
@GetMapping("/order-no-embedded/{orderId}")
public Mono<HalResourceWrapper<OrderDTO, Void>> getOrder(@PathVariable int orderId) { // 1
Mono<OrderDTO> orderMono = orderService.getOrder(orderId); // 2
return orderMono.map(order -> HalResourceWrapper.wrap(order) // 3
.withLinks( // 4
Link.of("orders/{orderId}/shipment") // 5
.expand(orderId) // 6
.withRel("shipment"), // 7
Link.linkAsSelfOf("orders/" + orderId) // 8
));
}
The numbered comments in the code correspond to the following explanations:
- Endpoint Mapping: The
@GetMapping("/{id}")annotation maps HTTP GET requests to thegetOrder()method. Note the generic types forHalResourceWrapper. The first is the main resource, which is anOrderDTO. Since we don’t have another embedded, i.e., secondary resource, the second generic is set toVoid. - Fetching the Order:
orderService.getOrder()is an arbitrary service call that could be reading a database or calling another service. - Wrapping the Order: The
Monois accessed, and the order is now wrapped.HalResourceWrapper.wrap()creates already aHalResourceWrapper. However, as it is now, no links or embedded resources are available. - Adding Links: This method adds an arbitrary number of links. Technically, there is no limit to how many links a resource can have. However, each
Linkneeds a relation that is also present exactly once. So, even if multipleLinksare added, each must be of a differentLinkRelation. - Creating a New Link: With
Link.of()a newLinkcan be created manually. The provided string is interpreted as being thehrefof theLink. In this case, it is a templated URI withidas a variable. - Expanding the Template:
expand(id)replaces the{id}placeholder in the shipment link with the actual order ID, finalizing the URL. - Defining the Relation:
withRel()assigns a relation name to the link, indicating its purpose in the context of the order resource. - Self Link:
withLinks()in line 4 accepts an array of links (varargs).Link.linkAsSelfOf("orders/" + id)generates aLinkwith the relationSELF, i.e., a self-referential link. This method is unique of its kind as it sets the relation and anhrefsimultaneously.
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_links": {
"shipment": {
"href": "orders/1234/shipment"
},
"self": {
"href": "orders/1234"
}
}
}
The fields id, total, and status are part of the OrderDTO and were fetched by the OrderService. The links were built in the code example above. Note that each relation is the key to the link’s attributes. In this case, we have two links with the relations “shipment” and “self”, while both provide only an href.
Creating a HalResourceWrapper with an Embedded Resource
The code might seem a bit lengthy; however, if you choose to use assemblers, they will handle most of it automatically for you (e.g. see here)!
Now we want to create a HalResourceWrapper that doesn’t just reference a shipment via a link, but also includes the whole object instead:
@GetMapping("/order-with-embedded/{orderId}")
public Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> getOrderWithShipment(@PathVariable int orderId) { // 1
Mono<OrderDTO> orderMono = orderService.getOrder(orderId); // 2
Mono<ShipmentDTO> shipmentMono = shipmentService.getShipmentByOrderId(orderId); // 3
return orderMono.zipWith(shipmentMono, (order, shipment) ->
HalResourceWrapper.wrap(order) // 4
.withLinks( // 5
Link.linkAsSelfOf("orders/" + orderId)) // 6
.withEmbeddedResource( // 7
HalEmbeddedWrapper.wrap(shipment) // 8
.withLinks( // 9
linkTo(ShipmentController.class, c -> c.getShipment(shipment.getId())) // 10
.withRel(IanaRelation.SELF) // 11
.withHreflang("en-US") // 12
)
)
);
}
The numbered comments in the code correspond to the following explanations:
- Endpoint Definition: The
@GetMapping("/{id}")annotation maps HTTP GET requests containing an order ID to thegetOrderWithShipment()method. Note the generic types forHalResourceWrapper. The first is the main resource, which is anOrderDTOas before. However, since we now have an embedded resource, the second generic is set toShipmentDTO. - Order Retrieval:
orderService.getOrder()is an arbitrary service call that could be reading a database or calling another service. - Shipment Retrieval:
shipmentService.getShipmentByOrderId()is an arbitrary service call that could be reading a database or calling another service. - Combining Order and Shipment Data:
orderMono.zipWith(shipmentMono, (order, shipment) -> ...)merges the results of bothorderMonoandshipmentMono. This combination allows the creation of aHalResourceWrapperthat encapsulates both the order and its associated shipment details. - Adding Links to the Order Resource: The
withLinks()method attaches hypermedia links to the order resource. - Self Link for the Order:
Link.linkAsSelfOf("orders/" + orderId)generates a self-referential link pointing to the current order resource. This time we didn’t use a template but simply concatenated thehref. - Embedding the Shipment Resource: The
withEmbeddedResource()adds an embedded resource to the main resource. - Wrapping Shipment Data for Embedding:
HalEmbeddedWrapper.wrap(shipment)transforms theShipmentDTOinto aHalEmbeddedWrapper. This wrapper prepares the shipment data for embedding within the order resource, ensuring it conforms to HAL standards. - Adding Links to the Shipment Resource: The
withLinks()method is now part of theHalEmbeddedWrapperand adds links to the embedded resource, not to the main one. - Creating a Self Link for the Shipment:
linkTo(ShipmentController.class, c -> c.getShipment(shipment.getId()))is part of theSpringControllerLinkBuilder. It constructs a link to the shipment resource by referencing thegetShipment()method of theShipmentController. This approach automatically builds thehrefbased on the controller’s and the method’s endpoint by reading values from Spring’s annotations such as@RequestMapping. It also automatically expands the URI templates and adds query parameters if any arguments are marked with@RequestParam. - Defining the Relation Type for the Shipment Link:
withRel(IanaRelation.SELF)assigns theselfrelation type to the shipment link. This time we’re using theIanaRelationenum, which defines a set of standardized relation types defined by IANA. - Specifying Additional Attributes of the Link:
withHreflang("en-US")sets thehreflangattribute of the shipment link to “en-US”. This attribute informs clients about the language of the linked resource, which can be useful for content negotiation and accessibility.
The serialized result of this HalResourceWrapper is as follows:
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipment": { // notice the name is not "shipmentDTO"
"id": 127,
"carrier": "UPS",
"trackingNumber": "154-ASD-1238724",
"status": "Completed",
"_links": {
"self": {
"href": "/shipment/127",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "orders/1234"
}
}
}
The root fields are part of the main resource, OrderDTO. The node _embedded includes the embedded resource, ShipmentDTO. Notice how the name of the object is not “shipmentDTO” but “shipment”. This is because the ShipmentDTO class has an @Relation annotation that defines how the class name should be written when it is serialized. Under _links, we can also see the two attributes that we added to the self link. One is the expanded href that the SpringControllerLinkBuilder extracted from the controller class and method, and the other is the hreflang we added.
Creating a HalListWrapper with Pagination
The code might seem a bit lengthy; however, if you choose to use assemblers, they will handle most of it automatically for you (e.g. see here)!
To not deviate too much from the previous examples, lets consider the use case, where the user wants to list all his orders. It is quite possible to implement this as Fluxof HalResourceWrapper<OrderDTO>. However, we decide to create a Mono of HalListWrapper<OrderDTO>:
import static de.kamillionlabs.hateoflux.utility.SortDirection.ASCENDING;
import static de.kamillionlabs.hateoflux.utility.SortDirection.DESCENDING;
@GetMapping("/orders-with-pagination")
public Mono<HalListWrapper<OrderDTO, Void>> getOrdersManualBuilt(@RequestParam Long userId, // 1
Pageable pageable, // 2
ServerWebExchange exchange) { // 3
Flux<OrderDTO> ordersFlux = orderService.getOrdersByUserId(userId, pageable); // 4
Mono<Long> totalElementsMono = orderService.countAllOrdersByUserId(userId); // 5
int pageSize = pageable.getPageSize();
long offset = pageable.getOffset();
List<SortCriteria> sortCriteria = pageable.getSort().get() // 6
.map(o -> SortCriteria.by(o.getProperty(), o.getDirection().isAscending() ? ASCENDING : DESCENDING)) // 7
.toList();
return ordersFlux.map(
order -> HalResourceWrapper.wrap(order) // 8
.withLinks(
Link.linkAsSelfOf("orders/" + order.getId())
.prependBaseUrl(exchange)))
.collectList() // 9
.zipWith(totalElementsMono,(ordersList, totalElements) -> { // 10
HalPageInfo pageInfo = HalPageInfo.assembleWithOffset(pageSize, totalElements, offset); // 11
return HalListWrapper.wrap(ordersList) // 12
.withLinks(Link.of("orders{?userId,someDifferentFilter}") // 13
.expand(userId) // 14
.prependBaseUrl(exchange) // 15
.deriveNavigationLinks(pageInfo, sortCriteria)) // 16
.withPageInfo(pageInfo); // 17
}
);
}
The numbered comments in the code correspond to the following explanations:
-
Endpoint Definition: The
@GetMappingannotation maps HTTP GET requests to thegetOrders()method. Note that the method returns aMonoof list instead of aFluxof values. -
Accepting Pagination Parameters: In cases where Spring Data is used,
Pageablecan encapsulate pagination information such as page size, page number, and sorting criteria. These details can be used to create hateoflux-specific objects that will be utilized, among other things, for link building. -
Accessing Server Exchange Context:
ServerWebExchangeis automatically injected by Spring to provide access to the current HTTP request and response. It’s used later to prepend the base URL when constructing hypermedia links, ensuring they are absolute and correctly reference the server. -
Retrieving Paginated Orders:
orderService.getOrdersByUserId()is a service call that retrieves a sorted list ofOrderDTOobjects for the specifieduserId. In contrast to Spring Data, hateoflux transforms an arbitraryFluxand does not rely on aPageor page-like structure. -
Counting Total Number of Orders:
orderService.countAllOrdersByUserId()returns aMono<Long>representing the total number of orders for the givenuserId. This total count is essential for constructing pagination metadata like total pages and total elements in the response. -
Extracting Sort Information: The
pageable.getSort().get()method retrieves the sorting directives from thePageableobject. It returns a stream ofSort.Orderobjects, each representing a sorting instruction on a particular field, such as sorting by date or price. -
Mapping to Internal Sort Criteria: The sorting stream is mapped to a list of
SortCriteriaobjects. -
Wrapping Orders: The
ordersFlux.map()operation transforms each individualOrderDTOinto aHalResourceWrapper<OrderDTO>by wrapping it and adding hypermedia links. This is important because the end result, i.e.,HalListWrapper, only accepts resources that are wrapped inHalResourceWrapperand not raw resources on their own. -
Collecting Orders into a List: The
collectList()method aggregates all theHalResourceWrapper<OrderDTO>items into aList<HalResourceWrapper<OrderDTO>>. -
Combining Orders with Total Elements: The
zipWith(totalElementsMono, (ordersList, totalElements) -> { ... })subscribes to bothPublishers and makes them together available for further processing. -
Creating Pagination Metadata:
HalPageInfo.assembleWithOffset()creates aHalPageInfoobject that contains pagination details like page size, total elements. -
Wrapping the Orders List in a HAL List Wrapper:
HalListWrapper.wrap(ordersList)creates a wrapper around the list of order resources. Similar toHalResourceWrapper.wrap(), the wrapper at this point only contains the resource and nothing else. -
Defining the Base Link for the Orders List:
Link.of("orders{?userId,someDifferentFilter}")creates a templated link for theHalListWrapperas a whole.userIdandsomeDifferentFilterare query parameters that follow the structure defined in RFC6570, which hateoflux uses. -
Expanding Link Templates with Parameters:
expand(userId)replaces the{userId}placeholder in the link template with the actualuserIdvalue from the request parameters. Note thatsomeDifferentFilteris missing and will therefore be simply ignored by the expansion, as query parameters are optional by nature. -
Appending Base URL to Links:
prependBaseUrl()extracts the base URL from theServerWebExchangeand prefixes the defined href with it. This ensures that the link is absolute and correctly references the server’s address, which is crucial when the service is behind a proxy or load balancer. -
Adding Pagination Navigation Links:
deriveNavigationLinks(pageInfo, sortCriteria)automatically generates standard pagination links (i.e.,first,prev,next,lastas well asself) based on the specified href in line 13, the current page information, and sorting criteria. Note that thehrefgenerally corresponds to the self link of the resource list. Navigation links are always relative to the href initially provided. This line also concludes the creation of links. -
Including Pagination Metadata in the Response:
withPageInfo()attaches theHalPageInfoobject to theHalListWrapper, providing clients with detailed pagination metadata. This includes information such as page size, total elements, total pages, and current page number.
The serialized result with example payload data of this HalListWrapper is as follows:
{
"page": { //1
"size": 2,
"totalElements": 6,
"totalPages": 3,
"number": 0
},
"_embedded": { //2
"orderDTOs": [ //3
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_links": {
"self": {
"href": "http://myservice:8080/orders/1234"
}
}
},
{
"id": 1057,
"userId": 37,
"total": 72.48,
"status": "Delivered",
"_links": {
"self": {
"href": "http://myservice:8080/orders/1057"
}
}
}
]
},
"_links": { //4
"next": {
"href": "http://myservice:8080/orders?userId=37?page=1&size=2&sort=id,desc"
},
"self": {
"href": "http://myservice:8080/orders?userId=37?page=0&size=2&sort=id,desc"
},
"last": {
"href": "http://myservice:8080/orders?userId=37?page=2&size=2&sort=id,desc"
}
}
}
The JSON has a few interesting points worth highlighting. The numbered comments are explained as follows:
- Pagination Metadata: The
"page"block contains pagination details provided bywithPageInfo(). It includes fields such as:size: Number of items per page (here, 2).totalElements: Total number of items available (15).totalPages: Total number of pages (8).number: Current page index (starting from 0).
These fields mirror those in a HAL JSON response from a service using Spring HATEOAS, ensuring consistency in pagination representation.
-
Embedding Resources: In HAL documents, lists of resources are included under the
_embeddedkey. While single resources add their fields at the top level, collections are nested within_embedded. This structure allows clients to easily locate and parse embedded resources. -
Naming of Embedded Resources: The list of resources within
_embeddedmust have a key that names the collection. In this example, the resources are under"orderDTOs", which is derived from the class nameOrderDTOby converting it to lowercase and adding an “s” to pluralize. Since we did not use the@Relationannotation to specify a custom relation name, the default naming convention applies. - Hypermedia Links and Navigation: The
_linkssection includes hypermedia links generated by thederiveNavigationLinks()method. It automatically created the"next","self", and"last"links. The"prev"and"first"links were omitted because they are not applicable:- The
numberfield indicates we are on page0, the first page, so there is no previous page. - The
"first"link is redundant since"self"already points to the first page. - Each link’s
hrefincludes query parameters such asuserId,page,size, andsort, reflecting the current request parameters and pagination state.
The inclusion of sorting parameters (e.g.,
sort=id,desc) is optional but provides clients with complete context for the data they are viewing. - The