#1253 - Allow custom media type aliases for HAL.
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.
This commit is contained in:
16
pom.xml
16
pom.xml
@@ -976,6 +976,13 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>4.0.1</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.jadler</groupId>
|
||||
<artifactId>jadler-all</artifactId>
|
||||
@@ -1007,15 +1014,6 @@
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Needs to be after Jadler to make sure it sees the Servlet 3.0 dependency pulled in for testing -->
|
||||
|
||||
<dependency>
|
||||
<groupId>javax.servlet</groupId>
|
||||
<artifactId>javax.servlet-api</artifactId>
|
||||
<version>3.1.0</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.tngtech.archunit</groupId>
|
||||
<artifactId>archunit</artifactId>
|
||||
|
||||
@@ -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<String, RenderSingleLinks> singleLinksPerPattern;
|
||||
private final Consumer<ObjectMapper> objectMapperCustomizer;
|
||||
private final List<MediaType> 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<String, RenderSingleLinks> singleLinksPerPattern,
|
||||
boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections,
|
||||
Consumer<ObjectMapper> objectMapperCustomizer) {
|
||||
Consumer<ObjectMapper> objectMapperCustomizer, List<MediaType> 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<ObjectMapper> 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<MediaType> 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<MediaType> getMediaTypes() {
|
||||
return mediaTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration option how to render single links of a given {@link LinkRelation}.
|
||||
*
|
||||
|
||||
@@ -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<MediaType> getMediaTypes() {
|
||||
return HypermediaType.HAL.getMediaTypes();
|
||||
return this.halConfiguration.getIfAvailable(HalConfiguration::new).getMediaTypes();
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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<MediaType> 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<MediaType> 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()); //
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user