From f49741e3ed729c97189d2a5eb6076a8cb69b0bbd Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 31 Aug 2017 13:55:24 +0100 Subject: [PATCH] Allow an operation to specify the media types that it produces Closes gh-10118 --- .../boot/endpoint/DeleteOperation.java | 8 +++ .../boot/endpoint/ReadOperation.java | 7 +++ .../boot/endpoint/WriteOperation.java | 7 +++ .../web/WebAnnotationEndpointDiscoverer.java | 10 +++- .../AbstractWebEndpointIntegrationTests.java | 29 +++++++++++ .../WebAnnotationEndpointDiscovererTests.java | 51 +++++++++++++++++++ 6 files changed, 110 insertions(+), 2 deletions(-) diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java index 5d07fa05023..4482f87d631 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/DeleteOperation.java @@ -26,6 +26,7 @@ import java.lang.annotation.Target; * Identifies a method on an {@link Endpoint} as being a delete operation. * * @author Stephane Nicoll + * @author Andy Wilkinson * @since 2.0.0 */ @Target(ElementType.METHOD) @@ -33,4 +34,11 @@ import java.lang.annotation.Target; @Documented public @interface DeleteOperation { + /** + * The media type of the result of the operation. + * + * @return the media type + */ + String[] produces() default {}; + } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java index c68397f8005..15ee948455d 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/ReadOperation.java @@ -33,4 +33,11 @@ import java.lang.annotation.Target; @Documented public @interface ReadOperation { + /** + * The media type of the result of the operation. + * + * @return the media type + */ + String[] produces() default {}; + } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java index eab3b30eef0..f9c6a3563af 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/WriteOperation.java @@ -33,4 +33,11 @@ import java.lang.annotation.Target; @Documented public @interface WriteOperation { + /** + * The media type of the result of the operation. + * + * @return the media type + */ + String[] produces() default {}; + } diff --git a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java index 0326470ecd5..8cbb519b5e7 100644 --- a/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java +++ b/spring-boot/src/main/java/org/springframework/boot/endpoint/web/WebAnnotationEndpointDiscoverer.java @@ -18,6 +18,7 @@ package org.springframework.boot.endpoint.web; 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; @@ -134,7 +135,8 @@ public class WebAnnotationEndpointDiscoverer extends OperationRequestPredicate requestPredicate = new OperationRequestPredicate( determinePath(endpointId, method), httpMethod, determineConsumedMediaTypes(httpMethod, method), - determineProducedMediaTypes(method)); + determineProducedMediaTypes( + operationAttributes.getStringArray("produces"), method)); OperationInvoker invoker = new ReflectiveOperationInvoker( this.parameterMapper, target, method); if (timeToLive > 0) { @@ -171,7 +173,11 @@ public class WebAnnotationEndpointDiscoverer extends return Collections.emptyList(); } - private Collection determineProducedMediaTypes(Method method) { + private Collection determineProducedMediaTypes(String[] produces, + Method method) { + if (produces.length > 0) { + return Arrays.asList(produces); + } if (Void.class.equals(method.getReturnType()) || void.class.equals(method.getReturnType())) { return Collections.emptyList(); diff --git a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java index 4a20ebaab0b..38ba9efa62f 100644 --- a/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java +++ b/spring-boot/src/test/java/org/springframework/boot/endpoint/web/AbstractWebEndpointIntegrationTests.java @@ -254,6 +254,14 @@ public abstract class AbstractWebEndpointIntegrationTests client.get().uri("/custommediatypes").exchange() + .expectStatus().isOk().expectHeader() + .valueMatches("Content-Type", "text/plain(;charset=.*)?")); + } + protected abstract T createApplicationContext(Class... config); protected abstract int getPort(T context); @@ -422,6 +430,17 @@ public abstract class AbstractWebEndpointIntegrationTests { + Map> endpoints = mapEndpoints( + discoverer.discoverEndpoints()); + assertThat(endpoints).containsOnlyKeys("custommediatypes"); + EndpointInfo endpoint = endpoints + .get("custommediatypes"); + assertThat(requestPredicates(endpoint)).has(requestPredicates( + path("custommediatypes").httpMethod(WebEndpointHttpMethod.GET) + .consumes().produces("text/plain"), + path("custommediatypes").httpMethod(WebEndpointHttpMethod.POST) + .consumes().produces("a/b", "c/d"), + path("custommediatypes").httpMethod(WebEndpointHttpMethod.DELETE) + .consumes().produces("text/plain"))); + }); + } + private void load(Class configuration, Consumer consumer) { this.load((id) -> null, configuration, consumer); @@ -415,6 +434,27 @@ public class WebAnnotationEndpointDiscovererTests { } + @Endpoint(id = "custommediatypes") + static class CustomMediaTypesEndpoint { + + @ReadOperation(produces = "text/plain") + public String read() { + return "read"; + } + + @WriteOperation(produces = { "a/b", "c/d" }) + public String write() { + return "write"; + + } + + @DeleteOperation(produces = "text/plain") + public String delete() { + return "delete"; + } + + } + @Configuration static class MultipleEndpointsConfiguration { @@ -578,6 +618,17 @@ public class WebAnnotationEndpointDiscovererTests { } + @Configuration + @Import(BaseConfiguration.class) + static class CustomMediaTypesEndpointConfiguration { + + @Bean + public CustomMediaTypesEndpoint customMediaTypesEndpoint() { + return new CustomMediaTypesEndpoint(); + } + + } + private static final class RequestPredicateMatcher { private final String path;