Migrate to servlet binder for web features

This commit is contained in:
Dave Syer
2017-08-08 08:27:04 +01:00
parent 540b4d378e
commit 1af0d451cf
107 changed files with 4055 additions and 2010 deletions

View File

@@ -1,32 +0,0 @@
/*
* 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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author Mark Fisher
*/
@SpringBootApplication
public class RestApplication {
public static void main(String[] args) {
SpringApplication.run(RestApplication.class, args);
}
}

View File

@@ -1,123 +0,0 @@
/*
* 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;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.request.FluxRequest;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* @author Dave Syer
* @author Mark Fisher
*/
@Component
public class FunctionController {
private static Log logger = LogFactory.getLog(FunctionController.class);
private FunctionInspector inspector;
private boolean debug = false;
public FunctionController(FunctionInspector inspector) {
this.inspector = inspector;
}
public void setDebug(boolean debug) {
this.debug = debug;
}
@PostMapping(path = "/**")
@ResponseBody
public ResponseEntity<Flux<?>> post(
@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.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) {
if (function != null) {
Flux<?> flux = body.flux();
if (debug) {
flux = flux.log();
}
Flux<?> result = function.apply(flux);
if (logger.isDebugEnabled()) {
logger.debug("Handled POST with function");
}
return ResponseEntity.ok().body(debug ? result.log() : result);
}
if (consumer != null) {
Flux<?> flux = body.flux().cache(); // send a copy back to the caller
if (debug) {
flux = flux.log();
}
consumer.accept(flux);
if (logger.isDebugEnabled()) {
logger.debug("Handled POST with consumer");
}
return ResponseEntity.status(HttpStatus.ACCEPTED).body(flux);
}
throw new IllegalArgumentException("no such function");
}
@GetMapping(path = "/**")
@ResponseBody
public Object get(
@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.constants.WebRequestConstants.supplier") Supplier<Flux<?>> supplier,
@RequestAttribute(required = false, name = "org.springframework.cloud.function.web.flux.constants.WebRequestConstants.argument") String argument) {
if (function != null) {
return value(function, argument);
}
return supplier(supplier);
}
private Flux<?> supplier(Supplier<Flux<?>> supplier) {
Flux<?> result = supplier.get();
if (logger.isDebugEnabled()) {
logger.debug("Handled GET with supplier");
}
return debug ? result.log() : result;
}
private Mono<?> value(Function<Flux<?>, Flux<?>> function,
@PathVariable String value) {
Object input = inspector.convert(function, value);
Mono<?> result = Mono.from(function.apply(Flux.just(input)));
if (logger.isDebugEnabled()) {
logger.debug("Handled GET with function");
}
return debug ? result.log() : result;
}
}

View File

@@ -1,161 +0,0 @@
/*
* Copyright 2012-2015 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;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import javax.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* @author Dave Syer
*
*/
@Configuration
@ConditionalOnClass(RequestMappingHandlerMapping.class)
public class FunctionHandlerMapping extends RequestMappingHandlerMapping
implements InitializingBean {
private final FunctionCatalog functions;
private final FunctionController controller;
@Value("${spring.cloud.function.web.path:}")
private String prefix = "";
@Value("${debug:${DEBUG:false}}")
private String debug = "false";
@Autowired
public FunctionHandlerMapping(FunctionCatalog catalog, FunctionInspector inspector) {
this.functions = catalog;
logger.info("FunctionCatalog: " + catalog + ", FunctionInspector: " + inspector);
setOrder(super.getOrder() - 5);
this.controller = new FunctionController(inspector);
}
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
this.controller.setDebug(!"false".equals(debug));
detectHandlerMethods(controller);
while (prefix.endsWith("/")) {
prefix = prefix.substring(0, prefix.length() - 1);
}
}
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request)
throws Exception {
HandlerMethod handler = super.getHandlerInternal(request);
if (handler == null) {
return null;
}
String path = (String) request
.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
if (StringUtils.hasText(prefix) && !path.startsWith(prefix)) {
return null;
}
if (path.startsWith(prefix)) {
path = path.substring(prefix.length());
}
if (path == null) {
return handler;
}
Object function = findFunctionForGet(request, path);
if (function != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found function for GET: " + path);
}
request.setAttribute(WebRequestConstants.HANDLER, function);
return handler;
}
function = findFunctionForPost(request, path);
if (function != null) {
if (logger.isDebugEnabled()) {
logger.debug("Found function for POST: " + path);
}
request.setAttribute(WebRequestConstants.HANDLER, function);
return handler;
}
return null;
}
private Object findFunctionForPost(HttpServletRequest request, String path) {
if (!request.getMethod().equals("POST")) {
return null;
}
path = path.startsWith("/") ? path.substring(1) : path;
Consumer<Object> consumer = functions.lookupConsumer(path);
if (consumer != null) {
request.setAttribute(WebRequestConstants.CONSUMER, consumer);
return consumer;
}
Function<Object, Object> function = functions.lookupFunction(path);
if (function != null) {
request.setAttribute(WebRequestConstants.FUNCTION, function);
return function;
}
return null;
}
private Object findFunctionForGet(HttpServletRequest request, String path) {
if (!request.getMethod().equals("GET")) {
return null;
}
path = path.startsWith("/") ? path.substring(1) : path;
Supplier<Object> supplier = functions.lookupSupplier(path);
if (supplier != null) {
request.setAttribute(WebRequestConstants.SUPPLIER, supplier);
return supplier;
}
StringBuilder builder = new StringBuilder();
String name = path;
String value = null;
for (String element : path.split("/")) {
if (builder.length() > 0) {
builder.append("/");
}
builder.append(element);
name = builder.toString();
value = path.length() > name.length() ? path.substring(name.length() + 1)
: null;
Function<Object, Object> function = functions.lookupFunction(name);
if (function != null) {
request.setAttribute(WebRequestConstants.FUNCTION, function);
request.setAttribute(WebRequestConstants.ARGUMENT, value);
return function;
}
}
return null;
}
}

