From 8c366e1687d0c331ed2e10f706e35ce314f90346 Mon Sep 17 00:00:00 2001 From: Greg Turnquist Date: Tue, 5 Feb 2019 11:11:06 -0600 Subject: [PATCH] #833 - Add API to register custom media types. --- .../CollectionJsonConfigurer.java | 63 +++++++++ .../config/EnableHypermediaSupport.java | 55 +++++--- .../hateoas/config/Hypermedia.java | 55 ++++++++ .../config/HypermediaObjectMapperCreator.java | 120 ------------------ ...ermediaSupportBeanDefinitionRegistrar.java | 64 ++-------- .../mvc/HypermediaWebMvcConfigurer.java | 79 +----------- .../mvc/WebMvcHateoasConfiguration.java | 12 +- .../reactive/HypermediaWebFluxConfigurer.java | 84 ++---------- .../config/reactive/WebClientConfigurer.java | 69 ++-------- .../reactive/WebFluxHateoasConfiguration.java | 19 +-- .../hateoas/hal/HalConfigurer.java | 71 +++++++++++ .../hateoas/hal/forms/HalFormsConfigurer.java | 71 +++++++++++ .../hateoas/uber/UberConfigurer.java | 63 +++++++++ .../mvc/CustomHypermediaWebMvcTest.java | 102 +++++++++++++++ ...ediaRestTemplateBeanPostProcessorTest.java | 27 ++++ .../reactive/CustomHypermediaWebFluxTest.java | 102 +++++++++++++++ .../hateoas/support/CustomHypermediaType.java | 56 ++++++++ .../hateoas/config/mvc/frodo.json | 8 ++ .../hateoas/config/reactive/frodo.json | 8 ++ 19 files changed, 708 insertions(+), 420 deletions(-) create mode 100644 src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonConfigurer.java create mode 100644 src/main/java/org/springframework/hateoas/config/Hypermedia.java delete mode 100644 src/main/java/org/springframework/hateoas/config/HypermediaObjectMapperCreator.java create mode 100644 src/main/java/org/springframework/hateoas/hal/HalConfigurer.java create mode 100644 src/main/java/org/springframework/hateoas/hal/forms/HalFormsConfigurer.java create mode 100644 src/main/java/org/springframework/hateoas/uber/UberConfigurer.java create mode 100644 src/test/java/org/springframework/hateoas/config/mvc/CustomHypermediaWebMvcTest.java create mode 100644 src/test/java/org/springframework/hateoas/config/reactive/CustomHypermediaWebFluxTest.java create mode 100644 src/test/java/org/springframework/hateoas/support/CustomHypermediaType.java create mode 100644 src/test/resources/org/springframework/hateoas/config/mvc/frodo.json create mode 100644 src/test/resources/org/springframework/hateoas/config/reactive/frodo.json diff --git a/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonConfigurer.java b/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonConfigurer.java new file mode 100644 index 00000000..d69b50c0 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/collectionjson/CollectionJsonConfigurer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 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.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Greg Turnquist + */ +@Configuration +public class CollectionJsonConfigurer { + + @Bean + Hypermedia collectionJsonBean() { + + return new Hypermedia() { + @Override + public List getMediaTypes() { + return HypermediaType.COLLECTION_JSON.getMediaTypes(); + } + + @Override + public ObjectMapper createObjectMapper(ObjectMapper mapper) { + + ObjectMapper mapper1 = mapper.copy(); + + mapper1.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper1.registerModule(new Jackson2CollectionJsonModule()); + + return mapper1; + } + }; + } + + @Bean + LinkDiscoverer linkDiscoverer() { + return new CollectionJsonLinkDiscoverer(); + } + +} diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index 77f81571..2ad80fe6 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-2018 the original author or authors. + * Copyright 2013-2019 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. @@ -20,24 +20,23 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; -import org.springframework.hateoas.EntityLinks; -import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.MediaTypes; +import org.springframework.hateoas.collectionjson.CollectionJsonConfigurer; +import org.springframework.hateoas.hal.HalConfigurer; +import org.springframework.hateoas.hal.forms.HalFormsConfigurer; +import org.springframework.hateoas.uber.UberConfigurer; +import org.springframework.http.MediaType; /** - * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans available for - * injection to ease building hypermedia related code. Which components get registered depends on the hypermedia type - * being activated through the {@link #type()} attribute. Hypermedia-type-specific implementations of the following - * components will be registered: - * + * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans to support all + * appropriate web stacks based on selected {@link Hypermedia}-type as well as the classpath. * - * @see LinkDiscoverer - * @see EntityLinks * @author Oliver Gierke * @author Greg Turnquist */ @@ -61,7 +60,7 @@ public @interface EnableHypermediaSupport { * @author Oliver Gierke * @author Greg Turnquist */ - enum HypermediaType { + enum HypermediaType implements Hypermedia { /** * HAL - Hypermedia Application Language. @@ -69,27 +68,47 @@ public @interface EnableHypermediaSupport { * @see http://stateless.co/hal_specification.html * @see http://tools.ietf.org/html/draft-kelly-json-hal-05 */ - HAL, + HAL(HalConfigurer.class, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8), /** * HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support * * @see https://rwcbook.github.io/hal-forms/ */ - HAL_FORMS, + HAL_FORMS(HalFormsConfigurer.class, MediaTypes.HAL_FORMS_JSON), /** * Collection+JSON * * @see http://amundsen.com/media-types/collection/format/ */ - COLLECTION_JSON, + COLLECTION_JSON(CollectionJsonConfigurer.class, MediaTypes.COLLECTION_JSON), /** * UBER Hypermedia * * @see http://uberhypermedia.org/ */ - UBER; + UBER(UberConfigurer.class, MediaTypes.UBER_JSON); + + private final Class configurer; + private final List mediaTypes; + + HypermediaType(Class configurer, MediaType... mediaTypes) { + + this.configurer = configurer; + this.mediaTypes = Arrays.asList(mediaTypes); + } + + @Override + public List getMediaTypes() { + return this.mediaTypes; + } + + @Override + public Optional> configurer() { + return Optional.ofNullable(this.configurer); + } + } } diff --git a/src/main/java/org/springframework/hateoas/config/Hypermedia.java b/src/main/java/org/springframework/hateoas/config/Hypermedia.java new file mode 100644 index 00000000..1b671e54 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/Hypermedia.java @@ -0,0 +1,55 @@ +/* + * Copyright 2019 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.util.List; +import java.util.Optional; + +import org.springframework.http.MediaType; +import org.springframework.util.MimeType; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Interface for registering custom hypermedia handlers. + * + * @author Greg Turnquist + */ +public interface Hypermedia { + + /** + * {@link MediaType}s this hypermedia can handle. + */ + List getMediaTypes(); + + /** + * Convert list of {@link MediaType}s into an array of {@link MimeType}s to support various Spring constructors. + */ + default MimeType[] getMimeTypes() { + return getMediaTypes().toArray(new MimeType[0]); + } + + /** + * Create an {@link ObjectMapper} and register custom serializers and deserializers for the supported media types. + */ + default ObjectMapper createObjectMapper(ObjectMapper mapper) { + return mapper.copy(); + } + + default Optional> configurer() { + return Optional.empty(); + } +} diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaObjectMapperCreator.java b/src/main/java/org/springframework/hateoas/config/HypermediaObjectMapperCreator.java deleted file mode 100644 index 793ee525..00000000 --- a/src/main/java/org/springframework/hateoas/config/HypermediaObjectMapperCreator.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright 2019 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.support.MessageSourceAccessor; -import org.springframework.hateoas.RelProvider; -import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule; -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.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator; -import org.springframework.hateoas.uber.Jackson2UberModule; - -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; - -/** - * Utility class for create {@link ObjectMapper} instances of all hypermedia types. - * - * @author Greg Turnquist - * @since 1.0 - */ -public final class HypermediaObjectMapperCreator { - - /** - * Create a {@link org.springframework.hateoas.MediaTypes#HAL_JSON}-based {@link ObjectMapper}. - * - * @param objectMapper - * @param curieProvider - * @param relProvider - * @param linkRelationMessageSource - * @param halConfiguration - * @return - */ - public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper, CurieProvider curieProvider, - RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource, HalConfiguration halConfiguration) { - - ObjectMapper mapper = objectMapper.copy(); - - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - mapper.registerModule(new Jackson2HalModule()); - mapper.setHandlerInstantiator( - new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource, halConfiguration)); - - return mapper; - } - - /** - * Create a {@link org.springframework.hateoas.MediaTypes#HAL_FORMS_JSON}-based {@link ObjectMapper}. - * - * @param objectMapper - * @param curieProvider - * @param relProvider - * @param linkRelationMessageSource - * @param halFormsConfiguration - * @return properly configured objectMapper - */ - public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper, CurieProvider curieProvider, - RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource, - HalFormsConfiguration halFormsConfiguration) { - - ObjectMapper mapper = objectMapper.copy(); - - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - mapper.registerModule(new Jackson2HalFormsModule()); - mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource, - true, halFormsConfiguration)); - - return mapper; - } - - /** - * Create a {@link org.springframework.hateoas.MediaTypes#COLLECTION_JSON}-based {@link ObjectMapper}. - * - * @param objectMapper - * @param linkRelationMessageSource - * @return properly configured objectMapper - */ - public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper) { - - ObjectMapper mapper = objectMapper.copy(); - - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - mapper.registerModule(new Jackson2CollectionJsonModule()); - - return mapper; - } - - /** - * Create a {@link org.springframework.hateoas.MediaTypes#UBER_JSON}-based {@link ObjectMapper}. - * - * @param objectMapper - * @return properly configured objectMapper - */ - public static ObjectMapper createUberObjectMapper(ObjectMapper objectMapper) { - - ObjectMapper mapper = objectMapper.copy(); - - mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); - mapper.registerModule(new Jackson2UberModule()); - - return mapper; - } -} diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java index 925a3daf..8fc393bc 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaSupportBeanDefinitionRegistrar.java @@ -22,22 +22,17 @@ import java.util.Arrays; import java.util.Collection; import java.util.Map; -import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; 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.annotation.Configuration; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.type.AnnotationMetadata; import org.springframework.hateoas.EntityLinks; -import org.springframework.hateoas.LinkDiscoverer; -import org.springframework.hateoas.collectionjson.CollectionJsonLinkDiscoverer; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; -import org.springframework.hateoas.hal.HalLinkDiscoverer; -import org.springframework.hateoas.hal.forms.HalFormsLinkDiscoverer; -import org.springframework.hateoas.uber.UberLinkDiscoverer; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** @@ -68,55 +63,18 @@ class HypermediaSupportBeanDefinitionRegistrar implements ImportBeanDefinitionRe */ for (HypermediaType type : types) { - BeanDefinitionBuilder hypermediaTypeBeanDefinition = genericBeanDefinition(HypermediaType.class, () -> type); - registerSourcedBeanDefinition(hypermediaTypeBeanDefinition, metadata, registry); + type.configurer().ifPresent(clazz -> { + + Assert.isTrue(clazz.isAnnotationPresent(Configuration.class), + clazz + " must have Spring's @Configuration annotation!"); + + BeanDefinitionBuilder hypermediaTypeBeanDefinition = genericBeanDefinition(clazz); + registerSourcedBeanDefinition(hypermediaTypeBeanDefinition, metadata, registry); + }); } - /* - * Only register JSONPath-based {@link LinkDiscoverer}s based on the registered {@link HypermediaType}s. - */ - if (JSONPATH_PRESENT) { - - for (HypermediaType type : types) { - - AbstractBeanDefinition linkDiscovererBeanDefinition = getLinkDiscovererBeanDefinition(type); - registerBeanDefinition(new BeanDefinitionHolder(linkDiscovererBeanDefinition, - BeanDefinitionReaderUtils.generateBeanName(linkDiscovererBeanDefinition, registry)), registry); - } - } } - - /** - * Returns a {@link LinkDiscoverer} {@link BeanDefinition} suitable for the given {@link HypermediaType}. - * - * @param type - * @return - */ - private AbstractBeanDefinition getLinkDiscovererBeanDefinition(HypermediaType type) { - - AbstractBeanDefinition definition; - - switch (type) { - case HAL: - definition = new RootBeanDefinition(HalLinkDiscoverer.class); - break; - case HAL_FORMS: - definition = new RootBeanDefinition(HalFormsLinkDiscoverer.class); - break; - case COLLECTION_JSON: - definition = new RootBeanDefinition(CollectionJsonLinkDiscoverer.class); - break; - case UBER: - definition = new RootBeanDefinition(UberLinkDiscoverer.class); - break; - default: - throw new IllegalStateException(String.format("Unsupported hypermedia type %s!", type)); - } - - definition.setSource(this); - return definition; - } - + private static String registerSourcedBeanDefinition(BeanDefinitionBuilder builder, AnnotationMetadata metadata, BeanDefinitionRegistry registry) { diff --git a/src/main/java/org/springframework/hateoas/config/mvc/HypermediaWebMvcConfigurer.java b/src/main/java/org/springframework/hateoas/config/mvc/HypermediaWebMvcConfigurer.java index 6d74947c..7743c03d 100644 --- a/src/main/java/org/springframework/hateoas/config/mvc/HypermediaWebMvcConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/mvc/HypermediaWebMvcConfigurer.java @@ -15,32 +15,16 @@ */ package org.springframework.hateoas.config.mvc; -import static org.springframework.hateoas.MediaTypes.*; -import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*; - import lombok.RequiredArgsConstructor; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.List; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.hateoas.ResourceSupport; -import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; -import org.springframework.hateoas.core.DelegatingRelProvider; -import org.springframework.hateoas.hal.CurieProvider; -import org.springframework.hateoas.hal.HalConfiguration; -import org.springframework.hateoas.hal.Jackson2HalModule; -import org.springframework.hateoas.hal.forms.HalFormsConfiguration; +import org.springframework.hateoas.config.Hypermedia; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.fasterxml.jackson.databind.ObjectMapper; @@ -51,27 +35,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ @Configuration @RequiredArgsConstructor -public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware { - - private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource"; +public class HypermediaWebMvcConfigurer implements WebMvcConfigurer { private final ObjectMapper mapper; - private final DelegatingRelProvider relProvider; - private final CurieProvider curieProvider; - private final HalConfiguration halConfiguration; - private final HalFormsConfiguration halFormsConfiguration; - private final Collection hypermediaTypes; - - private BeanFactory beanFactory; - - /* - * (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; - } + private final Collection hypermediaTypes; /* * (non-Javadoc) @@ -80,44 +47,10 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory @Override public void extendMessageConverters(List> converters) { - if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance) - .filter(AbstractJackson2HttpMessageConverter.class::isInstance) - .map(AbstractJackson2HttpMessageConverter.class::cast) - .map(AbstractJackson2HttpMessageConverter::getObjectMapper) - .anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) { - - return; - } - - MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); - - if (this.hypermediaTypes.contains(HypermediaType.HAL)) { - - converters.add(0, - new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class, - Arrays.asList(HAL_JSON, HAL_JSON_UTF8), createHalObjectMapper(this.mapper, this.curieProvider, - this.relProvider, linkRelationMessageSource, this.halConfiguration))); - } - - if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) { - - converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class, Collections.singletonList(HAL_FORMS_JSON), - createHalFormsObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource, - this.halFormsConfiguration))); - } - - if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) { - - converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class, - Arrays.asList(COLLECTION_JSON), createCollectionJsonObjectMapper(this.mapper))); - } - - if (this.hypermediaTypes.contains(HypermediaType.UBER)) { + this.hypermediaTypes.forEach(hypermedia -> { converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter( - ResourceSupport.class, Collections.singletonList(UBER_JSON), createUberObjectMapper(this.mapper))); - } + ResourceSupport.class, hypermedia.getMediaTypes(), hypermedia.createObjectMapper(this.mapper))); + }); } } diff --git a/src/main/java/org/springframework/hateoas/config/mvc/WebMvcHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/mvc/WebMvcHateoasConfiguration.java index 98908657..e6292e17 100644 --- a/src/main/java/org/springframework/hateoas/config/mvc/WebMvcHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/mvc/WebMvcHateoasConfiguration.java @@ -20,7 +20,7 @@ import java.util.Collection; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.CurieProvider; import org.springframework.hateoas.hal.HalConfiguration; @@ -38,13 +38,11 @@ public class WebMvcHateoasConfiguration { @Bean HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider mapper, - DelegatingRelProvider relProvider, ObjectProvider curieProvider, - ObjectProvider halConfiguration, ObjectProvider halFormsConfiguration, - Collection hypermediaTypes) { +// DelegatingRelProvider relProvider, ObjectProvider curieProvider, +// ObjectProvider halConfiguration, ObjectProvider halFormsConfiguration, + Collection hypermediaTypes) { - return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), relProvider, - curieProvider.getIfAvailable(), halConfiguration.getIfAvailable(HalConfiguration::new), - halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new), hypermediaTypes); + return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); } @Bean diff --git a/src/main/java/org/springframework/hateoas/config/reactive/HypermediaWebFluxConfigurer.java b/src/main/java/org/springframework/hateoas/config/reactive/HypermediaWebFluxConfigurer.java index 213f36e4..96bafbc2 100644 --- a/src/main/java/org/springframework/hateoas/config/reactive/HypermediaWebFluxConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/reactive/HypermediaWebFluxConfigurer.java @@ -15,25 +15,14 @@ */ package org.springframework.hateoas.config.reactive; -import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*; - import lombok.RequiredArgsConstructor; import java.util.Collection; -import org.springframework.beans.BeansException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.context.annotation.Configuration; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.StringDecoder; -import org.springframework.hateoas.MediaTypes; -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.forms.HalFormsConfiguration; +import org.springframework.hateoas.config.Hypermedia; import org.springframework.http.codec.CodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonDecoder; @@ -51,23 +40,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ @Configuration @RequiredArgsConstructor -public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFactoryAware { - - private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource"; +public class HypermediaWebFluxConfigurer implements WebFluxConfigurer { private final ObjectMapper mapper; - private final DelegatingRelProvider relProvider; - private final CurieProvider curieProvider; - private final HalConfiguration halConfiguration; - private final HalFormsConfiguration halFormsConfiguration; - private final Collection hypermediaTypes; - - private BeanFactory beanFactory; - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } + private final Collection hypermediaTypes; /** * Configure custom HTTP message readers and writers or override built-in ones. @@ -79,58 +55,14 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto @Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { - if (configurer.getReaders().stream() - .flatMap(httpMessageReader -> httpMessageReader.getReadableMediaTypes().stream()) - .anyMatch(mediaType -> mediaType == MediaTypes.HAL_JSON || mediaType == MediaTypes.HAL_JSON_UTF8 - || mediaType == MediaTypes.HAL_FORMS_JSON || mediaType == MediaTypes.COLLECTION_JSON - || mediaType == MediaTypes.UBER_JSON)) { - - // Already configured for hypermedia! - return; - } - - MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); - CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs(); - if (this.hypermediaTypes.contains(HypermediaType.HAL)) { + this.hypermediaTypes.forEach(hypermedia -> { - ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider, - linkRelationMessageSource, this.halConfiguration); - - customCodecs.encoder( - new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8)); - customCodecs.decoder( - new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8)); - } - - if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) { - - ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider, - this.relProvider, linkRelationMessageSource, this.halFormsConfiguration); - - customCodecs.encoder( - new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON)); - customCodecs.decoder( - new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON)); - } - - if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) { - - ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper); - - customCodecs.encoder(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON)); - customCodecs.decoder(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON)); - } - - if (this.hypermediaTypes.contains(HypermediaType.UBER)) { - - ObjectMapper uberObjectMapper = createUberObjectMapper(this.mapper); - - customCodecs.encoder(new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON)); - customCodecs.decoder(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON)); - } + ObjectMapper objectMapper = hypermedia.createObjectMapper(this.mapper); + customCodecs.encoder(new Jackson2JsonEncoder(objectMapper, hypermedia.getMimeTypes())); + customCodecs.decoder(new Jackson2JsonDecoder(objectMapper, hypermedia.getMimeTypes())); + }); customCodecs.encoder(CharSequenceEncoder.allMimeTypes()); customCodecs.decoder(StringDecoder.allMimeTypes()); diff --git a/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java index 8d07a57e..156193dd 100644 --- a/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/reactive/WebClientConfigurer.java @@ -15,31 +15,22 @@ */ package org.springframework.hateoas.config.reactive; -import static org.springframework.hateoas.config.HypermediaObjectMapperCreator.*; - import lombok.RequiredArgsConstructor; import java.util.ArrayList; 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.context.annotation.Configuration; -import org.springframework.context.support.MessageSourceAccessor; import org.springframework.core.codec.CharSequenceEncoder; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.codec.StringDecoder; -import org.springframework.hateoas.MediaTypes; 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.forms.HalFormsConfiguration; +import org.springframework.hateoas.config.Hypermedia; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; +import org.springframework.util.MimeType; import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; @@ -53,23 +44,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ @Configuration @RequiredArgsConstructor -public class WebClientConfigurer implements BeanFactoryAware { - - private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource"; +public class WebClientConfigurer { private final ObjectMapper mapper; - private final DelegatingRelProvider relProvider; - private final CurieProvider curieProvider; - private final HalConfiguration halConfiguration; - private final HalFormsConfiguration halFormsConfiguration; - private final Collection hypermediaTypes; - - private BeanFactory beanFactory; - - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - this.beanFactory = beanFactory; - } + private final Collection hypermediaTypes; /** * Return a set of {@link ExchangeStrategies} driven by registered {@link HypermediaType}s. @@ -78,45 +56,16 @@ public class WebClientConfigurer implements BeanFactoryAware { */ public ExchangeStrategies hypermediaExchangeStrategies() { - MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME, - MessageSourceAccessor.class); - List> encoders = new ArrayList<>(); List> decoders = new ArrayList<>(); - if (this.hypermediaTypes.contains(HypermediaType.HAL)) { + this.hypermediaTypes.forEach(hypermedia -> { - ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider, - linkRelationMessageSource, this.halConfiguration); + ObjectMapper objectMapper = hypermedia.createObjectMapper(this.mapper); - encoders.add(new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8)); - decoders.add(new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8)); - } - - if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) { - - ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider, - this.relProvider, linkRelationMessageSource, this.halFormsConfiguration); - - encoders.add(new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON)); - decoders.add(new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON)); - } - - if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) { - - ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper); - - encoders.add(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON)); - decoders.add(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON)); - } - - if (this.hypermediaTypes.contains(HypermediaType.UBER)) { - - ObjectMapper uberObjectMapper = createUberObjectMapper(this.mapper); - - encoders.add(new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON)); - decoders.add(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON)); - } + encoders.add(new Jackson2JsonEncoder(objectMapper, hypermedia.getMediaTypes().toArray(new MimeType[]{}))); + decoders.add(new Jackson2JsonDecoder(objectMapper, hypermedia.getMediaTypes().toArray(new MimeType[]{}))); + }); encoders.add(CharSequenceEncoder.allMimeTypes()); decoders.add(StringDecoder.allMimeTypes()); diff --git a/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java index 9f82f4ee..dcf15838 100644 --- a/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/reactive/WebFluxHateoasConfiguration.java @@ -20,7 +20,7 @@ import java.util.Collection; import org.springframework.beans.factory.ObjectProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; import org.springframework.hateoas.core.DelegatingRelProvider; import org.springframework.hateoas.hal.CurieProvider; import org.springframework.hateoas.hal.HalConfiguration; @@ -39,13 +39,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; public class WebFluxHateoasConfiguration { @Bean - WebClientConfigurer webClientConfigurer(ObjectProvider mapper, DelegatingRelProvider relProvider, - ObjectProvider curieProvider, ObjectProvider halConfiguration, - ObjectProvider halFormsConfiguration, Collection hypermediaTypes) { + WebClientConfigurer webClientConfigurer(ObjectProvider mapper, + Collection hypermediaTypes) { - return new WebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), relProvider, - curieProvider.getIfAvailable(), halConfiguration.getIfAvailable(HalConfiguration::new), - halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new), hypermediaTypes); + return new WebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); } @Bean @@ -55,13 +52,9 @@ public class WebFluxHateoasConfiguration { @Bean HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider mapper, - DelegatingRelProvider relProvider, ObjectProvider curieProvider, - ObjectProvider halConfiguration, ObjectProvider halFormsConfiguration, - Collection hypermediaTypes) { + Collection hypermediaTypes) { - return new HypermediaWebFluxConfigurer(mapper.getIfAvailable(ObjectMapper::new), relProvider, - curieProvider.getIfAvailable(), halConfiguration.getIfAvailable(HalConfiguration::new), - halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new), hypermediaTypes); + return new HypermediaWebFluxConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); } /** diff --git a/src/main/java/org/springframework/hateoas/hal/HalConfigurer.java b/src/main/java/org/springframework/hateoas/hal/HalConfigurer.java new file mode 100644 index 00000000..567fc5a3 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/HalConfigurer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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; + +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; +import org.springframework.hateoas.core.DelegatingRelProvider; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Greg Turnquist + */ +@Configuration +public class HalConfigurer { + + @Bean + Hypermedia halBean(DelegatingRelProvider relProvider, + ObjectProvider curieProvider, + ObjectProvider halConfiguration, + MessageSourceAccessor messageSourceAccessor) { + + return new Hypermedia() { + @Override + public List getMediaTypes() { + return HypermediaType.HAL.getMediaTypes(); + } + + @Override + public ObjectMapper createObjectMapper(ObjectMapper mapper) { + + ObjectMapper mapper1 = mapper.copy(); + + mapper1.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper1.registerModule(new Jackson2HalModule()); + mapper1.setHandlerInstantiator( + new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider.getIfAvailable(), messageSourceAccessor, halConfiguration.getIfAvailable(HalConfiguration::new))); + + return mapper1; + } + }; + } + + @Bean + LinkDiscoverer linkDiscoverer() { + return new HalLinkDiscoverer(); + } + +} diff --git a/src/main/java/org/springframework/hateoas/hal/forms/HalFormsConfigurer.java b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsConfigurer.java new file mode 100644 index 00000000..5b6fc1b9 --- /dev/null +++ b/src/main/java/org/springframework/hateoas/hal/forms/HalFormsConfigurer.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.MessageSourceAccessor; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; +import org.springframework.hateoas.core.DelegatingRelProvider; +import org.springframework.hateoas.hal.CurieProvider; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Greg Turnquist + */ +@Configuration +public class HalFormsConfigurer { + + @Bean + Hypermedia halFormsBean(DelegatingRelProvider relProvider, + ObjectProvider curieProvider, + ObjectProvider halFormsConfiguration, + MessageSourceAccessor messageSourceAccessor) { + + return new Hypermedia() { + @Override + public List getMediaTypes() { + return HypermediaType.HAL_FORMS.getMediaTypes(); + } + + @Override + public ObjectMapper createObjectMapper(ObjectMapper mapper) { + + ObjectMapper mapper1 = mapper.copy(); + + mapper1.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper1.registerModule(new Jackson2HalFormsModule()); + mapper1.setHandlerInstantiator(new Jackson2HalFormsModule.HalFormsHandlerInstantiator(relProvider, curieProvider.getIfAvailable(), messageSourceAccessor, + true, halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new))); + + return mapper1; + } + }; + } + + @Bean LinkDiscoverer linkDiscoverer() { + return new HalFormsLinkDiscoverer(); + } + +} diff --git a/src/main/java/org/springframework/hateoas/uber/UberConfigurer.java b/src/main/java/org/springframework/hateoas/uber/UberConfigurer.java new file mode 100644 index 00000000..8e3196ef --- /dev/null +++ b/src/main/java/org/springframework/hateoas/uber/UberConfigurer.java @@ -0,0 +1,63 @@ +/* + * Copyright 2019 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.uber; + +import java.util.List; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.LinkDiscoverer; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.Hypermedia; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Greg Turnquist + */ +@Configuration +public class UberConfigurer { + + @Bean + Hypermedia uberBean() { + + return new Hypermedia() { + @Override + public List getMediaTypes() { + return HypermediaType.UBER.getMediaTypes(); + } + + @Override + public ObjectMapper createObjectMapper(ObjectMapper mapper) { + + ObjectMapper mapper1 = mapper.copy(); + + mapper1.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper1.registerModule(new Jackson2UberModule()); + + return mapper1; + } + }; + } + + @Bean + LinkDiscoverer linkDiscoverer() { + return new UberLinkDiscoverer(); + } + +} diff --git a/src/test/java/org/springframework/hateoas/config/mvc/CustomHypermediaWebMvcTest.java b/src/test/java/org/springframework/hateoas/config/mvc/CustomHypermediaWebMvcTest.java new file mode 100644 index 00000000..4f75be42 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/config/mvc/CustomHypermediaWebMvcTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 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.mvc; + +import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*; +import static org.springframework.hateoas.support.CustomHypermediaType.*; +import static org.springframework.hateoas.support.MappingUtils.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; +import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.support.CustomHypermediaType; +import org.springframework.hateoas.support.Employee; +import org.springframework.http.HttpHeaders; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Integration test for a custom hypermedia support registration. + * + * @author Greg Turnquist + */ +@RunWith(SpringRunner.class) +@WebAppConfiguration +@ContextConfiguration +public class CustomHypermediaWebMvcTest { + + private @Autowired WebApplicationContext context; + private MockMvc mockMvc; + + @Before + public void setUp() { + this.mockMvc = webAppContextSetup(this.context).build(); + } + + @Test // #833 + public void getUsingCustomMediaType() throws Exception { + + String results = this.mockMvc.perform(get("/employees/1").accept(FRODO_MEDIATYPE)) + .andExpect(header().string(HttpHeaders.CONTENT_TYPE, FRODO_MEDIATYPE.toString() + ";charset=UTF-8")) + .andDo(print()).andReturn().getResponse().getContentAsString(); + + assertThat(results).isEqualTo(read(new ClassPathResource("frodo.json", getClass()))); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class TestConfig { + + @Bean + CustomHypermediaType customHypermediaType() { + return new CustomHypermediaType(); + } + + @Bean + EmployeeController employeeController() { + return new EmployeeController(); + } + } + + @RestController + static class EmployeeController { + + @GetMapping("/employees/1") + public Resource findOne() { + return new Resource<>(new Employee("Frodo Baggins", "ring bearer"), + linkTo(methodOn(EmployeeController.class).findOne()).withSelfRel()); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/config/mvc/HypermediaRestTemplateBeanPostProcessorTest.java b/src/test/java/org/springframework/hateoas/config/mvc/HypermediaRestTemplateBeanPostProcessorTest.java index 6a979d31..f0e9ea28 100644 --- a/src/test/java/org/springframework/hateoas/config/mvc/HypermediaRestTemplateBeanPostProcessorTest.java +++ b/src/test/java/org/springframework/hateoas/config/mvc/HypermediaRestTemplateBeanPostProcessorTest.java @@ -28,6 +28,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.config.EnableHypermediaSupport; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.support.CustomHypermediaType; import org.springframework.http.MediaType; import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; @@ -96,6 +97,22 @@ public class HypermediaRestTemplateBeanPostProcessorTest { }); } + @Test // #833 + public void shouldRegisterCustomHypermediaMessageConverters() { + + withContext(CustomHypermediaConfig.class, context -> { + + assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class))) // + .containsExactlyInAnyOrder( // + MediaTypes.HAL_JSON, // + MediaTypes.HAL_JSON_UTF8, // + MediaType.parseMediaType("application/frodo+json"), // + MediaType.APPLICATION_JSON, // + MediaType.parseMediaType("application/*+json") // + ); + }); + } + private List lookupSupportedHypermediaTypes(RestTemplate restTemplate) { return restTemplate.getMessageConverters().stream().filter(MappingJackson2HttpMessageConverter.class::isInstance) @@ -123,4 +140,14 @@ public class HypermediaRestTemplateBeanPostProcessorTest { @EnableHypermediaSupport( type = { HypermediaType.HAL, HypermediaType.HAL_FORMS, HypermediaType.COLLECTION_JSON, HypermediaType.UBER }) static class AllHypermediaConfig extends BaseConfig {} + + @Configuration + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class CustomHypermediaConfig extends BaseConfig { + + @Bean + CustomHypermediaType customHypermediaType() { + return new CustomHypermediaType(); + } + } } diff --git a/src/test/java/org/springframework/hateoas/config/reactive/CustomHypermediaWebFluxTest.java b/src/test/java/org/springframework/hateoas/config/reactive/CustomHypermediaWebFluxTest.java new file mode 100644 index 00000000..946f34e2 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/config/reactive/CustomHypermediaWebFluxTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2019 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.reactive; + +import static org.springframework.hateoas.reactive.WebFluxLinkBuilder.*; +import static org.springframework.hateoas.support.CustomHypermediaType.*; +import static org.springframework.hateoas.support.MappingUtils.*; + +import reactor.core.publisher.Mono; + +import java.io.IOException; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ClassPathResource; +import org.springframework.hateoas.Resource; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.support.CustomHypermediaType; +import org.springframework.hateoas.support.Employee; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.config.EnableWebFlux; + +/** + * @author Greg Turnquist + */ +public class CustomHypermediaWebFluxTest { + + WebTestClient testClient; + + @Before + public void setUp() { + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.register(TestConfig.class); + ctx.refresh(); + + WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class); + + this.testClient = WebTestClient.bindToApplicationContext(ctx).build() // + .mutate() // + .exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()) // + .build(); + } + + @Test // #833 + public void getUsingCustomMediaType() throws IOException { + + this.testClient.get().uri("http://localhost/employees/1") // + .accept(FRODO_MEDIATYPE) // + .exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(FRODO_MEDIATYPE.toString()) // + .expectBody(String.class) // + .isEqualTo(read(new ClassPathResource("frodo.json", getClass()))); + } + + @Configuration + @EnableWebFlux + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class TestConfig { + + @Bean + CustomHypermediaType customHypermediaType() { + return new CustomHypermediaType(); + } + + @Bean + EmployeeController employeeController() { + return new EmployeeController(); + } + } + + @RestController + static class EmployeeController { + + @GetMapping("/employees/1") + public Mono> findOne() { + + return linkTo(methodOn(EmployeeController.class).findOne()).withSelfRel().toMono() + .map(link -> new Resource<>(new Employee("Frodo Baggins", "ring bearer"), link)); + } + } +} diff --git a/src/test/java/org/springframework/hateoas/support/CustomHypermediaType.java b/src/test/java/org/springframework/hateoas/support/CustomHypermediaType.java new file mode 100644 index 00000000..9b95ab5e --- /dev/null +++ b/src/test/java/org/springframework/hateoas/support/CustomHypermediaType.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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.support; + +import java.util.Collections; +import java.util.List; + +import org.springframework.hateoas.config.Hypermedia; +import org.springframework.http.MediaType; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +/** + * @author Greg Turnquist + */ +public class CustomHypermediaType implements Hypermedia { + + public static final MediaType FRODO_MEDIATYPE = MediaType.parseMediaType("application/frodo+json"); + + /** + * {@link MediaType}s this hypermedia can handle. + */ + @Override + public List getMediaTypes() { + return Collections.singletonList(FRODO_MEDIATYPE); + } + + /** + * Copy the incoming {@link ObjectMapper} and change it's output format along with disabling failure on + * unknown properties. + */ + @Override + public ObjectMapper createObjectMapper(ObjectMapper mapper) { + + ObjectMapper objectMapper = mapper.copy(); + objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + objectMapper.enable(SerializationFeature.INDENT_OUTPUT); + + return objectMapper; + } +} diff --git a/src/test/resources/org/springframework/hateoas/config/mvc/frodo.json b/src/test/resources/org/springframework/hateoas/config/mvc/frodo.json new file mode 100644 index 00000000..c7bb07c9 --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/config/mvc/frodo.json @@ -0,0 +1,8 @@ +{ + "name" : "Frodo Baggins", + "role" : "ring bearer", + "links" : [ { + "rel" : "self", + "href" : "http://localhost/employees/1" + } ] +} \ No newline at end of file diff --git a/src/test/resources/org/springframework/hateoas/config/reactive/frodo.json b/src/test/resources/org/springframework/hateoas/config/reactive/frodo.json new file mode 100644 index 00000000..c7bb07c9 --- /dev/null +++ b/src/test/resources/org/springframework/hateoas/config/reactive/frodo.json @@ -0,0 +1,8 @@ +{ + "name" : "Frodo Baggins", + "role" : "ring bearer", + "links" : [ { + "rel" : "self", + "href" : "http://localhost/employees/1" + } ] +} \ No newline at end of file