From 8cfbfc1b890b00331524705346490e207f71a64d Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Tue, 19 Jun 2018 20:56:41 +0200 Subject: [PATCH] #719 - 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. --- .../CollectionJsonMessageConverter.java | 83 ----- .../CollectionJsonWebMvcConfigurer.java | 42 --- ...ConverterRegisteringBeanPostProcessor.java | 55 +++ .../ConverterRegisteringWebMvcConfigurer.java | 198 +++++++++++ .../hateoas/config/EnableEntityLinks.java | 5 +- .../config/EnableHypermediaSupport.java | 59 +--- .../config/EntityLinksConfiguration.java | 70 ++++ .../hateoas/config/HateoasConfiguration.java | 62 +++- ...ermediaSupportBeanDefinitionRegistrar.java | 328 +----------------- .../LinkBuilderBeanDefinitionRegistrar.java | 85 ----- .../hal/forms/HalFormsMessageConverter.java | 91 ----- .../hal/forms/HalFormsWebMvcConfigurer.java | 43 --- ...nableHypermediaSupportIntegrationTest.java | 206 ++++++----- .../HalFormsMessageConverterUnitTest.java | 17 +- .../HalFormsValidationIntegrationTest.java | 22 +- 15 files changed, 528 insertions(+), 838 deletions(-) delete mode 100644 src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonMessageConverter.java delete mode 100644 src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonWebMvcConfigurer.java 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 delete mode 100644 src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java delete mode 100644 src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java delete mode 100644 src/main/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcConfigurer.java diff --git a/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonMessageConverter.java b/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonMessageConverter.java deleted file mode 100644 index ae73b37a..00000000 --- a/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonMessageConverter.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2017 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.collectionjson; - -import java.io.IOException; -import java.util.Arrays; - -import org.springframework.hateoas.MediaTypes; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * A message converter that converts any object into a Collection+JSON document before bundling up as an - * {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into an object. - * - * @author Greg Turnquist - */ -public class CollectionJsonMessageConverter extends AbstractHttpMessageConverter { - - private final ObjectMapper objectMapper; - - public CollectionJsonMessageConverter(ObjectMapper objectMapper) { - - this.objectMapper = objectMapper; - this.objectMapper.registerModule(new Jackson2CollectionJsonModule()); - - setSupportedMediaTypes(Arrays.asList(MediaTypes.COLLECTION_JSON)); - } - - @Override - protected boolean supports(Class clazz) { - return true; - } - - @Override - protected Object readInternal(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - - return this.objectMapper.readValue(inputMessage.getBody(), clazz); - } - - @Override - protected void writeInternal(Object t, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8); - - // A workaround for JsonGenerators not applying serialization features - // https://github.com/FasterXML/jackson-databind/issues/12 - if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { - jsonGenerator.useDefaultPrettyPrinter(); - } - - try { - objectMapper.writeValue(jsonGenerator, t); - } catch (JsonProcessingException ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - - } -} diff --git a/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonWebMvcConfigurer.java b/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonWebMvcConfigurer.java deleted file mode 100644 index c54ba5ad..00000000 --- a/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonWebMvcConfigurer.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017 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.collectionjson; - -import java.util.List; - -import org.springframework.context.annotation.Configuration; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * @author Greg Turnquist - */ -@Configuration -public class CollectionJsonWebMvcConfigurer implements WebMvcConfigurer { - - /* - * (non-Javadoc) - * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#configureMessageConverters(java.util.List) - */ - @Override - public void configureMessageConverters(List> converters) { - converters.add(new CollectionJsonMessageConverter(new ObjectMapper())); - } - - -} 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..f8e4aa28 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringBeanPostProcessor.java @@ -0,0 +1,55 @@ +/* + * 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; + } +} 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..22c290d9 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/ConverterRegisteringWebMvcConfigurer.java @@ -0,0 +1,198 @@ +/* + * 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.collectionjson.Jackson2CollectionJsonModule; +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.hal.forms.HalFormsConfiguration; +import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule; +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.WebMvcConfigurer; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Oliver Gierke + */ +@Configuration +@RequiredArgsConstructor +public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, 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 final ObjectProvider halFormsConfiguration; + + 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(() -> new 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)); + } + + if (hypermediaTypes.contains(HypermediaType.HAL_FORMS)) { + converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource)); + } + + if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) { + converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource)); + } + } + + /** + * @param objectMapper + * @param linkRelationMessageSource + * @return + */ + protected MappingJackson2HttpMessageConverter createCollectionJsonConverter(ObjectMapper objectMapper, + MessageSourceAccessor linkRelationMessageSource) { + ObjectMapper collectionJsonObjectMapper = objectMapper.copy(); + + collectionJsonObjectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + collectionJsonObjectMapper.registerModule(new Jackson2CollectionJsonModule()); + collectionJsonObjectMapper.setHandlerInstantiator( + new Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator(linkRelationMessageSource)); + + MappingJackson2HttpMessageConverter collectionJsonConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + collectionJsonConverter.setSupportedMediaTypes(Arrays.asList(COLLECTION_JSON)); + collectionJsonConverter.setObjectMapper(collectionJsonObjectMapper); + return collectionJsonConverter; + } + + /** + * @param objectMapper + * @param curieProvider + * @param relProvider + * @param linkRelationMessageSource + * @return + */ + private MappingJackson2HttpMessageConverter createHalFormsConverter(ObjectMapper objectMapper, + CurieProvider curieProvider, RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) { + + Jackson2HalFormsModule.HalFormsHandlerInstantiator hi = new Jackson2HalFormsModule.HalFormsHandlerInstantiator( + relProvider, curieProvider, linkRelationMessageSource, true, + this.halFormsConfiguration.getIfAvailable(() -> new HalFormsConfiguration())); + ObjectMapper mapper = objectMapper.copy(); + + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.registerModule(new Jackson2HalFormsModule()); + mapper.setHandlerInstantiator(hi); + + MappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + converter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON)); + converter.setObjectMapper(mapper); + + return converter; + } + + /** + * @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(() -> new 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..2f8ee029 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,6 @@ import org.springframework.hateoas.core.DelegatingEntityLinks; @Target(ElementType.TYPE) @Inherited @Documented -@Import(LinkBuilderBeanDefinitionRegistrar.class) +@Import(EntityLinksConfiguration.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 976525f5..1fc5e5a6 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. @@ -15,26 +15,16 @@ */ package org.springframework.hateoas.config; -import lombok.extern.slf4j.Slf4j; - import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportSelector; -import org.springframework.core.type.AnnotationMetadata; import org.springframework.hateoas.EntityLinks; import org.springframework.hateoas.LinkDiscoverer; -import org.springframework.hateoas.collectionjson.CollectionJsonWebMvcConfigurer; -import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer; /** * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for @@ -54,8 +44,8 @@ import org.springframework.hateoas.hal.forms.HalFormsWebMvcConfigurer; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented -@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class, - EnableHypermediaSupport.HypermediaConfigurationImportSelector.class }) +@EnableEntityLinks +@Import({ HypermediaSupportBeanDefinitionRegistrar.class, HateoasConfiguration.class }) public @interface EnableHypermediaSupport { /** @@ -86,52 +76,13 @@ public @interface EnableHypermediaSupport { * * @see https://rwcbook.github.io/hal-forms/ */ - HAL_FORMS(HalFormsWebMvcConfigurer.class), + HAL_FORMS, /** * Collection+JSON * * @see http://amundsen.com/media-types/collection/format/ */ - COLLECTION_JSON(CollectionJsonWebMvcConfigurer.class); - - - private final List> configurations; - - HypermediaType(Class... configurations) { - this.configurations = Arrays.asList(configurations); - } - } - - @Slf4j - class HypermediaConfigurationImportSelector implements ImportSelector { - - @Override - public String[] selectImports(AnnotationMetadata metadata) { - - Map attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName()); - - HypermediaType[] types = (HypermediaType[]) attributes.get("type"); - - /** - * If no types are defined inside the annotation, add them all. - */ - if (types.length == 0) { - types = HypermediaType.values(); - } - - LOG.debug("Registering support for hypermedia types {} according to configuration on {}", types, - metadata.getClassName()); - - List configurationNames = new ArrayList(); - - for (HypermediaType type : types) { - for (Class configuration : type.configurations) { - configurationNames.add(configuration.getName()); - } - } - - return configurationNames.toArray(new String[0]); - } + COLLECTION_JSON; } } 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..3e61f525 --- /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..9b312289 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 discoverers) { + return new LinkDiscoverers(discoverers); + } } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index e9616266..bfbd6b73 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,64 +17,27 @@ 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.collectionjson.CollectionJsonLinkDiscoverer; -import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule; 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.hal.forms.HalFormsConfiguration; import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer; -import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule; -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 @@ -84,23 +47,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; * @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 HAL_FORMS_OBJECT_MAPPER_BEAN_NAME = "_halFormsObjectMapper"; - private static final String COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME = "_collectionJsonObjectMapper"; - 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) @@ -109,14 +58,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, @@ -124,84 +71,9 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe } } - if (types.contains(HypermediaType.HAL)) { - registerHypermediaComponents(metadata, registry, HAL_OBJECT_MAPPER_BEAN_NAME); - } - - if (types.contains(HypermediaType.HAL_FORMS)) { - registerHypermediaComponents(metadata, registry, HAL_FORMS_OBJECT_MAPPER_BEAN_NAME); - } - - if (types.contains(HypermediaType.COLLECTION_JSON)) { - registerHypermediaComponents(metadata, registry, COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME); - } - - 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); - } - - private static void registerHypermediaComponents(AnnotationMetadata metadata, BeanDefinitionRegistry registry, - String objectMapperBeanName) { - - if (JACKSON2_PRESENT) { - - BeanDefinitionBuilder queryMapperBuilder = rootBeanDefinition(ObjectMapper.class); - registerSourcedBeanDefinition(queryMapperBuilder, metadata, registry, objectMapperBeanName); - - BeanDefinitionBuilder customizerBeanDefinition = rootBeanDefinition(DefaultObjectMapperCustomizer.class); - registerSourcedBeanDefinition(customizerBeanDefinition, metadata, registry); - - BeanDefinitionBuilder builder = rootBeanDefinition(Jackson2ModuleRegisteringBeanPostProcessor.class); - registerSourcedBeanDefinition(builder, metadata, 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); } /** @@ -250,190 +122,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); - - List> result = new ArrayList>(converters.size()); - - if (beanFactory.containsBean(HAL_OBJECT_MAPPER_BEAN_NAME)) { - - 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()); - - try { - HalConfiguration halConfiguration = beanFactory.getBean(HalConfiguration.class); - halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, - linkRelationMessageSource, halConfiguration)); - } catch (BeansException e) { - halObjectMapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, - linkRelationMessageSource, new HalConfiguration())); - } - - MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class); - halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON, HAL_JSON_UTF8)); - halConverter.setObjectMapper(halObjectMapper); - result.add(halConverter); - } - - if (beanFactory.containsBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME)) { - - ObjectMapper halFormsObjectMapper = beanFactory.getBean(HAL_FORMS_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class); - MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); - - halFormsObjectMapper.registerModule(new Jackson2HalFormsModule()); - - try { - HalFormsConfiguration halFormsConfiguration = beanFactory.getBean(HalFormsConfiguration.class); - halFormsObjectMapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider, - linkRelationMessageSource, true, halFormsConfiguration)); - } catch (BeansException e) { - halFormsObjectMapper.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider, - linkRelationMessageSource, true, new HalFormsConfiguration())); - } - - MappingJackson2HttpMessageConverter halFormsConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class); - halFormsConverter.setSupportedMediaTypes(Arrays.asList(HAL_FORMS_JSON)); - halFormsConverter.setObjectMapper(halFormsObjectMapper); - result.add(halFormsConverter); - } - - if (beanFactory.containsBean(COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME)) { - - ObjectMapper collectionJsonObjectMapper = beanFactory.getBean(COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class); - MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); - - collectionJsonObjectMapper.registerModule(new Jackson2CollectionJsonModule()); - - collectionJsonObjectMapper.setHandlerInstantiator( - new Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator(linkRelationMessageSource)); - - MappingJackson2HttpMessageConverter jsonCollectionConverter = new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class); - jsonCollectionConverter.setSupportedMediaTypes(Arrays.asList(COLLECTION_JSON)); - jsonCollectionConverter.setObjectMapper(collectionJsonObjectMapper); - result.add(jsonCollectionConverter); - } - - 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) || HAL_FORMS_OBJECT_MAPPER_BEAN_NAME.equals(beanName) || COLLECTION_JSON_OBJECT_MAPPER_BEAN_NAME.equals(beanName)) { - ObjectMapper mapper = (ObjectMapper) bean; - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - return mapper; - } - - return bean; - } - - /* - * (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/LinkBuilderBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java deleted file mode 100644 index 5a9c02fa..00000000 --- a/src/main/java/org/springframework/hateoas/config/LinkBuilderBeanDefinitionRegistrar.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2012-2013 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 java.lang.annotation.Annotation; - -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; -import org.springframework.core.type.AnnotationMetadata; -import org.springframework.hateoas.EntityLinks; -import org.springframework.hateoas.LinkBuilderFactory; -import org.springframework.hateoas.core.ControllerEntityLinksFactoryBean; -import org.springframework.hateoas.core.DelegatingEntityLinks; -import org.springframework.hateoas.mvc.ControllerLinkBuilderFactory; -import org.springframework.plugin.core.support.PluginRegistryFactoryBean; -import org.springframework.stereotype.Controller; -import org.springframework.util.ClassUtils; - -/** - * {@link ImportBeanDefinitionRegistrar} to register a {@link DelegatingEntityLinks} instance as well as a - * {@link ControllerEntityLinksFactoryBean} for Spring MVC controllers and JAX-RS resources if present. - * - * @author Oliver Gierke - */ -class LinkBuilderBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { - - private static final boolean IS_JAX_RS_PRESENT = ClassUtils.isPresent("javax.ws.rs.Path", - ClassUtils.getDefaultClassLoader()); - - /* - * (non-Javadoc) - * @see org.springframework.context.annotation.ImportBeanDefinitionRegistrar#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry) - */ - @Override - public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { - - BeanDefinitionBuilder registryFactoryBeanBuilder = BeanDefinitionBuilder - .rootBeanDefinition(PluginRegistryFactoryBean.class); - registryFactoryBeanBuilder.addPropertyValue("type", EntityLinks.class); - registryFactoryBeanBuilder.addPropertyValue("exclusions", DelegatingEntityLinks.class); - - AbstractBeanDefinition registryBeanDefinition = registryFactoryBeanBuilder.getBeanDefinition(); - registry.registerBeanDefinition("entityLinksPluginRegistry", registryBeanDefinition); - - BeanDefinitionBuilder delegateBuilder = BeanDefinitionBuilder.rootBeanDefinition(DelegatingEntityLinks.class); - delegateBuilder.addConstructorArgValue(registryBeanDefinition); - - BeanDefinitionBuilder builder = getEntityControllerLinksFor(Controller.class, ControllerLinkBuilderFactory.class); - registry.registerBeanDefinition("controllerEntityLinks", builder.getBeanDefinition()); - delegateBuilder.addDependsOn("controllerEntityLinks"); - - AbstractBeanDefinition beanDefinition = delegateBuilder.getBeanDefinition(); - beanDefinition.setPrimary(true); - registry.registerBeanDefinition("delegatingEntityLinks", beanDefinition); - } - - private static BeanDefinitionBuilder getEntityControllerLinksFor(Class type, - Class> linkBuilderFactoryType) { - - RootBeanDefinition definition = new RootBeanDefinition(linkBuilderFactoryType); - definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); - - BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ControllerEntityLinksFactoryBean.class); - builder.addPropertyValue("annotation", type); - builder.addPropertyValue("linkBuilderFactory", definition); - - return builder; - } -} diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java deleted file mode 100644 index f0c03a60..00000000 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverter.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright 2016-2017 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.hal.forms; - -import java.io.IOException; -import java.util.Arrays; - -import org.springframework.hateoas.MediaTypes; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; - -import com.fasterxml.jackson.core.JsonEncoding; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; - -/** - * A message converter that converts any object into a HAL-FORMS document before bundling up as an - * {@link HttpOutputMessage}, or that converts any incoming {@link HttpInputMessage} into an object. - * - * @author Dietrich Schulten - * @author Greg Turnquist - */ -class HalFormsMessageConverter extends AbstractHttpMessageConverter { - - private final ObjectMapper objectMapper; - - public HalFormsMessageConverter(ObjectMapper objectMapper) { - - this.objectMapper = objectMapper; - this.objectMapper.registerModule(new Jackson2HalFormsModule()); - - setSupportedMediaTypes(Arrays.asList(MediaTypes.HAL_FORMS_JSON)); - } - - /* - * (non-Javadoc) - * @see org.springframework.http.converter.AbstractHttpMessageConverter#supports(java.lang.Class) - */ - @Override - protected boolean supports(Class clazz) { - return true; - } - - /* - * (non-Javadoc) - * @see org.springframework.http.converter.AbstractHttpMessageConverter#readInternal(java.lang.Class, org.springframework.http.HttpInputMessage) - */ - @Override - protected Object readInternal(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - - return this.objectMapper.readValue(inputMessage.getBody(), clazz); - } - - @Override - protected void writeInternal(Object t, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - JsonGenerator jsonGenerator = objectMapper.getFactory().createGenerator(outputMessage.getBody(), JsonEncoding.UTF8); - - // A workaround for JsonGenerators not applying serialization features - // https://github.com/FasterXML/jackson-databind/issues/12 - if (objectMapper.isEnabled(SerializationFeature.INDENT_OUTPUT)) { - jsonGenerator.useDefaultPrettyPrinter(); - } - - try { - objectMapper.writeValue(jsonGenerator, t); - } catch (JsonProcessingException ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - } -} diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcConfigurer.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcConfigurer.java deleted file mode 100644 index 699fdf47..00000000 --- a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsWebMvcConfigurer.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2016-2017 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.hal.forms; - -import java.util.List; - -import org.springframework.context.annotation.Configuration; -import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Configure a HAL-FORMS {@link HttpMessageConverter}. - * - * @author Oliver Gierke - * @author Greg Turnquist - */ -@Configuration -public class HalFormsWebMvcConfigurer implements WebMvcConfigurer { - - /* - * (non-Javadoc) - * @see org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter#configureMessageConverters(java.util.List) - */ - @Override - public void configureMessageConverters(List> converters) { - converters.add(new HalFormsMessageConverter(new ObjectMapper())); - } -} diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index c683b312..908984f8 100755 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -20,12 +20,13 @@ import static org.springframework.hateoas.hal.HalConfiguration.RenderSingleLinks import java.lang.reflect.Method; import java.util.List; +import java.util.Optional; +import java.util.function.Function; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.springframework.context.ApplicationContext; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -38,7 +39,6 @@ import org.springframework.hateoas.RelProvider; import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer; 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; @@ -46,13 +46,19 @@ import org.springframework.hateoas.hal.HalLinkDiscoverer; import org.springframework.hateoas.hal.forms.HalFormsConfiguration; import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer; 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.test.web.servlet.MockMvc; 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; @@ -69,6 +75,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; @RunWith(MockitoJUnitRunner.class) public class EnableHypermediaSupportIntegrationTest { + MockMvc mockMvc; + @Test public void bootstrapHalConfiguration() { assertHalSetupForConfigClass(HalConfig.class); @@ -105,7 +113,8 @@ public class EnableHypermediaSupportIntegrationTest { LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class); assertThat(discoverers).isNotNull(); - assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_FORMS_JSON)).isInstanceOf(HalFormsLinkDiscoverer.class); + assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_FORMS_JSON)) + .isInstanceOf(HalFormsLinkDiscoverer.class); assertRelProvidersSetUp(context); }); } @@ -118,7 +127,8 @@ public class EnableHypermediaSupportIntegrationTest { LinkDiscoverers discoverers = context.getBean(LinkDiscoverers.class); assertThat(discoverers).isNotNull(); - assertThat(discoverers.getLinkDiscovererFor(MediaTypes.COLLECTION_JSON)).isInstanceOf(CollectionJsonLinkDiscoverer.class); + assertThat(discoverers.getLinkDiscovererFor(MediaTypes.COLLECTION_JSON)) + .isInstanceOf(CollectionJsonLinkDiscoverer.class); assertRelProvidersSetUp(context); }); } @@ -147,9 +157,6 @@ public class EnableHypermediaSupportIntegrationTest { withContext(HalConfig.class, context -> { - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); - postProcessor.setBeanFactory(context.getAutowireCapableBeanFactory()); - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()) // @@ -182,14 +189,10 @@ public class EnableHypermediaSupportIntegrationTest { withContext(HalFormsConfig.class, context -> { - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); - postProcessor.setBeanFactory(context.getAutowireCapableBeanFactory()); - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.HAL_FORMS_JSON); + assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) + .contains(MediaTypes.HAL_FORMS_JSON); boolean found = false; @@ -201,12 +204,10 @@ public class EnableHypermediaSupportIntegrationTest { AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); + .getField(processor, "messageConverters"); assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class); - assertThat(converters.get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.HAL_FORMS_JSON); + assertThat(converters.get(0).getSupportedMediaTypes()).hasSize(1).contains(MediaTypes.HAL_FORMS_JSON); } } @@ -220,14 +221,10 @@ public class EnableHypermediaSupportIntegrationTest { withContext(CollectionJsonConfig.class, context -> { - Jackson2ModuleRegisteringBeanPostProcessor postProcessor = new HypermediaSupportBeanDefinitionRegistrar.Jackson2ModuleRegisteringBeanPostProcessor(); - postProcessor.setBeanFactory(context.getAutowireCapableBeanFactory()); - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.COLLECTION_JSON); + assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) + .contains(MediaTypes.COLLECTION_JSON); boolean found = false; @@ -239,12 +236,10 @@ public class EnableHypermediaSupportIntegrationTest { AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); + .getField(processor, "messageConverters"); assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class); - assertThat(converters.get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.COLLECTION_JSON); + assertThat(converters.get(0).getSupportedMediaTypes()).hasSize(1).contains(MediaTypes.COLLECTION_JSON); } } @@ -271,23 +266,60 @@ public class EnableHypermediaSupportIntegrationTest { public void registersHalFormsHttpMessageConvertersForRestTemplate() { withContext(HalFormsConfig.class, context -> { - RestTemplate template = context.getBean(RestTemplate.class); - assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.HAL_FORMS_JSON); + foo(context, RestTemplate.class, it -> it.getMessageConverters().get(0), converter -> { + + assertThat(converter.getSupportedMediaTypes()) // + .hasSize(1).contains(MediaTypes.HAL_FORMS_JSON); + }); }); } + private static void foo(ApplicationContext context, Class beanType, Function extractor, + ThrowingConsumer consumer) { + + T bean = context.getBean(beanType); + S result = extractor.apply(bean); + + try { + consumer.accept(result); + } catch (Throwable o_O) { + throw new RuntimeException(o_O); + } + } + + private static void assertObjectMapper(ApplicationContext context, MediaType mediaType, + ThrowingConsumer consumer) { + + Function mapper = adapter -> { + + Optional result = adapter.getMessageConverters().stream()// + .filter(it -> it.getSupportedMediaTypes().contains(mediaType)).findFirst() // + .map(AbstractJackson2HttpMessageConverter.class::cast) // + .map(it -> it.getObjectMapper()); + + if (!result.isPresent()) { + fail("Couldn't find ObjectMapper from HttpMessageConverter supporting " + mediaType); + } + + return result.orElseThrow(IllegalStateException::new); + }; + + foo(context, RequestMappingHandlerAdapter.class, mapper, consumer); + } + + interface ThrowingConsumer { + void accept(T source) throws Throwable; + } + @Test public void registersCollectionJsonHttpMessageConvertersForRestTemplate() { withContext(CollectionJsonConfig.class, context -> { RestTemplate template = context.getBean(RestTemplate.class); - assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()) - .hasSize(1) - .contains(MediaTypes.COLLECTION_JSON); + assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) + .contains(MediaTypes.COLLECTION_JSON); }); } @@ -299,9 +331,9 @@ public class EnableHypermediaSupportIntegrationTest { withContext(HalConfig.class, context -> { - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); - - assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + assertObjectMapper(context, MediaTypes.HAL_JSON, mapper -> { + assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + }); }); } @@ -313,9 +345,9 @@ public class EnableHypermediaSupportIntegrationTest { withContext(HalFormsConfig.class, context -> { - ObjectMapper mapper = context.getBean("_halFormsObjectMapper", ObjectMapper.class); - - assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + assertObjectMapper(context, MediaTypes.HAL_FORMS_JSON, mapper -> { + assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + }); }); } @@ -324,26 +356,36 @@ public class EnableHypermediaSupportIntegrationTest { withContext(CollectionJsonConfig.class, context -> { - ObjectMapper mapper = context.getBean("_collectionJsonObjectMapper", ObjectMapper.class); - - assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + assertObjectMapper(context, MediaTypes.COLLECTION_JSON, mapper -> { + assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse(); + }); }); } @Test public void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException { - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(HalConfig.class); + withContext(HalConfig.class, context -> { - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - ResourceSupport resourceSupport = new ResourceSupport(); - resourceSupport.add(new Link("localhost").withSelfRel()); + Optional mapper = adapter.getMessageConverters().stream() // + .filter(it -> it.getSupportedMediaTypes().contains(MediaType.parseMediaType("application/hal+json"))) // + .findFirst() // + .map(AbstractJackson2HttpMessageConverter.class::cast) // + .map(AbstractJackson2HttpMessageConverter::getObjectMapper); - assertThat(mapper.writeValueAsString(resourceSupport)) - .isEqualTo("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"); + assertThat(mapper).hasValueSatisfying(it -> { - context.close(); + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost").withSelfRel()); + + assertThatCode(() -> { // + assertThat(it.writeValueAsString(resourceSupport)) // + .isEqualTo("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"); + }); + }); + }); } @Test @@ -351,20 +393,27 @@ public class EnableHypermediaSupportIntegrationTest { withContext(RenderLinkAsSingleLinksConfig.class, context -> { - ObjectMapper mapper = context.getBean("_halObjectMapper", ObjectMapper.class); + assertObjectMapper(context, MediaTypes.HAL_JSON, mapper -> { - ResourceSupport resourceSupport = new ResourceSupport(); - resourceSupport.add(new Link("localhost").withSelfRel()); + ResourceSupport resourceSupport = new ResourceSupport(); + resourceSupport.add(new Link("localhost").withSelfRel()); + + assertThat(mapper.writeValueAsString(resourceSupport)) + .isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}"); + }); - assertThat(mapper.writeValueAsString(resourceSupport)) - .isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}"); }); } private static void withContext(Class configuration, - ConsumerWithException consumer) throws E { + ConsumerWithException consumer) throws E { + + try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) { + + context.register(configuration); + context.setServletContext(new MockServletContext()); + context.refresh(); - try (AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(configuration)) { consumer.accept(context); } } @@ -387,7 +436,6 @@ public class EnableHypermediaSupportIntegrationTest { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(HalLinkDiscoverer.class); - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); assertThat(rmha.getMessageConverters()) @@ -401,7 +449,6 @@ public class EnableHypermediaSupportIntegrationTest { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(HalFormsLinkDiscoverer.class); - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class); @@ -409,18 +456,15 @@ public class EnableHypermediaSupportIntegrationTest { }); } - @SuppressWarnings({ "unchecked" }) private static void assertCollectionJsonSetupForConfigClass(Class configClass) { withContext(configClass, context -> { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(CollectionJsonLinkDiscoverer.class); - assertThat(context.getBean(ObjectMapper.class)).isNotNull(); RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class); - }); } @@ -448,19 +492,10 @@ public class EnableHypermediaSupportIntegrationTest { } @Configuration + @EnableWebMvc @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(); @@ -483,6 +518,7 @@ public class EnableHypermediaSupportIntegrationTest { } + @EnableWebMvc @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL) static class RenderLinkAsSingleLinksConfig { @@ -491,25 +527,11 @@ public class EnableHypermediaSupportIntegrationTest { HalConfiguration halConfiguration() { return new HalConfiguration().withRenderSingleLinks(AS_ARRAY); } - - @Bean - public RequestMappingHandlerAdapter rmh() { - return new RequestMappingHandlerAdapter(); - } } @Import(DelegateHalFormsHypermediaConfig.class) static class HalFormsConfig { - static int numberOfMessageConverters = 0; - - @Bean - public RequestMappingHandlerAdapter rmh() { - RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter(); - numberOfMessageConverters = adapter.getMessageConverters().size(); - return adapter; - } - @Bean public RestTemplate restTemplate() { return new RestTemplate(); @@ -527,6 +549,7 @@ public class EnableHypermediaSupportIntegrationTest { } + @EnableWebMvc @Configuration @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) static class DelegateHalFormsHypermediaConfig { @@ -538,20 +561,11 @@ public class EnableHypermediaSupportIntegrationTest { void accept(T element) throws E; } + @EnableWebMvc @Configuration @Import(AlternateDelegateConfig.class) static class CollectionJsonConfig { - 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(); diff --git a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java index 8c4c911e..0648af1d 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsMessageConverterUnitTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 the original author or authors. + * Copyright 2017-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. @@ -30,6 +30,8 @@ import org.junit.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.hateoas.Link; import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.ResourceSupport; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; @@ -40,6 +42,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Greg Turnquist + * @author Oliver Gierke */ public class HalFormsMessageConverterUnitTest { @@ -50,15 +53,13 @@ public class HalFormsMessageConverterUnitTest { public void setUp() { this.mapper = new ObjectMapper(); - this.messageConverter = new HalFormsMessageConverter(this.mapper); - } + this.mapper.registerModule(new Jackson2HalFormsModule()); - @Test - public void verifyBasicAttributes() { + TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter( + ResourceSupport.class); + converter.setObjectMapper(mapper); - assertThat(this.messageConverter.getSupportedMediaTypes(), hasItems(MediaTypes.HAL_FORMS_JSON)); - assertThat(this.messageConverter.canRead(HalFormsDocument.class, MediaTypes.HAL_FORMS_JSON), is(true)); - assertThat(this.messageConverter.canWrite(HalFormsDocument.class, MediaTypes.HAL_FORMS_JSON), is(true)); + this.messageConverter = converter; } @Test diff --git a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java index 4138d296..e3fbc149 100644 --- a/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/hal/forms/HalFormsValidationIntegrationTest.java @@ -57,8 +57,6 @@ import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import com.fasterxml.jackson.databind.ObjectMapper; - /** * Test that when an {@link org.springframework.hateoas.Affordance} is included that does NOT match the self link, an * exception is thrown. @@ -72,8 +70,6 @@ public class HalFormsValidationIntegrationTest { @Autowired WebApplicationContext context; - @Autowired ObjectMapper mapper; - MockMvc mockMvc; @Before @@ -148,7 +144,7 @@ public class HalFormsValidationIntegrationTest { // Return the affordance + a link back to the entire collection resource. return new Resource<>(EMPLOYEES.get(id), findOneLink.andAffordances(employeesLink.getAffordances()), - employeesLink); + employeesLink); } @PostMapping("/employees") @@ -159,8 +155,10 @@ public class HalFormsValidationIntegrationTest { EMPLOYEES.put(newEmployeeId, employee); try { - return ResponseEntity.noContent().location(new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) - .build(); + return ResponseEntity.noContent() + .location( + new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); } @@ -172,8 +170,9 @@ public class HalFormsValidationIntegrationTest { EMPLOYEES.put(id, employee); try { - return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) - .build(); + return ResponseEntity.noContent() + .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); } @@ -197,8 +196,9 @@ public class HalFormsValidationIntegrationTest { EMPLOYEES.put(id, newEmployee); try { - return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) - .build(); + return ResponseEntity.noContent() + .location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse(""))) + .build(); } catch (URISyntaxException e) { return ResponseEntity.badRequest().body(e.getMessage()); }