From 80d13ed80e283bb06ce30873c2acbdef9c9a6ae7 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Wed, 20 Jun 2018 17:20:01 +0200 Subject: [PATCH] #723 - Simplified configuration setup. We now avoid to register ObjectMapper instances as Spring beans and rather use one already existing in the ApplicationContext and copying the setup before registering the individual HttpMessageConverters for the individual media types. Replaced a lot of the programmatic component setup via BeanDefinitions with their corresponding JavaConfig alternatives. Removed obsolete media type specific HttpMessageConverters and configuration classes registering them. Backport of fix for #719. --- ...ConverterRegisteringBeanPostProcessor.java | 64 +++++ .../ConverterRegisteringWebMvcConfigurer.java | 139 ++++++++++ .../hateoas/config/EnableEntityLinks.java | 8 +- .../config/EnableHypermediaSupport.java | 9 +- .../config/EntityLinksConfiguration.java | 70 +++++ .../hateoas/config/HateoasConfiguration.java | 62 ++++- ...ermediaSupportBeanDefinitionRegistrar.java | 262 +----------------- .../JaxRsConfigurationImportSelector.java | 66 +++++ .../EnableEntityLinksIntegrationTest.java | 10 +- ...nableHypermediaSupportIntegrationTest.java | 80 +++--- 10 files changed, 465 insertions(+), 305 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/config/ConverterRegisteringBeanPostProcessor.java create mode 100644 src/main/java/org/springframework/hateoas/config/ConverterRegisteringWebMvcConfigurer.java create mode 100644 src/main/java/org/springframework/hateoas/config/EntityLinksConfiguration.java create mode 100644 src/main/java/org/springframework/hateoas/config/JaxRsConfigurationImportSelector.java 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(); - } } }