diff --git a/src/main/java/org/springframework/hateoas/config/ConverterRegisteringBeanPostProcessor.java b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringBeanPostProcessor.java new file mode 100644 index 00000000..6829bd82 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringBeanPostProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 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 lombok.RequiredArgsConstructor; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.ObjectFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * {@link BeanPostProcessor} to register {@link Jackson2HalModule} with {@link ObjectMapper} instances registered in the + * {@link ApplicationContext}. + * + * @author Oliver Gierke + */ +@RequiredArgsConstructor +class ConverterRegisteringBeanPostProcessor implements BeanPostProcessor { + + private final ObjectFactory configurer; + + /* + * (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 RestTemplate) { + + ConverterRegisteringWebMvcConfigurer object = configurer.getObject(); + object.extendMessageConverters(((RestTemplate) bean).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; + } +} diff --git a/src/main/java/org/springframework/hateoas/config/ConverterRegisteringWebMvcConfigurer.java b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringWebMvcConfigurer.java new file mode 100644 index 00000000..82992f69 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringWebMvcConfigurer.java @@ -0,0 +1,139 @@ +/* + * Copyright 2018 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.hateoas.MediaTypes.*; + +import lombok.RequiredArgsConstructor; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.hateoas.RelProvider; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.core.DelegatingRelProvider; +import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.hateoas.hal.HalConfiguration; +import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Oliver Gierke + */ +@Configuration +@RequiredArgsConstructor +public class ConverterRegisteringWebMvcConfigurer extends WebMvcConfigurerAdapter implements BeanFactoryAware { + + private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource"; + + private final ObjectProvider mapper; + private final ObjectProvider relProvider; + private final ObjectProvider curieProvider; + private final ObjectProvider halConfiguration; + + private BeanFactory beanFactory; + private Collection hypermediaTypes; + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + /** + * @param hyperMediaTypes the hyperMediaTypes to set + */ + public void setHypermediaTypes(Collection hyperMediaTypes) { + this.hypermediaTypes = hyperMediaTypes; + } + + /* + * (non-Javadoc) + * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List) + */ + @Override + public void extendMessageConverters(List> converters) { + + for (HttpMessageConverter converter : converters) { + if (converter instanceof MappingJackson2HttpMessageConverter) { + MappingJackson2HttpMessageConverter halConverterCandidate = (MappingJackson2HttpMessageConverter) converter; + ObjectMapper objectMapper = halConverterCandidate.getObjectMapper(); + if (Jackson2HalModule.isAlreadyRegisteredIn(objectMapper)) { + return; + } + } + } + + ObjectMapper objectMapper = mapper.getIfAvailable(); + objectMapper = objectMapper == null ? new ObjectMapper() : objectMapper; + + CurieProvider curieProvider = this.curieProvider.getIfAvailable(); + RelProvider relProvider = this.relProvider.getObject(); + MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, + MessageSourceAccessor.class); + + if (hypermediaTypes.contains(HypermediaType.HAL)) { + converters.add(0, createHalConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource)); + } + } + + /** + * @param objectMapper + * @param curieProvider + * @param relProvider + * @param linkRelationMessageSource + * @return + */ + private MappingJackson2HttpMessageConverter createHalConverter(ObjectMapper objectMapper, CurieProvider curieProvider, + RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) { + + HalConfiguration halConfiguration = this.halConfiguration.getIfAvailable(); + halConfiguration = halConfiguration == null ? new HalConfiguration() : halConfiguration; + + HalHandlerInstantiator instantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, + linkRelationMessageSource, halConfiguration); + + ObjectMapper mapper = objectMapper.copy(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.registerModule(new Jackson2HalModule()); + mapper.setHandlerInstantiator(instantiator); + + MappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON, HAL_JSON_UTF8)); + converter.setObjectMapper(mapper); + + return converter; + } +} diff --git a/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java b/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java index cedd6f55..96fa3dbb 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java +++ b/src/main/java/org/springframework/hateoas/config/EnableEntityLinks.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2018 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. @@ -36,7 +36,9 @@ import org.springframework.hateoas.core.DelegatingEntityLinks; @Target(ElementType.TYPE) @Inherited @Documented -@Import(LinkBuilderBeanDefinitionRegistrar.class) +@Import({ // + EntityLinksConfiguration.class, // + JaxRsConfigurationImportSelector.class // +}) public @interface EnableEntityLinks { - } diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index 43526c02..5f11cb6b 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2018 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. @@ -39,11 +39,16 @@ 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 }) +@EnableEntityLinks +@Import({ // + HypermediaSupportBeanDefinitionRegistrar.class, // + HateoasConfiguration.class // +}) public @interface EnableHypermediaSupport { /** diff --git a/src/main/java/org/springframework/hateoas/config/EntityLinksConfiguration.java b/src/main/java/org/springframework/hateoas/config/EntityLinksConfiguration.java new file mode 100644 index 00000000..3e718b33 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/EntityLinksConfiguration.java @@ -0,0 +1,70 @@ +/* + * Copyright 2018 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 org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Primary; +import org.springframework.hateoas.EntityLinks; +import org.springframework.hateoas.core.ControllerEntityLinksFactoryBean; +import org.springframework.hateoas.core.DelegatingEntityLinks; +import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.plugin.core.support.PluginRegistryFactoryBean; +import org.springframework.stereotype.Controller; + +/** + * Spring configuration to register a {@link PluginRegistry} for {@link EntityLinks}. + * + * @author Greg Turnquist + * @author Oliver Gierke + */ +@Configuration +class EntityLinksConfiguration { + + @Bean + PluginRegistryFactoryBean> entityLinksPluginRegistry() { + + PluginRegistryFactoryBean> registry = new PluginRegistryFactoryBean>(); + registry.setType(EntityLinks.class); + registry.setExclusions(new Class[] { DelegatingEntityLinks.class }); + + return registry; + } + + @Primary + @Bean + @DependsOn("controllerEntityLinks") + DelegatingEntityLinks delegatingEntityLinks(PluginRegistry> entityLinksPluginRegistry) { + return new DelegatingEntityLinks(entityLinksPluginRegistry); + } + + @Bean + ControllerEntityLinksFactoryBean controllerEntityLinks(ControllerLinkBuilderFactory controllerLinkBuilderFactory) { + + ControllerEntityLinksFactoryBean factory = new ControllerEntityLinksFactoryBean(); + factory.setAnnotation(Controller.class); + factory.setLinkBuilderFactory(controllerLinkBuilderFactory); + + return factory; + } + + @Bean + ControllerLinkBuilderFactory controllerLinkBuilderFactoryBean() { + return new ControllerLinkBuilderFactory(); + } +} diff --git a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java index 3e942116..3a1ebdb8 100644 --- a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2018 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. @@ -16,10 +16,24 @@ package org.springframework.hateoas.config; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.ObjectFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.context.support.ReloadableResourceBundleMessageSource; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.LinkDiscoverers; +import org.springframework.hateoas.RelProvider; +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.http.MediaType; +import org.springframework.plugin.core.PluginRegistry; +import org.springframework.plugin.core.config.EnablePluginRegistries; +import org.springframework.plugin.core.support.PluginRegistryFactoryBean; +import org.springframework.util.ClassUtils; /** * Common HATEOAS specific configuration. @@ -29,6 +43,7 @@ import org.springframework.context.support.ReloadableResourceBundleMessageSource * @since 0.19 */ @Configuration +@EnablePluginRegistries({ LinkDiscoverer.class }) class HateoasConfiguration { /** @@ -50,4 +65,49 @@ class HateoasConfiguration { throw new BeanCreationException("resourceDescriptionMessageSourceAccessor", "", o_O); } } + + @Bean + ConverterRegisteringBeanPostProcessor jackson2ModuleRegisteringBeanPostProcessor( + ObjectFactory configurer) { + return new ConverterRegisteringBeanPostProcessor(configurer); + } + + // RelProvider + + @Bean + RelProvider defaultRelProvider() { + + return ClassUtils.isPresent("org.atteo.evo.inflector.English", null) // + ? new EvoInflectorRelProvider() + : new DefaultRelProvider(); + } + + @Bean + AnnotationRelProvider annotationRelProvider() { + return new AnnotationRelProvider(); + } + + @Primary + @Bean + DelegatingRelProvider _relProvider(PluginRegistry> relProviderPluginRegistry) { + return new DelegatingRelProvider(relProviderPluginRegistry); + } + + @Bean + PluginRegistryFactoryBean> relProviderPluginRegistry() { + + PluginRegistryFactoryBean> factory = new PluginRegistryFactoryBean>(); + + factory.setType(RelProvider.class); + factory.setExclusions(new Class[] { DelegatingRelProvider.class }); + + return factory; + } + + // LinkDiscoverers + + @Bean + LinkDiscoverers linkDiscoverers(PluginRegistry linkDiscovererRegistry) { + return new LinkDiscoverers(linkDiscovererRegistry); + } } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 1cb30c49..af75b1c2 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2016 the original author or authors. + * Copyright 2013-2018 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. @@ -17,59 +17,25 @@ 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.ListableBeanFactory; -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.HalConfiguration; 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 @@ -77,22 +43,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; * activated as well). * * @author Oliver Gierke + * @author Greg Turnquist */ -class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware { +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(); - - private ListableBeanFactory beanFactory; /* * (non-Javadoc) @@ -101,14 +56,12 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - linkBuilderBeanDefinitionRegistrar.registerBeanDefinitions(metadata, registry); - Map attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName()); Collection types = Arrays.asList((HypermediaType[]) attributes.get("type")); - for (HypermediaType type : types) { + if (JSONPATH_PRESENT) { - if (JSONPATH_PRESENT) { + for (HypermediaType type : types) { AbstractBeanDefinition linkDiscovererBeanDefinition = getLinkDiscovererBeanDefinition(type); registerBeanDefinition(new BeanDefinitionHolder(linkDiscovererBeanDefinition, @@ -116,76 +69,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe } } - 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 no HalConfiguration bean, create a default one. - if (this.beanFactory.getBeanNamesForType(HalConfiguration.class).length == 0) { - registerSourcedBeanDefinition(rootBeanDefinition(HalConfiguration.class), 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); - } - - @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. - * - * @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); + BeanDefinitionBuilder configurerBeanDefinition = rootBeanDefinition(ConverterRegisteringWebMvcConfigurer.class); + configurerBeanDefinition.addPropertyValue("hypermediaTypes", types); + registerSourcedBeanDefinition(configurerBeanDefinition, metadata, registry); } /** @@ -228,136 +114,4 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe 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> potentiallyRegisterModule(List> 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, HAL_JSON_UTF8)); - halConverter.setObjectMapper(halObjectMapper); - - List> result = new ArrayList>(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; - } - } } diff --git a/src/main/java/org/springframework/hateoas/config/JaxRsConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/JaxRsConfigurationImportSelector.java new file mode 100644 index 00000000..3ef7a09f --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/JaxRsConfigurationImportSelector.java @@ -0,0 +1,66 @@ +/* + * Copyright 2018 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 javax.ws.rs.Path; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.hateoas.core.ControllerEntityLinksFactoryBean; +import org.springframework.hateoas.jaxrs.JaxRsLinkBuilderFactory; +import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; +import org.springframework.util.ClassUtils; + +/** + * {@link ImportSelector} to register a {@link ControllerLinkBuilderFactory} for JaxRS' {@link Path} in case it's on the + * classpath. + * + * @author Oliver Gierke + * @since 0.25 + */ +class JaxRsConfigurationImportSelector implements ImportSelector { + + private static final boolean IS_JAX_RS_PRESENT = ClassUtils.isPresent("javax.ws.rs.Path", + ClassUtils.getDefaultClassLoader()); + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) + */ + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + + return IS_JAX_RS_PRESENT // + ? new String[] { JaxRsEntityLinksConfiguration.class.getName() } // + : new String[0]; + } + + @Configuration + static class JaxRsEntityLinksConfiguration { + + @Bean + ControllerEntityLinksFactoryBean jaxRsEntityLinks() { + + ControllerEntityLinksFactoryBean factory = new ControllerEntityLinksFactoryBean(); + factory.setAnnotation(Path.class); + factory.setLinkBuilderFactory(new JaxRsLinkBuilderFactory()); + + return factory; + } + } +} diff --git a/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java index 3443bc95..0408eab2 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableEntityLinksIntegrationTest.java @@ -57,15 +57,15 @@ public class EnableEntityLinksIntegrationTest { } @Autowired - DelegatingEntityLinks builder; + DelegatingEntityLinks links; @Test public void initializesDelegatingEntityLinks() { - assertThat(builder, is(notNullValue())); - assertThat(builder.supports(Person.class), is(true)); - assertThat(builder.supports(Address.class), is(true)); - assertThat(builder.supports(Object.class), is(false)); + assertThat(links, is(notNullValue())); + assertThat(links.supports(Person.class), is(true)); + assertThat(links.supports(Address.class), is(true)); + assertThat(links.supports(Object.class), is(false)); } @Controller diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index ff629b68..5373e330 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -28,7 +28,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.runners.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -40,19 +40,23 @@ import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; -import org.springframework.hateoas.config.HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor; import org.springframework.hateoas.core.DelegatingEntityLinks; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.HalConfiguration; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; +import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.mock.web.MockServletContext; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.client.RestTemplate; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.HandlerMethodArgumentResolverComposite; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; @@ -76,7 +80,7 @@ public class EnableHypermediaSupportIntegrationTest { @Test public void registersLinkDiscoverers() { - ApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + ConfigurableApplicationContext context = createApplicationContext(HalConfig.class); LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class); assertThat(discoverers, is(notNullValue())); @@ -97,10 +101,7 @@ public class EnableHypermediaSupportIntegrationTest { @SuppressWarnings("unchecked") public void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); - - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); - postProcessor.setBeanFactory(context.getAutowireCapableBeanFactory()); + ConfigurableApplicationContext context = createApplicationContext(HalConfig.class); RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); @@ -133,12 +134,11 @@ public class EnableHypermediaSupportIntegrationTest { @Test public void registersHttpMessageConvertersForRestTemplate() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + ConfigurableApplicationContext context = createApplicationContext(HalConfig.class); RestTemplate template = context.getBean(RestTemplate.class); assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes(), hasItems(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8)); - context.close(); } /** @@ -147,42 +147,32 @@ public class EnableHypermediaSupportIntegrationTest { @Test public void configuresDefaultObjectMapperForHalToIgnoreUnknownProperties() { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + ObjectMapper mapper = getObjectMapperFor(MediaTypes.HAL_JSON, createApplicationContext(HalConfig.class)); assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES), is(false)); - context.close(); } @Test public void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); - - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + ObjectMapper mapper = getObjectMapperFor(MediaTypes.HAL_JSON, createApplicationContext(HalConfig.class)); ResourceSupport resourceSupport = new ResourceSupport(); resourceSupport.add(new Link("localhost").withSelfRel()); assertThat(mapper.writeValueAsString(resourceSupport), is("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}")); - - context.close(); } @Test public void verifyRenderSingleLinkAsArrayViaOverridingBean() throws JsonProcessingException { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext( - RenderLinkAsSingleLinksConfig.class); - - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + ObjectMapper mapper = getObjectMapperFor(MediaTypes.HAL_JSON, + createApplicationContext(RenderLinkAsSingleLinksConfig.class)); ResourceSupport resourceSupport = new ResourceSupport(); resourceSupport.add(new Link("localhost").withSelfRel()); assertThat(mapper.writeValueAsString(resourceSupport), is("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}")); - - context.close(); } private static void assertEntityLinksSetUp(ApplicationContext context) { @@ -200,10 +190,10 @@ public class EnableHypermediaSupportIntegrationTest { @SuppressWarnings({ "unchecked" }) private static void assertHalSetupForConfigClass(Class configClass) { - ApplicationContext context = new AnnotationConfigApplicationContext(configClass); + ConfigurableApplicationContext context = createApplicationContext(HalConfig.class); + assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class), is(instanceOf(HalLinkDiscoverer.class))); - assertThat(context.getBean(ObjectMapper.class), is(notNullValue())); RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); assertThat(rmha.getMessageConverters(), @@ -233,20 +223,34 @@ public class EnableHypermediaSupportIntegrationTest { throw new IllegalStateException("Unexpected result when looking up argument resolvers!"); } + private static ConfigurableApplicationContext createApplicationContext(Class... configurations) { + + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.setServletContext(new MockServletContext()); + context.register(configurations); + context.refresh(); + + return context; + } + + private static ObjectMapper getObjectMapperFor(MediaType mediaType, ApplicationContext context) { + + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + + for (HttpMessageConverter converter : adapter.getMessageConverters()) { + if (converter.getSupportedMediaTypes().contains(mediaType)) { + return ((AbstractJackson2HttpMessageConverter) converter).getObjectMapper(); + } + } + + throw new IllegalArgumentException("Did not find HttpMessageConverter supporting " + mediaType + "!"); + } + + @EnableWebMvc @Configuration @Import(DelegateConfig.class) static class HalConfig { - static int numberOfMessageConverters = 0; - static int numberOfMessageConvertersLegacy = 0; - - @Bean - public RequestMappingHandlerAdapter rmh() { - RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); - numberOfMessageConverters = adapter.getMessageConverters().size(); - return adapter; - } - @Bean public RestTemplate restTemplate() { return new RestTemplate(); @@ -264,6 +268,7 @@ public class EnableHypermediaSupportIntegrationTest { } + @EnableWebMvc @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) static class RenderLinkAsSingleLinksConfig { @@ -272,10 +277,5 @@ public class EnableHypermediaSupportIntegrationTest { HalConfiguration halConfiguration() { return new HalConfiguration().withRenderSingleLinks(AS_ARRAY); } - - @Bean - public RequestMappingHandlerAdapter rmh() { - return new RequestMappingHandlerAdapter(); - } } }