View File

@@ -1,114 +0,0 @@
/*
* Copyright 2013-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;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.core.FunctionCatalog;
import org.springframework.cloud.function.web.flux.request.FluxHandlerMethodArgumentResolver;
import org.springframework.cloud.function.web.flux.response.FluxReturnValueHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.ClassUtils;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
* @author Mark Fisher
*/
@Configuration
@ConditionalOnWebApplication
@ConditionalOnClass({ Flux.class, AsyncHandlerMethodReturnValueHandler.class })
public class ReactorAutoConfiguration {
@Autowired
private ApplicationContext context;
@Bean
public FunctionHandlerMapping functionHandlerMapping(FunctionCatalog catalog,
FunctionInspector inspector) {
return new FunctionHandlerMapping(catalog, inspector);
}
@Configuration
@ConditionalOnMissingClass("org.springframework.core.ReactiveAdapter")
protected static class FluxReturnValueConfiguration {
@Bean
public FluxReturnValueHandler fluxReturnValueHandler(FunctionInspector inspector,
HttpMessageConverters converters) {
return new FluxReturnValueHandler(inspector, converters.getConverters());
}
}
@Configuration
protected static class FluxArgumentResolverConfiguration {
@Bean
public FluxHandlerMethodArgumentResolver fluxHandlerMethodArgumentResolver(
FunctionInspector inspector, ObjectMapper mapper) {
return new FluxHandlerMethodArgumentResolver(inspector, mapper);
}
}
@Bean
public BeanPostProcessor fluxRequestMappingHandlerAdapterProcessor() {
return new BeanPostProcessor() {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName)
throws BeansException {
if (bean instanceof RequestMappingHandlerAdapter) {
RequestMappingHandlerAdapter adapter = (RequestMappingHandlerAdapter) bean;
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(
adapter.getArgumentResolvers());
resolvers.add(0,
context.getBean(FluxHandlerMethodArgumentResolver.class));
adapter.setArgumentResolvers(resolvers);
if (!ClassUtils.isPresent("org.springframework.core.ReactiveAdapter",
null)) {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(
adapter.getReturnValueHandlers());
handlers.add(0, context.getBean(FluxReturnValueHandler.class));
adapter.setReturnValueHandlers(handlers);
}
}
return bean;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
throws BeansException {
return bean;
}
};
}
}

View File

@@ -1,39 +0,0 @@
/*
* 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

@@ -1,53 +0,0 @@
/*
* 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.request;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.cloud.function.context.FunctionInspector;
public abstract class DelegateHandler<T> {
private final ListableBeanFactory factory;
private FunctionInspector processor;
private Object handler;
private final Object source;
public DelegateHandler(ListableBeanFactory factory, Object source) {
this.factory = factory;
this.source = source;
}
public Class<?> type() {
return processor().getInputType(handler());
}
private Object handler() {
if (handler == null) {
handler = source instanceof String ? factory.getBean((String) source)
: source;
}
return handler;
}
private FunctionInspector processor() {
if (processor == null) {
processor = factory.getBean(FunctionInspector.class);
}
return processor;
}
}

View File

@@ -1,134 +0,0 @@
/*
* 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.request;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.util.ContentCachingRequestWrapper;
/**
* Converter for request bodies of type <code>Flux<String></code>.
*
* @author Dave Syer
*
*/
public class FluxHandlerMethodArgumentResolver
implements HandlerMethodArgumentResolver, Ordered {
private static Log logger = LogFactory
.getLog(FluxHandlerMethodArgumentResolver.class);
private final ObjectMapper mapper;
private FunctionInspector inspector;
public FluxHandlerMethodArgumentResolver(FunctionInspector inspector,
ObjectMapper mapper) {
this.inspector = inspector;
this.mapper = mapper;
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getInputType(handler);
if (type == null) {
type = Object.class;
}
boolean message = inspector.isMessage(handler);
List<Object> body;
ContentCachingRequestWrapper nativeRequest = new ContentCachingRequestWrapper(
webRequest.getNativeRequest(HttpServletRequest.class));
if (logger.isDebugEnabled()) {
logger.debug("Resolving request body into type: " + type);
}
if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) {
body = Arrays.asList(StreamUtils.copyToString(nativeRequest.getInputStream(),
Charset.forName("UTF-8")));
}
else {
try {
body = mapper.readValue(nativeRequest.getInputStream(),
mapper.getTypeFactory()
.constructCollectionLikeType(ArrayList.class, type));
}
catch (JsonMappingException e) {
nativeRequest.setAttribute(WebRequestConstants.INPUT_SINGLE, true);
body = Arrays.asList(
mapper.readValue(nativeRequest.getContentAsByteArray(), type));
}
}
if (message) {
List<Object> messages = new ArrayList<>();
for (Object payload : body) {
messages.add(MessageBuilder.withPayload(payload)
.copyHeaders(HeaderUtils.fromHttp(new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class))
.getHeaders()))
.build());
}
body = messages;
}
return new FluxRequest<Object>(body);
}
private boolean isPlainText(NativeWebRequest webRequest) {
String value = webRequest.getHeader("Content-Type");
if (value != null) {
return MediaType.valueOf(value).isCompatibleWith(MediaType.TEXT_PLAIN);
}
return false;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return FluxRequest.class.isAssignableFrom(parameter.getParameterType());
}
}

