#340 - Add new Affordances API + HAL-FORMS mediatype.

* Introduces new Affordances API to build links related to each other to serve other mediatypes
* Introduces HAL-FORMS, which uses affordances to automatically generate HTML form data based on Spring MVC annotations.

Original pull-request: #340, #447, #581
Related issues: #503, #334, #71
This commit is contained in:
Greg Turnquist
2017-07-13 13:36:36 -05:00
committed by Oliver Gierke
parent 79ebf9b5a4
commit 70448a8540
65 changed files with 3776 additions and 73 deletions

View File

@@ -0,0 +1,57 @@
/*
* 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;
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
*/
public interface Affordance {
/**
* HTTP method this affordance covers. For multiple methods, add multiple {@link Affordance}s.
*
* @return
*/
String getHttpMethod();
/**
* Name for the REST action this {@link Affordance} can take.
*
* @return
*/
String getName();
/**
* Look up the {@link AffordanceModel} for the requested {@link MediaType}.
*
* @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);
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
/**
* Marker interface for mediatypes to build up type-specific details for an {@link Affordance}
*
* @author Greg Turnquist
*/
public interface AffordanceModel {
}

View File

@@ -0,0 +1,57 @@
/*
* 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;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.Plugin;
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
*/
public abstract class AffordanceModelFactory implements Plugin<MediaType> {
/**
* Look up the {@link MediaType} of this factory.
*
* @return
*/
abstract public MediaType getMediaType();
/**
* Look up the {@link AffordanceModel} for this factory.
*
* @param affordance
* @param invocationValue
* @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());
}
}

View File

@@ -19,10 +19,10 @@ import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Wither;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@@ -34,6 +34,7 @@ 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;
@@ -48,10 +49,9 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
*/
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
@JsonIgnoreProperties("templated")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation" })
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "deprecation", "affordances" })
public class Link implements Serializable {
private static final long serialVersionUID = -9037755944661782121L;
@@ -73,6 +73,7 @@ public class Link implements Serializable {
private @XmlAttribute @Wither String type;
private @XmlAttribute @Wither String deprecation;
private @XmlTransient @JsonIgnore UriTemplate template;
private @XmlTransient @JsonIgnore List<Affordance> affordances;
/**
* Creates a new link to the given URI with the self rel.
@@ -108,6 +109,32 @@ public class Link implements Serializable {
this.template = template;
this.href = template.toString();
this.rel = rel;
this.affordances = new ArrayList<Affordance>();
}
public Link(String href, String rel, List<Affordance> affordances) {
this(href, rel);
Assert.notNull(affordances, "affordances must not be null!");
this.affordances = affordances;
}
/**
* Empty constructor required by the marshalling framework.
*/
protected Link() {
this.affordances = new ArrayList<Affordance>();
}
/**
* Returns safe copy of {@link Affordance}s.
*
* @return
*/
public List<Affordance> getAffordances() {
return new ArrayList<Affordance>(Collections.unmodifiableCollection(this.affordances));
}
/**
@@ -119,6 +146,39 @@ public class Link implements Serializable {
return withRel(Link.REL_SELF);
}
/**
* Create new {@link Link} with an additional {@link Affordance}.
*
* @param affordance
* @return
*/
public Link withAffordance(Affordance affordance) {
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);
}
/**
* Create new {@link Link} with additional {@link Affordance}s.
*
* @param affordances
* @return
*/
public Link addAffordances(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);
}
/**
* Returns the variable names contained in the template.
*

View File

@@ -22,7 +22,10 @@ import org.springframework.http.MediaType;
*
* @author Oliver Gierke
* @author Przemek Nowak
<<<<<<< HEAD
* @author Drummond Dawson
=======
>>>>>>> f5bf966... #340 - Add new Affordances API + HAL-FORMS mediatype.
* @author Greg Turnquist
*/
public class MediaTypes {
@@ -56,4 +59,15 @@ public class MediaTypes {
* Public constant media type for {@code application/alps+json}.
*/
public static final MediaType ALPS_JSON = MediaType.parseMediaType(ALPS_JSON_VALUE);
/**
* Public constant media type for {@code application/prs.hal-forms+json}.
*/
public static final String HAL_FORMS_JSON_VALUE = "application/prs.hal-forms+json";
/**
* Public constant media type for {@code applicatino/prs.hal-forms+json}.
*/
public static final MediaType HAL_FORMS_JSON = MediaType.parseMediaType(HAL_FORMS_JSON_VALUE);
}

View File

@@ -20,11 +20,20 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.ArrayList;
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;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer;
/**
* Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for
@@ -39,11 +48,13 @@ import org.springframework.hateoas.LinkDiscoverer;
* @see LinkDiscoverer
* @see EntityLinks
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class })
@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class,
EnableHypermediaSupport.HypermediaConfigurationImportSelector.class})
public @interface EnableHypermediaSupport {
/**
@@ -66,6 +77,50 @@ public @interface EnableHypermediaSupport {
* @see http://stateless.co/hal_specification.html
* @see http://tools.ietf.org/html/draft-kelly-json-hal-05
*/
HAL;
HAL,
/**
* 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);
private final List<Class<?>> configurations;
HypermediaType(Class<?>... configurations) {
this.configurations = Arrays.asList(configurations);
}
}
@Slf4j
class HypermediaConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
HypermediaType[] types = (HypermediaType[]) attributes.get("type");
/**
* If no types are defined inside the annotation, add them all.
*/
if (types.length == 0) {
types = HypermediaType.values();
}
log.debug("Registering support for hypermedia types {} according to configuration on {}",
types, metadata.getClassName());
List<String> configurationNames = new ArrayList<String>();
for (HypermediaType type : types) {
for (Class<?> configuration : type.configurations) {
configurationNames.add(configuration.getName());
}
}
return configurationNames.toArray(new String[0]);
}
}
}

