#340 - Polishing.

Moved AffordanceModelFactory into core package as it's SPI. Switched to Spring Factories lookup of implementation classes so that we avoid a package dependency between the MVC package and the media type specific packages. Removed reference to MediaType from AffordanceModelFactory to AffordanceModel so that a factory can even provide models for different MediaTypes (i.e. different flavors of the same one, e.g. HAL Forms for JSON and XML). Also removed addAffordanceModel(…) from Affordance to not force the implementations into mutability. Made most of the affordance building API types package protected. HalFormsAffordanceModel now uses MethodParameters abstraction to simplify model parsing code.

Tweaked HAL forms model to work with factory methods for required properties and wither methods to add optional properties. Tweaked and inlined mixin types in Jackson module for HAL forms.

Slight API polishing on Link to make sure Affordance collecting methods are not named with….

Tweaked Lombok setup to use all caps for logger constants. Removed deprecation warnings in Jackson2HalModule.
This commit is contained in:
Oliver Gierke
2017-08-28 16:47:26 +02:00
parent 70448a8540
commit 092ceb987f
41 changed files with 836 additions and 732 deletions

View File

@@ -15,12 +15,14 @@
*/
package org.springframework.hateoas;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
/**
* Abstract representation of an action a link is able to take. Web frameworks must provide concrete implementation.
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
public interface Affordance {
@@ -29,7 +31,7 @@ public interface Affordance {
*
* @return
*/
String getHttpMethod();
HttpMethod getHttpMethod();
/**
* Name for the REST action this {@link Affordance} can take.
@@ -44,14 +46,5 @@ public interface Affordance {
* @param mediaType
* @return
*/
AffordanceModel getAffordanceModel(MediaType mediaType);
/**
* Add a new {@link AffordanceModel} for a given {@link MediaType}.
*
* @param mediaType
* @param affordanceModel
*/
void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel);
<T extends AffordanceModel> T getAffordanceModel(MediaType mediaType);
}

View File

@@ -15,11 +15,23 @@
*/
package org.springframework.hateoas;
import java.util.Collection;
import org.springframework.http.MediaType;
/**
* Marker interface for mediatypes to build up type-specific details for an {@link Affordance}
* An affordance model is a media type specific description of an affordance.
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
public interface AffordanceModel {
/**
* The media types this is a model for. Can be multiple ones as often media types come in different flavors like an
* XML and JSON one and in simple cases a single model might serve them all.
*
* @return will never be {@literal null}.
*/
Collection<MediaType> getMediaTypes();
}

View File

@@ -34,12 +34,12 @@ import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
/**
* Value object for links.
@@ -48,6 +48,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
* @author Greg Turnquist
*/
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties("templated")
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@@ -113,11 +114,11 @@ public class Link implements Serializable {
}
public Link(String href, String rel, List<Affordance> affordances) {
this(href, rel);
Assert.notNull(affordances, "affordances must not be null!");
this.affordances = affordances;
}
@@ -134,7 +135,7 @@ public class Link implements Serializable {
* @return
*/
public List<Affordance> getAffordances() {
return new ArrayList<Affordance>(Collections.unmodifiableCollection(this.affordances));
return Collections.unmodifiableList(this.affordances);
}
/**
@@ -149,35 +150,46 @@ public class Link implements Serializable {
/**
* Create new {@link Link} with an additional {@link Affordance}.
*
* @param affordance
* @param affordance must not be {@literal null}.
* @return
*/
public Link withAffordance(Affordance affordance) {
public Link andAffordance(Affordance affordance) {
Assert.notNull(affordance, "Affordance must not be null!");
List<Affordance> newAffordances = new ArrayList<Affordance>();
newAffordances.addAll(this.affordances);
newAffordances.add(affordance);
return new Link(this.rel, this.href, this.hreflang ,this.media, this.title, this.type,
this.deprecation, this.template, newAffordances);
return withAffordances(newAffordances);
}
/**
* Create new {@link Link} with additional {@link Affordance}s.
*
* @param affordances
* @param affordances must not be {@literal null}.
* @return
*/
public Link addAffordances(List<Affordance> affordances) {
public Link andAffordances(List<Affordance> affordances) {
List<Affordance> newAffordances = new ArrayList<Affordance>();
newAffordances.addAll(this.affordances);
newAffordances.addAll(affordances);
return new Link(this.rel, this.href, this.hreflang ,this.media, this.title, this.type,
this.deprecation, this.template, newAffordances);
return withAffordances(newAffordances);
}
/**
* Creats a new {@link Link} with the given {@link Affordance}s.
*
* @param affordances must not be {@literal null}.
* @return
*/
public Link withAffordances(List<Affordance> affordances) {
return new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation,
this.template, affordances);
}
/**
* Returns the variable names contained in the template.

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.hateoas.config;
import lombok.extern.slf4j.Slf4j;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@@ -25,8 +27,6 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportSelector;
@@ -54,7 +54,7 @@ import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer;
@Target(ElementType.TYPE)
@Documented
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class,
EnableHypermediaSupport.HypermediaConfigurationImportSelector.class})
EnableHypermediaSupport.HypermediaConfigurationImportSelector.class })
public @interface EnableHypermediaSupport {
/**
@@ -68,6 +68,7 @@ public @interface EnableHypermediaSupport {
* Hypermedia representation types supported.
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
enum HypermediaType {
@@ -81,6 +82,7 @@ public @interface EnableHypermediaSupport {
/**
* HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support
*
* @see https://rwcbook.github.io/hal-forms/
*/
HAL_FORMS(HalFormsWebMvcConfigurer.class);
@@ -109,8 +111,8 @@ public @interface EnableHypermediaSupport {
types = HypermediaType.values();
}
log.debug("Registering support for hypermedia types {} according to configuration on {}",
types, metadata.getClassName());
LOG.debug("Registering support for hypermedia types {} according to configuration on {}", types,
metadata.getClassName());
List<String> configurationNames = new ArrayList<String>();

View File

@@ -86,7 +86,6 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String AFFORDANCE_MODEL_FACTORY_REGISTRY_BEAN_NAME = "_affordanceModelFactoryRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String HAL_FORMS_OBJECT_MAPPER_BEAN_NAME = "_halFormsObjectMapper";
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
@@ -146,7 +145,8 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
registerRelProviderPluginRegistryAndDelegate(registry);
}
private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, String objectMapperBeanName) {
private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry,
String objectMapperBeanName) {
if (JACKSON2_PRESENT) {
@@ -316,7 +316,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
MessageSourceAccessor.class);
halObjectMapper.registerModule(new Jackson2HalModule());
@@ -340,7 +340,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
MessageSourceAccessor.class);
halFormsObjectMapper.registerModule(new Jackson2HalFormsModule());
@@ -354,7 +354,7 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
}
MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);
ResourceSupport.class);
halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON));
halFormsConverter.setObjectMapper(halFormsObjectMapper);
result.add(halFormsConverter);

