Skip to content

Commit 72cd3f3

Browse files
committed
#340 - Adds new Affordances API + HAL-Forms mediatype
* Introduces new Affordances API to build links related to each other to serve other mediatypes * Introduces HAL-Forms, which uses affordances to automatically generate HTML form data based on Spring MVC annotations. Original pull-request: #340, #447, #581 Related issues: #503, #334
1 parent 05f687e commit 72cd3f3

File tree

58 files changed

+3712
-40
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+3712
-40
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2017 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas;
17+
18+
import java.util.Map;
19+
20+
/**
21+
* Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation.
22+
*
23+
* Affordances must include: an HTTP verb, a method name, required?, and a listing of possible properties and their types.
24+
*
25+
* @author Greg Turnquist
26+
*/
27+
public interface Affordance {
28+
29+
String getVerb();
30+
31+
String getMethodName();
32+
33+
boolean isRequired();
34+
35+
Map<String, Class<?>> getProperties();
36+
37+
String getUri();
38+
}

src/main/java/org/springframework/hateoas/Link.java

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import lombok.experimental.Wither;
2424

2525
import java.io.Serializable;
26+
import java.util.ArrayList;
2627
import java.util.Collections;
2728
import java.util.HashMap;
2829
import java.util.List;
@@ -48,10 +49,9 @@
4849
*/
4950
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
5051
@JsonIgnoreProperties("templated")
51-
@NoArgsConstructor(access = AccessLevel.PROTECTED)
5252
@AllArgsConstructor(access = AccessLevel.PACKAGE)
5353
@Getter
54-
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation" })
54+
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation", "affordances" })
5555
public class Link implements Serializable {
5656

5757
private static final long serialVersionUID = -9037755944661782121L;
@@ -73,6 +73,7 @@ public class Link implements Serializable {
7373
private @XmlAttribute @Wither String type;
7474
private @XmlAttribute @Wither String deprecation;
7575
private @XmlTransient @JsonIgnore UriTemplate template;
76+
private @XmlTransient @JsonIgnore List<Affordance> affordances;
7677

7778
/**
7879
* Creates a new link to the given URI with the self rel.
@@ -108,6 +109,32 @@ public Link(UriTemplate template, String rel) {
108109
this.template = template;
109110
this.href = template.toString();
110111
this.rel = rel;
112+
this.affordances = new ArrayList<Affordance>();
113+
}
114+
115+
public Link(String href, String rel, List<Affordance> affordances) {
116+
117+
this(href, rel);
118+
119+
Assert.notNull(affordances, "affordances must not be null!");
120+
121+
this.affordances = affordances;
122+
}
123+
124+
/**
125+
* Empty constructor required by the marshalling framework.
126+
*/
127+
protected Link() {
128+
this.affordances = new ArrayList<Affordance>();
129+
}
130+
131+
/**
132+
* Returns safe copy of {@link Affordance}s.
133+
*
134+
* @return
135+
*/
136+
public List<Affordance> getAffordances() {
137+
return new ArrayList<Affordance>(Collections.unmodifiableCollection(this.affordances));
111138
}
112139

113140
/**
@@ -119,6 +146,31 @@ public Link withSelfRel() {
119146
return withRel(Link.REL_SELF);
120147
}
121148

149+
/**
150+
* Creates new {@link Link} with an additional {@link Affordance}.
151+
*
152+
* @param affordance
153+
* @return
154+
*/
155+
public Link withAffordance(Affordance affordance) {
156+
157+
List<Affordance> newAffordances = new ArrayList<Affordance>();
158+
newAffordances.addAll(this.affordances);
159+
newAffordances.add(affordance);
160+
161+
return new Link(this.href, this.rel, newAffordances);
162+
}
163+
164+
public Link withAffordances(List<Affordance> affordances) {
165+
166+
List<Affordance> newAffordances = new ArrayList<Affordance>();
167+
newAffordances.addAll(this.affordances);
168+
newAffordances.addAll(affordances);
169+
170+
return new Link(this.href, this.rel, newAffordances);
171+
}
172+
173+
122174
/**
123175
* Returns the variable names contained in the template.
124176
*

src/main/java/org/springframework/hateoas/MediaTypes.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,12 @@ public class MediaTypes {
3434
* Public constant media type for {@code application/hal+json}.
3535
*/
3636
public static final MediaType HAL_JSON = MediaType.valueOf(HAL_JSON_VALUE);
37+
38+
/**
39+
* Public constant media type for {@code application/prs.hal-forms+json}.
40+
*/
41+
public static final String HAL_FORMS_JSON_VALUE = "application/prs.hal-forms+json";
42+
43+
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
44+
3745
}

src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,20 @@
2020
import java.lang.annotation.Retention;
2121
import java.lang.annotation.RetentionPolicy;
2222
import java.lang.annotation.Target;
23+
import java.util.ArrayList;
24+
import java.util.Arrays;
25+
import java.util.List;
26+
import java.util.Map;
27+
28+
import lombok.extern.slf4j.Slf4j;
2329

2430
import org.springframework.context.ApplicationContext;
2531
import org.springframework.context.annotation.Import;
32+
import org.springframework.context.annotation.ImportSelector;
33+
import org.springframework.core.type.AnnotationMetadata;
2634
import org.springframework.hateoas.EntityLinks;
2735
import org.springframework.hateoas.LinkDiscoverer;
36+
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
2837

2938
/**
3039
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
@@ -43,7 +52,8 @@
4352
@Retention(RetentionPolicy.RUNTIME)
4453
@Target(ElementType.TYPE)
4554
@Documented
46-
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
55+
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class,
56+
EnableHypermediaSupport.HypermediaConfigurationImportSelector.class})
4757
public @interface EnableHypermediaSupport {
4858

4959
/**
@@ -58,14 +68,54 @@
5868
*
5969
* @author Oliver Gierke
6070
*/
61-
static enum HypermediaType {
71+
enum HypermediaType {
6272

6373
/**
6474
* HAL - Hypermedia Application Language.
6575
*
6676
* @see http://stateless.co/hal_specification.html
6777
* @see http://tools.ietf.org/html/draft-kelly-json-hal-05
6878
*/
69-
HAL;
79+
HAL,
80+
81+
HAL_FORMS(HalFormsConfiguration.class);
82+
83+
private final List<Class<?>> configurations;
84+
85+
HypermediaType(Class<?>... configurations) {
86+
this.configurations = Arrays.asList(configurations);
87+
}
88+
}
89+
90+
@Slf4j
91+
class HypermediaConfigurationImportSelector implements ImportSelector {
92+
93+
@Override
94+
public String[] selectImports(AnnotationMetadata metadata) {
95+
96+
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
97+
98+
HypermediaType[] types = (HypermediaType[]) attributes.get("type");
99+
100+
/**
101+
* If no types are defined inside the annotation, add them all.
102+
*/
103+
if (types.length == 0) {
104+
types = HypermediaType.values();
105+
}
106+
107+
log.debug("Registering support for hypermedia types {} according to configuration on {}",
108+
types, metadata.getClassName());
109+
110+
List<String> configurationNames = new ArrayList<String>();
111+
112+
for (HypermediaType type : types) {
113+
for (Class<?> configuration : type.configurations) {
114+
configurationNames.add(configuration.getName());
115+
}
116+
}
117+
118+
return configurationNames.toArray(new String[0]);
119+
}
70120
}
71121
}

src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java

Lines changed: 62 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@
5555
import org.springframework.hateoas.hal.CurieProvider;
5656
import org.springframework.hateoas.hal.HalLinkDiscoverer;
5757
import org.springframework.hateoas.hal.Jackson2HalModule;
58+
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
59+
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
5860
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
5961
import org.springframework.http.converter.HttpMessageConverter;
6062
import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean;
@@ -81,6 +83,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
8183
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
8284
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
8385
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
86+
private static final String HAL_FORMS_OBJECT_MAPPER_BEAN_NAME = "_halFormsObjectMapper";
8487
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
8588

8689
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
@@ -113,18 +116,11 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR
113116
}
114117

