Skip to content

Commit a8a8836

Browse files
committed
#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.
1 parent f998ddf commit a8a8836

File tree

5 files changed

+181
-18
lines changed

5 files changed

+181
-18
lines changed

pom.xml

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -976,6 +976,13 @@
976976
<scope>test</scope>
977977
</dependency>
978978

979+
<dependency>
980+
<groupId>javax.servlet</groupId>
981+
<artifactId>javax.servlet-api</artifactId>
982+
<version>4.0.1</version>
983+
<scope>provided</scope>
984+
</dependency>
985+
979986
<dependency>
980987
<groupId>net.jadler</groupId>
981988
<artifactId>jadler-all</artifactId>
@@ -1007,15 +1014,6 @@
10071014
<scope>test</scope>
10081015
</dependency>
10091016

1010-
<!-- Needs to be after Jadler to make sure it sees the Servlet 3.0 dependency pulled in for testing -->
1011-
1012-
<dependency>
1013-
<groupId>javax.servlet</groupId>
1014-
<artifactId>javax.servlet-api</artifactId>
1015-
<version>3.1.0</version>
1016-
<scope>provided</scope>
1017-
</dependency>
1018-
10191017
<dependency>
10201018
<groupId>com.tngtech.archunit</groupId>
10211019
<artifactId>archunit</artifactId>

src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,18 @@
1515
*/
1616
package org.springframework.hateoas.mediatype.hal;
1717

18+
import java.util.ArrayList;
19+
import java.util.Collections;
1820
import java.util.LinkedHashMap;
21+
import java.util.List;
1922
import java.util.Map;
2023
import java.util.Map.Entry;
2124
import java.util.function.Consumer;
2225

