Spring HATEOAS - Embedded Resources with HAL

One common issue I see on the web is people having trouble rendering their JSON objects in HAL with embedded resources. For this example I am using groovy, but it explains how to configure the rendering of resources in a spring applicaiton.

The model

A lot of examples including the Spring HATEOAS readme say to extend ResourceSupport on your JSON/Jackson classes. I would suggest not doing that as embedding resources becomes a nightmare.
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonRootName

import javax.validation.constraints.NotNull

/**
 * Model for defining a Books RESTful resource model.
 */
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonRootName('book')
class HypermediaBook {

    @JsonProperty('id')
    String bookId

    @NotNull
    String title

    String isbn

    String publisher

}

The Controller

The key here is to wrap your object(s) with the spring hateoas Resource and Resources classes. Here is an example of a controller GET index method.
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo
import com.blogspot.keaplogik.domain.resource.HypermediaBook
import org.springframework.hateoas.*
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
class BookInventoryController {

    @RequestMapping(value = '/books', method = RequestMethod.GET)
    ResponseEntity<Resources<Resource>> index() {
        // Define links to this path.
        Link booksLink = linkTo(BookInventoryController).slash('/books').withSelfRel();

        // Build a list of resources and add the book.
        List<Resource> bookResources = []
        bookResources.add(buildABookResource('HATEOAS Wrapped Resource.'))
        bookResources.add(buildABookResource('Another HATEOAS Wrapped Resource.'))

        //Wrap your resources in a Resources object.
        Resources<Resource> resourceList = new Resources<Resource>(bookResources, booksLink)

        return new ResponseEntity<Resources<Resource>>(resourceList, HttpStatus.OK)
    }

    /**
     * Wrap book in a resource object, and provide self link.
     * @param title
     * @return wrapped book resource
     */
    private Resource<HypermediaBook> buildABookResource(String title) {
        HypermediaBook book = new HypermediaBook(
                title: title,
                isbn: 'xxx',
                publisher: 'keaplogik')
        book.bookId = book.hashCode()

        // Provide a link to lookup book. Method not provided in this example.
        Link bookLink = linkTo(BookInventoryController).slash('/book').slash(book.bookId).withSelfRel()
        new Resource<HypermediaBook>(book, bookLink.expand(book.bookId))
    }
}

Add a Custom Rel Provider

The defaul rel provider makes all JSON root names in the embedded list named after the classes name being serialized. For our example we want to use whatever is defined in the JsonRoot Jackson annotation.
import com.fasterxml.jackson.annotation.JsonRootName
import org.springframework.hateoas.RelProvider
import org.springframework.hateoas.core.DefaultRelProvider
import org.atteo.evo.inflector.English
/**
 * When an embedded resource is provided in a response using the {@code org.springframework.hateoas.Resources} model,
 * this provider can be configured at runtime to make any embedded values root json name be set based on the classes
 * annotated {@code JsonRootName ( " name " )}. By default Spring hateoas renders the embedded root field based on the class
 * name with first character in lowercase.
 */
class JsonRootRelProvider implements RelProvider {

    DefaultRelProvider defaultRelProvider = new DefaultRelProvider()

    @Override
    String getItemResourceRelFor(Class<?> type) {
        JsonRootName rootName = type.getAnnotationsByType(JsonRootName)?.find{ true }
        return rootName ? rootName.value() : defaultRelProvider.getItemResourceRelFor(type)
    }

    @Override
    String getCollectionResourceRelFor(Class<?> type) {
        JsonRootName rootName = type.getAnnotationsByType(JsonRootName)?.find{ true }
        return rootName ? English.plural(rootName.value()) : English.plural(defaultRelProvider.getCollectionResourceRelFor(type))
    }

    @Override
    boolean supports(Class<?> delimiter) {
        defaultRelProvider.supports(delimiter)
    }
}
Configure it as a spring bean in a @Component or @Configuration class and it will take president over the default rel provider.
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = [EnableHypermediaSupport.HypermediaType.HAL])
class WebConfiguration extends WebMvcConfigurerAdapter {
    @Bean
    RelProvider relProvider() {
        new JsonRootRelProvider()
    }
}

The HAL Results

And now we get nice results for HAL as shown below.
{
    "_links": {
        "self": {
            "href": "http://localhost:8080/books"
        }
    },
    "_embedded": {
        "books": [
            {
                "title": "HATEOAS Wrapped Resource.",
                "isbn": "xxx",
                "publisher": "keaplogik",
                "id": "1003203249",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/book/1003203249"
                    }
                }
            },
            {
                "title": "Another HATEOAS Wrapped Resource.",
                "isbn": "xxx",
                "publisher": "keaplogik",
                "id": "1011612007",
                "_links": {
                    "self": {
                        "href": "http://localhost:8080/book/1011612007"
                    }
                }
            }
        ]
    }
}

Spring HATEOAS Relation Annotation

If you plan to source your models with a dependency on spring hateoas, instead of just Jackson annotated models, I would suggest using the @Relation annotation provided by the library. An example can be found in the test source code

Comments

Post a Comment

Popular posts from this blog

Atmosphere Websockets & Comet with Spring MVC

Microservices Tech Stack with Spring and Vert.X