Add support for rendering single value in response

If the request is a single value, we already know, so if we also
know that the function returned a single-valued type, then we can
render the single value, instead of an array with a single item.

See gh-36
This commit is contained in:
Dave Syer
2017-05-24 10:16:22 +01:00
parent 0d2418a47b
commit 20a6796793
6 changed files with 123 additions and 32 deletions

View File

@@ -60,8 +60,9 @@ public class FunctionController {
@PostMapping(path = "/**") @PostMapping(path = "/**")
@ResponseBody @ResponseBody
public ResponseEntity<Flux<?>> post( public ResponseEntity<Flux<?>> post(
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.FunctionHandlerMapping.function") Function<Flux<?>, Flux<?>> function, @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function<Flux<?>, Flux<?>> function,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.FunctionHandlerMapping.consumer") Consumer<Flux<?>> consumer, @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.consumer") Consumer<Flux<?>> consumer,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.input_single") Boolean single,
@RequestBody FluxRequest<?> body) { @RequestBody FluxRequest<?> body) {
if (function != null) { if (function != null) {
Flux<?> result = (Flux<?>) function.apply(body.flux()); Flux<?> result = (Flux<?>) function.apply(body.flux());
@@ -84,9 +85,9 @@ public class FunctionController {
@GetMapping(path = "/**") @GetMapping(path = "/**")
@ResponseBody @ResponseBody
public Object get( public Object get(
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.FunctionHandlerMapping.function") Function<Flux<?>, Flux<?>> function, @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.function") Function<Flux<?>, Flux<?>> function,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.FunctionHandlerMapping.supplier") Supplier<Flux<?>> supplier, @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.supplier") Supplier<Flux<?>> supplier,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.FunctionHandlerMapping.argument") String argument) { @RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.argument") String argument) {
if (function != null) { if (function != null) {
return value(function, argument); return value(function, argument);
} }

View File

@@ -28,7 +28,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.FunctionInspector; import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.registry.FunctionCatalog; import org.springframework.cloud.function.registry.FunctionCatalog;
import org.springframework.cloud.function.web.flux.request.FluxHandlerMethodArgumentResolver; import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.HandlerMethod; import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
@@ -43,14 +43,6 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
public class FunctionHandlerMapping extends RequestMappingHandlerMapping public class FunctionHandlerMapping extends RequestMappingHandlerMapping
implements InitializingBean { implements InitializingBean {
public static final String FUNCTION = FunctionHandlerMapping.class.getName()
+ ".function";
public static final String CONSUMER = FunctionHandlerMapping.class.getName()
+ ".consumer";
public static final String SUPPLIER = FunctionHandlerMapping.class.getName()
+ ".supplier";
public static final String ARGUMENT = FunctionHandlerMapping.class.getName()
+ ".argument";
private final FunctionCatalog functions; private final FunctionCatalog functions;
private final FunctionController controller; private final FunctionController controller;
@@ -95,7 +87,7 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Found function for GET: " + path); logger.debug("Found function for GET: " + path);
} }
request.setAttribute(FluxHandlerMethodArgumentResolver.HANDLER, function); request.setAttribute(WebRequestConstants.HANDLER, function);
return handler; return handler;
} }
function = findFunctionForPost(request, path); function = findFunctionForPost(request, path);
@@ -103,7 +95,7 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Found function for POST: " + path); logger.debug("Found function for POST: " + path);
} }
request.setAttribute(FluxHandlerMethodArgumentResolver.HANDLER, function); request.setAttribute(WebRequestConstants.HANDLER, function);
return handler; return handler;
} }
return null; return null;
@@ -116,12 +108,12 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
path = path.startsWith("/") ? path.substring(1) : path; path = path.startsWith("/") ? path.substring(1) : path;
Consumer<Object> consumer = functions.lookupConsumer(path); Consumer<Object> consumer = functions.lookupConsumer(path);
if (consumer != null) { if (consumer != null) {
request.setAttribute(CONSUMER, consumer); request.setAttribute(WebRequestConstants.CONSUMER, consumer);
return consumer; return consumer;
} }
Function<Object, Object> function = functions.lookupFunction(path); Function<Object, Object> function = functions.lookupFunction(path);
if (function != null) { if (function != null) {
request.setAttribute(FUNCTION, function); request.setAttribute(WebRequestConstants.FUNCTION, function);
return function; return function;
} }
return null; return null;
@@ -134,7 +126,7 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
path = path.startsWith("/") ? path.substring(1) : path; path = path.startsWith("/") ? path.substring(1) : path;
Supplier<Object> supplier = functions.lookupSupplier(path); Supplier<Object> supplier = functions.lookupSupplier(path);
if (supplier != null) { if (supplier != null) {
request.setAttribute(SUPPLIER, supplier); request.setAttribute(WebRequestConstants.SUPPLIER, supplier);
return supplier; return supplier;
} }
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
@@ -150,8 +142,8 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
: null; : null;
Function<Object, Object> function = functions.lookupFunction(name); Function<Object, Object> function = functions.lookupFunction(name);
if (function != null) { if (function != null) {
request.setAttribute(FUNCTION, function); request.setAttribute(WebRequestConstants.FUNCTION, function);
request.setAttribute(ARGUMENT, value); request.setAttribute(WebRequestConstants.ARGUMENT, value);
return function; return function;
} }
} }

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2016-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.web.flux.constants;
/**
* Common storage for web request attribute names (in a separate package to avoid cycles).
*
* @author Dave Syer
*
*/
public abstract class WebRequestConstants {
public static final String FUNCTION = WebRequestConstants.class.getName()
+ ".function";
public static final String CONSUMER = WebRequestConstants.class.getName()
+ ".consumer";
public static final String SUPPLIER = WebRequestConstants.class.getName()
+ ".supplier";
public static final String ARGUMENT = WebRequestConstants.class.getName()
+ ".argument";
public static final String HANDLER = WebRequestConstants.class.getName() + ".handler";
public static final String INPUT_SINGLE = WebRequestConstants.class.getName()
+ ".input_single";
}

