/* * 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.stream.function; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.function.Consumer; import java.util.function.Function; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.UnicastProcessor; import reactor.util.function.Tuple2; import reactor.util.function.Tuples; import org.springframework.beans.factory.BeanCreationException; import org.springframework.boot.WebApplicationType; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.cloud.stream.binder.test.InputDestination; import org.springframework.cloud.stream.binder.test.OutputDestination; import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.integration.support.MessageBuilder; import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.AbstractMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; /** * * @author Oleg Zhurakousky * */ public class MultipleInputOutputFunctionTests { @Test(expected = BeanCreationException.class) public void testFailureWithNonReactiveFunction() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multipleInputNonReactive")) { context.getBean(InputDestination.class); } } @Test(expected = BeanCreationException.class) public void testFailureWithReactiveArrayOutput() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutput")) { context.getBean(InputDestination.class); } } @Test(expected = BeanCreationException.class) public void testFailureWithReactiveArrayOutputNonGeneric() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiReactiveInputReactiveArrayOutputNoGeneric")) { context.getBean(InputDestination.class); } } @Test(expected = BeanCreationException.class) public void testFailureWithReactiveArrayInput() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=genericReactiveArrayInput")) { context.getBean(InputDestination.class); } } @Test(expected = BeanCreationException.class) public void testFailureWithReactiveArrayInputNonGeneric() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=nonGenericReactiveArrayInput")) { context.getBean(InputDestination.class); } } @Test(expected = BeanCreationException.class) public void testFailureWithConsumer() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiInputConsumer")) { context.getBean(InputDestination.class); } } @Test public void testMultiInputSingleOutput() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiInputSingleOutput")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); Message<byte[]> stringInputMessage = MessageBuilder.withPayload("one".getBytes()).build(); Message<byte[]> integerInputMessage = MessageBuilder.withPayload("1".getBytes()).build(); inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0"); inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1"); Message<byte[]> outputMessage = outputDestination.receive(); assertThat(outputMessage.getPayload()).isEqualTo("one".getBytes()); outputMessage = outputDestination.receive(0, "multiInputSingleOutput-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("1".getBytes()); } } @Test public void testSingleInputMultiOutput() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=singleInputMultipleOutputs")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); for (int i = 0; i < 10; i++) { inputDestination.send(MessageBuilder.withPayload(String.valueOf(i).getBytes()).build(), "singleInputMultipleOutputs-in-0"); } int counter = 0; for (int i = 0; i < 5; i++) { Message<byte[]> even = outputDestination.receive(0, "singleInputMultipleOutputs-out-0"); assertThat(even.getPayload()).isEqualTo(("EVEN: " + String.valueOf(counter++)).getBytes()); Message<byte[]> odd = outputDestination.receive(0, "singleInputMultipleOutputs-out-1"); assertThat(odd.getPayload()).isEqualTo(("ODD: " + String.valueOf(counter++)).getBytes()); } } } @Test public void testMultipleFunctions() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase;reverse")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); inputDestination.send(inputMessage, "uppercase-in-0"); inputDestination.send(inputMessage, "reverse-in-0"); Message<byte[]> outputMessage = outputDestination.receive(0, "uppercase-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes()); outputMessage = outputDestination.receive(0, "reverse-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes()); } } @Test public void testMultipleFunctionsWithComposition() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ReactiveFunctionConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=uppercase|reverse;reverse|uppercase")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build(); inputDestination.send(inputMessage, "uppercasereverse-in-0"); inputDestination.send(inputMessage, "reverseuppercase-in-0"); Message<byte[]> outputMessage = outputDestination.receive(0, "uppercasereverse-out-0"); System.out.println(new String(outputMessage.getPayload())); assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); outputMessage = outputDestination.receive(0, "reverseuppercase-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("OLLEH".getBytes()); } } @Test public void testMultiInputSingleOutputWithCustomContentType() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ContentTypeConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiInputSingleOutput", "--spring.cloud.stream.bindings.multiInputSingleOutput-in-0.content-type=string/person", "--spring.cloud.stream.bindings.multiInputSingleOutput-in-1.content-type=string/employee")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build(); Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build(); inputDestination.send(stringInputMessage, "multiInputSingleOutput-in-0"); inputDestination.send(integerInputMessage, "multiInputSingleOutput-in-1"); Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("RICKY".getBytes()); outputMessage = outputDestination.receive(1000, "multiInputSingleOutput-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("BOBBY".getBytes()); } } @Test public void testMultiInputSingleOutputWithCustomContentType2() { try (ConfigurableApplicationContext context = new SpringApplicationBuilder( TestChannelBinderConfiguration.getCompleteConfiguration( ContentTypeConfiguration.class)) .web(WebApplicationType.NONE) .run("--spring.jmx.enabled=false", "--spring.cloud.function.definition=multiInputSingleOutput2", "--spring.cloud.stream.bindings.multiInputSingleOutput2-in-0.content-type=string/person", "--spring.cloud.stream.bindings.multiInputSingleOutput2-in-1.content-type=string/employee", "--spring.cloud.stream.bindings.multiInputSingleOutput2-out-0.content-type=string/person")) { context.getBean(InputDestination.class); InputDestination inputDestination = context.getBean(InputDestination.class); OutputDestination outputDestination = context.getBean(OutputDestination.class); Message<byte[]> stringInputMessage = MessageBuilder.withPayload("ricky".getBytes()).build(); Message<byte[]> integerInputMessage = MessageBuilder.withPayload("bobby".getBytes()).build(); inputDestination.send(stringInputMessage, "multiInputSingleOutput2-in-0"); inputDestination.send(integerInputMessage, "multiInputSingleOutput2-in-1"); Message<byte[]> outputMessage = outputDestination.receive(1000, "multiInputSingleOutput2-out-0"); assertThat(outputMessage.getPayload()).isEqualTo("rickybobby".getBytes()); } } @EnableAutoConfiguration public static class ReactiveFunctionConfiguration { @Bean public Function<String, String> uppercase() { return value -> value.toUpperCase(); } @Bean public Function<String, String> reverse() { return value -> new StringBuilder(value).reverse().toString(); } @Bean public Function<Tuple2<String, String>, String> multipleInputNonReactive() { // not supported return tuple -> null; } @Bean public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<?>[]> multiReactiveInputReactiveArrayOutput() { // not supported return tuple -> null; } @Bean public Consumer<Tuple2<Flux<String>, Flux<Integer>>> multiInputConsumer() { // not supported return tuple -> System.out.println(); } @SuppressWarnings("rawtypes") @Bean public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux[]> multiReactiveInputReactiveArrayOutputNoGeneric() { // not supported return tuple -> null; } @Bean public Function<Flux<?>[], Tuple2<Flux<String>, Flux<Integer>>> genericReactiveArrayInput() { // not supported return tuple -> null; } @SuppressWarnings("rawtypes") @Bean public Function<Flux[], Tuple2<Flux<String>, Flux<Integer>>> nonGenericReactiveArrayInput() { // not supported return tuple -> null; } @Bean public Function<Tuple2<Flux<String>, Flux<Integer>>, Flux<String>> multiInputSingleOutput() { return tuple -> { Flux<String> stringStream = tuple.getT1(); Flux<String> intStream = tuple.getT2().map(i -> String.valueOf(i)); return Flux.merge(stringStream, intStream); }; } @Bean @SuppressWarnings({ "unchecked", "rawtypes" }) public static Function<Flux<Integer>, Tuple2<Flux<String>, Flux<String>>> singleInputMultipleOutputs() { return flux -> { Flux<Integer> connectedFlux = flux.publish().autoConnect(2); UnicastProcessor even = UnicastProcessor.create(); UnicastProcessor odd = UnicastProcessor.create(); Flux<Integer> evenFlux = connectedFlux.filter(number -> number % 2 == 0).doOnNext(number -> even.onNext("EVEN: " + number)); Flux<Integer> oddFlux = connectedFlux.filter(number -> number % 2 != 0).doOnNext(number -> odd.onNext("ODD: " + number)); return Tuples.of(Flux.from(even).doOnSubscribe(x -> evenFlux.subscribe()), Flux.from(odd).doOnSubscribe(x -> oddFlux.subscribe())); }; } } @EnableAutoConfiguration public static class ContentTypeConfiguration { @Bean public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<String>> multiInputSingleOutput() { return tuple -> { Flux<String> stringStream = tuple.getT1().map(p -> p.getName().toUpperCase()); Flux<String> intStream = tuple.getT2().map(p -> p.getName().toUpperCase()); return Flux.merge(stringStream, intStream); }; } @Bean public Function<Tuple2<Flux<Person>, Flux<Employee>>, Flux<Person>> multiInputSingleOutput2() { return tuple -> { return Flux.merge(tuple.getT1(), tuple.getT2()).buffer(Duration.ofMillis(1000)).map(list -> { String personName = ((Person) list.get(0)).getName(); String employeeName = ((Employee) list.get(1)).getName(); return new Person(personName + employeeName); }); }; } @Bean public MessageConverter stringToPersonConverter() { return new AbstractMessageConverter(MimeType.valueOf("string/person")) { @Override protected boolean supports(Class<?> clazz) { return Person.class.isAssignableFrom(clazz); } @Override @Nullable protected Object convertFromInternal( Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) { String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8); Person person = new Person(name); return person; } @Override @Nullable protected Object convertToInternal( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { return ((Person) payload).getName().getBytes(StandardCharsets.UTF_8); } }; } @Bean public MessageConverter stringToEmployeeConverter() { return new AbstractMessageConverter(MimeType.valueOf("string/employee")) { @Override protected boolean supports(Class<?> clazz) { return Employee.class.isAssignableFrom(clazz); } @Override @Nullable protected Object convertFromInternal( Message<?> message, Class<?> targetClass, @Nullable Object conversionHint) { String name = new String(((byte[]) message.getPayload()), StandardCharsets.UTF_8); Employee person = new Employee(name); return person; } @Override @Nullable protected Object convertToInternal( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { return ((Employee) payload).getName().getBytes(StandardCharsets.UTF_8); } }; } } private static class Person { private final String name; Person(String name) { this.name = name; } public String getName() { return name; } } private static class Employee { private final String name; Employee(String name) { this.name = name; } public String getName() { return name; } } }