From 5882fcf0d6edf2e0b697059c2f9505cf701b2e80 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 25 Jul 2014 10:24:30 +0200 Subject: [PATCH] #219 - HAL setup now registers a more strict HttpMessageConverter. Previously we registered a special HttpMessageConverter with a custom ObjectMapper to deploy the HAL customizations. That converter was added to Spring MVC registered HandlerAdapters and registered as the first converter. In case Spring MVC handled a request with Accept header */* or no Accept header at all, this meant that this converter kicked in and served HAL even for objects that don't need the customizations. We now restrict the applicability of that special HttpMessageConverter to subtypes of ResourceSupport so that it will only be chosen if the object to render is of such type. --- ...ermediaSupportBeanDefinitionRegistrar.java | 5 +- ...edMappingJackson2HttpMessageConverter.java | 63 +++++++++++++++ ...nableHypermediaSupportIntegrationTest.java | 76 ++++++++++--------- ...gJackson2HttpMessageConverterUnitTest.java | 69 +++++++++++++++++ 4 files changed, 175 insertions(+), 38 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverter.java create mode 100644 src/test/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverterUnitTest.java diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 1d4ff049..6bcc960b 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -44,6 +44,7 @@ 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; @@ -52,6 +53,7 @@ import org.springframework.hateoas.core.EvoInflectorRelProvider; import org.springframework.hateoas.hal.CurieProvider; import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.hal.Jackson2HalModule; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.plugin.core.PluginRegistry; @@ -278,7 +280,8 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe halObjectMapper.registerModule(new Jackson2HalModule()); halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider)); - MappingJackson2HttpMessageConverter halConverter = new MappingJackson2HttpMessageConverter(); + MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON)); halConverter.setObjectMapper(halObjectMapper); diff --git a/src/main/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverter.java b/src/main/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverter.java new file mode 100644 index 00000000..6c837376 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverter.java @@ -0,0 +1,63 @@ +/* + * Copyright 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.hateoas.mvc; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Extension of {@link MappingJackson2HttpMessageConverter} to constrain the ability to read and write HTTP message + * based on the target type. Useful in case the {@link ObjectMapper} about to be configured has customizations that + * sholny be applied to object trees of a certain base type. + * + * @author Oliver Gierke + */ +public class TypeConstrainedMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { + + private final Class type; + + /** + * Creates a new {@link TypeConstrainedMappingJackson2HttpMessageConverter} for the given type. + * + * @param type must not be {@literal null}. + */ + public TypeConstrainedMappingJackson2HttpMessageConverter(Class type) { + + Assert.notNull(type, "Type must not be null!"); + this.type = type; + } + + /* + * (non-Javadoc) + * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#canRead(java.lang.Class, org.springframework.http.MediaType) + */ + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return type.isAssignableFrom(clazz) && super.canRead(clazz, mediaType); + } + + /* + * (non-Javadoc) + * @see org.springframework.http.converter.json.MappingJackson2HttpMessageConverter#canWrite(java.lang.Class, org.springframework.http.MediaType) + */ + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return type.isAssignableFrom(clazz) && super.canWrite(clazz, mediaType); + } +} diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index c8f05fdb..2d9cda98 100644 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -41,6 +41,7 @@ import org.springframework.hateoas.config.HypermediaSupportBeanDefinitionRegistr import org.springframework.hateoas.core.DelegatingEntityLinks; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.HalLinkDiscoverer; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.test.util.ReflectionTestUtils; @@ -77,10 +78,46 @@ public class EnableHypermediaSupportIntegrationTest { } @Test - public void foo() { + public void bootstrapsHalConfigurationForSubclass() { assertHalSetupForConfigClass(ExtendedHalConfig.class); } + /** + * @see #134, #219 + */ + @Test + @SuppressWarnings("unchecked") + public void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + + Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); + postProcessor.setBeanFactory(context); + + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + + assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes(), hasItem(MediaTypes.HAL_JSON)); + + boolean found = false; + + for (HandlerMethodArgumentResolver resolver : adapter.getArgumentResolvers().getResolvers()) { + + if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { + + found = true; + + AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; + List> converters = (List>) ReflectionTestUtils.getField( + processor, "messageConverters"); + + assertThat(converters.get(0), is(instanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class))); + assertThat(converters.get(0).getSupportedMediaTypes(), hasItem(MediaTypes.HAL_JSON)); + } + } + + assertThat(found, is(true)); + } + private static void assertEntityLinksSetUp(ApplicationContext context) { Map discoverers = context.getBeansOfType(EntityLinks.class); @@ -110,41 +147,6 @@ public class EnableHypermediaSupportIntegrationTest { Matchers.> hasItems(instanceOf(MappingJackson2HttpMessageConverter.class))); } - /** - * @see #134 - */ - @Test - @SuppressWarnings("unchecked") - public void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { - - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); - - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); - postProcessor.setBeanFactory(context); - - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes(), hasItem(MediaTypes.HAL_JSON)); - - boolean found = false; - - for (HandlerMethodArgumentResolver resolver : adapter.getArgumentResolvers().getResolvers()) { - - if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { - - found = true; - - AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; - List> converters = (List>) ReflectionTestUtils.getField( - processor, "messageConverters"); - - assertThat(converters.get(0).getSupportedMediaTypes(), hasItem(MediaTypes.HAL_JSON)); - } - } - - assertThat(found, is(true)); - } - @Configuration @Import(DelegateConfig.class) static class HalConfig { diff --git a/src/test/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverterUnitTest.java b/src/test/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverterUnitTest.java new file mode 100644 index 00000000..636fe979 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mvc/TypeConstrainedMappingJackson2HttpMessageConverterUnitTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 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.hateoas.mvc; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; + +import org.junit.Test; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; + +/** + * Unit tests for {@link TypeConstrainedMappingJackson2HttpMessageConverter}. + * + * @author Oliver Gierke + */ +public class TypeConstrainedMappingJackson2HttpMessageConverterUnitTest { + + /** + * @see #219 + */ + @Test(expected = IllegalArgumentException.class) + public void rejectsNullType() { + new TypeConstrainedMappingJackson2HttpMessageConverter(null); + } + + /** + * @see #219 + */ + @Test + public void canReadTypeIfAssignableToConfiguredType() { + + HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + + assertThat(converter.canRead(Object.class, MediaType.APPLICATION_JSON), is(false)); + assertThat(converter.canRead(ResourceSupport.class, MediaType.APPLICATION_JSON), is(true)); + assertThat(converter.canRead(Resource.class, MediaType.APPLICATION_JSON), is(true)); + } + + /** + * @see #219 + */ + @Test + public void canWriteTypeIfAssignableToConfiguredType() { + + HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + + assertThat(converter.canWrite(Object.class, MediaType.APPLICATION_JSON), is(false)); + assertThat(converter.canWrite(ResourceSupport.class, MediaType.APPLICATION_JSON), is(true)); + assertThat(converter.canWrite(Resource.class, MediaType.APPLICATION_JSON), is(true)); + } +}