From fd97c7553c8caef286ada0de8ac7490bec7286e8 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Mon, 22 Dec 2014 15:12:51 -0800 Subject: [PATCH] Apply HATEOAS module to primary ObjectMapper Update HypermediaAutoConfiguration to apply the Jackson2HalModule to the primary ObjectMapper. This restores the behavior of Spring Boot 1.1 where HATEOAS types could be serialized for both `application/json` and `application/json+hal` content types. A `spring.hateoas.apply-to-primary-object-mapper` property has also been provided to opt-out if necessary. Fixes gh-2147 --- .../hateoas/HateoasProperties.java | 43 +++++++ .../hateoas/HypermediaAutoConfiguration.java | 107 ++++++++++++++---- .../HypermediaAutoConfigurationTests.java | 6 +- .../appendix-application-properties.adoc | 3 + 4 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java new file mode 100644 index 00000000000..3b314ad3f68 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HateoasProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2014 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.boot.autoconfigure.hateoas; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@link ConfigurationProperties properties} for Spring HATEOAS. + * + * @author Phillip webb + * @since 1.2.1 + */ +@ConfigurationProperties(prefix = "spring.hateoas") +public class HateoasProperties { + + /** + * If HATEOAS support should be applied to the primary ObjectMapper. + */ + private boolean applyToPrimaryObjectMapper = true; + + public boolean isApplyToPrimaryObjectMapper() { + return this.applyToPrimaryObjectMapper; + } + + public void setApplyToPrimaryObjectMapper(boolean applyToPrimaryObjectMapper) { + this.applyToPrimaryObjectMapper = applyToPrimaryObjectMapper; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java index faf050c6f94..b3c1fff2e47 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java @@ -16,8 +16,14 @@ package org.springframework.boot.autoconfigure.hateoas; +import javax.annotation.PostConstruct; + 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.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; @@ -27,14 +33,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverers; +import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.Resource; import org.springframework.hateoas.config.EnableEntityLinks; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.hateoas.hal.Jackson2HalModule; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.plugin.core.Plugin; import org.springframework.web.bind.annotation.RequestMapping; @@ -55,6 +65,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @ConditionalOnWebApplication @AutoConfigureAfter({ WebMvcAutoConfiguration.class, JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class }) +@EnableConfigurationProperties(HateoasProperties.class) public class HypermediaAutoConfiguration { @Configuration @@ -65,40 +76,90 @@ public class HypermediaAutoConfiguration { @ConditionalOnClass({ Jackson2ObjectMapperBuilder.class, ObjectMapper.class }) protected static class HalObjectMapperConfiguration { + @Autowired + private HateoasProperties hateoasProperties; + @Autowired(required = false) - private Jackson2ObjectMapperBuilder objectMapperBuilder; + private CurieProvider curieProvider; + + @Autowired + @Qualifier("_relProvider") + private RelProvider relProvider; + + @Autowired(required = false) + private ObjectMapper primaryObjectMapper; + + @PostConstruct + public void configurePrimaryObjectMapper() { + if (this.primaryObjectMapper != null + && this.hateoasProperties.isApplyToPrimaryObjectMapper()) { + registerHalModule(this.primaryObjectMapper); + } + } + + private void registerHalModule(ObjectMapper objectMapper) { + objectMapper.registerModule(new Jackson2HalModule()); + Jackson2HalModule.HalHandlerInstantiator instantiator = new Jackson2HalModule.HalHandlerInstantiator( + HalObjectMapperConfiguration.this.relProvider, + HalObjectMapperConfiguration.this.curieProvider); + objectMapper.setHandlerInstantiator(instantiator); + } @Bean - public BeanPostProcessor halObjectMapperConfigurer() { - return new BeanPostProcessor() { - - @Override - public Object postProcessAfterInitialization(Object bean, - String beanName) throws BeansException { - if (HalObjectMapperConfiguration.this.objectMapperBuilder != null - && bean instanceof ObjectMapper - && "_halObjectMapper".equals(beanName)) { - HalObjectMapperConfiguration.this.objectMapperBuilder - .configure((ObjectMapper) bean); - } - return bean; - } - - @Override - public Object postProcessBeforeInitialization(Object bean, - String beanName) throws BeansException { - return bean; - } - - }; + public static HalObjectMapperConfigurer halObjectMapperConfigurer() { + return new HalObjectMapperConfigurer(); } + } + } @Configuration @ConditionalOnMissingBean(EntityLinks.class) @EnableEntityLinks protected static class EntityLinksConfiguration { + } + /** + * {@link BeanPostProcessor} to apply any {@link Jackson2ObjectMapperBuilder} + * configuration to the HAL {@link ObjectMapper}. + */ + private static class HalObjectMapperConfigurer implements BeanPostProcessor, + BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) + throws BeansException { + if (bean instanceof ObjectMapper && "_halObjectMapper".equals(beanName)) { + postProcessHalObjectMapper((ObjectMapper) bean); + } + return bean; + } + + private void postProcessHalObjectMapper(ObjectMapper objectMapper) { + try { + Jackson2ObjectMapperBuilder builder = this.beanFactory + .getBean(Jackson2ObjectMapperBuilder.class); + builder.configure(objectMapper); + } + catch (NoSuchBeanDefinitionException ex) { + // No Jackson configuration required + } + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) + throws BeansException { + return bean; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + } } diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java index 961449fcc2f..8c5655c9efd 100644 --- a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfigurationTests.java @@ -75,19 +75,17 @@ public class HypermediaAutoConfigurationTests { @Test public void doesBackOffIfEnableHypermediaSupportIsDeclaredManually() { - this.context = new AnnotationConfigWebApplicationContext(); this.context.register(SampleConfig.class, HypermediaAutoConfiguration.class); this.context.refresh(); - this.context.getBean(LinkDiscoverers.class); } @Test public void jacksonConfigurationIsAppliedToTheHalObjectMapper() { this.context = new AnnotationConfigWebApplicationContext(); - this.context.register(HypermediaAutoConfiguration.class, - JacksonAutoConfiguration.class); + this.context.register(JacksonAutoConfiguration.class, + HypermediaAutoConfiguration.class); EnvironmentTestUtils.addEnvironment(this.context, "spring.jackson.serialization.INDENT_OUTPUT:true"); this.context.refresh(); diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index e56fcfb84d1..1a1c16597cc 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -96,6 +96,9 @@ content into your application; rather pick only the properties that you need. spring.resources.cache-period= # cache timeouts in headers sent to browser spring.resources.add-mappings=true # if default mappings should be added + # SPRING HATEOS ({sc-spring-boot-autoconfigure}/hateoas/HateoasProperties.{sc-ext}[HateoasProperties]) + spring.hateoas.apply-to-primary-object-mapper=true # if the primary mapper should also be configured + # HTTP encoding ({sc-spring-boot-autoconfigure}/web/HttpEncodingProperties.{sc-ext}[HttpEncodingProperties]) spring.http.encoding.charset=UTF-8 # the encoding of HTTP requests/responses spring.http.encoding.enabled=true # enable http encoding support