From 8557e69fdb4af0907b75863eff62f6ecfa5b04b7 Mon Sep 17 00:00:00 2001 From: Oliver Drotbohm Date: Thu, 21 Jan 2021 17:29:28 +0100 Subject: [PATCH] #1442 - Exposes consumed media types on affordances. InputPayloadMetadata now captures and exposes supported media types. SpringAffordanceBuilder obtains them from the newly introduced API available on MappingDiscoverer. --- .../hateoas/AffordanceModel.java | 39 ++++++++++++------- .../hateoas/mediatype/Affordances.java | 13 ++++++- .../mediatype/ConfigurableAffordance.java | 14 +++++-- .../mediatype/TypeBasedPayloadMetadata.java | 28 +++++++------ .../core/AnnotationMappingDiscoverer.java | 18 +++++++++ .../server/core/CachingMappingDiscoverer.java | 11 ++++++ .../server/core/MappingDiscoverer.java | 11 ++++++ .../PropertyResolvingMappingDiscoverer.java | 11 ++++++ .../server/core/SpringAffordanceBuilder.java | 1 + .../AnnotationMappingDiscovererUnitTest.java | 14 +++++++ 10 files changed, 130 insertions(+), 30 deletions(-) diff --git a/src/main/java/org/springframework/hateoas/AffordanceModel.java b/src/main/java/org/springframework/hateoas/AffordanceModel.java index ceb93d25..344678e8 100644 --- a/src/main/java/org/springframework/hateoas/AffordanceModel.java +++ b/src/main/java/org/springframework/hateoas/AffordanceModel.java @@ -257,16 +257,29 @@ public abstract class AffordanceModel { * @return will never be {@literal null}. * @since 1.3 */ - InputPayloadMetadata withMediaType(@Nullable MediaType mediaType); + InputPayloadMetadata withMediaTypes(List mediaType); /** * Returns the {@link MediaType} that the payload requires. * - * @return can be {@literal null}. + * @return will never be {@literal null}. * @since 1.3 */ + List getMediaTypes(); + + /** + * Returns the primary {@link MediaType} expected for the input. That is, from {@link #getMediaTypes()} the first + * one, if available. + * + * @return can be {@literal null}. + */ @Nullable - MediaType getMediaType(); + default MediaType getPrimaryMediaType() { + + List mediaTypes = getMediaTypes(); + + return mediaTypes.isEmpty() ? null : mediaTypes.get(0); + } } /** @@ -277,15 +290,15 @@ public abstract class AffordanceModel { private static class DelegatingInputPayloadMetadata implements InputPayloadMetadata { private final PayloadMetadata metadata; - private final MediaType mediaType; + private final List mediaTypes; public static DelegatingInputPayloadMetadata of(PayloadMetadata metadata) { - return new DelegatingInputPayloadMetadata(metadata, null); + return new DelegatingInputPayloadMetadata(metadata, Collections.emptyList()); } - private DelegatingInputPayloadMetadata(PayloadMetadata metadata, MediaType mediaType) { + private DelegatingInputPayloadMetadata(PayloadMetadata metadata, List mediaTypes) { this.metadata = metadata; - this.mediaType = mediaType; + this.mediaTypes = mediaTypes; } /* @@ -326,20 +339,20 @@ public abstract class AffordanceModel { /* * (non-Javadoc) - * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaType(org.springframework.http.MediaType) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaTypes(java.util.List) */ @Override - public InputPayloadMetadata withMediaType(MediaType mediaType) { - return new DelegatingInputPayloadMetadata(metadata, mediaType); + public InputPayloadMetadata withMediaTypes(List mediaTypes) { + return new DelegatingInputPayloadMetadata(metadata, mediaTypes); } /* * (non-Javadoc) - * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaType() + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaTypes() */ @Override - public MediaType getMediaType() { - return mediaType; + public List getMediaTypes() { + return mediaTypes; } /* diff --git a/src/main/java/org/springframework/hateoas/mediatype/Affordances.java b/src/main/java/org/springframework/hateoas/mediatype/Affordances.java index 96304427..f0f4d382 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/Affordances.java +++ b/src/main/java/org/springframework/hateoas/mediatype/Affordances.java @@ -215,8 +215,17 @@ public class Affordances implements AffordanceOperations { * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputMediaType(org.springframework.http.MediaType) */ @Override - public AffordanceBuilder withInputMediaType(@Nullable MediaType inputMediaType) { - return withInput(inputMetdata.withMediaType(inputMediaType)); + public ConfigurableAffordance withInputMediaType(MediaType inputMediaType) { + return withInputMediaTypes(Arrays.asList(inputMediaType)); + } + + /* + * (non-Javadoc) + * @see org.springframework.hateoas.mediatype.ConfigurableAffordance#withInputMediaTypes(java.util.List) + */ + @Override + public ConfigurableAffordance withInputMediaTypes(List inputMediaTypes) { + return withInput(inputMetdata.withMediaTypes(inputMediaTypes)); } /* diff --git a/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java index a1eb3df8..6da8683c 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java +++ b/src/main/java/org/springframework/hateoas/mediatype/ConfigurableAffordance.java @@ -111,10 +111,18 @@ public interface ConfigurableAffordance extends AffordanceOperations { /** * Registers the input to expect to be of the given {@link MediaType}. * - * @param inputMediaType - * @return + * @param inputMediaType can be {@literal null}. + * @return will never be {@literal null}. */ - ConfigurableAffordance withInputMediaType(@Nullable MediaType inputMediaType); + ConfigurableAffordance withInputMediaType(MediaType inputMediaType); + + /** + * Registers the given {@link MediaType}s as input payload media types. + * + * @param inputMediaTypes must not be {@literal null}. + * @return will never be {@literal null}. + */ + ConfigurableAffordance withInputMediaTypes(List inputMediaTypes); /** * Replaces the current {@link QueryParameter} list with the given ones. diff --git a/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java b/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java index 8021feb8..4f4c8477 100644 --- a/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java +++ b/src/main/java/org/springframework/hateoas/mediatype/TypeBasedPayloadMetadata.java @@ -16,6 +16,7 @@ package org.springframework.hateoas.mediatype; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.SortedMap; import java.util.TreeMap; @@ -28,7 +29,7 @@ import org.springframework.hateoas.AffordanceModel.InputPayloadMetadata; import org.springframework.hateoas.AffordanceModel.Named; import org.springframework.hateoas.AffordanceModel.PropertyMetadata; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * {@link InputPayloadMetadata} implementation based on a Java type. @@ -39,19 +40,23 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata { private final ResolvableType type; private final SortedMap properties; - private final @Nullable MediaType mediaType; + private final List mediaTypes; TypeBasedPayloadMetadata(ResolvableType type, Stream properties) { this(type, new TreeMap<>( - properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity()))), null); + properties.collect(Collectors.toMap(PropertyMetadata::getName, Function.identity()))), Collections.emptyList()); } TypeBasedPayloadMetadata(ResolvableType type, SortedMap properties, - @Nullable MediaType mediaType) { + List mediaTypes) { + + Assert.notNull(type, "Type must not be null!"); + Assert.notNull(properties, "Properties must not be null!"); + Assert.notNull(mediaTypes, "Media types must not be null!"); this.type = type; this.properties = properties; - this.mediaType = mediaType; + this.mediaTypes = mediaTypes; } /* @@ -93,20 +98,19 @@ class TypeBasedPayloadMetadata implements InputPayloadMetadata { /* * (non-Javadoc) - * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaType(org.springframework.http.MediaType) + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#withMediaTypes(java.util.List) */ @Override - public InputPayloadMetadata withMediaType(@Nullable MediaType mediaType) { - return new TypeBasedPayloadMetadata(type, properties, mediaType); + public InputPayloadMetadata withMediaTypes(List mediaTypes) { + return new TypeBasedPayloadMetadata(type, properties, mediaTypes); } /* * (non-Javadoc) - * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaType() + * @see org.springframework.hateoas.AffordanceModel.InputPayloadMetadata#getMediaTypes() */ - @Nullable @Override - public MediaType getMediaType() { - return mediaType; + public List getMediaTypes() { + return mediaTypes; } } diff --git a/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java index a6b8b7fb..e04d4f41 100644 --- a/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/AnnotationMappingDiscoverer.java @@ -21,13 +21,16 @@ import static org.springframework.core.annotation.AnnotationUtils.*; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.Collectors; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.bind.annotation.RequestMethod; @@ -153,6 +156,21 @@ public class AnnotationMappingDiscoverer implements MappingDiscoverer { return requestMethodNames; } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.MappingDiscoverer#getConsumes(java.lang.reflect.Method) + */ + @Override + public List getConsumes(Method method) { + + Annotation annotation = findMergedAnnotation(method, annotationType); + String[] mediaTypes = (String[]) getValue(annotation, "consumes"); + + return mediaTypes == null + ? Collections.emptyList() + : Arrays.stream(mediaTypes).map(MediaType::parseMediaType).collect(Collectors.toList()); + } + private String[] getMappingFrom(@Nullable Annotation annotation) { if (annotation == null) { diff --git a/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java index f993eef8..19415ea7 100644 --- a/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/CachingMappingDiscoverer.java @@ -17,9 +17,11 @@ package org.springframework.hateoas.server.core; import java.lang.reflect.Method; import java.util.Collection; +import java.util.List; import java.util.Map; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.StringUtils; @@ -93,6 +95,15 @@ public class CachingMappingDiscoverer implements MappingDiscoverer { return METHODS.computeIfAbsent(key(type, method), __ -> delegate.getRequestMethod(type, method)); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.MappingDiscoverer#getConsumes(java.lang.reflect.Method) + */ + @Override + public List getConsumes(Method method) { + return delegate.getConsumes(method); + } + private static String key(Class type, @Nullable Method method) { StringBuilder builder = new StringBuilder(type.getName()); diff --git a/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java index 7ef485f0..966288a0 100644 --- a/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/MappingDiscoverer.java @@ -17,8 +17,10 @@ package org.springframework.hateoas.server.core; import java.lang.reflect.Method; import java.util.Collection; +import java.util.List; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; /** @@ -68,4 +70,13 @@ public interface MappingDiscoverer { * @return */ Collection getRequestMethod(Class type, Method method); + + /** + * Returns the {@link MediaType}s supported for consumption. + * + * @param method must not be {@literal null}. + * @return can be {@literal null}. + * @since 1.3 + */ + List getConsumes(Method method); } diff --git a/src/main/java/org/springframework/hateoas/server/core/PropertyResolvingMappingDiscoverer.java b/src/main/java/org/springframework/hateoas/server/core/PropertyResolvingMappingDiscoverer.java index cb64db7e..768fbf25 100644 --- a/src/main/java/org/springframework/hateoas/server/core/PropertyResolvingMappingDiscoverer.java +++ b/src/main/java/org/springframework/hateoas/server/core/PropertyResolvingMappingDiscoverer.java @@ -17,8 +17,10 @@ package org.springframework.hateoas.server.core; import java.lang.reflect.Method; import java.util.Collection; +import java.util.List; import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.context.ContextLoader; @@ -80,6 +82,15 @@ class PropertyResolvingMappingDiscoverer implements MappingDiscoverer { return delegate.getRequestMethod(type, method); } + /* + * (non-Javadoc) + * @see org.springframework.hateoas.server.core.MappingDiscoverer#getConsumes(java.lang.reflect.Method) + */ + @Override + public List getConsumes(Method method) { + return delegate.getConsumes(method); + } + @Nullable private static String resolveProperties(@Nullable String mapping) { diff --git a/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java b/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java index c5e6b175..152635d0 100644 --- a/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java +++ b/src/main/java/org/springframework/hateoas/server/core/SpringAffordanceBuilder.java @@ -72,6 +72,7 @@ public class SpringAffordanceBuilder { .withOutput(outputType) // .withParameters(queryMethodParameters) // .withName(methodName) // + .withInputMediaTypes(discoverer.getConsumes(method)) // .build() // .stream()) // .collect(Collectors.toList()); diff --git a/src/test/java/org/springframework/hateoas/server/core/AnnotationMappingDiscovererUnitTest.java b/src/test/java/org/springframework/hateoas/server/core/AnnotationMappingDiscovererUnitTest.java index adae9227..350d8b32 100755 --- a/src/test/java/org/springframework/hateoas/server/core/AnnotationMappingDiscovererUnitTest.java +++ b/src/test/java/org/springframework/hateoas/server/core/AnnotationMappingDiscovererUnitTest.java @@ -20,6 +20,7 @@ import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; import org.junit.jupiter.api.Test; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -160,6 +161,16 @@ class AnnotationMappingDiscovererUnitTest { assertThat(discoverer.getMapping(method)).isEqualTo("/type/foo/{bar}"); } + @Test // #1442 + void exposesConsumesClause() throws Exception { + + Method method = MyController.class.getMethod("mappingWithConsumesClause"); + assertThat(discoverer.getConsumes(method)).containsExactly(MediaType.APPLICATION_JSON); + + method = MyController.class.getMethod("method"); + assertThat(discoverer.getConsumes(method)).isEmpty(); + } + @RequestMapping("/type") interface MyController { @@ -174,6 +185,9 @@ class AnnotationMappingDiscovererUnitTest { @RequestMapping("/foo/{bar:[ABC]{1}}") void mappingWithMatchingExpression(); + + @RequestMapping(path = "/path", consumes = "application/json") + void mappingWithConsumesClause(); } interface ControllerWithoutTypeLevelMapping {