Skip to content

Commit

Permalink
#1018 - HAL Forms properties customizable via annotations on represen…
Browse files Browse the repository at this point in the history
…tation model classes.

We now consider additional Jackson and JSR-303 annotations on representation models to enrich the HAL Forms template properties with additional information:

- @NotNull on a property flips its `required` flag to `true`.
- The "regexp" attribute of @pattern on a property is forwarded into the "regex" field.
- @JsonProperty(Access.READ_ONLY) on a property is translated into the "readOnly" flag.

The default for the "required" flag have been changed to false even for PUT and POST as whether they're required or not is completely determined by the model, not the HTTP method.

All of this is backed by significant refactoring in the way that Affordance instances are build internally. The new API is centered around the Affordances type in the mediatype package. The methods on Link to create the Affordance` from its details have been removed and replaced by builder style APIs on Affordance. AffordanceModelFactory has been moved to the mediatype as well.

PropertyUtils has been significantly revamped to expose a PayloadMetadata/PropertyMetadata model to abstract the individual traits of a property. This allows to optionally plug in the support for JSR-303 annotations. AffordanceModelFactory has been refactored to rather work with those instead of ResolvableType.
  • Loading branch information
odrotbohm committed Jul 18, 2019
1 parent ca68c7d commit fc1b4a8
Show file tree
Hide file tree
Showing 48 changed files with 2,277 additions and 861 deletions.
8 changes: 8 additions & 0 deletions pom.xml
Expand Up @@ -798,6 +798,13 @@
<optional>true</optional>
</dependency>

<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
Expand Down Expand Up @@ -993,6 +1000,7 @@
<configuration>
<source>${source.level}</source>
<target>${source.level}</target>
<parameters>true</parameters>
</configuration>
</plugin>

Expand Down
54 changes: 54 additions & 0 deletions src/docs/java/org/springframework/hateoas/AffordancesSample.java
@@ -0,0 +1,54 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas;

import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;

import lombok.var;

import org.springframework.hateoas.SimpleRepresentationModelAssemblerTest.Employee;
import org.springframework.hateoas.mediatype.Affordances;
import org.springframework.http.HttpMethod;

/**
* Manual usage of {@link Affordances}.
*
* @author Oliver Drotbohm
*/
public class AffordancesSample {

void manualAffordance() {

// tag::affordances[]
var methodInvocation = methodOn(EmployeeController.class).all();

var link = Affordances.of(linkTo(methodInvocation).withSelfRel()) // <1>

.afford(HttpMethod.POST) // <2>
.withInputAndOutput(Employee.class) //
.withName("createEmployee") //

.andAfford(HttpMethod.GET) // <3>
.withOutput(Employee.class) //
.addParameters(//
QueryParameter.optional("name"), //
QueryParameter.optional("role")) //
.withName("search") //

.toLink();
// end::affordances[]
}
}
36 changes: 36 additions & 0 deletions src/docs/java/org/springframework/hateoas/EmployeeModel.java
@@ -0,0 +1,36 @@
/*
* Copyright 2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas;

import lombok.Data;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;

/**
* @author Oliver Drotbohm
*/
// tag:hal-forms-model[]
@Data
public class EmployeeModel extends RepresentationModel<EmployeeModel> {

@NotNull //
private String name;

@Pattern(regexp = "[A-Z_]") //
private String role;
}
// end:hal-forms-model[]
Expand Up @@ -7,35 +7,24 @@
"href" : "http://localhost:8080/employees/1"
}
},
"_templates" : { // <1>
"_templates" : {
"default" : {
"title" : null,
"method" : "put", // <2>
"contentType" : "",
"properties" : [ { // <3>
"name" : "firstName",
"required" : true // <4>
}, {
"name" : "lastName",
"method" : "put",
"properties" : [ {
"name" : "name",
"required" : true
}, {
"name" : "role",
"required" : true
"regex" : "[A-Z_]"
} ]
},
"partiallyUpdateEmployee" : { // <5>
"title" : null,
"method" : "patch", // <6>
"contentType" : "",
"properties" : [ {
"name" : "firstName",
"required" : false // <7>
}, {
"name" : "lastName",
"required" : false
"name" : "name"
}, {
"name" : "role",
"required" : false
"regex" : "[A-Z_]"
} ]
}
}
Expand Down
111 changes: 0 additions & 111 deletions src/main/asciidoc/fundamentals.adoc
Expand Up @@ -195,114 +195,3 @@ Collection<Person> people = Collections.singleton(new Person("Dave", "Matthews")
CollectionModel<Person> model = new CollectionModel<>(people);
----
====

[[fundamentals.affordances]]
== Affordances

[quote, James J. Gibson, The Ecological Approach to Visual Perception (page 126)]
____
The affordances of the environment are what it offers …​ what it provides or furnishes, either for good or ill. The verb 'to afford' is found in the dictionary, but the noun 'affordance' is not. I have made it up.
____

REST-based resources provide not just data but controls. The last ingredient to form a flexible service are detailed *affordances*
on how to use the various controls.

Because affordances are associated with links, Spring HATEOAS provides an API to attach as many related methods as needed to a link.
The following code shows how to take a *self* link and associate two more affordances:

.Connecting affordances to `GET /employees/{id}`
====
[source, java, indent=0, tabsize=2]
----
include::{code-dir}/EmployeeController.java[tag=get]
----
<1> Create the *self* link.
<2> Associate the `updateEmployee` method with the `self` link.
<3> Associate the `partiallyUpdateEmployee` method with the `self` link.
Using `.andAffordance(afford(...))`, you can use the controller's methods to connect a `PUT` and a `PATCH` operation to a `GET` operation.
====

Imagine that the related methods *afforded* above looking like this:

.`updateEmpoyee` method that responds to `PUT /employees/{id}`
====
[source, java, indent=0, tabsize=2]
----
include::{code-dir}/EmployeeController.java[tag=put]
----
====

.`partiallyUpdateEmployee` method that responds to `PATCH /employees/{id}`
====
[source, java, indent=0, tabsize=2]
----
include::{code-dir}/EmployeeController.java[tag=patch]
----
====

There are many media types that support rendering affordances. Unfortunately, HAL isn't one of them.

A HAL document for `GET /employees/{id}` would look like this:

.HAL document with no affordances
====
[source, json]
----
{
"firstname" : "Frodo",
"lastname" : "Baggins",
"role" : "ring bearer",
"_links" : {
"self" : {
"href" : "http://localhost:8080/employees/1"
}
}
}
----
====

HAL supports providing links, but nothing else. While powerful, it doesn't let you show clients what inputs are required
by its various operations. Nor does it show _what_ HTTP methods are supported.

However, https://rwcbook.github.io/hal-forms/[HAL-FORMS] (`application/prs.hal-forms+json`), is a backwards compatible
extension of HAL s that adds `_templates`. This affordance-aware media type can fill in what's missing.

The same resource above will render the following HAL-FORMS document:

.HAL-FORMS document with affordances
====
[source, json, tabsize=2]
----
include::{resource-dir}/docs/mediatype/hal/forms/hal-forms-sample-with-notes.json[]
----
<1> The `_templates` attribute provided by HAL-FORMS with affordance-based information.
<2> The `updateEmployee` method's `@PutMapping` annotation is translated to `put`.
<3> The method's `@RequestBody` input type is used to find domain `properties`.
<4> For `POST` and `PUT`, all attributes are `required`.
<5> The second affordance is named after the `partiallyUpdateEmployee` method.
<6> `@PatchMapping` is translated into `patch`.
<7> For `PATCH`, attributes are _not_ `required`.
====

This rich document, consumable by any HAL-FORMS aware client includes enough extra details for full interaction with the resource.

In fact, this type of document makes it easy to write custom client-side code to generate an HTML form:

[source, html, tabsize=2]
----
<form method="put" action="http://localhost:8080/employees/1">
<input type="text" id="firstName" name="firstName"/>
<input type="text" id="lastName" name="lastName" />
<input type="text" id="role" name="role" />
<input type="submit" value="Submit" />
</form>
----

Letting hypermedia drive web forms for users reduces the need for the client to know about the domain.

By trading in domain knowledge and instead adding protocol support for HAL-FORMS, clients can become flexible and receptive
to server-side changes. No need to update your client every time a domain change is made on the server.

IMPORTANT: HAL-FORMS only supports affordances against the `self` link, but other affordance-aware media types may not
have the same restriction. In general, don't define affordances based on one particular media type.
37 changes: 34 additions & 3 deletions src/main/asciidoc/mediatypes.adoc
Expand Up @@ -200,11 +200,42 @@ include::{resource-dir}/docs/mediatype/hal/forms/hal-forms-sample.json[]
====

Checkout the https://rwcbook.github.io/hal-forms/[HAL-FORMS spec] to understand the details of the *_templates* attribute.
Read about the <<fundamentals.affordances,Affordances API>> to augment your controllers with this extra metadata.
Read about the <<server.affordances,Affordances API>> to augment your controllers with this extra metadata.

As for single-item (`EntityModel`) and aggregate root collections (`CollectionModel`), Spring HATEOAS renders them
identically to <<mediatypes.hal,HAL documents>>.

[[mediatypes.hal-forms.metadata]]
=== Defining HAL-FORMS metadata
HAL-FORMS allows to describe criterias for each form field.
Spring HATEOAS allows to customize those by shaping the model type for the input and output types and using annotations on them.

[options="header", cols="1,4"]
|===============
|Attribute|Description
|`readOnly`| Set to `true` if there's no setter method for the property. If that is present, use Jackson's `@JsonProperty(Access.READ_ONLY)` on the accessors or field explicitly. Not rendered by default, thus defaulting to `false`.
|`regex`| Can be customized by using JSR-303's `@Pattern` annotation either on the field or a type. In case of the latter the pattern will be used for every property declared as that particular type. Not rendered by default.
|`required`| Can be customized by using JSR-303's `@NotNull`. Not rendered by default and thus defaulting to `false`. Templates using `PATCH` as method will automatically have set all properties to not required.
|===============

For types that you cannot annotate manually, you can register a custom pattern via a `HalFormsConfiguration` bean present in the application context.

[source, java]
----
@Configuration
class CustomConfiguration {
@Bean
HalFormsConfiguration halFormsConfiguration() {
HalFormsConfiguration configuration = new HalFormsConfiguration();
configuration.registerPatternFor(CreditCardNumber.class, "[0-9]{16}");
}
}
----

This setup will cause the HAL-FORMS template properties for representation model properties of type `CreditCardNumber` to declare a `regex` field with value `[0-9]{16}`.

[[mediatypes.hal-forms.i18n]]
=== Internationalization of form attributes
HAL-FORMS contains attributes that are intended for human interpretation, like a template's title or property prompts.
Expand Down Expand Up @@ -232,7 +263,7 @@ com.acme.Employee._templates.default.title=Create employee <4>
NOTE: Keys using the actual affordance name enjoy preference over the defaulted ones.

==== Property prompts
Property prompts can also be resolved via the `rest-messages` resource bundle automatically configured by Spring HATEOAS.
Property prompts can also be resolved via the `rest-messages` resource bundle automatically configured by Spring HATEOAS.
The keys can be defined globally, locally or fully-qualified and need an `._prompt` concatenated to the actual property key:

.Defining prompts for an `email` property
Expand Down Expand Up @@ -326,7 +357,7 @@ The previous fragment was lifted from the spec. When Spring HATEOAS renders an `
* Put the `self` link into both the document's `href` attribute and the item-level `href` attribute.
* Put the rest of the model's links into both the top-level `links` as well as the item-level `links`.
* Extract the properties from the `EntityModel` and turn them into
* Extract the properties from the `EntityModel` and turn them into
====

