Skip to content

Commit

Permalink
#482 - Add support for Collection+JSON mediatype.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
gregturn authored and odrotbohm committed Apr 18, 2018
1 parent 3af698d commit d6e0285
Show file tree
Hide file tree
Showing 74 changed files with 4,633 additions and 205 deletions.
21 changes: 21 additions & 0 deletions readme.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
image:https://spring.io/badges/spring-hateoas/ga.svg[http://projects.spring.io/spring-hateoas/#quick-start]
image:https://spring.io/badges/spring-hateoas/snapshot.svg[http://projects.spring.io/spring-hateoas/#quick-start]

= Spring HATEOAS

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.

== Working with Spring HATEOAS

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:

[source]
----
git config core.commentchar "/"
----

== Resources

* 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]
* http://docs.spring.io/spring-hateoas/docs/current-SNAPSHOT/api/[JavaDoc]
* https://spring.io/guides/gs/rest-hateoas/[Getting started guide]
11 changes: 0 additions & 11 deletions readme.md

This file was deleted.

2 changes: 1 addition & 1 deletion src/main/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ Since the purpose of the `CurieProvider` API is to allow for automatic curie cre
[[client.traverson]]
=== Traverson

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].
Spring HATEOAS provides an API for client side service traversal inspired by the https://blog.codecentric.de/en/2013/11/traverson/[Traverson JavaScript library].

[source, java]
----
Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/springframework/hateoas/Affordance.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
*/
package org.springframework.hateoas;

import java.util.List;

import org.springframework.core.MethodParameter;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;

Expand Down Expand Up @@ -47,4 +50,17 @@ public interface Affordance {
* @return
*/
<T extends AffordanceModel> T getAffordanceModel(MediaType mediaType);

/**
* Get a listing of {@link MethodParameter}s.
*
* @return
*/
List<MethodParameter> getInputMethodParameters();

/**
* Get a listing of {@link QueryParameter}s.
* @return
*/
List<QueryParameter> getQueryMethodParameters();
}
2 changes: 1 addition & 1 deletion src/main/java/org/springframework/hateoas/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
*/
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties("templated")
@JsonIgnoreProperties(value = "templated", ignoreUnknown = true)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation", "affordances" })
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/springframework/hateoas/MediaTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,13 @@ public class MediaTypes {
*/
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);

/**
* A String equivalent of {@link MediaTypes#COLLECTION_JSON}.
*/
public static final String COLLECTION_JSON_VALUE = "application/vnd.collection+json";

/**
* Public constant media type for {@code application/vnd.collection+json}.
*/
public static final MediaType COLLECTION_JSON = MediaType.valueOf(COLLECTION_JSON_VALUE);
}
33 changes: 33 additions & 0 deletions src/main/java/org/springframework/hateoas/QueryParameter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2018 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 lombok.Data;
import lombok.RequiredArgsConstructor;

