Creating Wrappers Using Assemblers
- Creating the Assembler
- Using an Assembler to Create a
HalListWrapper
For Resources With an Embedded Resource - Further Examples Using the Same Assembler
- Creating an Empty
HalResourceWrapper
- Creating a
HalResourceWrapper
with a Resource and No Embedded - Creating a
HalResourceWrapper
with a Resource and a Single Embedded - Creating a
HalResourceWrapper
with a Resource and a List of Embedded - Creating a
HalResourceWrapper
with a Resource and an Empty List of Embedded - Creating an Empty
HalListWrapper
- Creating a
HalListWrapper
with Resources Each Having a Single Embedded - Creating a
HalListWrapper
with Resources Each Having a Single Embedded with Paging - Creating a
HalListWrapper
with Resources Each Having a List of Embedded - Creating a
HalListWrapper
with Resources Each Having a List of Embedded with Some Beingnull
/Empty
- Creating an Empty
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.
When building wrappers manually, each field needs to be specified explicitly. This means that a single resource, a list of resources, pagination, and embedded resources all require different setups. However, with assemblers, this process is simplified. We only need to implement stubs that define how links are built, while the assemblers come with default implementations that can create wrappers in a single line, given the appropriate input.
In the following sections, we’ll create an assembler and provide multiple examples of the different types of wrappers that can be generated with it.
All combinations shown here can also be created manually by using the public methods of the wrappers themselves. Reviewing the default implementations of the assemblers can help clarify how this is done.
Creating the Assembler
The following is an assembler for wrappers that primarily wrap an OrderDTO
and embed a ShipmentDTO
. The assembler implements only the required methods, which mainly focus on creating self-links. Optional methods are available for adding additional links that may be desired.
@Component
public class OrderAssembler implements EmbeddingHalWrapperAssembler<OrderDTO, ShipmentDTO> { //1
@Override
public Class<OrderDTO> getResourceTClass() { //2
return OrderDTO.class; //3
}
@Override
public Class<ShipmentDTO> getEmbeddedTClass() { //4
return ShipmentDTO.class;
}
@Override
public Link buildSelfLinkForResource(OrderDTO resourceToWrap, ServerWebExchange exchange) { //5
return Link.of("order/" + resourceToWrap.getId()) //6
.prependBaseUrl(exchange); //7
}
@Override
public Link buildSelfLinkForEmbedded(ShipmentDTO embedded, ServerWebExchange exchange) { //8
return Link.of("shipment/" + embedded.getId())
.prependBaseUrl(exchange)
.withHreflang("en-US");
}
@Override
public Link buildSelfLinkForResourceList(ServerWebExchange exchange) { //9
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); //10
return Link.of("order{?userId,someDifferentFilter}") //11
.expand(queryParams)
.prependBaseUrl(exchange);
}
}
The numbered comments in the code correspond to the following explanations:
-
Implementing the Interface: The
OrderAssembler
implements theEmbeddingHalWrapperAssembler
with the generic typesOrderDTO
andShipmentDTO
. Similarly to how the generics describe the main and embedded resource, the generics in assemblers follow the same logic. -
The Method
getResourceTClass()
: It is a technical necessity. It is required so empty lists can be named correctly, as the name for lists is always derived from the class type (see here for more details). The@Relation
annotation is still honored (e.g.,OrderDTO
becomesorders
). -
Implementation of
getResourceTClass()
: The implementation is also trivial as it simply returns aResourceT
which the assembler already specifies. -
Implementation of
getEmbeddedTClass()
: Similarly we implement the same for the embedded resource type. -
The Method
buildSelfLinkForResource()
: Defines how the self link for the (main) resource should look. In this case, the self link is for anyOrderDTO
that is wrapped by the assembler. This applies only for a single resource. -
Implementation of
buildSelfLinkForResource()
: The link is manually built here (as opposed to building it withSpringControllerLinkBuilder
). TheresourceToWrap
is a given resource that the assembler wraps when prompted to. Note that the relation is automatically set, i.e., overwritten to “self”. This means that setting the relation here has no effect. -
Prepending the base URL: The
ServerWebExchange
is injected automatically into the controller by Spring, if specified, and holds various information about the HTTP request that a controller received.Link.prependBaseUrl()
extracts the base URL (i.e., protocol, host, and port) from theServerWebExchange
and prepends it to the specifiedhref
. -
Implementation of
buildSelfLinkForEmbedded()
: The method defines how the self link for the embedded resource should look. Technically, the embedded resource is also wrapped. In this case, the self link is for anyShipmentDTO
that is embedded in anOrderDTO
by the assembler. The base URL and an additional attributehreflang
are also added to the link. -
The Method
buildSelfLinkForResourceList
: Defines the self link for a list of resources, i.e., a list ofOrderDTO
s. In contrast tobuildSelfLinkForResource
, which builds the self link for a single resource, this method does not provide the elements. Generally, this shouldn’t be required in the first place, as the self link shouldn’t contain information about each and every list. -
Accessing Query Parameters: Among the other things that the
ServerWebExchange
provides are the query parameters used. By accessing them, we can construct the URL that was called to trigger the controller. -
Defining the Link: Since we know that the controller makes use of query parameters, we need to specify them in the URL (it depends on the controller implementation, of course). The URL should correspond to whatever was called to trigger the controller. Note that we didn’t use the
SpringControllerLinkBuilder
becauselinkTo
is type-safe and expects exact types, whereas theServerWebExchange
only provides aMultiValueMap<String, String>
that bundles together all variables. The link is then expanded and prepended with the base URL.
Using an Assembler to Create a HalListWrapper
For Resources With an Embedded Resource
Lets start with a more complicated setup to showcase what assemblers are capable of. In this example we’ll create wrapper with the following characteristics:
- Contains a list of resources
- All resources have an embedded resource
- The list is paginated
Using the assembler we just created, the OrderController
could have the following method:
import static de.kamillionlabs.hateoflux.utility.SortDirection.ASCENDING;
import static de.kamillionlabs.hateoflux.utility.SortDirection.DESCENDING;
@GetMapping("/orders-with-single-embedded-and-pagination")
public Mono<HalListWrapper<OrderDTO, ShipmentDTO>> getOrdersWithShipmentAndPagination(
@RequestParam(required = false) Long userId, // 1
Pageable pageable, // 2
ServerWebExchange exchange) { // 3
Flux<OrderDTO> orders = orderService.getOrders(userId, pageable);
PairFlux<OrderDTO, ShipmentDTO> ordersWithShipment =
PairFlux.zipWith(orders, (order -> shipmentService.getLastShipmentByOrderId(order.getId()))); // 4
Mono<Long> totalElements = orderService.countAllOrders(userId); // 5
int pageSize = pageable.getPageSize(); // 6
long offset = pageable.getOffset();
List<SortCriteria> sortCriteria = pageable.getSort().get()
.map(o -> SortCriteria.by(o.getProperty(), o.getDirection().isAscending() ? ASCENDING : DESCENDING))
.toList();
return orderAssembler.wrapInListWrapper(ordersWithShipment, totalElements, pageSize, offset, sortCriteria, exchange); // 7
}
}
The numbered comments in the code correspond to the following explanations:
-
Endpoint Definition: The
@GetMapping
annotation maps HTTP GET requests to thegetOrders()
method. Note that the method returns aMono
of aHalListWrapper
with the same generics the assembler was configured with. -
Using
Pageable
: Spring Data’sPageable
can be very useful. hateoflux itself does not use Spring Data and hence has no access to it, and therefore there is no automatic conversion provided. However, below an example is given. -
Injecting a
ServerWebExchange
: Spring automatically injects aServerWebExchange
if provided in the method signature of a REST controller. This can be useful to the assembler in order to build links. -
Getting Data: The services
orderService
andshipmentService
are arbitrary services that could be reading from a database or calling another service. ThePairFlux
is created by combing each order with its corresponding shipment. -
Get the Number of Total Elements: Since generally in WebFlux we work with
Flux
and notPage
instances, another query is required to get the total number of elements. -
Converting
Pageable
to a List ofSortCriteria
: We read the data provided by Spring Data’sPageable
and convert it to a list of hateoflux’sSortCriteria
. -
Create the Wrapper: Finally, wrap all individual main and embedded resources and put them in a
HalListWrapper
. Pagination is added simply by usingwrapInListWrapper
that also accepts paging information.
The serialized result with example payload data of this HalListWrapper
is as follows:
{
"page": {
"size": 2,
"totalElements": 6,
"totalPages": 3,
"number": 0
},
"_embedded": {
"orderDTOs": [
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipment": {
"id": 127,
"carrier": "UPS",
"trackingNumber": "154-ASD-1238724",
"status": "Completed",
"_links": {
"self": {
"href": "http://myservice:8080/shipment/127",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "http://myservice:8080/order/1234"
}
}
},
{
"id": 1057,
"userId": 37,
"total": 72.48,
"status": "Delivered",
"_embedded": {
"shipment": {
"id": 105,
"carrier": "UPS",
"trackingNumber": "154-ASD-1284724",
"status": "Completed",
"_links": {
"self": {
"href": "http://myservice:8080/shipment/105",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "http://myservice:8080/order/1057"
}
}
}
]
},
"_links": {
"next": {
"href": "http://myservice:8080/order?userId=37?page=1&size=2&sort=id,asc"
},
"self": {
"href": "http://myservice:8080/order?userId=37?page=0&size=2&sort=id,asc"
},
"last": {
"href": "http://myservice:8080/order?userId=37?page=2&size=2&sort=id,asc"
}
}
}
Further Examples Using the Same Assembler
In this section, we will reuse the defined assembler to showcase other possibilities for its usage. The following examples do not include detailed explanations, unlike the example above. However, inline comments may be added where some guidance is deemed necessary. These examples exclusively demonstrate how to interact with the assembler using Mono
s and Flux
es, even though it is also possible to use non-reactive types.
Creating an Empty HalResourceWrapper
This is generally not possible. There must be at least a resource; otherwise, e.g. when wrapping an empty Mono
, the assembler simply returns another empty Mono
. This should not be an issue, as a service would typically respond with an HTTP 404 in such cases.
Creating a HalResourceWrapper
with a Resource and No Embedded
Code
// Given input
Mono<OrderDTO> resource = orderService.getOrder(1234);
Mono<ShipmentDTO> embedded = Mono.empty();
// Assembler call
Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInResourceWrapper(resource, embedded, exchange);
Output
The serialized result of the HalResourceWrapper
is as follows:
Click to expand
Note that an empty embedded object results in the removal of the _embedded
node, whereas an empty list/flux of embedded, results in an empty JSON array.
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_links": {
"self": {
"href": "https://www.example.com/order/1234"
}
}
}
Creating a HalResourceWrapper
with a Resource and a Single Embedded
Code
// Given input
Mono<OrderDTO> resource = orderService.getOrder(1234);
Mono<ShipmentDTO> embedded = shipmentService.getShipment(127);
// Assembler call
Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInResourceWrapper(resource, embedded, exchange);
Output
The serialized result of the HalResourceWrapper
is as follows:
Click to expand
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipment": {
"id": 127,
"carrier": "UPS",
"trackingNumber": "154-ASD-1238724",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/127",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "https://www.example.com/order/1234"
}
}
}
Creating a HalResourceWrapper
with a Resource and a List of Embedded
Code
// Given input
Mono<OrderDTO> resource = orderService.getOrder(1234);
Flux<ShipmentDTO> embeddedList = shipmentService.getShipments(3287, 4125);
// Assembler call
Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInResourceWrapper(resource, embeddedList, exchange);
Output
The serialized result of the HalResourceWrapper
is as follows:
Click to expand
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipments": [
{
"id": 3287,
"carrier": "DHL",
"trackingNumber": "562-DHL-9182736",
"status": "Pending",
"_links": {
"self": {
"href": "https://www.example.com/shipment/3287",
"hreflang": "en-US"
}
}
},
{
"id": 4125,
"carrier": "USPS",
"trackingNumber": "739-USP-1827364",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/4125",
"hreflang": "en-US"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order/1234"
}
}
}
Creating a HalResourceWrapper
with a Resource and an Empty List of Embedded
Code
// Given input
Mono<OrderDTO> resource = orderService.getOrder(1234);
Flux<ShipmentDTO> embeddedList = Flux.empty();
// Assembler call
Mono<HalResourceWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInResourceWrapper(resource, embeddedList, exchange);
Output
The serialized result of the HalResourceWrapper
is as follows:
Click to expand
Note that an empty list/flux of embedded results in an empty JSON array, whereas an empty embedded object, results in the removal of the _embedded
node altogether.
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipments": []
},
"_links": {
"self": {
"href": "https://www.example.com/order/1234"
}
}
}
Creating an Empty HalListWrapper
Code
//Given input
PairFlux<OrderDTO,ShipmentDTO> emptyPairFlux = PairFlux.empty();
MultiRightPairFlux<OrderDTO,ShipmentDTO> emptyMultiRightPairFlux = MultiRightPairFlux.empty();
//Assembler call
// Option 1
HalListWrapper<OrderDTO,ShipmentDTO> resultOp1 = orderAssembler.createEmptyListWrapper(OrderDTO.class, exchange);
// Option 2
Mono<HalListWrapper<OrderDTO,ShipmentDTO>> resultOp2 = orderAssembler.wrapInListWrapper(emptyPairFlux, exchange);
// Option 3
Mono<HalListWrapper<OrderDTO,ShipmentDTO>> resultOp3 = orderAssembler.wrapInListWrapper(emptyMultiRightPairFlux,exchange);
Output
The serialized result of the HalListWrapper
is as follows:
Click to expand
{
"_embedded": {
"orderDTOs": []
},
"_links": {
"self": {
"href": "https://www.example.com/order"
}
}
}
Creating a HalListWrapper
with Resources Each Having a Single Embedded
Code
//Given input
Flux<OrderDTO> orders = orderService.getOrdersByUserId(38L);
PairFlux<OrderDTO, ShipmentDTO> resourcesWithEmbedded;
resourcesWithEmbedded = PairFlux.from(orders)
.with(order -> shipmentService.getLastShipmentByOrderId(order.getId()));
//Assembler call
Mono<HalListWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInListWrapper(resourcesWithEmbedded, exchange);
Output
The serialized result of the HalListWrapper
is as follows:
Click to expand
{
"_embedded": {
"orderDTOs": [
{
"id": 9550,
"userId": 38,
"total": 149.99,
"status": "Created",
"_embedded": {
"shipment": {
"id": 3105,
"carrier": "FedEx",
"trackingNumber": "759-FDX-1029384",
"status": "Out for Delivery",
"_links": {
"self": {
"href": "https://www.example.com/shipment/3105",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "https://www.example.com/order/9550"
}
}
},
{
"id": 5058,
"userId": 38,
"total": 149.99,
"status": "Delivered",
"_embedded": {
"shipment": {
"id": 5032,
"carrier": "FedEx",
"trackingNumber": "357-FDX-2938475",
"status": "In Transit",
"_links": {
"self": {
"href": "https://www.example.com/shipment/5032",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "https://www.example.com/order/5058"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order"
}
}
}
Creating a HalListWrapper
with Resources Each Having a Single Embedded with Paging
Code
//Given input
int pageNumber = 0;
int pageSize = 2;
Pageable pageable = PageRequest.of(pageNumber, pageSize); // This would usually be provided by Spring automatically
Flux<OrderDTO> orders = orderService.getOrdersByUserId(37L, pageable);
PairFlux<OrderDTO, ShipmentDTO> resourcesWithEmbedded;
resourcesWithEmbedded= PairFlux.from(orders)
.with(order -> shipmentService.getLastShipmentByOrderId(order.getId()));
Mono<Long> totalNumberOfElements = orderService.countAllOrdersByUserId(37L);
//Assembler call
Mono<HalListWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInListWrapper(resourcesWithEmbedded,
totalNumberOfElements,
pageSize,
pageable.getOffset(),
null, //for simplicity, we do not sort
exchange);
Output
The serialized result of the HalListWrapper
is as follows:
Click to expand
{
"page": {
"size": 2,
"totalElements": 6,
"totalPages": 3,
"number": 0
},
"_embedded": {
"orderDTOs": [
{
"id": 1234,
"userId": 37,
"total": 99.99,
"status": "Processing",
"_embedded": {
"shipment": {
"id": 127,
"carrier": "UPS",
"trackingNumber": "154-ASD-1238724",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/127",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "https://www.example.com/order/1234"
}
}
},
{
"id": 1057,
"userId": 37,
"total": 72.48,
"status": "Delivered",
"_embedded": {
"shipment": {
"id": 105,
"carrier": "UPS",
"trackingNumber": "154-ASD-1284724",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/105",
"hreflang": "en-US"
}
}
}
},
"_links": {
"self": {
"href": "https://www.example.com/order/1057"
}
}
}
]
},
"_links": {
"next": {
"href": "https://www.example.com/order?page=1&size=2"
},
"self": {
"href": "https://www.example.com/order?page=0&size=2"
},
"last": {
"href": "https://www.example.com/order?page=2&size=2"
}
}
}
Creating a HalListWrapper
with Resources Each Having a List of Embedded
Code
//Given input
Flux<OrderDTO> ordersWithReturns = orderService.getOrdersByUserId(17L);
MultiRightPairFlux<OrderDTO, ShipmentDTO> resourcesWithEmbedded;
resourcesWithEmbedded = MultiRightPairFlux.from(ordersWithReturns)
.with(order -> shipmentService.getShipmentsByOrderId(order.getId()));
//Assembler call
Mono<HalListWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInListWrapper(resourcesWithEmbedded, exchange);
Output
The serialized result of the HalListWrapper
is as follows:
Click to expand
{
"_embedded": {
"orderDTOs": [
{
"id": 1070,
"userId": 17,
"total": 199.99,
"status": "Returned",
"_embedded": {
"shipments": [
{
"id": 2551,
"carrier": "UPS",
"trackingNumber": "610-UPS-3748291",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/2551",
"hreflang": "en-US"
}
}
},
{
"id": 3904,
"carrier": "DHL",
"trackingNumber": "680-DHL-9182736",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/3904",
"hreflang": "en-US"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order/1070"
}
}
},
{
"id": 5078,
"userId": 17,
"total": 34.0,
"status": "Returned",
"_embedded": {
"shipments": [
{
"id": 3750,
"carrier": "USPS",
"trackingNumber": "755-USP-8374652",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/3750",
"hreflang": "en-US"
}
}
},
{
"id": 4203,
"carrier": "FedEx",
"trackingNumber": "920-FDX-5647382",
"status": "Completed",
"_links": {
"self": {
"href": "https://www.example.com/shipment/4203",
"hreflang": "en-US"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order/5078"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order"
}
}
}
Creating a HalListWrapper
with Resources Each Having a List of Embedded with Some Being null
/Empty
Code
//Given input
Flux<OrderDTO> ordersWithAndWithoutShipments = orderService.getOrdersByUserId(39L);
MultiRightPairFlux<OrderDTO, ShipmentDTO> resourcesWithEmbedded;
resourcesWithEmbedded = MultiRightPairFlux.from(ordersWithAndWithoutShipments)
.with(order -> shipmentService.getShipmentsByOrderId(order.getId()));
//Assembler call
Mono<HalListWrapper<OrderDTO, ShipmentDTO>> result = orderAssembler.wrapInListWrapper(resourcesWithEmbedded, exchange);
Output
The serialized result of the HalListWrapper
is as follows:
Click to expand
{
"_embedded": {
"orderDTOs": [
{
"id": 7250,
"userId": 39,
"total": 34.0,
"status": "Created",
"_embedded": {
"shipments": []
},
"_links": {
"self": {
"href": "https://www.example.com/order/7250"
}
}
},
{
"id": 1230,
"userId": 39,
"total": 99.99,
"status": "Delivered",
"_embedded": {
"shipments": [
{
"id": 4005,
"carrier": "FedEx",
"trackingNumber": "634-FDX-8473621",
"status": "Delivered",
"_links": {
"self": {
"href": "https://www.example.com/shipment/4005",
"hreflang": "en-US"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order/1230"
}
}
}
]
},
"_links": {
"self": {
"href": "https://www.example.com/order"
}
}
}