View File

@@ -57,6 +57,9 @@ import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean;
@@ -77,12 +80,15 @@ import com.fasterxml.jackson.databind.ObjectMapper;
* activated as well).
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
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";
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
@@ -117,23 +123,11 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
}
if (types.contains(HypermediaType.HAL)) {
registerHypermediaComponents(metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME);
}
if (JACKSON2_PRESENT) {
BeanDefinitionBuilder halQueryMapperBuilder = rootBeanDefinition(ObjectMapper.class);
registerSourcedBeanDefinition(halQueryMapperBuilder, metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME);
BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class);
registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);
BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
registerSourcedBeanDefinition(builder, metadata, registry);
}
// If no HalConfiguration bean, create a default one.
if (this.beanFactory.getBeanNamesForType(HalConfiguration.class).length == 0) {
registerSourcedBeanDefinition(rootBeanDefinition(HalConfiguration.class), metadata, registry);
}
if (types.contains(HypermediaType.HAL_FORMS)) {
registerHypermediaComponents(metadata, registry, HAL_FORMS_OBJECT_MAPPER_BEAN_NAME);
}
if (!types.isEmpty()) {
@@ -152,11 +146,26 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
registerRelProviderPluginRegistryAndDelegate(registry);
}
private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, String objectMapperBeanName) {
if (JACKSON2_PRESENT) {
BeanDefinitionBuilder queryMapperBuilder = rootBeanDefinition(ObjectMapper.class);
registerSourcedBeanDefinition(queryMapperBuilder, metadata, registry, objectMapperBeanName);
BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class);
registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry);
BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class);
registerSourcedBeanDefinition(builder, metadata, registry);
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ListableBeanFactory) beanFactory;
}
/**
* Registers bean definitions for a {@link PluginRegistry} to capture {@link RelProvider} instances. Wraps the
* registry into a {@link DelegatingRelProvider} bean definition backed by the registry.
@@ -202,6 +211,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
case HAL:
definition = new RootBeanDefinition(HalLinkDiscoverer.class);
break;
case HAL_FORMS:
definition = new RootBeanDefinition(HalFormsLinkDiscoverer.class);
break;
default:
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
}
@@ -297,22 +309,59 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
halObjectMapper.registerModule(new Jackson2HalModule());
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, beanFactory));
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON, HAL_JSON_UTF8));
halConverter.setObjectMapper(halObjectMapper);
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
result.add(halConverter);
if (beanFactory.containsBean(HAL_OBJECT_MAPPER_BEAN_NAME)) {
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
halObjectMapper.registerModule(new Jackson2HalModule());
try {
HalConfiguration halConfiguration = beanFactory.getBean(HalConfiguration.class);
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, halConfiguration));
} catch (BeansException e) {
halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, new HalConfiguration()));
}
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON, HAL_JSON_UTF8));
halConverter.setObjectMapper(halObjectMapper);
result.add(halConverter);
}
if (beanFactory.containsBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME)) {
ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
halFormsObjectMapper.registerModule(new Jackson2HalFormsModule());
try {
HalFormsConfiguration halFormsConfiguration = beanFactory.getBean(HalFormsConfiguration.class);
halFormsObjectMapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, true, halFormsConfiguration));
} catch (BeansException e) {
halFormsObjectMapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, true, new HalFormsConfiguration()));
}
MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class);
halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON));
halFormsConverter.setObjectMapper(halFormsObjectMapper);
result.add(halFormsConverter);
}
result.addAll(converters);
return result;
}
@@ -341,14 +390,13 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
return bean;
if (HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || HAL_FORMS_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
ObjectMapper mapper = (ObjectMapper) bean;
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
}
ObjectMapper mapper = (ObjectMapper) bean;
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
return bean;
}
/*

View File

@@ -20,15 +20,19 @@ import static org.springframework.core.annotation.AnnotationUtils.*;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* {@link MappingDiscoverer} implementation that inspects mappings from a particular annotation.
*
* @author Oliver Gierke
* @author Mark Paluch
* @author Greg Turnquist
*/
public class AnnotationMappingDiscoverer implements MappingDiscoverer {
@@ -106,6 +110,34 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer {
return typeMapping == null || "/".equals(typeMapping) ? mapping[0] : join(typeMapping, mapping[0]);
}
/**
* 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) {
Assert.notNull(type, "Type must not be null!");
Assert.notNull(method, "Method must not be null!");
Annotation mergedAnnotation = findMergedAnnotation(method, annotationType);
Object value = getValue(mergedAnnotation, "method");
RequestMethod[] requestMethods = (RequestMethod[]) value;
List<String> requestMethodNames = new ArrayList<String>();
for (RequestMethod requestMethod : requestMethods) {
requestMethodNames.add(requestMethod.toString());
}
return requestMethodNames.toArray(new String[]{});
}
private String[] getMappingFrom(Annotation annotation) {
if (annotation == null) {

View File

@@ -18,9 +18,14 @@ package org.springframework.hateoas.core;
import static org.springframework.hateoas.core.EncodingUtils.*;
import static org.springframework.web.util.UriComponentsBuilder.*;
import lombok.Getter;
import java.net.URI;
import java.util.Optional;
import java.util.ArrayList;
import java.util.List;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
@@ -36,11 +41,14 @@ import org.springframework.web.util.UriComponentsBuilder;
* @author Oliver Gierke
* @author Kamill Sokol
* @author Kevin Conaway
* @author Greg Turnquist
*/
public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkBuilder {
private final UriComponents uriComponents;
private @Getter final List<Affordance> affordances;
/**
* Creates a new {@link LinkBuilderSupport} using the given {@link UriComponentsBuilder}.
*
@@ -50,17 +58,19 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
Assert.notNull(builder, "UriComponentsBuilder must not be null!");
this.uriComponents = builder.build();
this.affordances = new ArrayList<Affordance>();
}
/**
* Creates a new {@link LinkBuilderSupport} using the given {@link UriComponents}.
*
*
* @param uriComponents must not be {@literal null}.
*/
public LinkBuilderSupport(UriComponents uriComponents) {
Assert.notNull(uriComponents, "UriComponents must not be null!");
this.uriComponents = uriComponents;
this.affordances = new ArrayList<Affordance>();
}
/*
@@ -133,12 +143,25 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
return uriComponents.encode().toUri().normalize();
}
public LinkBuilderSupport addAffordances(List<Affordance> affordances) {
this.affordances.addAll(affordances);
return this;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkBuilder#withRel(java.lang.String)
*/
public Link withRel(String rel) {
return new Link(toString(), rel);
Link link = new Link(toString(), rel);
for (Affordance affordance : this.affordances) {
link = link.withAffordance(affordance);
}
return link;
}
/*

View File

@@ -18,9 +18,11 @@ package org.springframework.hateoas.core;
import java.lang.reflect.Method;
/**
* Strategy interface to discover a URI mapping 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
*/
public interface MappingDiscoverer {
@@ -49,4 +51,14 @@ public interface MappingDiscoverer {
* @return the method mapping including the type-level one or {@literal null} if neither of them present.
*/
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.
*
* @param type
* @param method
* @return
*/
String[] getRequestType(Class<?> type, Method method);
}

View File

@@ -144,6 +144,13 @@ public class Jackson2HalModule extends SimpleModule {
this.halConfiguration = halConfiguration;
}
/**
* Needed to support Jackson
*/
HalLinkListSerializer() {
this(null, null, null, null, new HalConfiguration().withRenderSingleLinks(RenderSingleLinks.AS_SINGLE));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
@@ -679,27 +686,6 @@ public class Jackson2HalModule extends SimpleModule {
private final Map<Class<?>, Object> serializers = new HashMap<>();
private final AutowireCapableBeanFactory delegate;
/**
* Creates a new {@link HalHandlerInstantiator} using the given {@link RelProvider}, {@link CurieProvider} and
* {@link MessageSourceAccessor} and {@link AutowireCapableBeanFactory}. Registers a prepared
* {@link HalResourcesSerializer} and {@link HalLinkListSerializer} falling back to instantiation using the given
* {@link AutowireCapableBeanFactory} if provided, or simple default constructor instantiation if not.
*
* @param provider must not be {@literal null}.
* @param curieProvider can be {@literal null}.
* @param accessor can be {@literal null}.
* @param beanFactory can be {@literal null}
*/
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
AutowireCapableBeanFactory beanFactory, HalConfiguration halConfiguration) {
this(provider, curieProvider, accessor, true, beanFactory, halConfiguration);
}
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
MessageSourceAccessor messageSourceAccessor, AutowireCapableBeanFactory beanFactory) {
this(provider, curieProvider, messageSourceAccessor, beanFactory, beanFactory.getBean(HalConfiguration.class));
}
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
MessageSourceAccessor messageSourceAccessor) {
this(provider, curieProvider, messageSourceAccessor, new HalConfiguration());
@@ -864,7 +850,7 @@ public class Jackson2HalModule extends SimpleModule {
*
* @author Oliver Gierke
*/
private static class EmbeddedMapper {
public static class EmbeddedMapper {
private RelProvider relProvider;
private CurieProvider curieProvider;

View File

@@ -31,7 +31,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @author Greg Turnquist
*/
@JsonIgnoreProperties({"rel", "media"})
abstract class LinkMixin extends Link {
public abstract class LinkMixin extends Link {
private static final long serialVersionUID = 4720588561299667409L;

View File

@@ -28,7 +28,14 @@ import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
abstract class ResourceSupportMixin extends ResourceSupport {
/**
* Custom mixin to render {@link Link}s in HAL.
*
* @author Alexander Baetz
* @author Oliver Gierke
* @author Greg Turnquist
*/
public abstract class ResourceSupportMixin extends ResourceSupport {
@Override
@XmlElement(name = "link")

View File

@@ -28,6 +28,13 @@ import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Custom mixin to to render collection content as {@literal _embedded}.
*
* @author Alexander Baetz
* @author Oliver Gierke
* @author Greg Turnquist
*/
@JsonPropertyOrder({ "content", "links" })
public abstract class ResourcesMixin<T> extends Resources<T> {

View File

@@ -0,0 +1,149 @@
/*
* 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.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.util.UriComponents;
/**
* {@link AffordanceModel} for a HAL-FORMS {@link org.springframework.http.MediaType}.
*
* @author Greg Turnquist
*/
public class HalFormsAffordanceModel implements AffordanceModel {
private static final Logger log = LoggerFactory.getLogger(HalFormsAffordanceModel.class);
/**
* 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());
}
}
/**
* Transform the details of the Spring MVC method's {@link RequestBody} into a collection of {@link HalFormsProperty}s.
*
* @return
*/
public List<HalFormsProperty> getProperties() {
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));
}
return halFormsProperties;
}
/**
* Look up the path of the {@link UriComponents}.
*
* @return
*/
public String getPath() {
return this.components.getPath();
}
/**
* Based on the Spring MVC controller's HTTP method, decided whether or not input attributes are required or not.
*
* @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;
}
}
/**
* 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) {
if (method == null) {
return;
}
log.debug("Gathering details about " + method.getDeclaringClass().getCanonicalName() + "." + method.getName());
for (int i = 0; i < method.getParameterTypes().length; i++) {
for (Annotation annotation : method.getParameterAnnotations()[i]) {
if (annotation.annotationType().equals(RequestBody.class)) {
log.debug("\tRequest body: " + method.getParameterTypes()[i].getCanonicalName() + "(");
for (PropertyDescriptor descriptor : BeanUtils.getPropertyDescriptors(method.getParameterTypes()[i])) {
if (!descriptor.getName().equals("class")) {
log.debug("\t\t" + descriptor.getPropertyType().getCanonicalName() + " " + descriptor.getName());
this.properties.put(descriptor.getName(), descriptor.getPropertyType());
}
}
log.debug(")");
}
}
}
log.debug("Assembled " + this.toString());
}
}

View File

@@ -0,0 +1,42 @@
/*
* 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 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.DummyInvocationUtils.MethodInvocation;
import org.springframework.http.MediaType;
import org.springframework.web.util.UriComponents;
/**
* Factory for creating {@link HalFormsAffordanceModel}s.
*
* @author Greg Turnquist
*/
@Getter
public class HalFormsAffordanceModelFactory extends AffordanceModelFactory {
private final MediaType mediaType = MediaTypes.HAL_FORMS_JSON;
@Override
public AffordanceModel getAffordanceModel(Affordance affordance, MethodInvocation invocationValue, UriComponents components) {
return new HalFormsAffordanceModel(affordance, invocationValue, components);
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.experimental.Wither;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.hal.HalConfiguration;
/**
* @author Greg Turnquist
*/
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class HalFormsConfiguration {
private @Wither @Getter RenderSingleLinks renderSingleLinks = RenderSingleLinks.AS_SINGLE;
public enum RenderSingleLinks {
/**
* A single {@link Link} is rendered as a JSON object.
*/
AS_SINGLE,
/**
* A single {@link Link} is rendered as a JSON Array.
*/
AS_ARRAY
}
/**
* Translate a {@link HalFormsConfiguration} into a {@link HalConfiguration}.
*
* @return
*/
public HalConfiguration toHalConfiguration() {
if (this.getRenderSingleLinks() == RenderSingleLinks.AS_SINGLE) {
return new HalConfiguration().withRenderSingleLinks(HalConfiguration.RenderSingleLinks.AS_SINGLE);
}
if (this.getRenderSingleLinks() == RenderSingleLinks.AS_ARRAY) {
return new HalConfiguration().withRenderSingleLinks(HalConfiguration.RenderSingleLinks.AS_ARRAY);
}
throw new IllegalStateException("Don't know how to translate " + this);
}
}

View File

@@ -0,0 +1,145 @@
/*
* 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.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
import com.fasterxml.jackson.databind.type.TypeFactory;
/**
* Collection of components needed to deserialize a HAL-FORMS document.
*
* @author Greg Turnquist
*/
public class HalFormsDeserializers {
static class HalFormsResourcesDeserializer extends ContainerDeserializerBase<List<Object>> implements ContextualDeserializer {
private JavaType contentType;
HalFormsResourcesDeserializer(JavaType contentType) {
super(contentType);
this.contentType = contentType;
}
HalFormsResourcesDeserializer() {
this(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, Object.class));
}
@Override
public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
List<Object> result = new ArrayList<Object>();
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(contentType);
Object object;
// links is an object, so we parse till we find its end.
while (!JsonToken.END_OBJECT.equals(jp.nextToken())) {
if (!JsonToken.FIELD_NAME.equals(jp.getCurrentToken())) {
throw new JsonParseException("Expected relation name", jp.getCurrentLocation());
}
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
object = deser.deserialize(jp, ctxt);
result.add(object);
}
} else {
object = deser.deserialize(jp, ctxt);
result.add(object);
}
}
return result;
}
@Override
public JavaType getContentType() {
return this.contentType;
}
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
if (property != null) {
JavaType vc = property.getType().getContentType();
return new HalFormsResourcesDeserializer(vc);
} else {
return new HalFormsResourcesDeserializer(ctxt.getContextualType());
}
}
}
/**
* Deserialize a {@link MediaType} embedded inside a HAL-FORMS document.
*/
static class MediaTypesDeserializer extends ContainerDeserializerBase<List<MediaType>> {
private static final long serialVersionUID = -7218376603548438390L;
public MediaTypesDeserializer() {
super(TypeFactory.defaultInstance().constructCollectionLikeType(List.class, MediaType.class));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public List<MediaType> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return MediaType.parseMediaTypes(p.getText());
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* 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 static com.fasterxml.jackson.annotation.JsonInclude.*;
import static org.springframework.hateoas.hal.Jackson2HalModule.*;
import lombok.Builder;
import lombok.Data;
import lombok.Singular;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Representation of a HAL-FORMS document.
*
* @author Dietrich Schulten
* @author Greg Turnquist
*/
@Data
@Builder(builderMethodName = "halFormsDocument")
@JsonPropertyOrder({ "resource", "resources", "embedded", "links", "templates", "metadata" })
public class HalFormsDocument<T> {
@JsonUnwrapped
@JsonInclude(Include.NON_NULL)
private T resource;
@JsonIgnore
@JsonInclude(Include.NON_EMPTY)
private Collection<T> resources;
@JsonProperty("_embedded")
@JsonInclude(Include.NON_NULL)
private Map<String, Object> embedded;
@JsonProperty("page")
@JsonInclude(Include.NON_NULL)
private PagedResources.PageMetadata pageMetadata;
@Singular private List<Link> links;
@Singular 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;
}
HalFormsDocument() {
this(null, null, null, null, new ArrayList<Link>(), new HashMap<String, HalFormsTemplate>());
}
@JsonProperty("_links")
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = HalLinkListSerializer.class)
@JsonDeserialize(using = HalLinkListDeserializer.class)
public List<Link> getLinks() {
return this.links;
}
@JsonProperty("_templates")
@JsonInclude(Include.NON_EMPTY)
public Map<String, HalFormsTemplate> getTemplates() {
return this.templates;
}
@JsonIgnore
public HalFormsTemplate getTemplate() {
return getTemplate(HalFormsTemplate.DEFAULT_KEY);
}
@JsonIgnore
public HalFormsTemplate getTemplate(String key) {
return this.templates.get(key);
}
}

View File

@@ -0,0 +1,31 @@
/*
* 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.MediaTypes;
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
/**
* HAL-FORMS based {@link JsonPathLinkDiscoverer}.
*
* @author Greg Turnquist
*/
public class HalFormsLinkDiscoverer extends JsonPathLinkDiscoverer {
public HalFormsLinkDiscoverer() {
super("$._links..['%s']..href", MediaTypes.HAL_FORMS_JSON);
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2016-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.io.IOException;
import java.util.Arrays;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
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.
*
* @author Dietrich Schulten
* @author Greg Turnquist
*/
public class HalFormsMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public HalFormsMessageConverter(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
this.objectMapper.registerModule(new Jackson2HalFormsModule());
setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_FORMS_JSON));
}
/*
* (non-Javadoc)
* @see org.springframework.http.converter.AbstractHttpMessageConverter#supports(java.lang.Class)
*/
@Override
protected boolean supports(final Class<?> clazz) {
return true;
}
/*
* (non-Javadoc)
* @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)
throws IOException, HttpMessageNotReadableException {
return this.objectMapper.readValue(inputMessage.getBody(), clazz);
}
@Override
protected void writeInternal(final Object t, final HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {
JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8);
// A workaround for JsonGenerators not applying serialization features
// https://github.com/FasterXML/jackson-databind/issues/12
if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
jsonGenerator.useDefaultPrettyPrinter();
}
try {
objectMapper.writeValue(jsonGenerator, t);
} catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex);
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2016-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 lombok.AllArgsConstructor;
import lombok.Value;
import org.springframework.hateoas.AffordanceModel;
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
*
* @see http://mamund.site44.com/misc/hal-forms/
*/
@JsonInclude(Include.NON_DEFAULT)
@Value
@AllArgsConstructor
public class HalFormsProperty {
private String name;
/**
* readOnly uses {@link Boolean} not {@literal boolean}, because if {@literal null}, the element won't be rendered
*/
private Boolean readOnly;
private String value;
private String prompt;
private String regex;
private boolean templated;
private @JsonInclude(Include.ALWAYS) boolean required;
private boolean multi;
/**
* Default constructor to support Jackson.
*/
HalFormsProperty() {
this(null, null, null, null, null, false, false, false);
}
}

View File

@@ -0,0 +1,245 @@
/*
* 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.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;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.http.HttpMethod;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
/**
* Collection of components needed to serialize a HAL-FORMS document.
*
* @author Greg Turnquist
*/
public class HalFormsSerializers {
/**
* Serializer for {@link Resources}.
*/
static class HalFormsResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
private final BeanProperty property;
HalFormsResourceSerializer(BeanProperty property) {
super(Resource.class, false);
this.property = property;
}
HalFormsResourceSerializer() {
this(null);
}
@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();
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
}
@Override
public JavaType getContentType() {
return null;
}
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
@Override
public boolean hasSingleElement(Resource<?> resource) {
return false;
}
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer typeSerializer) {
return null;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new HalFormsResourceSerializer(property);
}
}
/**
* Serializer for {@link Resources}
*/
static class HalFormsResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
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;
}
HalFormsResourcesSerializer(Jackson2HalModule.EmbeddedMapper embeddedMapper) {
this(null, embeddedMapper);
}
@Override
public void serialize(Resources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> embeddeds = embeddedMapper.map(value);
HalFormsDocument<?> doc;
if (value instanceof PagedResources) {
doc = HalFormsDocument.<Object> halFormsDocument()
.embedded(embeddeds)
.pageMetadata(((PagedResources) value).getMetadata())
.links(value.getLinks())
.templates(findTemplates(value))
.build();
} else {
doc = HalFormsDocument.<Object> halFormsDocument()
.embedded(embeddeds)
.pageMetadata(null)
.links(value.getLinks())
.templates(findTemplates(value))
.build();
}
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
}
@Override
public JavaType getContentType() {
return null;
}
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
@Override
public boolean hasSingleElement(Resources<?> resources) {
return resources.getContent().size() == 1;
}
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer typeSerializer) {
return null;
}
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new HalFormsResourcesSerializer(property, embeddedMapper);
}
}
/**
* Extract template details from a {@link ResourceSupport}'s {@link Affordance}s.
*
* @param resource
* @return
*/
private static Map<String, HalFormsTemplate> findTemplates(ResourceSupport resource) {
Map<String, HalFormsTemplate> templates = new HashMap<String, HalFormsTemplate>();
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);
if (!affordance.getHttpMethod().equals(HttpMethod.GET.toString())) {
validate(resource, affordance, model);
HalFormsTemplate template = new HalFormsTemplate();
template.setHttpMethod(HttpMethod.valueOf(affordance.getHttpMethod()));
template.setProperties(model.getProperties());
/**
* First template in HAL-FORMS is "default".
*/
if (templates.isEmpty()) {
templates.put("default", template);
} else {
templates.put(affordance.getName(), template);
}
}
}
}
return templates;
}
/**
* Verify that the resource's self link and the affordance's URI have the same relative path.
* @param resource
* @param affordance
* @param model
*/
private static void validate(ResourceSupport resource, Affordance affordance, HalFormsAffordanceModel model) {
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");
}
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2016-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 lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import java.util.ArrayList;
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.StringUtils;
import com.fasterxml.jackson.annotation.JsonIgnore;
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;
/**
* Value object for a HAL-FORMS template. Describes the available state transition details.
*
* @author Dietrich Schulten
* @author Greg Turnquist
* @see https://rwcbook.github.io/hal-forms/#_the_code__templates_code_element
*/
@Data
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@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;
/**
* Configure a HAL-FORMS template with a key value.
* @param key
*/
public HalFormsTemplate(String key) {
this.key = key;
}
/**
* 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>.
*/
public HalFormsTemplate() {
this(HalFormsTemplate.DEFAULT_KEY);
}
public String getContentType() {
return StringUtils.collectionToCommaDelimitedString(contentType);
}
@JsonDeserialize(using = MediaTypesDeserializer.class)
public void setContentType(List<MediaType> contentType) {
this.contentType = contentType;
}
public String getMethod() {
return this.httpMethod == null ? null : this.httpMethod.toString().toLowerCase();
}
public void setMethod(String method) {
this.httpMethod = HttpMethod.valueOf(method.toUpperCase());
}
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2016-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.List;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Configure a HAL-FORMS {@link HttpMessageConverter}.
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@Configuration
public class HalFormsWebMvcConfigurer extends WebMvcConfigurerAdapter {
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#configureMessageConverters(java.util.List)
*/
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new HalFormsMessageConverter(new ObjectMapper()));
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2016-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.HashMap;
import java.util.Map;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.Jackson2HalModule.EmbeddedMapper;
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.HalFormsSerializers.HalFormsResourcesSerializer;
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.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;
/**
* Serialize/Deserialize all the parts of HAL-FORMS documents using Jackson.
*
* @author Dietrich Schulten
* @author Greg Turnquist
*/
public class Jackson2HalFormsModule extends SimpleModule {
private static final long serialVersionUID = -4496351128468451196L;
public Jackson2HalFormsModule() {
super("hal-forms-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
setMixInAnnotation(Link.class, LinkMixin.class);
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
setMixInAnnotation(Resource.class, ResourceMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
}
/**
* Create new HAL-FORMS serializers based on the context.
*/
public static class HalFormsHandlerInstantiator extends HalHandlerInstantiator {
private final Map<Class<?>, Object> serializers = new HashMap<Class<?>, Object>();
public HalFormsHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
HalFormsConfiguration halFormsConfiguration) {
super(resolver, curieProvider, messageSource, enforceEmbeddedCollections, halFormsConfiguration.toHalConfiguration());
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(mapper));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, messageSource, halFormsConfiguration.toHalConfiguration()));
}
public HalFormsHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider,
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
AutowireCapableBeanFactory beanFactory) {
this(relProvider, curieProvider, messageSource, enforceEmbeddedCollections, beanFactory.getBean(HalFormsConfiguration.class));
}
private Object findInstance(Class<?> type) {
return this.serializers.get(type);
}
/*
* (non-Javadoc)
*
* @see
* com.fasterxml.jackson.databind.cfg.HandlerInstantiator#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) {
Object jsonDeser = findInstance(deserClass);
return jsonDeser != null ? (JsonDeserializer<?>) jsonDeser
: 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)
*/
@Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> keyDeserClass) {
Object keyDeser = findInstance(keyDeserClass);
return keyDeser != null ? (KeyDeserializer) keyDeser
: 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)
*/
@Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
Object jsonSer = findInstance(serClass);
return jsonSer != null ? (JsonSerializer<?>) jsonSer : super.serializerInstance(config, annotated, serClass);
}
/*
* (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)
*/
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated,
Class<?> builderClass) {
Object builder = findInstance(builderClass);
return builder != null ? (TypeResolverBuilder<?>) builder
: 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)
*/
@Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
Object resolver = findInstance(resolverClass);
return resolver != null ? (TypeIdResolver) resolver
: super.typeIdResolverInstance(config, annotated, resolverClass);
}
}
}

