From ff7741ea7417d182784e56519ad8a60488821c0c Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Wed, 14 Jul 2021 14:46:21 +0200 Subject: [PATCH] GH-713 Add support for default function for HTTP Resolves #713 --- .../main/asciidoc/spring-cloud-function.adoc | 5 ++++ .../context/FunctionalSpringApplication.java | 3 +- .../ContextFunctionCatalogInitializer.java | 2 ++ .../web/flux/FunctionHandlerMapping.java | 8 +++-- .../web/flux/ReactorAutoConfiguration.java | 5 ++-- .../function/FunctionEndpointInitializer.java | 17 +++++++---- .../web/mvc/FunctionHandlerMapping.java | 8 +++-- .../web/mvc/ReactorAutoConfiguration.java | 5 ++-- .../FunctionWebRequestProcessingHelper.java | 29 ++++++++++++++----- 9 files changed, 59 insertions(+), 23 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index f130d7646..c1bbad7f4 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -604,6 +604,11 @@ For example, The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path. +The same property will also work for cases where function can not be resolved via URL. For example, your URL may be `localhost:8080/uppercase`, but there is no `uppercase` function. +However there are function `foo` and `bar`. So, in this case `localhost:8080/uppercase` will resolve to `foo|bar`. +This could be useful especially for cases when URL is used to communicate certain information since there will be Message header called `uri` with the value +of the actual URL, giving user ability to use it for evaluation and computation. + === Function Filtering rules In situations where there are more then one function in catalog there may be a need to only export certain functions or function compositions. In that case you can use diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java index 31fb28195..eebf2f0d2 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/FunctionalSpringApplication.java @@ -25,6 +25,7 @@ import java.util.function.Supplier; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.ApplicationContextFactory; import org.springframework.boot.WebApplicationType; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; @@ -63,7 +64,7 @@ public class FunctionalSpringApplication public FunctionalSpringApplication(Class... primarySources) { super(primarySources); - setApplicationContextClass(GenericApplicationContext.class); + setApplicationContextFactory(ApplicationContextFactory.ofContextClass(GenericApplicationContext.class)); if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", null)) { setWebApplicationType(WebApplicationType.REACTIVE); diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java index 109a97c38..31832ff54 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogInitializer.java @@ -36,6 +36,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProce import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry; @@ -185,6 +186,7 @@ public class ContextFunctionCatalogInitializer implements ApplicationContextInit ConversionService conversionService = new DefaultConversionService(); return new SimpleFunctionRegistry(conversionService, messageConverter, this.context.getBean(JsonMapper.class)); }); + this.context.registerBean(FunctionProperties.class, () -> new FunctionProperties()); this.context.registerBean(FunctionRegistrationPostProcessor.class, () -> new FunctionRegistrationPostProcessor(this.context.getAutowireCapableBeanFactory() .getBeanProvider(FunctionRegistration.class))); diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java index 3b13d660b..93f89f49f 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/FunctionHandlerMapping.java @@ -24,6 +24,7 @@ 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.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper; import org.springframework.context.annotation.Configuration; @@ -45,16 +46,19 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping private final FunctionController controller; + private final FunctionProperties functionProperties; + @Value("${spring.cloud.function.web.path:}") private String prefix = ""; @Autowired public FunctionHandlerMapping(FunctionCatalog catalog, - FunctionController controller) { + FunctionController controller, FunctionProperties functionProperties) { this.functions = catalog; this.logger.info("FunctionCatalog: " + catalog); setOrder(super.getOrder() - 5); this.controller = controller; + this.functionProperties = functionProperties; } @Override @@ -80,7 +84,7 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping path = path.substring(this.prefix.length()); } Object function = FunctionWebRequestProcessingHelper - .findFunction(request.getRequest().getMethod(), this.functions, request.getAttributes(), path, new String[] {}); + .findFunction(this.functionProperties, request.getRequest().getMethod(), this.functions, request.getAttributes(), path, new String[] {}); if (function != null) { if (this.logger.isDebugEnabled()) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java index ce4f7c43b..0aade98bb 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/flux/ReactorAutoConfiguration.java @@ -27,6 +27,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.web.BasicStringConverter; import org.springframework.cloud.function.web.StringConverter; import org.springframework.context.annotation.Bean; @@ -47,8 +48,8 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl public class ReactorAutoConfiguration { @Bean - public FunctionHandlerMapping functionHandlerMapping(FunctionCatalog catalog, FunctionController controller) { - return new FunctionHandlerMapping(catalog, controller); + public FunctionHandlerMapping functionHandlerMapping(FunctionCatalog catalog, FunctionController controller, FunctionProperties functionProperties) { + return new FunctionHandlerMapping(catalog, controller, functionProperties); } @Bean diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java index be6a40f1c..b2f893ab8 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/function/FunctionEndpointInitializer.java @@ -31,11 +31,12 @@ import reactor.netty.http.server.HttpServer; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.web.ErrorProperties; -import org.springframework.boot.autoconfigure.web.ResourceProperties; +import org.springframework.boot.autoconfigure.web.WebProperties.Resources; import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler; import org.springframework.boot.web.reactive.error.DefaultErrorAttributes; import org.springframework.boot.web.reactive.error.ErrorAttributes; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.FunctionalSpringApplication; import org.springframework.cloud.function.context.catalog.FunctionTypeUtils; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; @@ -106,7 +107,7 @@ class FunctionEndpointInitializer implements ApplicationContextInitializer new RequestProcessor(context.getBeanProvider(JsonMapper.class), context.getBeanProvider(ServerCodecConfigurer.class))); context.registerBean(FunctionEndpointFactory.class, - () -> new FunctionEndpointFactory(context.getBean(FunctionCatalog.class), + () -> new FunctionEndpointFactory(context.getBean(FunctionProperties.class), context.getBean(FunctionCatalog.class), context.getBean(RequestProcessor.class), context.getEnvironment())); RouterFunctionRegister.register(context); } @@ -120,9 +121,10 @@ class FunctionEndpointInitializer implements ApplicationContextInitializer new DefaultErrorAttributes()); context.registerBean(ErrorProperties.class, () -> new ErrorProperties()); - context.registerBean(ResourceProperties.class, () -> new ResourceProperties()); + + context.registerBean(Resources.class, () -> new Resources()); DefaultErrorWebExceptionHandler handler = new DefaultErrorWebExceptionHandler( - context.getBean(ErrorAttributes.class), context.getBean(ResourceProperties.class), + context.getBean(ErrorAttributes.class), context.getBean(Resources.class), context.getBean(ErrorProperties.class), context); ServerCodecConfigurer codecs = ServerCodecConfigurer.create(); handler.setMessageWriters(codecs.getWriters()); @@ -203,7 +205,9 @@ class FunctionEndpointFactory { private final RequestProcessor processor; - FunctionEndpointFactory(FunctionCatalog functionCatalog, RequestProcessor processor, Environment environment) { + private final FunctionProperties functionProperties; + + FunctionEndpointFactory(FunctionProperties functionProperties, FunctionCatalog functionCatalog, RequestProcessor processor, Environment environment) { String handler = environment.resolvePlaceholders("${function.handler}"); if (handler.startsWith("$")) { handler = null; @@ -211,6 +215,7 @@ class FunctionEndpointFactory { this.processor = processor; this.functionCatalog = functionCatalog; this.handler = handler; + this.functionProperties = functionProperties; } private FunctionInvocationWrapper extract(ServerRequest request) { @@ -223,7 +228,7 @@ class FunctionEndpointFactory { } else { String[] accept = FunctionWebRequestProcessingHelper.acceptContentTypes(request.headers().accept()); - function = FunctionWebRequestProcessingHelper.findFunction(request.method(), functionCatalog, request.attributes(), + function = FunctionWebRequestProcessingHelper.findFunction(this.functionProperties, request.method(), functionCatalog, request.attributes(), request.path(), accept); } return function; diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionHandlerMapping.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionHandlerMapping.java index 8db4ac9f2..f22f2e2db 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionHandlerMapping.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/FunctionHandlerMapping.java @@ -25,6 +25,7 @@ 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.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper; import org.springframework.context.annotation.Configuration; @@ -47,16 +48,19 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping private final FunctionController controller; + private final FunctionProperties functionProperties; + @Value("${spring.cloud.function.web.path:}") private String prefix = ""; @Autowired - public FunctionHandlerMapping(FunctionCatalog catalog, + public FunctionHandlerMapping(FunctionProperties functionProperties, FunctionCatalog catalog, FunctionController controller) { this.functions = catalog; this.logger.info("FunctionCatalog: " + catalog); setOrder(super.getOrder() - 5); this.controller = controller; + this.functionProperties = functionProperties; } @Override @@ -91,7 +95,7 @@ public class FunctionHandlerMapping extends RequestMappingHandlerMapping path = path.substring(this.prefix.length()); } - Object function = FunctionWebRequestProcessingHelper.findFunction(HttpMethod.resolve(request.getMethod()), + Object function = FunctionWebRequestProcessingHelper.findFunction(this.functionProperties, HttpMethod.resolve(request.getMethod()), this.functions, new HttpRequestAttributeDelegate(request), path, new String[] {}); if (function != null) { if (this.logger.isDebugEnabled()) { diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/ReactorAutoConfiguration.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/ReactorAutoConfiguration.java index 73cc2c0f7..4c9666f55 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/ReactorAutoConfiguration.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/mvc/ReactorAutoConfiguration.java @@ -24,6 +24,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.web.BasicStringConverter; import org.springframework.cloud.function.web.StringConverter; import org.springframework.context.annotation.Bean; @@ -43,8 +44,8 @@ import org.springframework.web.method.support.AsyncHandlerMethodReturnValueHandl public class ReactorAutoConfiguration { @Bean - public FunctionHandlerMapping functionHandlerMapping(FunctionCatalog catalog, FunctionController controller) { - return new FunctionHandlerMapping(catalog, controller); + public FunctionHandlerMapping functionHandlerMapping(FunctionProperties functionProperties, FunctionCatalog catalog, FunctionController controller) { + return new FunctionHandlerMapping(functionProperties, catalog, controller); } @Bean diff --git a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java index 19f83fa34..b459a6954 100644 --- a/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java +++ b/spring-cloud-function-web/src/main/java/org/springframework/cloud/function/web/util/FunctionWebRequestProcessingHelper.java @@ -28,6 +28,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.cloud.function.context.FunctionCatalog; +import org.springframework.cloud.function.context.FunctionProperties; import org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper; import org.springframework.cloud.function.web.constants.WebRequestConstants; import org.springframework.http.HttpHeaders; @@ -54,10 +55,10 @@ public final class FunctionWebRequestProcessingHelper { } - public static FunctionInvocationWrapper findFunction(HttpMethod method, FunctionCatalog functionCatalog, + public static FunctionInvocationWrapper findFunction(FunctionProperties functionProperties, HttpMethod method, FunctionCatalog functionCatalog, Map attributes, String path, String[] acceptContentTypes) { if (method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST)) { - return doFindFunction(method, functionCatalog, attributes, path, acceptContentTypes); + return doFindFunction(functionProperties.getDefinition(), method, functionCatalog, attributes, path, acceptContentTypes); } else { throw new IllegalStateException("HTTP method '" + method + "' is not supported;"); @@ -145,8 +146,9 @@ public final class FunctionWebRequestProcessingHelper { return message.getPayload(); } - private static FunctionInvocationWrapper doFindFunction(HttpMethod method, FunctionCatalog functionCatalog, + private static FunctionInvocationWrapper doFindFunction(String functionDefinition, HttpMethod method, FunctionCatalog functionCatalog, Map attributes, String path, String[] acceptContentTypes) { + path = path.startsWith("/") ? path.substring(1) : path; if (method.equals(HttpMethod.GET)) { FunctionInvocationWrapper function = functionCatalog.lookup(path, acceptContentTypes); @@ -169,16 +171,27 @@ public final class FunctionWebRequestProcessingHelper { : null; FunctionInvocationWrapper function = functionCatalog.lookup(name, acceptContentTypes); if (function != null) { - attributes.put(WebRequestConstants.FUNCTION, function); - if (value != null) { - attributes.put(WebRequestConstants.ARGUMENT, value); - } - return function; + return postProcessFunction(function, value, attributes); + } + } + + if (StringUtils.hasText(functionDefinition)) { + FunctionInvocationWrapper function = functionCatalog.lookup(functionDefinition, acceptContentTypes); + if (function != null) { + return postProcessFunction(function, value, attributes); } } return null; } + private static FunctionInvocationWrapper postProcessFunction(FunctionInvocationWrapper function, String argument, Map attributes) { + attributes.put(WebRequestConstants.FUNCTION, function); + if (argument != null) { + attributes.put(WebRequestConstants.ARGUMENT, argument); + } + return function; + } + @SuppressWarnings({ "unchecked", "rawtypes" }) private static Object postProcessResult(Object result, boolean isMessage) { if (result instanceof Flux) {