View File

@@ -30,6 +30,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionInspector; import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -52,9 +53,6 @@ public class FluxHandlerMethodArgumentResolver
private static Log logger = LogFactory private static Log logger = LogFactory
.getLog(FluxHandlerMethodArgumentResolver.class); .getLog(FluxHandlerMethodArgumentResolver.class);
public static final String HANDLER = FluxHandlerMethodArgumentResolver.class.getName()
+ ".HANDLER";
private final ObjectMapper mapper; private final ObjectMapper mapper;
private FunctionInspector inspector; private FunctionInspector inspector;
@@ -74,7 +72,8 @@ public class FluxHandlerMethodArgumentResolver
public Object resolveArgument(MethodParameter parameter, public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception { WebDataBinderFactory binderFactory) throws Exception {
Object handler = webRequest.getAttribute(HANDLER, NativeWebRequest.SCOPE_REQUEST); Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getInputType(inspector.getName(handler)); Class<?> type = inspector.getInputType(inspector.getName(handler));
if (type == null) { if (type == null) {
type = Object.class; type = Object.class;
@@ -96,6 +95,7 @@ public class FluxHandlerMethodArgumentResolver
.constructCollectionLikeType(ArrayList.class, type)); .constructCollectionLikeType(ArrayList.class, type));
} }
catch (JsonMappingException e) { catch (JsonMappingException e) {
nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true);
body = Arrays.asList( body = Arrays.asList(
mapper.readValue(nativeRequest.getContentAsByteArray(), type)); mapper.readValue(nativeRequest.getContentAsByteArray(), type));
} }

View File

