Hypermedia REST API adapter

Enabling hypermedia REST at RentIT

We will start by updating RentIT’s REST adapter to integrate hypermedia controls in the resource representations (i.e. REST data transfer objects). To this end, we will focus in the life cycle of Purchase Orders and our goal is then to integrate hyperlinks in the resource representations to reflect the transitions on states. The following state model models the Purchase order life cycle.

600

Note that the state model differs from the assumptions made in the previous practicals. The idea is to make explicit that the acceptance/rejection of a purchase order by a RentIt’s clerk. Therefore, we will assume that as part of the creation of the purchase order a plant reservation is automatically started. If no plant inventory item is available the purchase order is automatically reject. On the other hand, if the plant reservation succeeds, the purchase order is set to the status pending confirmation. Later, a RentIt’s clerk will be responsible for deciding whether to accept or reject a purchase order. You must take this change into consideration and update RentIt’s implementation accordingly. We will now guide you through the major changes that we have to apply to RentIt’s application.

We fist need to configure Spring hateoas to produce JSON HAL resource representations. This step is straight forward as we only have to annotate the entry class of our RentIt’s project, as shown below.

@SpringBootApplication
@EnableHypermediaSupport(type = EnableHypermediaSupport.HypermediaType.HAL) (1)
public class DemoApplication {

  @Configuration
  static class ObjectMapperCustomizer {
    @Autowired @Qualifier("_halObjectMapper")
    private ObjectMapper springHateoasObjectMapper; (2)

    @Bean(name = "objectMapper")
    ObjectMapper objectMapper() {
      return springHateoasObjectMapper
          .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
          .configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
          .configure(SerializationFeature.WRITE_DATE_TIMESTAMPS_AS_NANOSECONDS, false)
          .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
          .configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
          .registerModules(new JavaTimeModule());
		}
	}

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}
}
  1. Spring Hateoas supports to JSON HAL format (You just need to annotate the entry project class)

  2. Spring Hateoas uses its own object mapper. Unfortunately, it does not take into account the global configuration (e.g. support to Java time), so we need to manually configure the mapper.

I recently found that, for the version of Spring boot that we using, we will have to add the following dependency:

<dependency>
	  <groupId>org.springframework.plugin</groupId>
    <artifactId>spring-plugin-core</artifactId>
</dependency>

It might be the case that the dependency above is not required in your project.

In order to implement the transitions in the state model, we first specify the corresponding controller methods (at least an empty implementation that will eventually replaced with the actual one). For instance, to implement the transitions that are enabled at PENDING state, we would need to add the following methods to PurchaseOrderRestController.

PurchaseOrderRestController.java
@PostMapping("/{id}/accept")
public PurchaseOrderDTO acceptPurchaseOrder(@PathVariable String id) throws Exception {
    return null;
}

@DeleteMapping("/{id}/accept")
public PurchaseOrderDTO rejectPurchaseOrder(@PathVariable String id) throws Exception {
    return null;
}

The state machine is built inside the resource assembler. If we follow with the example above, we can add the corresponding transitions in PurchaseOrderAssembler as follows:

PurchaseOrderAssembler.java
    @Override
    public PurchaseOrderDTO toResource(PurchaseOrder purchaseOrder) {
        PurchaseOrderDTO dto = ...
        // I omitted the lines where we copy the values from the domain object
        // to the DTO

        try {
            switch (purchaseOrder.getStatus()) {
                case PENDING:
                    dto.add(new ExtendedLink(
                            linkTo(methodOn(SalesRestController.class)
                              .acceptPurchaseOrder(dto.get_id())).toString(),
                            "accept", POST));
                    dto.add(new ExtendedLink(
                            linkTo(methodOn(SalesRestController.class)
                              .rejectPurchaseOrder(dto.get_id())).toString(),
                            "reject", DELETE));
                    break;
               default break;
            }
        } catch (Exception e) {}
        return dto;
    }

Spring Hateoas provides some convenience methods (e.g. linkTo and methodOn) to build the links. With this methods we build the URI template that would allow us to reach a method in the controller. For instance, we would like to build the URI /api/sales/orders/{id} and connect it to the method PurchaseOrderRestController.acceptPurchaseOrder. Accordingly, we would use linkTo(methodOn(PurchaseOrderController).acceptPurchaseOrder(dto.get_id())). Internally, Spring Hateoas will analyze the set of annotations in the controller class to create the corresponding URI template.

As mentioned before, HAL does not allow us to specify the HTTP verb to be used with a given URI. That is why we have implemented a couple of classes to add the additional information. You will find such classes in the reference RentIT implementation that we have provided (the classes are located in package com.rentit.common.rest and are called ExtendedLink and ResourceSupport). Take note that an extended link must be created via its constructor as shown above. The constructor requires a regular Spring Hateoas link, the name of the relation (e.g. accept) and the HTTP verb.

Very important, you must update all your DTOs to extend the class ResourceSupport that is located in package com.rentit.common.rest and not the one provided by Spring Hateoas.

In order to use the convenience methods, you must include the following import statements (among others):

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
import static org.springframework.http.HttpMethod.POST;

Testing Hypermedia in RentIT’s side

We will now modify our Controller tests in RentIt’s project to check and exploit the presence of hyperlinks in the resource representations. As pointed out before, Spring hateoas uses a separate object mapper which has been tailer to render the hyperlinks. Do not forget to use such object mapper in your test code. The latter is done by adding the @Qualifier annotation to the code as shown below.

SalesRestControllerTests.java
@Autowired @Qualifier("_halObjectMapper")
ObjectMapper mapper;

@Test
@Sql("plants-dataset.sql")
public void testPurchaseOrderAcceptance() throws Exception {
  MvcResult result = mockMvc.perform(
      get("/api/inventory/plants?name=Exc&startDate=2016-03-14&endDate=2016-03-25"))
      .andReturn();
  List<PlantInventoryEntryDTO> plants =
    mapper.readValue(result.getResponse().getContentAsString(),
        new TypeReference<List<PlantInventoryEntryDTO>>() { });

  PurchaseOrderDTO order = new PurchaseOrderDTO();
  order.setPlant(plants.get(2));
  order.setRentalPeriod(BusinessPeriodDTO.of(LocalDate.now(), LocalDate.now()));

  result = mockMvc.perform(post("/api/sales/orders")
                           .content(mapper.writeValueAsString(order))
                           .contentType(MediaType.APPLICATION_JSON))
      .andExpect(status().isCreated())
      .andExpect(header().string("Location", not(isEmptyOrNullString())))
      .andReturn();

  order = mapper.readValue(result.getResponse().getContentAsString(), PurchaseOrderDTO.class);

  assertThat(order.get_xlink("accept"), is(notNullValue()));

  mockMvc.perform(post(order.get_xlink("accept").getHref()))
      .andReturn();
}

The test code above looks quite similar to what you should already have, except for the last two lines. You can see that we now have the method get_xlink() to fetch the hyperlink associated with a given relation. In the case above, we are checking that after creating the purchase order (that is, when the purchase order PENDING, waiting for confirmation), the resource representation must include a hyperlink associated with accept. In the last line of the test, we use such hyperlink to accept the purchase order.