View File

@@ -1,45 +0,0 @@
/*
* 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.request;
import java.util.List;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*
*/
public class FluxRequest<T> {
private List<T> body;
public FluxRequest(List<T> body) {
this.body = body;
}
public Flux<T> flux() {
return Flux.fromIterable(body);
}
public List<T> body() {
return body;
}
}

View File

@@ -1,60 +0,0 @@
/*
* Copyright 2013-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.response;
import org.reactivestreams.Publisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;
/**
* A specialized {@link ResponseBodyEmitter} that handles {@link Flux} return types.
*
* @author Dave Syer
*/
class FluxResponseBodyEmitter extends ResponseBodyEmitter {
private final MediaType mediaType;
private ResponseBodyEmitterSubscriber subscriber;
public FluxResponseBodyEmitter(Publisher<?> observable) {
this(new HttpHeaders(), null, observable);
}
public FluxResponseBodyEmitter(HttpHeaders request, MediaType mediaType,
Publisher<?> observable) {
super();
this.mediaType = mediaType;
this.subscriber = new ResponseBodyEmitterSubscriber(request, mediaType,
observable, this, MediaType.APPLICATION_JSON.isCompatibleWith(mediaType));
}
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
this.subscriber.extendResponse(outputMessage);
HttpHeaders headers = outputMessage.getHeaders();
if (headers.getContentType() == null && this.mediaType != null
&& !MediaType.ALL.equals(this.mediaType)) {
headers.setContentType(this.mediaType);
}
}
}

View File

@@ -1,55 +0,0 @@
/*
* Copyright 2013-2016 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.response;
import org.reactivestreams.Publisher;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import reactor.core.publisher.Flux;
/**
* A specialized {@link ResponseBodyEmitter} that handles {@link Flux} return types with
* SSE streams.
*
* @author Dave Syer
*/
class FluxResponseSseEmitter extends SseEmitter {
private ResponseBodyEmitterSubscriber subscriber;
public FluxResponseSseEmitter(Publisher<?> observable) {
this(new HttpHeaders(), MediaType.valueOf("text/plain"), observable);
}
public FluxResponseSseEmitter(HttpHeaders request, MediaType mediaType,
Publisher<?> observable) {
super();
this.subscriber = new ResponseBodyEmitterSubscriber(request, mediaType,
observable, this, false);
}
@Override
protected void extendResponse(ServerHttpResponse outputMessage) {
super.extendResponse(outputMessage);
this.subscriber.extendResponse(outputMessage);
}
}

View File

