A library implementing Siren as a custom Spring HATEOAS hypermedia type. Siren is a hypermedia specification for representing entities.
Legal
Copyright © 2019-2020 The original authors.
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically. |
1. Introduction
Siren: a hypermedia specification for representing entities
Siren specification
This library extends Spring HATEOAS with the custom hypermedia type Siren.
The media type for Siren is defined as application/vnd.siren+json
.
The current version of this library (version 1.4.0-SNAPSHOT) is based on Spring HATEOAS version 1.5.0-SNAPSHOT. The source of this library can be found here. The Javadoc API documentation can be found here.
For further understanding of this document, please be aware of both the Spring HATEOAS and the Siren documentation. The following documentation assumes that the reader knows above documents.
2. Setup
To enable the Siren hypermedia type you simply need to add this library as a dependency to your project. The library is accessible through Maven Central or one of its proxies.
If you use Apache Maven, add the following to your build file:
<dependency>
<groupId>de.ingogriebsch.hateoas</groupId>
<artifactId>spring-hateoas-siren</artifactId>
<version>1.4.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
If you prefer to use Gradle, add the following to your build file:
dependencies {
implementation("de.ingogriebsch.hateoas:spring-hateoas-siren:1.4.0-SNAPSHOT")
}
Having this library on the classpath is all you need in a Spring Boot based project to get the hypermedia type automatically enabled.
If you want to use the library in a project that is not based on Spring Boot, you need to use class SirenMediaTypeConfiguration
.
This means, that you need to create an instance of this class and that you are responsible for that the methods that are necessary to initialize the library are executed.
This way incoming requests asking for the mentioned media type will get an appropriate response. The library is also able to deserialize a Json representation of the media type into corresponding representation models.
3. Server Side support
3.1. Serialization
Using this library will make your application respond to requests that have an Accept
header of application/vnd.siren+json
.
3.1.1. Representation Models
In general, each Spring HATEOAS representation model provided through a @RestController
method is rendered into a Siren entity.
Depending on the respective type of the representation model the following rules apply:
Representation Model
If this library serializes a representation model, it maps
-
any custom properties of the representation model (because it is subclassed) to Siren Entity properties.
-
the type of the representation model to the Siren Entity title if the type is mapped through the Internationalization mechanism.
-
the links of the representation model to Siren Entity links and actions (see section Links to understand how they are rendered).
Define a representation model:
class PersonModel extends RepresentationModel<PersonModel> {
String firstname, lastname;
}
Use the representation model:
PersonModel model = new PersonModel();
model.firstname = "Dave";
model.lastname = "Matthews";
// add some links (having affordances) to the model...
The resulting Siren representation:
{
"class": [
...
],
"properties": {
"firstname": "Dave",
"lastname": "Matthews"
},
"links": [
...
],
"actions": [
...
]
}
Entity Model
If this library renders an entity model, it maps
-
the value of the content property of the entity model to Siren Entity properties if the value represents an instance of a simple pojo.
-
the value of the content property of the entity model to Siren entities if the value is an instance of one of the available representation models.
-
the type of the content property of the entity model to the Siren Entity title if the value is an instance of a simple pojo and the type is mapped through the Internationalization mechanism.
-
the type of the entity model to the Siren Entity title if the value is an instance of one of the available representation models and the type is mapped through the Internationalization mechanism.
-
the links of the entity model to Siren Entity links and actions (see section Links to understand how they are rendered).
A person class:
class Person {
String firstname, lastname;
}
An entity model wrapping a person object:
Person person = new Person();
person.firstname = "Dave";
person.lastname = "Matthews";
EntityModel<Person> model = EntityModel.of(person);
// add some links (having affordances) to the model...
The resulting Siren representation:
{
"class": [
...
],
"properties": {
"firstname": "Dave",
"lastname": "Matthews"
},
"links": [
...
],
"actions": [
...
]
}
A person class:
class Person {
String firstname, lastname;
}
An entity model wrapping a person object:
Person person = new Person();
person.firstname = "Dave";
person.lastname = "Matthews";
EntityModel<Person> personModel = EntityModel.of(person);
// add some links (having affordances) to the person model...
Another entity model wrapping the entity model:
EntityModel<EntityModel<Person>> model = EntityModel.of(personModel);
// add some links (having affordances) to the model...
The resulting Siren representation:
{
"class": [
...
],
"entities": [
"class": [
...
],
"rel": [
...
],
"properties": {
"firstname": "Dave",
"lastname": "Matthews"
}
],
"links": [
...
],
"actions": [
...
]
}
Collection Model
If this library renders a collection model, it maps
-
the value of the content property of the collection model to Siren entities regardless if it represents instances of one of the available representation models or simple pojos.
-
the type of the collection model to the Siren Entity title if the type is mapped through the Internationalization mechanism.
-
the links of the collection model to Siren Entity links and actions (see section Links to understand how they are rendered).
A person class:
class Person {
String firstname, lastname;
}
Some entity models each wrapping a person object:
Person p1 = new Person();
p1.firstname = "Dave";
p1.lastname = "Matthews";
EntityModel<Person> pm1 = EntityModel.of(p1);
// add some links (having affordances) to the model...
Person p2 = new Person();
p2.firstname = "Stefan";
p2.lastname = "Lessard";
EntityModel<Person> pm2 = EntityModel.of(p2);
// add some links (having affordances) to the model...
A collection model wrapping the entity models:
Collection<EntityModel<Person>> people = Arrays.asList(pm1, pm2);
CollectionModel<EntityModel<Person>> people = CollectionModel.of(people);
// add some links (having affordances) to the model...
The resulting Siren representation:
{
"class": [
...
],
"entities": [{
"class": [
...
],
"properties": {
"firstname": "Dave",
"lastname": "Matthews"
},
"links": [
...
],
"actions": [
...
]
},{
"class": [
...
],
"properties": {
"firstname": "Stefan",
"lastname": "Lessard"
},
"links": [
...
],
"actions": [
...
]
}],
"links": [
...
],
"actions": [
...
]
}
Paged Model
If this library renders a paged model, it maps
-
the value of the content property of the paged model to Siren entities regardless if it represents instances of one of the available representation models or simple pojos.
-
the page metadata of the paged model to Siren Entity properties.
-
the type of the paged model to the Siren Entity title if the type is mapped through the Internationalization mechanism.
-
the links of the paged model to Siren Entity links and actions (see section Links to understand how they are rendered).
A person class:
class Person {
String firstname, lastname;
}
Some entity models each wrapping a person object:
Person p1 = new Person();
p1.firstname = "Dave";
p1.lastname = "Matthews";
EntityModel<Person> pm1 = EntityModel.of(p1);
// add some links (having affordances) to the model...
Person p2 = new Person();
p2.firstname = "Stefan";
p2.lastname = "Lessard";
EntityModel<Person> pm2 = EntityModel.of(p2);
// add some links (having affordances) to the model...
A paged model wrapping the entity models:
Collection<EntityModel<Person>> people = Collections.singleton(personModel);
PageMetadata metadata = new PageMetadata(20, 0, 1, 1);
PagedModel<EntityModel<Person>> model = PagedModel.of(people, metadata);
// add some links (having affordances) to the model...
The resulting Siren representation:
{
"class": [
...
],
"properties": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
},
"entities": [{
"class": [
...
],
"properties": {
"firstname": "Dave",
"lastname": "Matthews"
},
"links": [
...
],
"actions": [
...
]
},{
"class": [
...
],
"properties": {
"firstname": "Stefan",
"lastname": "Lessard"
},
"links": [
...
],
"actions": [
...
]
}],
"links": [
...
],
"actions": [
...
]
}
3.1.2. Links
If this library renders a link, it maps
-
links having an http method equal to
GET
to Siren Entity links. -
the rel of the link to the Siren Entity Link title if available through the Internationalization mechanism.
-
affordances bound to a link to Siren Entity actions.
-
the name of an affordance bound to a link to the Siren Action title if available through the Internationalization mechanism.
-
the name of an input property which is part of an affordance bound to a link to the Siren Action Field title if available through the Internationalization mechanism.
If this library renders a link, it does not
A person class:
class Person {
String firstname, lastname;
}
A person controller class:
@RestController
class PersonController {
@GetMapping("/persons/{id}")
ResponseEntity<EntityModel<Person>> findOne(Long id) { ... }
@PutMapping("/persons/{id}")
ResponseEntity<EntityModel<Person>> update(Long id, Person person) { ... }
@DeleteMapping("/persons/{id}")
ResponseEntity<Void> delete(Long id) { ... }
}
A self link having affordances created based on the available person controller methods:
@GetMapping("/persons/{id}")
ResponseEntity<EntityModel<Person>> findOne(Long id) {
Person person = personService.findOne(id);
Link selfLink = linkTo(methodOn(controllerClass).findOne(id)).withSelfRel() //
.andAffordance(afford(methodOn(controllerClass).update(id, null))) //
.andAffordance(afford(methodOn(controllerClass).delete(id)));
EntityModel<Person> model = EntityModel.of(person, selfLink);
return ResponseEntity.ok(model);
}
The resulting Siren representation:
{
...
"links": [{
"rel": [
"self"
],
"href": "http://localhost:8080/persons/1"
}],
"actions": [{
"name": "update",
"method": "PUT",
"href": "http://localhost:8080/persons/1",
"fields": [{
"name": "firstname",
"type": "text"
},{
"name": "lastname",
"type": "text"
}]
},{
"name": "delete",
"method": "DELETE",
"href": "http://localhost:8080/persons/1"
}]
}
3.1.3. Siren Model
Siren defines a resource as an entity which has not only properties and navigable links but may also contain embedded representations.
Because such representations retain all the characteristics of an entity you can build quite complex resource structures. Even if it is in most cases probably sufficient to simply use the available representation models it can be necessary in some cases to be able to build such quite complex structures.
Therefore this library provides a builder API that allows to build a Siren model which is then transfered into the respective Siren Entity structure. Means the library provides a SirenModelBuilder
that allows to create RepresentationModel
instances through a Siren idiomatic API.
3.2. Internationalization
Siren defines a title attribute for its entities, links and actions (including their fields).
These titles can be populated by using Spring’s resource bundle abstraction together with a resource bundle named rest-messages
.
This bundle will be set up automatically and is used during the serialization process.
3.2.1. Entities
To define a title for a Siren entity, use the key template _entity.$type.title
.
The type used to build the resulting key depends on which type of Spring HATEOAS representation model is used.
To evaluate if a title is available for a specific type, the fqcn
will be checked first, followed by the simple name
.
Finally, it is checked whether type default
is available.
3.2.3. Actions
To define a title for a Siren action, use the key template _action.$name.title
.
To evaluate if a title is available for the action, the name
of the Spring HATEOAS affordance will be checked first.
Finally, it is checked whether type default
is available.
To define a title for a Siren action field, use the key template _field.$name.title
.
To evaluate if a title is available for the action field, the name
of the input property which is part of the Spring HATEOAS affordance will be checked first.
Finally, it is checked whether type default
is available.
3.3. Restrictions
Siren embedded links are currently not implemented through the library itself.
If you want them, you need to implement a pojo representing an embedded link and add it as content of either a CollectionModel
or PagedModel
instance.
4. Client Side support
4.1. Deserialization
This library allows to use/handle the Siren hypermedia type on clients requesting data from servers producing this hypermedia type. This means that adding and enabling this library is sufficient to be able to deserialize responses containing data of the Siren hypermedia type into their respective representation models.
Please be aware of that the deserialization mechanism is currently not able to deserialize all types of complex Siren Entity structures that can be build with Siren model builder API.
Please be aware of that the deserialization mechanism is currently not able to deserialize a Siren action into the corresponding affordance model.
4.2. Traverson
The hypermedia type application/vnd.siren+json
is currently not usable with the Traverson
implementation provided through Spring HATEOAS.
4.3. Link Discovery
When working with hypermedia enabled representations, a common task is to find a link with a particular relation type in it.
Spring HATEOAS provides JsonPath-based implementations of the LinkDiscoverer
interface for the configured hypermedia types.
When using this library, an instance supporting this hypermedia type is exposed as a Spring bean.
Alternatively, you can set up and use an instance as follows:
String content = "{'_links' : { 'foo' : { 'href' : '/foo/bar' }}}";
LinkDiscoverer discoverer = new SirenLinkDiscoverer();
Link link = discoverer.findLinkWithRel("foo", content);
assertThat(link.getRel(), is("foo"));
assertThat(link.getHref(), is("/foo/bar"));
5. Configuration
This library currently uses a really simple approach to map the respective representation model to the class attribute of the Siren entity.
If you want to override/enhance this behavior you need to expose an implementation of the SirenEntityClassProvider
interface as a Spring bean.
This library currently uses a really simple approach to evaluate the relation between a representation model and its contained representation model to set the rel attribute of the Siren entity.
If you want to override/enhance this behavior you need to expose an implementation of the SirenEntityRelProvider
interface as a Spring bean.
This library currently uses a really simple approach to map the respective type of a payload property of an affordance model to the type attribute of the Siren action field.
If you need to specify additional mappings or if you want to override the default behavior, you can do so through the SirenConfiguration
.
If this is not enough you need to expose an implementation of the SirenActionFieldTypeConverter
interface as a Spring bean.
But then the support offered through the SirenConfiguration
is not active anymore.
This library currently uses a really simple approach to instantiate the concrete instances of the representation models during the deserialization process.
If you want to override/enhance this behavior you need to expose an implementation of the RepresentationModelFactories
interface as a Spring bean.
6. Experimental
This section deals with experimental functions that are currently implemented but for which it is not yet clear whether they will find their way into the library.
6.1. Subclassing Specific Representation Models
Siren defines a resource as an entity which has not only properties and navigable links but may also contain embedded representations. Because such representations retain all the characteristics of an entity you can build quite complex resource structures.
It is in the nature of Spring HATEOAS' representation models of type RepresentationModel
to be subclassed.
But it is not intended to subclass the other representation model types, namely EntityModel
, CollectionModel
and PagedModel
.
Even if this is not intended through Spring HATEOAS we 'bend' the intended behavior to allow to build such complex structures.
This means that this library is able to handle subclassed representation models of type EntityModel
and CollectionModel
.
It still makes no sense to subclass representation models of type PagedModel
because they already contain specific properties explaining the nature of this type of resource.
To use this experimental feature you need to configure explicitly that you want to subclass the mentioned representation model types (i.e. this feature is disabled by default). You can enable this functionality in the following way:
@Configuration
public class HateoasConfiguration {
@Bean
public SirenConfiguration sirenConfiguration() {
return new SirenConfiguration().withEntityAndCollectionModelSubclassingEnabled(true);
}
}
The following example explains what is currently possible to do. We will skip parts of the Siren representation like class, links or actions and concentrate on the properties and embedded representations.
A representation model:
class Capital extends RepresentationModel<Capital> {
String name;
}
An entity model:
class State extends EntityModel<Capital> {
String name;
}
A collection model:
class Country extends CollectionModel<State> {
String name;
}
Use the different types of representation models:
Capital denpasar = new Capital("Denpasar");
State bali = new State("Bali", denpasar);
Capital ambon = new Capital("Ambon");
State maluku = new State("Maluku", ambon);
Capital pekanbaru = new Capital("Pekanbaru");
State riau = new State("Riau", pekanbaru);
List<State> states = List.of(bali, maluka, riau);
Country indonesia = new Country("Indonesia", states);
The resulting Siren representation:
{
"properties": {
"name": "Indonesia"
},
"entities": [{
"properties": {
"name": "Bali"
},
"entities": [{
"properties": {
"name": "Denpasar"
},
}]
}, {
"properties": {
"name": "Maluku"
},
"entities": [{
"properties": {
"name": "Ambon"
},
}]
}, {
"properties": {
"name": "Riau"
},
"entities": [{
"properties": {
"name": "Pekanbaru"
},
}]
}]
}
This is still a relatively simple example of what is possible if using subclassed representation models together. Especially mixing entity models with collection models and vice versa allows to build quite complex structures.
License
This code is open source software licensed under the Apache 2.0 License.