#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:
committed by
Oliver Gierke
parent
3af698d928
commit
d6e02857f1
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user