@@ -16,6 +16,7 @@
package org.springframework.cloud.function.web.flux.response; package org.springframework.cloud.function.web.flux.response;
import java.lang.reflect.Method;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -24,18 +25,21 @@ import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.apache.el.stream.Optional;
import org.reactivestreams.Publisher; import org.reactivestreams.Publisher;
import org.springframework.cloud.function.context.FunctionInspector; import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.request.FluxHandlerMethodArgumentResolver; import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler; import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer; import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitterReturnValueHandler;
@@ -50,19 +54,28 @@ import reactor.core.publisher.Mono;
*/ */
public class FluxReturnValueHandler implements AsyncHandlerMethodReturnValueHandler { public class FluxReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private static Log logger = LogFactory private static Log logger = LogFactory.getLog(FluxReturnValueHandler.class);
.getLog(FluxReturnValueHandler.class);
private ResponseBodyEmitterReturnValueHandler delegate; private ResponseBodyEmitterReturnValueHandler delegate;
private RequestResponseBodyMethodProcessor single;
private long timeout = 1000L; private long timeout = 1000L;
private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream"); private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream");
private FunctionInspector inspector; private FunctionInspector inspector;
private MethodParameter singleReturnType;
public FluxReturnValueHandler(FunctionInspector inspector, public FluxReturnValueHandler(FunctionInspector inspector,
List<HttpMessageConverter<?>> messageConverters) { List<HttpMessageConverter<?>> messageConverters) {
this.inspector = inspector; this.inspector = inspector;
this.delegate = new ResponseBodyEmitterReturnValueHandler(messageConverters); this.delegate = new ResponseBodyEmitterReturnValueHandler(messageConverters);
this.single = new RequestResponseBodyMethodProcessor(messageConverters);
Method method = ReflectionUtils.findMethod(getClass(), "singleValue");
singleReturnType = new MethodParameter(method, -1);
}
ResponseEntity<Object> singleValue() {
return null;
} }
/** /**
@@ -120,24 +133,46 @@ public class FluxReturnValueHandler implements AsyncHandlerMethodReturnValueHand
} }
Publisher<?> flux = (Publisher<?>) adaptFrom; Publisher<?> flux = (Publisher<?>) adaptFrom;
Object handler = webRequest.getAttribute( Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
FluxHandlerMethodArgumentResolver.HANDLER,
NativeWebRequest.SCOPE_REQUEST); NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getOutputType(inspector.getName(handler)); Class<?> type = inspector.getOutputType(inspector.getName(handler));
Boolean inputSingle = (Boolean) webRequest.getAttribute(
WebRequestConstants.INPUT_SINGLE, NativeWebRequest.SCOPE_REQUEST);
if (inputSingle!=null && inputSingle && isOutputSingle(handler)) {
single.handleReturnValue(Flux.from(flux).blockFirst(), singleReturnType,
mavContainer, webRequest);
return;
}
MediaType mediaType = null; MediaType mediaType = null;
if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) { if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) {
mediaType = MediaType.TEXT_PLAIN; mediaType = MediaType.TEXT_PLAIN;
} else { }
else {
mediaType = findMediaType(webRequest); mediaType = findMediaType(webRequest);
} }
if (logger.isDebugEnabled()) { if (logger.isDebugEnabled()) {
logger.debug("Handling return value " + type + " with media type: " + mediaType); logger.debug(
"Handling return value " + type + " with media type: " + mediaType);
} }
delegate.handleReturnValue(getEmitter(timeout, flux, mediaType), returnType, delegate.handleReturnValue(getEmitter(timeout, flux, mediaType), returnType,
mavContainer, webRequest); mavContainer, webRequest);
} }
private boolean isOutputSingle(Object handler) {
String name = inspector.getName(handler);
Class<?> type = inspector.getOutputType(name);
Class<?> wrapper = inspector.getOutputWrapper(name);
if (wrapper==type) {
return true;
}
if (Mono.class.equals(wrapper) || Optional.class.equals(wrapper)) {
return true;
}
return false;
}
private MediaType findMediaType(NativeWebRequest webRequest) { private MediaType findMediaType(NativeWebRequest webRequest) {
List<MediaType> accepts = Arrays.asList(MediaType.ALL); List<MediaType> accepts = Arrays.asList(MediaType.ALL);
MediaType mediaType = null; MediaType mediaType = null;

View File

@@ -283,6 +283,25 @@ public class RestApplicationTests {
.isEqualTo("[{\"value\":\"FOO\"}]"); .isEqualTo("[{\"value\":\"FOO\"}]");
} }
@Test
public void bareUppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void bareUppercaseFoo() throws Exception {
// Single Foo can be parsed and returns a single value if the function is defined that way
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpFoos")).contentType(MediaType.APPLICATION_JSON)
.body("{\"value\":\"foo\"}"), String.class);
assertThat(result.getBody())
.isEqualTo("{\"value\":\"FOO\"}");
}
@Test @Test
public void bareUppercase() throws Exception { public void bareUppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity ResponseEntity<String> result = rest.exchange(RequestEntity
@@ -382,6 +401,11 @@ public class RestApplicationTests {
.map(value -> new Foo(value.getValue().trim().toUpperCase())); .map(value -> new Foo(value.getValue().trim().toUpperCase()));
} }
@Bean
public Function<Foo, Foo> bareUpFoos() {
return value -> new Foo(value.getValue().trim().toUpperCase());
}
@Bean @Bean
public Function<Flux<Integer>, Flux<String>> wrap() { public Function<Flux<Integer>, Flux<String>> wrap() {
return flux -> flux.log().map(value -> ".." + value + ".."); return flux -> flux.log().map(value -> ".." + value + "..");