Skip to content

Add support for Collection+JSON media type #482

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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