View File

@@ -0,0 +1,37 @@
/*
* 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

@@ -0,0 +1,28 @@
/*
* 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

@@ -0,0 +1,45 @@
/*
* 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,16 +22,24 @@ import lombok.experimental.Delegate;
import java.lang.reflect.Method;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
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.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.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -83,14 +91,33 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
* @param uriComponents must not be {@literal null}.
*/
ControllerLinkBuilder(UriComponents uriComponents) {
this(uriComponents, TemplateVariables.NONE);
this(uriComponents, TemplateVariables.NONE, null);
}
ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables) {
ControllerLinkBuilder(UriComponents uriComponents, TemplateVariables variables, MethodInvocation invocation) {
super(uriComponents);
this.variables = variables;
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);
}
/**
@@ -194,6 +221,29 @@ public class ControllerLinkBuilder extends LinkBuilderSupport<ControllerLinkBuil
return FACTORY.linkTo(invocationValue);
}
/**
* 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)))
* </pre>
*
* This takes a link and adds an {@link Affordance} based on another Spring MVC handler method.
*
* @param invocationValue
* @return
*/
public static Affordance afford(Object invocationValue) {
ControllerLinkBuilder linkBuilder = linkTo(invocationValue);
Assert.isTrue(linkBuilder.getAffordances().size() == 1, "A base can only have one affordance, itself");
return linkBuilder.getAffordances().get(0);
}
/**
* Wrapper for {@link DummyInvocationUtils#methodOn(Class, Object...)} to be available in case you work with static
* imports of {@link ControllerLinkBuilder}.

View File

@@ -65,6 +65,7 @@ import org.springframework.web.util.UriTemplate;
* @author Oemer Yildiz
* @author Kevin Conaway
* @author Andrew Naydyonock
* @author Greg Turnquist
*/
public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<ControllerLinkBuilder> {
@@ -137,6 +138,7 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
Method method = invocation.getMethod();
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), method);
UriComponentsBuilder builder = ControllerLinkBuilder.getBuilder().path(mapping);
UriTemplate template = new UriTemplate(mapping);
@@ -184,10 +186,10 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
variables = variables.concat(variable);
}
return new ControllerLinkBuilder(components, variables);
return new ControllerLinkBuilder(components, variables, invocation);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.MethodLinkBuilderFactory#linkTo(java.lang.reflect.Method, java.lang.Object[])
*/
@@ -196,6 +198,8 @@ public class ControllerLinkBuilderFactory implements MethodLinkBuilderFactory<Co
return ControllerLinkBuilder.linkTo(method, parameters);
}
/**
* Applies the configured {@link UriComponentsContributor}s to the given {@link UriComponentsBuilder}.
*

View File

@@ -0,0 +1,81 @@
/*
* 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 lombok.Data;
import java.lang.reflect.Method;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModel;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* Spring MVC-based representation of an {@link Affordance}.
*
* @author Greg Turnquist
*/
@Data
public class SpringMvcAffordance implements Affordance {
private static final Logger log = LoggerFactory.getLogger(SpringMvcAffordance.class);
private final HashMap<MediaType, AffordanceModel> affordanceModels;
/**
* Request method verb associated with the Spring MVC controller method.
*/
private final RequestMethod requestMethod;
/**
* Handle on the Spring MVC controller {@link Method}.
*/
private final Method method;
/**
* 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();
}
@Override
public String getName() {
return this.method.getName();
}
@Override
public AffordanceModel getAffordanceModel(MediaType mediaType) {
return this.affordanceModels.get(mediaType);
}
@Override
public void addAffordanceModel(MediaType mediaType, AffordanceModel affordanceModel) {
this.affordanceModels.put(mediaType, affordanceModel);
}
}

View File

@@ -0,0 +1,76 @@
/*
* 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.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModelFactory;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MappingDiscoverer;
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;
/**
* Construct {@link SpringMvcAffordance}s using a collection of {@link AffordanceModelFactory}s.
*
* @author Greg Turnquist
*/
public 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;
}
/**
* 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) {
Method method = invocation.getMethod();
String[] httpMethods = discoverer.getRequestType(invocation.getTargetType(), method);
List<Affordance> affordances = new ArrayList<Affordance>();
for (String requestMethod : httpMethods) {
SpringMvcAffordance springMvcAffordance = new SpringMvcAffordance(RequestMethod.valueOf(requestMethod), invocation.getMethod());
for (AffordanceModelFactory factory : factories) {
springMvcAffordance.addAffordanceModel(factory.getMediaType(), factory.getAffordanceModel(springMvcAffordance, invocation, components));
}
affordances.add(springMvcAffordance);
}
return affordances;
}
}