Oliver Drotbohm Archive About Tags

Spring RESTBucks in 2021

April 14th, 2021

The RESTBucks sample project has served as showcase for various Spring related technologies in the context of building a hypermedia based API implementing a high-level business process for almost a decade. It has evolved significantly since then and – in the last couple of months – accumulated quite a batch of changes. I think that it’s worthwhile to have a look at them and how new libraries, specs, and their integration into Spring projects shape the sample.

jMolecules

I’ve written about the jMolecules project in a previous post. Its general idea is to allow user code to express architectural concepts in code, and a lot of the boilerplate code that’s needed, if – for example – the model shall be persisted with JPA. I know that whether to persist a domain model using JPA directly is a topic for debate, but I’d like to leave that for another post for now. This commit moved Spring RESTBucks domain model to jMolecules and essentially consist of the following steps:

All JPA annotations and customizations that were necessary could be removed in favor of the jMolecules ByteBuddy plugin to generate the mappings and boilerplate derived from the jMolecules annotations.

Spring Data REST

The Spring Data Pascal release train ships fundamental integration with jMolecules, like the ability to convert Identifier instances into primitives and properly integrate Association into the object mapping. Also, Spring Data REST now picks up Converter implementations shipped by the jMolecules Integrations project. All of that causes RESTBucks just to work seamlessly with the DDD building blocks expressed via jMolecules abstractions.

Jackson

Another piece of the puzzle is the jMolecules Jackson support that automatically customizes the serialization, deserialization of both Identifier types and value objects declared through jMolecules @ValueObject annotation. This can be seen in effect on CreditCardNumber that’s a value object in the domain model wrapping a String to apply rules of the domain concept to it. Despite the nesting, it serializes as plain String and also seamlessly deserializes Strings into a CreditCardNumber. You can find more details about the jMolecules Jackson integration here.

Advanced metadata for state transitions

The very start of a significant improvement in the API was to realize that the way the sample handled order submission wasn’t really a representation of the real-life process. The standard Spring Data REST export of the Order domain type assumed that an order was created by POSTing line items to the server, including prices. That is not quite how it works in the real world at best. To be blunt: it’s been wrong from day one, but I effectively didn’t care about this as the example project’s focus was on the general use of hypermedia in APIs, not taking every part of the process and implement it perfectly to the very end.

This step of the process has now changed to be rather implemented by a custom Spring MVC controller as Spring Data REST allows us to hook into the URI space it exposes and selectively override individual resources and mappings. We start by introducing a dedicated data transfer object (DTO) to capture all information necessary for placing an order. In our case it’s the location the drink is supposed to be consumed at, as well as a list of drinks. Also, we add a method to create a new Order from that information to the DTO:

@Data
class LocationAndDrinks {

  @NotNull //
  private Location location;
  private List<Drink> drinks;

  Order toOrder() {

    Order order = new Order(Collections.emptyList(), location);
    drinks.forEach(order::add);

    return order;
  }
}

We now use that type in our controller method to map the @RequestBody. That process makes use of a new feature just introduced in Spring Data REST, that allows to bind aggregate instances from URIs of resources exposed by it. Drink is a Spring Data REST managed aggregate, and thus, we have to submit a list of URIs pointing to drinks:

{
  "drinks": [ "…/drinks/3322ac99-…" ],
  "location": "Zum mitnehmen"
}

Spring Data REST will take care of resolving the URI to the Drink instance with identifier 3322ac99-… automatically for us. This leaves the question of how the client will know about that structure and how to populate the fields. We will this discuss this in detail below. Before we do that, let’s have a look at the controller method implementation as it’s quite interesting as well:

@BasePathAwareController
@RequiredArgsConstructor
class OrderController {

  private final Orders orders;

  @PostMapping(path = "/orders")
  HttpEntity<?> placeOrder(@RequestBody LocationAndDrinks payload,
    Errors errors, PersistentEntityResourceAssembler assembler) {

    return MappedPayloads.of(payload, errors)
        .mapIfValid(LocationAndDrinks::toOrder)
        .mapIfValid(orders::save)
        .mapIfValid(assembler::toFullResource)
        .concludeIfValid(it -> {

          var uri = it.getRequiredLink(IanaLinkRelations.SELF).toUri();

          return ResponseEntity.created(uri).body(it);
        });
  }
}

Note, how it makes use of the MappedPayloads abstraction currently living in my spring-playground repository. It provides an API to express the typical processing steps of a web request fluently: apply some validation, transformation of the payload, invoke business code, assemble a response representation. We simply accept the LocationAndDrinks instance deserialized by Jackson, create an Order instance from it, store that and then use Spring Data REST’s PersistentEntityResourceAssembler to produce a response following the usual rules of pointing to referenced aggregates etc. The pipeline will transparently handle binding errors by returning a 400 Bad Request rendering the Errors instance as response, including support to internationalized error messages etc. Read more about this in the documentation.

