#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:
committed by
Oliver Gierke
parent
79ebf9b5a4
commit
70448a8540
57
src/main/java/org/springframework/hateoas/Affordance.java
Normal file
57
src/main/java/org/springframework/hateoas/Affordance.java
Normal 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);
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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}.
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user