@@ -1,267 +0,0 @@
/*
* Copyright 2013-2016 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.response;
import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.el.stream.Optional;
import org.reactivestreams.Publisher;
import org.springframework.cloud.function.context.FunctionInspector;
import org.springframework.cloud.function.web.flux.constants.WebRequestConstants;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.messaging.Message;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandler;
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.ResponseBodyEmitterReturnValueHandler;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* A specialized {@link AsyncHandlerMethodReturnValueHandler} that handles {@link Flux}
* return types.
*
* @author Dave Syer
*/
public class FluxReturnValueHandler implements AsyncHandlerMethodReturnValueHandler {
private static Log logger = LogFactory.getLog(FluxReturnValueHandler.class);
private ResponseBodyEmitterReturnValueHandler delegate;
private RequestResponseBodyMethodProcessor single;
private long timeout = 1000L;
private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream");
private FunctionInspector inspector;
private MethodParameter singleReturnType;
public FluxReturnValueHandler(FunctionInspector inspector,
List<HttpMessageConverter<?>> messageConverters) {
this.inspector = inspector;
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;
}
/**
* Timeout for clients. If no items are seen on an HTTP response in this period then
* the response is closed.
*
* @param timeout the timeout to set
*/
public void setTimeout(long timeout) {
this.timeout = timeout;
}
@Override
public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType) {
if (returnValue != null) {
return supportsReturnType(returnType);
}
return false;
}
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.getParameterType() != null
&& (Publisher.class.isAssignableFrom(returnType.getParameterType())
|| isResponseEntity(returnType)))
|| Publisher.class
.isAssignableFrom(returnType.getMethod().getReturnType());
}
private boolean isResponseEntity(MethodParameter returnType) {
if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
Class<?> bodyType = ResolvableType.forMethodParameter(returnType)
.getGeneric(0).resolve();
return bodyType != null && Flux.class.isAssignableFrom(bodyType);
}
return false;
}
@Override
public void handleReturnValue(Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws Exception {
if (returnValue == null) {
mavContainer.setRequestHandled(true);
return;
}
Object adaptFrom = returnValue;
if (returnValue instanceof ResponseEntity) {
ResponseEntity<?> value = (ResponseEntity<?>) returnValue;
adaptFrom = value.getBody();
HttpServletResponse response = webRequest
.getNativeResponse(HttpServletResponse.class);
response.setStatus(value.getStatusCodeValue());
HttpHeaders headers = value.getHeaders();
for (String name : headers.keySet()) {
List<String> list = headers.get(name);
for (String header : list) {
response.addHeader(name, header);
}
}
}
Publisher<?> flux = (Publisher<?>) adaptFrom;
Object handler = webRequest.getAttribute(WebRequestConstants.HANDLER,
NativeWebRequest.SCOPE_REQUEST);
Class<?> type = inspector.getOutputType(handler);
boolean inputSingle = isInputSingle(webRequest, handler);
if (inputSingle && isOutputSingle(handler)) {
Object result = Flux.from(flux).blockFirst();
if (result instanceof Message) {
Message<?> message = (Message<?>) result;
result = message.getPayload();
addHeaders(webRequest, message);
}
single.handleReturnValue(result, singleReturnType, mavContainer, webRequest);
return;
}
MediaType mediaType = null;
if (isPlainText(webRequest) && CharSequence.class.isAssignableFrom(type)) {
mediaType = MediaType.TEXT_PLAIN;
}
else {
mediaType = findMediaType(webRequest);
}
if (logger.isDebugEnabled()) {
logger.debug(
"Handling return value " + type + " with media type: " + mediaType);
}
ServletServerHttpRequest request = new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class));
delegate.handleReturnValue(
getEmitter(timeout, flux, mediaType, request.getHeaders()), returnType,
mavContainer, webRequest);
}
private void addHeaders(NativeWebRequest webRequest, Message<?> message) {
HttpServletResponse response = webRequest
.getNativeResponse(HttpServletResponse.class);
ServletServerHttpRequest request = new ServletServerHttpRequest(
webRequest.getNativeRequest(HttpServletRequest.class));
HttpHeaders headers = HeaderUtils.fromMessage(message.getHeaders(),
request.getHeaders());
for (String name : headers.keySet()) {
for (Object object : headers.get(name)) {
response.addHeader(name, object.toString());
}
}
}
private boolean isInputSingle(NativeWebRequest webRequest, Object handler) {
Boolean single = (Boolean) webRequest.getAttribute(
WebRequestConstants.INPUT_SINGLE, NativeWebRequest.SCOPE_REQUEST);
if (single == null) {
return handler instanceof Supplier;
}
return single;
}
private boolean isOutputSingle(Object handler) {
Class<?> type = inspector.getOutputType(handler);
Class<?> wrapper = inspector.getOutputWrapper(handler);
if (Stream.class.isAssignableFrom(type)) {
return false;
}
if (wrapper == type) {
return true;
}
if (Mono.class.equals(wrapper) || Optional.class.equals(wrapper)) {
return true;
}
return false;
}
private MediaType findMediaType(NativeWebRequest webRequest) {
List<MediaType> accepts = Arrays.asList(MediaType.ALL);
MediaType mediaType = null;
if (webRequest.getHeader("Accept") != null) {
accepts = MediaType.parseMediaTypes(webRequest.getHeader("Accept"));
for (MediaType accept : accepts) {
if (!MediaType.ALL.equals(accept)
&& MediaType.APPLICATION_JSON.isCompatibleWith(accept)) {
mediaType = MediaType.APPLICATION_JSON;
// Prefer JSON if that is acceptable
break;
}
else if (mediaType == null) {
mediaType = accept;
}
}
}
if (mediaType == null) {
mediaType = MediaType.APPLICATION_JSON;
}
return mediaType;
}
private boolean isPlainText(NativeWebRequest webRequest) {
String value = webRequest.getHeader("Content-Type");
if (value != null) {
return MediaType.valueOf(value).isCompatibleWith(MediaType.TEXT_PLAIN);
}
return false;
}
private ResponseBodyEmitter getEmitter(Long timeout, Publisher<?> flux,
MediaType mediaType, HttpHeaders request) {
Publisher<?> exported = flux instanceof Mono ? Mono.from(flux)
: Flux.from(flux).timeout(Duration.ofMillis(timeout), Flux.empty());
if (!MediaType.ALL.equals(mediaType)
&& EVENT_STREAM.isCompatibleWith(mediaType)) {
// TODO: more subtle content negotiation
return new FluxResponseSseEmitter(request, MediaType.APPLICATION_JSON,
exported);
}
return new FluxResponseBodyEmitter(request, mediaType, exported);
}
}

View File

