From a8a8836898f385dea07df6539310ee615e0eb462 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Wed, 10 Feb 2021 16:18:57 +0100 Subject: [PATCH] #1253 - Allow custom media type aliases for HAL. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HalConfiguration now exposes a withMediaType(…) that adds custom media types in front of the default of `application/hal+json`. This allows developers to use a project specific media type which is treated like it being HAL in the first place. --- pom.xml | 16 ++--- .../mediatype/hal/HalConfiguration.java | 59 +++++++++++++++-- .../hal/HalMediaTypeConfiguration.java | 3 +- .../hateoas/mediatype/MediaTypeTestUtils.java | 64 +++++++++++++++++++ ...MediaTypeConfigurationIntegrationTest.java | 57 +++++++++++++++++ 5 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java create mode 100644 src/test/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationIntegrationTest.java diff --git a/pom.xml b/pom.xml index 04c04c5f..3bff5ad8 100644 --- a/pom.xml +++ b/pom.xml @@ -976,6 +976,13 @@ test + + javax.servlet + javax.servlet-api + 4.0.1 + provided + + net.jadler jadler-all @@ -1007,15 +1014,6 @@ test - - - - javax.servlet - javax.servlet-api - 3.1.0 - provided - - com.tngtech.archunit archunit diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java index e62d3774..4e14ec7b 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -15,13 +15,18 @@ */ package org.springframework.hateoas.mediatype.hal; +import java.util.ArrayList; +import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.Consumer; import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; +import org.springframework.hateoas.MediaTypes; +import org.springframework.http.MediaType; import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; import org.springframework.util.PathMatcher; @@ -45,6 +50,7 @@ public class HalConfiguration { private final RenderSingleLinks renderSingleLinks; private final Map singleLinksPerPattern; private final Consumer objectMapperCustomizer; + private final List mediaTypes; /** * Configures whether the Jackson property naming strategy is applied to link relations and within {@code _embedded} @@ -62,22 +68,26 @@ public class HalConfiguration { * Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document. */ public HalConfiguration() { - this(RenderSingleLinks.AS_SINGLE, new LinkedHashMap<>(), true, true, __ -> {}); + + this(RenderSingleLinks.AS_SINGLE, new LinkedHashMap<>(), true, true, __ -> {}, + Collections.singletonList(MediaTypes.HAL_JSON)); } private HalConfiguration(RenderSingleLinks renderSingleLinks, Map singleLinksPerPattern, boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections, - Consumer objectMapperCustomizer) { + Consumer objectMapperCustomizer, List mediaTypes) { Assert.notNull(renderSingleLinks, "RenderSingleLinks must not be null!"); Assert.notNull(singleLinksPerPattern, "Single links per pattern map must not be null!"); Assert.notNull(objectMapperCustomizer, "ObjectMapper customizer must not be null!"); + Assert.notNull(mediaTypes, "MediaTypes must not be null!"); this.renderSingleLinks = renderSingleLinks; this.singleLinksPerPattern = singleLinksPerPattern; this.applyPropertyNamingStrategy = applyPropertyNamingStrategy; this.enforceEmbeddedCollections = enforceEmbeddedCollections; this.objectMapperCustomizer = objectMapperCustomizer; + this.mediaTypes = mediaTypes; } /** @@ -144,7 +154,7 @@ public class HalConfiguration { return this.renderSingleLinks == renderSingleLinks // ? this // : new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, - enforceEmbeddedCollections, objectMapperCustomizer); + enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes); } /** @@ -160,7 +170,7 @@ public class HalConfiguration { return this.singleLinksPerPattern == singleLinksPerPattern // ? this // : new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, - enforceEmbeddedCollections, objectMapperCustomizer); + enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes); } /** @@ -175,7 +185,7 @@ public class HalConfiguration { return this.applyPropertyNamingStrategy == applyPropertyNamingStrategy // ? this // : new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, - enforceEmbeddedCollections, objectMapperCustomizer); + enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes); } /** @@ -190,15 +200,46 @@ public class HalConfiguration { return this.enforceEmbeddedCollections == enforceEmbeddedCollections // ? this // : new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, - enforceEmbeddedCollections, objectMapperCustomizer); + enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes); } + /** + * Configures an {@link ObjectMapper} customizer to tweak the instance after it has been pre-configured with all HAL + * specific setup. + * + * @param objectMapperCustomizer must not be {@literal null}. + * @return will never be {@literal null}. + */ public HalConfiguration withObjectMapperCustomizer(Consumer objectMapperCustomizer) { return this.objectMapperCustomizer == objectMapperCustomizer // ? this // : new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, - enforceEmbeddedCollections, objectMapperCustomizer); + enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes); + } + + /** + * Registers additional media types that are supposed to be aliases to {@link MediaTypes#HAL_JSON}. Registered + * {@link MediaType}s will be preferred over the default one, i.e. they'll be listed first in client's accept headers + * etc. + * + * @param mediaType must not be {@literal null}. + * @return will never be {@literal null}. + * @since 1.3 + */ + public HalConfiguration withMediaType(MediaType mediaType) { + + Assert.notNull(mediaType, "MediaType must not be null!"); + + if (mediaTypes.contains(mediaType)) { + return this; + } + + List newMediaTypes = new ArrayList<>(mediaTypes); + newMediaTypes.add(mediaTypes.size() - 1, mediaType); + + return new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy, + enforceEmbeddedCollections, objectMapperCustomizer, newMediaTypes); } public RenderSingleLinks getRenderSingleLinks() { @@ -220,6 +261,10 @@ public class HalConfiguration { return this; } + List getMediaTypes() { + return mediaTypes; + } + /** * Configuration option how to render single links of a given {@link LinkRelation}. * diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java index 02e83f32..ed03c631 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java @@ -23,7 +23,6 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.hateoas.client.LinkDiscoverer; -import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.hateoas.config.HypermediaMappingInformation; import org.springframework.hateoas.mediatype.MessageResolver; import org.springframework.hateoas.server.LinkRelationProvider; @@ -69,7 +68,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation { */ @Override public List getMediaTypes() { - return HypermediaType.HAL.getMediaTypes(); + return this.halConfiguration.getIfAvailable(HalConfiguration::new).getMediaTypes(); } /* diff --git a/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java b/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java new file mode 100644 index 00000000..8b667bf4 --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/MediaTypeTestUtils.java @@ -0,0 +1,64 @@ +/* + * 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.mediatype; + +import java.util.Collections; +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.hateoas.RepresentationModel; +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; + +/** + * Test utilities to verify configuration of media type support. + * + * @author Oliver Drotbohm + */ +public class MediaTypeTestUtils { + + /** + * Looks up the the media types supported for {@link RepresentationModel} in the {@link RequestMappingHandlerAdapter} + * within the given {@link ApplicationContext}. + * + * @param context must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static List getSupportedHypermediaTypes(ApplicationContext context) { + return getSupportedHypermediaTypes(context, RepresentationModel.class); + } + + /** + * Looks up the the media types supported for the given type in the {@link RequestMappingHandlerAdapter} within the + * given {@link ApplicationContext}. + * + * @param context must not be {@literal null}. + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + */ + public static List getSupportedHypermediaTypes(ApplicationContext context, Class type) { + + RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); + + return adapter.getMessageConverters().stream() // + .filter(MappingJackson2HttpMessageConverter.class::isInstance) // + .map(MappingJackson2HttpMessageConverter.class::cast) // + .findFirst() // + .map(it -> it.getSupportedMediaTypes(type)) // + .orElseGet(() -> Collections.emptyList()); // + } +} diff --git a/src/test/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationIntegrationTest.java b/src/test/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationIntegrationTest.java new file mode 100644 index 00000000..60ba114e --- /dev/null +++ b/src/test/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationIntegrationTest.java @@ -0,0 +1,57 @@ +/* + * 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.mediatype.hal; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.hateoas.support.ContextTester.*; + +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; +import org.springframework.hateoas.mediatype.MediaTypeTestUtils; +import org.springframework.http.MediaType; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +/** + * Integration tests for HAL media type configuration. + * + * @author Oliver Drotbohm + */ +class HalMediaTypeConfigurationIntegrationTest { + + static final MediaType CUSTOM_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.my-custom-mediatype"); + + @Test // #1253 + void includesCustomMediaTypeFromConfiguration() { + + withServletContext(ConfigurationWithCustomMediaType.class, it -> { + assertThat(MediaTypeTestUtils.getSupportedHypermediaTypes(it)).contains(CUSTOM_MEDIA_TYPE); + }); + } + + @Configuration + @EnableWebMvc + @EnableHypermediaSupport(type = HypermediaType.HAL) + static class ConfigurationWithCustomMediaType { + + @Bean + HalConfiguration halConfiguration() { + return new HalConfiguration().withMediaType(CUSTOM_MEDIA_TYPE); + } + } +}