Skip to content

Commit 580973e

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 580973e

File tree

62 files changed

+3589
-43
lines changed

Some content is hidden

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

62 files changed

+3589
-43
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,72 @@
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.List;
19+
import java.util.Map;
20+
21+
import org.springframework.http.MediaType;
22+
23+
/**
24+
* Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation.
25+
*
26+
* @author Greg Turnquist
27+
*/
28+
public interface Affordance {
29+
30+
/**
31+
* HTTP method this affordance covers. For multiple methods, add multiple {@link Affordance}s.
32+
*
33+
* @return
34+
*/
35+
String getHttpMethod();
36+
37+
/**
38+
* Name for the REST action this {@link Affordance} can take.
39+
*
40+
* @return
41+
*/
42+
String getName();
43+
44+
/**
45+
* Are the properties on this {@link Affordance} required or not? Distinguish between PUT (all required)
46+
* vs. PATCH (none required).
47+
*
48+
* @return
49+
*/
50+
boolean isRequired();
51+
52+
/**
53+
* Collection of the {@link Affordance}'s properties, mapped by {@literal name} and {@link Class}.
54+
* For example, a class Employee { String name, String role } would be mapped as:
55+
* Map(name -> java.lang.String, role -> java.lang.String)
56+
*
57+
* @return
58+
*/
59+
Map<String, Class<?>> getProperties();
60+
61+
/**
62+
* The URI of the {@link Affordance} because certain mediatypes need to list it. Other
63+
* mediatypes need to validate that the URI matches the {@literal self} link.
64+
*
65+
* @return
66+
*/
67+
String getUri();
68+
69+
List<AffordanceModel> getAffordanceModel(MediaType mediaType);
70+
71+
void addAffordanceModels(MediaType mediaType, List<AffordanceModel> affordanceModels);
72+
}
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,37 @@
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.List;
19+
20+
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
21+
import org.springframework.http.MediaType;
22+
import org.springframework.plugin.core.Plugin;
23+
24+
/**
25+
* @author Greg Turnquist
26+
*/
27+
public abstract class AffordanceModelFactory implements Plugin<MediaType> {
28+
29+
abstract public MediaType getMediaType();
30+
31+
abstract public List<AffordanceModel> findAffordanceModels(Affordance affordance, MethodInvocation invocationValue);
32+
33+
@Override
34+
public boolean supports(MediaType delimiter) {
35+
return delimiter != null && delimiter.equals(this.getMediaType());
36+
}
37+
}

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

+12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
*
2323
* @author Oliver Gierke
2424
* @author Przemek Nowak
25+
* @author Greg Turnquist
2526
*/
2627
public class MediaTypes {
2728

@@ -34,4 +35,15 @@ public class MediaTypes {
3435
* Public constant media type for {@code application/hal+json}.
3536
*/
3637
public static final MediaType HAL_JSON = MediaType.valueOf(HAL_JSON_VALUE);
38+
39+
/**
40+
* Public constant media type for {@code application/prs.hal-forms+json}.
41+
*/
42+
public static final String HAL_FORMS_JSON_VALUE = "application/prs.hal-forms+json";
43+
44+
/**
45+
* Public constant media type for {@code applicatino/prs.hal-forms+json}.
46+
*/
47+
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
48+
3749
}

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

+58-3
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
@@ -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
/**
@@ -58,14 +69,58 @@
5869
*
5970
* @author Oliver Gierke
6071
*/
61-
static enum HypermediaType {
72+
enum HypermediaType {
6273

6374
/**
6475
* HAL - Hypermedia Application Language.
6576
*
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(HalFormsConfiguration.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)