#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.
This commit is contained in:
Greg Turnquist
2015-08-26 15:09:22 -05:00
committed by Oliver Gierke
parent 3af698d928
commit d6e02857f1
74 changed files with 4632 additions and 204 deletions

View File

@@ -15,29 +15,21 @@
*/
package org.springframework.hateoas.hal.forms;
import lombok.extern.slf4j.Slf4j;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.support.PropertyUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.util.UriComponents;
/**
@@ -45,40 +37,41 @@ import org.springframework.web.util.UriComponents;
*
* @author Greg Turnquist
*/
@Slf4j
class HalFormsAffordanceModel implements AffordanceModel {
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
HttpMethod.PATCH);
private final Affordance affordance;
private final UriComponents components;
private final boolean required;
private final Map<String, Class<?>> properties;
private final List<String> properties;
public HalFormsAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
public HalFormsAffordanceModel(Affordance affordance, UriComponents components) {
this.affordance = affordance;
this.components = components;
this.required = determineRequired(affordance.getHttpMethod());
this.properties = METHODS_FOR_INPUT_DETECTTION.contains(affordance.getHttpMethod()) //
? determineAffordanceInputs(invocationValue.getMethod()) //
: Collections.<String, Class<?>> emptyMap();
? determineAffordanceInputs() //
: Collections.emptyList();
}
/**
* Transform the details of the Spring MVC method's {@link RequestBody} into a collection of
* Transform the details of the REST method's {@link MethodParameters} into
* {@link HalFormsProperty}s.
*
* @return
*/
public List<HalFormsProperty> getProperties() {
return properties.entrySet().stream() //
.map(entry -> entry.getKey()) //
.map(key -> HalFormsProperty.named(key).withRequired(required)).collect(Collectors.toList());
return properties.stream() //
.map(name -> HalFormsProperty.named(name).withRequired(required)) //
.collect(Collectors.toList());
}
public String getPath() {
return components.getPath();
public String getURI() {
return components.toUriString();
}
/**
@@ -91,7 +84,7 @@ class HalFormsAffordanceModel implements AffordanceModel {
Assert.notNull(path, "Path must not be null!");
return getPath().equals(path);
return getURI().equals(path);
}
/*
@@ -115,39 +108,15 @@ class HalFormsAffordanceModel implements AffordanceModel {
/**
* Look at the inputs for a Spring MVC controller method to decide the {@link Affordance}'s properties.
*
* @param method - {@link Method} of the Spring MVC controller tied to this affordance
*/
private Map<String, Class<?>> determineAffordanceInputs(Method method) {
private List<String> determineAffordanceInputs() {
if (method == null) {
return Collections.emptyMap();
}
LOG.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName());
Map<String, Class<?>> properties = new TreeMap<>();
MethodParameters parameters = new MethodParameters(method);
for (MethodParameter parameter : parameters.getParametersWith(RequestBody.class)) {
Class<?> parameterType = parameter.getParameterType();
LOG.debug("\tRequest body: " + parameterType.getCanonicalName() + "(");
for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(parameterType)) {
if (!descriptor.getName().equals("class")) {
LOG.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName());
properties.put(descriptor.getName(), descriptor.getPropertyType());
}
}
LOG.debug(")");
}
LOG.debug("Assembled " + this.toString());
return properties;
return this.affordance.getInputMethodParameters().stream()
.findFirst()
.map(methodParameter -> {
ResolvableType resolvableType = ResolvableType.forMethodParameter(methodParameter);
return PropertyUtils.findProperties(resolvableType);
})
.orElse(Collections.emptyList());
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.hateoas.hal.forms;
import lombok.Getter;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.MediaTypes;
@@ -31,6 +33,8 @@ import org.springframework.web.util.UriComponents;
*/
class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
private final @Getter MediaType mediaType = MediaTypes.HAL_FORMS_JSON;
/*
* (non-Javadoc)
* @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(org.springframework.hateoas.Affordance, org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation, org.springframework.web.util.UriComponents)
@@ -38,15 +42,6 @@ class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
@Override
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue,
UriComponents components) {
return new HalFormsAffordanceModel(affordance, invocationValue, components);
}
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/
@Override
public boolean supports(MediaType mediaType) {
return MediaTypes.HAL_FORMS_JSON.equals(mediaType);
return new HalFormsAffordanceModel(affordance, components);
}
}

View File

@@ -16,12 +16,9 @@
package org.springframework.hateoas.hal.forms;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.Link;
@@ -224,18 +221,12 @@ class HalFormsSerializers {
*/
private static void validate(ResourceSupport resource, Affordance affordance, HalFormsAffordanceModel model) {
try {
String affordanceUri = model.getURI();
String selfLinkUri = resource.getRequiredLink(Link.REL_SELF).getHref();
Optional<Link> selfLink = resource.getLink(Link.REL_SELF);
URI selfLinkUri = new URI(selfLink.map(link -> link.expand().getHref()).orElse(""));
if (!model.hasPath(selfLinkUri.getPath())) {
throw new IllegalStateException("Affordance's URI " + model.getPath() + " doesn't match self link "
+ selfLinkUri.getPath() + " as expected in HAL-FORMS");
}
} catch (URISyntaxException e) {
throw new RuntimeException(e);
if (!affordanceUri.equals(selfLinkUri)) {
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link "
+ selfLinkUri + " as expected in HAL-FORMS");
}
}
}