View File

@@ -13,8 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas;
package org.springframework.hateoas.core;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.Plugin;
@@ -24,15 +26,9 @@ import org.springframework.web.util.UriComponents;
* TODO: Replace this with an interface and a default implementation of {@link #supports(MediaType)} in Java 8.
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
public abstract class AffordanceModelFactory implements Plugin<MediaType> {
/**
* Look up the {@link MediaType} of this factory.
*
* @return
*/
abstract public MediaType getMediaType();
public interface AffordanceModelFactory extends Plugin<MediaType> {
/**
* Look up the {@link AffordanceModel} for this factory.
@@ -42,16 +38,5 @@ public abstract class AffordanceModelFactory implements Plugin<MediaType> {
* @param components
* @return
*/
abstract public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components);
/**
* Find factories based on {@link MediaType}.
*
* @param delimiter
* @return
*/
@Override
public boolean supports(MediaType delimiter) {
return delimiter != null && delimiter.equals(this.getMediaType());
}
AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components);
}

View File

@@ -21,9 +21,11 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
@@ -111,15 +113,15 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
}
/**
* Extract {@link org.springframework.web.bind.annotation.RequestMapping}'s list of {@link RequestMethod}s
* into an array of {@link String}s.
* Extract {@link org.springframework.web.bind.annotation.RequestMapping}'s list of {@link RequestMethod}s into an
* array of {@link String}s.
*
* @param type
* @param method
* @return
*/
@Override
public String[] getRequestType(Class<?> type, Method method) {
public Collection<HttpMethod> getRequestMethod(Class<?> type, Method method) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(method, "Method must not be null!");
@@ -129,13 +131,13 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
RequestMethod[] requestMethods = (RequestMethod[]) value;
List<String> requestMethodNames = new ArrayList<String>();
List<HttpMethod> requestMethodNames = new ArrayList<HttpMethod>();
for (RequestMethod requestMethod : requestMethods) {
requestMethodNames.add(requestMethod.toString());
requestMethodNames.add(HttpMethod.valueOf(requestMethod.toString()));
}
return requestMethodNames.toArray(new String[]{});
return requestMethodNames;
}
private String[] getMappingFrom(Annotation annotation) {

View File

@@ -23,6 +23,7 @@ import lombok.Getter;
import java.net.URI;
import java.util.Optional;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.hateoas.Affordance;
@@ -143,10 +144,10 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
return uriComponents.encode().toUri().normalize();
}
public LinkBuilderSupport addAffordances(List<Affordance> affordances) {
public T addAffordances(Collection<Affordance> affordances) {
this.affordances.addAll(affordances);
return this;
return getThis();
}
/*
@@ -154,14 +155,7 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
* @see org.springframework.hateoas.LinkBuilder#withRel(java.lang.String)
*/
public Link withRel(String rel) {
Link link = new Link(toString(), rel);
for (Affordance affordance : this.affordances) {
link = link.withAffordance(affordance);
}
return link;
return new Link(toString(), rel).withAffordances(affordances);
}
/*

View File

@@ -16,10 +16,13 @@
package org.springframework.hateoas.core;
import java.lang.reflect.Method;
import java.util.Collection;
import org.springframework.http.HttpMethod;
/**
* Strategy interface to discover a URI mapping and related {@link org.springframework.hateoas.Affordance}s
* for either a given type or method.
* Strategy interface to discover a URI mapping and related {@link org.springframework.hateoas.Affordance}s for either a
* given type or method.
*
* @author Oliver Gierke
* @author Greg Turnquist
@@ -53,12 +56,12 @@ public interface MappingDiscoverer {
String getMapping(Class<?> type, Method method);
/**
* Returns the HTTP verbs for the given {@link Method} invoked on the given type. This can be used to build
* hypermedia templates.
* Returns the HTTP verbs for the given {@link Method} invoked on the given type. This can be used to build hypermedia
* templates.
*
* @param type
* @param method
* @return
*/
String[] getRequestType(Class<?> type, Method method);
Collection<HttpMethod> getRequestMethod(Class<?> type, Method method);
}

View File

