diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java index 2bdfd9f03..a387e9a5e 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfiguration.java @@ -33,6 +33,7 @@ import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Stream; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -66,6 +67,7 @@ import org.springframework.messaging.Message; import org.springframework.stereotype.Component; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; import reactor.core.publisher.Flux; @@ -94,8 +96,10 @@ public class ContextFunctionCatalogAutoConfiguration { @Bean public FunctionCatalog functionCatalog(ContextFunctionPostProcessor processor) { - return new InMemoryFunctionCatalog( - processor.merge(registrations, consumers, suppliers, functions)); + return new BeanFactoryFunctionCatalog( + new InMemoryFunctionCatalog( + processor.merge(registrations, consumers, suppliers, functions)), + processor); } @Bean @@ -103,6 +107,76 @@ public class ContextFunctionCatalogAutoConfiguration { return new BeanFactoryFunctionInspector(processor); } + protected class BeanFactoryFunctionCatalog implements FunctionCatalog { + + private final FunctionCatalog delegate; + private final ContextFunctionPostProcessor processor; + + @SuppressWarnings("unchecked") + public Supplier lookupSupplier(String name) { + Supplier result = this.delegate.lookupSupplier(name); + if (result != null) { + return result; + } + if (name.contains(",")) { + Object composed = processor.compose(name); + if (composed instanceof Supplier) { + result = (Supplier) composed; + } + } + return result; + } + + @SuppressWarnings("unchecked") + public Function lookupFunction(String name) { + Function result = this.delegate.lookupFunction(name); + if (result != null) { + return result; + } + if (name.contains(",")) { + Object composed = processor.compose(name); + if (composed instanceof Function) { + result = (Function) composed; + } + } + return result; + } + + @SuppressWarnings("unchecked") + public Consumer lookupConsumer(String name) { + Consumer result = this.delegate.lookupConsumer(name); + if (result != null) { + return result; + } + if (name.contains(",")) { + Object composed = processor.compose(name); + if (composed instanceof Consumer) { + result = (Consumer) composed; + } + } + return result; + } + + public Set getSupplierNames() { + return this.delegate.getSupplierNames(); + } + + public Set getFunctionNames() { + return this.delegate.getFunctionNames(); + } + + public Set getConsumerNames() { + return this.delegate.getConsumerNames(); + } + + public BeanFactoryFunctionCatalog(FunctionCatalog delegate, + ContextFunctionPostProcessor processor) { + this.delegate = delegate; + this.processor = processor; + } + + } + protected class BeanFactoryFunctionInspector implements FunctionInspector { private ContextFunctionPostProcessor processor; @@ -159,6 +233,7 @@ public class ContextFunctionCatalogAutoConfiguration { private BeanDefinitionRegistry registry; private ConversionService conversionService; private Map registrations = new HashMap<>(); + private Map lookup = new HashMap<>(); private Map>> types = new HashMap<>(); @Override @@ -166,6 +241,67 @@ public class ContextFunctionCatalogAutoConfiguration { this.registry = registry; } + public Object compose(String name) { + if (lookup.containsKey(name)) { + return lookup.get(name); + } + String[] stages = StringUtils.tokenizeToStringArray(name, ","); + Object function = Stream.of(stages).map(key -> lookup(key)).sequential() + .reduce(this::compose).get(); + lookup.computeIfAbsent(name, key -> function); + Map> values = types.computeIfAbsent(name, + key -> new HashMap<>()); + for (ParamType type : ParamType.values()) { + if (!values.containsKey(type)) { + if (type.isInput()) { + values.put(type, types.get(stages[0]).get(type)); + } + else { + values.put(type, types.get(stages[stages.length - 1]).get(type)); + } + } + } + registrations.put(function, name); + return function; + } + + private Object lookup(String name) { + Object result = lookup.get(name); + if (result != null) { + Map> values = types.computeIfAbsent(name, + key -> new HashMap<>()); + for (ParamType type : ParamType.values()) { + if (!values.containsKey(type)) { + values.put(type, findType(result, type)); + } + } + } + return result; + } + + @SuppressWarnings("unchecked") + private Object compose(Object a, Object b) { + if (a instanceof Supplier && b instanceof Function) { + Supplier supplier = (Supplier) a; + Function function = (Function) b; + return (Supplier) () -> function.apply(supplier.get()); + } + else if (a instanceof Function && b instanceof Function) { + Function function1 = (Function) a; + Function function2 = (Function) b; + return function1.andThen(function2); + } + else if (a instanceof Function && b instanceof Consumer) { + Function function = (Function) a; + Consumer consumer = (Consumer) b; + return (Consumer) v -> consumer.accept(function.apply(v)); + } + else { + throw new IllegalArgumentException(String.format( + "Could not compose %s and %s", a.getClass(), b.getClass())); + } + } + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory factory) throws BeansException { @@ -275,6 +411,7 @@ public class ContextFunctionCatalogAutoConfiguration { registration.target(target((Function) target, key)); } registrations.remove(target); + this.lookup.put(key, registration.getTarget()); this.registrations.put(registration.getTarget(), key); } diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java index d8e154884..fac3eeac3 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/InMemoryFunctionCatalog.java @@ -66,12 +66,9 @@ public class InMemoryFunctionCatalog implements FunctionCatalog { } @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) + @SuppressWarnings("unchecked") public Function lookupFunction(String name) { - return (Function) Stream.of(StringUtils.tokenizeToStringArray(name, ",")) - .map(functions::get) - .filter(f -> f != null) - .reduce(null, (f1, f2) -> f1 == null ? f2 : f1.andThen((Function)f2)); + return (Function) functions.get(name); } @Override diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java index b54a2b6ed..660104b0a 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/ContextFunctionCatalogAutoConfigurationTests.java @@ -37,6 +37,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.function.compiler.CompiledFunctionFactory; import org.springframework.cloud.function.compiler.FunctionCompiler; +import org.springframework.cloud.function.core.FunctionCatalog; import org.springframework.cloud.function.scan.ScannedFunction; import org.springframework.cloud.function.test.GenericFunction; import org.springframework.context.ConfigurableApplicationContext; @@ -63,7 +64,7 @@ import reactor.core.publisher.Flux; public class ContextFunctionCatalogAutoConfigurationTests { private ConfigurableApplicationContext context; - private InMemoryFunctionCatalog catalog; + private FunctionCatalog catalog; private FunctionInspector inspector; private static String value; @@ -81,10 +82,13 @@ public class ContextFunctionCatalogAutoConfigurationTests { assertThat(context.getBean("function")).isInstanceOf(Function.class); assertThat(catalog.lookupFunction("function")).isInstanceOf(Function.class); assertThat(context.getBean("function2")).isInstanceOf(Function.class); - assertThat(catalog.lookupFunction("function,function2")).isInstanceOf(Function.class); - Function,Flux> f = catalog.lookupFunction("function,function2,function3"); + assertThat(catalog.lookupFunction("function,function2")) + .isInstanceOf(Function.class); + Function, Flux> f = catalog + .lookupFunction("function,function2,function3"); assertThat(f).isInstanceOf(Function.class); - assertThat(f.apply(Flux.just("hello")).blockFirst()).isEqualTo("HELLOfunction2function3"); + assertThat(f.apply(Flux.just("hello")).blockFirst()) + .isEqualTo("HELLOfunction2function3"); assertThat(context.getBean("supplierFoo")).isInstanceOf(Supplier.class); assertThat(catalog.lookupSupplier("supplierFoo")).isInstanceOf(Supplier.class); assertThat(context.getBean("supplier_Foo")).isInstanceOf(Supplier.class); @@ -104,6 +108,41 @@ public class ContextFunctionCatalogAutoConfigurationTests { } + @Test + public void composedFunction() { + create(MultipleConfiguration.class); + assertThat(catalog.lookupFunction("foos,bars")).isInstanceOf(Function.class); + assertThat(catalog.lookupFunction("names,foos")).isNull(); + assertThat(inspector.getInputType(catalog.lookupFunction("foos,bars"))) + .isAssignableFrom(String.class); + assertThat(inspector.getOutputType(catalog.lookupFunction("foos,bars"))) + .isAssignableFrom(Bar.class); + } + + @Test + public void composedSupplier() { + create(MultipleConfiguration.class); + assertThat(catalog.lookupSupplier("names,foos")).isInstanceOf(Supplier.class); + assertThat(catalog.lookupFunction("names,foos")).isNull(); + assertThat(inspector.getOutputType(catalog.lookupSupplier("names,foos"))) + .isAssignableFrom(Foo.class); + // The input type is the same as the output type of the first element in the chain + assertThat(inspector.getInputType(catalog.lookupSupplier("names,foos"))) + .isAssignableFrom(String.class); + } + + @Test + public void composedConsumer() { + create(MultipleConfiguration.class); + assertThat(catalog.lookupConsumer("foos,print")).isInstanceOf(Consumer.class); + assertThat(catalog.lookupFunction("foos,print")).isNull(); + assertThat(inspector.getInputType(catalog.lookupConsumer("foos,print"))) + .isAssignableFrom(String.class); + // The output type is the same as the input type of the last element in the chain + assertThat(inspector.getOutputType(catalog.lookupConsumer("foos,print"))) + .isAssignableFrom(Foo.class); + } + @Test public void genericFunction() { create(GenericConfiguration.class); @@ -331,7 +370,7 @@ public class ContextFunctionCatalogAutoConfigurationTests { private void create(Class[] types, String... props) { context = new SpringApplicationBuilder((Object[]) types).properties(props).run(); - catalog = context.getBean(InMemoryFunctionCatalog.class); + catalog = context.getBean(FunctionCatalog.class); inspector = context.getBean(FunctionInspector.class); } @@ -369,7 +408,7 @@ public class ContextFunctionCatalogAutoConfigurationTests { return () -> "hello"; } - @Bean(name={"supplierFoo", "supplier_Foo"}) + @Bean(name = { "supplierFoo", "supplier_Foo" }) public Supplier foo() { return () -> "hello"; } @@ -397,6 +436,30 @@ public class ContextFunctionCatalogAutoConfigurationTests { } } + @EnableAutoConfiguration + @Configuration + protected static class MultipleConfiguration { + @Bean + public Function foos() { + return value -> new Foo(value.toUpperCase()); + } + + @Bean + public Function bars() { + return value -> new Bar(value.getValue()); + } + + @Bean + public Consumer print() { + return System.out::println; + } + + @Bean + public Supplier names() { + return () -> "Mark"; + } + } + @EnableAutoConfiguration @Configuration protected static class GenericConfiguration { @@ -423,14 +486,14 @@ public class ContextFunctionCatalogAutoConfigurationTests { beanFactory.registerSingleton("function", new SingletonFunction()); } } - + protected static class SingletonFunction implements Function { @Override public String apply(Integer input) { return "value=" + input; } - + } @EnableAutoConfiguration @@ -532,4 +595,24 @@ public class ContextFunctionCatalogAutoConfigurationTests { this.value = value; } } + + public static class Bar { + private String message; + + public Bar(String value) { + this.message = value; + } + + Bar() { + } + + public String getMessage() { + return this.message; + } + + public void setMessage(String message) { + this.message = message; + } + + } }