#1453 - Switch to new converter / codec registration APIs in Spring 5.3.4.

Changed the configuration of media type specific representation model rendering to rather use the existing MappingJackson2JsonConverter than registering custom HttpMessageConverters ourselves. The same applies to codecs in WebFlux.

Added HypermediaMappingInformationComparator to be able to sort HypermediaMappingInformation instances by their corresponding media type configuration on @EnableHypermediaSupport.
This commit is contained in:
Oliver Drotbohm
2021-01-26 20:03:02 +01:00
parent 2a0b113a3d
commit c561822a45
21 changed files with 514 additions and 499 deletions

View File

@@ -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<ObjectMapper> mapper,
List<HypermediaMappingInformation> information) {
List<HypermediaMappingInformation> information, Optional<HypermediaMappingInformationComparator> comparator) {
comparator.ifPresent(information::sort);
return WebConverters.of(mapper.getIfUnique(ObjectMapper::new), information);
}

View File

@@ -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<MediaTypeConfigurationProvider> configurationProviders = SpringFactoriesLoader.loadFactories(
MediaTypeConfigurationProvider.class, HypermediaConfigurationImportSelector.class.getClassLoader());

View File

@@ -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<HypermediaMappingInformation> {
private final List<MediaType> mediaTypes;
/**
* Creates a new {@link HypermediaMappingInformationComparator} using the given reference {@link MediaType}s.
*
* @param mediaTypes must not be {@literal null}.
*/
HypermediaMappingInformationComparator(List<MediaType> 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;
}
}

View File

@@ -44,7 +44,8 @@ public class HypermediaRestTemplateConfigurer {
*/
public RestTemplate registerHypermediaTypes(RestTemplate template) {
template.setMessageConverters(converters.and(template.getMessageConverters()));
converters.augmentClient(template.getMessageConverters());
return template;
}
}

View File

@@ -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<ClientCodecConfigurer> 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<HypermediaMappingInformation> 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);
});
}
}

View File

@@ -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<HypermediaMappingInformation> 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();
}
}

View File

@@ -42,7 +42,11 @@ class WebClientHateoasConfiguration {
@Lazy
HypermediaWebClientConfigurer webClientConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> hypermediaTypes) {
return new HypermediaWebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes);
WebfluxCodecCustomizer withGenericJsonTypes = new WebfluxCodecCustomizer(hypermediaTypes,
mapper.getIfAvailable(ObjectMapper::new)).withGenericJsonTypes();
return new HypermediaWebClientConfigurer(withGenericJsonTypes);
}
@Bean

View File

@@ -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<HttpMessageConverter<?>> converters;
private static MediaType ANY_JSON = MediaType.parseMediaType("application/*+json");
private final List<HypermediaMappingInformation> 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<HypermediaMappingInformation> 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<MediaType> 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<HttpMessageConverter<?>> converters) {
public void augmentServer(List<HttpMessageConverter<?>> converters) {
augment(converters, false);
}
public void augmentClient(List<HttpMessageConverter<?>> converters) {
augment(converters, true);
}
private void augment(List<HttpMessageConverter<?>> 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<HttpMessageConverter<?>> and(Collection<HttpMessageConverter<?>> converters) {
ObjectMapper first = null;
Assert.notNull(converters, "HttpMessageConverters must not be null!");
for (HypermediaMappingInformation info : infos) {
List<HttpMessageConverter<?>> 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<MediaType, ObjectMapper> 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));
});
}
}

View File

