From 589e451d145a978d088887ee52d0fb9dc642b60d Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Mon, 28 Jan 2019 18:47:14 +0100 Subject: [PATCH] GH-247 Added spring.cloud.function.definition property Added spring.cloud.function.definition property which is used by FunctionRegistry as a supplement instruction to resolve nameless lookups. It is used by web module to map single or multiple (composed) functions to the root path (/) Resolves #247 --- .../main/asciidoc/spring-cloud-function.adoc | 18 ++++- .../context/AbstractFunctionRegistry.java | 44 ++++++++++++ .../catalog/InMemoryFunctionCatalog.java | 10 +-- ...ntextFunctionCatalogAutoConfiguration.java | 9 ++- .../spring-configuration-metadata.json | 6 ++ .../MoreThenOneFunctionRootMappingTests.java | 69 +++++++++++++++++++ 6 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/AbstractFunctionRegistry.java create mode 100644 spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/MoreThenOneFunctionRootMappingTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-function.adoc b/docs/src/main/asciidoc/spring-cloud-function.adoc index 103d919af..62270db68 100644 --- a/docs/src/main/asciidoc/spring-cloud-function.adoc +++ b/docs/src/main/asciidoc/spring-cloud-function.adoc @@ -122,7 +122,23 @@ plain text and JSON. |=== -As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response will also contain a single value. For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream". If there is only one function (consumer etc.) then the name in the path is optional. Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line). +As the table above shows the behaviour of the endpoint depends on the method and also the type of incoming request data. When the incoming data is single valued, and the target function is declared as obviously single valued (i.e. not returning a collection or `Flux`), then the response will also contain a single value. +For multi-valued responses the client can ask for a server-sent event stream by sending `Accept: text/event-stream". + +If there is only a single function (consumer etc.) in the catalog, the name in the path is optional. +Composite functions can be addressed using pipes or commas to separate function names (pipes are legal in URL paths, but a bit awkward to type on the command line). + +For cases where there is more then a single function in catalog and you want to map a specific function to the root +path (e.g., "/"), or you want to compose several functions and then map to the root path you can do so by providing +`spring.cloud.function.definition` property which essentially used by spring-=cloud-function-web module to provide +default mapping for cases where there is some type of a conflict (e.g., more then one function available etc). + +For example, +---- +--spring.cloud.function.definition=foo|bar +---- + +The above property will compose 'foo' and 'bar' function and map the composed function to the "/" path. Functions and consumers that are declared with input and output in `Message` will see the request headers on the input messages, and the output message headers will be converted to HTTP headers. diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/AbstractFunctionRegistry.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/AbstractFunctionRegistry.java new file mode 100644 index 000000000..517fb1b19 --- /dev/null +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/AbstractFunctionRegistry.java @@ -0,0 +1,44 @@ +/* + * Copyright 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 + * + * 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.context; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.util.StringUtils; + +/** + * + * @author Oleg Zhurakousky + * + * @since 2.0.1 + * + */ +public abstract class AbstractFunctionRegistry implements FunctionRegistry { + + @Autowired + private Environment environment = new StandardEnvironment(); + + + public T lookup(Class type, String name) { + String functionDefinitionName = !StringUtils.hasText(name) && environment.containsProperty("spring.cloud.function.definition") + ? environment.getProperty("spring.cloud.function.definition") : name; + return this.doLookup(type, functionDefinitionName); + } + + protected abstract T doLookup(Class type, String name); +} diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InMemoryFunctionCatalog.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InMemoryFunctionCatalog.java index b6cebdd77..df4d84c43 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InMemoryFunctionCatalog.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/catalog/InMemoryFunctionCatalog.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 the original author or authors. + * Copyright 2016-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. @@ -28,8 +28,8 @@ import java.util.stream.Collectors; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; +import org.springframework.cloud.function.context.AbstractFunctionRegistry; import org.springframework.cloud.function.context.FunctionRegistration; -import org.springframework.cloud.function.context.FunctionRegistry; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.util.Assert; @@ -39,8 +39,8 @@ import org.springframework.util.Assert; * @author Mark Fisher * @author Oleg Zhurakousky */ -public class InMemoryFunctionCatalog - implements FunctionRegistry, FunctionInspector, ApplicationEventPublisherAware { +public class InMemoryFunctionCatalog extends AbstractFunctionRegistry + implements FunctionInspector, ApplicationEventPublisherAware { private final Map, Map> functions; @@ -122,7 +122,7 @@ public class InMemoryFunctionCatalog @Override @SuppressWarnings("unchecked") - public T lookup(Class type, String name) { + public T doLookup(Class type, String name) { T function = null; if (type == null) { function = (T) functions.values().stream() diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java index d7f32de56..6add1ec69 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/ContextFunctionCatalogAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2018 the original author or authors. + * Copyright 2016-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. @@ -41,6 +41,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.function.context.AbstractFunctionRegistry; import org.springframework.cloud.function.context.FunctionCatalog; import org.springframework.cloud.function.context.FunctionRegistration; import org.springframework.cloud.function.context.FunctionRegistry; @@ -103,6 +104,8 @@ public class ContextFunctionCatalogAutoConfiguration { @Autowired(required = false) private Map> registrations = Collections.emptyMap(); + + @Bean public FunctionRegistry functionCatalog(ContextFunctionRegistry processor) { processor.merge(registrations, consumers, suppliers, functions); @@ -114,7 +117,7 @@ public class ContextFunctionCatalogAutoConfiguration { return new BeanFactoryFunctionInspector(processor); } - protected static class BeanFactoryFunctionCatalog implements FunctionRegistry { + protected static class BeanFactoryFunctionCatalog extends AbstractFunctionRegistry { private final ContextFunctionRegistry processor; @@ -127,7 +130,7 @@ public class ContextFunctionCatalogAutoConfiguration { @Override @SuppressWarnings("unchecked") - public T lookup(Class type, String name) { + protected T doLookup(Class type, String name) { T function = null; if (type == null) { function = (T) processor.lookupFunction(name); diff --git a/spring-cloud-function-context/src/main/resources/META-INF/spring-configuration-metadata.json b/spring-cloud-function-context/src/main/resources/META-INF/spring-configuration-metadata.json index 647c720cd..243510875 100644 --- a/spring-cloud-function-context/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/spring-cloud-function-context/src/main/resources/META-INF/spring-configuration-metadata.json @@ -7,6 +7,12 @@ "type": "java.lang.String", "description": "Triggers scanning within the specified base packages for any class that is assignable to java.util.function.Function. For each detected Function class, a bean instance will be added to the context.", "defaultValue": "functions" + }, + { + "name": "spring.cloud.function.definition", + "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.", + "defaultValue": "" } ] } \ No newline at end of file diff --git a/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/MoreThenOneFunctionRootMappingTests.java b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/MoreThenOneFunctionRootMappingTests.java new file mode 100644 index 000000000..0d8e4405a --- /dev/null +++ b/spring-cloud-function-web/src/test/java/org/springframework/cloud/function/test/MoreThenOneFunctionRootMappingTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 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 + * + * 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.test; + +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.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.reactive.server.WebTestClient; + +import reactor.core.publisher.Mono; + +/** + * @author Oleg Zhurakousky + * + */ +@RunWith(SpringRunner.class) +@SpringBootTest({ "spring.main.web-application-type=REACTIVE", + "spring.functional.enabled=false", + "spring.cloud.function.definition=uppercase|reverse"}) +@AutoConfigureWebTestClient +@DirtiesContext +public class MoreThenOneFunctionRootMappingTests { + + @Autowired + private WebTestClient client; + + @Test + public void words() throws Exception { + client.post().uri("/").body(Mono.just("star"), String.class).exchange() + .expectStatus().isOk().expectBody(String.class).isEqualTo("RATS"); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + protected static class TestConfiguration { + @Bean + public Function uppercase() { + return v -> v.toUpperCase(); + } + + @Bean + public Function reverse() { + return v -> new StringBuilder(v).reverse().toString(); + } + } +}