diff --git a/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java index 600c9aad..1fc87f7d 100644 --- a/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/RestTemplateHateoasConfiguration.java @@ -18,11 +18,9 @@ package org.springframework.hateoas.config; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeansException; -import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.hateoas.config.WebMvcHateoasConfiguration.HypermediaWebMvcConfigurer; import org.springframework.lang.NonNull; import org.springframework.web.client.RestTemplate; @@ -37,10 +35,10 @@ class RestTemplateHateoasConfiguration { @Bean static HypermediaRestTemplateBeanPostProcessor hypermediaRestTemplateBeanPostProcessor( - ObjectProvider configurer) { - return new HypermediaRestTemplateBeanPostProcessor(configurer); + WebMvcConverters converters) { + return new HypermediaRestTemplateBeanPostProcessor(converters); } - + /** * {@link BeanPostProcessor} to register hypermedia support with {@link RestTemplate} instances found in the * application context. @@ -51,7 +49,7 @@ class RestTemplateHateoasConfiguration { @RequiredArgsConstructor static class HypermediaRestTemplateBeanPostProcessor implements BeanPostProcessor { - private final ObjectProvider configurer; + private final WebMvcConverters converters; /* * (non-Javadoc) @@ -65,9 +63,10 @@ class RestTemplateHateoasConfiguration { return bean; } - configurer.getObject().extendMessageConverters(((RestTemplate) bean).getMessageConverters()); + RestTemplate template = (RestTemplate) bean; + template.setMessageConverters(converters.and(template.getMessageConverters())); - return bean; + return template; } } } diff --git a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java index 03762ef6..df72025f 100644 --- a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java @@ -17,12 +17,17 @@ package org.springframework.hateoas.config; import lombok.RequiredArgsConstructor; +import java.util.ArrayList; 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.annotation.Lazy; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.Encoder; +import org.springframework.http.MediaType; +import org.springframework.http.codec.CodecConfigurer.CustomCodecs; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; @@ -36,16 +41,25 @@ import com.fasterxml.jackson.databind.ObjectMapper; * Spring WebFlux HATEOAS configuration. * * @author Greg Turnquist - * @since 1.0 TODO: Inspect ApplicationContext -> WebApplicationContext -> WebMVC + * @author Oliver Drotbohm + * @since 1.0 */ @Configuration class WebFluxHateoasConfiguration { @Bean - HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider mapper, - List hypermediaTypes) { + WebFluxCodecs hypermediaConverters(ObjectProvider mapper, + List mappingInformation) { + return new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation); + } - return new HypermediaWebFluxConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); + @Bean + HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider mapper, + List mappingInformation) { + + WebFluxCodecs codecs = new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation); + + return new HypermediaWebFluxConfigurer(codecs); } @Bean @@ -64,27 +78,53 @@ class WebFluxHateoasConfiguration { @RequiredArgsConstructor static class HypermediaWebFluxConfigurer implements WebFluxConfigurer { - private final ObjectMapper mapper; - private final List hypermediaTypes; + private final WebFluxCodecs codecs; /** * Configure custom HTTP message readers and writers or override built-in ones. *

* The configured readers and writers will be used for both annotated controllers and functional endpoints. * - * @param configurer the configurer to use + * @param configurer the configurer to use, must not be {@literal null}. */ @Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { + codecs.registerCodecs(configurer.customCodecs()); + } + } - this.hypermediaTypes.forEach(hypermedia -> { + private static class WebFluxCodecs { - ObjectMapper objectMapper = hypermedia.configureObjectMapper(this.mapper.copy()); - MimeType[] mimeTypes = hypermedia.getMediaTypes().toArray(new MimeType[0]); - - configurer.customCodecs().encoder(new Jackson2JsonEncoder(objectMapper, mimeTypes)); - configurer.customCodecs().decoder(new Jackson2JsonDecoder(objectMapper, mimeTypes)); - }); + private final List> decoders; + private final List> encoders; + + private WebFluxCodecs(ObjectMapper mapper, List mappingInformation) { + + this.decoders = new ArrayList<>(); + this.encoders = new ArrayList<>(); + + for (HypermediaMappingInformation information : mappingInformation) { + + ObjectMapper objectMapper = information.configureObjectMapper(mapper.copy()); + List mediaTypes = information.getMediaTypes(); + + this.decoders.add(getDecoder(objectMapper, mediaTypes)); + this.encoders.add(getEncoder(objectMapper, mediaTypes)); + } + } + + public void registerCodecs(CustomCodecs codecs) { + + decoders.forEach(codecs::decoder); + encoders.forEach(codecs::encoder); + } + + private static Decoder getDecoder(ObjectMapper mapper, List mediaTypes) { + return new Jackson2JsonDecoder(mapper, mediaTypes.toArray(new MimeType[0])); + } + + private static Encoder getEncoder(ObjectMapper mapper, List mediaTypes) { + return new Jackson2JsonEncoder(mapper, mediaTypes.toArray(new MimeType[0])); } } } diff --git a/src/main/java/org/springframework/hateoas/config/WebMvcConverters.java b/src/main/java/org/springframework/hateoas/config/WebMvcConverters.java new file mode 100644 index 00000000..88cca0ad --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/WebMvcConverters.java @@ -0,0 +1,112 @@ +/* + * 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 + * + * https://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.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.hateoas.RepresentationModel; +import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.util.Assert; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Value type to handle registration of hypermedia related {@link HttpMessageConverter}s. + * + * @author Oliver Drotbohm + */ +class WebMvcConverters { + + private final List> converters; + + /** + * Creates a new {@link WebMvcConverters} from the given {@link ObjectMapper} and + * {@link HypermediaMappingInformation}s. + * + * @param mapper must not be {@literal null}. + * @param mappingInformation must not be {@literal null}. + */ + private WebMvcConverters(ObjectMapper mapper, List mappingInformation) { + + this.converters = mappingInformation.stream() // + .map(it -> createMessageConverter(it, it.configureObjectMapper(mapper.copy()))) // + .collect(Collectors.toList()); + } + + /** + * Creates a new {@link WebMvcConverters} from the given {@link ObjectMapper} and + * {@link HypermediaMappingInformation}s. + * + * @param mapper must not be {@literal null}. + * @param mappingInformations must not be {@literal null}. + * @return + */ + public static WebMvcConverters of(ObjectMapper mapper, List mappingInformations) { + + Assert.notNull(mapper, "ObjectMapper must not be null!"); + Assert.notNull(mappingInformations, "Mapping information must not be null!"); + + return new WebMvcConverters(mapper, mappingInformations); + } + + /** + * Augments the given {@link List} of {@link HttpMessageConverter}s with the hypermedia enabled ones. + * + * @param converters must not be {@literal null}. + */ + public void augment(List> converters) { + + Assert.notNull(converters, "HttpMessageConverters must not be null!"); + + this.converters.forEach(it -> converters.add(0, it)); + } + + /** + * Returns a new {@link List} of {@link HttpMessageConverter}s consisting of both the hypermedia based ones as well as + * the given ones. + * + * @param converters must not be {@literal null}. + */ + public List> and(Collection> converters) { + + Assert.notNull(converters, "HttpMessageConverters must not be null!"); + + List> result = new ArrayList<>(this.converters); + result.addAll(converters); + + return result; + } + + /** + * Creates a new {@link TypeConstrainedMappingJackson2HttpMessageConverter} to handle {@link RepresentationModel} for + * the given {@link HypermediaMappingInformation} using a copy of the given {@link ObjectMapper}. + * + * @param type must not be {@literal null}. + * @param mapper must not be {@literal null}. + * @return + */ + private static AbstractJackson2HttpMessageConverter createMessageConverter(HypermediaMappingInformation type, + ObjectMapper mapper) { + + return new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class, type.getMediaTypes(), + type.configureObjectMapper(mapper)); + } +} diff --git a/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java index b2c8b523..c5f163e7 100644 --- a/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java @@ -27,11 +27,9 @@ import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.hateoas.RepresentationModel; import org.springframework.hateoas.server.RepresentationModelProcessor; import org.springframework.hateoas.server.mvc.RepresentationModelProcessorHandlerMethodReturnValueHandler; import org.springframework.hateoas.server.mvc.RepresentationModelProcessorInvoker; -import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.hateoas.server.mvc.UriComponentsContributor; import org.springframework.hateoas.server.mvc.WebMvcLinkBuilderFactory; import org.springframework.http.converter.HttpMessageConverter; @@ -53,10 +51,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; class WebMvcHateoasConfiguration { @Bean - HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(ObjectProvider mapper, - List hypermediaTypes) { + WebMvcConverters hypermediaWebMvcConverters(ObjectProvider mapper, + List information) { + return WebMvcConverters.of(mapper.getIfUnique(ObjectMapper::new), information); + } - return new HypermediaWebMvcConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); + @Bean + HypermediaWebMvcConfigurer hypermediaWebMvcConfigurer(WebMvcConverters converters) { + return new HypermediaWebMvcConfigurer(converters); } @Bean @@ -88,8 +90,7 @@ class WebMvcHateoasConfiguration { @RequiredArgsConstructor static class HypermediaWebMvcConfigurer implements WebMvcConfigurer { - private final ObjectMapper mapper; - private final List hypermediaTypes; + private final @NonNull WebMvcConverters hypermediaConverters; /* * (non-Javadoc) @@ -97,12 +98,7 @@ class WebMvcHateoasConfiguration { */ @Override public void extendMessageConverters(List> converters) { - - this.hypermediaTypes.forEach(hypermedia -> { - - converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(RepresentationModel.class, - hypermedia.getMediaTypes(), hypermedia.configureObjectMapper(mapper.copy()))); - }); + hypermediaConverters.augment(converters); } } diff --git a/src/test/java/org/springframework/hateoas/ArchitectureTest.java b/src/test/java/org/springframework/hateoas/ArchitectureTest.java index 380c9a71..659bcb07 100644 --- a/src/test/java/org/springframework/hateoas/ArchitectureTest.java +++ b/src/test/java/org/springframework/hateoas/ArchitectureTest.java @@ -15,25 +15,36 @@ */ package org.springframework.hateoas; -import org.junit.jupiter.api.Test; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; +import static org.springframework.hateoas.ArchitectureTest.Architecture.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.springframework.lang.Nullable; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.core.importer.ImportOptions; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; import com.tngtech.archunit.library.dependencies.SliceRule; import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; /** * Tests to verify certain architectural assumptions. * - * @author Oliver Gierke + * @author Oliver Drotbohm */ +@TestInstance(Lifecycle.PER_CLASS) class ArchitectureTest { ImportOptions options = new ImportOptions().with(ImportOption.Predefined.DONT_INCLUDE_TESTS); JavaClasses classes = new ClassFileImporter(options) // - .importPackages("org.springframework.hateoas"); + .importPackages("org.springframework.hateoas", "org.springframework"); @Test void assertNoCyclicPackageDependencies() { @@ -44,4 +55,58 @@ class ArchitectureTest { rule.check(classes); } + + @Test // #1119 + void onlyReactivePackagesReferToReactiveTypesInSpringFramework() { + + DescribedPredicate areSpringFrameworkClassesWithReactiveDependency = JavaClass.Predicates + .resideInAnyPackage("org.springframework..") // + .and(JavaClass.Predicates.resideOutsideOfPackage("..hateoas..")) // + .and(dependsOn(reactiveType())); + + ArchRuleDefinition.noClasses().that() // + .resideInAnyPackage("org.springframework.hateoas..") // + .and().resideOutsideOfPackages("..reactive") // + .and().haveSimpleNameNotStartingWith("WebFlux") // + .and().haveSimpleNameNotStartingWith("WebClient") // + .should().dependOnClassesThat(areSpringFrameworkClassesWithReactiveDependency) // + .check(classes); + } + + static class Architecture { + + public static DescribedPredicate hasWebFluxPrefix() { + return simpleNameStartingWith("WebClient").or(simpleNameStartingWith("WebFlux")); + } + + public static DescribedPredicate reactorType() { + return resideInAPackage("reactor.."); + } + + public static DescribedPredicate reactiveStreamsType() { + return resideInAPackage("org.reactivestreams.."); + } + + public static DescribedPredicate reactiveType() { + return reactorType().or(reactiveStreamsType()); + } + + public static DescribedPredicate dependsOn(DescribedPredicate predicate) { + + return new DescribedPredicate("depends on reactive types") { + + /* + * (non-Javadoc) + * @see com.tngtech.archunit.base.DescribedPredicate#apply(java.lang.Object) + */ + @Override + public boolean apply(@Nullable JavaClass input) { + + return input != null && input.getDirectDependenciesFromSelf().stream() // + .map(Dependency::getTargetClass) // + .anyMatch(predicate::apply); + } + }; + } + } }