115118
if (types.contains(HypermediaType.HAL)) {
119+
registerHypermediaComponents(metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME);
120+
}
116121

117-
if (JACKSON2_PRESENT) {
118-
119-
BeanDefinitionBuilder halQueryMapperBuilder = rootBeanDefinition(ObjectMapper.class);
120-
registerSourcedBeanDefinition(halQueryMapperBuilder, metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME);
121-
122-
BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class);
123-
registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);
124-
125-
BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
126-
registerSourcedBeanDefinition(builder, metadata, registry);
127-
}
122+
if (types.contains(HypermediaType.HAL_FORMS)) {
123+
registerHypermediaComponents(metadata, registry, HAL_FORMS_OBJECT_MAPPER_BEAN_NAME);
128124
}
129125

130126
if (!types.isEmpty()) {
@@ -143,6 +139,21 @@ public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionR
143139
registerRelProviderPluginRegistryAndDelegate(registry);
144140
}
145141

142+
private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, String objectMapperBeanName) {
143+
144+
if (JACKSON2_PRESENT) {
145+
146+
BeanDefinitionBuilder queryMapperBuilder = rootBeanDefinition(ObjectMapper.class);
147+
registerSourcedBeanDefinition(queryMapperBuilder, metadata, registry, objectMapperBeanName);
148+
149+
BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class);
150+
registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);
151+
152+
BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
153+
registerSourcedBeanDefinition(builder, metadata, registry);
154+
}
155+
}
156+
146157
/**
147158
* Registers bean definitions for a {@link PluginRegistry} to capture {@link RelProvider} instances. Wraps the
148159
* registry into a {@link DelegatingRelProvider} bean definition backed by the registry.
@@ -188,6 +199,9 @@ private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType ty
188199
case HAL:
189200
definition = new RootBeanDefinition(HalLinkDiscoverer.class);
190201
break;
202+
case HAL_FORMS:
203+
definition = new RootBeanDefinition(HalFormsLinkDiscoverer.class);
204+
break;
191205
default:
192206
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
193207
}
@@ -283,22 +297,45 @@ private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessage
283297

284298
CurieProvider curieProvider = getCurieProvider(beanFactory);
285299
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
286-
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
287-
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
300+
301+
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
302+
303+
if (beanFactory.containsBean(HAL_OBJECT_MAPPER_BEAN_NAME)) {
304+
305+
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
306+
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
288307
MessageSourceAccessor.class);
289308

290-
halObjectMapper.registerModule(new Jackson2HalModule());
291-
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
309+
halObjectMapper.registerModule(new Jackson2HalModule());
310+
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
292311
linkRelationMessageSource, beanFactory));
293312

294-
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
313+
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
295314
ResourceSupport.class);
296-
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
297-
halConverter.setObjectMapper(halObjectMapper);
315+
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
316+
halConverter.setObjectMapper(halObjectMapper);
317+
result.add(halConverter);
318+
}
319+
320+
if (beanFactory.containsBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME)) {
321+
322+
ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
323+
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
324+
MessageSourceAccessor.class);
325+
326+
halFormsObjectMapper.registerModule(new Jackson2HalFormsModule());
327+
halFormsObjectMapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider,
328+
linkRelationMessageSource, true));
329+
330+
MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
331+
ResourceSupport.class);
332+
halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON));
333+
halFormsConverter.setObjectMapper(halFormsObjectMapper);
334+
result.add(halFormsConverter);
335+
}
298336

299-
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
300-
result.add(halConverter);
301337
result.addAll(converters);
338+
302339
return result;
303340
}
304341

@@ -327,14 +364,13 @@ private static class DefaultObjectMapperCustomizer implements BeanPostProcessor
327364
@Override
328365
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
329366

330-
if (!HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
331-
return bean;
367+
if (HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || HAL_FORMS_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
368+
ObjectMapper mapper = (ObjectMapper) bean;
369+
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
370+
return mapper;
332371
}
333372

334-
ObjectMapper mapper = (ObjectMapper) bean;
335-
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
336-
337-
return mapper;
373+
return bean;
338374
}
339375

340376
/*

0 commit comments

Comments
 (0)