diff --git a/applications/stream-applications-build/stream-apps-descriptor/pom.xml b/applications/stream-applications-build/stream-apps-descriptor/pom.xml index 5707a52b..30db0071 100644 --- a/applications/stream-applications-build/stream-apps-descriptor/pom.xml +++ b/applications/stream-applications-build/stream-apps-descriptor/pom.xml @@ -1,7 +1,7 @@ - apps-build + stream-applications-build org.springframework.cloud.stream.app Fahrenheit.BUILD-SNAPSHOT diff --git a/applications/stream-applications-build/stream-apps-docs/pom.xml b/applications/stream-applications-build/stream-apps-docs/pom.xml index 1e787427..6a25e9d7 100644 --- a/applications/stream-applications-build/stream-apps-docs/pom.xml +++ b/applications/stream-applications-build/stream-apps-docs/pom.xml @@ -6,7 +6,7 @@ Stream Apps Docs org.springframework.cloud.stream.app - apps-build + stream-applications-build Fahrenheit.BUILD-SNAPSHOT diff --git a/functions/consumer/redis-consumer/pom.xml b/functions/consumer/redis-consumer/pom.xml new file mode 100644 index 00000000..1fd37fb6 --- /dev/null +++ b/functions/consumer/redis-consumer/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + redis-consumer + 1.0.0.BUILD-SNAPSHOT + redis-consumer + Redis Consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + 2.2.0.RELEASE + 1.48 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.integration + spring-integration-redis + + + org.springframework.boot + spring-boot-starter-validation + + + javax.validation + validation-api + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + com.playtika.testcontainers + embedded-redis + ${embedded-redis.version} + test + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud + spring-cloud-starter + ${spring-cloud-starters.version} + test + + + org.springframework.integration + spring-integration-test-support + test + + + org.awaitility + awaitility + test + + + junit + junit + + + + + + diff --git a/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerConfiguration.java b/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerConfiguration.java new file mode 100644 index 00000000..5641b63a --- /dev/null +++ b/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2020 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.fn.consumer.redis; + +import java.util.function.Consumer; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.integration.redis.outbound.RedisPublishingMessageHandler; +import org.springframework.integration.redis.outbound.RedisQueueOutboundChannelAdapter; +import org.springframework.integration.redis.outbound.RedisStoreWritingMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; + +/** + * @author Eric Bottard + * @author Mark Pollack + * @author Gary Russell + * @author Soby Chacko + * @author Artem Bilan + */ +@Configuration +@EnableConfigurationProperties(RedisConsumerProperties.class) +public class RedisConsumerConfiguration { + + @Bean + public Consumer> redisConsumer() { + return redisConsumerMessageHandler(null, null)::handleMessage; + } + + @Bean + public MessageHandler redisConsumerMessageHandler(RedisConnectionFactory redisConnectionFactory, + RedisConsumerProperties redisConsumerProperties) { + if (redisConsumerProperties.isKeyPresent()) { + RedisStoreWritingMessageHandler redisStoreWritingMessageHandler = new RedisStoreWritingMessageHandler( + redisConnectionFactory); + redisStoreWritingMessageHandler.setKeyExpression(redisConsumerProperties.keyExpression()); + return redisStoreWritingMessageHandler; + } + else if (redisConsumerProperties.isQueuePresent()) { + return new RedisQueueOutboundChannelAdapter(redisConsumerProperties.queueExpression(), + redisConnectionFactory); + } + else { // must be topic + RedisPublishingMessageHandler redisPublishingMessageHandler = new RedisPublishingMessageHandler( + redisConnectionFactory); + redisPublishingMessageHandler.setTopicExpression(redisConsumerProperties.topicExpression()); + return redisPublishingMessageHandler; + } + } + +} diff --git a/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerProperties.java b/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerProperties.java new file mode 100644 index 00000000..11bebd21 --- /dev/null +++ b/functions/consumer/redis-consumer/src/main/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerProperties.java @@ -0,0 +1,156 @@ +/* + * Copyright 2015-2020 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.fn.consumer.redis; + +import java.util.Arrays; +import java.util.Collections; + +import javax.validation.constraints.AssertTrue; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +/** + * Used to configure those Redis Sink module options that are not related to connecting to Redis. + * + * @author Eric Bottard + * @author Mark Pollack + * @author Artem Bilan + * @author Soby Chacko + */ +@ConfigurationProperties("redis.consumer") +@Validated +public class RedisConsumerProperties { + + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + /** + * A SpEL expression to use for topic. + */ + private String topicExpression; + + /** + * A SpEL expression to use for queue. + */ + private String queueExpression; + + /** + * A SpEL expression to use for storing to a key. + */ + private String keyExpression; + + /** + * A literal key name to use when storing to a key. + */ + private String key; + + /** + * A literal queue name to use when storing in a queue. + */ + private String queue; + + /** + * A literal topic name to use when publishing to a topic. + */ + private String topic; + + public Expression keyExpression() { + return key != null ? new LiteralExpression(key) : EXPRESSION_PARSER.parseExpression(keyExpression); + } + + public Expression queueExpression() { + return queue != null ? new LiteralExpression(queue) : EXPRESSION_PARSER.parseExpression(queueExpression); + } + + public Expression topicExpression() { + return topic != null ? new LiteralExpression(topic) : EXPRESSION_PARSER.parseExpression(topicExpression); + } + + boolean isKeyPresent() { + return StringUtils.hasText(key) || keyExpression != null; + } + + boolean isQueuePresent() { + return StringUtils.hasText(queue) || queueExpression != null; + } + + boolean isTopicPresent() { + return StringUtils.hasText(topic) || topicExpression != null; + } + + public String getTopicExpression() { + return topicExpression; + } + + public void setTopicExpression(String topicExpression) { + this.topicExpression = topicExpression; + } + + public String getQueueExpression() { + return queueExpression; + } + + public void setQueueExpression(String queueExpression) { + this.queueExpression = queueExpression; + } + + public String getKeyExpression() { + return keyExpression; + } + + public void setKeyExpression(String keyExpression) { + this.keyExpression = keyExpression; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getQueue() { + return queue; + } + + public void setQueue(String queue) { + this.queue = queue; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + // The javabean property name is what will be reported in case of violation. Make it meaningful + @AssertTrue(message = "Exactly one of 'queue', 'queueExpression', 'key', 'keyExpression', " + + "'topic' and 'topicExpression' must be set") + public boolean isMutuallyExclusive() { + Object[] props = new Object[] { queue, queueExpression, key, keyExpression, topic, topicExpression }; + return (props.length - 1) == Collections.frequency(Arrays.asList(props), null); + } + +} diff --git a/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/AbstractRedisConsumerTests.java b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/AbstractRedisConsumerTests.java new file mode 100644 index 00000000..2b0a6cb0 --- /dev/null +++ b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/AbstractRedisConsumerTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 2020-2020 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.fn.consumer.redis; + +import java.util.function.Consumer; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; + +/** + * @author Soby Chacko + */ +@SpringBootTest +@DirtiesContext +public class AbstractRedisConsumerTests { + + @Autowired + Consumer> redisConsumer; + + @Autowired + RedisConnectionFactory redisConnectionFactory; + + @Autowired + StringRedisTemplate redisTemplate; + + @SpringBootApplication + static class TestApplication { + + } +} diff --git a/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerKeyTests.java b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerKeyTests.java new file mode 100644 index 00000000..b6fc741d --- /dev/null +++ b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerKeyTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2015-2020 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.fn.consumer.redis; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.redis.support.collections.DefaultRedisList; +import org.springframework.data.redis.support.collections.RedisList; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Pollack + * @author Marius Bogoevici + * @author Gary Russell + */ +@TestPropertySource(properties = "redis.consumer.key = foo") +public class RedisConsumerKeyTests extends AbstractRedisConsumerTests { + + @Test + public void testWithKey() { + //Setup + String key = "foo"; + redisTemplate.delete(key); + + RedisList redisList = new DefaultRedisList<>(key, redisTemplate); + List list = new ArrayList<>(); + list.add("Manny"); + list.add("Moe"); + list.add("Jack"); + + //Execute + Message> message = new GenericMessage<>(list); + + redisConsumer.accept(message); + + //Assert + assertThat(redisList.size()).isEqualTo(3); + assertThat(redisList.get(0)).isEqualTo("Manny"); + assertThat(redisList.get(1)).isEqualTo("Moe"); + assertThat(redisList.get(2)).isEqualTo("Jack"); + + //Cleanup + redisTemplate.delete(key); + } +} diff --git a/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerQueueTests.java b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerQueueTests.java new file mode 100644 index 00000000..eecf8ca2 --- /dev/null +++ b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerQueueTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2020-2020 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.fn.consumer.redis; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +@TestPropertySource(properties = "redis.consumer.queue = test-queue") +public class RedisConsumerQueueTests extends AbstractRedisConsumerTests { + + @Test + public void testWithQueue() { + Message message = MessageBuilder.withPayload("hello").build(); + + redisConsumer.accept(message); + + Object result = redisTemplate.boundListOps("test-queue").rightPop(5000, TimeUnit.MILLISECONDS); + assertThat(result).isEqualTo("hello"); + } +} diff --git a/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerTopicTests.java b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerTopicTests.java new file mode 100644 index 00000000..6d7ce0dc --- /dev/null +++ b/functions/consumer/redis-consumer/src/test/java/org/springframework/cloud/fn/consumer/redis/RedisConsumerTopicTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2020-2020 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.fn.consumer.redis; + +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.listener.ChannelTopic; +import org.springframework.data.redis.listener.RedisMessageListenerContainer; +import org.springframework.data.redis.listener.Topic; +import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +@TestPropertySource(properties = "redis.consumer.topic = foo-topic") +public class RedisConsumerTopicTests extends AbstractRedisConsumerTests { + + @Autowired + RedisConnectionFactory connectionFactory; + + @Test + public void testWithTopic() throws Exception { + + int numToTest = 10; + String topic = "foo-topic"; + final CountDownLatch latch = new CountDownLatch(numToTest); + + MessageListenerAdapter listener = new MessageListenerAdapter(); + listener.setDelegate(new Listener(latch)); + listener.setSerializer(new StringRedisSerializer()); + listener.afterPropertiesSet(); + + RedisMessageListenerContainer container = new RedisMessageListenerContainer(); + container.setConnectionFactory(connectionFactory); + container.afterPropertiesSet(); + container.addMessageListener(listener, Collections.singletonList(new ChannelTopic(topic))); + container.start(); + + Awaitility.await().until(() -> TestUtils.getPropertyValue(container, "subscriptionTask.connection", + RedisConnection.class) != null); + + Message message = MessageBuilder.withPayload("hello").build(); + for (int i = 0; i < numToTest; i++) { + redisConsumer.accept(message); + } + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + container.stop(); + } + + private static class Listener { + + private final CountDownLatch latch; + + Listener(CountDownLatch latch) { + this.latch = latch; + } + + @SuppressWarnings("unused") + public void handleMessage(String s) { + this.latch.countDown(); + } + } +} diff --git a/functions/consumer/redis-consumer/src/test/resources/application.properties b/functions/consumer/redis-consumer/src/test/resources/application.properties new file mode 100644 index 00000000..a78ad15d --- /dev/null +++ b/functions/consumer/redis-consumer/src/test/resources/application.properties @@ -0,0 +1,3 @@ +spring.redis.host=${embedded.redis.host} +spring.redis.port=${embedded.redis.port} +spring.redis.password=${embedded.redis.password} diff --git a/functions/pom.xml b/functions/pom.xml index b0f4bfb2..9193cc46 100644 --- a/functions/pom.xml +++ b/functions/pom.xml @@ -47,6 +47,7 @@ consumer/log-consumer consumer/mongodb-consumer consumer/rabbit-consumer + consumer/redis-consumer function/filter-function function/spel-function