diff --git a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java index 058d27fc..f83ae91e 100644 --- a/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/HateoasConfiguration.java @@ -20,6 +20,7 @@ import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.Properties; import java.util.stream.Collectors; @@ -75,7 +76,10 @@ public class HateoasConfiguration { @Bean WebConverters hypermediaWebMvcConverters(ObjectProvider mapper, - List information) { + List information, Optional comparator) { + + comparator.ifPresent(information::sort); + return WebConverters.of(mapper.getIfUnique(ObjectMapper::new), information); } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index 8d5c9a60..c1a2f23f 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -22,6 +22,10 @@ import java.util.Map; import java.util.stream.Collectors; import java.util.stream.Stream; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ImportSelector; import org.springframework.core.io.ResourceLoader; @@ -38,11 +42,12 @@ import org.springframework.util.ClassUtils; * @author Oliver Drotbohm * @author Greg Turnquist */ -class HypermediaConfigurationImportSelector implements ImportSelector, ResourceLoaderAware { +class HypermediaConfigurationImportSelector implements ImportSelector, ResourceLoaderAware, BeanFactoryAware { public static final String SPRING_TEST = "org.springframework.test.web.reactive.server.WebTestClient"; private ResourceLoader resourceLoader; + private ConfigurableBeanFactory beanFactory; /* * (non-Javadoc) @@ -53,6 +58,15 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL this.resourceLoader = resourceLoader; } + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ConfigurableBeanFactory) beanFactory; + } + /* * (non-Javadoc) * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) @@ -68,6 +82,10 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL .flatMap(it -> it.getMediaTypes().stream()) // .collect(Collectors.toList()); + if (!beanFactory.containsBean("hateoasMediaTypeConfigurer")) { + beanFactory.registerSingleton("hateoasMediaTypeConfigurer", new HypermediaMappingInformationComparator(types)); + } + List configurationProviders = SpringFactoriesLoader.loadFactories( MediaTypeConfigurationProvider.class, HypermediaConfigurationImportSelector.class.getClassLoader()); diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformationComparator.java b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformationComparator.java new file mode 100644 index 00000000..f50fd01e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/HypermediaMappingInformationComparator.java @@ -0,0 +1,71 @@ +/* + * Copyright 2021 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.Comparator; +import java.util.List; + +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * A {@link Comparator} over {@link HypermediaMappingInformation} to sort them by the appearance of the + * {@link MediaType}s configured. I.e. given the media types UBER and HAL FORMS, the + * {@link HypermediaMappingInformation} supporting the former will be ordered before the one for the latter. + * + * @author Oliver Drotbohm + */ +class HypermediaMappingInformationComparator implements Comparator { + + private final List mediaTypes; + + /** + * Creates a new {@link HypermediaMappingInformationComparator} using the given reference {@link MediaType}s. + * + * @param mediaTypes must not be {@literal null}. + */ + HypermediaMappingInformationComparator(List mediaTypes) { + + Assert.notEmpty(mediaTypes, "MediaTypes must not be empty!"); + + this.mediaTypes = mediaTypes; + } + + /* + * (non-Javadoc) + * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) + */ + @Override + public int compare(@Nullable HypermediaMappingInformation left, @Nullable HypermediaMappingInformation right) { + + for (MediaType mediaType : mediaTypes) { + + boolean leftSupports = left != null && left.getMediaTypes().contains(mediaType); + boolean rightSupports = right != null && right.getMediaTypes().contains(mediaType); + + if (leftSupports && !rightSupports) { + return -1; + } + + if (!leftSupports && rightSupports) { + return 1; + } + } + + return 0; + } +} diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java b/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java index 3b81e6c7..1a400b5f 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurer.java @@ -44,7 +44,8 @@ public class HypermediaRestTemplateConfigurer { */ public RestTemplate registerHypermediaTypes(RestTemplate template) { - template.setMessageConverters(converters.and(template.getMessageConverters())); + converters.augmentClient(template.getMessageConverters()); + return template; } } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java index 7b9f179c..9caa22dd 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java @@ -15,14 +15,8 @@ */ package org.springframework.hateoas.config; -import java.util.List; -import java.util.function.Consumer; - -import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; -import org.springframework.util.Assert; -import org.springframework.util.MimeType; import org.springframework.web.reactive.function.client.WebClient; import com.fasterxml.jackson.databind.ObjectMapper; @@ -37,7 +31,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ public class HypermediaWebClientConfigurer { - final Consumer configurer; + final WebfluxCodecCustomizer customizer; /** * Creates a new {@link HypermediaWebClientConfigurer} for the given {@link ObjectMapper} and @@ -46,19 +40,8 @@ public class HypermediaWebClientConfigurer { * @param mapper must not be {@literal null}. * @param hypermediaTypes must not be {@literal null}. */ - HypermediaWebClientConfigurer(ObjectMapper mapper, List hypermediaTypes) { - - Assert.notNull(mapper, "ObjectMapper must not be null!"); - Assert.notNull(hypermediaTypes, "HypermediaMappingInformations must not be null!"); - - this.configurer = clientCodecConfigurer -> hypermediaTypes.forEach(hypermediaType -> { - - ObjectMapper objectMapper = hypermediaType.configureObjectMapper(mapper.copy()); - MimeType[] mimeTypes = hypermediaType.getMediaTypes().toArray(new MimeType[0]); - - clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonEncoder(objectMapper, mimeTypes)); - clientCodecConfigurer.customCodecs().registerWithDefaultConfig(new Jackson2JsonDecoder(objectMapper, mimeTypes)); - }); + HypermediaWebClientConfigurer(WebfluxCodecCustomizer customizer) { + this.customizer = customizer; } /** @@ -68,6 +51,9 @@ public class HypermediaWebClientConfigurer { * @return {@link WebClient.Builder} registered to handle hypermedia types. */ public WebClient.Builder registerHypermediaTypes(WebClient.Builder builder) { - return builder.codecs(this.configurer); + + return builder.codecs(it -> { + it.defaultCodecs().configureDefaultCodec(customizer); + }); } } diff --git a/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java index 0c061679..4e594fdc 100644 --- a/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/WebClientConfigurer.java @@ -34,7 +34,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; @Deprecated public class WebClientConfigurer { - private final HypermediaWebClientConfigurer hypermediaWebClientConfigurer; + private final WebfluxCodecCustomizer customizer; /** * Creates a new {@link WebClientConfigurer} for the given {@link ObjectMapper} and @@ -44,7 +44,7 @@ public class WebClientConfigurer { * @param hypermediaTypes must not be {@literal null}. */ public WebClientConfigurer(ObjectMapper mapper, List hypermediaTypes) { - this.hypermediaWebClientConfigurer = new HypermediaWebClientConfigurer(mapper, hypermediaTypes); + this.customizer = new WebfluxCodecCustomizer(hypermediaTypes, mapper); } /** @@ -55,7 +55,7 @@ public class WebClientConfigurer { public ExchangeStrategies hypermediaExchangeStrategies() { return ExchangeStrategies.builder() // - .codecs(this.hypermediaWebClientConfigurer.configurer) // + .codecs(it -> it.defaultCodecs().configureDefaultCodec(customizer)) // .build(); } @@ -66,6 +66,9 @@ public class WebClientConfigurer { * @return mutated webClient with hypermedia support. */ public WebClient registerHypermediaTypes(WebClient webClient) { - return this.hypermediaWebClientConfigurer.registerHypermediaTypes(webClient.mutate()).build(); + + return webClient.mutate() + .codecs(it -> it.defaultCodecs().configureDefaultCodec(customizer)) + .build(); } } diff --git a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java index faedb148..3ca06b7b 100644 --- a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java @@ -42,7 +42,11 @@ class WebClientHateoasConfiguration { @Lazy HypermediaWebClientConfigurer webClientConfigurer(ObjectProvider mapper, List hypermediaTypes) { - return new HypermediaWebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); + + WebfluxCodecCustomizer withGenericJsonTypes = new WebfluxCodecCustomizer(hypermediaTypes, + mapper.getIfAvailable(ObjectMapper::new)).withGenericJsonTypes(); + + return new HypermediaWebClientConfigurer(withGenericJsonTypes); } @Bean diff --git a/src/main/java/org/springframework/hateoas/config/WebConverters.java b/src/main/java/org/springframework/hateoas/config/WebConverters.java index b879a0b8..4d26abee 100644 --- a/src/main/java/org/springframework/hateoas/config/WebConverters.java +++ b/src/main/java/org/springframework/hateoas/config/WebConverters.java @@ -15,15 +15,15 @@ */ package org.springframework.hateoas.config; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.function.Function; import java.util.stream.Collectors; +import java.util.stream.Stream; -import org.springframework.hateoas.RepresentationModel; -import org.springframework.hateoas.server.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.util.Assert; import com.fasterxml.jackson.databind.ObjectMapper; @@ -35,7 +35,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ class WebConverters { - private final List> converters; + private static MediaType ANY_JSON = MediaType.parseMediaType("application/*+json"); + + private final List infos; + private final ObjectMapper mapper; /** * Creates a new {@link WebConverters} from the given {@link ObjectMapper} and {@link HypermediaMappingInformation}s. @@ -45,9 +48,8 @@ class WebConverters { */ private WebConverters(ObjectMapper mapper, List mappingInformation) { - this.converters = mappingInformation.stream() // - .map(it -> createMessageConverter(it, it.configureObjectMapper(mapper.copy()))) // - .collect(Collectors.toList()); + this.mapper = mapper; + this.infos = mappingInformation; } /** @@ -65,46 +67,63 @@ class WebConverters { return new WebConverters(mapper, mappingInformations); } + List getSupportedMediaTypes() { + + return infos.stream() // + .flatMap(it -> it.getMediaTypes().stream()) + .collect(Collectors.toList()); + } + /** * 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) { + public void augmentServer(List> converters) { + augment(converters, false); + } + + public void augmentClient(List> converters) { + augment(converters, true); + } + + private void augment(List> converters, boolean includeGenericJsonTypes) { Assert.notNull(converters, "HttpMessageConverters must not be null!"); - this.converters.forEach(it -> converters.add(0, it)); - } + MappingJackson2HttpMessageConverter converter = converters.stream() + .filter(MappingJackson2HttpMessageConverter.class::isInstance) + .map(MappingJackson2HttpMessageConverter.class::cast) + .findFirst() + .orElseGet(() -> new MappingJackson2HttpMessageConverter(mapper)); - /** - * 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) { + ObjectMapper first = null; - Assert.notNull(converters, "HttpMessageConverters must not be null!"); + for (HypermediaMappingInformation info : infos) { - List> result = new ArrayList<>(this.converters); - result.addAll(converters); + Class rootType = info.getRootType(); + ObjectMapper objectMapper = info.configureObjectMapper(mapper.copy()); - return result; - } + if (first == null) { + first = objectMapper; + } - /** - * 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) { + Map mappers = info.getMediaTypes().stream().distinct() + .collect(Collectors.toMap(Function.identity(), __ -> objectMapper)); - return new TypeConstrainedMappingJackson2HttpMessageConverter(type.getRootType(), type.getMediaTypes(), - type.configureObjectMapper(mapper)); + converter.registerObjectMappersForType(rootType, map -> map.putAll(mappers)); + } + + if (!includeGenericJsonTypes) { + return; + } + + Class rootType = infos.get(0).getRootType(); + ObjectMapper mapper = first; + + converter.registerObjectMappersForType(rootType, map -> { + Stream.of(MediaType.APPLICATION_JSON, ANY_JSON) + .forEach(it -> map.put(it, mapper)); + }); } } diff --git a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java index 21038bdd..a6566abe 100644 --- a/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebFluxHateoasConfiguration.java @@ -15,21 +15,14 @@ */ package org.springframework.hateoas.config; -import java.util.ArrayList; import java.util.List; +import java.util.Optional; 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; -import org.springframework.util.MimeType; import org.springframework.web.filter.reactive.ServerWebExchangeContextFilter; import org.springframework.web.reactive.config.WebFluxConfigurer; @@ -45,19 +38,17 @@ import com.fasterxml.jackson.databind.ObjectMapper; @Configuration(proxyBeanMethods = false) class WebFluxHateoasConfiguration { - @Bean - WebFluxCodecs hypermediaConverters(ObjectProvider mapper, - List mappingInformation) { - return new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation); - } - @Bean HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider mapper, - List mappingInformation) { + List mappingInformation, + Optional comparator) { - WebFluxCodecs codecs = new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation); + comparator.ifPresent(mappingInformation::sort); - return new HypermediaWebFluxConfigurer(codecs); + WebfluxCodecCustomizer customizer = new WebfluxCodecCustomizer(mappingInformation, + mapper.getIfAvailable(ObjectMapper::new)); + + return new HypermediaWebFluxConfigurer(customizer); } @Bean @@ -75,10 +66,10 @@ class WebFluxHateoasConfiguration { */ static class HypermediaWebFluxConfigurer implements WebFluxConfigurer { - private final WebFluxCodecs codecs; + private final WebfluxCodecCustomizer customizer; - public HypermediaWebFluxConfigurer(WebFluxCodecs codecs) { - this.codecs = codecs; + public HypermediaWebFluxConfigurer(WebfluxCodecCustomizer customizer) { + this.customizer = customizer; } /** @@ -90,42 +81,7 @@ class WebFluxHateoasConfiguration { */ @Override public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) { - codecs.registerCodecs(configurer.customCodecs()); - } - } - - private static class WebFluxCodecs { - - 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::registerWithDefaultConfig); - encoders.forEach(codecs::registerWithDefaultConfig); - } - - 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])); + configurer.defaultCodecs().configureDefaultCodec(customizer); } } } diff --git a/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java index 0af5cd06..44c04472 100644 --- a/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebMvcHateoasConfiguration.java @@ -91,7 +91,7 @@ class WebMvcHateoasConfiguration { */ @Override public void extendMessageConverters(List> converters) { - hypermediaConverters.augment(converters); + hypermediaConverters.augmentServer(converters); } } diff --git a/src/main/java/org/springframework/hateoas/config/WebfluxCodecCustomizer.java b/src/main/java/org/springframework/hateoas/config/WebfluxCodecCustomizer.java new file mode 100644 index 00000000..e1a1025c --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/WebfluxCodecCustomizer.java @@ -0,0 +1,99 @@ +/* + * Copyright 2021 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.List; +import java.util.function.Consumer; +import java.util.stream.Stream; + +import org.springframework.http.MediaType; +import org.springframework.http.codec.json.Jackson2CodecSupport; +import org.springframework.lang.Nullable; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Oliver Drotbohm + */ +class WebfluxCodecCustomizer implements Consumer { + + private static final MediaType ANY_JSON = MediaType.parseMediaType("application/*+json"); + + private final List mappingInformations; + private final ObjectMapper mapper; + private final boolean withGenericJsonTypes; + + /** + * @param mappingInformations + * @param mapper + */ + public WebfluxCodecCustomizer(List mappingInformations, ObjectMapper mapper) { + this(mappingInformations, mapper, false); + } + + private WebfluxCodecCustomizer(List mappingInformations, ObjectMapper mapper, + boolean withGenericJsonTypes) { + + this.mappingInformations = mappingInformations; + this.mapper = mapper; + this.withGenericJsonTypes = withGenericJsonTypes; + } + + WebfluxCodecCustomizer withGenericJsonTypes() { + return new WebfluxCodecCustomizer(mappingInformations, mapper, true); + } + + /* + * (non-Javadoc) + * @see java.util.function.Consumer#accept(java.lang.Object) + */ + @Override + public void accept(@Nullable Object it) { + + if (it == null || !Jackson2CodecSupport.class.isInstance(it)) { + return; + } + + Jackson2CodecSupport codec = (Jackson2CodecSupport) it; + ObjectMapper firstMapper = null; + + for (HypermediaMappingInformation information : mappingInformations) { + + ObjectMapper objectMapper = information.configureObjectMapper(mapper.copy()); + + if (firstMapper == null) { + firstMapper = objectMapper; + } + + for (MediaType mediaType : information.getMediaTypes()) { + codec.registerObjectMappersForType(information.getRootType(), map -> { + map.put(mediaType, objectMapper); + }); + } + } + + if (!withGenericJsonTypes) { + return; + } + + Class type = mappingInformations.get(0).getRootType(); + ObjectMapper mapper = firstMapper; + + codec.registerObjectMappersForType(type, map -> { + Stream.of(MediaType.APPLICATION_JSON, ANY_JSON).forEach(mediaType -> map.put(mediaType, mapper)); + }); + } +} diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java index cbbea52e..43d185fd 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java @@ -33,7 +33,7 @@ import com.fasterxml.jackson.databind.Module; * @author Greg Turnquist * @author Oliver Drotbohm */ -@Configuration +@Configuration(proxyBeanMethods = false) class CollectionJsonMediaTypeConfiguration implements HypermediaMappingInformation { @Bean diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java index 3e948730..c544ffc7 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java @@ -33,7 +33,7 @@ import com.fasterxml.jackson.databind.Module; * @author Greg Turnquist * @author Oliver Drotbohm */ -@Configuration +@Configuration(proxyBeanMethods = false) class UberMediaTypeConfiguration implements HypermediaMappingInformation { @Bean diff --git a/src/test/java/org/springframework/hateoas/config/CustomHypermediaWebFluxTest.java b/src/test/java/org/springframework/hateoas/config/CustomHypermediaWebFluxTest.java index 00e4d095..5617fd08 100644 --- a/src/test/java/org/springframework/hateoas/config/CustomHypermediaWebFluxTest.java +++ b/src/test/java/org/springframework/hateoas/config/CustomHypermediaWebFluxTest.java @@ -56,7 +56,8 @@ class CustomHypermediaWebFluxTest { this.testClient = WebTestClient.bindToApplicationContext(ctx).build() // .mutate() // - .exchangeStrategies(it -> it.codecs(webClientConfigurer.configurer)) // + .exchangeStrategies( + it -> it.codecs(inner -> inner.defaultCodecs().configureDefaultCodec(webClientConfigurer.customizer))) // .build(); } diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index d9807d9b..c14182a6 100755 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -53,11 +53,12 @@ import org.springframework.hateoas.server.EntityLinks; import org.springframework.hateoas.server.LinkRelationProvider; import org.springframework.hateoas.server.core.DelegatingEntityLinks; import org.springframework.hateoas.server.core.DelegatingLinkRelationProvider; -import org.springframework.hateoas.server.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.lang.Nullable; +import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.ReflectionUtils; import org.springframework.web.client.RestTemplate; @@ -69,8 +70,6 @@ import org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConv import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.ObjectMapper; /** * Integration tests for {@link EnableHypermediaSupport}. @@ -195,335 +194,110 @@ class EnableHypermediaSupportIntegrationTest { assertUberSetupForConfigClass(ExtendedUberConfig.class); } - /** - * @see #134, #219 - */ @Test - @SuppressWarnings("unchecked") - void halSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { - - withServletContext(HalConfig.class, context -> { - - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()) // - .contains(MediaTypes.HAL_JSON); - - boolean found = false; - - for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) { - - if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { - - found = true; - - AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; - List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); - - assertThat(converters.get(0)).isInstanceOfSatisfying(TypeConstrainedMappingJackson2HttpMessageConverter.class, - it -> assertThat(it.getSupportedMediaTypes()).contains(MediaTypes.HAL_JSON)); - } - } - - assertThat(found).isTrue(); - }); - } - - @Test - @SuppressWarnings("unchecked") - void halFormsSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { - - withServletContext(HalFormsConfig.class, context -> { - - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) - .contains(MediaTypes.HAL_FORMS_JSON); - - boolean found = false; - - for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) { - - if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { - - found = true; - - AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; - List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); - - assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class); - assertThat(converters.get(0).getSupportedMediaTypes()).hasSize(1).contains(MediaTypes.HAL_FORMS_JSON); - } - } - - assertThat(found).isTrue(); - }); - } - - @Test - @SuppressWarnings("unchecked") void collectionJsonSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { withServletContext(CollectionJsonConfig.class, context -> { + assertMediaTypeSupported(context, MediaTypes.COLLECTION_JSON, RepresentationModel.class); + }); + } - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + private static Object assertMediaTypeSupported(ApplicationContext context, MediaType mediaType, Class type) { + return assertMediaTypeSupported(context, mediaType, type, null); + } - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) - .contains(MediaTypes.COLLECTION_JSON); + @Nullable + private static String assertMediaTypeSupported(ApplicationContext context, MediaType mediaType, Class type, + @Nullable Object source) { - boolean found = false; + context.getBeanProvider(RestTemplate.class).ifAvailable(it -> { + assertMediaTypeSupported(it.getMessageConverters(), mediaType, type, source); + }); - for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) { + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { + boolean found = false; - found = true; + for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) { - AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; - List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); + if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { - assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class); - assertThat(converters.get(0).getSupportedMediaTypes()).hasSize(1).contains(MediaTypes.COLLECTION_JSON); - } + found = true; + + AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; + List> converters = (List>) ReflectionTestUtils + .getField(processor, "messageConverters"); + + assertMediaTypeSupported(converters, MediaTypes.HAL_FORMS_JSON, RepresentationModel.class); } - - assertThat(found).isTrue(); - }); - } - - @Test - @SuppressWarnings("unchecked") - void uberSetupIsAppliedToAllTransitiveComponentsInRequestMappingHandlerAdapter() { - - withServletContext(UberConfig.class, context -> { - - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); - - assertThat(adapter.getMessageConverters().get(0).getSupportedMediaTypes()) // - .hasSize(1) // - .contains(MediaTypes.UBER_JSON); - - boolean found = false; - - for (HandlerMethodArgumentResolver resolver : getResolvers(adapter)) { - - if (resolver instanceof AbstractMessageConverterMethodArgumentResolver) { - - found = true; - - AbstractMessageConverterMethodArgumentResolver processor = (AbstractMessageConverterMethodArgumentResolver) resolver; - List> converters = (List>) ReflectionTestUtils - .getField(processor, "messageConverters"); - - assertThat(converters.get(0)).isInstanceOf(TypeConstrainedMappingJackson2HttpMessageConverter.class); - assertThat(converters.get(0).getSupportedMediaTypes()) // - .hasSize(1) // - .contains(MediaTypes.UBER_JSON); - } - } - - assertThat(found).isTrue(); - }); - } - - /** - * @see #293 - */ - @Test - void registersHalHttpMessageConvertersForRestTemplate() { - - withServletContext(HalConfig.class, context -> { - - RestTemplate template = context.getBean(RestTemplate.class); - - assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()) // - .contains(MediaTypes.HAL_JSON); - }); - } - - @Test - void registersHalFormsHttpMessageConvertersForRestTemplate() { - - withServletContext( // - HalFormsConfig.class, // - context -> 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); } + + assertThat(found).isTrue(); + + return assertMediaTypeSupported(adapter.getMessageConverters(), mediaType, type, source); } - 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(AbstractJackson2HttpMessageConverter::getObjectMapper); - - if (!result.isPresent()) { - fail("Couldn't find ObjectMapper from HttpMessageConverter supporting " + mediaType); - } - - return result.orElseThrow(IllegalStateException::new); - }; - - foo(context, RequestMappingHandlerAdapter.class, mapper, consumer); + @Nullable + private static Object assertMediaTypeSupported(List> converters, MediaType mediaType, + Class type) { + return assertMediaTypeSupported(converters, mediaType, type, null); } - interface ThrowingConsumer { - void accept(T source) throws Throwable; - } + @Nullable + private static String assertMediaTypeSupported(List> converters, MediaType mediaType, + Class type, @Nullable Object source) { - @Test - void registersCollectionJsonHttpMessageConvertersForRestTemplate() { + Optional result = converters.stream()// + .filter(AbstractJackson2HttpMessageConverter.class::isInstance) // + .findFirst() // + .map(AbstractJackson2HttpMessageConverter.class::cast); - withServletContext(CollectionJsonConfig.class, context -> { - RestTemplate template = context.getBean(RestTemplate.class); - - assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()).hasSize(1) - .contains(MediaTypes.COLLECTION_JSON); + assertThat(result).hasValueSatisfying(it -> { + assertThat(it.getSupportedMediaTypes(type)); }); - } - @Test - void registersUberHttpMessageConvertersForRestTemplate() { + if (source == null) { + return null; + } - withServletContext(UberConfig.class, context -> { - RestTemplate template = context.getBean(RestTemplate.class); + HttpMessageConverter converter = result.get(); + MockHttpOutputMessage message = new MockHttpOutputMessage(); - assertThat(template.getMessageConverters().get(0).getSupportedMediaTypes()) // - .hasSize(1) // - .contains(MediaTypes.UBER_JSON); - }); - } + assertThatCode(() -> converter.write(source, mediaType, message)).doesNotThrowAnyException(); - /** - * @see #341 - */ - @Test - void configuresDefaultObjectMapperForHalToIgnoreUnknownProperties() { - - withServletContext( // - HalConfig.class, // - context -> assertObjectMapper( // - context, // - MediaTypes.HAL_JSON, // - mapper -> assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) // - .isFalse() // - ) // - ); - } - - /** - * @see #341 - */ - @Test - void configuresDefaultObjectMapperForHalFormsToIgnoreUnknownProperties() { - - withServletContext( // - HalFormsConfig.class, // - context -> assertObjectMapper( // - context, // - MediaTypes.HAL_FORMS_JSON, // - mapper -> assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) // - .isFalse() // - ) // - ); - } - - @Test - void configuresDefaultObjectMapperForCollectionJsonToIgnoreUnknownProperties() { - - withServletContext( // - CollectionJsonConfig.class, // - context -> assertObjectMapper( // - context, // - MediaTypes.COLLECTION_JSON, // - mapper -> assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) // - .isFalse() // - ) // - ); - } - - @Test - void configuresDefaultObjectMapperForUberToIgnoreUnknownProperties() { - - withServletContext( // - UberConfig.class, // - context -> assertObjectMapper( // - context, // - MediaTypes.UBER_JSON, // - mapper -> assertThat(mapper.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)) // - .isFalse() // - ) // - ); + return message.getBodyAsString(); } @Test void verifyDefaultHalConfigurationRendersSingleItemAsSingleItem() throws JsonProcessingException { + RepresentationModel resourceSupport = new RepresentationModel<>(); + resourceSupport.add(Link.of("localhost").withSelfRel()); + withServletContext(HalConfig.class, context -> { - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + assertMediaTypeSupported(context.getBean(RestTemplate.class).getMessageConverters(), MediaTypes.HAL_FORMS_JSON, + RepresentationModel.class); - Optional mapper = adapter.getMessageConverters().stream() // - .filter(it -> it.getSupportedMediaTypes().contains(MediaType.parseMediaType("application/hal+json"))) // - .findFirst() // - .map(AbstractJackson2HttpMessageConverter.class::cast) // - .map(AbstractJackson2HttpMessageConverter::getObjectMapper); + String result = assertMediaTypeSupported(context, MediaTypes.HAL_JSON, RepresentationModel.class, + resourceSupport); - assertThat(mapper).hasValueSatisfying(it -> { - - RepresentationModel resourceSupport = new RepresentationModel<>(); - resourceSupport.add(Link.of("localhost").withSelfRel()); - - assertThatCode(() -> { - assertThat(it.writeValueAsString(resourceSupport)) // - .isEqualTo("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"); - }).doesNotThrowAnyException(); - }); + assertThat(result).isEqualTo("{\"_links\":{\"self\":{\"href\":\"localhost\"}}}"); }); } @Test void verifyRenderSingleLinkAsArrayViaOverridingBean() { - withServletContext( // - RenderLinkAsSingleLinksConfig.class, // - context -> assertObjectMapper( // - context, // - MediaTypes.HAL_JSON, // - mapper -> { // - RepresentationModel resourceSupport = new RepresentationModel<>(); // - resourceSupport.add(Link.of("localhost").withSelfRel()); // - assertThat(mapper.writeValueAsString(resourceSupport)) // - .isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}"); // - } // - ) // - ); + RepresentationModel model = new RepresentationModel<>(); // + model.add(Link.of("localhost").withSelfRel()); // + + withServletContext(RenderLinkAsSingleLinksConfig.class, context -> { + + String result = assertMediaTypeSupported(context, MediaTypes.HAL_JSON, RepresentationModel.class, model); + + assertThat(result).isEqualTo("{\"_links\":{\"self\":[{\"href\":\"localhost\"}]}}"); // + }); } @Test // #1019 @@ -571,6 +345,31 @@ class EnableHypermediaSupportIntegrationTest { }); } + /* + * HAL FORMS, UBER + * RepresentationModel -> hal-forms, application/json, application/*+json + * RepresentationModel -> uber, application/json, application/*+json + * + * hal-forms, uber, application/json, application/*+json + * + * + * + * + * + */ + + @Test + void ordersMediaTypeIntegrationBasedOnConfiguration() { + + withServletContext(MediaTypeOrdering.class, context -> { + + WebConverters converters = context.getBean(WebConverters.class); + + assertThat(converters.getSupportedMediaTypes()) // + .containsExactly(MediaTypes.UBER_JSON, MediaTypes.HAL_FORMS_JSON); + }); + } + private static void assertEntityLinksSetUp(ApplicationContext context) { assertThat(context.getBeansOfType(EntityLinks.class).values()) // @@ -602,10 +401,7 @@ class EnableHypermediaSupportIntegrationTest { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(HalFormsLinkDiscoverer.class); - - RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); - assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class); - + assertMediaTypeSupported(context, MediaTypes.HAL_FORMS_JSON, RepresentationModel.class); }); } @@ -615,9 +411,7 @@ class EnableHypermediaSupportIntegrationTest { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(CollectionJsonLinkDiscoverer.class); - - RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); - assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class); + assertMediaTypeSupported(context, MediaTypes.COLLECTION_JSON, RepresentationModel.class); }); } @@ -627,9 +421,7 @@ class EnableHypermediaSupportIntegrationTest { assertEntityLinksSetUp(context); assertThat(context.getBean(LinkDiscoverer.class)).isInstanceOf(UberLinkDiscoverer.class); - - RequestMappingHandlerAdapter rmha = context.getBean(RequestMappingHandlerAdapter.class); - assertThat(rmha.getMessageConverters().get(0)).isInstanceOf(MappingJackson2HttpMessageConverter.class); + assertMediaTypeSupported(context, MediaTypes.UBER_JSON, RepresentationModel.class); }); } @@ -790,4 +582,10 @@ class EnableHypermediaSupportIntegrationTest { static class HalAndHalFormsConfig { } + + @Configuration + @EnableHypermediaSupport(type = { HypermediaType.UBER, HypermediaType.HAL_FORMS }) + static class MediaTypeOrdering { + + } } diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java index 8da41f41..dba786d8 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java @@ -16,22 +16,22 @@ package org.springframework.hateoas.config; import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import static org.springframework.hateoas.mediatype.MediaTypeTestUtils.*; import static org.springframework.hateoas.support.ContextTester.*; -import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; +import java.util.function.Function; import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.config.RestTemplateHateoasConfiguration.HypermediaRestTemplateBeanPostProcessor; import org.springframework.hateoas.support.CustomHypermediaType; import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractHttpMessageConverter; -import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; -import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.client.RestTemplate; /** @@ -41,6 +41,9 @@ import org.springframework.web.client.RestTemplate; */ class HypermediaRestTemplateBeanPostProcessorTest { + static final Function>> REST_TEMPLATE_EXTRACTOR = it -> it + .getBean(RestTemplate.class).getMessageConverters(); + /** * @see #728 */ @@ -49,7 +52,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { withContext(HalConfig.class, context -> { - assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class))) // + assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // MediaType.APPLICATION_JSON, // @@ -65,7 +68,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { withContext(HalAndCollectionJsonConfig.class, context -> { - assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class))) // + assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // MediaTypes.COLLECTION_JSON, // @@ -82,7 +85,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { withContext(AllHypermediaConfig.class, context -> { - assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class))) // + assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // MediaTypes.HAL_FORMS_JSON, // @@ -98,7 +101,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { withContext(CustomHypermediaConfig.class, context -> { - assertThat(lookupSupportedHypermediaTypes(context.getBean(RestTemplate.class))) // + assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // MediaType.parseMediaType("application/frodo+json"), // @@ -108,13 +111,6 @@ class HypermediaRestTemplateBeanPostProcessorTest { }); } - private List lookupSupportedHypermediaTypes(RestTemplate restTemplate) { - - return restTemplate.getMessageConverters().stream().filter(MappingJackson2HttpMessageConverter.class::isInstance) - .map(AbstractJackson2HttpMessageConverter.class::cast).map(AbstractHttpMessageConverter::getSupportedMediaTypes) - .flatMap(Collection::stream).collect(Collectors.toList()); - } - static class BaseConfig { @Bean diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java index d427939b..70a2f3b9 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateConfigurerTest.java @@ -1,7 +1,23 @@ +/* + * Copyright 2021 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 static org.assertj.core.api.Assertions.*; import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*; +import static org.springframework.hateoas.mediatype.MediaTypeTestUtils.*; import static org.springframework.hateoas.support.ContextTester.*; import java.util.Collections; @@ -10,7 +26,6 @@ import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; import org.springframework.hateoas.MediaTypes; import org.springframework.http.MediaType; -import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.client.RestTemplate; public class HypermediaRestTemplateConfigurerTest { @@ -23,10 +38,9 @@ public class HypermediaRestTemplateConfigurerTest { withContext(HalConfig.class, context -> { HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); - RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); - assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) + assertThat(getSupportedHypermediaTypes(restTemplate.getMessageConverters())) // .contains(MediaTypes.HAL_JSON) // .doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); }); @@ -38,10 +52,9 @@ public class HypermediaRestTemplateConfigurerTest { withContext(AllHypermediaConfig.class, context -> { HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); - RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); - assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) + assertThat(getSupportedHypermediaTypes(restTemplate.getMessageConverters())) // .contains(MediaTypes.HAL_JSON, MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); }); } @@ -52,11 +65,10 @@ public class HypermediaRestTemplateConfigurerTest { withContext(CustomHypermediaConfig.class, context -> { HypermediaRestTemplateConfigurer configurer = context.getBean(HypermediaRestTemplateConfigurer.class); - RestTemplate restTemplate = configurer.registerHypermediaTypes(new RestTemplate()); - assertThat(restTemplate.getMessageConverters()).flatExtracting(HttpMessageConverter::getSupportedMediaTypes) - .contains(MediaTypes.HAL_JSON, FRODO_JSON) + assertThat(getSupportedHypermediaTypes(restTemplate.getMessageConverters())) // + .contains(MediaTypes.HAL_JSON, FRODO_JSON) // .doesNotContain(MediaTypes.HAL_FORMS_JSON, MediaTypes.COLLECTION_JSON, MediaTypes.UBER_JSON); }); } diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaWebClientConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaWebClientConfigurerTest.java index b5d8b8a9..f84c23f2 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaWebClientConfigurerTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaWebClientConfigurerTest.java @@ -1,7 +1,23 @@ +/* + * Copyright 2021 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 static org.assertj.core.api.Assertions.*; import static org.springframework.hateoas.MediaTypes.*; +import static org.springframework.hateoas.mediatype.MediaTypeTestUtils.*; import static org.springframework.hateoas.support.ContextTester.*; import java.util.Collections; @@ -9,11 +25,8 @@ import java.util.Collections; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Bean; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.mediatype.MediaTypeTestUtils; import org.springframework.http.MediaType; -import org.springframework.http.codec.HttpMessageReader; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.test.web.reactive.server.WebTestClient; -import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; public class HypermediaWebClientConfigurerTest { @@ -26,11 +39,9 @@ public class HypermediaWebClientConfigurerTest { withContext(HalConfig.class, context -> { HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class); - WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build(); - assertThat(exchangeStrategies(webClient).messageReaders()) - .flatExtracting(HttpMessageReader::getReadableMediaTypes) // + assertThat(getSupportedHypermediaTypes(webClient)) // .contains(HAL_JSON) // .doesNotContain(HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON); }); @@ -42,11 +53,9 @@ public class HypermediaWebClientConfigurerTest { withContext(AllHypermediaConfig.class, context -> { HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class); - WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build(); - assertThat(exchangeStrategies(webClient).messageReaders()) - .flatExtracting(HttpMessageReader::getReadableMediaTypes) // + assertThat(getSupportedHypermediaTypes(webClient)) // .contains(HAL_JSON, HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON); }); } @@ -57,29 +66,14 @@ public class HypermediaWebClientConfigurerTest { withContext(CustomHypermediaConfig.class, context -> { HypermediaWebClientConfigurer configurer = context.getBean(HypermediaWebClientConfigurer.class); - WebClient webClient = configurer.registerHypermediaTypes(WebClient.builder()).build(); - assertThat(exchangeStrategies(webClient).messageReaders()) - .flatExtracting(HttpMessageReader::getReadableMediaTypes) // + assertThat(MediaTypeTestUtils.getSupportedHypermediaTypes(webClient)) // .contains(HAL_JSON, FRODO_JSON) // .doesNotContain(HAL_FORMS_JSON, COLLECTION_JSON, UBER_JSON); }); } - /** - * Extract the {@link ExchangeStrategies} from a {@link WebTestClient} to assert it has the proper message readers and - * writers. - * - * @param webClient - * @return - */ - private static ExchangeStrategies exchangeStrategies(WebClient webClient) { - - return (ExchangeStrategies) ReflectionTestUtils - .getField(ReflectionTestUtils.getField(webClient, "exchangeFunction"), "strategies"); - } - @EnableHypermediaSupport(type = HypermediaType.HAL) static class HalConfig { diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java index 078d6d2d..bfb60666 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaWebFluxConfigurerTest.java @@ -17,6 +17,7 @@ package org.springframework.hateoas.config; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; +import static org.hamcrest.CoreMatchers.*; import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*; import static org.springframework.hateoas.server.reactive.WebFluxLinkBuilder.*; @@ -261,7 +262,7 @@ class HypermediaWebFluxConfigurerTest { } /** - * When requesting an unregistered media type, fallback to Spring Framework's default JSON handler. + * When requesting an unregistered media type, expect a 406 Not Acceptable. * * @see #728 */ @@ -272,10 +273,9 @@ class HypermediaWebFluxConfigurerTest { this.testClient.get().uri("/").accept(MediaTypes.UBER_JSON) // .exchange() // - .expectStatus().isOk() // + .expectStatus().value(is(406)) .returnResult(String.class).getResponseBody() // .as(StepVerifier::create) // - .expectNext("{\"links\":[{\"rel\":\"self\",\"href\":\"/\"},{\"rel\":\"employees\",\"href\":\"/employees\"}]}") .verifyComplete(); } @@ -316,10 +316,14 @@ class HypermediaWebFluxConfigurerTest { return true; }).verifyComplete(); - this.testClient.get().uri("/reactive/employees/1").accept(MediaTypes.HAL_JSON).exchange() // - .expectStatus().isOk().expectHeader().contentType(MediaTypes.HAL_JSON) // + this.testClient.get() // + .uri("/reactive/employees/1") // + .accept(MediaTypes.HAL_JSON).exchange() // + .expectStatus().isOk() // + .expectHeader().contentType(MediaTypes.HAL_JSON) // .returnResult(this.resourceEmployeeType).getResponseBody() // - .as(StepVerifier::create).expectNextMatches(employee -> { + .as(StepVerifier::create) // + .expectNextMatches(employee -> { assertThat(employee.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer")); assertThat(employee.getLinks()) // @@ -336,8 +340,8 @@ class HypermediaWebFluxConfigurerTest { this.testClient.get().uri("/sample/4711").exchange() // .expectStatus().isEqualTo(HttpStatus.I_AM_A_TEAPOT) // - .returnResult(String.class).getResponseBody() - .as(StepVerifier::create) + .returnResult(String.class).getResponseBody() // + .as(StepVerifier::create) // .expectNextMatches(it -> { assertThat(it).isEqualTo("/sample/sample"); @@ -356,7 +360,8 @@ class HypermediaWebFluxConfigurerTest { this.testClient.get().uri("/").accept(requestType).exchange() // .expectStatus().isOk() // .expectHeader().contentType(responseType) // - .returnResult(RepresentationModel.class).getResponseBody().as(StepVerifier::create) + .returnResult(RepresentationModel.class) // + .getResponseBody().as(StepVerifier::create) // .expectNextMatches(resourceSupport -> { assertThat(resourceSupport.getLinks()) // diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaWebMvcConfigurerTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaWebMvcConfigurerTest.java index 4d295bad..4debeb2b 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaWebMvcConfigurerTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaWebMvcConfigurerTest.java @@ -245,22 +245,13 @@ class HypermediaWebMvcConfigurerTest { verifyCreatingNewEntityWorks(MediaTypes.UBER_JSON); } - /** - * When requesting an unregistered media type, fallback to Spring Framework's default JSON handler. - */ @Test void callingForUnregisteredMediaTypeShouldFallBackToDefaultHandler() throws Exception { setUp(HalWebMvcConfig.class); - String unformattedJson = this.mockMvc.perform(get("/").accept(MediaTypes.UBER_JSON)) // - .andExpect(status().isOk()) // - .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.UBER_JSON.toString())) // - .andReturn() // - .getResponse().getContentAsString(); // - - assertThat(unformattedJson) - .isEqualTo("{\"links\":[{\"rel\":\"self\",\"href\":\"/\"},{\"rel\":\"employees\",\"href\":\"/employees\"}]}"); + this.mockMvc.perform(get("/").accept(MediaTypes.UBER_JSON)) + .andExpect(status().isNotAcceptable()); } @Test // #118 diff --git a/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java b/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java index 8b667bf4..7cd519b7 100644 --- a/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java +++ b/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java @@ -17,11 +17,20 @@ package org.springframework.hateoas.mediatype; import java.util.Collections; import java.util.List; +import java.util.function.Function; import org.springframework.context.ApplicationContext; +import org.springframework.core.ResolvableType; import org.springframework.hateoas.RepresentationModel; import org.springframework.http.MediaType; +import org.springframework.http.codec.DecoderHttpMessageReader; +import org.springframework.http.codec.json.Jackson2JsonDecoder; +import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.web.reactive.function.client.ExchangeStrategies; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; /** @@ -52,13 +61,61 @@ public class MediaTypeTestUtils { */ public static List getSupportedHypermediaTypes(ApplicationContext context, Class type) { - RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + return getSupportedHypermediaTypes(context, + it -> it.getBean(RequestMappingHandlerAdapter.class).getMessageConverters(), type); + } - return adapter.getMessageConverters().stream() // + public static List getSupportedHypermediaTypes(ApplicationContext context, + Function>> extractor) { + return getSupportedHypermediaTypes(context, extractor, RepresentationModel.class); + } + + public static List getSupportedHypermediaTypes(ApplicationContext context, + Function>> extractor, Class type) { + return getSupportedHypermediaTypes(extractor.apply(context), type); + } + + public static List getSupportedHypermediaTypes(List> converters) { + return getSupportedHypermediaTypes(converters, RepresentationModel.class); // + } + + public static List getSupportedHypermediaTypes(List> converters, Class type) { + + return converters.stream() // .filter(MappingJackson2HttpMessageConverter.class::isInstance) // .map(MappingJackson2HttpMessageConverter.class::cast) // .findFirst() // .map(it -> it.getSupportedMediaTypes(type)) // .orElseGet(() -> Collections.emptyList()); // } + + public static List getSupportedHypermediaTypes(WebClient client) { + return getSupportedHypermediaTypes(client, RepresentationModel.class); // + } + + @SuppressWarnings("unchecked") + public static List getSupportedHypermediaTypes(WebClient client, Class type) { + + return exchangeStrategies(client).messageReaders().stream() // + .filter(DecoderHttpMessageReader.class::isInstance) // + .map(DecoderHttpMessageReader.class::cast) // + .filter(it -> Jackson2JsonDecoder.class.isInstance(it.getDecoder())) + .findFirst() // + .map(it -> it.getReadableMediaTypes(ResolvableType.forClass(type))) // + .orElseGet(() -> Collections.emptyList()); + } + + /** + * Extract the {@link ExchangeStrategies} from a {@link WebTestClient} to assert it has the proper message readers and + * writers. + * + * @param webClient + * @return + */ + @SuppressWarnings("null") + private static ExchangeStrategies exchangeStrategies(WebClient webClient) { + + return (ExchangeStrategies) ReflectionTestUtils + .getField(ReflectionTestUtils.getField(webClient, "exchangeFunction"), "strategies"); + } }