/*
* Copyright 2013-2016 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.config;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.*;
import static org.springframework.beans.factory.support.BeanDefinitionReaderUtils.*;
import static org.springframework.hateoas.MediaTypes.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.hateoas.EntityLinks;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkDiscoverers;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.AnnotationRelProvider;
import org.springframework.hateoas.core.DefaultRelProvider;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.plugin.core.support.PluginRegistryFactoryBean;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link ImportBeanDefinitionRegistrar} implementation to activate hypermedia support based on the configured
* hypermedia type. Activates {@link EntityLinks} support as well (essentially as if {@link EnableEntityLinks} was
* activated as well).
*
* @author Oliver Gierke
*/
class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
private static final boolean JACKSON2_PRESENT = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper",
null);
private static final boolean JSONPATH_PRESENT = ClassUtils.isPresent("com.jayway.jsonpath.JsonPath", null);
private static final boolean EVO_PRESENT = ClassUtils.isPresent("org.atteo.evo.inflector.English", null);
private final ImportBeanDefinitionRegistrar linkBuilderBeanDefinitionRegistrar = new LinkBuilderBeanDefinitionRegistrar();
/*
* (non-Javadoc)
* @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(metadata, registry);
Map<String, Object> attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName());
Collection<HypermediaType> types = Arrays.asList((HypermediaType[]) attributes.get("type"));
for (HypermediaType type : types) {
if (JSONPATH_PRESENT) {
AbstractBeanDefinition linkDiscovererBeanDefinition = getLinkDiscovererBeanDefinition(type);
registerBeanDefinition(new BeanDefinitionHolder(linkDiscovererBeanDefinition,
BeanDefinitionReaderUtils.generateBeanName(linkDiscovererBeanDefinition, registry)), registry);
}
}
if (types.contains(HypermediaType.HAL)) {
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 (!types.isEmpty()) {
BeanDefinitionBuilder linkDiscoverersRegistryBuilder = BeanDefinitionBuilder
.rootBeanDefinition(PluginRegistryFactoryBean.class);
linkDiscoverersRegistryBuilder.addPropertyValue("type", LinkDiscoverer.class);
registerSourcedBeanDefinition(linkDiscoverersRegistryBuilder, metadata, registry,
LINK_DISCOVERER_REGISTRY_BEAN_NAME);
BeanDefinitionBuilder linkDiscoverersBuilder = BeanDefinitionBuilder.rootBeanDefinition(LinkDiscoverers.class);
linkDiscoverersBuilder.addConstructorArgReference(LINK_DISCOVERER_REGISTRY_BEAN_NAME);
registerSourcedBeanDefinition(linkDiscoverersBuilder, metadata, registry);
}
registerRelProviderPluginRegistryAndDelegate(registry);
}
/**
* 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.
*
* @param registry
*/
private static void registerRelProviderPluginRegistryAndDelegate(BeanDefinitionRegistry registry) {
Class<?> defaultRelProviderType = EVO_PRESENT ? EvoInflectorRelProvider.class : DefaultRelProvider.class;
RootBeanDefinition defaultRelProviderBeanDefinition = new RootBeanDefinition(defaultRelProviderType);
registry.registerBeanDefinition("defaultRelProvider", defaultRelProviderBeanDefinition);
RootBeanDefinition annotationRelProviderBeanDefinition = new RootBeanDefinition(AnnotationRelProvider.class);
registry.registerBeanDefinition("annotationRelProvider", annotationRelProviderBeanDefinition);
BeanDefinitionBuilder registryFactoryBeanBuilder = BeanDefinitionBuilder
.rootBeanDefinition(PluginRegistryFactoryBean.class);
registryFactoryBeanBuilder.addPropertyValue("type", RelProvider.class);
registryFactoryBeanBuilder.addPropertyValue("exclusions", DelegatingRelProvider.class);
AbstractBeanDefinition registryBeanDefinition = registryFactoryBeanBuilder.getBeanDefinition();
registry.registerBeanDefinition("relProviderPluginRegistry", registryBeanDefinition);
BeanDefinitionBuilder delegateBuilder = BeanDefinitionBuilder.rootBeanDefinition(DelegatingRelProvider.class);
delegateBuilder.addConstructorArgValue(registryBeanDefinition);
AbstractBeanDefinition beanDefinition = delegateBuilder.getBeanDefinition();
beanDefinition.setPrimary(true);
registry.registerBeanDefinition(DELEGATING_REL_PROVIDER_BEAN_NAME, beanDefinition);
}
/**
* Returns a {@link LinkDiscoverer} {@link BeanDefinition} suitable for the given {@link HypermediaType}.
*
* @param type
* @return
*/
private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType type) {
AbstractBeanDefinition definition;
switch (type) {
case HAL:
definition = new RootBeanDefinition(HalLinkDiscoverer.class);
break;
default:
throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type));
}
definition.setSource(this);
return definition;
}
private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builder, AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
String generateBeanName = BeanDefinitionReaderUtils.generateBeanName(beanDefinition, registry);
return registerSourcedBeanDefinition(builder, metadata, registry, generateBeanName);
}
private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builder, AnnotationMetadata metadata,
BeanDefinitionRegistry registry, String name) {
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
beanDefinition.setSource(metadata);
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, name);
registerBeanDefinition(holder, registry);
return name;
}
/**
* {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in
* the {@link ApplicationContext}.
*
* @author Oliver Gierke
*/
static class Jackson2ModuleRegisteringBeanPostProcessor implements BeanPostProcessor, BeanFactoryAware {
private AutowireCapableBeanFactory beanFactory;
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
Assert.isInstanceOf(AutowireCapableBeanFactory.class, beanFactory,
"BeanFactory must be an AutowireCapableBeanFactory!");
this.beanFactory = (AutowireCapableBeanFactory) beanFactory;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
adapter.setMessageConverters(potentiallyRegisterModule(adapter.getMessageConverters()));
}
if (bean instanceof RestTemplate) {
RestTemplate template = (RestTemplate) bean;
template.setMessageConverters(potentiallyRegisterModule(template.getMessageConverters()));
}
return bean;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
private List<HttpMessageConverter<?>> potentiallyRegisterModule(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof MappingJackson2HttpMessageConverter) {
MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter;
ObjectMapper objectMapper = halConverterCandidate.getObjectMapper();
if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) {
return converters;
}
}
}
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));
halConverter.setObjectMapper(halObjectMapper);
List<HttpMessageConverter<?>> result = new ArrayList<HttpMessageConverter<?>>(converters.size());
result.add(halConverter);
result.addAll(converters);
return result;
}
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
}
/**
* {@link BeanPostProcessor} to disable the default HAL {@link ObjectMapper} to fail on unknown properties. Needed as
* the methods to do that on {@link Jackson2ObjectMapperFactoryBean} were introduced in Spring 4.1 only.
*
* @author Oliver Gierke
*/
private static class DefaultObjectMapperCustomizer implements BeanPostProcessor {
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessAfterInitialization(java.lang.Object, java.lang.String)
*/
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (!HAL_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) {
return bean;
}
ObjectMapper mapper = (ObjectMapper) bean;
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
}
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String)
*/
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
}
}