Skip to content

Commit ca6b31f

Browse files
committed
#340 - Add 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, #71
1 parent 5a71012 commit ca6b31f

File tree

65 files changed

+3770
-70
lines changed

Some content is hidden

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

65 files changed

+3770
-70
lines changed

src/main/asciidoc/index.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ A `RelProvider` is exposed as Spring bean when using `@EnableHypermediaSupport`
316316
[[spis.curie-provider]]
317317
=== CurieProvider API
318318

319-
The http://tools.ietf.org/html/rfc5988=section-4[Web Linking RFC] describes registered and extension link relation types. Registered rels are well-known strings registered with the http://www.iana.org/assignments/link-relations/link-relations.xhtml[IANA registry of link relation types]. Extension rels can be used by applications that do not wish to register a relation type. They are a URI that uniquely identifies the relation type. The rel URI can be serialized as a compact URI or http://www.w3.org/TR/curie[Curie]. E.g. a curie `ex:persons` stands for the link relation type `http://example.com/rels/persons` if `ex` is defined as `http://example.com/rels/{rels}`. If curies are used, the base URI must be present in the response scope.
319+
The http://tools.ietf.org/html/rfc5988=section-4[Web Linking RFC] describes registered and extension link relation types. Registered rels are well-known strings registered with the http://www.iana.org/assignments/link-relations/link-relations.xhtml[IANA registry of link relation types]. Extension rels can be used by applications that do not wish to register a relation type. They are a URI that uniquely identifies the relation type. The rel URI can be serialized as a compact URI or http://www.w3.org/TR/curie[Curie]. E.g. a curie `ex:persons` stands for the link relation type `http://example.com/rels/persons` if `ex` is defined as `http://example.com/rels/{rel}`. If curies are used, the base URI must be present in the response scope.
320320

321321
The rels created by the default `RelProvider` are extension relation types and as such must be URIs, which can cause a lot of overhead. The `CurieProvider` API takes care of that: it allows to define a base URI as URI template and a prefix which stands for that base URI. If a `CurieProvider` is present, the `RelProvider` prepends all rels with the curie prefix. Furthermore a `curies` link is automatically added to the HAL resource.
322322

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 org.springframework.http.MediaType;
19+
20+
/**
21+
* Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation.
22+
*
23+
* @author Greg Turnquist
24+
*/
25+
public interface Affordance {
26+
27+
/**
28+
* HTTP method this affordance covers. For multiple methods, add multiple {@link Affordance}s.
29+
*
30+
* @return
31+
*/
32+
String getHttpMethod();
33+
34+
/**
35+
* Name for the REST action this {@link Affordance} can take.
36+
*
37+
* @return
38+
*/
39+
String getName();
40+
41+
/**
42+
* Look up the {@link AffordanceModel} for the requested {@link MediaType}.
43+
*
44+
* @param mediaType
45+
* @return
46+
*/
47+
AffordanceModel getAffordanceModel(MediaType mediaType);
48+
49+
/**
50+
* Add a new {@link AffordanceModel} for a given {@link MediaType}.
51+
*
52+
* @param mediaType
53+
* @param affordanceModel
54+
*/
55+
void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel);
56+
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
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+
/**
19+
* Marker interface for mediatypes to build up type-specific details for an {@link Affordance}
20+
*
21+
* @author Greg Turnquist
22+
*/
23+
public interface AffordanceModel {
24+
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
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 org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
19+
import org.springframework.http.MediaType;
20+
import org.springframework.plugin.core.Plugin;
21+
import org.springframework.web.util.UriComponents;
22+
23+
/**
24+
* TODO: Replace this with an interface and a default implementation of {@link #supports(MediaType)} in Java 8.
25+
*
26+
* @author Greg Turnquist
27+
*/
28+
public abstract class AffordanceModelFactory implements Plugin<MediaType> {
29+
30+
/**
31+
* Look up the {@link MediaType} of this factory.
32+
*
33+
* @return
34+
*/
35+
abstract public MediaType getMediaType();
36+
37+
/**
38+
* Look up the {@link AffordanceModel} for this factory.
39+
*
40+
* @param affordance
41+
* @param invocationValue
42+
* @param components
43+
* @return
44+
*/
45+
abstract public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components);
46+
47+
/**
48+
* Find factories based on {@link MediaType}.
49+
*
50+
* @param delimiter
51+
* @return
52+
*/
53+
@Override
54+
public boolean supports(MediaType delimiter) {
55+
return delimiter != null && delimiter.equals(this.getMediaType());
56+
}
57+
}

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

+63-3
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@
1919
import lombok.AllArgsConstructor;
2020
import lombok.EqualsAndHashCode;
2121
import lombok.Getter;
22-
import lombok.NoArgsConstructor;
2322
import lombok.experimental.Wither;
2423

