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.
Configure it as a spring bean in a
A lot of examples including the Spring HATEOAS readme say to extend The model
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 key here is to wrap your object(s) with the spring hateoas The Controller
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))
}
}
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 Add a Custom Rel Provider
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)
}
}
@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()
}
}
And now we get nice results for HAL as shown below. The HAL Results
{
"_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.
That is very good thank you
ReplyDelete