GH-238 Added initial support for RoutingFunction
- Added initial implementation of RoutingFunction which is bootstrapped optionally based on setting ‘spring.cloud.function.routing.enabled’ property to true. - Added initial documentation and tests Resolves #238
This commit is contained in:
@@ -70,6 +70,38 @@ of a non publisher type (which is normal), it will be converted to a
|
|||||||
function that returns a publisher, so that it can be subscribed to in
|
function that returns a publisher, so that it can be subscribed to in
|
||||||
a controlled way.
|
a controlled way.
|
||||||
|
|
||||||
|
=== Function Routing
|
||||||
|
|
||||||
|
Since version 2.2 Spring Cloud Function provides routing feature allowing
|
||||||
|
you to invoke a single function which acts as a router to an actual function you wish to invoke
|
||||||
|
This feature is very useful in certain FAAS environments where maintaining configurations
|
||||||
|
for several functions could be cumbersome or exposing more then one function is not possible.
|
||||||
|
|
||||||
|
You enable this feature via `spring.cloud.function.routing.enabled` property setting it
|
||||||
|
to `true` (default is `false`).
|
||||||
|
This enables `RoutingFunction` under the name `router` which is loaded in FunctionCatalog.
|
||||||
|
|
||||||
|
This function has the following signature:
|
||||||
|
|
||||||
|
[source, java]
|
||||||
|
----
|
||||||
|
public class RoutingFunction implements Function<Publisher<Message<?>>, Publisher<?>>, Consumer<Publisher<Message<?>>> {
|
||||||
|
. . .
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
This allows the above function to act as both `Function` and `Consumer`.
|
||||||
|
As you can see it takes `Message<?>` as an input argument. This allows you to communicate
|
||||||
|
the name of the actual function you want to invoke by providing `function.name` Message header.
|
||||||
|
|
||||||
|
In specific execution environments/models the adapters are responsible to translate and communicate `function.name`
|
||||||
|
via Message header. For example, when using _spring-cloud-function-web_ you can provide `function.name` as an HTTP
|
||||||
|
header and the framework will propagate it as well as other HTTP headers as Message headers.
|
||||||
|
|
||||||
|
Using Message also allows us to benefit from `MessageConverter`s to convert incoming request to the actual input type
|
||||||
|
of the target function
|
||||||
|
|
||||||
|
|
||||||
=== Kotlin Lambda support
|
=== Kotlin Lambda support
|
||||||
|
|
||||||
We also provide support for Kotlin lambdas (since v2.0).
|
We also provide support for Kotlin lambdas (since v2.0).
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import java.net.URL;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
@@ -42,6 +41,7 @@ import org.springframework.boot.SpringApplication;
|
|||||||
import org.springframework.boot.WebApplicationType;
|
import org.springframework.boot.WebApplicationType;
|
||||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||||
import org.springframework.cloud.function.context.config.FunctionContextUtils;
|
import org.springframework.cloud.function.context.config.FunctionContextUtils;
|
||||||
|
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||||
import org.springframework.context.ApplicationContextInitializer;
|
import org.springframework.context.ApplicationContextInitializer;
|
||||||
import org.springframework.context.ConfigurableApplicationContext;
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
import org.springframework.context.support.GenericApplicationContext;
|
import org.springframework.context.support.GenericApplicationContext;
|
||||||
@@ -196,7 +196,6 @@ public abstract class AbstractSpringFunctionAdapterInitializer<C> implements Clo
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
//@SuppressWarnings("unchecked")
|
|
||||||
protected Object convertOutput(Object input, Object output) {
|
protected Object convertOutput(Object input, Object output) {
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
@@ -368,22 +367,19 @@ public abstract class AbstractSpringFunctionAdapterInitializer<C> implements Clo
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.catalog.size() == 1) {
|
|
||||||
Iterator<String> names = this.catalog.getNames(Function.class).iterator();
|
if (this.catalog.size() >= 1 && this.catalog.size() <= 2) { // we may have RoutingFunction function
|
||||||
if (names.hasNext()) {
|
String functionName = this.catalog.getNames(Function.class).stream()
|
||||||
this.function = this.catalog.lookup(Function.class, names.next());
|
.filter(n -> !n.equals(RoutingFunction.FUNCTION_NAME))
|
||||||
|
.findFirst().orElseGet(() -> null);
|
||||||
|
if (functionName != null) {
|
||||||
|
this.function = this.catalog.lookup(Function.class, functionName);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
functionName = this.catalog.getNames(Supplier.class).stream()
|
||||||
names = this.catalog.getNames(Consumer.class).iterator();
|
.findFirst().orElseGet(() -> null);
|
||||||
if (names.hasNext()) {
|
if (functionName != null) {
|
||||||
this.consumer = this.catalog.lookup(Consumer.class, names.next());
|
this.supplier = this.catalog.lookup(Supplier.class, functionName);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
names = this.catalog.getNames(Supplier.class).iterator();
|
|
||||||
if (names.hasNext()) {
|
|
||||||
this.supplier = this.catalog.lookup(Supplier.class, names.next());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import reactor.core.publisher.Flux;
|
|||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.beans.factory.BeanNameAware;
|
import org.springframework.beans.factory.BeanNameAware;
|
||||||
|
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||||
import org.springframework.cloud.function.core.FluxConsumer;
|
import org.springframework.cloud.function.core.FluxConsumer;
|
||||||
import org.springframework.cloud.function.core.FluxFunction;
|
import org.springframework.cloud.function.core.FluxFunction;
|
||||||
import org.springframework.cloud.function.core.FluxSupplier;
|
import org.springframework.cloud.function.core.FluxSupplier;
|
||||||
@@ -152,6 +153,13 @@ public class FunctionRegistration<T> implements BeanNameAware {
|
|||||||
if (this.type == null) {
|
if (this.type == null) {
|
||||||
result = (FunctionRegistration<S>) this;
|
result = (FunctionRegistration<S>) this;
|
||||||
}
|
}
|
||||||
|
else if (this.target instanceof RoutingFunction) {
|
||||||
|
S target = (S) this.target;
|
||||||
|
result = new FunctionRegistration<S>(target);
|
||||||
|
result.type(this.type.getType());
|
||||||
|
result = result.target(target).names(this.names)
|
||||||
|
.type(result.type.wrap(Flux.class)).properties(this.properties);
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
S target = (S) this.target;
|
S target = (S) this.target;
|
||||||
result = new FunctionRegistration<S>(target);
|
result = new FunctionRegistration<S>(target);
|
||||||
@@ -178,7 +186,6 @@ public class FunctionRegistration<T> implements BeanNameAware {
|
|||||||
else if (target instanceof Function) {
|
else if (target instanceof Function) {
|
||||||
target = (S) new FluxedFunction((Function<?, ?>) target);
|
target = (S) new FluxedFunction((Function<?, ?>) target);
|
||||||
}
|
}
|
||||||
|
|
||||||
result = result.target(target).names(this.names)
|
result = result.target(target).names(this.names)
|
||||||
.type(result.type.wrap(Flux.class)).properties(this.properties);
|
.type(result.type.wrap(Flux.class)).properties(this.properties);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import reactor.core.publisher.Mono;
|
|||||||
import org.springframework.cloud.function.context.FunctionRegistration;
|
import org.springframework.cloud.function.context.FunctionRegistration;
|
||||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||||
import org.springframework.cloud.function.context.FunctionType;
|
import org.springframework.cloud.function.context.FunctionType;
|
||||||
|
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||||
import org.springframework.cloud.function.core.FluxConsumer;
|
import org.springframework.cloud.function.core.FluxConsumer;
|
||||||
import org.springframework.cloud.function.core.FluxSupplier;
|
import org.springframework.cloud.function.core.FluxSupplier;
|
||||||
import org.springframework.cloud.function.core.FluxToMonoFunction;
|
import org.springframework.cloud.function.core.FluxToMonoFunction;
|
||||||
@@ -292,8 +293,11 @@ public abstract class AbstractComposableFunctionRegistry implements FunctionRegi
|
|||||||
if (lookup.containsKey(name)) {
|
if (lookup.containsKey(name)) {
|
||||||
composedFunction = lookup.get(name);
|
composedFunction = lookup.get(name);
|
||||||
}
|
}
|
||||||
else if (name.equals("") && lookup.size() == 1) {
|
else if (name.equals("") && lookup.size() >= 1 && lookup.size() <= 2) { // we may have RoutingFunction function
|
||||||
composedFunction = lookup.values().iterator().next();
|
String functionName = lookup.keySet().stream()
|
||||||
|
.filter(fName -> !fName.equals(RoutingFunction.FUNCTION_NAME))
|
||||||
|
.findFirst().orElseGet(() -> null);
|
||||||
|
composedFunction = lookup.get(functionName);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
String[] stages = StringUtils.delimitedListToStringArray(name, "|");
|
String[] stages = StringUtils.delimitedListToStringArray(name, "|");
|
||||||
|
|||||||
@@ -33,11 +33,12 @@ import org.springframework.messaging.support.MessageBuilder;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
|
* @since 2.1
|
||||||
*/
|
*/
|
||||||
public class MessageFunction
|
public class MessageFunction
|
||||||
implements Function<Publisher<Message<?>>, Publisher<Message<?>>> {
|
implements Function<Publisher<Message<?>>, Publisher<Message<?>>> {
|
||||||
|
|
||||||
private Function<?, ?> delegate;
|
private final Function<?, ?> delegate;
|
||||||
|
|
||||||
public MessageFunction(Function<?, ?> delegate) {
|
public MessageFunction(Function<?, ?> delegate) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
@@ -91,5 +92,4 @@ public class MessageFunction
|
|||||||
value -> MessageBuilder.withPayload(function.apply(value.getPayload()))
|
value -> MessageBuilder.withPayload(function.apply(value.getPayload()))
|
||||||
.copyHeaders(value.getHeaders()).build());
|
.copyHeaders(value.getHeaders()).build());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.function.context.config;
|
package org.springframework.cloud.function.context.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -50,6 +51,7 @@ import org.springframework.cloud.function.context.FunctionRegistration;
|
|||||||
import org.springframework.cloud.function.context.FunctionRegistry;
|
import org.springframework.cloud.function.context.FunctionRegistry;
|
||||||
import org.springframework.cloud.function.context.FunctionType;
|
import org.springframework.cloud.function.context.FunctionType;
|
||||||
import org.springframework.cloud.function.context.catalog.AbstractComposableFunctionRegistry;
|
import org.springframework.cloud.function.context.catalog.AbstractComposableFunctionRegistry;
|
||||||
|
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||||
import org.springframework.cloud.function.context.catalog.FunctionUnregistrationEvent;
|
import org.springframework.cloud.function.context.catalog.FunctionUnregistrationEvent;
|
||||||
import org.springframework.cloud.function.json.GsonMapper;
|
import org.springframework.cloud.function.json.GsonMapper;
|
||||||
import org.springframework.cloud.function.json.JacksonMapper;
|
import org.springframework.cloud.function.json.JacksonMapper;
|
||||||
@@ -62,6 +64,11 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
import org.springframework.context.annotation.FilterType;
|
import org.springframework.context.annotation.FilterType;
|
||||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||||
import org.springframework.core.type.StandardMethodMetadata;
|
import org.springframework.core.type.StandardMethodMetadata;
|
||||||
|
import org.springframework.messaging.converter.ByteArrayMessageConverter;
|
||||||
|
import org.springframework.messaging.converter.CompositeMessageConverter;
|
||||||
|
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
|
||||||
|
import org.springframework.messaging.converter.MessageConverter;
|
||||||
|
import org.springframework.messaging.converter.StringMessageConverter;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
@@ -84,6 +91,17 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
return new BeanFactoryFunctionCatalog();
|
return new BeanFactoryFunctionCatalog();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean(RoutingFunction.FUNCTION_NAME)
|
||||||
|
@ConditionalOnProperty(name = "spring.cloud.function.routing.enabled", havingValue = "true")
|
||||||
|
RoutingFunction gateway(FunctionCatalog functionCatalog, FunctionInspector functionInspector) {
|
||||||
|
Collection<MessageConverter> messageConverters = new ArrayList<MessageConverter>();
|
||||||
|
messageConverters.add(new MappingJackson2MessageConverter());
|
||||||
|
messageConverters.add(new StringMessageConverter());
|
||||||
|
messageConverters.add(new ByteArrayMessageConverter());
|
||||||
|
CompositeMessageConverter messageConverter = new CompositeMessageConverter(messageConverters);
|
||||||
|
return new RoutingFunction(functionCatalog, functionInspector, messageConverter);
|
||||||
|
}
|
||||||
|
|
||||||
protected static class BeanFactoryFunctionCatalog
|
protected static class BeanFactoryFunctionCatalog
|
||||||
extends AbstractComposableFunctionRegistry
|
extends AbstractComposableFunctionRegistry
|
||||||
implements SmartInitializingSingleton, BeanFactoryAware {
|
implements SmartInitializingSingleton, BeanFactoryAware {
|
||||||
@@ -96,6 +114,7 @@ public class ContextFunctionCatalogAutoConfiguration {
|
|||||||
* Will collect all suppliers, functions, consumers and function registration as
|
* Will collect all suppliers, functions, consumers and function registration as
|
||||||
* late as possible in the lifecycle.
|
* late as possible in the lifecycle.
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
@Override
|
@Override
|
||||||
public void afterSingletonsInstantiated() {
|
public void afterSingletonsInstantiated() {
|
||||||
Map<String, Supplier> supplierBeans = this.beanFactory
|
Map<String, Supplier> supplierBeans = this.beanFactory
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2019-2019 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
|
||||||
|
*
|
||||||
|
* https://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.context.config;
|
||||||
|
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.SignalType;
|
||||||
|
|
||||||
|
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||||
|
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||||
|
import org.springframework.cloud.function.core.WrappedFunction;
|
||||||
|
import org.springframework.messaging.Message;
|
||||||
|
import org.springframework.messaging.converter.MessageConverter;
|
||||||
|
import org.springframework.messaging.support.MessageBuilder;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of Function which acts as a gateway/router by actually
|
||||||
|
* delegating incoming invocation to a function specified via `function.name`
|
||||||
|
* message header. <br>
|
||||||
|
* {@link Message} is used as a canonical representation of a request which
|
||||||
|
* contains some metadata and it is the responsibility of the higher level
|
||||||
|
* framework to convert the incoming request into a Message. For example;
|
||||||
|
* spring-cloud-function-web will create Message from HttpRequest copying all
|
||||||
|
* HTTP headers as message headers.
|
||||||
|
*
|
||||||
|
* @author Oleg Zhurakousky
|
||||||
|
* @since 2.1
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public class RoutingFunction implements Function<Publisher<Message<?>>, Publisher<?>> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The name of this function use by BeanFactory.
|
||||||
|
*/
|
||||||
|
public static final String FUNCTION_NAME = "router";
|
||||||
|
|
||||||
|
private final FunctionCatalog functionCatalog;
|
||||||
|
|
||||||
|
private final FunctionInspector functionInspector;
|
||||||
|
|
||||||
|
private final MessageConverter messageConverter;
|
||||||
|
|
||||||
|
RoutingFunction(FunctionCatalog functionCatalog, FunctionInspector functionInspector,
|
||||||
|
MessageConverter messageConverter) {
|
||||||
|
this.functionCatalog = functionCatalog;
|
||||||
|
this.functionInspector = functionInspector;
|
||||||
|
this.messageConverter = messageConverter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
@Override
|
||||||
|
public Publisher<?> apply(Publisher<Message<?>> input) {
|
||||||
|
return Flux.from(input)
|
||||||
|
.switchOnFirst((signal, flux) -> {
|
||||||
|
Assert.isTrue(signal.hasValue()
|
||||||
|
&& signal.getType() == SignalType.ON_NEXT, "Signal has no value or wrong type " + signal);
|
||||||
|
Function<Flux<Object>, Publisher<Object>> function = this.getRouteToFunction(signal.get());
|
||||||
|
return flux.map(message -> {
|
||||||
|
Object inputValue = this.convertInput(message, function);
|
||||||
|
return inputValue;
|
||||||
|
}).transform(function);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
|
private WrappedFunction getRouteToFunction(Message<?> message) {
|
||||||
|
String routeToFunctionName = (String) message.getHeaders().get("function.name");
|
||||||
|
WrappedFunction function = functionCatalog.lookup(routeToFunctionName);
|
||||||
|
Assert.notNull(function, "Failed to locate function specified with 'function.name':"
|
||||||
|
+ message.getHeaders().get("function.name"));
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object convertInput(Message<?> message, Object function) {
|
||||||
|
Class<?> inputType = functionInspector.getInputType(function);
|
||||||
|
Object inputValue = message.getPayload();
|
||||||
|
if (!inputValue.getClass().isAssignableFrom(inputType)) {
|
||||||
|
inputValue = this.messageConverter.fromMessage(message, functionInspector.getInputType(function));
|
||||||
|
}
|
||||||
|
if (this.functionInspector.isMessage(function)) {
|
||||||
|
inputValue = MessageBuilder.createMessage(inputValue, message.getHeaders());
|
||||||
|
}
|
||||||
|
Assert.notNull(inputValue, "Failed to determine input value of type "
|
||||||
|
+ inputType + " from Message '"
|
||||||
|
+ message + "'. No suitable Message Converter found.");
|
||||||
|
return inputValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,12 @@
|
|||||||
"type": "java.lang.String",
|
"type": "java.lang.String",
|
||||||
"description": "Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog.",
|
"description": "Name (e.g., 'foo') or composition instruction (e.g., 'foo|bar') used to resolve default function especially for cases where there is more then once function available in catalog.",
|
||||||
"defaultValue": ""
|
"defaultValue": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "spring.cloud.function.routing.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "Enables RoutingFunction which delegates incoming request to a function named via function.name header",
|
||||||
|
"defaultValue": false
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import org.reactivestreams.Publisher;
|
|||||||
* @author Oleg Zhurakousky
|
* @author Oleg Zhurakousky
|
||||||
* @since 2.0.1
|
* @since 2.0.1
|
||||||
*/
|
*/
|
||||||
abstract class WrappedFunction<I, O, IP extends Publisher<I>, OP extends Publisher<O>, T>
|
public abstract class WrappedFunction<I, O, IP extends Publisher<I>, OP extends Publisher<O>, T>
|
||||||
implements Function<IP, OP>, FluxWrapper<T> {
|
implements Function<IP, OP>, FluxWrapper<T> {
|
||||||
|
|
||||||
private final T target;
|
private final T target;
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ import reactor.core.publisher.Flux;
|
|||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
|
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||||
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
import org.springframework.cloud.function.context.catalog.FunctionInspector;
|
||||||
|
import org.springframework.cloud.function.context.config.RoutingFunction;
|
||||||
import org.springframework.cloud.function.context.message.MessageUtils;
|
import org.springframework.cloud.function.context.message.MessageUtils;
|
||||||
import org.springframework.cloud.function.core.FluxConsumer;
|
import org.springframework.cloud.function.core.FluxConsumer;
|
||||||
import org.springframework.cloud.function.core.FluxedConsumer;
|
import org.springframework.cloud.function.core.FluxedConsumer;
|
||||||
@@ -77,6 +79,8 @@ public class RequestProcessor {
|
|||||||
|
|
||||||
private final FunctionInspector inspector;
|
private final FunctionInspector inspector;
|
||||||
|
|
||||||
|
private final FunctionCatalog functionCatalog;
|
||||||
|
|
||||||
private final StringConverter converter;
|
private final StringConverter converter;
|
||||||
|
|
||||||
private final JsonMapper mapper;
|
private final JsonMapper mapper;
|
||||||
@@ -84,10 +88,12 @@ public class RequestProcessor {
|
|||||||
private final List<HttpMessageReader<?>> messageReaders;
|
private final List<HttpMessageReader<?>> messageReaders;
|
||||||
|
|
||||||
public RequestProcessor(FunctionInspector inspector,
|
public RequestProcessor(FunctionInspector inspector,
|
||||||
|
FunctionCatalog functionCatalog,
|
||||||
ObjectProvider<JsonMapper> mapper, StringConverter converter,
|
ObjectProvider<JsonMapper> mapper, StringConverter converter,
|
||||||
ObjectProvider<ServerCodecConfigurer> codecs) {
|
ObjectProvider<ServerCodecConfigurer> codecs) {
|
||||||
this.mapper = mapper.getIfAvailable();
|
this.mapper = mapper.getIfAvailable();
|
||||||
this.inspector = inspector;
|
this.inspector = inspector;
|
||||||
|
this.functionCatalog = functionCatalog;
|
||||||
this.converter = converter;
|
this.converter = converter;
|
||||||
ServerCodecConfigurer source = codecs.getIfAvailable();
|
ServerCodecConfigurer source = codecs.getIfAvailable();
|
||||||
this.messageReaders = source == null ? null : source.getReaders();
|
this.messageReaders = source == null ? null : source.getReaders();
|
||||||
@@ -97,23 +103,23 @@ public class RequestProcessor {
|
|||||||
Function<? extends Publisher<?>, ? extends Publisher<?>> function,
|
Function<? extends Publisher<?>, ? extends Publisher<?>> function,
|
||||||
Consumer<? extends Publisher<?>> consumer,
|
Consumer<? extends Publisher<?>> consumer,
|
||||||
Supplier<? extends Publisher<?>> supplier) {
|
Supplier<? extends Publisher<?>> supplier) {
|
||||||
return new FunctionWrapper(function, consumer, supplier);
|
return new FunctionWrapper(function, supplier);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static FunctionWrapper wrapper(
|
public static FunctionWrapper wrapper(
|
||||||
Function<? extends Publisher<?>, ? extends Publisher<?>> function) {
|
Function<? extends Publisher<?>, ? extends Publisher<?>> function) {
|
||||||
return new FunctionWrapper(function, null, null);
|
return new FunctionWrapper(function, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<ResponseEntity<?>> get(FunctionWrapper wrapper) {
|
public Mono<ResponseEntity<?>> get(FunctionWrapper wrapper) {
|
||||||
if (wrapper.function() != null) {
|
if (wrapper.function() != null) {
|
||||||
return response(wrapper, wrapper.function(),
|
return response(wrapper, wrapper.function(), value(wrapper), true, true);
|
||||||
value(wrapper.function(), wrapper.argument()), true, true);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return response(wrapper, wrapper.supplier(), wrapper.supplier().get(), null,
|
return response(wrapper, wrapper.supplier(), wrapper.supplier().get(), null,
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<ResponseEntity<?>> post(FunctionWrapper wrapper,
|
public Mono<ResponseEntity<?>> post(FunctionWrapper wrapper,
|
||||||
@@ -130,7 +136,8 @@ public class RequestProcessor {
|
|||||||
|
|
||||||
Object input = body == null && inputType.isAssignableFrom(String.class) ? "" : body;
|
Object input = body == null && inputType.isAssignableFrom(String.class) ? "" : body;
|
||||||
|
|
||||||
if (input != null) {
|
if ((isInputMultiple(this.getTargetIfRouting(wrapper, function)) || !(function instanceof RoutingFunction))
|
||||||
|
&& input != null) { // TODO rework. . . pretty ugly
|
||||||
if (this.shouldUseJsonConversion((String) input, wrapper.headers.getContentType())) {
|
if (this.shouldUseJsonConversion((String) input, wrapper.headers.getContentType())) {
|
||||||
Type jsonType = body.startsWith("[")
|
Type jsonType = body.startsWith("[")
|
||||||
&& Collection.class.isAssignableFrom(inputType)
|
&& Collection.class.isAssignableFrom(inputType)
|
||||||
@@ -149,28 +156,53 @@ public class RequestProcessor {
|
|||||||
return response(wrapper, input, stream);
|
return response(wrapper, input, stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Mono<ResponseEntity<?>> stream(FunctionWrapper request) {
|
||||||
|
Publisher<?> result = request.function() != null
|
||||||
|
? value(request)
|
||||||
|
: request.supplier().get();
|
||||||
|
return stream(request, result);
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldUseJsonConversion(String body, MediaType contentType) {
|
private boolean shouldUseJsonConversion(String body, MediaType contentType) {
|
||||||
return (body.startsWith("[") || body.startsWith("{"))
|
return (body.startsWith("[") || body.startsWith("{"))
|
||||||
&& (contentType == null || (contentType != null
|
&& (contentType == null || (contentType != null
|
||||||
&& !"text".equalsIgnoreCase(contentType.getType())));
|
&& !"text".equalsIgnoreCase(contentType.getType())));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Mono<ResponseEntity<?>> stream(FunctionWrapper request) {
|
|
||||||
Publisher<?> result = request.function() != null
|
|
||||||
? value(request.function(), request.argument())
|
|
||||||
: request.supplier().get();
|
|
||||||
return stream(request, result);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<HttpMessageReader<?>> getMessageReaders() {
|
private List<HttpMessageReader<?>> getMessageReaders() {
|
||||||
return this.messageReaders;
|
return this.messageReaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Mono<ResponseEntity<?>> response(FunctionWrapper request, Object handler,
|
||||||
|
Publisher<?> result, Boolean single, boolean getter) {
|
||||||
|
|
||||||
|
BodyBuilder builder = ResponseEntity.ok();
|
||||||
|
if (this.inspector.isMessage(handler)) {
|
||||||
|
result = Flux.from(result)
|
||||||
|
.map(message -> MessageUtils.unpack(handler, message))
|
||||||
|
.doOnNext(value -> addHeaders(builder, value))
|
||||||
|
.map(message -> message.getPayload());
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
builder.headers(HeaderUtils.sanitize(request.headers()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOutputSingle(handler)
|
||||||
|
&& (single != null && single || getter || isInputMultiple(handler))) {
|
||||||
|
result = Mono.from(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result instanceof Flux) {
|
||||||
|
result = Flux.from(result).collectList();
|
||||||
|
}
|
||||||
|
return Mono.from(result).flatMap(body -> Mono.just(builder.body(body)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
private Mono<ResponseEntity<?>> response(FunctionWrapper wrapper, Object body,
|
private Mono<ResponseEntity<?>> response(FunctionWrapper wrapper, Object body,
|
||||||
boolean stream) {
|
boolean stream) {
|
||||||
|
|
||||||
Function<Publisher<?>, Publisher<?>> function = wrapper.function();
|
Function function = wrapper.function();
|
||||||
Consumer<Publisher<?>> consumer = wrapper.consumer();
|
|
||||||
|
|
||||||
Flux<?> flux;
|
Flux<?> flux;
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
@@ -197,7 +229,7 @@ public class RequestProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.inspector.isMessage(function)) {
|
if (this.inspector.isMessage(function)) {
|
||||||
flux = messages(wrapper, function == null ? consumer : function, flux);
|
flux = messages(wrapper, function, flux);
|
||||||
}
|
}
|
||||||
Mono<ResponseEntity<?>> responseEntityMono = null;
|
Mono<ResponseEntity<?>> responseEntityMono = null;
|
||||||
|
|
||||||
@@ -208,19 +240,33 @@ public class RequestProcessor {
|
|||||||
.just(ResponseEntity.status(HttpStatus.ACCEPTED).build());
|
.just(ResponseEntity.status(HttpStatus.ACCEPTED).build());
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Flux<?> result = Flux.from(function.apply(flux));
|
Flux<?> result = Flux.from((Publisher) function.apply(flux));
|
||||||
logger.debug("Handled POST with function");
|
logger.debug("Handled POST with function");
|
||||||
if (stream) {
|
if (stream) {
|
||||||
responseEntityMono = stream(wrapper, result);
|
responseEntityMono = stream(wrapper, result);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
responseEntityMono = response(wrapper, function, result,
|
responseEntityMono = response(wrapper, getTargetIfRouting(wrapper, function), result,
|
||||||
body == null ? null : !(body instanceof Collection), false);
|
body == null ? null : !(body instanceof Collection), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return responseEntityMono;
|
return responseEntityMono;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Called when building response and returns the actual
|
||||||
|
* target function in case the current function is RoutingFunction.
|
||||||
|
* This is necessary to determine the type of the output (e.g., Flux =
|
||||||
|
* multiple or Mono = single etc). See isOutputSingle(..).
|
||||||
|
*/
|
||||||
|
private Object getTargetIfRouting(FunctionWrapper wrapper, Object function) {
|
||||||
|
if (function instanceof RoutingFunction) {
|
||||||
|
String name = wrapper.headers.get("function.name").iterator().next();
|
||||||
|
function = this.functionCatalog.lookup(name);
|
||||||
|
}
|
||||||
|
return function;
|
||||||
|
}
|
||||||
|
|
||||||
private Flux<?> messages(FunctionWrapper request, Object function, Flux<?> flux) {
|
private Flux<?> messages(FunctionWrapper request, Object function, Flux<?> flux) {
|
||||||
Map<String, Object> headers = HeaderUtils.fromHttp(request.headers());
|
Map<String, Object> headers = HeaderUtils.fromHttp(request.headers());
|
||||||
return flux.map(payload -> MessageUtils.create(function, payload, headers));
|
return flux.map(payload -> MessageUtils.create(function, payload, headers));
|
||||||
@@ -246,30 +292,7 @@ public class RequestProcessor {
|
|||||||
return Flux.from(output).then(Mono.fromSupplier(() -> builder.body(output)));
|
return Flux.from(output).then(Mono.fromSupplier(() -> builder.body(output)));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Mono<ResponseEntity<?>> response(FunctionWrapper request, Object handler,
|
|
||||||
Publisher<?> result, Boolean single, boolean getter) {
|
|
||||||
|
|
||||||
BodyBuilder builder = ResponseEntity.ok();
|
|
||||||
if (this.inspector.isMessage(handler)) {
|
|
||||||
result = Flux.from(result)
|
|
||||||
.map(message -> MessageUtils.unpack(handler, message))
|
|
||||||
.doOnNext(value -> addHeaders(builder, value))
|
|
||||||
.map(message -> message.getPayload());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
builder.headers(HeaderUtils.sanitize(request.headers()));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isOutputSingle(handler)
|
|
||||||
&& (single != null && single || getter || isInputMultiple(handler))) {
|
|
||||||
result = Mono.from(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result instanceof Flux) {
|
|
||||||
result = Flux.from(result).collectList();
|
|
||||||
}
|
|
||||||
return Mono.from(result).flatMap(body -> Mono.just(builder.body(body)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isInputMultiple(Object handler) {
|
private boolean isInputMultiple(Object handler) {
|
||||||
Class<?> type = this.inspector.getInputType(handler);
|
Class<?> type = this.inspector.getInputType(handler);
|
||||||
@@ -373,11 +396,13 @@ public class RequestProcessor {
|
|||||||
return ReactiveAdapterRegistry.getSharedInstance();
|
return ReactiveAdapterRegistry.getSharedInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Publisher<?> value(Function<Publisher<?>, Publisher<?>> function,
|
private Publisher<?> value(FunctionWrapper wrapper) {
|
||||||
Publisher<String> value) {
|
Flux<?> input = Flux.from(wrapper.argument)
|
||||||
Flux<?> input = Flux.from(value)
|
.map(body -> this.converter.convert(wrapper.function, body));
|
||||||
.map(body -> this.converter.convert(function, body));
|
if (this.inspector.isMessage(wrapper.function)) {
|
||||||
return Mono.from(function.apply(input));
|
input = messages(wrapper, wrapper.function, input);
|
||||||
|
}
|
||||||
|
return Mono.from(wrapper.function.apply(input));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Type getItemType(Object function) {
|
private Type getItemType(Object function) {
|
||||||
@@ -413,8 +438,6 @@ public class RequestProcessor {
|
|||||||
|
|
||||||
private final Function<Publisher<?>, Publisher<?>> function;
|
private final Function<Publisher<?>, Publisher<?>> function;
|
||||||
|
|
||||||
private final Consumer<Publisher<?>> consumer;
|
|
||||||
|
|
||||||
private final Supplier<Publisher<?>> supplier;
|
private final Supplier<Publisher<?>> supplier;
|
||||||
|
|
||||||
private final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
private final MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
|
||||||
@@ -426,26 +449,21 @@ public class RequestProcessor {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public FunctionWrapper(
|
public FunctionWrapper(
|
||||||
Function<? extends Publisher<?>, ? extends Publisher<?>> function,
|
Function<? extends Publisher<?>, ? extends Publisher<?>> function,
|
||||||
Consumer<? extends Publisher<?>> consumer,
|
|
||||||
Supplier<? extends Publisher<?>> supplier) {
|
Supplier<? extends Publisher<?>> supplier) {
|
||||||
this.function = (Function<Publisher<?>, Publisher<?>>) function;
|
this.function = (Function<Publisher<?>, Publisher<?>>) function;
|
||||||
this.consumer = (Consumer<Publisher<?>>) consumer;
|
|
||||||
this.supplier = (Supplier<Publisher<?>>) supplier;
|
this.supplier = (Supplier<Publisher<?>>) supplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Object handler() {
|
public Object handler() {
|
||||||
return this.function != null ? this.function
|
return this.function != null
|
||||||
: this.consumer != null ? this.consumer : this.supplier;
|
? this.function
|
||||||
|
: this.supplier;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Function<Publisher<?>, Publisher<?>> function() {
|
public Function<Publisher<?>, Publisher<?>> function() {
|
||||||
return this.function;
|
return this.function;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Consumer<Publisher<?>> consumer() {
|
|
||||||
return this.consumer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Supplier<Publisher<?>> supplier() {
|
public Supplier<Publisher<?>> supplier() {
|
||||||
return this.supplier;
|
return this.supplier;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class FunctionEndpointInitializer implements ApplicationContextInitializer<Gener
|
|||||||
() -> new BasicStringConverter(context.getBean(FunctionInspector.class), context.getBeanFactory()));
|
() -> new BasicStringConverter(context.getBean(FunctionInspector.class), context.getBeanFactory()));
|
||||||
context.registerBean(RequestProcessor.class,
|
context.registerBean(RequestProcessor.class,
|
||||||
() -> new RequestProcessor(context.getBean(FunctionInspector.class),
|
() -> new RequestProcessor(context.getBean(FunctionInspector.class),
|
||||||
|
context.getBean(FunctionCatalog.class),
|
||||||
context.getBeanProvider(JsonMapper.class), context.getBean(StringConverter.class),
|
context.getBeanProvider(JsonMapper.class), context.getBean(StringConverter.class),
|
||||||
context.getBeanProvider(ServerCodecConfigurer.class)));
|
context.getBeanProvider(ServerCodecConfigurer.class)));
|
||||||
context.registerBean(FunctionEndpointFactory.class,
|
context.registerBean(FunctionEndpointFactory.class,
|
||||||
|
|||||||
@@ -16,21 +16,19 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.function.web.mvc;
|
package org.springframework.cloud.function.web.mvc;
|
||||||
|
|
||||||
import java.util.function.Consumer;
|
import java.util.HashMap;
|
||||||
import java.util.function.Function;
|
|
||||||
import java.util.function.Supplier;
|
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
import org.reactivestreams.Publisher;
|
|
||||||
|
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
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.FunctionCatalog;
|
import org.springframework.cloud.function.context.FunctionCatalog;
|
||||||
import org.springframework.cloud.function.web.constants.WebRequestConstants;
|
import org.springframework.cloud.function.web.constants.WebRequestConstants;
|
||||||
|
import org.springframework.cloud.function.web.util.FunctionWebUtils;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.method.HandlerMethod;
|
import org.springframework.web.method.HandlerMethod;
|
||||||
import org.springframework.web.servlet.HandlerMapping;
|
import org.springframework.web.servlet.HandlerMapping;
|
||||||
@@ -38,7 +36,7 @@ import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandl
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
*
|
* @author Oleg Zhurakousky
|
||||||
*/
|
*/
|
||||||
@Configuration
|
@Configuration
|
||||||
@ConditionalOnClass(RequestMappingHandlerMapping.class)
|
@ConditionalOnClass(RequestMappingHandlerMapping.class)
|
||||||
@@ -92,7 +90,9 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
|
|||||||
if (path.startsWith(this.prefix)) {
|
if (path.startsWith(this.prefix)) {
|
||||||
path = path.substring(this.prefix.length());
|
path = path.substring(this.prefix.length());
|
||||||
}
|
}
|
||||||
Object function = findFunctionForGet(request, path);
|
|
||||||
|
Object function = FunctionWebUtils.findFunction(HttpMethod.resolve(request.getMethod()),
|
||||||
|
this.functions, new HttpRequestAttributeDelegate(request), path);
|
||||||
if (function != null) {
|
if (function != null) {
|
||||||
if (this.logger.isDebugEnabled()) {
|
if (this.logger.isDebugEnabled()) {
|
||||||
this.logger.debug("Found function for GET: " + path);
|
this.logger.debug("Found function for GET: " + path);
|
||||||
@@ -100,65 +100,20 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping
|
|||||||
request.setAttribute(WebRequestConstants.HANDLER, function);
|
request.setAttribute(WebRequestConstants.HANDLER, function);
|
||||||
return handler;
|
return handler;
|
||||||
}
|
}
|
||||||
function = findFunctionForPost(request, path);
|
|
||||||
if (function != null) {
|
|
||||||
if (this.logger.isDebugEnabled()) {
|
|
||||||
this.logger.debug("Found function for POST: " + path);
|
|
||||||
}
|
|
||||||
request.setAttribute(WebRequestConstants.HANDLER, function);
|
|
||||||
return handler;
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Object findFunctionForPost(HttpServletRequest request, String path) {
|
@SuppressWarnings("serial")
|
||||||
if (!request.getMethod().equals("POST")) {
|
private static class HttpRequestAttributeDelegate extends HashMap<String, Object> {
|
||||||
return null;
|
private final HttpServletRequest request;
|
||||||
|
HttpRequestAttributeDelegate(HttpServletRequest request) {
|
||||||
|
this.request = request;
|
||||||
}
|
}
|
||||||
path = path.startsWith("/") ? path.substring(1) : path;
|
|
||||||
Consumer<Publisher<?>> consumer = this.functions.lookup(Consumer.class, path);
|
|
||||||
if (consumer != null) {
|
|
||||||
request.setAttribute(WebRequestConstants.CONSUMER, consumer);
|
|
||||||
return consumer;
|
|
||||||
}
|
|
||||||
Function<Object, Object> function = this.functions.lookup(Function.class, path);
|
|
||||||
if (function != null) {
|
|
||||||
request.setAttribute(WebRequestConstants.FUNCTION, function);
|
|
||||||
return function;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object findFunctionForGet(HttpServletRequest request, String path) {
|
public Object put(String key, Object value) {
|
||||||
if (!request.getMethod().equals("GET")) {
|
this.request.setAttribute(key, value);
|
||||||
return null;
|
return value;
|
||||||
}
|
}
|
||||||
path = path.startsWith("/") ? path.substring(1) : path;
|
|
||||||
Supplier<Publisher<?>> supplier = this.functions.lookup(Supplier.class, 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 = this.functions.lookup(Function.class,
|
|
||||||
name);
|
|
||||||
if (function != null) {
|
|
||||||
request.setAttribute(WebRequestConstants.FUNCTION, function);
|
|
||||||
request.setAttribute(WebRequestConstants.ARGUMENT, value);
|
|
||||||
return function;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
package org.springframework.cloud.function.web.util;
|
package org.springframework.cloud.function.web.util;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
@@ -36,66 +35,45 @@ public final class FunctionWebUtils {
|
|||||||
|
|
||||||
public static Object findFunction(HttpMethod method, FunctionCatalog functionCatalog,
|
public static Object findFunction(HttpMethod method, FunctionCatalog functionCatalog,
|
||||||
Map<String, Object> attributes, String path) {
|
Map<String, Object> attributes, String path) {
|
||||||
if (method.equals(HttpMethod.GET)) {
|
if (method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST)) {
|
||||||
return findFunctionForGet(functionCatalog, attributes, path);
|
return doFindFunction(method, functionCatalog, attributes, path);
|
||||||
}
|
|
||||||
else if (method.equals(HttpMethod.POST)) {
|
|
||||||
return findFunctionForPost(functionCatalog, attributes, path);
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
throw new IllegalStateException("HTTP method '" + method + "' is not supported;");
|
throw new IllegalStateException("HTTP method '" + method + "' is not supported;");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Object findFunctionForGet(FunctionCatalog functionCatalog,
|
private static Object doFindFunction(HttpMethod method, FunctionCatalog functionCatalog,
|
||||||
Map<String, Object> attributes, String path) {
|
Map<String, Object> attributes, String path) {
|
||||||
path = path.startsWith("/") ? path.substring(1) : path;
|
path = path.startsWith("/") ? path.substring(1) : path;
|
||||||
|
if (method.equals(HttpMethod.GET)) {
|
||||||
Object functionForGet = null;
|
Supplier<Publisher<?>> supplier = functionCatalog.lookup(Supplier.class, path);
|
||||||
Supplier<Publisher<?>> supplier = functionCatalog.lookup(Supplier.class, path);
|
if (supplier != null) {
|
||||||
if (supplier != null) {
|
attributes.put(WebRequestConstants.SUPPLIER, supplier);
|
||||||
attributes.put(WebRequestConstants.SUPPLIER, supplier);
|
return supplier;
|
||||||
functionForGet = supplier;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
StringBuilder builder = new StringBuilder();
|
|
||||||
String name = path;
|
|
||||||
String[] splitPath = path.split("/");
|
|
||||||
Function<Object, Object> function = null;
|
|
||||||
for (int i = 0; i < splitPath.length && functionForGet == null; i++) {
|
|
||||||
String element = splitPath[i];
|
|
||||||
if (builder.length() > 0) {
|
|
||||||
builder.append("/");
|
|
||||||
}
|
|
||||||
builder.append(element);
|
|
||||||
name = builder.toString();
|
|
||||||
|
|
||||||
function = functionCatalog.lookup(Function.class, name);
|
|
||||||
if (function != null) {
|
|
||||||
attributes.put(WebRequestConstants.FUNCTION, function);
|
|
||||||
String value = path.length() > name.length()
|
|
||||||
? path.substring(name.length() + 1) : null;
|
|
||||||
attributes.put(WebRequestConstants.ARGUMENT, value);
|
|
||||||
functionForGet = function;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return functionForGet;
|
StringBuilder builder = new StringBuilder();
|
||||||
}
|
String name = path;
|
||||||
|
String value = null;
|
||||||
private static Object findFunctionForPost(FunctionCatalog functionCatalog,
|
for (String element : path.split("/")) {
|
||||||
Map<String, Object> attributes, String path) {
|
if (builder.length() > 0) {
|
||||||
path = path.startsWith("/") ? path.substring(1) : path;
|
builder.append("/");
|
||||||
Consumer<Publisher<?>> consumer = functionCatalog.lookup(Consumer.class, path);
|
}
|
||||||
if (consumer != null) {
|
builder.append(element);
|
||||||
attributes.put(WebRequestConstants.CONSUMER, consumer);
|
name = builder.toString();
|
||||||
return consumer;
|
value = path.length() > name.length() ? path.substring(name.length() + 1)
|
||||||
}
|
: null;
|
||||||
Function<Object, Object> function = functionCatalog.lookup(Function.class, path);
|
Function<Object, Object> function = functionCatalog.lookup(Function.class,
|
||||||
if (function != null) {
|
name);
|
||||||
attributes.put(WebRequestConstants.FUNCTION, function);
|
if (function != null) {
|
||||||
return function;
|
attributes.put(WebRequestConstants.FUNCTION, function);
|
||||||
|
if (value != null) {
|
||||||
|
attributes.put(WebRequestConstants.ARGUMENT, value);
|
||||||
|
}
|
||||||
|
return function;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,6 +83,9 @@ public final class HeaderUtils {
|
|||||||
name = name.toLowerCase();
|
name = name.toLowerCase();
|
||||||
Object value = values == null ? null
|
Object value = values == null ? null
|
||||||
: (values.size() == 1 ? values.iterator().next() : values);
|
: (values.size() == 1 ? values.iterator().next() : values);
|
||||||
|
if (name.toLowerCase().equals(HttpHeaders.CONTENT_TYPE.toLowerCase())) {
|
||||||
|
name = MessageHeaders.CONTENT_TYPE;
|
||||||
|
}
|
||||||
map.put(name, value);
|
map.put(name, value);
|
||||||
}
|
}
|
||||||
return new MessageHeaders(map);
|
return new MessageHeaders(map);
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2012-2019 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
|
||||||
|
*
|
||||||
|
* https://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.mvc;
|
||||||
|
|
||||||
|
import java.net.URI;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
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.context.config.RoutingFunction;
|
||||||
|
import org.springframework.cloud.function.web.RestApplication;
|
||||||
|
import org.springframework.cloud.function.web.mvc.RoutingFunctionTests.TestConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
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.ContextConfiguration;
|
||||||
|
import org.springframework.test.context.junit4.SpringRunner;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Oleg Zhurakousky
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
@RunWith(SpringRunner.class)
|
||||||
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
|
||||||
|
"spring.main.web-application-type=servlet",
|
||||||
|
"spring.cloud.function.web.path=/functions",
|
||||||
|
"spring.cloud.function.routing.enabled=true"})
|
||||||
|
@ContextConfiguration(classes = { RestApplication.class, TestConfiguration.class })
|
||||||
|
public class RoutingFunctionTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private TestRestTemplate rest;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFunctionMessage() throws Exception {
|
||||||
|
HttpEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header("function.name", "employee")
|
||||||
|
.body("{\"name\":\"Bob\",\"age\":25}"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("{\"name\":\"Bob\",\"age\":25}");
|
||||||
|
assertThat(postForEntity.getHeaders().containsKey("x-content-type")).isTrue();
|
||||||
|
assertThat(postForEntity.getHeaders().get("x-content-type").get(0))
|
||||||
|
.isEqualTo("application/xml");
|
||||||
|
assertThat(postForEntity.getHeaders().get("foo").get(0)).isEqualTo("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFunctionPrimitive() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("function.name", "echo")
|
||||||
|
.body("{\"name\":\"Bob\",\"age\":25}"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("{\"name\":\"Bob\",\"age\":25}");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFluxFunctionPrimitive() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("function.name", "fluxuppercase")
|
||||||
|
.body("hello"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("[\"HELLO\"]");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("function.name", "fluxuppercase")
|
||||||
|
.body("hello1"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("[\"HELLO1\"]");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
|
||||||
|
postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("function.name", "fluxuppercase")
|
||||||
|
.body("hello2"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("[\"HELLO2\"]");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFluxFunctionPrimitiveArray() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header("function.name", "fluxuppercase")
|
||||||
|
.body(new String[] {"a", "b", "c"}), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("[\"A\",\"B\",\"C\"]");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFluxConsumer() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header("function.name", "fluxconsumer")
|
||||||
|
.body(new String[] {"a", "b", "c"}), String.class);
|
||||||
|
//assertThat(postForEntity.getBody()).isEqualTo("[\"A\",\"B\",\"C\"]");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testFunctionPojo() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON)
|
||||||
|
.header("function.name", "echoPojo")
|
||||||
|
.body("{\"value\":\"foo\"}"), String.class);
|
||||||
|
assertThat(postForEntity.getBody()).isEqualTo("{\"foo\":{\"value\":\"foo\"},\"value\":\"bar\"}");
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testConsumerMessage() throws Exception {
|
||||||
|
ResponseEntity<String> postForEntity = this.rest
|
||||||
|
.exchange(RequestEntity.post(new URI("/functions/" + RoutingFunction.FUNCTION_NAME))
|
||||||
|
.contentType(MediaType.TEXT_PLAIN)
|
||||||
|
.header("function.name", "messageConsumer")
|
||||||
|
.body("{\"name\":\"Bob\",\"age\":25}"), String.class);
|
||||||
|
assertThat(postForEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@org.springframework.boot.test.context.TestConfiguration
|
||||||
|
protected static class TestConfiguration {
|
||||||
|
|
||||||
|
@Bean({ "employee" })
|
||||||
|
public Function<Message<Map<String, Object>>, Message<Map<String, Object>>> function() {
|
||||||
|
return request -> {
|
||||||
|
Message<Map<String, Object>> message = MessageBuilder
|
||||||
|
.withPayload(request.getPayload())
|
||||||
|
.setHeader("X-Content-Type", "application/xml")
|
||||||
|
.setHeader("foo", "bar").build();
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Consumer<Message<String>> messageConsumer() {
|
||||||
|
return value -> System.out.println("Value: " + value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<String, String> echo() {
|
||||||
|
return v -> v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Flux<String>, Flux<String>> fluxuppercase() {
|
||||||
|
return v -> v.map(s -> {
|
||||||
|
System.out.println(s);
|
||||||
|
return s.toUpperCase();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Consumer<Flux<String>> fluxconsumer() {
|
||||||
|
// return v -> v.map(value -> {
|
||||||
|
// return value.toUpperCase();
|
||||||
|
// });
|
||||||
|
return flux -> flux.doOnNext(s -> {
|
||||||
|
System.out.println(s + " jkljlkjlkj l");
|
||||||
|
}).subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Function<Foo, Bar> echoPojo() {
|
||||||
|
return v -> {
|
||||||
|
Bar bar = new Bar();
|
||||||
|
bar.setFoo(v);
|
||||||
|
bar.setValue("bar");
|
||||||
|
return bar;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Foo {
|
||||||
|
private String value;
|
||||||
|
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Bar {
|
||||||
|
private Foo foo;
|
||||||
|
private String value;
|
||||||
|
public Foo getFoo() {
|
||||||
|
return foo;
|
||||||
|
}
|
||||||
|
public void setFoo(Foo foo) {
|
||||||
|
this.foo = foo;
|
||||||
|
}
|
||||||
|
public String getValue() {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
public void setValue(String value) {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user