@@ -207,9 +207,8 @@ public class Jackson2HalModule extends SimpleModule {
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(Collections.emptySet(), mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration),
null);
MapSerializer serializer = MapSerializer.construct(Collections.<String> emptySet(), mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration), null);
serializer.serialize(sortedLinks, jgen, provider);
}
@@ -402,14 +401,6 @@ public class Jackson2HalModule extends SimpleModule {
private final Map<Class<?>, JsonSerializer<Object>> serializers;
private final HalConfiguration halConfiguration;
public OptionalListJackson2Serializer() {
this(null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE));
}
public OptionalListJackson2Serializer(BeanProperty property) {
this(property, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE));
}
/**
* Creates a new {@link OptionalListJackson2Serializer} using the given {@link BeanProperty}.
*
@@ -527,7 +518,7 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new OptionalListJackson2Serializer(property);
return new OptionalListJackson2Serializer(property, halConfiguration);
}
}
@@ -573,7 +564,7 @@ public class Jackson2HalModule extends SimpleModule {
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation());
throw new JsonParseException(jp, "Expected relation name");
}
// save the relation in case the link does not contain it
@@ -649,7 +640,7 @@ public class Jackson2HalModule extends SimpleModule {
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException(jp, "Expected relation name", jp.getCurrentLocation());
throw new JsonParseException(jp, "Expected relation name");
}
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {

View File

@@ -30,7 +30,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @author Oliver Gierke
* @author Greg Turnquist
*/
@JsonIgnoreProperties({"rel", "media"})
@JsonIgnoreProperties({ "rel", "media", "affordances", "template" })
public abstract class LinkMixin extends Link {
private static final long serialVersionUID = 4720588561299667409L;
@@ -66,7 +66,7 @@ public abstract class LinkMixin extends Link {
@Override
@JsonInclude(Include.NON_NULL)
public abstract String getDeprecation();
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Link#isTemplate()

View File

@@ -15,20 +15,29 @@
*/
package org.springframework.hateoas.hal.forms;
import lombok.extern.slf4j.Slf4j;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.core.MethodParameter;
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.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;
@@ -37,42 +46,28 @@ import org.springframework.web.util.UriComponents;
*
* @author Greg Turnquist
*/
public class HalFormsAffordanceModel implements AffordanceModel {
@Slf4j
class HalFormsAffordanceModel implements AffordanceModel {
private static final Logger log = LoggerFactory.getLogger(HalFormsAffordanceModel.class);
private static final List<HttpMethod> METHODS_FOR_INPUT_DETECTTION = Arrays.asList(HttpMethod.POST, HttpMethod.PUT,
HttpMethod.PATCH);
/**
* Details about the affordance's
*/
private final UriComponents components;
/**
* Is this required/not required?
*/
private final boolean required;
/**
* {@link Map} of property names and their types associated with the incoming request body.
*/
private final Map<String, Class<?>> properties;
public HalFormsAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
this.components = components;
this.required = determineRequired(affordance.getHttpMethod());
this.properties = new TreeMap<String, Class<?>>();
if (affordance.getHttpMethod().equalsIgnoreCase("POST") ||
affordance.getHttpMethod().equalsIgnoreCase("PUT") ||
affordance.getHttpMethod().equalsIgnoreCase("PATCH")) {
determineAffordanceInputs(invocationValue.getMethod());
}
this.properties = METHODS_FOR_INPUT_DETECTTION.contains(affordance.getHttpMethod()) //
? determineAffordanceInputs(invocationValue.getMethod()) //
: Collections.<String, Class<?>> emptyMap();
}
/**
* Transform the details of the Spring MVC method's {@link RequestBody} into a collection of {@link HalFormsProperty}s.
* Transform the details of the Spring MVC method's {@link RequestBody} into a collection of
* {@link HalFormsProperty}s.
*
* @return
*/
@@ -80,20 +75,42 @@ public class HalFormsAffordanceModel implements AffordanceModel {
List<HalFormsProperty> halFormsProperties = new ArrayList<HalFormsProperty>();
for (Map.Entry<String, Class<?>> entry : this.properties.entrySet()) {
halFormsProperties.add(new HalFormsProperty(entry.getKey(), null, null, null, null, false, this.required, false));
for (Entry<String, Class<?>> entry : this.properties.entrySet()) {
HalFormsProperty property = HalFormsProperty//
.named(entry.getKey())//
.withRequired(required);
halFormsProperties.add(property);
}
return halFormsProperties;
}
public String getPath() {
return components.getPath();
}
/**
* Look up the path of the {@link UriComponents}.
* Returns whether the affordance is pointing to the same path as the given one.
*
* @param path must not be {@literal null}.
* @return
*/
public String getPath() {
return this.components.getPath();
public boolean hasPath(String path) {
Assert.notNull(path, "Path must not be null!");
return getPath().equals(path);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.AffordanceModel#getMediaType()
*/
@Override
public Collection<MediaType> getMediaTypes() {
return Collections.singleton(MediaTypes.HAL_FORMS_JSON);
}
/**
@@ -102,48 +119,45 @@ public class HalFormsAffordanceModel implements AffordanceModel {
* @param httpMethod - string representation of an HTTP method, e.g. GET, POST, etc.
* @return
*/
private boolean determineRequired(String httpMethod) {
if (httpMethod.equalsIgnoreCase("POST") || httpMethod.equalsIgnoreCase("PUT")) {
return true;
} else {
return false;
}
private boolean determineRequired(HttpMethod httpMethod) {
return Arrays.asList(HttpMethod.POST, HttpMethod.PUT).contains(httpMethod);
}
/**
* 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 void determineAffordanceInputs(Method method) {
private Map<String, Class<?>> determineAffordanceInputs(Method method) {
if (method == null) {
return;
return Collections.emptyMap();
}
log.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName());
LOG.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName());
for (int i = 0; i < method.getParameterTypes().length; i++) {
Map<String, Class<?>> properties = new TreeMap<String, Class<?>>();
MethodParameters parameters = new MethodParameters(method);
for (Annotation annotation : method.getParameterAnnotations()[i]) {
for (MethodParameter parameter : parameters.getParametersWith(RequestBody.class)) {
if (annotation.annotationType().equals(RequestBody.class)) {
Class<?> parameterType = parameter.getParameterType();
log.debug("\tRequest body: " + method.getParameterTypes()[i].getCanonicalName() + "(");
LOG.debug("\tRequest body: " + parameterType.getCanonicalName() + "(");
for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(method.getParameterTypes()[i])) {
for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(parameterType)) {
if (!descriptor.getName().equals("class")) {
log.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName());
this.properties.put(descriptor.getName(), descriptor.getPropertyType());
}
}
log.debug(")");
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());
LOG.debug("Assembled " + this.toString());
return properties;
}
}

View File

@@ -15,12 +15,10 @@
*/
package org.springframework.hateoas.hal.forms;
import lombok.Getter;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.AffordanceModelFactory;
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;
@@ -29,14 +27,26 @@ import org.springframework.web.util.UriComponents;
* Factory for creating {@link HalFormsAffordanceModel}s.
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
@Getter
public class HalFormsAffordanceModelFactory extends AffordanceModelFactory {
class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
private final 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)
*/
@Override
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
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);
}
}

View File

@@ -38,9 +38,12 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
*
* @author Greg Turnquist
*/
public class HalFormsDeserializers {
class HalFormsDeserializers {
static class HalFormsResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements ContextualDeserializer {
static class HalFormsResourcesDeserializer extends ContainerDeserializerBase<List<Object>>
implements ContextualDeserializer {
private static final long serialVersionUID = -7325599536381465624L;
private JavaType contentType;
@@ -65,7 +68,7 @@ public class HalFormsDeserializers {
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
throw new JsonParseException(jp, "Expected relation name");
}
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
@@ -93,7 +96,8 @@ public class HalFormsDeserializers {
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
if (property != null) {
JavaType vc = property.getType().getContentType();

View File

@@ -15,24 +15,28 @@
*/
package org.springframework.hateoas.hal.forms;
import static com.fasterxml.jackson.annotation.JsonInclude.*;
import static org.springframework.hateoas.hal.Jackson2HalModule.*;
import lombok.Builder;
import lombok.Data;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Singular;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
@@ -44,69 +48,154 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
*
* @author Dietrich Schulten
* @author Greg Turnquist
* @author Oliver Gierke
*/
@Data
@Builder(builderMethodName = "halFormsDocument")
@Value
@Wither
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
@JsonPropertyOrder({ "resource", "resources", "embedded", "links", "templates", "metadata" })
public class HalFormsDocument<T> {
@JsonUnwrapped
@JsonInclude(Include.NON_NULL)
@JsonUnwrapped //
@JsonInclude(Include.NON_NULL) //
@Wither(AccessLevel.PRIVATE) //
private T resource;
@JsonIgnore
@JsonInclude(Include.NON_EMPTY)
@JsonIgnore //
@Wither(AccessLevel.PRIVATE) //
private Collection<T> resources;
@JsonProperty("_embedded")
@JsonInclude(Include.NON_NULL)
@JsonProperty("_embedded") //
@JsonInclude(Include.NON_EMPTY) //
private Map<String, Object> embedded;
@JsonProperty("page")
@JsonInclude(Include.NON_NULL)
@JsonProperty("page") //
@JsonInclude(Include.NON_NULL) //
private PagedResources.PageMetadata pageMetadata;
@Singular private List<Link> links;
@Singular //
@JsonProperty("_links") //
@JsonInclude(Include.NON_EMPTY) //
@JsonSerialize(using = HalLinkListSerializer.class) //
@JsonDeserialize(using = HalLinkListDeserializer.class) //
private List<Link> links;
@Singular private Map<String, HalFormsTemplate> templates;
@Singular //
@JsonProperty("_templates") //
@JsonInclude(Include.NON_EMPTY) //
private Map<String, HalFormsTemplate> templates;
HalFormsDocument(T resource, Collection<T> resources, Map<String, Object> embedded,
PagedResources.PageMetadata pageMetadata, List<Link> links, Map<String, HalFormsTemplate> templates) {
this.resource = resource;
this.resources = resources;
this.embedded = embedded;
this.pageMetadata = pageMetadata;
this.links = links;
this.templates = templates;
private HalFormsDocument() {
this(null, null, Collections.<String, Object> emptyMap(), null, Collections.<Link> emptyList(),
Collections.<String, HalFormsTemplate> emptyMap());
}
HalFormsDocument() {
this(null, null, null, null, new ArrayList<Link>(), new HashMap<String, HalFormsTemplate>());
/**
* Creates a new {@link HalFormsDocument} for the given resource.
*
* @param resource can be {@literal null}.
* @return
*/
public static <T> HalFormsDocument<T> forResource(T resource) {
return new HalFormsDocument<T>().withResource(resource);
}
@JsonProperty("_links")
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = HalLinkListSerializer.class)
@JsonDeserialize(using = HalLinkListDeserializer.class)
public List<Link> getLinks() {
return this.links;
/**
* returns a new {@link HalFormsDocument} for the given resources.
*
* @param resources must not be {@literal null}.
* @return
*/
public static <T> HalFormsDocument<T> forResources(Collection<T> resources) {
Assert.notNull(resources, "Resources must not be null!");
return new HalFormsDocument<T>().withResources(resources);
}
@JsonProperty("_templates")
@JsonInclude(Include.NON_EMPTY)
public Map<String, HalFormsTemplate> getTemplates() {
return this.templates;
/**
* Creates a new empty {@link HalFormsDocument}.
*
* @return
*/
public static HalFormsDocument<?> empty() {
return new HalFormsDocument<Object>();
}
/**
* Returns the default template of the document.
*
* @return
*/
@JsonIgnore
public HalFormsTemplate getTemplate() {
public HalFormsTemplate getDefaultTemplate() {
return getTemplate(HalFormsTemplate.DEFAULT_KEY);
}
/**
* Returns the template with the given name.
*
* @param key must not be {@literal null}.
* @return
*/
@JsonIgnore
public HalFormsTemplate getTemplate(String key) {
Assert.notNull(key, "Template key must not be null!");
return this.templates.get(key);
}
/**
* Adds the given {@link Link} to the current document.
*
* @param link must not be {@literal null}.
* @return
*/
public HalFormsDocument<T> andLink(Link link) {
Assert.notNull(link, "Link must not be null!");
List<Link> links = new ArrayList<Link>(this.links);
links.add(link);
return new HalFormsDocument<T>(resource, resources, embedded, pageMetadata, links, templates);
}
/**
* Adds the given {@link HalFormsTemplate} to the current document.
*
* @param name must not be {@literal null} or empty.
* @param template must not be {@literal null}.
* @return
*/
public HalFormsDocument<T> andTemplate(String name, HalFormsTemplate template) {
Assert.hasText(name, "Template name must not be null or empty!");
Assert.notNull(template, "Template must not be null!");
Map<String, HalFormsTemplate> templates = new HashMap<String, HalFormsTemplate>(this.templates);
templates.put(name, template);
return new HalFormsDocument<T>(resource, resources, embedded, pageMetadata, links, templates);
}
/**
* Adds the given value as embedded one.
*
* @param key must not be {@literal null} or empty.
* @param value must not be {@literal null}.
* @return
*/
public HalFormsDocument<T> andEmbedded(String key, Object value) {
Assert.notNull(key, "Embedded key must not be null!");
Assert.notNull(value, "Embedded value must not be null!");
Map<String, Object> embedded = new HashMap<String, Object>(this.embedded);
embedded.put(key, value);
return new HalFormsDocument<T>(resource, resources, embedded, pageMetadata, links, templates);
}
}

View File

@@ -32,14 +32,13 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
/**
* A message converter that converts any object into a HAL-FORMS document before bundling up
* as an {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into
* an object.
* A message converter that converts any object into a HAL-FORMS document before bundling up as an
* {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into an object.
*
* @author Dietrich Schulten
* @author Greg Turnquist
*/
public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Object> {
class HalFormsMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
@@ -47,6 +46,7 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Objec
this.objectMapper = objectMapper;
this.objectMapper.registerModule(new Jackson2HalFormsModule());
setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_FORMS_JSON));
}
@@ -55,7 +55,7 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Objec
* @see org.springframework.http.converter.AbstractHttpMessageConverter#supports(java.lang.Class)
*/
@Override
protected boolean supports(final Class<?> clazz) {
protected boolean supports(Class<?> clazz) {
return true;
}
@@ -64,14 +64,14 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Objec
* @see org.springframework.http.converter.AbstractHttpMessageConverter#readInternal(java.lang.Class, org.springframework.http.HttpInputMessage)
*/
@Override
protected Object readInternal(final Class<? extends Object> clazz, final HttpInputMessage inputMessage)
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException {
return this.objectMapper.readValue(inputMessage.getBody(), clazz);
}
@Override
protected void writeInternal(final Object t, final HttpOutputMessage outputMessage)
protected void writeInternal(Object t, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8);
@@ -88,5 +88,4 @@ public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Objec
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
}

View File

@@ -15,32 +15,33 @@
*/
package org.springframework.hateoas.hal.forms;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.Value;
import lombok.experimental.Wither;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
/**
* Describe a parameter for the associated state transition in a HAL-FORMS document.
* A {@link HalFormsTemplate} may contain a list of {@link HalFormsProperty}s
* Describe a parameter for the associated state transition in a HAL-FORMS document. A {@link HalFormsTemplate} may
* contain a list of {@link HalFormsProperty}s
*
* @see http://mamund.site44.com/misc/hal-forms/
*/
@JsonInclude(Include.NON_DEFAULT)
@Value
@AllArgsConstructor
@Wither
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
public class HalFormsProperty {
private String name;
/**
* readOnly uses {@link Boolean} not {@literal boolean}, because if {@literal null}, the element won't be rendered
*/
private @Wither(AccessLevel.PRIVATE) @NonNull String name;
private Boolean readOnly;
private String value;
private String prompt;
private String regex;
@@ -49,9 +50,15 @@ public class HalFormsProperty {
private boolean multi;
/**
* Default constructor to support Jackson.
* Creates a new {@link HalFormsProperty} with the given name.
*
* @param name must not be {@literal null} or empty.
* @return
*/
HalFormsProperty() {
this(null, null, null, null, null, false, false, false);
public static HalFormsProperty named(String name) {
Assert.hasText(name, "Property name must not be null or empty!");
return new HalFormsProperty().withName(name);
}
}

View File

@@ -48,13 +48,15 @@ import com.fasterxml.jackson.databind.ser.ContextualSerializer;
*
* @author Greg Turnquist
*/
public class HalFormsSerializers {
class HalFormsSerializers {
/**
* Serializer for {@link Resources}.
*/
static class HalFormsResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
private static final long serialVersionUID = -7912243216469101379L;
private final BeanProperty property;
HalFormsResourceSerializer(BeanProperty property) {
@@ -70,15 +72,11 @@ public class HalFormsSerializers {
@Override
public void serialize(Resource<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
HalFormsDocument<?> doc = HalFormsDocument.<Object> halFormsDocument()
.resource(value.getContent())
.links(value.getLinks())
.templates(findTemplates(value))
.build();
HalFormsDocument<?> doc = HalFormsDocument.forResource(value.getContent()) //
.withLinks(value.getLinks()) //
.withTemplates(findTemplates(value));
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@Override
@@ -102,7 +100,8 @@ public class HalFormsSerializers {
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalFormsResourceSerializer(property);
}
}
@@ -112,12 +111,15 @@ public class HalFormsSerializers {
*/
static class HalFormsResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
private static final long serialVersionUID = -3601146866067500734L;
private final BeanProperty property;
private final Jackson2HalModule.EmbeddedMapper embeddedMapper;
HalFormsResourcesSerializer(BeanProperty property, Jackson2HalModule.EmbeddedMapper embeddedMapper) {
super(Resources.class, false);
this.property = property;
this.embeddedMapper = embeddedMapper;
}
@@ -135,25 +137,21 @@ public class HalFormsSerializers {
if (value instanceof PagedResources) {
doc = HalFormsDocument.<Object> halFormsDocument()
.embedded(embeddeds)
.pageMetadata(((PagedResources) value).getMetadata())
.links(value.getLinks())
.templates(findTemplates(value))
.build();
doc = HalFormsDocument.empty() //
.withEmbedded(embeddeds) //
.withPageMetadata(((PagedResources<?>) value).getMetadata()) //
.withLinks(value.getLinks()) //
.withTemplates(findTemplates(value));
} else {
doc = HalFormsDocument.<Object> halFormsDocument()
.embedded(embeddeds)
.pageMetadata(null)
.links(value.getLinks())
.templates(findTemplates(value))
.build();
doc = HalFormsDocument.empty() //
.withEmbedded(embeddeds) //
.withLinks(value.getLinks()) //
.withTemplates(findTemplates(value));
}
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@Override
@@ -177,7 +175,8 @@ public class HalFormsSerializers {
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
return new HalFormsResourcesSerializer(property, embeddedMapper);
}
}
@@ -195,25 +194,19 @@ public class HalFormsSerializers {
if (resource.hasLink(Link.REL_SELF)) {
for (Affordance affordance : resource.getLink(Link.REL_SELF).map(Link::getAffordances).orElse(Collections.emptyList())) {
HalFormsAffordanceModel model =
(HalFormsAffordanceModel) affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON);
HalFormsAffordanceModel model = affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON);
if (!affordance.getHttpMethod().equals(HttpMethod.GET.toString())) {
if (!affordance.getHttpMethod().equals(HttpMethod.GET)) {
validate(resource, affordance, model);
HalFormsTemplate template = new HalFormsTemplate();
template.setHttpMethod(HttpMethod.valueOf(affordance.getHttpMethod()));
template.setProperties(model.getProperties());
HalFormsTemplate template = HalFormsTemplate.forMethod(affordance.getHttpMethod()) //
.withProperties(model.getProperties());
/**
* First template in HAL-FORMS is "default".
*/
if (templates.isEmpty()) {
templates.put("default", template);
} else {
templates.put(affordance.getName(), template);
}
templates.put(templates.isEmpty() ? "default" : affordance.getName(), template);
}
}
}
@@ -223,7 +216,8 @@ public class HalFormsSerializers {
/**
* Verify that the resource's self link and the affordance's URI have the same relative path.
* @param resource
*
* @param resource
* @param affordance
* @param model
*/
@@ -231,15 +225,15 @@ public class HalFormsSerializers {
try {
Optional<Link> selfLink = resource.getLink(Link.REL_SELF);
URI selfLinkUri = new URI(selfLink.map(link -> link.expand().getHref()).orElse(""));
if (!selfLinkUri.getPath().equals(model.getPath())) {
throw new IllegalStateException("Affordance's URI " + model.getPath() + " doesn't match self link " + selfLinkUri.getPath() + " as expected in HAL-FORMS");
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);
}
}
}

View File

@@ -18,19 +18,25 @@ package org.springframework.hateoas.hal.forms;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Wither;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.hateoas.hal.forms.HalFormsDeserializers.MediaTypesDeserializer;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
@@ -43,50 +49,79 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @see https://rwcbook.github.io/hal-forms/#_the_code__templates_code_element
*/
@Data
@Setter(AccessLevel.NONE)
@Wither
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@EqualsAndHashCode
@ToString
@JsonAutoDetect(getterVisibility = Visibility.NON_PRIVATE)
@JsonIgnoreProperties({ "httpMethod", "contentTypes" })
@JsonPropertyOrder({ "title", "method", "contentType", "properties" })
@JsonIgnoreProperties({ "key" })
public class HalFormsTemplate {
public static final String DEFAULT_KEY = "default";
private @JsonIgnore String key;
private List<HalFormsProperty> properties = new ArrayList<HalFormsProperty>();
private String title;
private @JsonIgnore HttpMethod httpMethod;
private List<MediaType> contentType;
private @Wither(AccessLevel.PRIVATE) HttpMethod httpMethod;
private List<HalFormsProperty> properties;
private List<MediaType> contentTypes;
/**
* Configure a HAL-FORMS template with a key value.
* @param key
*/
public HalFormsTemplate(String key) {
this.key = key;
private HalFormsTemplate() {
this(null, null, Collections.<HalFormsProperty> emptyList(), Collections.<MediaType> emptyList());
}
public static HalFormsTemplate forMethod(HttpMethod httpMethod) {
return new HalFormsTemplate().withHttpMethod(httpMethod);
}
/**
* A HAL-FORMS template with no name is dubbed the <a href="https://rwcbook.github.io/hal-forms/#_the_code__templates_code_element">"default" template</a>.
* Returns a new {@link HalFormsTemplate} with the given {@link HalFormsProperty} added.
*
* @param property must not be {@literal null}.
* @return
*/
public HalFormsTemplate() {
this(HalFormsTemplate.DEFAULT_KEY);
public HalFormsTemplate andProperty(HalFormsProperty property) {
Assert.notNull(property, "Property must not be null!");
ArrayList<HalFormsProperty> properties = new ArrayList<HalFormsProperty>(this.properties);
properties.add(property);
return new HalFormsTemplate(title, httpMethod, properties, contentTypes);
}
public String getContentType() {
return StringUtils.collectionToCommaDelimitedString(contentType);
/**
* Returns a new {@link HalFormsTemplate} with the given {@link MediaType} added as content type.
*
* @param mediaType must not be {@literal null}.
* @return
*/
public HalFormsTemplate andContentType(MediaType mediaType) {
Assert.notNull(mediaType, "Media type must not be null!");
ArrayList<MediaType> contentTypes = new ArrayList<MediaType>(this.contentTypes);
contentTypes.add(mediaType);
return new HalFormsTemplate(title, httpMethod, properties, contentTypes);
}
// Jackson helper methods to create the right representation format
String getContentType() {
return StringUtils.collectionToDelimitedString(contentTypes, ", ");
}
@JsonDeserialize(using = MediaTypesDeserializer.class)
public void setContentType(List<MediaType> contentType) {
this.contentType = contentType;
void setContentType(List<MediaType> mediaTypes) {
this.contentTypes = mediaTypes;
}
public String getMethod() {
String getMethod() {
return this.httpMethod == null ? null : this.httpMethod.toString().toLowerCase();
}
public void setMethod(String method) {
void setMethod(String method) {
this.httpMethod = HttpMethod.valueOf(method.toUpperCase());
}
}

View File

@@ -15,9 +15,12 @@
*/
package org.springframework.hateoas.hal.forms;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
@@ -32,25 +35,36 @@ import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.hal.LinkMixin;
import org.springframework.hateoas.hal.ResourceSupportMixin;
import org.springframework.hateoas.hal.forms.HalFormsDeserializers.HalFormsResourcesDeserializer;
import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourceSerializer;
import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourcesSerializer;
import org.springframework.hateoas.mvc.JacksonSerializers.MediaTypeDeserializer;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
/**
* Serialize/Deserialize all the parts of HAL-FORMS documents using Jackson.
* Serialize / deserialize all the parts of HAL-FORMS documents using Jackson.
*
* @author Dietrich Schulten
* @author Greg Turnquist
* @author Oliver Gierke
*/
public class Jackson2HalFormsModule extends SimpleModule {
@@ -65,8 +79,37 @@ public class Jackson2HalFormsModule extends SimpleModule {
setMixInAnnotation(Resource.class, ResourceMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
setMixInAnnotation(MediaType.class, MediaTypeMixin.class);
}
@JsonSerialize(using = HalFormsResourceSerializer.class)
static interface ResourceMixin {}
@JsonSerialize(using = HalFormsResourcesSerializer.class)
abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@XmlElement(name = "embedded")
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonDeserialize(using = HalFormsResourcesDeserializer.class)
public abstract Collection<T> getContent();
}
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
@Override
@JsonProperty("page")
@JsonInclude(Include.NON_EMPTY)
public PageMetadata getMetadata() {
return super.getMetadata();
}
}
@JsonSerialize(using = ToStringSerializer.class)
@JsonDeserialize(using = MediaTypeDeserializer.class)
static interface MediaTypeMixin {}
/**
* Create new HAL-FORMS serializers based on the context.
*/
@@ -99,41 +142,33 @@ public class Jackson2HalFormsModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see
* com.fasterxml.jackson.databind.cfg.HandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.
* DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#deserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
*/
@Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> deserClass) {
Class<?> deserClass) {
Object jsonDeser = findInstance(deserClass);
return jsonDeser != null ? (JsonDeserializer<?>) jsonDeser
: super.deserializerInstance(config, annotated, deserClass);
: super.deserializerInstance(config, annotated, deserClass);
}
/*
* (non-Javadoc)
*
* @see com.fasterxml.jackson.databind.cfg.HandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.
* databind. DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#keyDeserializerInstance(com.fasterxml.jackson.databind.DeserializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
*/
@Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> keyDeserClass) {
Class<?> keyDeserClass) {
Object keyDeser = findInstance(keyDeserClass);
return keyDeser != null ? (KeyDeserializer) keyDeser
: super.keyDeserializerInstance(config, annotated, keyDeserClass);
: super.keyDeserializerInstance(config, annotated, keyDeserClass);
}
/*
* (non-Javadoc)
*
* @see
* com.fasterxml.jackson.databind.cfg.HandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.
* SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#serializerInstance(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
*/
@Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
@@ -144,33 +179,27 @@ public class Jackson2HalFormsModule extends SimpleModule {
/*
* (non-Javadoc)
*
* @see
* com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.
* databind .cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#typeResolverBuilderInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
*/
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated,
Class<?> builderClass) {
Class<?> builderClass) {
Object builder = findInstance(builderClass);
return builder != null ? (TypeResolverBuilder<?>) builder
: super.typeResolverBuilderInstance(config, annotated, builderClass);
: super.typeResolverBuilderInstance(config, annotated, builderClass);
}
/*
* (non-Javadoc)
*
* @see
* com.fasterxml.jackson.databind.cfg.HandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.
* cfg. MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
* @see org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator#typeIdResolverInstance(com.fasterxml.jackson.databind.cfg.MapperConfig, com.fasterxml.jackson.databind.introspect.Annotated, java.lang.Class)
*/
@Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
Object resolver = findInstance(resolverClass);
return resolver != null ? (TypeIdResolver) resolver
: super.typeIdResolverInstance(config, annotated, resolverClass);
: super.typeIdResolverInstance(config, annotated, resolverClass);
}
}
}

View File

@@ -1,37 +0,0 @@
/*
* Copyright 2017 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.hal.forms;
import org.springframework.hateoas.PagedResources;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Custom mixin to render {@link org.springframework.hateoas.PagedResources.PageMetadata} in HAL.
*
* @author Greg Turnquist
*/
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
@Override
@JsonProperty("page")
@JsonInclude(Include.NON_EMPTY)
public PageMetadata getMetadata() {
return super.getMetadata();
}
}

View File

@@ -1,28 +0,0 @@
/*
* Copyright 2017 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.hal.forms;
import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourceSerializer;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* @author Greg Turnquist
*/
@JsonSerialize(using = HalFormsResourceSerializer.class)
abstract class ResourceMixin {
}

View File

@@ -1,45 +0,0 @@
/*
* Copyright 2017 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.hal.forms;
import java.util.Collection;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.hal.forms.HalFormsDeserializers.HalFormsResourcesDeserializer;
import org.springframework.hateoas.hal.forms.HalFormsSerializers.HalFormsResourcesSerializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* @author Greg Turnquist
*/
@JsonSerialize(using = HalFormsResourcesSerializer.class)
abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@XmlElement(name = "embedded")
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonDeserialize(using = HalFormsResourcesDeserializer.class)
public abstract Collection<T> getContent();
}

View File

@@ -22,24 +22,25 @@ import lombok.experimental.Delegate;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModelFactory;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.AffordanceModelFactory;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.DummyInvocationUtils;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.hal.forms.HalFormsAffordanceModelFactory;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.OrderAwarePluginRegistry;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -70,6 +71,17 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
new AnnotationMappingDiscoverer(RequestMapping.class));
private static final ControllerLinkBuilderFactory FACTORY = new ControllerLinkBuilderFactory();
private static final CustomUriTemplateHandler HANDLER = new CustomUriTemplateHandler();
private static final SpringMvcAffordanceBuilder AFFORDANCE_BUILDER;
static {
List<AffordanceModelFactory> factories = SpringFactoriesLoader.loadFactories(AffordanceModelFactory.class,
ControllerLinkBuilder.class.getClassLoader());
PluginRegistry<? extends AffordanceModelFactory, MediaType> MODEL_FACTORIES = OrderAwarePluginRegistry
.create(factories);
AFFORDANCE_BUILDER = new SpringMvcAffordanceBuilder(MODEL_FACTORIES);
}
private final TemplateVariables variables;
@@ -102,24 +114,6 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
this.addAffordances(findAffordances(invocation, uriComponents));
}
/**
* Look up {@link Affordance}s and {@link org.springframework.hateoas.AffordanceModel}s based on the
* {@link MethodInvocation} and {@link UriComponents}.
*
* @param invocation
* @param components
* @return
*/
private List<Affordance> findAffordances(MethodInvocation invocation, UriComponents components) {
OrderAwarePluginRegistry<? extends AffordanceModelFactory, MediaType> modelFactories =
OrderAwarePluginRegistry.create(Arrays.asList(new HalFormsAffordanceModelFactory()));
SpringMvcAffordanceBuilder springMvcAffordanceBuilder = new SpringMvcAffordanceBuilder(modelFactories);
return springMvcAffordanceBuilder.create(invocation, DISCOVERER, components);
}
/**
* Creates a new {@link ControllerLinkBuilder} with a base of the mapping annotated to the given controller class.
*
@@ -222,12 +216,12 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
}
/**
* Extract a {@link Link} from the {@link ControllerLinkBuilder} and look up the related {@link Affordance}.
* Should only be one.
* Extract a {@link Link} from the {@link ControllerLinkBuilder} and look up the related {@link Affordance}. Should
* only be one.
*
* <pre>
* Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
* findOneLink.withAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)))
* Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel()
* .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id)));
* </pre>
*
* This takes a link and adds an {@link Affordance} based on another Spring MVC handler method.
@@ -351,6 +345,18 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
return servletRequest;
}
/**
* Look up {@link Affordance}s and {@link org.springframework.hateoas.AffordanceModel}s based on the
* {@link MethodInvocation} and {@link UriComponents}.
*
* @param invocation
* @param components
* @return
*/
private static Collection<Affordance> findAffordances(MethodInvocation invocation, UriComponents components) {
return AFFORDANCE_BUILDER.create(invocation, DISCOVERER, components);
}
@RequiredArgsConstructor
private static class CachingAnnotationMappingDiscoverer implements MappingDiscoverer {

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2017 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.mvc;
import java.io.IOException;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/**
* Simple Jackson serializers and deserializers.
*
* @author Oliver Gierke
*/
public class JacksonSerializers {
/**
* Custom {@link JsonDeserializer} for Spring's {@link MediaType} using the {@link MediaType#paparseMediaType(String)}
* method.
*
* @author Oliver Gierke
*/
public static class MediaTypeDeserializer extends StdDeserializer<MediaType> {
private static final long serialVersionUID = 391537719262033410L;
public MediaTypeDeserializer() {
super(MediaType.class);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public MediaType deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
return MediaType.parseMediaType(p.getText());
}
}
}

View File

@@ -15,16 +15,19 @@
*/
package org.springframework.hateoas.mvc;
import lombok.Data;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
/**
@@ -32,50 +35,58 @@ import org.springframework.web.bind.annotation.RequestMethod;
*
* @author Greg Turnquist
*/
@Data
public class SpringMvcAffordance implements Affordance {
private static final Logger log = LoggerFactory.getLogger(SpringMvcAffordance.class);
private final HashMap<MediaType, AffordanceModel> affordanceModels;
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class SpringMvcAffordance implements Affordance {
/**
* Request method verb associated with the Spring MVC controller method.
*/
private final RequestMethod requestMethod;
private final HttpMethod httpMethod;
/**
* Handle on the Spring MVC controller {@link Method}.
*/
private final Method method;
private final Map<MediaType, AffordanceModel> affordanceModels;
/**
* Construct a Spring MVC-based {@link Affordance} based on Spring MVC controller method and {@link RequestMethod}.
*/
public SpringMvcAffordance(RequestMethod requestMethod, Method method) {
this.requestMethod = requestMethod;
this.method = method;
this.affordanceModels = new HashMap<MediaType, AffordanceModel>();
}
@Override
public String getHttpMethod() {
return this.requestMethod.toString();
public SpringMvcAffordance(HttpMethod httpMethod, Method method) {
this(httpMethod, method, new HashMap<MediaType, AffordanceModel>());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Affordance#getName()
*/
@Override
public String getName() {
return this.method.getName();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Affordance#getAffordanceModel(org.springframework.http.MediaType)
*/
@Override
public AffordanceModel getAffordanceModel(MediaType mediaType) {
return this.affordanceModels.get(mediaType);
@SuppressWarnings("unchecked")
public <T extends AffordanceModel> T getAffordanceModel(MediaType mediaType) {
return (T) this.affordanceModels.get(mediaType);
}
@Override
public void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel) {
this.affordanceModels.put(mediaType, affordanceModel);
/**
* Adds the given {@link AffordanceModel} to the {@link Affordance}.
*
* @param affordanceModel must not be {@literal null}.
*/
public void addAffordanceModel(AffordanceModel affordanceModel) {
Assert.notNull(affordanceModel, "Affordance model must not be null!");
for (MediaType mediaType : affordanceModel.getMediaTypes()) {
this.affordanceModels.put(mediaType, affordanceModel);
}
}
}

View File

@@ -15,18 +15,21 @@
*/
package org.springframework.hateoas.mvc;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModelFactory;
import org.springframework.hateoas.core.AffordanceModelFactory;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.util.UriComponents;
/**
@@ -34,41 +37,35 @@ import org.springframework.web.util.UriComponents;
*
* @author Greg Turnquist
*/
public class SpringMvcAffordanceBuilder {
@RequiredArgsConstructor
class SpringMvcAffordanceBuilder {
private final PluginRegistry<? extends AffordanceModelFactory, MediaType> factories;
public SpringMvcAffordanceBuilder(PluginRegistry<? extends AffordanceModelFactory, MediaType> factories) {
Assert.notNull(factories, "Registry of LinkDiscoverer must not be null!");
this.factories = factories;
}
private final @NonNull PluginRegistry<? extends AffordanceModelFactory, MediaType> factories;
/**
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to
* create a set of {@link Affordance}s.
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
* a set of {@link Affordance}s.
*
* @param invocation
* @param discoverer
* @param components
* @return
*/
public List<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer, UriComponents components) {
public Collection<Affordance> create(MethodInvocation invocation, MappingDiscoverer discoverer,
UriComponents components) {
Method method = invocation.getMethod();
String[] httpMethods = discoverer.getRequestType(invocation.getTargetType(), method);
List<Affordance> affordances = new ArrayList<Affordance>();
for (String requestMethod : httpMethods) {
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), method)) {
SpringMvcAffordance springMvcAffordance = new SpringMvcAffordance(RequestMethod.valueOf(requestMethod), invocation.getMethod());
SpringMvcAffordance affordance = new SpringMvcAffordance(requestMethod, invocation.getMethod());
for (AffordanceModelFactory factory : factories) {
springMvcAffordance.addAffordanceModel(factory.getMediaType(), factory.getAffordanceModel(springMvcAffordance, invocation, components));
affordance.addAffordanceModel(factory.getAffordanceModel(affordance, invocation, components));
}
affordances.add(springMvcAffordance);
affordances.add(affordance);
}
return affordances;