diff --git a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java index 53fe2736a..b15061df6 100644 --- a/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java +++ b/spring-cloud-function-context/src/main/java/org/springframework/cloud/function/context/config/RoutingFunction.java @@ -16,9 +16,11 @@ package org.springframework.cloud.function.context.config; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.function.Function; +import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -193,11 +195,31 @@ public class RoutingFunction implements Function { private FunctionInvocationWrapper locateFunctionFromDefinitionOrExpression(Message message) { for (Entry headerEntry : message.getHeaders().entrySet()) { - if (headerEntry.getKey().equalsIgnoreCase(FunctionProperties.FUNCTION_DEFINITION)) { - return functionFromDefinition((String) headerEntry.getValue()); + String headerKey = headerEntry.getKey(); + Object headerValue = headerEntry.getValue(); + + if (headerKey == null || headerValue == null) { + continue; } - else if (headerEntry.getKey().equalsIgnoreCase(FunctionProperties.ROUTING_EXPRESSION)) { - return this.functionFromExpression((String) headerEntry.getValue(), message, true); + + boolean isFunctionDefinition = FunctionProperties.FUNCTION_DEFINITION.equalsIgnoreCase(headerKey); + boolean isRoutingExpression = FunctionProperties.ROUTING_EXPRESSION.equalsIgnoreCase(headerKey); + + if (isFunctionDefinition) { + if (headerValue instanceof String definition) { + return functionFromDefinition(definition); + } + else if (headerValue instanceof List definitions && !definitions.isEmpty()) { + return functionFromDefinition(definitions.stream().map(Object::toString).collect(Collectors.joining(","))); + } + } + else if (isRoutingExpression) { + if (headerValue instanceof String expression) { + return functionFromExpression(expression, message, true); + } + else if (headerValue instanceof List expressions && !expressions.isEmpty()) { + return functionFromExpression(expressions.get(0).toString(), message, true); + } } } return null; diff --git a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java index 20a3d6b02..20e5ce9c6 100644 --- a/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java +++ b/spring-cloud-function-context/src/test/java/org/springframework/cloud/function/context/config/RoutingFunctionTests.java @@ -17,6 +17,7 @@ package org.springframework.cloud.function.context.config; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.function.Function; @@ -98,7 +99,7 @@ public class RoutingFunctionTests { @SuppressWarnings({ "unchecked", "rawtypes" }) @Test - public void testInvocationWithMessageAndHeader() { + public void testInvocationWithMessageAndStringHeader() { FunctionCatalog functionCatalog = this.configureCatalog(); Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); assertThat(function).isNotNull(); @@ -107,6 +108,57 @@ public class RoutingFunctionTests { assertThat(function.apply(message)).isEqualTo("olleh"); } + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfSingleElementHeader() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", List.of("reverse")) + .build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testCompositionWithMessageAndListOfMultipleElementsHeader() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".definition", + List.of("reverse", "uppercase")) + .build(); + assertThat(function.apply(message)).isEqualTo("OLLEH"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfSingleRoutingExpression() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".routing-expression", + List.of("'reverse'")) + .build(); + assertThat(function.apply(message)).isEqualTo("olleh"); + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + @Test + public void testInvocationWithMessageAndListOfMultipleRoutingExpressions() { + FunctionCatalog functionCatalog = this.configureCatalog(); + Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME); + assertThat(function).isNotNull(); + Message message = MessageBuilder.withPayload("hello") + .setHeader(FunctionProperties.PREFIX + ".routing-expression", + List.of("'uppercase'", "'reverse'")) + .build(); + assertThat(function.apply(message)).isEqualTo("HELLO"); + } + @SuppressWarnings({ "unchecked", "rawtypes" }) @Test public void testRoutingSimpleInputWithReactiveFunctionWithMessageHeader() {