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
This commit is contained in:
Oliver Drotbohm
2025-01-17 13:10:57 +01:00
parent b19f7de6d3
commit 8a8e4b0404
11 changed files with 59 additions and 26 deletions

View File

@@ -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}.
*/

View File

@@ -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<MediaType> 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<MediaType> mediaTypes, String localPackageName) {
this.mediaTypes = mediaTypes;
this.localPackageName = localPackageName;
}
public List<MediaType> 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;
}

View File

@@ -79,7 +79,7 @@ class HypermediaConfigurationImportSelector implements ImportSelector, ResourceL
List<MediaType> 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")) {

View File

@@ -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<MediaType> getMediaTypes() {
return Collections.singletonList(HypermediaType.COLLECTION_JSON.getMediaType());
return HypermediaType.COLLECTION_JSON.getMediaTypes();
}
/*

View File

@@ -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<String, RenderSingleLinks> singleLinksPerPattern,

View File

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

View File

@@ -44,6 +44,8 @@ class HalMediaTypeConfigurationProvider implements MediaTypeConfigurationProvide
*/
@Override
public boolean supportsAny(Collection<MediaType> mediaTypes) {
return mediaTypes.contains(MediaTypes.HAL_JSON);
return mediaTypes.contains(MediaTypes.HAL_JSON)
|| mediaTypes.contains(MediaTypes.VND_HAL_JSON);
}
}

View File

@@ -40,7 +40,8 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*/
class HalTraversonDefaults implements TraversonDefaults {
private static final List<MediaType> HAL_FLAVORS = Collections.singletonList(MediaTypes.HAL_JSON);
private static final List<MediaType> 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<LinkDiscoverer> getLinkDiscoverers(Collection<MediaType> 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();
}

View File

@@ -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<MediaType> getMediaTypes() {
return Collections.singletonList(HypermediaType.UBER.getMediaType());
return HypermediaType.UBER.getMediaTypes();
}
/*

View File

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

View File

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