2326
import org.springframework.hateoas.Link;
2427
import org.springframework.hateoas.LinkRelation;
28+
import org.springframework.hateoas.MediaTypes;
29+
import org.springframework.http.MediaType;
2530
import org.springframework.util.AntPathMatcher;
2631
import org.springframework.util.Assert;
2732
import org.springframework.util.PathMatcher;
@@ -45,6 +50,7 @@ public class HalConfiguration {
4550
private final RenderSingleLinks renderSingleLinks;
4651
private final Map<String, RenderSingleLinks> singleLinksPerPattern;
4752
private final Consumer<ObjectMapper> objectMapperCustomizer;
53+
private final List<MediaType> mediaTypes;
4854

4955
/**
5056
* Configures whether the Jackson property naming strategy is applied to link relations and within {@code _embedded}
@@ -62,22 +68,26 @@ public class HalConfiguration {
6268
* Creates a new default {@link HalConfiguration} rendering single links as immediate sub-document.
6369
*/
6470
public HalConfiguration() {
65-
this(RenderSingleLinks.AS_SINGLE, new LinkedHashMap<>(), true, true, __ -> {});
71+
72+
this(RenderSingleLinks.AS_SINGLE, new LinkedHashMap<>(), true, true, __ -> {},
73+
Collections.singletonList(MediaTypes.HAL_JSON));
6674
}
6775

6876
private HalConfiguration(RenderSingleLinks renderSingleLinks, Map<String, RenderSingleLinks> singleLinksPerPattern,
6977
boolean applyPropertyNamingStrategy, boolean enforceEmbeddedCollections,
70-
Consumer<ObjectMapper> objectMapperCustomizer) {
78+
Consumer<ObjectMapper> objectMapperCustomizer, List<MediaType> mediaTypes) {
7179

7280
Assert.notNull(renderSingleLinks, "RenderSingleLinks must not be null!");
7381
Assert.notNull(singleLinksPerPattern, "Single links per pattern map must not be null!");
7482
Assert.notNull(objectMapperCustomizer, "ObjectMapper customizer must not be null!");
83+
Assert.notNull(mediaTypes, "MediaTypes must not be null!");
7584

7685
this.renderSingleLinks = renderSingleLinks;
7786
this.singleLinksPerPattern = singleLinksPerPattern;
7887
this.applyPropertyNamingStrategy = applyPropertyNamingStrategy;
7988
this.enforceEmbeddedCollections = enforceEmbeddedCollections;
8089
this.objectMapperCustomizer = objectMapperCustomizer;
90+
this.mediaTypes = mediaTypes;
8191
}
8292

8393
/**
@@ -144,7 +154,7 @@ public HalConfiguration withRenderSingleLinks(RenderSingleLinks renderSingleLink
144154
return this.renderSingleLinks == renderSingleLinks //
145155
? this //
146156
: new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
147-
enforceEmbeddedCollections, objectMapperCustomizer);
157+
enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes);
148158
}
149159

150160
/**
@@ -160,7 +170,7 @@ private HalConfiguration withSingleLinksPerPattern(Map<String, RenderSingleLinks
160170
return this.singleLinksPerPattern == singleLinksPerPattern //
161171
? this //
162172
: new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
163-
enforceEmbeddedCollections, objectMapperCustomizer);
173+
enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes);
164174
}
165175

166176
/**
@@ -175,7 +185,7 @@ public HalConfiguration withApplyPropertyNamingStrategy(boolean applyPropertyNam
175185
return this.applyPropertyNamingStrategy == applyPropertyNamingStrategy //
176186
? this //
177187
: new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
178-
enforceEmbeddedCollections, objectMapperCustomizer);
188+
enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes);
179189
}
180190

181191
/**
@@ -190,15 +200,46 @@ public HalConfiguration withEnforceEmbeddedCollections(boolean enforceEmbeddedCo
190200
return this.enforceEmbeddedCollections == enforceEmbeddedCollections //
191201
? this //
192202
: new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
193-
enforceEmbeddedCollections, objectMapperCustomizer);
203+
enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes);
194204
}
195205

206+
/**
207+
* Configures an {@link ObjectMapper} customizer to tweak the instance after it has been pre-configured with all HAL
208+
* specific setup.
209+
*
210+
* @param objectMapperCustomizer must not be {@literal null}.
211+
* @return will never be {@literal null}.
212+
*/
196213
public HalConfiguration withObjectMapperCustomizer(Consumer<ObjectMapper> objectMapperCustomizer) {
197214

198215
return this.objectMapperCustomizer == objectMapperCustomizer //
199216
? this //
200217
: new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
201-
enforceEmbeddedCollections, objectMapperCustomizer);
218+
enforceEmbeddedCollections, objectMapperCustomizer, mediaTypes);
219+
}
220+
221+
/**
222+
* Registers additional media types that are supposed to be aliases to {@link MediaTypes#HAL_JSON}. Registered
223+
* {@link MediaType}s will be preferred over the default one, i.e. they'll be listed first in client's accept headers
224+
* etc.
225+
*
226+
* @param mediaType must not be {@literal null}.
227+
* @return will never be {@literal null}.
228+
* @since 1.3
229+
*/
230+
public HalConfiguration withMediaType(MediaType mediaType) {
231+
232+
Assert.notNull(mediaType, "MediaType must not be null!");
233+
234+
if (mediaTypes.contains(mediaType)) {
235+
return this;
236+
}
237+
238+
List<MediaType> newMediaTypes = new ArrayList<>(mediaTypes);
239+
newMediaTypes.add(mediaTypes.size() - 1, mediaType);
240+
241+
return new HalConfiguration(renderSingleLinks, singleLinksPerPattern, applyPropertyNamingStrategy,
242+
enforceEmbeddedCollections, objectMapperCustomizer, newMediaTypes);
202243
}
203244

