Skip to content

Java Library and Annotation Processor for Building Siren Web APIs

License

Notifications You must be signed in to change notification settings

unbroken-dome/siren-java

Repository files navigation

siren-java

Note

This is work in progress - use at your own risk. Feedback, issue reports and contributions are welcome.

This library aids in creating Siren-compatible Web APIs, particularly when building REST controllers with the Spring Web MVC or Spring WebFlux frameworks.

It provides a set of core classes and builders that model the various elements of the Siren specification, as well as an annotation processor that generates methods for inserting links and actions based on Spring controllers.

Why not Spring HATEOAS?

The Spring HATEOAS project follows a similar goal, and will include some sort of support for Siren eventually (it currently only supports HAL).

The main difference is that instead of generating code using an annotation processor, Spring HATEOAS derives information about links and actions ("affordances") at runtime. It does so by making an intercepted call to the controller method, and using the values of the passed arguments (similar to how mocking frameworks work). This approach works well enough for HAL, which has a much simpler model, but shows several shortcomings and lack of flexibility when applied to Siren.

Including the library

Declare a dependency on the siren-core JAR in your build tool:

Gradle
dependencies {
    compile 'org.unbrokendome.siren:siren-core:0.2.0'
}
Maven
<dependencies>
    <dependency>
        <groupId>org.unbrokendome.siren</groupId>
        <artifactId>siren-core</artifactId>
        <version>0.2.0</version>
    </dependency>
</dependencies>

There is also a bill-of-materials (BOM) artefact named siren-java-bom which you can refer to when using Maven, or Gradle with a compatible plugin like io.spring.dependency-management.

Using the Siren Model and Builder

The siren-core library includes a number of classes that model the various Siren elements. The root entity is represented by the class RootEntity, which you should return by a Siren-compliant Spring controller method. The following example constructs the example "order" entity that is listed in Siren JSON format in the Siren spec:

@RestController
@RequestMapping(value = "/order", produces = "application/vnd.siren+json")
public class OrderController {

    @GetMapping("/{id}")
    public RootEntity getOrder(@PathVariable String id) {
        return RootEntity.builder()
                .setClassName("order")
                .addProperty("orderNumber", 42)
                .addProperty("itemCount", 3)
                .addProperty("status", "pending")
                .addEmbeddedLink("http://x.io/rels/order-items", link -> link
                        .setClassNames("items", "collection")
                        .setHref("http://api.x.io/orders/42/items"))
                .addEmbeddedEntity("http://x.io/rels/customer", entity -> entity
                        .setClassNames("info", "customer")
                        .addProperty("customerId", "pj123")
                        .addProperty("name", "Peter Joseph")
                        .addLink("self", link -> link.setHref("http://api.x.io/customers/pj123")))
                .addAction("add-item", action -> action
                        .setTitle("Add Item")
                        .setMethod("POST")
                        .setHref("http://api.x.io/orders/42/items")
                        .setType("application/x-www-form-urlencoded")
                        .addField("orderNumber", field -> field
                                .setType(ActionField.Type.HIDDEN)
                                .setValue(42))
                        .addField("productCode", field -> field
                                .setType(ActionField.Type.TEXT))
                        .addField("quantity", field -> field
                                .setType(ActionField.Type.NUMBER)))
                .addLink("self", link -> link.setHref("http://api.x.io/orders/42"))
                .addLink("previous", link -> link.setHref("http://api.x.io/orders/41"))
                .addLink("next", link -> link.setHref("http://api.x.io/orders/43"))
                .build();
    }
}

Using the builders should be quite straightforward. The only thing of note is the use of Consumer<T> lambdas as arguments to the add…​ methods. For example, the method addLink takes the link rel (which is mandatory) and a lambda to act on a LinkBuilder:

public RootEntityBuilder addLink(String rel, Consumer<LinkBuilder> spec);

This pattern is used throughout the various builder classes. It reduces boilerplate code (we don’t need to construct a LinkBuilder, act on it, and call build() in the end), and it enables us to keep the fluent style with arbitrarily nested structures, which would not be possible without lambdas.

Kotlin Support

When using Kotlin, the library offers a "micro-DSL" for constructing Siren entities, which directly translates to the builders (as above) but results in cleaner and more readable code:

@RestController
@RequestMapping("/order", produces = arrayOf("application/vnd.siren+json"))
class OrderController {

    @GetMapping("/{id}")
    fun getOrder(@PathVariable id: String) = rootEntity {
        className = "order"
        property("orderNumber", 42)
        property("itemCount", 3)
        property("status", "pending")
        link("self") {
            href = "http://api.x.io/orders/42"
        }
        embeddedLink("http://x.io/rels/order-items") {
            classNames = listOf("items", "collection")
            href = "http://api.x.io/orders/42/items"
        }
        // ...
    }
}

Using the Annotation Processor

The annotation processor for Spring Web is available in the artefact siren-spring-ap. For Gradle, it is recommended to use the net.ltgt.apt plugin:

Gradle
plugins {
    id 'net.ltgt.apt' version '0.10'
}

dependencies {
    implementation 'org.unbrokendome.siren:siren-core:0.2.0'
    apt 'org.unbrokendome.siren:siren-spring-ap:0.2.0'
}

The annotation processor generates a <ControllerName>Links class and/or a <ControllerName>Actions class for every annotated Spring controller. These helper classes contain static methods for each @RequestMapping-annotated method in your controller, which you can use wherever you would use a Customer<ActionBuilder> or Consumer<LinkBuilder>:

@RequestMapping(value = "/", produces = "application/vnd.siren+json")
public class HomeController {

    @GetMapping
    public RootEntity home() {
        return RootEntity.builder()
            // The HomeControllerLinks.home() method is generated by the annotation processor
            // and returns a Consumer<LinkBuilder>
            .addLink("self", HomeControllerLinks.home())
            .build();
    }
}

There is a lot of logic behind how controller methods are mapped to actions or links, some of which can be fine-tuned by special annotations. More documentation will follow soon.

As a rule of thumb, links are created for GET mappings, and actions for other HTTP methods. Parameters to the controller method are either mapped to action fields (for actions), or must be given to the ControllerLinks method (for links).

Spring WebFlux Support

As of version 0.2 of the library, the annotation processor will work with both Spring Web MVC and Spring WebFlux (annotation-based flavor only). Unlike Web MVC, Spring WebFlux doesn’t offer a thread-bound "current request", so you have to pass in the ServerRequest from the handler method explicitly when generating actions or links:

@RequestMapping(value = "/", produces = "application/vnd.siren+json")
public class HomeController {

    @GetMapping
    public Mono<RootEntity> home(ServerRequest request) {
        return Mono.just(RootEntity.builder()
            // The HomeControllerLinks.home(ServerRequest request) method is
            // generated by the annotation processor and returns
            // a Consumer<LinkBuilder>
            .addLink("self", HomeControllerLinks.home(request))
            .build());
    }
}

Using the Siren Annotation Processor with kapt in Kotlin Projects

Kotlin uses its own annotation processor called kapt, and the Siren annotation processor should be compatible with it. In your Gradle script, use the org.jetbrains.kotlin.kapt Gradle plugin and declare the annotation processor as a kapt dependency:

Gradle
plugins {
    id 'org.jetbrains.kotlin.kapt' version "$kotlinVersion"
}

dependencies {
    compile 'org.unbrokendome.siren:siren-core:0.2.0'
    kapt 'org.unbrokendome.siren:siren-spring-ap:0.2.0'
}