Skip to content

Commit 4a0756d

Browse files
committed
#482 - Add support for Collection+JSON mediatype.
Introduce support for media type application/vnd.collection+json. Collection+JSON doesn't allow metadata at the top, so paging data can't be covered, however, everything else fits. Also moved a little bit more into Affordance and SpringMvcAffordance to avoid using Spring MVC annotations directly in a given mediatype's AffordanceModel. Refactored bits of HAL-FORMS to reuse the new PropertyUtils, ensuring Jackson ignore annotations are taken into consideration. Also added MockMVC tests to show HAL-FORMS and Collection+JSON working together, against the same controller.
1 parent 1e32bb5 commit 4a0756d

File tree

73 files changed

+4503
-208
lines changed

Some content is hidden

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

73 files changed

+4503
-208
lines changed

readme.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
image:https://spring.io/badges/spring-hateoas/ga.svg[http://projects.spring.io/spring-hateoas/#quick-start]
2+
image:https://spring.io/badges/spring-hateoas/snapshot.svg[http://projects.spring.io/spring-hateoas/#quick-start]
3+
4+
= Spring HATEOAS
5+
6+
This project provides some APIs to ease creating REST representations that follow the http://en.wikipedia.org/wiki/HATEOAS[HATEOAS] principle when working with Spring and especially Spring MVC. The core problem it tries to address is link creation and representation assembly.
7+
8+
== Working with Spring HATEOAS
9+
10+
Since all commits are headlined with its github issue, git will treat it as a comment. To get around this, apply the following configuration to your clone:
11+
12+
[source]
13+
----
14+
git config core.commentchar "/"
15+
----
16+
17+
== Resources
18+
19+
* Reference documentation - http://docs.spring.io/spring-hateoas/docs/current/reference/html/[html], http://docs.spring.io/spring-hateoas/docs/current/reference/pdf/spring-hateoas-reference.pdf[pdf]
20+
* http://docs.spring.io/spring-hateoas/docs/current-SNAPSHOT/api/[JavaDoc]
21+
* https://spring.io/guides/gs/rest-hateoas/[Getting started guide]

readme.md

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/main/asciidoc/index.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ Since the purpose of the `CurieProvider` API is to allow for automatic curie cre
363363
[[client.traverson]]
364364
=== Traverson
365365

366-
As of version 0.11 Spring HATEOAS provides an API for client side service traversal inspired by the https://blog.codecentric.de/en/2013/11/traverson/[Traverson JavaScript library].
366+
Spring HATEOAS provides an API for client side service traversal inspired by the https://blog.codecentric.de/en/2013/11/traverson/[Traverson JavaScript library].
367367

368368
[source, java]
369369
----

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
*/
1616
package org.springframework.hateoas;
1717

18+
import java.util.List;
19+
20+
import org.springframework.core.MethodParameter;
1821
import org.springframework.http.HttpMethod;
1922
import org.springframework.http.MediaType;
2023