Advertising the order creation using HAL FORMS

Now that we’ve customized the step of creating an order in the first place, how does the client find out about it? The HAL FORMS media type allows to describe resource state transitions, and the payload structure expected for those. Spring HATEOAS 1.3 has shipped advanced support for that and allows us to easily create such forms by defining affordances. Affordances capture the semantics expressed by e.g. a Spring MVC controller method and depending on the configuration exposes those in the way a particular media type is defined. As of the Spring Data Pascal release train, Spring Data REST enabled projects generally enable the exposure of affordances as HAL FORMS, but they do not expose any forms automatically yet.

To expose the order creation form we enrich the RepositoryRootResource’s order link with an affordance. All we need to do to achieve that is point at the method that is accepting the request in a RepresentationModelProcessor.

@Component
class RootResourceModelProcessor
  implements RepresentationModelProcessor<RepositoryLinksResource> {

  @Override
  public RepositoryLinksResource process(RepositoryLinksResource model) {

    // We point to the method we want to advertise
    var affordance = afford(methodOn(OrderController.class)
      .placeOrder(null, null, null));

    // Add the affordance to the orders link
    return model.mapLink(LinkRelation.of("orders"), link -> {
      return link.andAffordance(affordance);
    });
  }
}

Note, how we, in a media type independent way, describe what’s to be advertised from the root resource. We deliberately guide the client to propose a state transition using the controller method pointed to. Alternatively, we could have built the affordance manually using Spring HATEOAS’ AffordanceBuilder API. Clients that continue to request HAL won’t see any of this, but ones that understand HAL FORMs can now request the resource using an Accept header set to application/prs.hal-forms+json and see the following:

GET /
Accept: application/prs.hal-forms+json
{
  ,
  "_templates" : {
    "default": {
      "method": "post",
      "properties": [
        {
          "name": "location",
          "options": {
            "inline": [
              "Zum mitnehmen",
              "Vor Ort"
            ],
            "maxItems": 1
          },
          "prompt": "Ort",
          "required": true,
          "value": "Zum mitnehmen"
        },
        {
          "name": "drinks",
          "options": {
            "link": {
              "href": "http://localhost:8080/drinks/by-name{?q}",
              "templated": true
            },
            "minItems": 1
          },
          "prompt": "Getränke"
        }
      ],
      "target": "http://localhost:8080/orders",
      "title": "Eine Bestellung erzeugen"
    }
  }
}

There’s quite a lot going on here. The response contains a _template rendered for the affordance that we described in our RepresentationModelProcessor. According to the HAL FORMS spec, if only one is contained in it, it’s named default. The target and method attributes are derived from the controller method we pointed to and also, the LocationAndDrinks’ payload type has been unfolded and its properties are advertised. We have both the title and prompt attributes internationalized to German using a simple resource bundle as described in Spring HATEOAS’ reference documentation.

Both properties use the options element, indicating that the value to be submitted for them has to be one or more items from a given list of options. For location, that list is a simple inline list of text values. They in turn an internationalized variants of the Location enum that’s part of the server side model handled by Spring Data REST’s automatic enum translation feature.

However, the slightly more interesting case is the option definition for drink. We can express that the list of available options is dynamic, and the values to be provided for selection are available via a particular (templated) URI. HAL FORMS defines that, unless otherwise specified, the target resource is supposed to return a collection of either scalar values, or a dictionary with fields value and prompt. Thus, we now expose a dedicated resource blending into the /drinks URI space from DrinksOptions:


@BasePathAwareController
public class DrinksOptions {

  private final Drinks drinks;
  private final TypedEntityLinks<Drink> links;

  /* Constructor omitted */

  @GetMapping("/drinks/by-name")
  public HttpEntity<?> getOptions(@RequestParam Optional<String> q) {

    var sort = sort(Drink.class).by(Drink::getName).ascending();

    var options = q.map(it -> drinks.findByNameContaining(it, sort))
        .orElseGet(() -> drinks.findAll(sort))
        .map(this::toPromptedValue)
        .toList();

    var model = HalModelBuilder.halModel()
        .embed(options, LinkRelation.of("drinks"))
        .build();

    return ResponseEntity.ok(model);
  }

  HalFormsPromptedValue toPromptedValue(Drink drink) {
    return HalFormsPromptedValue.of(drink.getName(),
      links.linkToItemResource(drink).getHref())
  }
}

The request is answered by either looking up all drinks or applying a name filter, mapping the Drink instances to expose their name as prompt, and the URI they’re exposed under as value. Thus, the responses look as follows:

