From b0d86edbc7056e3fed3b00899ff7bc3d3b0077f4 Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Fri, 15 May 2020 10:44:48 -0400 Subject: [PATCH] MQTT source as supplier/source Resolves https://github.com/spring-cloud/stream-applications/issues/38 --- common/mqtt-common/pom.xml | 36 +++++ .../fn/common/mqtt/MqttConfiguration.java | 64 ++++++++ .../cloud/fn/common/mqtt/MqttProperties.java | 138 ++++++++++++++++++ pom.xml | 2 + supplier/mqtt-supplier/pom.xml | 53 +++++++ .../mqtt/MqttSupplierConfiguration.java | 85 +++++++++++ .../supplier/mqtt/MqttSupplierProperties.java | 103 +++++++++++++ .../fn/supplier/mqtt/MqttSupplierTests.java | 108 ++++++++++++++ 8 files changed, 589 insertions(+) create mode 100644 common/mqtt-common/pom.xml create mode 100644 common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttConfiguration.java create mode 100644 common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttProperties.java create mode 100644 supplier/mqtt-supplier/pom.xml create mode 100644 supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierConfiguration.java create mode 100644 supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierProperties.java create mode 100644 supplier/mqtt-supplier/src/test/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierTests.java diff --git a/common/mqtt-common/pom.xml b/common/mqtt-common/pom.xml new file mode 100644 index 00000000..5d36d7e9 --- /dev/null +++ b/common/mqtt-common/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + mqtt-common + 1.0.0-SNAPSHOT + mqtt-common + file consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-mqtt + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + diff --git a/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttConfiguration.java b/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttConfiguration.java new file mode 100644 index 00000000..0335940b --- /dev/null +++ b/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttConfiguration.java @@ -0,0 +1,64 @@ +/* + * Copyright 2017-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.common.mqtt; + +import org.eclipse.paho.client.mqttv3.MqttConnectOptions; +import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence; +import org.eclipse.paho.client.mqttv3.persist.MqttDefaultFilePersistence; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.mqtt.core.DefaultMqttPahoClientFactory; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.util.ObjectUtils; + +/** + * Generic mqtt configuration. + * + * @author Janne Valkealahti + * + */ +@Configuration +public class MqttConfiguration { + + @Autowired + private MqttProperties mqttProperties; + + @Bean + public MqttPahoClientFactory mqttClientFactory() { + + MqttConnectOptions mqttConnectOptions = new MqttConnectOptions(); + mqttConnectOptions.setServerURIs(mqttProperties.getUrl()); + mqttConnectOptions.setUserName(mqttProperties.getUsername()); + mqttConnectOptions.setPassword(mqttProperties.getPassword().toCharArray()); + mqttConnectOptions.setCleanSession(mqttProperties.isCleanSession()); + mqttConnectOptions.setConnectionTimeout(mqttProperties.getConnectionTimeout()); + mqttConnectOptions.setKeepAliveInterval(mqttProperties.getKeepAliveInterval()); + + DefaultMqttPahoClientFactory factory = new DefaultMqttPahoClientFactory(); + factory.setConnectionOptions(mqttConnectOptions); + + if (ObjectUtils.nullSafeEquals(mqttProperties.getPersistence(), "file")) { + factory.setPersistence(new MqttDefaultFilePersistence(mqttProperties.getPersistenceDirectory())); + } + else if (ObjectUtils.nullSafeEquals(mqttProperties.getPersistence(), "memory")) { + factory.setPersistence(new MemoryPersistence()); + } + return factory; + } +} diff --git a/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttProperties.java b/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttProperties.java new file mode 100644 index 00000000..e8c414f8 --- /dev/null +++ b/common/mqtt-common/src/main/java/org/springframework/cloud/fn/common/mqtt/MqttProperties.java @@ -0,0 +1,138 @@ +/* + * Copyright 2017-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.common.mqtt; + +import javax.validation.constraints.Size; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Generic mqtt connection properties. + * + * @author Janne Valkealahti + * + */ +@Validated +@ConfigurationProperties("mqtt") +public class MqttProperties { + + /** + * location of the mqtt broker(s) (comma-delimited list). + */ + private String[] url = new String[] { "tcp://localhost:1883" }; + + /** + * the username to use when connecting to the broker. + */ + private String username = "guest"; + + /** + * the password to use when connecting to the broker. + */ + private String password = "guest"; + + /** + * whether the client and server should remember state across restarts and reconnects. + */ + private boolean cleanSession = true; + + /** + * the connection timeout in seconds. + */ + private int connectionTimeout = 30; + + /** + * the ping interval in seconds. + */ + private int keepAliveInterval = 60; + + /** + * 'memory' or 'file'. + */ + private String persistence = "memory"; + + /** + * Persistence directory. + */ + private String persistenceDirectory = "/tmp/paho"; + + @Size(min = 1) + public String[] getUrl() { + return url; + } + + public void setUrl(String[] url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isCleanSession() { + return cleanSession; + } + + public void setCleanSession(boolean cleanSession) { + this.cleanSession = cleanSession; + } + + public int getKeepAliveInterval() { + return keepAliveInterval; + } + + public void setKeepAliveInterval(int keepAliveInterval) { + this.keepAliveInterval = keepAliveInterval; + } + + public int getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(int connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public String getPersistence() { + return persistence; + } + + public void setPersistence(String persistence) { + this.persistence = persistence; + } + + public String getPersistenceDirectory() { + return persistenceDirectory; + } + + public void setPersistenceDirectory(String persistenceDirectory) { + this.persistenceDirectory = persistenceDirectory; + } +} diff --git a/pom.xml b/pom.xml index ed08a63d..360d1f90 100644 --- a/pom.xml +++ b/pom.xml @@ -43,6 +43,7 @@ common/ftp-common common/function-test-support common/tcp-common + common/mqtt-common consumer/cassandra-consumer consumer/counter-consumer @@ -65,6 +66,7 @@ supplier/http-supplier supplier/jdbc-supplier supplier/mongodb-supplier + supplier/mqtt-supplier supplier/tcp-supplier supplier/time-supplier diff --git a/supplier/mqtt-supplier/pom.xml b/supplier/mqtt-supplier/pom.xml new file mode 100644 index 00000000..1749f5d6 --- /dev/null +++ b/supplier/mqtt-supplier/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + mqtt-supplier + 1.0.0-SNAPSHOT + mqtt-supplier + mqtt supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.cloud.fn + mqtt-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + org.testcontainers + testcontainers + 1.9.1 + test + + + + diff --git a/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierConfiguration.java b/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierConfiguration.java new file mode 100644 index 00000000..c80d08cd --- /dev/null +++ b/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierConfiguration.java @@ -0,0 +1,85 @@ +/* + * Copyright 2017-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.supplier.mqtt; + +import java.util.function.Supplier; + +import reactor.core.publisher.Flux; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.fn.common.mqtt.MqttConfiguration; +import org.springframework.cloud.fn.common.mqtt.MqttProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.integration.channel.FluxMessageChannel; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.integration.mqtt.inbound.MqttPahoMessageDrivenChannelAdapter; +import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter; +import org.springframework.messaging.Message; + +/** + * A source module that receives data from Mqtt. + * + * @author Janne Valkealahti + * @author Soby Chacko + */ +@Configuration +@EnableConfigurationProperties({MqttProperties.class, MqttSupplierProperties.class}) +@Import(MqttConfiguration.class) +public class MqttSupplierConfiguration { + + @Autowired + private MqttSupplierProperties properties; + + @Autowired + private MqttPahoClientFactory mqttClientFactory; + + @Autowired + private BeanFactory beanFactory; + + @Bean + public Supplier>> mqttSupplier() { + return () -> Flux.from(output()) + .doOnSubscribe(subscription -> mqttInbound().start()); + } + + @Bean + public MqttPahoMessageDrivenChannelAdapter mqttInbound() { + MqttPahoMessageDrivenChannelAdapter adapter = new MqttPahoMessageDrivenChannelAdapter(properties.getClientId(), + mqttClientFactory, properties.getTopics()); + adapter.setQos(properties.getQos()); + adapter.setConverter(pahoMessageConverter(beanFactory)); + adapter.setOutputChannel(output()); + adapter.setAutoStartup(false); + return adapter; + } + + @Bean + public FluxMessageChannel output() { + return new FluxMessageChannel(); + } + + public DefaultPahoMessageConverter pahoMessageConverter(BeanFactory beanFactory) { + DefaultPahoMessageConverter converter = new DefaultPahoMessageConverter(properties.getCharset()); + converter.setPayloadAsBytes(properties.isBinary()); + converter.setBeanFactory(beanFactory); + return converter; + } +} diff --git a/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierProperties.java b/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierProperties.java new file mode 100644 index 00000000..79f3c670 --- /dev/null +++ b/supplier/mqtt-supplier/src/main/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierProperties.java @@ -0,0 +1,103 @@ +/* + * Copyright 2017-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.supplier.mqtt; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Size; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Properties for the Mqtt Source. + * + * @author Janne Valkealahti + * @author Soby Chacko + * + */ +@Validated +@ConfigurationProperties("mqtt.supplier") +public class MqttSupplierProperties { + + /** + * identifies the client. + */ + private String clientId = "stream.client.id.source"; + + /** + * the topic(s) (comma-delimited) to which the source will subscribe. + */ + private String[] topics = new String[] { "stream.mqtt" }; + + /** + * the qos; a single value for all topics or a comma-delimited list to match the topics. + */ + private int[] qos = new int[] { 0 }; + + /** + * true to leave the payload as bytes. + */ + private boolean binary = false; + + /** + * the charset used to convert bytes to String (when binary is false). + */ + private String charset = "UTF-8"; + + @NotBlank + @Size(min = 1, max = 23) + public String getClientId() { + return this.clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String[] getTopics() { + return this.topics; + } + + public void setTopics(String[] topics) { + this.topics = topics; + } + + public int[] getQos() { + return this.qos; + } + + public void setQos(int[] qos) { + this.qos = qos; + } + + public String getCharset() { + return this.charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public boolean isBinary() { + return this.binary; + } + + public void setBinary(boolean binary) { + this.binary = binary; + } + +} diff --git a/supplier/mqtt-supplier/src/test/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierTests.java b/supplier/mqtt-supplier/src/test/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierTests.java new file mode 100644 index 00000000..3af340ee --- /dev/null +++ b/supplier/mqtt-supplier/src/test/java/org/springframework/cloud/fn/supplier/mqtt/MqttSupplierTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2017-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.supplier.mqtt; + +import java.util.function.Consumer; +import java.util.function.Supplier; + +import com.github.dockerjava.api.command.CreateContainerCmd; +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.PortBinding; +import com.github.dockerjava.api.model.Ports; +import org.junit.jupiter.api.Test; +import org.testcontainers.containers.GenericContainer; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.mqtt.core.MqttPahoClientFactory; +import org.springframework.integration.mqtt.outbound.MqttPahoMessageHandler; +import org.springframework.integration.mqtt.support.DefaultPahoMessageConverter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for Mqtt Supplier. + * + * @author Janne Valkealahti + * @author Gary Russell + * @author Soby Chacko + * + */ +@SpringBootTest(properties = {"mqtt.supplier.topics=test,fake", "mqtt.supplier.qos=0,0"}) +@DirtiesContext +public class MqttSupplierTests { + + static { + Consumer cmd = e -> e.withPortBindings(new PortBinding(Ports.Binding.bindPort(1883), new ExposedPort(1883))); + GenericContainer mosquitto = new GenericContainer("eclipse-mosquitto") + .withExposedPorts(1883) + .withCreateContainerCmdModifier(cmd); + mosquitto.start(); + } + + @Autowired + private Supplier>> mqttSupplier; + + @Autowired + private MessageHandler mqttOutbound; + + @Test + public void testBasicFlow() { + + mqttOutbound.handleMessage(MessageBuilder.withPayload("hello").build()); + + final Flux> messageFlux = mqttSupplier.get(); + + StepVerifier.create(messageFlux) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo("hello"); + } + ) + .thenCancel() + .verify(); + } + + @SpringBootApplication + static class TestApplication { + + @Autowired + private MqttPahoClientFactory mqttClientFactory; + + @Bean + public MessageHandler mqttOutbound() { + MqttPahoMessageHandler messageHandler = new MqttPahoMessageHandler("test", mqttClientFactory); + messageHandler.setAsync(true); + messageHandler.setDefaultTopic("test"); + messageHandler.setConverter(producerConverter()); + return messageHandler; + } + + @Bean + public DefaultPahoMessageConverter producerConverter() { + return new DefaultPahoMessageConverter(1, true, "UTF-8"); + } + } +}