From 1fe892d61758786dd4cf4f1d3a446686d8451c9a Mon Sep 17 00:00:00 2001 From: Daniel Frey Date: Fri, 4 Nov 2022 14:37:48 -0400 Subject: [PATCH] add xmpp-sink application --- README.adoc | 3 + applications/sink/pom.xml | 17 +-- applications/sink/xmpp-sink/README.adoc | 58 ++++++++ applications/sink/xmpp-sink/pom.xml | 85 ++++++++++++ ...onfiguration-metadata-whitelist.properties | 1 + ...dataflow-configuration-metadata.properties | 1 + .../sink/xmpp/XmppSinkInvalidConfigTests.java | 95 +++++++++++++ .../stream/app/sink/xmpp/XmppSinkTests.java | 126 ++++++++++++++++++ .../xmpp/XmppConnectionFactoryProperties.java | 2 +- 9 files changed, 379 insertions(+), 9 deletions(-) create mode 100644 applications/sink/xmpp-sink/README.adoc create mode 100644 applications/sink/xmpp-sink/pom.xml create mode 100644 applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata.properties create mode 100644 applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkInvalidConfigTests.java create mode 100644 applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkTests.java diff --git a/README.adoc b/README.adoc index 5c34864d..77bec007 100644 --- a/README.adoc +++ b/README.adoc @@ -164,6 +164,9 @@ The following are the various components of this repository. | | |link:applications/sink/websocket-sink/README.adoc[Websocket] +| +| +|link:applications/sink/xmpp-sink/README.adoc[XMPP] |=== === Build diff --git a/applications/sink/pom.xml b/applications/sink/pom.xml index df36a0c7..fda6db90 100644 --- a/applications/sink/pom.xml +++ b/applications/sink/pom.xml @@ -12,26 +12,27 @@ analytics-sink cassandra-sink + elasticsearch-sink file-sink ftp-sink + jdbc-sink log-sink mongodb-sink mqtt-sink - jdbc-sink - redis-sink + pgcopy-sink rabbit-sink + redis-sink router-sink - sftp-sink + rsocket-sink s3-sink + sftp-sink tcp-sink throughput-sink - websocket-sink - twitter-update-sink twitter-message-sink + twitter-update-sink wavefront-sink - pgcopy-sink - rsocket-sink - elasticsearch-sink + websocket-sink + xmpp-sink zeromq-sink diff --git a/applications/sink/xmpp-sink/README.adoc b/applications/sink/xmpp-sink/README.adoc new file mode 100644 index 00000000..af29bd60 --- /dev/null +++ b/applications/sink/xmpp-sink/README.adoc @@ -0,0 +1,58 @@ +//tag::ref-doc[] += ZeroMQ Sink + +The "zeromq" sink enables sending messages to a ZeroMQ socket. + +== Input + +* `byte[]` + +== Output + +=== Payload + +N/A + +== Options + +The **$$zeromq$$** $$sink$$ has the following options: + +//tag::configuration-properties[] +Properties grouped by prefix: + + +=== xmpp.consumer + +$$chat-to$$:: $$XMPP handle to send message to.$$ *($$String$$, default: `$$$$`)* + +=== xmpp.factory + +$$host$$:: $$XMPP Host server to connect to.$$ *($$String$$, default: `$$$$`)* +$$password$$:: $$The Password for the connected user.$$ *($$String$$, default: `$$$$`)* +$$port$$:: $$Port for connecting to the host. - Default Client Port: 5222$$ *($$Integer$$, default: `$$5222$$`)* +$$resource$$:: $$The Resource to bind to on the XMPP Host. - Can be empty, server will generate one if not set$$ *($$String$$, default: `$$$$`)* +$$security-mode$$:: $$$$ *($$SecurityMode$$, default: `$$$$`, possible values: `required`,`ifpossible`,`disabled`)* +$$service-name$$:: $$The Service Name to set for the XMPP Domain.$$ *($$String$$, default: `$$$$`)* +$$subscription-mode$$:: $$$$ *($$SubscriptionMode$$, default: `$$$$`, possible values: `accept_all`,`reject_all`,`manual`)* +$$user$$:: $$The User the connection should connect as.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +== Build + +``` +$ ./mvnw clean install -PgenerateApps +$ cd apps +``` +You can find the corresponding binder based projects here. +You can then cd into one of the folders and build it: +``` +$ ./mvnw clean package +``` + +== Examples + +``` +java -jar zeromq-sink.jar --zeromq.consumer.connectUrl=tcp://server:port --zeromq.consumer.topic= +``` + +//end::ref-doc[] diff --git a/applications/sink/xmpp-sink/pom.xml b/applications/sink/xmpp-sink/pom.xml new file mode 100644 index 00000000..6ffb2ace --- /dev/null +++ b/applications/sink/xmpp-sink/pom.xml @@ -0,0 +1,85 @@ + + + 4.0.0 + + xmpp-sink + xmpp-sink + XMPP sink apps + jar + + + org.springframework.cloud.stream.app + stream-applications-core + 4.0.0-SNAPSHOT + ../../stream-applications-core/pom.xml + + + + + + + org.springframework.cloud.fn + xmpp-consumer + + + + + io.projectreactor + reactor-test + test + + + org.awaitility + awaitility + test + + + + org.springframework.cloud.fn + function-test-support + ${project.version} + test + + + + + + + + org.springframework.cloud + spring-cloud-dataflow-apps-docs-plugin + + + org.springframework.cloud + spring-cloud-dataflow-apps-generator-plugin + + + xmpp + sink + ${project.version} + org.springframework.cloud.fn.consumer.xmpp.XmppConsumerConfiguration.class + + + + + + org.springframework.cloud.fn + xmpp-consumer + + + org.springframework.cloud.stream.app + stream-applications-composite-function-support + ${stream-apps-core.version} + + + + + + + + + + + + diff --git a/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..880770fa --- /dev/null +++ b/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.xmpp.XmppConsumerProperties,org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties diff --git a/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata.properties b/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata.properties new file mode 100644 index 00000000..880770fa --- /dev/null +++ b/applications/sink/xmpp-sink/src/main/resources/META-INF/dataflow-configuration-metadata.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.xmpp.XmppConsumerProperties,org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties diff --git a/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkInvalidConfigTests.java b/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkInvalidConfigTests.java new file mode 100644 index 00000000..52f5ba3c --- /dev/null +++ b/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkInvalidConfigTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2014-2022 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.app.sink.xmpp; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties; +import org.springframework.cloud.fn.consumer.xmpp.XmppConsumerProperties; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for XMPP Sink with invalid config. + * + * @author Daniel Frey + * @since 4.0.0 + */ +public class XmppSinkInvalidConfigTests { + + @Test + public void testEmptyFactoryHost() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("xmpp.factory.host: ").applyTo(context); + context.register(Config.class); + context.refresh(); + + }) + .withMessageContaining("Error creating bean with name 'xmpp.factory-org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties': Could not bind properties to 'XmppConnectionFactoryProperties'") + .havingRootCause() + .withMessageContaining("Binding validation errors on xmpp.factory") + .withMessageContaining("Field error in object 'xmpp.factory' on field 'host': rejected value []"); + } + + @Test + public void testEmptyFactoryUser() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("xmpp.factory.user: ").applyTo(context); + context.register(Config.class); + context.refresh(); + + }) + .withMessageContaining("Error creating bean with name 'xmpp.factory-org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties': Could not bind properties to 'XmppConnectionFactoryProperties'") + .havingRootCause() + .withMessageContaining("Binding validation errors on xmpp.factory") + .withMessageContaining("Field error in object 'xmpp.factory' on field 'user': rejected value []"); + } + + @Test + public void testEmptyFactoryPassword() { + assertThatExceptionOfType(BeanCreationException.class) + .isThrownBy(() -> { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + TestPropertyValues.of("xmpp.factory.password: ").applyTo(context); + context.register(Config.class); + context.refresh(); + + }) + .withMessageContaining("Error creating bean with name 'xmpp.factory-org.springframework.cloud.fn.common.xmpp.XmppConnectionFactoryProperties': Could not bind properties to 'XmppConnectionFactoryProperties'") + .havingRootCause() + .withMessageContaining("Binding validation errors on xmpp.factory") + .withMessageContaining("Field error in object 'xmpp.factory' on field 'password': rejected value []"); + } + + @Configuration + @EnableConfigurationProperties({XmppConnectionFactoryProperties.class, XmppConsumerProperties.class}) + static class Config { + } + +} diff --git a/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkTests.java b/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkTests.java new file mode 100644 index 00000000..f70970f3 --- /dev/null +++ b/applications/sink/xmpp-sink/src/test/java/org/springframework/cloud/stream/app/sink/xmpp/XmppSinkTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2014-2022 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.app.sink.xmpp; + +import java.io.IOException; +import java.time.Duration; + +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.SmackException; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.filter.StanzaTypeFilter; +import org.jivesoftware.smack.packet.Stanza; +import org.jivesoftware.smack.tcp.XMPPTCPConnection; +import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.consumer.xmpp.XmppConsumerConfiguration; +import org.springframework.cloud.fn.test.support.xmpp.XmppTestContainerSupport; +import org.springframework.cloud.stream.binder.test.InputDestination; +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.integration.xmpp.XmppHeaders; +import org.springframework.messaging.support.MessageBuilder; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; + +/** + * Tests for XMPP Sink. + * + * @author Daniel Frey + * + * @since 4.0.0 + */ +public class XmppSinkTests implements XmppTestContainerSupport { + + private XMPPTCPConnection clientConnection; + + @BeforeEach + void setup() throws IOException, SmackException, XMPPException, InterruptedException { + + var builder = XMPPTCPConnectionConfiguration.builder(); + builder.setSecurityMode(ConnectionConfiguration.SecurityMode.disabled); + builder.setHost(XmppTestContainerSupport.getXmppHost()); + builder.setPort(XmppTestContainerSupport.getXmppMappedPort()); + builder.setResource(SERVICE_NAME); + builder.setUsernameAndPassword(JANE_USER, USER_PW) + .setXmppDomain(SERVICE_NAME); + this.clientConnection = new XMPPTCPConnection(builder.build()); + this.clientConnection.connect(); + this.clientConnection.login(); + } + + + @Test + public void testSinkFromFunction() { + + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(XmppSinkTestApplication.class)).run( + "--spring.cloud.function.definition=xmppConsumer", + "--xmpp.factory.host=" + XmppTestContainerSupport.getXmppHost(), + "--xmpp.factory.port=" + XmppTestContainerSupport.getXmppMappedPort(), + "--xmpp.factory.user=" + JOHN_USER, + "--xmpp.factory.password=" + USER_PW, + "--xmpp.factory.service-name=" + SERVICE_NAME, + "--xmpp.factory.security-mode=disabled" + )) { + + var inputDestination = context.getBean(InputDestination.class); + + var collector + = this.clientConnection.createStanzaCollector(StanzaTypeFilter.MESSAGE); + + var testMessage = MessageBuilder.withPayload("test") + .setHeader(XmppHeaders.TO, JANE_USER + "@" + SERVICE_NAME) + .build(); + + await().atMost(Duration.ofSeconds(20)).pollDelay(Duration.ofMillis(100)) + .untilAsserted(() -> { + inputDestination.send(testMessage); + Stanza stanza = collector.nextResult(); + assertStanza(stanza); + }); + } + } + + private void assertStanza(Stanza stanza) { + assertTo(stanza); + assertFrom(stanza); + } + + private void assertTo(Stanza stanza) { + assertThat(stanza.getTo().asBareJid().asUnescapedString()).isEqualTo(JANE_USER + "@" + SERVICE_NAME); + } + + private void assertFrom(Stanza stanza) { + assertThat(stanza.getFrom().asBareJid().asUnescapedString()).isEqualTo(JOHN_USER + "@" + SERVICE_NAME); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @Import(XmppConsumerConfiguration.class) + static class XmppSinkTestApplication { + + } + +} diff --git a/functions/common/xmpp-common/src/main/java/org/springframework/cloud/fn/common/xmpp/XmppConnectionFactoryProperties.java b/functions/common/xmpp-common/src/main/java/org/springframework/cloud/fn/common/xmpp/XmppConnectionFactoryProperties.java index aaf0e958..99cd071e 100644 --- a/functions/common/xmpp-common/src/main/java/org/springframework/cloud/fn/common/xmpp/XmppConnectionFactoryProperties.java +++ b/functions/common/xmpp-common/src/main/java/org/springframework/cloud/fn/common/xmpp/XmppConnectionFactoryProperties.java @@ -89,7 +89,7 @@ public class XmppConnectionFactoryProperties { this.password = password; } - @NotEmpty(message = "port is required") + @NotEmpty(message = "password is required") public String getPassword() { return password; }