{
  "_embedded" : {
    "drinks" : [
      { "prompt": "Cappuchino", "value": "…/drinks/462a692d-…" },
      { "prompt": "Java Chip", "value": "…/drinks/07217680-…" }
    ]
  }
}

The client is now supposed to follow the link attribute of the options document found in the form, and – depending on the minItems/maxItems arrangement – offer a selection control for the values returned. The server is even prepared to serve some kind of auto-completion by taking the partial user input as query parameter (through the exposed template variable). It would then submit the form to the URI described in target and use the values backing the individual option items. The options configuration is added via the HalFormsConfiguration bean located in JacksonCustomizations.

@Configuration(proxyBeanMethods = false)
class JacksonCustomizations {

  @Bean
  HalFormsConfiguration halFormsConfiguration() {

    Supplier<Link> drinksOptionsUri = linkTo(methodOn(DrinksOptions.class)
      .getOptions(Optional.empty()))
      .withSelfRel()
      .withType(MediaTypes.HAL_JSON_VALUE);

    return new HalFormsConfiguration()
        .withPattern(CreditCardNumber.class, CreditCardNumber.REGEX)
        .withOptions(LocationAndDrinks.class, "location",
            it -> HalFormsOptions.inline(Location.values())
              .withSelectedValue(Location.TAKE_AWAY)
              .withMaxItems(1L))
        .withOptions(LocationAndDrinks.class, "drinks",
            it -> HalFormsOptions.remote(drinksOptionsUri.get())
              .withMinItems(1L));
  }
}

See how we define inline options for the LocationAndDrinks.location property by simply pointing to the enum values and set up the remote options for drinks by constructing a URI.

A similar form has been exposed for the process step of issuing a payment (see PaymentOrderModelProcessor). Note, how it is only conditionally added to the representations using RepresentationModel.mapLinkIf(…) which means that the form will only be visible to clients if the order is to be paid in the first place implementing the hypermedia as the engine of application state idea. The HalFormsConfiguration has set up an explicit pattern for the CreditCardNumber domain type (see above). This causes the form derived from PaymentForm (the type mapping the payload) expose that pattern as regex for the number field.

HAL Explorer

It’s not by accident that the just released 0.16.0 version of the HAL Explorer library maintained by Spring HATEOAS community veteran Kai Tödter has shipped extended support for HAL FORMS in general and the options field in particular. Kai has helped to shape the addition of the options element to the HAL FORMS significantly, and I am glad that we could get Spring Data REST’s HAL Explorer module to be updated to consume the WebJAR of that version for the Pascal release. That means, the advanced support for HAL FORMS in Spring HATEOAS and Spring Data REST can be visualized right away. Let’s have a quick walk through the individual process steps using the HAL Explorer.

To start the application, simply clone the sample project repository, move into the server directory, run ./mvnw spring-boot:run and point your browser to http://localhost:8080. The HAL Explorer will show up and issue an HTTP request asking for HAL FORMS to the root of the API. The above mentioned form returned will cause the UI show that it found a form.

Note, how the internationalized title returned in the form is used for display purposes. Clicking on the plus icon will reveal the form and allow users to actually fill it out:

See how HAL Explorer advertises the properties required to be submitted, how it has rendered a single-value selection drop-down menu for the location as well as a multi-select element for the drinks, populated with the values obtained from the resource. In the body section of the form you can also see the actual payload that HAL Explorer is going to submit to the server. If you select a second element from the “Getränke” section, it will automatically add that to the array value of the drinks field.

Once we submitted the form, the client follows the Location header of the 201 Created response. As the order is now in “payment expected” state, it exposes both the restbucks:payment link as well as a form to submit the payment:

We can again click the button to reveal the full form:

See how the client has made use of the regex field of the form definition to automatically apply validation to the input field, and reports the error, as we’re still lacking to enter the 16th digit of the credit card number.

While HAL Explorer is of course not a UI that you’d like to expose to end users, it is a great tool to make a hypermedia API discoverable by client developers. Especially, as – in contrast to other approaches – does not only inspect static API description, but also integrates with the dynamic nature of hypermedia APIs nicely.

Summary

That’s been quite a ride! I hope you could get an impression of how the features shipped in the latest Spring HATEOAS and Spring Data releases help you to build richer APIs and at the same time reduce the amount of persistence technology induced boilerplate in your domain model. There’s more in the pipeline. In particular, I am going to improve on the testing and documentation story, so that you get an impression of how to use Spring REST Docs to document hypermedia APIs and package that documentation right with the application so that it’s discoverable by HAL Explorer. Also, I’d like to polish up my Android client implementation also contained in the repository to make more extensive use of the affordances exposed by the API.

Anything you liked in particular? Suggestions to make what to improve on? Please leave a comment below!

blog comments powered by Disqus