@@ -47,4 +50,17 @@ public interface Affordance {
4750
* @return
4851
*/
4952
<T extends AffordanceModel> T getAffordanceModel(MediaType mediaType);
53+
54+
/**
55+
* Get a listing of {@link MethodParameter}s.
56+
*
57+
* @return
58+
*/
59+
List<MethodParameter> getInputMethodParameters();
60+
61+
/**
62+
* Get a listing of {@link QueryParameter}s.
63+
* @return
64+
*/
65+
List<QueryParameter> getQueryMethodParameters();
5066
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
*/
5050
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
5151
@JsonInclude(JsonInclude.Include.NON_NULL)
52-
@JsonIgnoreProperties("templated")
52+
@JsonIgnoreProperties(value = "templated", ignoreUnknown = true)
5353
@AllArgsConstructor(access = AccessLevel.PACKAGE)
5454
@Getter
5555
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation", "affordances" })

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,13 @@ public class MediaTypes {
6767
*/
6868
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
6969

70+
/**
71+
* A String equivalent of {@link MediaTypes#COLLECTION_JSON}.
72+
*/
73+
public static final String COLLECTION_JSON_VALUE = "application/vnd.collection+json";
74+
75+
/**
76+
* Public constant media type for {@code application/vnd.collection+json}.
77+
*/
78+
public static final MediaType COLLECTION_JSON = MediaType.valueOf(COLLECTION_JSON_VALUE);
7079
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright 2018 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 lombok.Data;
19+
import lombok.RequiredArgsConstructor;
20+
21+
/**
22+
* Web framework-neutral representation of a web request's query parameter (http://example.com?name=foo).
23+
*
24+
* @author Greg Turnquist
25+
*/
26+
@Data
27+
@RequiredArgsConstructor
28+
public class QueryParameter {
29+
30+
private final String name;
31+
private final boolean required;
32+
private final String value;
33+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* A simple {@link Resource} wrapping a domain object and adding links to it.
3030
*
3131
* @author Oliver Gierke
32+
* @author Greg Turnquist
3233
*/
3334
@XmlRootElement
3435
public class Resource<T> extends ResourceSupport {
@@ -38,7 +39,7 @@ public class Resource<T> extends ResourceSupport {
3839
/**
3940
* Creates an empty {@link Resource}.
4041
*/
41-
Resource() {
42+
protected Resource() {
4243
this.content = null;
4344
}
4445

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2015 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.collectionjson;
17+
18+
import lombok.AccessLevel;
19+
import lombok.Value;
20+
import lombok.experimental.Wither;
21+
22+
import java.util.List;
23+
24+
import org.springframework.hateoas.Link;
25+
26+
import com.fasterxml.jackson.annotation.JsonCreator;
27+
import com.fasterxml.jackson.annotation.JsonInclude;
28+
import com.fasterxml.jackson.annotation.JsonInclude.Include;
29+
import com.fasterxml.jackson.annotation.JsonProperty;
30+
31+
/**
32+
* Representation of the "collection" part of a Collection+JSON document.
33+
*
34+
* @author Greg Turnquist
35+
*/
36+
@Value
37+
@Wither(AccessLevel.PACKAGE)
38+
class CollectionJson<T> {
39+
40+
private String version;
41+
private String href;
42+
43+
@JsonInclude(Include.NON_EMPTY)
44+
private List<Link> links;
45+
46+
@JsonInclude(Include.NON_EMPTY)
47+
private List<CollectionJsonItem<T>> items;
48+
49+
@JsonInclude(Include.NON_EMPTY)
50+
private List<CollectionJsonQuery> queries;
51+
52+
@JsonInclude(Include.NON_NULL)
53+
private CollectionJsonTemplate template;
54+
55+
@JsonInclude(Include.NON_NULL)
56+
private CollectionJsonError error;
57+
58+
@JsonCreator
59+
CollectionJson(@JsonProperty("version") String version, @JsonProperty("href") String href,
60+
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
61+
@JsonProperty("queries") List<CollectionJsonQuery> queries,
62+
@JsonProperty("template") CollectionJsonTemplate template,
63+
@JsonProperty("error") CollectionJsonError error) {
64+
65+
this.version = version;
66+
this.href = href;
67+
this.links = links;
68+
this.items = items;
69+
this.queries = queries;
70+
this.template = template;
71+
this.error = error;
72+
}
73+
74+
CollectionJson() {
75+
this("1.0", null, null, null, null, null, null);
76+
}
77+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
/*
2+
* Copyright 2018 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.collectionjson;
17+
18+
import lombok.Getter;
19+
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.List;
24+
import java.util.stream.Collectors;
25+
26+
import org.springframework.core.ResolvableType;
27+
import org.springframework.hateoas.Affordance;
28+
import org.springframework.hateoas.AffordanceModel;
29+
import org.springframework.hateoas.MediaTypes;
30+
import org.springframework.hateoas.QueryParameter;
31+
import org.springframework.hateoas.support.PropertyUtils;
32+
import org.springframework.http.HttpMethod;
33+
import org.springframework.http.MediaType;
34+
import org.springframework.web.util.UriComponents;
35+
36+
/**
37+
* @author Greg Turnquist
38+
*/
39+
class CollectionJsonAffordanceModel implements AffordanceModel {
40+
41+
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
42+
HttpMethod.PATCH);
43+
44+
private final Affordance affordance;
45+
private final UriComponents components;
46+
private final @Getter List<CollectionJsonData> inputProperties;
47+
private final @Getter List<CollectionJsonData> queryProperties;
48+
49+
CollectionJsonAffordanceModel(Affordance affordance, UriComponents components) {
50+
51+
this.affordance = affordance;
52+
this.components = components;
53+
54+
this.inputProperties = determineAffordanceInputs();
55+
this.queryProperties = determineQueryProperties();
56+
}
57+
58+
@Override
59+
public Collection<MediaType> getMediaTypes() {
60+
return Collections.singleton(MediaTypes.COLLECTION_JSON);
61+
}
62+
63+
public String getRel() {
64+
return isHttpGetMethod() ? this.affordance.getName() : "";
65+
}
66+
67+
public String getUri() {
68+
return isHttpGetMethod() ? this.components.toUriString() : "";
69+
}
70+
71+
/**
72+
* Transform a list of {@link QueryParameter}s into a list of {@link CollectionJsonData} objects.
73+
*
74+
* @return
75+
*/
76+
private List<CollectionJsonData> determineQueryProperties() {
77+
78+
if (!isHttpGetMethod()) {
79+
return Collections.emptyList();
80+
}
81+
82+
return this.affordance.getQueryMethodParameters().stream()
83+
.map(queryProperty -> new CollectionJsonData().withName(queryProperty.getName()).withValue(""))
84+
.collect(Collectors.toList());
85+
}
86+
87+
private boolean isHttpGetMethod() {
88+
return this.affordance.getHttpMethod().equals(HttpMethod.GET);
89+
}
90+
91+
/**
92+
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
93+
* Then transform them into a list of {@link CollectionJsonData} objects.
94+
*/
95+
private List<CollectionJsonData> determineAffordanceInputs() {
96+
97+
if (!METHODS_FOR_INPUT_DETECTION.contains(affordance.getHttpMethod())) {
98+
return Collections.emptyList();
99+
}
100+
101+
return this.affordance.getInputMethodParameters().stream()
102+
.findFirst()
103+
.map(methodParameter -> {
104+
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
105+
return PropertyUtils.findProperties(resolvableType);
106+
})
107+
.orElse(Collections.emptyList())
108+
.stream()
109+
.map(property -> new CollectionJsonData().withName(property).withValue(""))
110+
.collect(Collectors.toList());
111+
}
112+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2018 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.collectionjson;
17+
18+
import lombok.Getter;
19+
20+
import org.springframework.hateoas.Affordance;
21+
import org.springframework.hateoas.AffordanceModel;
22+
import org.springframework.hateoas.MediaTypes;
23+
import org.springframework.hateoas.core.AffordanceModelFactory;
24+
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
25+
import org.springframework.http.MediaType;
26+
import org.springframework.web.util.UriComponents;
27+
28+
/**
29+
* @author Greg Turnquist
30+
*/
31+
class CollectionJsonAffordanceModelFactory implements AffordanceModelFactory {
32+
33+
private final @Getter MediaType mediaType = MediaTypes.COLLECTION_JSON;
34+
35+
/**
36+
* Look up the {@link AffordanceModel} for this factory.
37+
*
38+
* @param affordance
39+
* @param invocationValue
40+
* @param components
41+
* @return
42+
*/
43+
@Override
44+
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
45+
return new CollectionJsonAffordanceModel(affordance, components);
46+
}
47+
}

0 commit comments

Comments
 (0)