Polish affordances section.
This commit is contained in:
@@ -286,62 +286,13 @@ Boot sets things up for HAL. To switch to HAL-FORMS, you need to create this:
|
||||
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
|
||||
public class HypermediaConfiguration {
|
||||
|
||||
@Bean
|
||||
public static HalObjectMapperConfigurer halObjectMapperConfigurer() {
|
||||
return new HalObjectMapperConfigurer();
|
||||
}
|
||||
|
||||
private static class HalObjectMapperConfigurer
|
||||
implements BeanPostProcessor, BeanFactoryAware {
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
/**
|
||||
* Assume any {@link ObjectMapper} starts with {@literal _hal} and ends with {@literal Mapper}.
|
||||
*/
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
if (bean instanceof ObjectMapper && beanName.startsWith("_hal") && beanName.endsWith("Mapper")) {
|
||||
postProcessHalObjectMapper((ObjectMapper) bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private void postProcessHalObjectMapper(ObjectMapper objectMapper) {
|
||||
try {
|
||||
Jackson2ObjectMapperBuilder builder = this.beanFactory.getBean(Jackson2ObjectMapperBuilder.class);
|
||||
builder.configure(objectMapper);
|
||||
} catch (NoSuchBeanDefinitionException ex) {
|
||||
// No Jackson configuration required
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object postProcessAfterInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
return bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
There is lot packed in here:
|
||||
There is lot packed in this tiny class:
|
||||
|
||||
* `@Configuration` makes this class automatically picked up by Spring Boot's component scanning.
|
||||
* `@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)` activates Spring HATEOAS's hypermedia support, setting the format to HAL-FORMS.
|
||||
* When you use this annotation, all of Spring Boot's autoconfigured hypermedia support is disabled. You are taking over, so the rest of the code is
|
||||
about finding any registered `ObjectMapper` beans in the app context and registering the HAL-FORMS support through builtin callbacks.
|
||||
|
||||
WARNING: You currently cannot support more than one hypermedia-based mediatype as this point in time. If you try to use
|
||||
both `HAL` and `HAL_FORMS` in the annotation, Spring Boot will fail to launch.
|
||||
|
||||
IMPORTANT: We are working on simplifying the means to select different *and* multiple hypermedia formats.
|
||||
|
||||
Before launching the application, you'll want to pre-load some test data:
|
||||
|
||||
@@ -369,7 +320,7 @@ class DatabaseLoader {
|
||||
|
||||
This little database loader will:
|
||||
|
||||
* Be picked up by component scanning due to the `@Component` annotation.
|
||||
* Be picked up by Spring Boot's component scanning due to the `@Component` annotation.
|
||||
* The `CommandLineRunner` bean is executed by Spring Boot after the entire application context is up.
|
||||
* Inside that chunk of code, the injected `EmployeeRepository` is used to create a couple database entries.
|
||||
|
||||
@@ -439,6 +390,9 @@ This template data is enough information for you to generate an HTML form on a w
|
||||
</form>
|
||||
----
|
||||
|
||||
IMPORTANT: Spring HATEOAS doesn't provide the JavaScript to do this. This hypermedia format, though, has all the information you need to create it yourself. Or deploy somebody's
|
||||
3rd party library that speaks HAL-FORMS.
|
||||
|
||||
Are you wondering why Spring HATEOAS doesn't simply render an HTML form straight up? There are other mediatypes designed
|
||||
for this, especially XHTML. Using the Affordances API, we plan to add support in the future, allowing you to negotiate
|
||||
for the format you prefer.
|
||||
@@ -504,7 +458,7 @@ To round things out, you can also interrogate a single employee resource as show
|
||||
* There is a second template, *deleteEmployee* with a method of *delete*. It has no properties meaning all you need is the
|
||||
URI to delete an existing employee.
|
||||
|
||||
This information could easily be used on your web site to generate update forms:
|
||||
This information could easily be used on your web site to generate an update form:
|
||||
|
||||
[source,html]
|
||||
----
|
||||
@@ -528,7 +482,7 @@ You could also craft another form based on the `deleteEmployee` template:
|
||||
|
||||
These are just a couple ways to render forms based on the hypermedia's templates.
|
||||
|
||||
NOTE: `method="put"` and `method="delete"` aren't exactly valid HTML5. Either you can handle that in your code, or you
|
||||
NOTE: `method="put"` and `method="delete"` aren't valid HTML5. Either you handle that in your code, or you
|
||||
have some sort of filter like Spring MVC's `HiddenHttpMethodFilter` that lets you construct it as
|
||||
`<form method="post" _method="put" ...>`, which converts a *POST* into a *PUT* before invoking the code.
|
||||
|
||||
@@ -538,5 +492,6 @@ With the Affordances API, you can link related methods. And with HAL-FORMS suppo
|
||||
relationships into automated bits of HTML to enhance the user experience without having to inject domain knowledge into
|
||||
the client layer.
|
||||
|
||||
And that's a key part of REST--reducing the amount of domain knowledge needed in the client. But instead pushing relevant
|
||||
forms straight out to the end user, the client can more easily adapt to changes on the server.
|
||||
And that's a key part of REST--reducing the amount of domain knowledge needed in the client. Teach your clients a little more
|
||||
knowledge about the protocol, and they won't have to know as much about the domain. Instead, push the relevant
|
||||
forms straight out to the end user, are more easily adapt to changes on the server!
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2017 the original author or authors.
|
||||
* 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.
|
||||
@@ -15,66 +15,15 @@
|
||||
*/
|
||||
package org.springframework.hateoas.examples;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.BeanPostProcessor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport;
|
||||
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
/**
|
||||
* Right now, only one hypermedia type can be registered at a time. An extras will break Spring Boot's
|
||||
* autoconfiguration options. For this example, we are using {@literal HAL_FORMS}.
|
||||
*
|
||||
* As a side effect, of using {@link EnableHypermediaSupport}, we must configure post processing the related
|
||||
* {@link ObjectMapper} directly.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
@Configuration
|
||||
@EnableHypermediaSupport(type = HypermediaType.HAL_FORMS)
|
||||
public class HypermediaConfiguration {
|
||||
class HypermediaConfiguration {
|
||||
|
||||
@Bean
|
||||
public static HalObjectMapperConfigurer halObjectMapperConfigurer() {
|
||||
return new HalObjectMapperConfigurer();
|
||||
}
|
||||
|
||||
private static class HalObjectMapperConfigurer
|
||||
implements BeanPostProcessor, BeanFactoryAware {
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
/**
|
||||
* Assume any {@link ObjectMapper} starts with {@literal _hal} and ends with {@literal Mapper}.
|
||||
*/
|
||||
@Override
|
||||
public Object postProcessBeforeInitialization(Object bean, String beanName)
|
||||
throws BeansException {
|
||||
if (bean instanceof ObjectMapper && beanName.startsWith("_hal") && beanName.endsWith("Mapper")) {
|
||||
postProcessHalObjectMapper((ObjectMapper) bean);
|
||||
}
|
||||
return bean;
|
||||
}
|
||||
|
||||
private void postProcessHalObjectMapper(ObjectMapper objectMapper) {
|
||||
try {
|
||||
Jackson2ObjectMapperBuilder builder = this.beanFactory.getBean(Jackson2ObjectMapperBuilder.class);
|
||||
builder.configure(objectMapper);
|
||||
} catch (NoSuchBeanDefinitionException ex) {
|
||||
// No Jackson configuration required
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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;
|
||||
|
||||
/**
|
||||
* Analogous to {@link ResourceAssembler} but for resource collections.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public interface ResourcesAssembler<T, D extends ResourceSupport> {
|
||||
|
||||
/**
|
||||
* Converts all given entities into resources and wraps the collection as a resource as well.
|
||||
*
|
||||
* @see ResourceAssembler#toResource(Object)
|
||||
* @param entities must not be {@literal null}.
|
||||
* @return {@link Resources} containing {@link Resource} of {@code T}.
|
||||
*/
|
||||
Resources<D> toResources(Iterable<? extends T> entities);
|
||||
|
||||
}
|
||||
@@ -25,9 +25,12 @@ import org.springframework.hateoas.core.EvoInflectorRelProvider;
|
||||
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
|
||||
|
||||
/**
|
||||
* A {@link SimpleResourceAssembler} that mixes together a Spring web controller and a {@link RelProvider} to build links
|
||||
* upon a certain strategy.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class SimpleIdentifiableResourceAssembler<T extends Identifiable<?>> extends SimpleResourceAssembler<T> {
|
||||
public class SimpleIdentifiableResourceAssembler<T extends Identifiable<?>> implements SimpleResourceAssembler<T> {
|
||||
|
||||
/**
|
||||
* The Spring MVC class for the {@link Identifiable} from which links will be built.
|
||||
@@ -77,35 +80,36 @@ public class SimpleIdentifiableResourceAssembler<T extends Identifiable<?>> exte
|
||||
}
|
||||
|
||||
/**
|
||||
* Define links to add to every {@link Resource}.
|
||||
* Add single item self link based on {@link Identifiable} and link back to aggregate root of the {@literal T} domain
|
||||
* type using {@link RelProvider#getCollectionResourceRelFor(Class)}}.
|
||||
*
|
||||
* @param resource
|
||||
*/
|
||||
@Override
|
||||
protected void addLinks(Resource<T> resource) {
|
||||
public void addLinks(Resource<T> resource) {
|
||||
|
||||
resource.add(getCollectionLinkBuilder().slash(resource.getContent()).withSelfRel());
|
||||
resource.add(getCollectionLinkBuilder().withRel(this.relProvider.getCollectionResourceRelFor(this.resourceType)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Define links to add to {@link Resources} collection.
|
||||
* Add a self link to the aggregate root.
|
||||
*
|
||||
* @param resources
|
||||
*/
|
||||
@Override
|
||||
protected void addLinks(Resources<Resource<T>> resources) {
|
||||
public void addLinks(Resources<Resource<T>> resources) {
|
||||
resources.add(getCollectionLinkBuilder().withSelfRel());
|
||||
}
|
||||
|
||||
/**
|
||||
* Build up a URI for the collection using the Spring MVC controller followed by the resource type transformed
|
||||
* Build up a URI for the collection using the Spring web controller followed by the resource type transformed
|
||||
* by the {@link RelProvider}.
|
||||
*
|
||||
* Assumption is that an {@link org.springframework.hateoas.examples.EmployeeController} serving up {@link org.springframework.hateoas.examples.Employee}
|
||||
* Assumption is that an {@literal EmployeeController} serving up {@literal Employee}
|
||||
* objects will be serving resources at {@code /employees} and {@code /employees/1}.
|
||||
*
|
||||
* If this is not the case, simply override this method in your concrete instance, or simply resort to
|
||||
* If this is not the case, simply override this method in your concrete instance, or resort to
|
||||
* overriding {@link #addLinks(Resource)} and {@link #addLinks(Resources)} where you have full control over exactly
|
||||
* what links are put in the individual and collection resources.
|
||||
*
|
||||
@@ -124,6 +128,9 @@ public class SimpleIdentifiableResourceAssembler<T extends Identifiable<?>> exte
|
||||
return linkBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide opportunity to override the base path for the URI.
|
||||
*/
|
||||
private String getPrefix() {
|
||||
return getBasePath().isEmpty() ? "" : getBasePath() + "/";
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link ResourceAssembler}/{@link ResourcesAssembler} that focuses purely on the domain type,
|
||||
* returning back {@link Resource} and {@link Resources} for that type instead of
|
||||
* {@link org.springframework.hateoas.ResourceSupport}.
|
||||
*
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class SimpleResourceAssembler<T> implements ResourceAssembler<T, Resource<T>>, ResourcesAssembler<T, Resource<T>> {
|
||||
|
||||
/**
|
||||
* Converts the given entity into a {@link Resource}.
|
||||
*
|
||||
* @param entity
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Resource<T> toResource(T entity) {
|
||||
|
||||
Resource<T> resource = new Resource<T>(entity);
|
||||
|
||||
addLinks(resource);
|
||||
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts all given entities into resources and wraps the collection as a resource as well.
|
||||
*
|
||||
* @see #toResource(Object)
|
||||
* @param entities must not be {@literal null}.
|
||||
* @return {@link Resources} containing {@link Resource} of {@code T}.
|
||||
*/
|
||||
public Resources<Resource<T>> toResources(Iterable<? extends T> entities) {
|
||||
|
||||
Assert.notNull(entities, "Entities must not be null!");
|
||||
List<Resource<T>> result = new ArrayList<Resource<T>>();
|
||||
|
||||
for (T entity : entities) {
|
||||
result.add(toResource(entity));
|
||||
}
|
||||
|
||||
Resources<Resource<T>> resources = new Resources<>(result);
|
||||
|
||||
addLinks(resources);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define links to add to every individual {@link Resource}.
|
||||
*
|
||||
* @param resource
|
||||
*/
|
||||
protected void addLinks(Resource<T> resource) {
|
||||
// Default adds no links
|
||||
}
|
||||
|
||||
/**
|
||||
* Define links to add to the {@link Resources} collection.
|
||||
*
|
||||
* @param resources
|
||||
*/
|
||||
protected void addLinks(Resources<Resource<T>> resources) {
|
||||
// Default adds no links.
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
/*
|
||||
* Copyright 2017 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
|
||||
*
|
||||
* http://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.hamcrest.MatcherAssert.*;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import lombok.Data;
|
||||
import org.hamcrest.MatcherAssert;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* @author Greg Turnquist
|
||||
*/
|
||||
public class SimpleResourceAssemblerTest {
|
||||
|
||||
@Test
|
||||
public void convertingToResourceShouldWork() {
|
||||
|
||||
TestResourceAssembler assembler = new TestResourceAssembler();
|
||||
Resource<Employee> resource = assembler.toResource(new Employee("Frodo"));
|
||||
assertThat(resource.getContent().getName(), is("Frodo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertingToResourcesShouldWork() {
|
||||
|
||||
TestResourceAssembler assembler = new TestResourceAssembler();
|
||||
Resources<Resource<Employee>> resources = assembler.toResources(Arrays.asList(new Employee("Frodo")));
|
||||
assertThat(resources.getContent(), hasSize(1));
|
||||
assertThat(resources.getContent(), Matchers.<Resource<Employee>>contains(new Resource(new Employee("Frodo"))));
|
||||
MatcherAssert.assertThat(resources.getLinks(), is(Matchers.<Link>empty()));
|
||||
|
||||
assertThat(resources.getContent().iterator().next(), is(new Resource(new Employee("Frodo"))));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertingToResourceWithCustomLinksShouldWork() {
|
||||
|
||||
ResourceAssemblerWithCustomLink assembler = new ResourceAssemblerWithCustomLink();
|
||||
Resource<Employee> resource = assembler.toResource(new Employee("Frodo"));
|
||||
assertThat(resource.getContent().getName(), is("Frodo"));
|
||||
assertThat(resource.getLinks(), hasSize(1));
|
||||
assertThat(resource.getLinks(), hasItem(new Link("/employees").withRel("employees")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void convertingToResourcesWithCustomLinksShouldWork() {
|
||||
|
||||
ResourceAssemblerWithCustomLink assembler = new ResourceAssemblerWithCustomLink();
|
||||
Resources<Resource<Employee>> resources = assembler.toResources(Arrays.asList(new Employee("Frodo")));
|
||||
assertThat(resources.getContent(), hasSize(1));
|
||||
assertThat(resources.getContent(),
|
||||
Matchers.<Resource<Employee>>contains(new Resource(new Employee("Frodo"), new Link("/employees").withRel("employees"))));
|
||||
assertThat(resources.getLinks(), is(Matchers.<Link>empty()));
|
||||
|
||||
assertThat(resources.getContent().iterator().next(),
|
||||
is(new Resource(new Employee("Frodo"), new Link("/employees").withRel("employees"))));
|
||||
}
|
||||
|
||||
|
||||
class TestResourceAssembler extends SimpleResourceAssembler<Employee> {}
|
||||
|
||||
class ResourceAssemblerWithCustomLink extends SimpleResourceAssembler<Employee> {
|
||||
|
||||
@Override
|
||||
protected void addLinks(Resource<Employee> resource) {
|
||||
resource.add(new Link("/employees").withRel("employees"));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
class Employee {
|
||||
private final String name;
|
||||
}
|
||||
|
||||
}
|
||||
9
pom.xml
9
pom.xml
@@ -9,9 +9,9 @@
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<name>Spring HATEOAS - Examples</name>
|
||||
<description></description>
|
||||
<description>Examples using Spring HATEOAS to build RESTful services</description>
|
||||
|
||||
<inceptionYear>2017</inceptionYear>
|
||||
<inceptionYear>2017-2019</inceptionYear>
|
||||
|
||||
<licenses>
|
||||
<license>
|
||||
@@ -39,7 +39,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>2.0.0.RC1</version>
|
||||
<version>2.1.2.RELEASE</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>1.8</java.version>
|
||||
|
||||
<evo.version>1.2.2</evo.version>
|
||||
<spring-hateoas.version>1.0.0.BUILD-SNAPSHOT</spring-hateoas.version>
|
||||
<spring-plugin.version>2.0.0.BUILD-SNAPSHOT</spring-plugin.version>
|
||||
</properties>
|
||||
@@ -94,7 +95,7 @@
|
||||
<dependency>
|
||||
<groupId>org.atteo</groupId>
|
||||
<artifactId>evo-inflector</artifactId>
|
||||
<version>1.2.1</version>
|
||||
<version>${evo.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
Reference in New Issue
Block a user