#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.
This commit is contained in:
Oliver Drotbohm
2021-01-21 17:29:28 +01:00
parent 7bd414e79a
commit 8557e69fdb
10 changed files with 130 additions and 30 deletions

View File

@@ -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> mediaType);
/**
* Returns the {@link MediaType} that the payload requires.
*
* @return can be {@literal null}.
* @return will never be {@literal null}.
* @since 1.3
*/
List<MediaType> 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<MediaType> 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<MediaType> 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<MediaType> 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<MediaType> 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<MediaType> getMediaTypes() {
return mediaTypes;
}
/*

View File

@@ -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<MediaType> inputMediaTypes) {
return withInput(inputMetdata.withMediaTypes(inputMediaTypes));
}
/*

View File

@@ -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<MediaType> inputMediaTypes);
/**
* Replaces the current {@link QueryParameter} list with the given ones.

View File

@@ -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<String, PropertyMetadata> properties;
private final @Nullable MediaType mediaType;
private final List<MediaType> mediaTypes;
TypeBasedPayloadMetadata(ResolvableType type, Stream<PropertyMetadata> 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<String, PropertyMetadata> properties,
@Nullable MediaType mediaType) {
List<MediaType> 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<MediaType> 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<MediaType> getMediaTypes() {
return mediaTypes;
}
}

View File

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

View File

@@ -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<MediaType> getConsumes(Method method) {
return delegate.getConsumes(method);
}
private static String key(Class<?> type, @Nullable Method method) {
StringBuilder builder = new StringBuilder(type.getName());

View File

@@ -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<HttpMethod> 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<MediaType> getConsumes(Method method);
}

View File

@@ -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<MediaType> getConsumes(Method method) {
return delegate.getConsumes(method);
}
@Nullable
private static String resolveProperties(@Nullable String mapping) {

View File

@@ -72,6 +72,7 @@ public class SpringAffordanceBuilder {
.withOutput(outputType) //
.withParameters(queryMethodParameters) //
.withName(methodName) //
.withInputMediaTypes(discoverer.getConsumes(method)) //
.build() //
.stream()) //
.collect(Collectors.toList());

View File

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