From 8a8e4b04047605c6bcfc0b4314680e1448694f68 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Fri, 17 Jan 2025 13:10:57 +0100 Subject: [PATCH] GH-2276 - Add support for official IANA HAL media type identifier. The media type identifier for HAL registered with the IANA is application/vnd.hal+json, notably different from application/hal+json used in the RFC document. This commit adds a constant for that media type identifier and updates the configuration spots necessary to additionally consider it. [0] https://www.iana.org/assignments/media-types/application/vnd.hal+json [1] https://datatracker.ietf.org/doc/html/draft-kelly-json-hal --- .../springframework/hateoas/MediaTypes.java | 18 +++++++++-- .../config/EnableHypermediaSupport.java | 32 +++++++++++++------ ...HypermediaConfigurationImportSelector.java | 2 +- .../CollectionJsonMediaTypeConfiguration.java | 3 +- .../mediatype/hal/HalConfiguration.java | 4 +-- .../mediatype/hal/HalLinkDiscoverer.java | 3 +- .../HalMediaTypeConfigurationProvider.java | 4 ++- .../mediatype/hal/HalTraversonDefaults.java | 7 ++-- .../uber/UberMediaTypeConfiguration.java | 3 +- ...nableHypermediaSupportIntegrationTest.java | 4 +++ ...ediaRestTemplateBeanPostProcessorTest.java | 5 ++- 11 files changed, 59 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/MediaTypes.java b/src/main/java/org/springframework/hateoas/MediaTypes.java index d2856a31..c552e036 100644 --- a/src/main/java/org/springframework/hateoas/MediaTypes.java +++ b/src/main/java/org/springframework/hateoas/MediaTypes.java @@ -19,7 +19,7 @@ import org.springframework.http.MediaType; /** * Constants for well-known hypermedia types. - * + * * @author Oliver Gierke * @author Przemek Nowak * @author Drummond Dawson @@ -37,6 +37,20 @@ public class MediaTypes { */ public static final MediaType HAL_JSON = MediaType.valueOf(HAL_JSON_VALUE); + /** + * A String equivalent of {@link MediaTypes#VND_HAL_JSON}. + * + * @since 2.5 + */ + public static final String VND_HAL_JSON_VALUE = "application/vnd.hal+json"; + + /** + * Public constant media type for {@code application/vnd.hal+json}. + * + * @since 2.5 + */ + public static final MediaType VND_HAL_JSON = MediaType.valueOf(VND_HAL_JSON_VALUE); + /** * A String equivalent of {@link MediaTypes#ALPS_JSON}. */ @@ -76,7 +90,7 @@ public class MediaTypes { * Public constant media type for {@code application/vnd.amundsen-uber+json}. */ public static final MediaType UBER_JSON = MediaType.parseMediaType(UBER_JSON_VALUE); - + /** * A String equivalent of {@link MediaTypes#VND_ERROR_JSON}. */ diff --git a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java index d1dac3af..5aa80716 100644 --- a/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java +++ b/src/main/java/org/springframework/hateoas/config/EnableHypermediaSupport.java @@ -20,13 +20,12 @@ import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Import; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.support.WebStack; import org.springframework.http.MediaType; -import org.springframework.web.reactive.function.client.WebClient; /** * Activates hypermedia support in the {@link ApplicationContext}. Will register infrastructure beans to support all @@ -50,8 +49,8 @@ public @interface EnableHypermediaSupport { /** * Configures which {@link WebStack}s we're supposed to enable support for. By default we're activating it for all - * available ones if they happen to be in use. Configure this explicitly in case you're using WebFlux components - * like {@link WebClient} but don't want to use hypermedia operations with it. + * available ones if they happen to be in use. Configure this explicitly in case you're using WebFlux components like + * {@link WebClient} but don't want to use hypermedia operations with it. * * @return */ @@ -69,9 +68,9 @@ public @interface EnableHypermediaSupport { * HAL - Hypermedia Application Language. * * @see http://stateless.co/hal_specification.html - * @see https://tools.ietf.org/html/draft-kelly-json-hal-05 + * @see https://tools.ietf.org/html/draft-kelly-json-hal */ - HAL(MediaTypes.HAL_JSON, "hal"), + HAL(List.of(MediaTypes.HAL_JSON, MediaTypes.VND_HAL_JSON), "hal"), /** * HAL-FORMS - Independent, backward-compatible extension of the HAL designed to add runtime FORM support @@ -96,18 +95,33 @@ public @interface EnableHypermediaSupport { */ UBER(MediaTypes.UBER_JSON, "uber"); - private final MediaType mediaTypes; + private final List mediaTypes; private final String localPackageName; HypermediaType(MediaType mediaType, String localPackageName) { - this.mediaTypes = mediaType; + this.mediaTypes = List.of(mediaType); this.localPackageName = localPackageName; } - public MediaType getMediaType() { + HypermediaType(List mediaTypes, String localPackageName) { + this.mediaTypes = mediaTypes; + this.localPackageName = localPackageName; + } + + public List getMediaTypes() { return this.mediaTypes; } + /** + * @deprecated since 2.5, in favor of {@link #getMediaTypes()} as a logical media type might have been associated + * with multiple media type identifiers (see {@link #HAL}). + * @return will never be {@literal null}. + */ + @Deprecated(since = "2.5", forRemoval = true) + public MediaType getMediaType() { + return this.mediaTypes.get(0); + } + public String getLocalPackageName() { return localPackageName; } diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index bb6f73e4..70d1edd9 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -79,7 +79,7 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL List types = attributes == null // ? Collections.emptyList() // : Arrays.stream((HypermediaType[]) attributes.get("type")) // - .map(it -> it.getMediaType()) // + .flatMap(it -> it.getMediaTypes().stream()) // .collect(Collectors.toList()); if (!beanFactory.containsBean("hateoasMediaTypeConfigurer")) { diff --git a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java index ab2e1404..9e7eb2a0 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/collectionjson/CollectionJsonMediaTypeConfiguration.java @@ -15,7 +15,6 @@ */ package org.springframework.hateoas.mediatype.collectionjson; -import java.util.Collections; import java.util.List; import org.springframework.context.annotation.Bean; @@ -48,7 +47,7 @@ class CollectionJsonMediaTypeConfiguration implements HypermediaMappingInformati */ @Override public List getMediaTypes() { - return Collections.singletonList(HypermediaType.COLLECTION_JSON.getMediaType()); + return HypermediaType.COLLECTION_JSON.getMediaTypes(); } /* 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 054affaa..b8bc6eb1 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalConfiguration.java @@ -16,14 +16,12 @@ 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; @@ -70,7 +68,7 @@ public class HalConfiguration { public HalConfiguration() { this(RenderSingleLinks.AS_SINGLE, new LinkedHashMap<>(), true, true, __ -> {}, - Collections.singletonList(MediaTypes.HAL_JSON)); + List.of(MediaTypes.HAL_JSON, MediaTypes.VND_HAL_JSON)); } private HalConfiguration(RenderSingleLinks renderSingleLinks, Map singleLinksPerPattern, diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkDiscoverer.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkDiscoverer.java index 4c35f539..816d53c8 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalLinkDiscoverer.java @@ -21,7 +21,6 @@ import org.springframework.hateoas.Link; import org.springframework.hateoas.LinkRelation; import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.client.JsonPathLinkDiscoverer; -import org.springframework.hateoas.client.LinkDiscoverer; import org.springframework.http.MediaType; /** @@ -36,7 +35,7 @@ public class HalLinkDiscoverer extends JsonPathLinkDiscoverer { * Constructor for {@link MediaTypes#HAL_JSON}. */ public HalLinkDiscoverer() { - this(MediaTypes.HAL_JSON); + this(MediaTypes.HAL_JSON, MediaTypes.VND_HAL_JSON); } protected HalLinkDiscoverer(MediaType... mediaTypes) { diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationProvider.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationProvider.java index e3a63f5d..5459abcf 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationProvider.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfigurationProvider.java @@ -44,6 +44,8 @@ class HalMediaTypeConfigurationProvider implements MediaTypeConfigurationProvide */ @Override public boolean supportsAny(Collection mediaTypes) { - return mediaTypes.contains(MediaTypes.HAL_JSON); + + return mediaTypes.contains(MediaTypes.HAL_JSON) + || mediaTypes.contains(MediaTypes.VND_HAL_JSON); } } diff --git a/src/main/java/org/springframework/hateoas/mediatype/hal/HalTraversonDefaults.java b/src/main/java/org/springframework/hateoas/mediatype/hal/HalTraversonDefaults.java index df99f491..d5e867ad 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/hal/HalTraversonDefaults.java +++ b/src/main/java/org/springframework/hateoas/mediatype/hal/HalTraversonDefaults.java @@ -40,7 +40,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ class HalTraversonDefaults implements TraversonDefaults { - private static final List HAL_FLAVORS = Collections.singletonList(MediaTypes.HAL_JSON); + private static final List HAL_FLAVORS = List.of(MediaTypes.HAL_JSON, MediaTypes.VND_HAL_JSON, + MediaTypes.HAL_FORMS_JSON); /* * (non-Javadoc) @@ -70,8 +71,8 @@ class HalTraversonDefaults implements TraversonDefaults { @Override public List getLinkDiscoverers(Collection mediaTypes) { - return mediaTypes.stream().anyMatch(it -> it.isCompatibleWith(MediaTypes.HAL_JSON)) // - ? Collections.singletonList(new HalLinkDiscoverer()) // + return mediaTypes.stream().anyMatch(it -> HAL_FLAVORS.stream().anyMatch(it::isCompatibleWith)) // + ? List.of(new HalLinkDiscoverer()) // : Collections.emptyList(); } diff --git a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java index c1c82c2d..4557201b 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java +++ b/src/main/java/org/springframework/hateoas/mediatype/uber/UberMediaTypeConfiguration.java @@ -15,7 +15,6 @@ */ package org.springframework.hateoas.mediatype.uber; -import java.util.Collections; import java.util.List; import org.springframework.context.annotation.Bean; @@ -48,7 +47,7 @@ class UberMediaTypeConfiguration implements HypermediaMappingInformation { */ @Override public List getMediaTypes() { - return Collections.singletonList(HypermediaType.UBER.getMediaType()); + return HypermediaType.UBER.getMediaTypes(); } /* diff --git a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java index 3cab091e..b443964d 100755 --- a/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java +++ b/src/test/java/org/springframework/hateoas/config/EnableHypermediaSupportIntegrationTest.java @@ -110,6 +110,8 @@ class EnableHypermediaSupportIntegrationTest { assertThat(discoverers).isNotNull(); assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON)) .hasValueSatisfying(HalLinkDiscoverer.class::isInstance); + assertThat(discoverers.getLinkDiscovererFor(MediaTypes.VND_HAL_JSON)) + .hasValueSatisfying(HalLinkDiscoverer.class::isInstance); assertRelProvidersSetUp(context); }); } @@ -138,6 +140,8 @@ class EnableHypermediaSupportIntegrationTest { assertThat(discoverers).isNotNull(); assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_JSON)) .hasValueSatisfying(HalLinkDiscoverer.class::isInstance); + assertThat(discoverers.getLinkDiscovererFor(MediaTypes.VND_HAL_JSON)) + .hasValueSatisfying(HalLinkDiscoverer.class::isInstance); assertThat(discoverers.getLinkDiscovererFor(MediaTypes.HAL_FORMS_JSON)) .hasValueSatisfying(HalFormsLinkDiscoverer.class::isInstance); diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java index cec65fb5..df7e4d7b 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaRestTemplateBeanPostProcessorTest.java @@ -28,7 +28,6 @@ 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.HttpMessageConverter; @@ -55,6 +54,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // + MediaTypes.VND_HAL_JSON, // MediaType.APPLICATION_JSON, // MediaType.parseMediaType("application/*+json")); }); @@ -71,6 +71,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // + MediaTypes.VND_HAL_JSON, // MediaTypes.COLLECTION_JSON, // MediaType.APPLICATION_JSON, // MediaType.parseMediaType("application/*+json")); @@ -88,6 +89,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // + MediaTypes.VND_HAL_JSON, // MediaTypes.HAL_FORMS_JSON, // MediaTypes.COLLECTION_JSON, // MediaTypes.UBER_JSON, // @@ -104,6 +106,7 @@ class HypermediaRestTemplateBeanPostProcessorTest { assertThat(getSupportedHypermediaTypes(context, REST_TEMPLATE_EXTRACTOR)) // .containsExactlyInAnyOrder( // MediaTypes.HAL_JSON, // + MediaTypes.VND_HAL_JSON, // MediaType.parseMediaType("application/frodo+json"), // MediaType.APPLICATION_JSON, // MediaType.parseMediaType("application/*+json") //