@@ -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<ObjectMapper> mapper,
List<HypermediaMappingInformation> mappingInformation) {
return new WebFluxCodecs(mapper.getIfAvailable(ObjectMapper::new), mappingInformation);
}
@Bean
HypermediaWebFluxConfigurer hypermediaWebFluxConfigurer(ObjectProvider<ObjectMapper> mapper,
List<HypermediaMappingInformation> mappingInformation) {
List<HypermediaMappingInformation> mappingInformation,
Optional<HypermediaMappingInformationComparator> 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<Decoder<?>> decoders;
private final List<Encoder<?>> encoders;
private WebFluxCodecs(ObjectMapper mapper, List<HypermediaMappingInformation> mappingInformation) {
this.decoders = new ArrayList<>();
this.encoders = new ArrayList<>();
for (HypermediaMappingInformation information : mappingInformation) {
ObjectMapper objectMapper = information.configureObjectMapper(mapper.copy());
List<MediaType> 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<MediaType> mediaTypes) {
return new Jackson2JsonDecoder(mapper, mediaTypes.toArray(new MimeType[0]));
}
private static Encoder<?> getEncoder(ObjectMapper mapper, List<MediaType> mediaTypes) {
return new Jackson2JsonEncoder(mapper, mediaTypes.toArray(new MimeType[0]));
configurer.defaultCodecs().configureDefaultCodec(customizer);
}
}
}

View File

@@ -91,7 +91,7 @@ class WebMvcHateoasConfiguration {
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
hypermediaConverters.augment(converters);
hypermediaConverters.augmentServer(converters);
}
}

View File

@@ -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<Object> {
private static final MediaType ANY_JSON = MediaType.parseMediaType("application/*+json");
private final List<HypermediaMappingInformation> mappingInformations;
private final ObjectMapper mapper;
private final boolean withGenericJsonTypes;
/**
* @param mappingInformations
* @param mapper
*/
public WebfluxCodecCustomizer(List<HypermediaMappingInformation> mappingInformations, ObjectMapper mapper) {
this(mappingInformations, mapper, false);
}
private WebfluxCodecCustomizer(List<HypermediaMappingInformation> 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));
});
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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();
}

View File

@@ -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<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) 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<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) 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<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) 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<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) 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<HttpMessageConverter<?>> converters = (List<HttpMessageConverter<?>>) 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 <T, S> void foo(ApplicationContext context, Class<T> beanType, Function<T, S> extractor,
ThrowingConsumer<S> 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 <T, S> void assertObjectMapper(ApplicationContext context, MediaType mediaType,
ThrowingConsumer<ObjectMapper> consumer) {
Function<RequestMappingHandlerAdapter, ObjectMapper> mapper = adapter -> {
Optional<ObjectMapper> 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<HttpMessageConverter<?>> converters, MediaType mediaType,
Class<?> type) {
return assertMediaTypeSupported(converters, mediaType, type, null);
}
interface ThrowingConsumer<T> {
void accept(T source) throws Throwable;
}
@Nullable
private static String assertMediaTypeSupported(List<HttpMessageConverter<?>> converters, MediaType mediaType,
Class<?> type, @Nullable Object source) {
@Test
void registersCollectionJsonHttpMessageConvertersForRestTemplate() {
Optional<AbstractJackson2HttpMessageConverter> 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<Object> 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<ObjectMapper> 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 {
}
}

View File

@@ -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<ApplicationContext, List<HttpMessageConverter<?>>> 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<MediaType> 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

View File

@@ -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);
});
}

View File

@@ -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 {

View File

@@ -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()) //

View File

@@ -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

View File

@@ -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<MediaType> 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<MediaType> getSupportedHypermediaTypes(ApplicationContext context,
Function<ApplicationContext, List<HttpMessageConverter<?>>> extractor) {
return getSupportedHypermediaTypes(context, extractor, RepresentationModel.class);
}
public static List<MediaType> getSupportedHypermediaTypes(ApplicationContext context,
Function<ApplicationContext, List<HttpMessageConverter<?>>> extractor, Class<?> type) {
return getSupportedHypermediaTypes(extractor.apply(context), type);
}
public static List<MediaType> getSupportedHypermediaTypes(List<HttpMessageConverter<?>> converters) {
return getSupportedHypermediaTypes(converters, RepresentationModel.class); //
}
public static List<MediaType> getSupportedHypermediaTypes(List<HttpMessageConverter<?>> 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<MediaType> getSupportedHypermediaTypes(WebClient client) {
return getSupportedHypermediaTypes(client, RepresentationModel.class); //
}
@SuppressWarnings("unchecked")
public static List<MediaType> 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");
}
}