/**
* Web framework-neutral representation of a web request's query parameter (http://example.com?name=foo).
*
* @author Greg Turnquist
*/
@Data
@RequiredArgsConstructor
public class QueryParameter {

private final String name;
private final boolean required;
private final String value;
}
3 changes: 2 additions & 1 deletion src/main/java/org/springframework/hateoas/Resource.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
* A simple {@link Resource} wrapping a domain object and adding links to it.
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@XmlRootElement
public class Resource<T> extends ResourceSupport {
Expand All @@ -38,7 +39,7 @@ public class Resource<T> extends ResourceSupport {
/**
* Creates an empty {@link Resource}.
*/
Resource() {
protected Resource() {
this.content = null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright 2015 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.collectionjson;

import lombok.AccessLevel;
import lombok.Value;
import lombok.experimental.Wither;

import java.util.List;

import org.springframework.hateoas.Link;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
* Representation of the "collection" part of a Collection+JSON document.
*
* @author Greg Turnquist
*/
@Value
@Wither(AccessLevel.PACKAGE)
class CollectionJson<T> {

private String version;
private String href;

@JsonInclude(Include.NON_EMPTY)
private List<Link> links;

@JsonInclude(Include.NON_EMPTY)
private List<CollectionJsonItem<T>> items;

@JsonInclude(Include.NON_EMPTY)
private List<CollectionJsonQuery> queries;

@JsonInclude(Include.NON_NULL)
private CollectionJsonTemplate template;

@JsonInclude(Include.NON_NULL)
private CollectionJsonError error;

@JsonCreator
CollectionJson(@JsonProperty("version") String version, @JsonProperty("href") String href,
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
@JsonProperty("queries") List<CollectionJsonQuery> queries,
@JsonProperty("template") CollectionJsonTemplate template,
@JsonProperty("error") CollectionJsonError error) {

this.version = version;
this.href = href;
this.links = links;
this.items = items;
this.queries = queries;
this.template = template;
this.error = error;
}

CollectionJson() {
this("1.0", null, null, null, null, null, null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/*
* Copyright 2018 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.collectionjson;

import lombok.Getter;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

import org.springframework.core.ResolvableType;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.QueryParameter;
import org.springframework.hateoas.support.PropertyUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriComponents;

/**
* @author Greg Turnquist
*/
class CollectionJsonAffordanceModel implements AffordanceModel {

private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
HttpMethod.PATCH);

private final Affordance affordance;
private final UriComponents components;
private final @Getter List<CollectionJsonData> inputProperties;
private final @Getter List<CollectionJsonData> queryProperties;

CollectionJsonAffordanceModel(Affordance affordance, UriComponents components) {

this.affordance = affordance;
this.components = components;

this.inputProperties = determineAffordanceInputs();
this.queryProperties = determineQueryProperties();
}

@Override
public Collection<MediaType> getMediaTypes() {
return Collections.singleton(MediaTypes.COLLECTION_JSON);
}

public String getRel() {
return isHttpGetMethod() ? this.affordance.getName() : "";
}

public String getUri() {
return isHttpGetMethod() ? this.components.toUriString() : "";
}

/**
* Transform a list of {@link QueryParameter}s into a list of {@link CollectionJsonData} objects.
*
* @return
*/
private List<CollectionJsonData> determineQueryProperties() {

if (!isHttpGetMethod()) {
return Collections.emptyList();
}

return this.affordance.getQueryMethodParameters().stream()
.map(queryProperty -> new CollectionJsonData().withName(queryProperty.getName()).withValue(""))
.collect(Collectors.toList());
}

private boolean isHttpGetMethod() {
return this.affordance.getHttpMethod().equals(HttpMethod.GET);
}

/**
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
* Then transform them into a list of {@link CollectionJsonData} objects.
*/
private List<CollectionJsonData> determineAffordanceInputs() {

if (!METHODS_FOR_INPUT_DETECTION.contains(affordance.getHttpMethod())) {
return Collections.emptyList();
}

return this.affordance.getInputMethodParameters().stream()
.findFirst()
.map(methodParameter -> {
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
return PropertyUtils.findProperties(resolvableType);
})
.orElse(Collections.emptyList())
.stream()
.map(property -> new CollectionJsonData().withName(property).withValue(""))
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2018 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.collectionjson;

import lombok.Getter;

import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.core.AffordanceModelFactory;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriComponents;

/**
* @author Greg Turnquist
*/
class CollectionJsonAffordanceModelFactory implements AffordanceModelFactory {

private final @Getter MediaType mediaType = MediaTypes.COLLECTION_JSON;

/**
* Look up the {@link AffordanceModel} for this factory.
*
* @param affordance
* @param invocationValue
* @param components
* @return
*/
@Override
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
return new CollectionJsonAffordanceModel(affordance, components);
}
}
Loading

0 comments on commit d6e0285

Please sign in to comment.