204245
public RenderSingleLinks getRenderSingleLinks() {
@@ -220,6 +261,10 @@ public HalConfiguration customize(ObjectMapper mapper) {
220261
return this;
221262
}
222263

264+
List<MediaType> getMediaTypes() {
265+
return mediaTypes;
266+
}
267+
223268
/**
224269
* Configuration option how to render single links of a given {@link LinkRelation}.
225270
*

src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import org.springframework.context.annotation.Bean;
2424
import org.springframework.context.annotation.Configuration;
2525
import org.springframework.hateoas.client.LinkDiscoverer;
26-
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
2726
import org.springframework.hateoas.config.HypermediaMappingInformation;
2827
import org.springframework.hateoas.mediatype.MessageResolver;
2928
import org.springframework.hateoas.server.LinkRelationProvider;
@@ -69,7 +68,7 @@ LinkDiscoverer halLinkDisocoverer() {
6968
*/
7069
@Override
7170
public List<MediaType> getMediaTypes() {
72-
return HypermediaType.HAL.getMediaTypes();
71+
return this.halConfiguration.getIfAvailable(HalConfiguration::new).getMediaTypes();
7372
}
7473

7574
/*
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype;
17+
18+
import java.util.Collections;
19+
import java.util.List;
20+
21+
import org.springframework.context.ApplicationContext;
22+
import org.springframework.hateoas.RepresentationModel;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
25+
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
26+
27+
/**
28+
* Test utilities to verify configuration of media type support.
29+
*
30+
* @author Oliver Drotbohm
31+
*/
32+
public class MediaTypeTestUtils {
33+
34+
/**
35+
* Looks up the the media types supported for {@link RepresentationModel} in the {@link RequestMappingHandlerAdapter}
36+
* within the given {@link ApplicationContext}.
37+
*
38+
* @param context must not be {@literal null}.
39+
* @return will never be {@literal null}.
40+
*/
41+
public static List<MediaType> getSupportedHypermediaTypes(ApplicationContext context) {
42+
return getSupportedHypermediaTypes(context, RepresentationModel.class);
43+
}
44+
45+
/**
46+
* Looks up the the media types supported for the given type in the {@link RequestMappingHandlerAdapter} within the
47+
* given {@link ApplicationContext}.
48+
*
49+
* @param context must not be {@literal null}.
50+
* @param type must not be {@literal null}.
51+
* @return will never be {@literal null}.
52+
*/
53+
public static List<MediaType> getSupportedHypermediaTypes(ApplicationContext context, Class<?> type) {
54+
55+
RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class);
56+
57+
return adapter.getMessageConverters().stream() //
58+
.filter(MappingJackson2HttpMessageConverter.class::isInstance) //
59+
.map(MappingJackson2HttpMessageConverter.class::cast) //
60+
.findFirst() //
61+
.map(it -> it.getSupportedMediaTypes(type)) //
62+
.orElseGet(() -> Collections.emptyList()); //
63+
}
64+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype.hal;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.springframework.hateoas.support.ContextTester.*;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.springframework.context.annotation.Bean;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.hateoas.config.EnableHypermediaSupport;
25+
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
26+
import org.springframework.hateoas.mediatype.MediaTypeTestUtils;
27+
import org.springframework.http.MediaType;
28+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
29+
30+
/**
31+
* Integration tests for HAL media type configuration.
32+
*
33+
* @author Oliver Drotbohm
34+
*/
35+
class HalMediaTypeConfigurationIntegrationTest {
36+
37+
static final MediaType CUSTOM_MEDIA_TYPE = MediaType.parseMediaType("application/vnd.my-custom-mediatype");
38+
39+
@Test // #1253
40+
void includesCustomMediaTypeFromConfiguration() {
41+
42+
withServletContext(ConfigurationWithCustomMediaType.class, it -> {
43+
assertThat(MediaTypeTestUtils.getSupportedHypermediaTypes(it)).contains(CUSTOM_MEDIA_TYPE);
44+
});
45+
}
46+
47+
@Configuration
48+
@EnableWebMvc
49+
@EnableHypermediaSupport(type = HypermediaType.HAL)
50+
static class ConfigurationWithCustomMediaType {
51+
52+
@Bean
53+
HalConfiguration halConfiguration() {
54+
return new HalConfiguration().withMediaType(CUSTOM_MEDIA_TYPE);
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)