2524
import java.io.Serializable;
25+
import java.util.ArrayList;
2626
import java.util.Collections;
2727
import java.util.HashMap;
2828
import java.util.List;
@@ -34,6 +34,7 @@
3434
import javax.xml.bind.annotation.XmlTransient;
3535
import javax.xml.bind.annotation.XmlType;
3636

37+
import org.springframework.hateoas.core.LinkBuilderSupport;
3738
import org.springframework.util.Assert;
3839
import org.springframework.util.StringUtils;
3940

@@ -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,39 @@ public Link withSelfRel() {
119146
return withRel(Link.REL_SELF);
120147
}
121148

149+
/**
150+
* Create 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.rel, this.href, this.hreflang ,this.media, this.title, this.type,
162+
this.deprecation, this.template, newAffordances);
163+
}
164+
165+
/**
166+
* Create new {@link Link} with additional {@link Affordance}s.
167+
*
168+
* @param affordances
169+
* @return
170+
*/
171+
public Link addAffordances(List<Affordance> affordances) {
172+
173+
List<Affordance> newAffordances = new ArrayList<Affordance>();
174+
newAffordances.addAll(this.affordances);
175+
newAffordances.addAll(affordances);
176+
177+
return new Link(this.rel, this.href, this.hreflang ,this.media, this.title, this.type,
178+
this.deprecation, this.template, newAffordances);
179+
}
180+
181+
122182
/**
123183
* Returns the variable names contained in the template.
124184
*

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

+14
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,10 @@
2222
*
2323
* @author Oliver Gierke
2424
* @author Przemek Nowak
25+
<<<<<<< HEAD
2526
* @author Drummond Dawson
27+
=======
28+
>>>>>>> f5bf966... #340 - Add new Affordances API + HAL-FORMS mediatype.
2629
* @author Greg Turnquist
2730
*/
2831
public class MediaTypes {
@@ -56,4 +59,15 @@ public class MediaTypes {
5659
* Public constant media type for {@code application/alps+json}.
5760
*/
5861
public static final MediaType ALPS_JSON = MediaType.parseMediaType(ALPS_JSON_VALUE);
62+
63+
/**
64+
* Public constant media type for {@code application/prs.hal-forms+json}.
65+
*/
66+
public static final String HAL_FORMS_JSON_VALUE = "application/prs.hal-forms+json";
67+
68+
/**
69+
* Public constant media type for {@code applicatino/prs.hal-forms+json}.
70+
*/
71+
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
72+
5973
}

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

+57-2
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.HalFormsWebMvcConfigurer;
2837

2938
/**
3039
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
@@ -39,11 +48,13 @@
3948
* @see LinkDiscoverer
4049
* @see EntityLinks
4150
* @author Oliver Gierke
51+
* @author Greg Turnquist
4252
*/
4353
@Retention(RetentionPolicy.RUNTIME)
4454
@Target(ElementType.TYPE)
4555
@Documented
46-
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
56+
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class,
57+
EnableHypermediaSupport.HypermediaConfigurationImportSelector.class})
4758
public @interface EnableHypermediaSupport {
4859

4960
/**
@@ -66,6 +77,50 @@ enum HypermediaType {
6677
* @see http://stateless.co/hal_specification.html
6778
* @see http://tools.ietf.org/html/draft-kelly-json-hal-05
6879
*/
69-
HAL;
80+
HAL,
81+
82+
/**
83+
* HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support
84+
* @see https://rwcbook.github.io/hal-forms/
85+
*/
86+
HAL_FORMS(HalFormsWebMvcConfigurer.class);
87+
88+
private final List<Class<?>> configurations;
89+
90+
HypermediaType(Class<?>... configurations) {
91+
this.configurations = Arrays.asList(configurations);
92+
}
93+
}
94+
95+
@Slf4j
96+
class HypermediaConfigurationImportSelector implements ImportSelector {
97+
98+
@Override
99+
public String[] selectImports(AnnotationMetadata metadata) {
100+
101+
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
102+
103+
HypermediaType[] types = (HypermediaType[]) attributes.get("type");
104+
105+
/**
106+
* If no types are defined inside the annotation, add them all.
107+
*/
108+
if (types.length == 0) {
109+
types = HypermediaType.values();
110+
}
111+
112+
log.debug("Registering support for hypermedia types {} according to configuration on {}",
113+
types, metadata.getClassName());
114+
115+
List<String> configurationNames = new ArrayList<String>();
116+
117+
for (HypermediaType type : types) {
118+
for (Class<?> configuration : type.configurations) {
119+
configurationNames.add(configuration.getName());
120+
}
121+
}
122+
123+
return configurationNames.toArray(new String[0]);
124+
}
70125
}
71126
}

0 commit comments

Comments
 (0)