When rendering a collection of resources, the document is almost the same, except there will be multiple entries inside
Expand Down
8 changes: 8 additions & 0 deletions src/main/asciidoc/migrate-to-1.0.adoc
Expand Up @@ -62,3 +62,11 @@ Note that the script will not necessarily be able to entirely fix all changes, b

Now verify the changes made to the files in your favorite Git client and commit as appropriate.
In case you find method or type references unmigrated, please open a ticket in out issue tracker.

[[migration.1-0-M3-to-1-0-RC1]]
== Migrating from 1.0 M3 to 1.0 RC1

- `Link.andAffordance(…)` taking Affordance details have been moved to `Affordances`. To manually build up `Affordance` instances now use `Affordances.of(link).afford(…)`. Also note the new `AffordanceBuilder` type exposed from `Affordances` for fluent usage. See <<server.affordances>> for details.
- `AffordanceModelFactory.getAffordanceModel(…)` now receives `InputPayloadMetadata` and `PayloadMetadata` instances instead of ``ResolvableType``s to allow non-type-based implementations. Custom media type implementations have to be adapted to that accordingly.
- HAL Forms now does not render property attributes if their value adheres to what's defined as default in the spec. I.e. if previously `required` was explicitly set to `false`, we now just omit the entry for `required`.
We also now only force them to be non-required for templates that use `PATCH` as the HTTP method.

0 comments on commit fc1b4a8

Please sign in to comment.