@@ -1,212 +0,0 @@
/*
* Copyright 2013-2016 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.response;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.springframework.cloud.function.web.util.HeaderUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.messaging.Message;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyEmitter;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Subscriber that emits any value produced by the {@link Flux} into the delegated
* {@link ResponseBodyEmitter}.
*
* @author Dave Syer
*/
class ResponseBodyEmitterSubscriber implements Subscriber<Object> {
private final MediaType mediaType;
private Subscription subscription;
private final ResponseBodyEmitter responseBodyEmitter;
private boolean completed;
private boolean firstElementWritten;
private boolean single;
private final boolean json;
private Message<?> first;
private final HttpHeaders request;
public ResponseBodyEmitterSubscriber(HttpHeaders request, MediaType mediaType,
Publisher<?> observable, ResponseBodyEmitter responseBodyEmitter,
boolean json) {
this.request = request;
this.mediaType = mediaType;
this.responseBodyEmitter = responseBodyEmitter;
this.json = json;
this.responseBodyEmitter.onTimeout(new Timeout());
this.responseBodyEmitter.onCompletion(new Complete());
this.single = observable instanceof Mono;
observable.subscribe(this);
}
public void extendResponse(ServerHttpResponse response) {
headers(response);
}
@Override
public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
subscription.request(Long.MAX_VALUE);
}
@Override
public void onNext(Object value) {
Object object = value;
if (object instanceof Message) {
Message<?> message = (Message<?>) object;
object = message.getPayload();
this.first = message;
}
try {
if (isJson()) {
if (!this.firstElementWritten) {
if (!single) {
responseBodyEmitter.send("[");
this.firstElementWritten = true;
}
}
else {
responseBodyEmitter.send(",");
}
if (!single && object.getClass() == String.class
&& !((String) object).contains("\"")) {
object = "\"" + object + "\"";
}
}
if (!completed) {
responseBodyEmitter.send(object, mediaType);
}
}
catch (
IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
private void headers(ServerHttpResponse response) {
if (this.first != null) {
Message<?> message = first;
try {
HttpHeaders headers = HeaderUtils.fromMessage(message.getHeaders(),
request);
for (String name : headers.keySet()) {
for (String value : headers.get(name)) {
response.getHeaders().add(name, value);
}
}
}
catch (Exception e) {
// Headers could not be set
}
}
}
@Override
public void onError(Throwable e) {
if (!completed) {
completed = true;
try {
if (isJson()) {
if (!single) {
if (!this.firstElementWritten) {
responseBodyEmitter.send("[]");
}
else {
responseBodyEmitter.send("]");
}
}
}
if (e instanceof TimeoutException) {
responseBodyEmitter.complete();
}
else {
responseBodyEmitter.completeWithError(e);
}
}
catch (IOException ex) {
throw new RuntimeException(ex.getMessage(), ex);
}
}
}
@Override
public void onComplete() {
if (!completed) {
completed = true;
try {
if (isJson()) {
if (!single) {
if (!this.firstElementWritten) {
responseBodyEmitter.send("[");
}
responseBodyEmitter.send("]");
}
}
}
catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
responseBodyEmitter.complete();
}
}
private boolean isJson() {
return json;
}
class Complete implements Runnable {
@Override
public void run() {
ResponseBodyEmitterSubscriber.this.subscription.cancel();
}
}
class Timeout implements Runnable {
@Override
public void run() {
onComplete();
ResponseBodyEmitterSubscriber.this.subscription.cancel();
}
}
}

View File

@@ -1,84 +0,0 @@
/*
* 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.util;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.http.HttpHeaders;
import org.springframework.messaging.MessageHeaders;
import org.springframework.util.ObjectUtils;
/**
* @author Dave Syer
*
*/
public class HeaderUtils {
public static HttpHeaders fromMessage(MessageHeaders headers, HttpHeaders request) {
HttpHeaders result = new HttpHeaders();
for (String name : headers.keySet()) {
Object value = headers.get(name);
name = name.toLowerCase();
if (MessageHeaders.ID.equals(name)) {
continue;
}
if (request.containsKey(name)) {
if (name.startsWith("x-")) {
if (!name.startsWith("x-forwarded")) {
Collection<?> values = multi(value);
for (Object object : values) {
result.set(name, object.toString());
}
}
}
}
else {
Collection<?> values = multi(value);
for (Object object : values) {
result.set(name, object.toString());
}
}
}
return result;
}
private static Collection<?> multi(Object value) {
if (value instanceof Collection) {
Collection<?> collection = (Collection<?>) value;
return collection;
}
else if (ObjectUtils.isArray(value)) {
Object[] values = ObjectUtils.toObjectArray(value);
return Arrays.asList(values);
}
return Arrays.asList(value);
}
public static MessageHeaders fromHttp(HttpHeaders headers) {
Map<String, Object> map = new LinkedHashMap<>();
for (String name : headers.keySet()) {
Collection<?> values = multi(headers.get(name));
name = name.toLowerCase();
Object value = values == null ? null
: (values.size() == 1 ? values.iterator().next() : values);
map.put(name, value);
}
return new MessageHeaders(map);
}
}

View File

@@ -1,9 +0,0 @@
{"properties": [
{
"name": "spring.cloud.function.web.path",
"type": "java.lang.String",
"description": "Path to web resources for functions (should start with / if not empty).",
"defaultValue": ""
}]
}

View File

@@ -1,2 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.function.web.flux.ReactorAutoConfiguration

View File

@@ -1,392 +0,0 @@
/*
* 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.mvc;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.cloud.function.mvc.MvcRestApplicationTests.TestConfiguration;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* Tests for vanilla MVC handling (no function layer). Validates the MVC customizations
* that are added in this project independently of the specific concerns of function.
*
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = TestConfiguration.class, webEnvironment = WebEnvironment.RANDOM_PORT)
public class MvcRestApplicationTests {
private static final MediaType EVENT_STREAM = MediaType.valueOf("text/event-stream");
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
@Autowired
private TestConfiguration test;
@Before
public void init() {
test.list.clear();
}
@Test
public void wordsSSE() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/words")).accept(EVENT_STREAM).build(),
String.class).getBody()).isEqualTo(sse("foo", "bar"));
}
@Test
public void wordsJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/words"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Fix error handling")
public void errorJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/bang"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void foos() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/foos")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void getMore() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/get/more")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
public void updates() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/updates")).body("one\ntwo"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("onetwo");
}
@Test
public void updatesJson() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/updates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
}
@Test
public void addFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/addFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void timeout() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/timeout")).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void emptyJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/empty"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[]");
}
@Test
public void sentences() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/sentences")).build(), String.class)
.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptAny() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(),
String.class).getBody())
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptJson() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.get(new URI("/sentences"))
.accept(MediaType.APPLICATION_JSON).build(),
String.class);
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
assertThat(result.getHeaders().getContentType())
.isGreaterThanOrEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void uppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void uppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void transform() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/transform")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void postMore() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/post/more")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"[FOO]\",\"[BAR]\"]");
}
@Test
public void uppercaseGet() {
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("[FOO]");
}
@Test
public void convertGet() {
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
}
@Test
public void convertGetJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/entity/321"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("{\"value\":321}");
}
@Test
public void uppercaseJsonStream() throws Exception {
assertThat(rest
.exchange(RequestEntity.post(new URI("/maps"))
.contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class)
.getBody()).isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void uppercaseSSE() throws Exception {
assertThat(rest.exchange(RequestEntity.post(new URI("/uppercase"))
.accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class).getBody())
.isEqualTo(sse("[FOO]", "[BAR]"));
}
private String sse(String... values) {
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
}
@EnableAutoConfiguration
@RestController
@Configuration
public static class TestConfiguration {
private List<String> list = new ArrayList<>();
@PostMapping({ "/uppercase", "/transform", "/post/more" })
public Flux<?> uppercase(@RequestBody List<String> flux) {
return Flux.fromIterable(flux).log()
.map(value -> "[" + value.trim().toUpperCase() + "]");
}
@PostMapping("/upFoos")
public Flux<Foo> upFoos(@RequestBody List<Foo> list) {
return Flux.fromIterable(list).log()
.map(value -> new Foo(value.getValue().trim().toUpperCase()));
}
@GetMapping("/uppercase/{id}")
public Mono<?> uppercaseGet(@PathVariable String id) {
return Mono.just(id).map(value -> "[" + value.trim().toUpperCase() + "]");
}
@GetMapping("/wrap/{id}")
public Mono<?> wrapGet(@PathVariable int id) {
return Mono.just(id).log().map(value -> ".." + value + "..");
}
@GetMapping("/entity/{id}")
public Mono<Map<String, Object>> entity(@PathVariable Integer id) {
return Mono.just(id).log()
.map(value -> Collections.singletonMap("value", value));
}
@PostMapping("/maps")
public Flux<Map<String, String>> maps(
@RequestBody List<Map<String, String>> flux) {
return Flux.fromIterable(flux).map(value -> {
value.put("value", value.get("value").trim().toUpperCase());
return value;
});
}
@GetMapping({ "/words", "/get/more" })
public Flux<Object> words() {
return Flux.fromArray(new String[] { "foo", "bar" });
}
@GetMapping("/foos")
public Flux<Foo> foos() {
return Flux.just(new Foo("foo"), new Foo("bar"));
}
@PostMapping("/updates")
@ResponseStatus(HttpStatus.ACCEPTED)
public Flux<?> updates(@RequestBody List<String> list) {
Flux<String> flux = Flux.fromIterable(list).cache();
flux.subscribe(value -> this.list.add(value));
return flux;
}
@PostMapping("/addFoos")
@ResponseStatus(HttpStatus.ACCEPTED)
public Flux<Foo> addFoos(@RequestBody List<Foo> list) {
Flux<Foo> flux = Flux.fromIterable(list).cache();
flux.subscribe(value -> this.list.add(value.getValue()));
return flux;
}
@GetMapping("/bang")
public Flux<?> bang() {
return Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
if (value.equals("bar")) {
throw new RuntimeException("Bar");
}
return value;
});
}
@GetMapping("/empty")
public Flux<?> empty() {
return Flux.fromIterable(Collections.emptyList());
}
@GetMapping("/timeout")
public Flux<?> timeout() {
return Flux.defer(() -> Flux.<String>create(emitter -> {
emitter.next("foo");
}).timeout(Duration.ofMillis(100L), Flux.empty()));
}
@GetMapping("/sentences")
public Flux<List<String>> sentences() {
return Flux.just(Arrays.asList("go", "home"), Arrays.asList("come", "back"));
}
}
public static class Foo {
private String value;
public Foo(String value) {
this.value = value;
}
Foo() {
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -1,87 +0,0 @@
/*
* Copyright 2012-2015 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.scan;
import java.net.URI;
import java.util.function.Function;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ComponentTests {
@LocalServerPort
private int port;
@Autowired
private Greeter greeter;
@Autowired
private TestRestTemplate rest;
@Test
public void contextLoads() throws Exception {
assertThat(greeter).isNotNull();
}
@Test
public void greeter() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/greeter"))
.contentType(MediaType.TEXT_PLAIN).body("World"),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("Hello World");
}
@SpringBootApplication
@ComponentScan
protected static class TestConfiguration {
}
@Component("greeter")
protected static class Greeter implements Function<Flux<String>, Flux<String>> {
@Override
public Flux<String> apply(Flux<String> flux) {
return flux.map(name -> "Hello " + name);
}
}
}

View File

@@ -1,77 +0,0 @@
/*
* Copyright 2012-2015 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;
import java.net.URI;
import java.util.function.Supplier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "spring.cloud.function.web.path=/functions")
public class PrefixTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/functions/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void missing() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}
@EnableAutoConfiguration
@org.springframework.boot.test.context.TestConfiguration
protected static class TestConfiguration {
@Bean({ "words", "get/more" })
public Supplier<Flux<String>> words() {
return () -> Flux.fromArray(new String[] { "foo", "bar" });
}
}
}

View File

@@ -1,599 +0,0 @@
/*
* 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;
import java.net.URI;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class RestApplicationTests {
private static final MediaType EVENT_STREAM = MediaType.TEXT_EVENT_STREAM;
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
@Autowired
private ApplicationConfiguration test;
@Before
public void init() {
test.list.clear();
}
@Test
public void staticResource() throws Exception {
assertThat(rest.getForObject("/test.html", String.class)).contains("<body>Test");
}
@Test
public void wordsSSE() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/words")).accept(EVENT_STREAM).build(),
String.class).getBody()).isEqualTo(sse("foo", "bar"));
}
@Test
public void wordsJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/words"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Fix error handling")
public void errorJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/bang"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void word() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/word")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("foo");
}
@Test
public void foos() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/foos")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void qualifierFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/foos"))
.contentType(MediaType.APPLICATION_JSON).body("[\"foo\",\"bar\"]"),
String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"[FOO]\"},{\"value\":\"[BAR]\"}]");
}
@Test
public void getMore() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/get/more")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
public void bareWords() throws Exception {
ResponseEntity<String> result = rest
.exchange(RequestEntity.get(new URI("/bareWords")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@Test
@Ignore("Should this even work? Or do we need to be explicit about the JSON?")
public void updates() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/updates")).body("one\ntwo"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("onetwo");
}
@Test
public void updatesJson() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/updates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
}
@Test
public void addFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/addFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"foo\"},{\"value\":\"bar\"}]");
}
@Test
public void bareUpdates() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUpdates")).contentType(MediaType.APPLICATION_JSON)
.body("[\"one\",\"two\"]"), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.ACCEPTED);
assertThat(test.list).hasSize(2);
assertThat(result.getBody()).isEqualTo("[\"one\",\"two\"]");
}
@Test
public void timeoutJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/timeout"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[\"foo\"]");
}
@Test
public void emptyJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/empty"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("[]");
}
@Test
public void sentences() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/sentences")).build(), String.class)
.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptAny() throws Exception {
assertThat(rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(MediaType.ALL).build(),
String.class).getBody())
.isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
}
@Test
public void sentencesAcceptJson() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.get(new URI("/sentences"))
.accept(MediaType.APPLICATION_JSON).build(),
String.class);
assertThat(result.getBody()).isEqualTo("[[\"go\",\"home\"],[\"come\",\"back\"]]");
assertThat(result.getHeaders().getContentType())
.isGreaterThanOrEqualTo(MediaType.APPLICATION_JSON);
}
@Test
public void sentencesAcceptSse() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/sentences")).accept(EVENT_STREAM).build(),
String.class);
assertThat(result.getBody())
.isEqualTo(sse("[\"go\",\"home\"]", "[\"come\",\"back\"]"));
assertThat(result.getHeaders().getContentType().isCompatibleWith(EVENT_STREAM))
.isTrue();
}
@Test
public void uppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/uppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void messages() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/messages")).contentType(MediaType.APPLICATION_JSON)
.header("x-foo", "bar").body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
assertThat(result.getHeaders().getFirst("x-foo")).isEqualTo("bar");
assertThat(result.getHeaders()).doesNotContainKey("id");
}
@Test
public void headers() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/headers")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
assertThat(result.getHeaders().getFirst("foo")).isEqualTo("bar");
assertThat(result.getHeaders()).doesNotContainKey("id");
}
@Test
public void uppercaseSingleValue() throws Exception {
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/uppercase"))
.contentType(MediaType.TEXT_PLAIN).body("foo"),
String.class);
assertThat(result.getBody()).isEqualTo("(FOO)");
}
@Test
@Ignore("WebFlux would split the request body into lines: TODO make this work the same")
public void uppercasePlainText() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.post(new URI("/uppercase"))
.contentType(MediaType.TEXT_PLAIN).body("foo\nbar"),
String.class);
assertThat(result.getBody()).isEqualTo("(FOO)(BAR)");
}
@Test
public void uppercaseFoos() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("[{\"value\":\"foo\"},{\"value\":\"bar\"}]"), String.class);
assertThat(result.getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void uppercaseFoo() throws Exception {
// Single Foo can be parsed
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/upFoos")).contentType(MediaType.APPLICATION_JSON)
.body("{\"value\":\"foo\"}"), String.class);
assertThat(result.getBody()).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
public void bareUppercase() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/bareUppercase")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void transform() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/transform")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void postMore() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity
.post(new URI("/post/more")).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class);
assertThat(result.getBody()).isEqualTo("[\"(FOO)\",\"(BAR)\"]");
}
@Test
public void postMoreFoo() {
assertThat(rest.getForObject("/post/more/foo", String.class)).isEqualTo("(FOO)");
}
@Test
public void uppercaseGet() {
assertThat(rest.getForObject("/uppercase/foo", String.class)).isEqualTo("(FOO)");
}
@Test
public void convertGet() {
assertThat(rest.getForObject("/wrap/123", String.class)).isEqualTo("..123..");
}
@Test
public void convertPost() throws Exception {
ResponseEntity<String> result = rest.exchange(RequestEntity.post(new URI("/wrap"))
.contentType(MediaType.TEXT_PLAIN).body("123"), String.class);
assertThat(result.getBody()).isEqualTo("..123..");
}
@Test
public void convertPostJson() throws Exception {
// If you POST a single value to a Function<Flux<Integer>,Flux<Integer>> it can't
// determine if the output is single valued, so it has to send an array back
ResponseEntity<String> result = rest
.exchange(
RequestEntity.post(new URI("/doubler"))
.contentType(MediaType.TEXT_PLAIN).body("123"),
String.class);
assertThat(result.getBody()).isEqualTo("[246]");
}
@Test
public void supplierFirst() {
assertThat(rest.getForObject("/not/a/function", String.class))
.isEqualTo("[\"hello\"]");
}
@Test
public void convertGetJson() throws Exception {
assertThat(rest
.exchange(RequestEntity.get(new URI("/entity/321"))
.accept(MediaType.APPLICATION_JSON).build(), String.class)
.getBody()).isEqualTo("{\"value\":321}");
}
@Test
public void uppercaseJsonArray() throws Exception {
assertThat(rest.exchange(
RequestEntity.post(new URI("/maps"))
.contentType(MediaType.APPLICATION_JSON)
// The new line in the middle is optional
.body("[{\"value\":\"foo\"},\n{\"value\":\"bar\"}]"),
String.class).getBody())
.isEqualTo("[{\"value\":\"FOO\"},{\"value\":\"BAR\"}]");
}
@Test
public void uppercaseSSE() throws Exception {
assertThat(rest.exchange(RequestEntity.post(new URI("/uppercase"))
.accept(EVENT_STREAM).contentType(MediaType.APPLICATION_JSON)
.body("[\"foo\",\"bar\"]"), String.class).getBody())
.isEqualTo(sse("(FOO)", "(BAR)"));
}
private String sse(String... values) {
return "data:" + StringUtils.arrayToDelimitedString(values, "\n\ndata:") + "\n\n";
}
@EnableAutoConfiguration
@TestConfiguration
public static class ApplicationConfiguration {
private List<String> list = new ArrayList<>();
@Bean({ "uppercase", "transform", "post/more" })
public Function<Flux<String>, Flux<String>> uppercase() {
return flux -> flux.log()
.map(value -> "(" + value.trim().toUpperCase() + ")");
}
@Bean
public Function<String, String> bareUppercase() {
return value -> "(" + value.trim().toUpperCase() + ")";
}
@Bean
public Function<Message<String>, Message<String>> messages() {
return value -> MessageBuilder
.withPayload("(" + value.getPayload().trim().toUpperCase() + ")")
.copyHeaders(value.getHeaders()).build();
}
@Bean
public Function<Flux<Message<String>>, Flux<Message<String>>> headers() {
return flux -> flux.map(value -> MessageBuilder
.withPayload("(" + value.getPayload().trim().toUpperCase() + ")")
.setHeader("foo", "bar").build());
}
@Bean
public Function<Flux<Foo>, Flux<Foo>> upFoos() {
return flux -> flux.log()
.map(value -> new Foo(value.getValue().trim().toUpperCase()));
}
@Bean
public Function<Foo, Foo> bareUpFoos() {
return value -> new Foo(value.getValue().trim().toUpperCase());
}
@Bean
public Function<Flux<Integer>, Flux<String>> wrap() {
return flux -> flux.log().map(value -> ".." + value + "..");
}
@Bean
public Function<Flux<Integer>, Flux<Integer>> doubler() {
return flux -> flux.log().map(value -> 2 * value);
}
@Bean
public Function<Flux<Integer>, Flux<Map<String, Object>>> entity() {
return flux -> flux.log()
.map(value -> Collections.singletonMap("value", value));
}
@Bean
public Function<Flux<HashMap<String, String>>, Flux<Map<String, String>>> maps() {
return flux -> flux.map(value -> {
value.put("value", value.get("value").trim().toUpperCase());
return value;
});
}
@Bean({ "words", "get/more" })
public Supplier<Flux<String>> words() {
return () -> Flux.just("foo", "bar");
}
@Bean
public Supplier<String> word() {
return () -> "foo";
}
@Bean
public Supplier<Flux<Foo>> foos() {
return () -> Flux.just(new Foo("foo"), new Foo("bar"));
}
@Bean
@Qualifier("foos")
public Function<String, Foo> qualifier() {
return value -> new Foo("[" + value.trim().toUpperCase() + "]");
}
@Bean
public Supplier<List<String>> bareWords() {
return () -> Arrays.asList("foo", "bar");
}
@Bean
public Consumer<Flux<String>> updates() {
return flux -> flux.subscribe(value -> list.add(value));
}
@Bean
public Consumer<Flux<Foo>> addFoos() {
return flux -> flux.subscribe(value -> list.add(value.getValue()));
}
@Bean
public Consumer<String> bareUpdates() {
return value -> list.add(value);
}
@Bean
public Supplier<Flux<String>> bang() {
return () -> Flux.fromArray(new String[] { "foo", "bar" }).map(value -> {
if (value.equals("bar")) {
throw new RuntimeException("Bar");
}
return value;
});
}
@Bean
public Supplier<Flux<String>> empty() {
return () -> Flux.fromIterable(Collections.emptyList());
}
@Bean("not/a/function")
public Supplier<Flux<String>> supplier() {
return () -> Flux.just("hello");
}
@Bean("not/a")
public Function<Flux<String>, Flux<String>> function() {
return input -> Flux.just("bye");
}
@Bean
public Supplier<Flux<String>> timeout() {
return () -> Flux.defer(() -> Flux.<String>create(emitter -> {
emitter.next("foo");
}).timeout(Duration.ofMillis(100L), Flux.empty()));
}
@Bean
public Supplier<Flux<List<String>>> sentences() {
return () -> Flux.just(Arrays.asList("go", "home"),
Arrays.asList("come", "back"));
}
}
public static class Foo {
private String value;
public Foo(String value) {
this.value = value;
}
Foo() {
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}

View File

@@ -1,97 +0,0 @@
/*
* Copyright 2012-2015 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;
import java.net.URI;
import java.util.function.Supplier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.context.embedded.LocalServerPort;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import reactor.core.publisher.Flux;
/**
* @author Dave Syer
*
*/
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class SingletonTests {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate rest;
@Test
public void words() throws Exception {
ResponseEntity<String> result = rest.exchange(
RequestEntity.get(new URI("/words")).build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isEqualTo("[\"foo\",\"bar\"]");
}
@EnableAutoConfiguration
@org.springframework.boot.test.context.TestConfiguration
protected static class TestConfiguration {
@Bean
public static BeanDefinitionRegistryPostProcessor processor() {
return new BeanDefinitionRegistryPostProcessor() {
@Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory)
throws BeansException {
}
@Override
public void postProcessBeanDefinitionRegistry(
BeanDefinitionRegistry registry) throws BeansException {
// Simulates what happens when you add a compiled function
RootBeanDefinition beanDefinition = new RootBeanDefinition(MySupplier.class);
registry.registerBeanDefinition("words", beanDefinition);
}
};
}
}
static class MySupplier implements Supplier<Flux<String>> {
@Override
public Flux<String> get() {
return Flux.just("foo", "bar");
}
}
}

View File

@@ -1 +0,0 @@
<html><body>Test</body></html>