From 517ccd5b40932d65d6c0c34a2fb0633182416dbc Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Mon, 4 May 2020 17:50:58 -0400 Subject: [PATCH] Initial Commit Migrating the existing structure from the following location: https://github.com/spring-cloud-stream-app-starters/stream-applications/tree/restructuring --- .gitignore | 27 + .mvn/jvm.config | 1 + .mvn/maven.config | 1 + .mvn/wrapper/MavenWrapperDownloader.java | 117 ++++ .mvn/wrapper/maven-wrapper.jar | Bin 0 -> 50710 bytes .mvn/wrapper/maven-wrapper.properties | 2 + README.adoc | 1 + applications/apps-core/.settings.xml | 81 +++ applications/apps-core/CODE_OF_CONDUCT.adoc | 44 ++ applications/apps-core/LICENSE | 201 ++++++ applications/apps-core/README.adoc | 2 + applications/apps-core/common/pom.xml | 25 + .../common/stream-apps-file-common/pom.xml | 21 + .../app/file/FileConsumerProperties.java | 82 +++ .../stream/app/file/FileReadingMode.java | 30 + .../cloud/stream/app/file/FileUtils.java | 103 +++ .../remote/AbstractRemoteFileProperties.java | 71 +++ .../AbstractRemoteFileSinkProperties.java | 102 +++ .../AbstractRemoteFileSourceProperties.java | 120 ++++ .../AbstractRemoteServerProperties.java | 86 +++ .../stream/app/file/remote/FilePathUtils.java | 57 ++ ...ngTransactionSynchronizationProcessor.java | 63 ++ .../common/stream-apps-ftp-common/pom.xml | 26 + .../ftp/FtpSessionFactoryConfiguration.java | 56 ++ .../app/ftp/FtpSessionFactoryProperties.java | 82 +++ .../README.adoc | 181 ++++++ .../stream-apps-metadata-store-common/pom.xml | 152 +++++ .../ClientCacheAutoConfiguration.java | 44 ++ .../MetadataStoreAutoConfiguration.java | 234 +++++++ .../app/metadata/MetadataStoreProperties.java | 290 +++++++++ .../main/resources/META-INF/spring.factories | 2 + .../MetadataStoreAutoConfigurationTests.java | 158 +++++ .../stream-apps-micrometer-common/pom.xml | 35 + .../CloudFoundryMicrometerCommonTags.java | 75 +++ ...SpringCloudStreamMicrometerCommonTags.java | 77 +++ ...eamMicrometerEnvironmentPostProcessor.java | 64 ++ .../main/resources/META-INF/spring.factories | 6 + .../common/AbstractMicrometerTagTest.java | 96 +++ .../CloudFoundryMicrometerCommonTagsTest.java | 100 +++ .../InfluxReservedKeywordHandlingTest.java | 62 ++ ...ngCloudStreamMicrometerCommonTagsTest.java | 77 +++ ...icrometerEnvironmentPostProcessorTest.java | 94 +++ .../app/micrometer/common/pcf-scs-info.json | 13 + .../stream-apps-postprocessor-common/pom.xml | 22 + .../ContentTypeEnvironmentPostProcessor.java | 88 +++ ...tentTypeEnvironmentPostProcessorTests.java | 227 +++++++ .../stream-apps-security-common/README.adoc | 33 + .../stream-apps-security-common/pom.xml | 52 ++ ...arterWebFluxSecurityAutoConfiguration.java | 66 ++ ...ppStarterWebSecurityAutoConfiguration.java | 75 +++ ...ebSecurityAutoConfigurationProperties.java | 57 ++ .../common/OnHttpCsrfOrSecurityDisabled.java | 44 ++ .../main/resources/META-INF/spring.factories | 3 + .../common/AbstractSecurityCommonTests.java | 40 ++ ...isabledManagementSecurityEnabledTests.java | 70 ++ ...SecurityDisabledAuthorizedAccessTests.java | 84 +++ ...curityDisabledUnauthorizedAccessTests.java | 64 ++ ...EnabledManagementSecurityEnabledTests.java | 70 ++ ...isabledManagementSecurityEnabledTests.java | 71 +++ ...SecurityDisabledAuthorizedAccessTests.java | 86 +++ ...curityDisabledUnauthorizedAccessTests.java | 73 +++ ...EnabledManagementSecurityEnabledTests.java | 72 +++ .../README.adoc | 52 ++ .../pom.xml | 45 ++ .../DataFlowTaskLaunchRequest.java | 64 ++ ...lowTaskLaunchRequestAutoConfiguration.java | 162 +++++ .../DataflowTaskLaunchRequestProperties.java | 111 ++++ ...essionEvaluatingTaskNameMessageMapper.java | 38 ++ .../tasklaunchrequest/KeyValueListParser.java | 66 ++ .../TaskLaunchRequestFunction.java | 31 + .../TaskLaunchRequestMessageProcessor.java | 70 ++ .../CommandLineArgumentsMessageMapper.java | 23 + .../support/TaskLaunchRequestSupplier.java | 65 ++ .../support/TaskNameMessageMapper.java | 23 + ...onfiguration-metadata-whitelist.properties | 2 + .../main/resources/META-INF/spring.factories | 2 + .../KeyValueListParserTests.java | 97 +++ .../TaskLaunchRequestIntegrationTests.java | 313 +++++++++ .../TaskLaunchRequestPropertiesTests.java | 77 +++ .../common/stream-apps-test-support/pom.xml | 29 + .../test/BinderTestPropertiesInitializer.java | 58 ++ .../app/test/PropertiesInitializer.java | 36 ++ .../file/remote/RemoteFileTestSupport.java | 149 +++++ .../app/test/ip/IpSinkTestConfiguration.java | 43 ++ .../test/ip/IpSourceTestConfiguration.java | 43 ++ .../app/test/redis/RedisTestSupport.java | 43 ++ .../script/ScriptableTestConfiguration.java | 44 ++ applications/apps-core/pom.xml | 348 ++++++++++ .../apps-metadata/CODE_OF_CONDUCT.adoc | 44 ++ applications/apps-metadata/LICENSE | 201 ++++++ applications/apps-metadata/pom.xml | 87 +++ .../release-tools/core-tag-next-version.sh | 29 + .../release-tools/core-version-check.sh | 38 ++ .../release-tools/core-version-upgrade.sh | 39 ++ .../stream-apps-descriptor/pom.xml | 140 ++++ .../META-INF/kafka-apps-docker.properties | 13 + .../kafka-apps-maven-repo-url.properties | 26 + .../META-INF/kafka-apps-maven.properties | 27 + .../META-INF/rabbit-apps-docker.properties | 13 + .../rabbit-apps-maven-repo-url.properties | 26 + .../META-INF/rabbit-apps-maven.properties | 27 + .../apps-metadata/stream-apps-docs/README.md | 2 + .../apps-metadata/stream-apps-docs/pom.xml | 325 ++++++++++ .../src/main/asciidoc/.gitignore | 2 + .../src/main/asciidoc/Guardfile | 20 + .../src/main/asciidoc/appendix.adoc | 2 + .../src/main/asciidoc/contributing.adoc | 42 ++ .../images/app-starter-naming-conventions.png | Bin 0 -> 133938 bytes .../src/main/asciidoc/images/logo.png | Bin 0 -> 19499 bytes .../images/starters-pom-dependencies.png | Bin 0 -> 142803 bytes .../src/main/asciidoc/index-docinfo.xml | 14 + .../src/main/asciidoc/index.adoc | 50 ++ .../src/main/asciidoc/overview.adoc | 102 +++ .../src/main/asciidoc/pom-dependencies.adoc | 23 + .../src/main/asciidoc/processors.adoc | 15 + .../src/main/asciidoc/sinks.adoc | 19 + .../src/main/asciidoc/sources.adoc | 20 + .../src/main/docbook/css/highlight.css | 35 + .../src/main/docbook/css/manual-multipage.css | 9 + .../main/docbook/css/manual-singlepage.css | 6 + .../src/main/docbook/css/manual.css | 344 ++++++++++ .../src/main/docbook/images/background.png | Bin 0 -> 10947 bytes .../src/main/docbook/images/callouts/1.png | Bin 0 -> 329 bytes .../src/main/docbook/images/callouts/10.png | Bin 0 -> 361 bytes .../src/main/docbook/images/callouts/11.png | Bin 0 -> 565 bytes .../src/main/docbook/images/callouts/12.png | Bin 0 -> 617 bytes .../src/main/docbook/images/callouts/13.png | Bin 0 -> 623 bytes .../src/main/docbook/images/callouts/14.png | Bin 0 -> 411 bytes .../src/main/docbook/images/callouts/15.png | Bin 0 -> 640 bytes .../src/main/docbook/images/callouts/2.png | Bin 0 -> 353 bytes .../src/main/docbook/images/callouts/3.png | Bin 0 -> 350 bytes .../src/main/docbook/images/callouts/4.png | Bin 0 -> 345 bytes .../src/main/docbook/images/callouts/5.png | Bin 0 -> 348 bytes .../src/main/docbook/images/callouts/6.png | Bin 0 -> 355 bytes .../src/main/docbook/images/callouts/7.png | Bin 0 -> 344 bytes .../src/main/docbook/images/callouts/8.png | Bin 0 -> 357 bytes .../src/main/docbook/images/callouts/9.png | Bin 0 -> 357 bytes .../src/main/docbook/images/caution.png | Bin 0 -> 2099 bytes .../src/main/docbook/images/cover.png | Bin 0 -> 78214 bytes .../src/main/docbook/images/important.png | Bin 0 -> 2085 bytes .../src/main/docbook/images/logo.png | Bin 0 -> 19499 bytes .../src/main/docbook/images/logo.svg | 16 + .../src/main/docbook/images/note.png | Bin 0 -> 2257 bytes .../src/main/docbook/images/tip.png | Bin 0 -> 931 bytes .../src/main/docbook/images/warning.png | Bin 0 -> 2130 bytes .../src/main/docbook/xsl/common.xsl | 45 ++ .../src/main/docbook/xsl/epub.xsl | 31 + .../src/main/docbook/xsl/html-multipage.xsl | 73 +++ .../src/main/docbook/xsl/html-singlepage.xsl | 30 + .../src/main/docbook/xsl/html.xsl | 141 +++++ .../src/main/docbook/xsl/pdf.xsl | 591 +++++++++++++++++ .../src/main/docbook/xsl/xslthl-config.xml | 23 + .../main/docbook/xsl/xslthl/asciidoc-hl.xml | 41 ++ .../src/main/docbook/xsl/xslthl/bourne-hl.xml | 95 +++ .../src/main/docbook/xsl/xslthl/c-hl.xml | 117 ++++ .../src/main/docbook/xsl/xslthl/cpp-hl.xml | 151 +++++ .../src/main/docbook/xsl/xslthl/csharp-hl.xml | 194 ++++++ .../src/main/docbook/xsl/xslthl/css-hl.xml | 176 +++++ .../src/main/docbook/xsl/xslthl/html-hl.xml | 122 ++++ .../src/main/docbook/xsl/xslthl/ini-hl.xml | 45 ++ .../src/main/docbook/xsl/xslthl/java-hl.xml | 117 ++++ .../main/docbook/xsl/xslthl/javascript-hl.xml | 147 +++++ .../src/main/docbook/xsl/xslthl/json-hl.xml | 37 ++ .../src/main/docbook/xsl/xslthl/perl-hl.xml | 120 ++++ .../src/main/docbook/xsl/xslthl/php-hl.xml | 154 +++++ .../main/docbook/xsl/xslthl/properties-hl.xml | 38 ++ .../src/main/docbook/xsl/xslthl/python-hl.xml | 100 +++ .../src/main/docbook/xsl/xslthl/ruby-hl.xml | 109 ++++ .../main/docbook/xsl/xslthl/sql2003-hl.xml | 565 +++++++++++++++++ .../src/main/docbook/xsl/xslthl/yaml-hl.xml | 47 ++ .../src/main/javadoc/spring-javadoc.css | 599 ++++++++++++++++++ applications/pom.xml | 19 + .../processor/bridge-processor/README.adoc | 10 + .../processor/bridge-processor/pom.xml | 80 +++ .../app/BridgeProcessorConfiguration.java | 34 + .../stream/app/BridgeProcessorTests.java | 60 ++ .../processor/filter-processor/README.adoc | 22 + .../processor/filter-processor/pom.xml | 91 +++ ...onfiguration-metadata-whitelist.properties | 1 + .../processor/FilterProcessorTests.java | 68 ++ applications/processor/pom.xml | 19 + .../processor/splitter-processor/README.adoc | 28 + .../processor/splitter-processor/pom.xml | 99 +++ ...onfiguration-metadata-whitelist.properties | 1 + .../processor/SplitterProcessorTests.java | 63 ++ .../test/resources/META-INF/spring.binders | 2 + .../processor/transform-processor/README.adoc | 23 + .../processor/transform-processor/pom.xml | 91 +++ ...onfiguration-metadata-whitelist.properties | 1 + .../processor/TransformProcessorTests.java | 60 ++ applications/sink/cassandra-sink/README.adoc | 44 ++ applications/sink/cassandra-sink/pom.xml | 79 +++ ...onfiguration-metadata-whitelist.properties | 3 + applications/sink/counter-sink/README.adoc | 60 ++ applications/sink/counter-sink/pom.xml | 98 +++ ...onfiguration-metadata-whitelist.properties | 2 + .../resources/MicrometerCounterAppStarter.png | Bin 0 -> 87567 bytes .../app/counter/sink/CounterSinkTests.java | 74 +++ applications/sink/file-sink/README.adoc | 28 + applications/sink/file-sink/pom.xml | 78 +++ ...onfiguration-metadata-whitelist.properties | 1 + .../cloud/stream/app/FileSinkTests.java | 71 +++ applications/sink/jdbc-sink/README.adoc | 58 ++ applications/sink/jdbc-sink/pom.xml | 96 +++ ...onfiguration-metadata-whitelist.properties | 9 + .../stream/app/jdbc/sink/JdbcSinkTests.java | 94 +++ .../jdbc-sink/src/test/resources/schema.sql | 7 + applications/sink/log-sink/README.adoc | 21 + applications/sink/log-sink/pom.xml | 90 +++ ...onfiguration-metadata-whitelist.properties | 1 + .../cloud/stream/app/LogSinkTests.java | 59 ++ applications/sink/mongodb-sink/README.adoc | 25 + applications/sink/mongodb-sink/pom.xml | 78 +++ ...onfiguration-metadata-whitelist.properties | 1 + applications/sink/pom.xml | 21 + applications/sink/rabbit-sink/README.adoc | 34 + applications/sink/rabbit-sink/pom.xml | 112 ++++ ...onfiguration-metadata-whitelist.properties | 2 + .../app/rabbit/sink/OwnConnectionTest.java | 61 ++ .../sink/RabbitSinkIntegrationTests.java | 102 +++ .../sink/RabbitSinkInvalidConfigTests.java | 84 +++ .../SimpleRoutingKeyAndCustomHeaderTests.java | 46 ++ applications/source/file-source/README.adoc | 33 + applications/source/file-source/pom.xml | 78 +++ ...onfiguration-metadata-whitelist.properties | 2 + .../FileSourceTests.java | 63 ++ applications/source/http-source/README.adoc | 31 + applications/source/http-source/pom.xml | 95 +++ ...onfiguration-metadata-whitelist.properties | 3 + .../cloud/stream/app/HttpSourceTests.java | 57 ++ applications/source/jdbc-source/README.adoc | 36 ++ applications/source/jdbc-source/pom.xml | 100 +++ ...onfiguration-metadata-whitelist.properties | 10 + .../app/jdbc/source/DefaultBehaviorTests.java | 58 ++ .../source/JdbcSourceIntegrationTests.java | 56 ++ .../Select2PerPollNoSplitWithUpdateTests.java | 65 ++ .../jdbc/source/SelectAllNoSplitTests.java | 59 ++ .../jdbc/source/SelectAllWithDelayTests.java | 59 ++ .../source/SelectAllWithMinDelayTests.java | 65 ++ .../jdbc-source/src/test/resources/schema.sql | 10 + .../source/mongodb-source/README.adoc | 25 + applications/source/mongodb-source/pom.xml | 79 +++ ...onfiguration-metadata-whitelist.properties | 1 + applications/source/pom.xml | 19 + applications/source/time-source/README.adoc | 18 + applications/source/time-source/pom.xml | 78 +++ ...onfiguration-metadata-whitelist.properties | 2 + .../cloud/stream/app/TimeSourceTests.java | 66 ++ .../consumer/cassandra-consumer/.gitignore | 30 + .../consumer/cassandra-consumer/.toDelete | 0 functions/consumer/cassandra-consumer/pom.xml | 78 +++ .../CassandraConsumerConfiguration.java | 209 ++++++ .../CassandraConsumerProperties.java | 97 +++ .../CassandraAppClusterConfiguration.java | 157 +++++ .../cluster/CassandraClusterProperties.java | 85 +++ .../cluster/TrustAllSSLContextFactory.java | 66 ++ .../cassandra/query/ColumnNameExtractor.java | 29 + .../query/InsertQueryColumnNameExtractor.java | 56 ++ .../query/UpdateQueryColumnNameExtractor.java | 58 ++ .../src/main/resources/application.properties | 1 + .../CassandraConsumerApplicationTests.java | 103 +++ .../cassandra/CassandraEntityInsertTests.java | 67 ++ .../cassandra/CassandraIngestInsertTests.java | 65 ++ .../CassandraIngestNamedParamsTests.java | 72 +++ .../cassandra/CassandraIngestUpdateTests.java | 67 ++ .../fn/consumer/cassandra/domain/Book.java | 146 +++++ .../src/test/resources/init-db.cql | 10 + .../consumer/counter-consumer/.gitignore | 28 + functions/consumer/counter-consumer/pom.xml | 54 ++ .../counter/CounterConsumerConfiguration.java | 178 ++++++ .../counter/CounterConsumerProperties.java | 172 +++++ .../StringToSpelConversionFunction.java | 61 ++ .../counter/ConverterFunctionAdapter.java | 22 + .../consumer/counter/CountWithAmountTest.java | 43 ++ .../counter/CounterConsumerParentTest.java | 46 ++ .../fn/consumer/counter/EmptyTagsTests.java | 53 ++ .../counter/ExpressionCounterNameTests.java | 41 ++ .../fn/consumer/counter/FixedTagsTests.java | 51 ++ .../counter/LiteralTagExpressionsTests.java | 52 ++ .../fn/consumer/counter/NullTagsTests.java | 53 ++ functions/consumer/file-consumer/pom.xml | 41 ++ .../file/FileConsumerConfiguration.java | 73 +++ .../consumer/file/FileConsumerProperties.java | 162 +++++ .../file/AbstractFileConsumerTests.java | 57 ++ .../fn/consumer/file/BinaryFileTests.java | 45 ++ .../fn/consumer/file/ExpressionTests.java | 64 ++ .../cloud/fn/consumer/file/TextFileTests.java | 46 ++ functions/consumer/jdbc-consumer/pom.xml | 63 ++ .../DefaultInitializationScriptResource.java | 60 ++ .../jdbc/JdbcConsumerConfiguration.java | 314 +++++++++ .../consumer/jdbc/JdbcConsumerProperties.java | 109 ++++ .../consumer/jdbc/ShorthandMapConverter.java | 61 ++ .../jdbc/BatchInsertTimeoutTests.java | 51 ++ .../jdbc/DataReceivedAsByteArrayTests.java | 49 ++ .../jdbc/ExplicitTableCreationTests.java | 53 ++ .../fn/consumer/jdbc/HeaderInsertTests.java | 47 ++ .../jdbc/ImplicitTableCreationTests.java | 53 ++ .../jdbc/JdbcConsumerApplicationTests.java | 92 +++ .../jdbc/JsonStringPayloadInsertTests.java | 61 ++ .../consumer/jdbc/MapPayloadInsertTests.java | 69 ++ .../consumer/jdbc/SimpleBatchInsertTests.java | 51 ++ .../fn/consumer/jdbc/SimpleInsertTests.java | 46 ++ .../fn/consumer/jdbc/SimpleMappingTests.java | 50 ++ .../cloud/fn/consumer/jdbc/SpELTests.java | 52 ++ .../UnqualifiableColumnExpressionTests.java | 46 ++ .../fn/consumer/jdbc/VaryingInsertTests.java | 63 ++ .../src/test/resources/explicit-script.sql | 6 + .../src/test/resources/schema.sql | 7 + functions/consumer/log-consumer/.gitignore | 31 + functions/consumer/log-consumer/pom.xml | 59 ++ .../log/LogConsumerConfiguration.java | 52 ++ .../consumer/log/LogConsumerProperties.java | 84 +++ .../log/LogConsumerApplicationTests.java | 91 +++ .../consumer/mongodb-consumer/.gitignore | 31 + functions/consumer/mongodb-consumer/pom.xml | 58 ++ .../mongo/MongoDbConsumerConfiguration.java | 70 ++ .../mongo/MongoDbConsumerProperties.java | 66 ++ .../MongoDbConsumerApplicationTests.java | 91 +++ functions/consumer/rabbit-consumer/pom.xml | 40 ++ .../rabbit/RabbitConsumerConfiguration.java | 150 +++++ .../rabbit/RabbitConsumerProperties.java | 142 +++++ functions/function/filter-function/.gitignore | 28 + functions/function/filter-function/pom.xml | 36 ++ .../filter/FilterFunctionConfiguration.java | 43 ++ .../src/main/resources/application.properties | 1 + .../FilterFunctionApplicationTests.java | 53 ++ .../payload-converter-function/pom.xml | 37 ++ .../java/functions/ByteArrayTextToString.java | 51 ++ .../functions/ByteArrayTextToStringTests.java | 79 +++ functions/function/spel-function/.gitignore | 28 + functions/function/spel-function/pom.xml | 46 ++ .../fn/spel/SpelFunctionConfiguration.java | 47 ++ .../cloud/fn/spel/SpelFunctionProperties.java | 47 ++ .../fn/spel/SpelFunctionApplicationTests.java | 60 ++ .../function/splitter-function/.gitignore | 28 + functions/function/splitter-function/pom.xml | 50 ++ .../SplitterFunctionConfiguration.java | 125 ++++ .../splitter/SplitterFunctionProperties.java | 128 ++++ .../SplitterFunctionApplicationTests.java | 48 ++ functions/pom.xml | 205 ++++++ functions/spring-functions-parent/pom.xml | 40 ++ functions/supplier/file-supplier/pom.xml | 51 ++ .../supplier/file/FileConsumerProperties.java | 84 +++ .../fn/supplier/file/FileReadingMode.java | 29 + .../file/FileSupplierConfiguration.java | 104 +++ .../supplier/file/FileSupplierProperties.java | 112 ++++ .../cloud/fn/supplier/file/FileUtils.java | 103 +++ .../file/AbstractFileSupplierTests.java | 60 ++ .../file/DefaultFileSupplierTests.java | 77 +++ .../fn/supplier/file/FileModeRefTests.java | 63 ++ .../file/FilePayloadWithPatternTests.java | 67 ++ .../file/FilePayloadWithRegexTests.java | 67 ++ .../LinesAndMarkersAsJsonPayloadTests.java | 80 +++ .../fn/supplier/file/LinesPayloadTests.java | 55 ++ functions/supplier/http-supplier/.gitignore | 28 + functions/supplier/http-supplier/pom.xml | 55 ++ .../supplier/http/HttpSourceProperties.java | 120 ++++ .../http/HttpSupplierConfiguration.java | 77 +++ .../src/main/resources/application.properties | 1 + .../http/HttpSupplierApplicationTests.java | 123 ++++ .../http-supplier/src/test/resources/test.jks | Bin 0 -> 1276 bytes functions/supplier/jdbc-supplier/pom.xml | 66 ++ .../jdbc/JdbcSupplierConfiguration.java | 84 +++ .../supplier/jdbc/JdbcSupplierProperties.java | 85 +++ .../jdbc/DefaultJdbcSupplierTests.java | 81 +++ .../jdbc/NonSplitJdbcSupplierTests.java | 62 ++ .../src/test/resources/schema.sql | 10 + .../supplier/mongodb-supplier/.gitignore | 31 + functions/supplier/mongodb-supplier/pom.xml | 68 ++ .../mongo/MongodbSupplierConfiguration.java | 99 +++ .../mongo/MongodbSupplierProperties.java | 91 +++ .../src/main/resources/application.properties | 1 + .../MongodbSupplierApplicationTests.java | 96 +++ functions/supplier/time-supplier/pom.xml | 45 ++ .../cloud/fn/supplier/time/DateFormat.java | 81 +++ .../fn/supplier/time/TimeProperties.java | 43 ++ .../time/TimeSupplierConfiguration.java | 41 ++ .../time/SimpleTimeSupplierTests.java | 43 ++ .../time/TimeSupplierApplicationTests.java | 42 ++ .../supplier/time/VariationToSimpleTests.java | 54 ++ mvnw | 253 ++++++++ mvnw.cmd | 182 ++++++ pom.xml | 16 + 383 files changed, 24851 insertions(+) create mode 100644 .gitignore create mode 100644 .mvn/jvm.config create mode 100644 .mvn/maven.config create mode 100644 .mvn/wrapper/MavenWrapperDownloader.java create mode 100644 .mvn/wrapper/maven-wrapper.jar create mode 100644 .mvn/wrapper/maven-wrapper.properties create mode 100644 README.adoc create mode 100644 applications/apps-core/.settings.xml create mode 100644 applications/apps-core/CODE_OF_CONDUCT.adoc create mode 100644 applications/apps-core/LICENSE create mode 100644 applications/apps-core/README.adoc create mode 100644 applications/apps-core/common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-file-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileConsumerProperties.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileReadingMode.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileUtils.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileProperties.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSinkProperties.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSourceProperties.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteServerProperties.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/FilePathUtils.java create mode 100644 applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/RemoteFileDeletingTransactionSynchronizationProcessor.java create mode 100644 applications/apps-core/common/stream-apps-ftp-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryProperties.java create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/README.adoc create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/ClientCacheAutoConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreProperties.java create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/src/main/resources/META-INF/spring.factories create mode 100644 applications/apps-core/common/stream-apps-metadata-store-common/src/test/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfigurationTests.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTags.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTags.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessor.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/main/resources/META-INF/spring.factories create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/AbstractMicrometerTagTest.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTagsTest.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/InfluxReservedKeywordHandlingTest.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTagsTest.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessorTest.java create mode 100644 applications/apps-core/common/stream-apps-micrometer-common/src/test/resources/org/springframework/cloud/stream/app/micrometer/common/pcf-scs-info.json create mode 100644 applications/apps-core/common/stream-apps-postprocessor-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-postprocessor-common/src/main/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessor.java create mode 100644 applications/apps-core/common/stream-apps-postprocessor-common/src/test/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessorTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/README.adoc create mode 100644 applications/apps-core/common/stream-apps-security-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebFluxSecurityAutoConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfigurationProperties.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/OnHttpCsrfOrSecurityDisabled.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/main/resources/META-INF/spring.factories create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/AbstractSecurityCommonTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityDisabledManagementSecurityEnabledTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityEnabledTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityDisabledManagementSecurityEnabledTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java create mode 100644 applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityEnabledTests.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/README.adoc create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/pom.xml create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequest.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequestAutoConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataflowTaskLaunchRequestProperties.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/ExpressionEvaluatingTaskNameMessageMapper.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParser.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestFunction.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestMessageProcessor.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/CommandLineArgumentsMessageMapper.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskLaunchRequestSupplier.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskNameMessageMapper.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring-configuration-metadata-whitelist.properties create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring.factories create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParserTests.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestIntegrationTests.java create mode 100644 applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestPropertiesTests.java create mode 100644 applications/apps-core/common/stream-apps-test-support/pom.xml create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/BinderTestPropertiesInitializer.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/PropertiesInitializer.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/file/remote/RemoteFileTestSupport.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSinkTestConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSourceTestConfiguration.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/redis/RedisTestSupport.java create mode 100644 applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/script/ScriptableTestConfiguration.java create mode 100644 applications/apps-core/pom.xml create mode 100644 applications/apps-metadata/CODE_OF_CONDUCT.adoc create mode 100644 applications/apps-metadata/LICENSE create mode 100644 applications/apps-metadata/pom.xml create mode 100755 applications/apps-metadata/release-tools/core-tag-next-version.sh create mode 100755 applications/apps-metadata/release-tools/core-version-check.sh create mode 100755 applications/apps-metadata/release-tools/core-version-upgrade.sh create mode 100644 applications/apps-metadata/stream-apps-descriptor/pom.xml create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-docker.properties create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven-repo-url.properties create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven.properties create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-docker.properties create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven-repo-url.properties create mode 100644 applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven.properties create mode 100644 applications/apps-metadata/stream-apps-docs/README.md create mode 100644 applications/apps-metadata/stream-apps-docs/pom.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/.gitignore create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/Guardfile create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/appendix.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/contributing.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/app-starter-naming-conventions.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/logo.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/starters-pom-dependencies.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index-docinfo.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/overview.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/pom-dependencies.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/processors.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sinks.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sources.adoc create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/css/highlight.css create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-multipage.css create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-singlepage.css create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual.css create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/background.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/1.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/10.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/11.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/12.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/13.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/14.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/15.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/2.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/3.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/4.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/5.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/6.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/7.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/8.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/9.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/caution.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/cover.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/important.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.svg create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/note.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/tip.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/images/warning.png create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/common.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/epub.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-multipage.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-singlepage.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/pdf.xsl create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl-config.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/asciidoc-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/bourne-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/c-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/cpp-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/csharp-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/css-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/html-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ini-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/java-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/javascript-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/json-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/perl-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/php-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/properties-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/python-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ruby-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/sql2003-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/yaml-hl.xml create mode 100644 applications/apps-metadata/stream-apps-docs/src/main/javadoc/spring-javadoc.css create mode 100644 applications/pom.xml create mode 100644 applications/processor/bridge-processor/README.adoc create mode 100644 applications/processor/bridge-processor/pom.xml create mode 100644 applications/processor/bridge-processor/src/main/java/org/springframework/cloud/stream/app/BridgeProcessorConfiguration.java create mode 100644 applications/processor/bridge-processor/src/test/java/org/springframework/cloud/stream/app/BridgeProcessorTests.java create mode 100644 applications/processor/filter-processor/README.adoc create mode 100644 applications/processor/filter-processor/pom.xml create mode 100644 applications/processor/filter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/processor/filter-processor/src/test/java/org/springframework/cloud/stream/app/filter/processor/FilterProcessorTests.java create mode 100644 applications/processor/pom.xml create mode 100644 applications/processor/splitter-processor/README.adoc create mode 100644 applications/processor/splitter-processor/pom.xml create mode 100644 applications/processor/splitter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/processor/splitter-processor/src/test/java/org/springframework/cloud/stream/app/splitter/processor/SplitterProcessorTests.java create mode 100644 applications/processor/splitter-processor/src/test/resources/META-INF/spring.binders create mode 100644 applications/processor/transform-processor/README.adoc create mode 100644 applications/processor/transform-processor/pom.xml create mode 100644 applications/processor/transform-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/processor/transform-processor/src/test/java/org/springframework/cloud/stream/app/transform/processor/TransformProcessorTests.java create mode 100644 applications/sink/cassandra-sink/README.adoc create mode 100644 applications/sink/cassandra-sink/pom.xml create mode 100644 applications/sink/cassandra-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/counter-sink/README.adoc create mode 100644 applications/sink/counter-sink/pom.xml create mode 100644 applications/sink/counter-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/counter-sink/src/main/resources/MicrometerCounterAppStarter.png create mode 100644 applications/sink/counter-sink/src/test/java/org/springframework/cloud/stream/app/counter/sink/CounterSinkTests.java create mode 100644 applications/sink/file-sink/README.adoc create mode 100644 applications/sink/file-sink/pom.xml create mode 100644 applications/sink/file-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/file-sink/src/test/java/org/springframework/cloud/stream/app/FileSinkTests.java create mode 100644 applications/sink/jdbc-sink/README.adoc create mode 100644 applications/sink/jdbc-sink/pom.xml create mode 100644 applications/sink/jdbc-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/jdbc-sink/src/test/java/org/springframework/cloud/stream/app/jdbc/sink/JdbcSinkTests.java create mode 100644 applications/sink/jdbc-sink/src/test/resources/schema.sql create mode 100644 applications/sink/log-sink/README.adoc create mode 100644 applications/sink/log-sink/pom.xml create mode 100644 applications/sink/log-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/log-sink/src/test/java/org/springframework/cloud/stream/app/LogSinkTests.java create mode 100644 applications/sink/mongodb-sink/README.adoc create mode 100644 applications/sink/mongodb-sink/pom.xml create mode 100644 applications/sink/mongodb-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/pom.xml create mode 100644 applications/sink/rabbit-sink/README.adoc create mode 100644 applications/sink/rabbit-sink/pom.xml create mode 100644 applications/sink/rabbit-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/OwnConnectionTest.java create mode 100644 applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkIntegrationTests.java create mode 100644 applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkInvalidConfigTests.java create mode 100644 applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/SimpleRoutingKeyAndCustomHeaderTests.java create mode 100644 applications/source/file-source/README.adoc create mode 100644 applications/source/file-source/pom.xml create mode 100644 applications/source/file-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/source/file-source/src/test/java/org.springframework.cloud.stream.app/FileSourceTests.java create mode 100644 applications/source/http-source/README.adoc create mode 100644 applications/source/http-source/pom.xml create mode 100644 applications/source/http-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/source/http-source/src/test/java/org/springframework/cloud/stream/app/HttpSourceTests.java create mode 100644 applications/source/jdbc-source/README.adoc create mode 100644 applications/source/jdbc-source/pom.xml create mode 100644 applications/source/jdbc-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/DefaultBehaviorTests.java create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/JdbcSourceIntegrationTests.java create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/Select2PerPollNoSplitWithUpdateTests.java create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllNoSplitTests.java create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithDelayTests.java create mode 100644 applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithMinDelayTests.java create mode 100644 applications/source/jdbc-source/src/test/resources/schema.sql create mode 100644 applications/source/mongodb-source/README.adoc create mode 100644 applications/source/mongodb-source/pom.xml create mode 100644 applications/source/mongodb-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/source/pom.xml create mode 100644 applications/source/time-source/README.adoc create mode 100644 applications/source/time-source/pom.xml create mode 100644 applications/source/time-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties create mode 100644 applications/source/time-source/src/test/java/org/springframework/cloud/stream/app/TimeSourceTests.java create mode 100644 functions/consumer/cassandra-consumer/.gitignore create mode 100644 functions/consumer/cassandra-consumer/.toDelete create mode 100644 functions/consumer/cassandra-consumer/pom.xml create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerConfiguration.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerProperties.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraAppClusterConfiguration.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraClusterProperties.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/TrustAllSSLContextFactory.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/ColumnNameExtractor.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/InsertQueryColumnNameExtractor.java create mode 100644 functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/UpdateQueryColumnNameExtractor.java create mode 100644 functions/consumer/cassandra-consumer/src/main/resources/application.properties create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerApplicationTests.java create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraEntityInsertTests.java create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestInsertTests.java create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestNamedParamsTests.java create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestUpdateTests.java create mode 100644 functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/domain/Book.java create mode 100644 functions/consumer/cassandra-consumer/src/test/resources/init-db.cql create mode 100644 functions/consumer/counter-consumer/.gitignore create mode 100644 functions/consumer/counter-consumer/pom.xml create mode 100644 functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerConfiguration.java create mode 100644 functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerProperties.java create mode 100644 functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/StringToSpelConversionFunction.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ConverterFunctionAdapter.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CountWithAmountTest.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerParentTest.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/EmptyTagsTests.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ExpressionCounterNameTests.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/FixedTagsTests.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/LiteralTagExpressionsTests.java create mode 100644 functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/NullTagsTests.java create mode 100644 functions/consumer/file-consumer/pom.xml create mode 100644 functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerConfiguration.java create mode 100644 functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerProperties.java create mode 100644 functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/AbstractFileConsumerTests.java create mode 100644 functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/BinaryFileTests.java create mode 100644 functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/ExpressionTests.java create mode 100644 functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/TextFileTests.java create mode 100644 functions/consumer/jdbc-consumer/pom.xml create mode 100644 functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/DefaultInitializationScriptResource.java create mode 100644 functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerConfiguration.java create mode 100644 functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerProperties.java create mode 100644 functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/ShorthandMapConverter.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/BatchInsertTimeoutTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/DataReceivedAsByteArrayTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ExplicitTableCreationTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/HeaderInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ImplicitTableCreationTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerApplicationTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JsonStringPayloadInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/MapPayloadInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleBatchInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleMappingTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SpELTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/UnqualifiableColumnExpressionTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/VaryingInsertTests.java create mode 100644 functions/consumer/jdbc-consumer/src/test/resources/explicit-script.sql create mode 100644 functions/consumer/jdbc-consumer/src/test/resources/schema.sql create mode 100644 functions/consumer/log-consumer/.gitignore create mode 100644 functions/consumer/log-consumer/pom.xml create mode 100644 functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerConfiguration.java create mode 100644 functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerProperties.java create mode 100644 functions/consumer/log-consumer/src/test/java/org/springframework/cloud/fn/consumer/log/LogConsumerApplicationTests.java create mode 100644 functions/consumer/mongodb-consumer/.gitignore create mode 100644 functions/consumer/mongodb-consumer/pom.xml create mode 100644 functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerConfiguration.java create mode 100644 functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerProperties.java create mode 100644 functions/consumer/mongodb-consumer/src/test/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerApplicationTests.java create mode 100644 functions/consumer/rabbit-consumer/pom.xml create mode 100644 functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerConfiguration.java create mode 100644 functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerProperties.java create mode 100644 functions/function/filter-function/.gitignore create mode 100644 functions/function/filter-function/pom.xml create mode 100644 functions/function/filter-function/src/main/java/org/springframework/cloud/fn/filter/FilterFunctionConfiguration.java create mode 100644 functions/function/filter-function/src/main/resources/application.properties create mode 100644 functions/function/filter-function/src/test/java/org/springframework/cloud/fn/filter/FilterFunctionApplicationTests.java create mode 100644 functions/function/payload-converter-function/pom.xml create mode 100644 functions/function/payload-converter-function/src/main/java/functions/ByteArrayTextToString.java create mode 100644 functions/function/payload-converter-function/src/test/java/functions/ByteArrayTextToStringTests.java create mode 100644 functions/function/spel-function/.gitignore create mode 100644 functions/function/spel-function/pom.xml create mode 100644 functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionConfiguration.java create mode 100644 functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionProperties.java create mode 100644 functions/function/spel-function/src/test/java/org/springframework/cloud/fn/spel/SpelFunctionApplicationTests.java create mode 100644 functions/function/splitter-function/.gitignore create mode 100644 functions/function/splitter-function/pom.xml create mode 100644 functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionConfiguration.java create mode 100644 functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionProperties.java create mode 100644 functions/function/splitter-function/src/test/java/org/springframework/cloud/fn/splitter/SplitterFunctionApplicationTests.java create mode 100644 functions/pom.xml create mode 100644 functions/spring-functions-parent/pom.xml create mode 100644 functions/supplier/file-supplier/pom.xml create mode 100644 functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileConsumerProperties.java create mode 100644 functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileReadingMode.java create mode 100644 functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierConfiguration.java create mode 100644 functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierProperties.java create mode 100644 functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileUtils.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/AbstractFileSupplierTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/DefaultFileSupplierTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FileModeRefTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithPatternTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithRegexTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesAndMarkersAsJsonPayloadTests.java create mode 100644 functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesPayloadTests.java create mode 100644 functions/supplier/http-supplier/.gitignore create mode 100644 functions/supplier/http-supplier/pom.xml create mode 100644 functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSourceProperties.java create mode 100644 functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSupplierConfiguration.java create mode 100644 functions/supplier/http-supplier/src/main/resources/application.properties create mode 100644 functions/supplier/http-supplier/src/test/java/org/springframework/cloud/fn/supplier/http/HttpSupplierApplicationTests.java create mode 100644 functions/supplier/http-supplier/src/test/resources/test.jks create mode 100644 functions/supplier/jdbc-supplier/pom.xml create mode 100644 functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierConfiguration.java create mode 100644 functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierProperties.java create mode 100644 functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/DefaultJdbcSupplierTests.java create mode 100644 functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/NonSplitJdbcSupplierTests.java create mode 100644 functions/supplier/jdbc-supplier/src/test/resources/schema.sql create mode 100644 functions/supplier/mongodb-supplier/.gitignore create mode 100644 functions/supplier/mongodb-supplier/pom.xml create mode 100644 functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierConfiguration.java create mode 100644 functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierProperties.java create mode 100644 functions/supplier/mongodb-supplier/src/main/resources/application.properties create mode 100644 functions/supplier/mongodb-supplier/src/test/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierApplicationTests.java create mode 100644 functions/supplier/time-supplier/pom.xml create mode 100644 functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/DateFormat.java create mode 100644 functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeProperties.java create mode 100644 functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeSupplierConfiguration.java create mode 100644 functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/SimpleTimeSupplierTests.java create mode 100644 functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/TimeSupplierApplicationTests.java create mode 100644 functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/VariationToSimpleTests.java create mode 100755 mvnw create mode 100644 mvnw.cmd create mode 100644 pom.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f5712a10 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +apps/ +/application.yml +/application.properties +asciidoctor.css +*~ +.#* +*# +target/ +build/ +bin/ +_site/ +.classpath +.project +.settings +.springBeans +.DS_Store +*.sw* +*.iml +*.ipr +*.iws +.idea/ +.factorypath +spring-xd-samples/*/xd +dump.rdb +coverage-error.log +.apt_generated +aws.credentials.properties diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 00000000..0e7dabef --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1 @@ +-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom \ No newline at end of file diff --git a/.mvn/maven.config b/.mvn/maven.config new file mode 100644 index 00000000..3b8cf46e --- /dev/null +++ b/.mvn/maven.config @@ -0,0 +1 @@ +-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local -P spring diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 00000000..c32394f1 --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present 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 + * + * http://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. + */ +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.5"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..0d5e649888a4843c1520054d9672f80c62ebbb48 GIT binary patch literal 50710 zcmbTd1F&Yzk}llaw%yydZQHhOtG8|2wr$%sdfWEC{mnUpfBrjP%(-twMXZRmGOM!c zd9yOJo|2OU0!ID;4i5g~#}E8J?LU7Ie;%cUmH4T}WkhI!e#l9J{q@Zcz<+)r_dg0E z|5rh2ei?BQVMQexX_2HDe#ihic;RQiO?))5*`S|S7OJR$0!15$@o}&gh{KEX8>-aS zebwz)UwGRGE9?4DhKZ)R2wjvy<%rYe_z!fyA~>e=tmvNPLiuHP53`)W`FLgV1o9b@ z?3)Q4hagTgvBzZDa`v_DRkmwm>bk&&5@m;ZKwovq%oDWOE5u zleR0Z)LP%g z*ydlFD2)HVxVbHjlfI?CgZaOti1hCi{oA;xT^;o8?2H}$CAG}|d$o49)--kwwtsqX zGBi1>nE^FB$)DBl&kl0=BkJj!u8pT3X-SM$t*%!O7Tx#?VUN(J@J7 z%mqmlxhp6bH9rj)^iYq`pf?`O*$x~aBDK%&CjpjW0Dmepb(vLDTzk@0d>tccth>%{ zqcr7aeZu!Zr23hdL)!RGizX}aWJj6ClX4Gb=bet4tBUy?-|r{nUh$7yJ*eiA?Z;B2`eF1LaPBSu_fx@B5isJF5&|yU7hLsa5}05d3gQRmO4{!66oMh zigvqS{W+|Y0wOi($g$qiEf^jL)}>W~AR*|m?Ia0Mm&;BjorRn-!}CxKVO!7^_eSU; za}~KI`cHaF*!+>B5a-KI>36u#or|tTiuzm;hLCR>bMq9@2Z1fr4d$A`%|rCLKl^5z z`Z~yYPy)~i?x3_LE7|;0GLF#mVOpQ8X>1gNNLX!4rWD(!q!EVsGZPum^~IQ?OAy9U z#lqI;WcC{U(KHra8q6HKa`%NZ^;gqs))9Mb3hgxa%QY1dO_YQok3%a5hFXmwyQwt5 zokv+V7DJgXNlo1Jv9u21JB$WF~oaC)aF8zY-VK6{ynvH6F zk|{{&#%crN>5Vm&6byp)q(XYXIF)9Q`;lMGWJIP3e)3zmi0gVmI|;n*$`v-Jtj5!h>;@Y&fY9%VqR zdvyz`W~hk%)WdNHVGkD6tdf`iv8B&HpjCgRcx=@$^CrBuzraY$k`dZ&LmR8t+(FSQ zL7=y~l+GL+%Xzvj66Xb`Ey}35$xDv5O2@5ywUr2_>Jz*srt`dPuFp2>5mTdt>H7NR zvg!zAScv9uGBZa^gCeh77YJ4_0xc@0!jSG}P@Pn!)t0|+UFI7!?W90^55Ha1de+3Y zNz}7<*xPlOFN5;J!=rS=Zwb(PT)j`|B_(F8EmsvkQZ1wGuG&Xu)OZmTR0Y99D$5#tf%OElqb{J^!W*E8vy2$QkhN-E(3>~vNdny^ z&_#^RRL>0Mog`;hZ~2=uUwy|8W@gdO$pq$;8M?Z?{ z(!g)#LR-;l-oCvHxx--!6D~z2_%z~DPIcWwnzgGa&;ouDP~Bx#u>)3HUKjSUTv2kS z*jfLRyc-Yu(ClrUvuAvfnmu_BkvFbTk8>#tYv@*?nq_h~A!A!yM;do9 zC^E#;pW}3;$ApFCRQo(dyU5c>3TcRmq%|Z|8p^lxDmk7JN6llr_&U?Rg|@NljYOR2 zb=vg=oS1GN>(^NCAaiE9rbhk__1Nwu!OuPddM7KQJj)Bezh85DvUl}a?!*ZJEMKfp zbU*8SY`{iQ=%fl0#Af$k6~2*0v^?llf1Emdn5Q5YG+%7`*5uyO_^txn^`x2l^J_As2-4_Tm|5b}0q$5okF$ zHaO03%@~_Z=jpV!WTbL$}e;NgXz=Uw!ogI}+S@aBP**2Wo^yN#ZG z4G$m^yaM9g?M5E1ft8jOLuzc3Psca*;7`;gnI0YzS0%f4{|VGEzKceaptfluwyY#7 z^=q#@gi@?cOm99Qz!EylA4G~7kbF7hlRIzcrb~{_2(x@@z`7d96Bi_**(vyr_~9Of z!n>Gqk|ZWyu!xhi9f53&PM3`3tNF}pHaq}(;KEn#pmm6DZBu8*{kyrTxk<;mx~(;; z1NMrp@Zd0ZqI!oTJo3b|HROE}UNcQash!p5eLjTcz)>kP=Bp@z)5rLGnaF5{~@z;MFCP9s_dDdADddy z{|Zd9ou-;laEHid_b7A^ zBw1J-^uo$K|@udwk;w* za_|mNqh!k}0fkzR#`|v?iVB@HJt^?0Fo^YGim=lqWD&K7$=J2L(HMp@*5YwV1U)1Aj@><#btD=m0Ga1X))fcKJ=s(v}E7fc1fa_$nGP%d9Opjh3) zRid3zuc5^mNmnnsg4G>m;Sfh@hH$ZT$p%QswzSRa2bh;(7lOaWT>Jv@Ki>_Ep?jx7 z&hwEG^YF=vEgvUwjT_VgWlSZeS{CTjedc)A>N0*uAU(9G@5|><%)^NxRcyx@4!m3s z%1?oiq^@>V!+tKZka-ax2e-`Deeb9_AaTF~z;arjq>Im$ zMc`JAOruhFrFTj6I-Al5$^z4tyu_l2Qk04>>;9#)B#fF})h0_OHP)%xv~m#T+6VG< zP6O@;?5g^t6wm{HX+54ZPoe%(;HU^*OPSEojLYRFRE~=mPXE!0pb|Zs=psR=-v`L# zB2`|mvJBoNTvW`LJ}a;cHP~jC@klxY0|ec3Y!w-`mQ6>CzF}GQCHmrB>k3`fk=3Ck z+WwgG3U_aN&(|RY$ss6CYZ(%4!~tuVWSHu?q=6{-Izay&o_Mvxm=!*?C-NQZFC8=n{?qfRf$3o_VSHs%zfSMdMQ5_f3xt6~+{RX=$H8at z9Si~lTmp}|lmm;++^zA%Iv+XJAHcTf1_jRxfEgz$XozU8$D?08YntWwMY-9iyk@u#wR?JxR2bky5j9 z3Sl-dQQU?#rO0xa)Sp<|MJnx@%w#GcXXM7*Vs=VPdSFt5$aJux89D%D?lA0_j&L42 zcyGz!opsIob%M&~(~&UkX0ndOq^MqjxXw8MIN}U@vAKq_fp@*Vp$uVFiNfahq2MzA zU`4uR8m$S~m+h{-pKVzp%Gs(Wz+%>h;R9Sg-MrB38r?e_Tx6PD%>)bi(#$!a@*_#j zCKr_wm;wtEtOCDwzW25?t{~PANe*e(EXogwcq&Ysl-nT2MBB3E96NP8`Ej_iQFT@X zG22M5ibzYHNJ~tR(et8lDFp|we$&U1tZ33H-o#?o$(o&(>aCNWlMw#Y{b}!fw$6_p z{k}778KP{PZ`c87HBXWDJK)sKXU5xF2))N*t_1C^~Q5(q1W#@r0y#QUke zY9@kew61E>;G2Ds$-gvm=pMuXW~T4Tv@ZhzZkH)DZ_mlk!&rL#E+5JaIx|cf&@b{g ziV)ouh%FU9i6D+C!e&>1x91bwV26SChDV1};|%rXHfqfEpP9?svl6*wM_)kY1DlTX zVN?D2ru8SysDeW~0<@G�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 JS$+5G{{hr`vzY(@ literal 0 HcmV?d00001 diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..7d59a01f --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar diff --git a/README.adoc b/README.adoc new file mode 100644 index 00000000..a202629f --- /dev/null +++ b/README.adoc @@ -0,0 +1 @@ +== Stream applications \ No newline at end of file diff --git a/applications/apps-core/.settings.xml b/applications/apps-core/.settings.xml new file mode 100644 index 00000000..d8e1afac --- /dev/null +++ b/applications/apps-core/.settings.xml @@ -0,0 +1,81 @@ + + + + + repo.spring.io + ${env.CI_DEPLOY_USERNAME} + ${env.CI_DEPLOY_PASSWORD} + + + + + + spring + + true + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + + gemstone-release + GemStone Maven Release Repository + http://dist.gemstone.com/maven/release + + true + always + + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + + + diff --git a/applications/apps-core/CODE_OF_CONDUCT.adoc b/applications/apps-core/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..17783c7c --- /dev/null +++ b/applications/apps-core/CODE_OF_CONDUCT.adoc @@ -0,0 +1,44 @@ += Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open +and welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or +patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors +that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will +be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. Maintainers are obligated to maintain confidentiality +with regard to the reporter of an incident. + +This Code of Conduct is adapted from the +https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at +https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] diff --git a/applications/apps-core/LICENSE b/applications/apps-core/LICENSE new file mode 100644 index 00000000..9b259bdf --- /dev/null +++ b/applications/apps-core/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/applications/apps-core/README.adoc b/applications/apps-core/README.adoc new file mode 100644 index 00000000..1f493a1c --- /dev/null +++ b/applications/apps-core/README.adoc @@ -0,0 +1,2 @@ +#Core components shared by other projects in the app starters organization +This module consists of core dependencies and other common artifacts. diff --git a/applications/apps-core/common/pom.xml b/applications/apps-core/common/pom.xml new file mode 100644 index 00000000..213da9a8 --- /dev/null +++ b/applications/apps-core/common/pom.xml @@ -0,0 +1,25 @@ + + + + stream-apps-parent + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-common + pom + stream-apps-common + + + stream-apps-file-common + stream-apps-ftp-common + stream-apps-test-support + stream-apps-postprocessor-common + stream-apps-micrometer-common + stream-apps-metadata-store-common + stream-apps-task-launch-request-common + stream-apps-security-common + + + diff --git a/applications/apps-core/common/stream-apps-file-common/pom.xml b/applications/apps-core/common/stream-apps-file-common/pom.xml new file mode 100644 index 00000000..ac0e8a43 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/pom.xml @@ -0,0 +1,21 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-file-common + stream-apps-file-common + + + + org.springframework.integration + spring-integration-file + true + + + + diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileConsumerProperties.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileConsumerProperties.java new file mode 100644 index 00000000..33ff4b0c --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileConsumerProperties.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2016 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.file; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * @author David Turanski + * @author Artem Bilan + */ +@ConfigurationProperties("file.consumer") +@Validated +public class FileConsumerProperties { + + /** + * The FileReadingMode to use for file reading sources. + * Values are 'ref' - The File object, + * 'lines' - a message per line, or + * 'contents' - the contents as bytes. + */ + private FileReadingMode mode = FileReadingMode.contents; + + /** + * Set to true to emit start of file/end of file marker messages before/after the data. + * Only valid with FileReadingMode 'lines'. + */ + private Boolean withMarkers = null; + + /** + * When 'fileMarkers == true', specify if they should be produced + * as FileSplitter.FileMarker objects or JSON. + */ + private boolean markersJson = true; + + @NotNull + public FileReadingMode getMode() { + return this.mode; + } + + public void setMode(FileReadingMode mode) { + this.mode = mode; + } + + public Boolean getWithMarkers() { + return this.withMarkers; + } + + public void setWithMarkers(Boolean withMarkers) { + this.withMarkers = withMarkers; + } + + public boolean getMarkersJson() { + return this.markersJson; + } + + public void setMarkersJson(boolean markersJson) { + this.markersJson = markersJson; + } + + @AssertTrue(message = "withMarkers can only be supplied when FileReadingMode is 'lines'") + public boolean isWithMarkersValid() { + return this.withMarkers == null || FileReadingMode.lines == this.mode; + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileReadingMode.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileReadingMode.java new file mode 100644 index 00000000..593fc757 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileReadingMode.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 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.file; + + +/** + * Defines the supported modes of reading and processing files for the + * {@code File}, {@code FTP} and {@code SFTP} sources. + * + * @author Gunnar Hillert + * @author David Turanski + */ +public enum FileReadingMode { + ref, + lines, + contents; +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileUtils.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileUtils.java new file mode 100644 index 00000000..4537ee66 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/FileUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright 2015-2018 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.file; + +import java.util.Collections; + +import org.springframework.integration.dsl.IntegrationFlowBuilder; +import org.springframework.integration.file.splitter.FileSplitter; +import org.springframework.integration.file.transformer.FileToByteArrayTransformer; +import org.springframework.integration.transformer.StreamTransformer; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.MimeTypeUtils; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Christian Tzolov + * + */ +public class FileUtils { + + /** + * Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on + * {@link FileConsumerProperties}. + * @param flowBuilder the flow builder. + * @param fileConsumerProperties the properties. + * @return the updated flow builder. + */ + public static IntegrationFlowBuilder enhanceFlowForReadingMode(IntegrationFlowBuilder flowBuilder, + FileConsumerProperties fileConsumerProperties) { + switch (fileConsumerProperties.getMode()) { + case contents: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)) + .transform(new FileToByteArrayTransformer()); + break; + case lines: + Boolean withMarkers = fileConsumerProperties.getWithMarkers(); + if (withMarkers == null) { + withMarkers = false; + } + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.TEXT_PLAIN_VALUE)) + .split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson())); + break; + case ref: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_JSON_VALUE)); + break; + default: + throw new IllegalArgumentException(fileConsumerProperties.getMode().name() + + " is not a supported file reading mode."); + } + return flowBuilder; + } + + /** + * Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on + * {@link FileConsumerProperties}; used for streaming sources. + * @param flowBuilder the flow builder. + * @param fileConsumerProperties the properties. + * @return the updated flow builder. + */ + public static IntegrationFlowBuilder enhanceStreamFlowForReadingMode(IntegrationFlowBuilder flowBuilder, + FileConsumerProperties fileConsumerProperties) { + switch (fileConsumerProperties.getMode()) { + case contents: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)) + .transform(new StreamTransformer()); + break; + case lines: + Boolean withMarkers = fileConsumerProperties.getWithMarkers(); + if (withMarkers == null) { + withMarkers = false; + } + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.TEXT_PLAIN_VALUE)) + .split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson())); + break; + case ref: + default: + throw new IllegalArgumentException(fileConsumerProperties.getMode().name() + + " is not a supported file reading mode when streaming."); + } + return flowBuilder; + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileProperties.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileProperties.java new file mode 100644 index 00000000..cb24e8e9 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileProperties.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2017 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.file.remote; + +import org.hibernate.validator.constraints.NotBlank; + +/** + * @deprecated - properties are flattened. + * + * @author Gary Russell + * + */ +@Deprecated +public abstract class AbstractRemoteFileProperties { + + /** + * The remote FTP directory. + */ + private String remoteDir = "/"; + + /** + * The suffix to use while the transfer is in progress. + */ + private String tmpFileSuffix = ".tmp"; + + /** + * The remote file separator. + */ + private String remoteFileSeparator = "/"; + + @NotBlank + public String getRemoteDir() { + return remoteDir; + } + + public final void setRemoteDir(String remoteDir) { + this.remoteDir = remoteDir; + } + + @NotBlank + public String getTmpFileSuffix() { + return tmpFileSuffix; + } + + public void setTmpFileSuffix(String tmpFileSuffix) { + this.tmpFileSuffix = tmpFileSuffix; + } + + @NotBlank + public String getRemoteFileSeparator() { + return remoteFileSeparator; + } + + public void setRemoteFileSeparator(String remoteFileSeparator) { + this.remoteFileSeparator = remoteFileSeparator; + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSinkProperties.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSinkProperties.java new file mode 100644 index 00000000..b3db48dc --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSinkProperties.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2016 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.file.remote; + +import javax.validation.constraints.NotNull; + +import org.hibernate.validator.constraints.NotBlank; + +import org.springframework.expression.Expression; +import org.springframework.integration.file.support.FileExistsMode; + +/** + * @deprecated - properties are flattened. + * + * @author Gary Russell + * + */ +@Deprecated +public abstract class AbstractRemoteFileSinkProperties extends AbstractRemoteFileProperties { + + /** + * A temporary directory where the file will be written if {@link #isUseTemporaryFilename()} + * is true. + */ + private String temporaryRemoteDir = "/"; + + /** + * Whether or not to create the remote directory. + */ + private boolean autoCreateDir = true; + + /** + * Action to take if the remote file already exists. + */ + private FileExistsMode mode = FileExistsMode.REPLACE; + + /** + * Whether or not to write to a temporary file and rename. + */ + private boolean useTemporaryFilename = true; + + /** + * A SpEL expression to generate the remote file name. + */ + private Expression filenameExpression; + + @NotBlank + public String getTemporaryRemoteDir() { + return this.temporaryRemoteDir; + } + + public void setTemporaryRemoteDir(String temporaryRemoteDir) { + this.temporaryRemoteDir = temporaryRemoteDir; + } + + public boolean isAutoCreateDir() { + return this.autoCreateDir; + } + + public void setAutoCreateDir(boolean autoCreateDir) { + this.autoCreateDir = autoCreateDir; + } + + @NotNull + public FileExistsMode getMode() { + return this.mode; + } + + public void setMode(FileExistsMode mode) { + this.mode = mode; + } + + public boolean isUseTemporaryFilename() { + return this.useTemporaryFilename; + } + + public void setUseTemporaryFilename(boolean useTemporaryFilename) { + this.useTemporaryFilename = useTemporaryFilename; + } + + public Expression getFilenameExpression() { + return this.filenameExpression; + } + + public void setFilenameExpression(Expression filenameExpression) { + this.filenameExpression = filenameExpression; + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSourceProperties.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSourceProperties.java new file mode 100644 index 00000000..73173f75 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteFileSourceProperties.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2017 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.file.remote; + +import java.io.File; +import java.util.regex.Pattern; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +/** + * Common properties for remote file sources (e.g. (S)FTP). + * + * @deprecated - properties are flattened. + * + * @author David Turanski + * @author Gary Russell + * + */ +@Deprecated +public abstract class AbstractRemoteFileSourceProperties extends AbstractRemoteFileProperties { + + /** + * Set to true to delete remote files after successful transfer. + */ + private boolean deleteRemoteFiles = false; + + /** + * The local directory to use for file transfers. + */ + private File localDir = new File(System.getProperty("java.io.tmpdir") + "/xd/ftp"); + + /** + * Set to true to create the local directory if it does not exist. + */ + private boolean autoCreateLocalDir = true; + + /** + * A filter pattern to match the names of files to transfer. + */ + private String filenamePattern; + + /** + * A filter regex pattern to match the names of files to transfer. + */ + private Pattern filenameRegex; + + /** + * Set to true to preserve the original timestamp. + */ + private boolean preserveTimestamp = true; + + public boolean isAutoCreateLocalDir() { + return autoCreateLocalDir; + } + + public void setAutoCreateLocalDir(boolean autoCreateLocalDir) { + this.autoCreateLocalDir = autoCreateLocalDir; + } + + public boolean isDeleteRemoteFiles() { + return deleteRemoteFiles; + } + + public void setDeleteRemoteFiles(boolean deleteRemoteFiles) { + this.deleteRemoteFiles = deleteRemoteFiles; + } + + @NotNull + public File getLocalDir() { + return localDir; + } + + public final void setLocalDir(File localDir) { + this.localDir = localDir; + } + + public String getFilenamePattern() { + return filenamePattern; + } + + public void setFilenamePattern(String filenamePattern) { + this.filenamePattern = filenamePattern; + } + + public Pattern getFilenameRegex() { + return filenameRegex; + } + + public void setFilenameRegex(Pattern filenameRegex) { + this.filenameRegex = filenameRegex; + } + + public boolean isPreserveTimestamp() { + return preserveTimestamp; + } + + public void setPreserveTimestamp(boolean preserveTimestamp) { + this.preserveTimestamp = preserveTimestamp; + } + + @AssertTrue(message = "filenamePattern and filenameRegex are mutually exclusive") + public boolean isExclusivePatterns() { + return !(this.filenamePattern != null && this.filenameRegex != null); + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteServerProperties.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteServerProperties.java new file mode 100644 index 00000000..a5c3ee35 --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/AbstractRemoteServerProperties.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2016 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.file.remote; + +import org.hibernate.validator.constraints.NotBlank; + +/** + * Common properties for remote servers (e.g. (S)FTP). + * + * @deprecated - properties are flattened. + * + * @author David Turanski + * @author Gary Russell + * + */ +@Deprecated +public abstract class AbstractRemoteServerProperties { + + /** + * The host name of the server. + */ + private String host = "localhost"; + + /** + * The username to use to connect to the server. + */ + + private String username; + /** + * The password to use to connect to the server. + */ + private String password; + + /** + * Cache sessions + */ + private Boolean cacheSessions; + + @NotBlank + public String getHost() { + return this.host; + } + + public void setHost(String host) { + this.host = host; + } + + @NotBlank + public String getUsername() { + return this.username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return this.password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getCacheSessions() { + return this.cacheSessions; + } + + public void setCacheSessions(Boolean cacheSessions) { + this.cacheSessions = cacheSessions; + } + +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/FilePathUtils.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/FilePathUtils.java new file mode 100644 index 00000000..f2d0306b --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/FilePathUtils.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 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.file.remote; + +import java.nio.file.Paths; + +import org.springframework.integration.file.FileHeaders; +import org.springframework.lang.Nullable; +import org.springframework.messaging.Message; +import org.springframework.util.Assert; + +/** + * @author David Turanski + **/ +public abstract class FilePathUtils { + /** + * Returns a remote file path for a message with a file name as payload and {@link FileHeaders#REMOTE_DIRECTORY} + * included as a message header. + * + * @param message the message containing the header. + * @return the file path. + */ + @Nullable + public static String getRemoteFilePath(Message message) { + if (message.getHeaders().containsKey(FileHeaders.REMOTE_DIRECTORY)) { + String filename = (String) message.getPayload(); + String remoteDirectory = (String) message.getHeaders().get(FileHeaders.REMOTE_DIRECTORY); + return getPath(remoteDirectory, filename); + } + return null; + } + + public static String getLocalFilePath(String localDirectory, String filename) { + if (localDirectory != null) { + return getPath(localDirectory, filename); + } + return filename; + } + + private static String getPath(String dirName, String fileName) { + return Paths.get(dirName, fileName).toString(); + } +} diff --git a/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/RemoteFileDeletingTransactionSynchronizationProcessor.java b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/RemoteFileDeletingTransactionSynchronizationProcessor.java new file mode 100644 index 00000000..7c4518ef --- /dev/null +++ b/applications/apps-core/common/stream-apps-file-common/src/main/java/org/springframework/cloud/stream/app/file/remote/RemoteFileDeletingTransactionSynchronizationProcessor.java @@ -0,0 +1,63 @@ +/* + * Copyright 2017 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.file.remote; + +import org.springframework.integration.file.FileHeaders; +import org.springframework.integration.file.remote.RemoteFileTemplate; +import org.springframework.integration.transaction.IntegrationResourceHolder; +import org.springframework.integration.transaction.TransactionSynchronizationProcessor; + +/** + * A {@link TransactionSynchronizationProcessor} that deletes a remote file on + * success. + * + * @author Gary Russell + * + */ +public class RemoteFileDeletingTransactionSynchronizationProcessor implements TransactionSynchronizationProcessor { + + private final RemoteFileTemplate template; + + private final String remoteFileSeparator; + + /** + * Construct an instance with the provided template and separator. + * @param template the template. + * @param remoteFileSeparator the separator. + */ + public RemoteFileDeletingTransactionSynchronizationProcessor(RemoteFileTemplate template, + String remoteFileSeparator) { + this.template = template; + this.remoteFileSeparator = remoteFileSeparator; + } + + @Override + public void processBeforeCommit(IntegrationResourceHolder holder) { + } + + @Override + public void processAfterRollback(IntegrationResourceHolder holder) { + } + + @Override + public void processAfterCommit(IntegrationResourceHolder holder) { + String remoteDir = (String) holder.getMessage().getHeaders().get(FileHeaders.REMOTE_DIRECTORY); + String remoteFile = (String) holder.getMessage().getHeaders().get(FileHeaders.REMOTE_FILE); + this.template.remove(remoteDir + this.remoteFileSeparator + remoteFile); + } + +} diff --git a/applications/apps-core/common/stream-apps-ftp-common/pom.xml b/applications/apps-core/common/stream-apps-ftp-common/pom.xml new file mode 100644 index 00000000..d1c410f5 --- /dev/null +++ b/applications/apps-core/common/stream-apps-ftp-common/pom.xml @@ -0,0 +1,26 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-ftp-common + stream-apps-ftp-common + + + + org.springframework.integration + spring-integration-ftp + true + + + org.springframework.cloud.stream.app + stream-apps-file-common + ${project.version} + + + + diff --git a/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryConfiguration.java b/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryConfiguration.java new file mode 100644 index 00000000..b28b9c2b --- /dev/null +++ b/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2015-2016 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.ftp; + +import org.apache.commons.net.ftp.FTPFile; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.file.remote.session.CachingSessionFactory; +import org.springframework.integration.file.remote.session.SessionFactory; +import org.springframework.integration.ftp.session.DefaultFtpSessionFactory; + +/** + * FTP Session factory configuration. + * + * @author David Turanski + * @author Gary Russell + */ +@Configuration +@EnableConfigurationProperties(FtpSessionFactoryProperties.class) +public class FtpSessionFactoryConfiguration { + + @Bean + @ConditionalOnMissingBean + public SessionFactory ftpSessionFactory(FtpSessionFactoryProperties properties) { + DefaultFtpSessionFactory ftpSessionFactory = new DefaultFtpSessionFactory(); + ftpSessionFactory.setHost(properties.getHost()); + ftpSessionFactory.setPort(properties.getPort()); + ftpSessionFactory.setUsername(properties.getUsername()); + ftpSessionFactory.setPassword(properties.getPassword()); + ftpSessionFactory.setClientMode(properties.getClientMode().getMode()); + if (properties.getCacheSessions() != null) { + CachingSessionFactory csf = new CachingSessionFactory<>(ftpSessionFactory); + return csf; + } + else { + return ftpSessionFactory; + } + } + +} diff --git a/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryProperties.java b/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryProperties.java new file mode 100644 index 00000000..cf4ea143 --- /dev/null +++ b/applications/apps-core/common/stream-apps-ftp-common/src/main/java/org/springframework/cloud/stream/app/ftp/FtpSessionFactoryProperties.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-2016 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.ftp; + +import javax.validation.constraints.NotNull; + +import org.apache.commons.net.ftp.FTPClient; +import org.hibernate.validator.constraints.Range; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.stream.app.file.remote.AbstractRemoteServerProperties; +import org.springframework.validation.annotation.Validated; + +/** + * FTP {@code SessionFactory} properties. + * + * @author David Turanski + * @author Gary Russell + */ +@ConfigurationProperties("ftp.factory") +@Validated +public class FtpSessionFactoryProperties extends AbstractRemoteServerProperties { + + /** + * The port of the server. + */ + private int port = 21; + + /** + * The client mode to use for the FTP session. + */ + private ClientMode clientMode = ClientMode.PASSIVE; + + @Range(min = 0, max = 65535) + public int getPort() { + return this.port; + } + + public void setPort(int port) { + this.port = port; + } + + @NotNull + public ClientMode getClientMode() { + return this.clientMode; + } + + public void setClientMode(ClientMode clientMode) { + this.clientMode = clientMode; + } + + public static enum ClientMode { + + ACTIVE(FTPClient.ACTIVE_LOCAL_DATA_CONNECTION_MODE), + PASSIVE(FTPClient.PASSIVE_LOCAL_DATA_CONNECTION_MODE); + + private final int mode; + + private ClientMode(int mode) { + this.mode = mode; + } + + public int getMode() { + return mode; + } + + } + +} diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/README.adoc b/applications/apps-core/common/stream-apps-metadata-store-common/README.adoc new file mode 100644 index 00000000..d1a32b96 --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/README.adoc @@ -0,0 +1,181 @@ +=== `MetadataStore` Common Module + +This artifact contains a Spring Boot auto-configuration for the `MetadataStore`which can be used in various Spring Integration scenarios, like file polling, idempotent receiver, offset management etc. +See Spring Integration "`https://docs.spring.io/spring-integration/docs/5.0.6.RELEASE/reference/html/system-management-chapter.html#metadata-store[Reference Manual]`" for more information. + +In addition to the standard Spring Boot configuration properties this module exposes a `MetadataStoreProperties` with the `metadata.store` prefix. + +To auto-configure particular `MetadataStore` you just need to bring respective dependencies into the target app starter: + +==== Redis + +The `RedisMetadataStore` requires regular Spring Boot auto-configuration for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-redis[Spring Data Redis] and minimal set of dependencies is like this: + +[source,xml] +---- + + org.springframework.integration + spring-integration-redis + + + org.springframework.boot + spring-boot-starter-data-redis + +---- + +Additional configuration property for `RedisMetadataStore` is: + +$$metadata.store.redis.key$$:: $$Redis key for metadata.$$ *($$String$$, default: `$$MetaData$$`)* + +==== MongoDb + +The `MongoDbMetadataStore` requires regular Spring Boot auto-configuration for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-mongodb[Spring Data MongoDB] and minimal set of dependencies is like this: + +[source,xml] +---- + + org.springframework.integration + spring-integration-mongodb + + + org.springframework.boot + spring-boot-starter-data-mongodb + +---- + +Additional configuration property for `MongoDbMetadataStore` is: + +$$metadata.store.mongo-db.collection$$:: $$MongoDB collection name for metadata.$$ *($$String$$, default: `$$metadataStore$$`)* + +==== Pivotal Gemfire / Apache Geode + +The `GemfireMetadataStore` requires these dependencies for auto-configuration: + +[source,xml] +---- + + org.springframework.integration + spring-integration-gemfire + +---- + +or when your environment is based on the Open Source https://geode.apache.org/[Apache Geode]: + +[source,xml] +---- + + org.springframework.integration + spring-integration-gemfire + + + org.springframework.data + spring-data-gemfire + + + + + org.springframework.data + spring-data-geode + +---- + +You also can consider to use https://github.com/spring-projects/spring-boot-data-geode[Spring Boot Data Geode] instead for automatic dependency management and proper Spring Boot auto-configuration for Pivotal Gemfire/Apache Geode. + +Additional configuration property for `GemfireMetadataStore` is: + +$$metadata.store.gemfire.region$$:: $$Gemfire region name for metadata.$$ *($$String$$, default: `$$MetaData$$`)* + +In addition, for the `GemfireMetadataStore`, a `MetadataStoreListener` bean can be configured in the application context to react to the `MetadataStore` events. + +A default auto-configured `ClientRegionFactoryBean`, based on the auto-configured `GemFireCache`, bean can be overridden in the target application. + +==== Hazelcast + +The `HazelcastMetadataStore` requires regular Spring Boot auto-configuration for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-caching-provider-hazelcast[Hazelcast] and minimal set of dependencies is like this: + +[source,xml] +---- + + org.springframework.integration + spring-integration-hazelcast + +---- + +There are no additional configuration properties for the `HazelcastMetadataStore`, however a `MetadataStoreListener` bean can be configured in the application context to react to the `MetadataStore` events. + +==== Zookeeper + +The `ZookeeperMetadataStore` requires this dependency for auto-configuration: + +[source,xml] +---- + + org.springframework.integration + spring-integration-zookeeper + +---- + +The configuration properties for `ZookeeperMetadataStore` are: + +$$metadata.store.zookeeper.connect-string$$:: $$Zookeeper connect string in form HOST:PORT.$$ *($$String$$, default: `$$127.0.0.1:2181$$`)* +$$metadata.store.zookeeper.retry-interval$$:: $$Retry interval for Zookeeper operations in milliseconds.$$ *($$int$$, default: `$$1000$$`)* +$$metadata.store.zookeeper.encoding$$:: $$Encoding to use when storing data in Zookeeper.$$ *($$Charset$$, default: `$$UTF-8$$`)* +$$metadata.store.zookeeper.root$$:: $$Root node - store entries are children of this node.$$ *($$String$$, default: `$$/SpringIntegration-MetadataStore$$`)* + +In addition, for the `ZookeeperMetadataStore`, a `MetadataStoreListener` bean can be configured in the application context to react to the `MetadataStore` events. +Also a `CuratorFramework` bean can be provided to override a default auto-configured one. + +==== AWS DymanoDb + +The `DynamoDbMetadataStore` requires regular Spring Cloud AWS auto-configuration for https://cloud.spring.io/spring-cloud-static/spring-cloud-aws/2.0.0.RELEASE/single/spring-cloud-aws.html#_spring_boot_auto_configuration[Spring Boot] and minimal set of dependencies is like this: + +[source,xml] +---- + + org.springframework.integration + spring-integration-aws + + + com.amazonaws + aws-java-sdk-dynamodb + +---- + +Additional configuration properties for `DynamoDbMetadataStore` are: + +$$metadata.store.dynamo-db.table:: $$Table name for metadata.$$ *($$String$$, default: `$$SpringIntegrationMetadataStore$$`)* +$$metadata.store.dynamo-db.read-capacity:: $$Read capacity on the table.$$ *($$long$$, default: `$$1$$`)* +$$metadata.store.dynamo-db.write-capacity:: $$Write capacity on the table.$$ *($$long$$, default: `$$1$$`)* +$$metadata.store.dynamo-db.create-delay:: $$Delay between create table retries.$$ *($$int$$, default: `$$1$$`)* +$$metadata.store.dynamo-db.create-retries:: $$Retry number for create table request.$$ *($$int$$, default: `$$25$$`)* +$$metadata.store.dynamo-db.time-to-live:: $$TTL for table entries.$$ *($$Integer$$, default: `$$$$`)* + +A default, auto-configured `AmazonDynamoDBAsync` bean can be overridden in the target application. + +==== JDBC + +The `JdbcMetadataStore` requires regular Spring Boot auto-configuration for https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-sql[JDBC DataSource] and minimal set of dependencies is like this: + +[source,xml] +---- + + org.springframework.integration + spring-integration-jdbc + + + org.springframework.boot + spring-boot-starter-jdbc + +---- + +Plus vendor-specific JDBC driver artifact(s). + +Additional configuration properties for `JdbcMetadataStore` are: + +$$metadata.store.jdbc.table-prefix:: $$Prefix for the custom table name.$$ *($$String$$, default: `$$INT_$$`)* +$$metadata.store.jdbc.region:: $$Unique grouping identifier for messages persisted with this store.$$ *($$String$$, default: `$$DEFAULT$$`)* + + + +When no any of those technologies dependencies are preset, an in-memory `SimpleMetadataStore` is auto-configured. +The target application can also provide its own `MetadataStore` bean to override any auto-configuration hooks. diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/pom.xml b/applications/apps-core/common/stream-apps-metadata-store-common/pom.xml new file mode 100644 index 00000000..37733f6a --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/pom.xml @@ -0,0 +1,152 @@ + + + 4.0.0 + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + + stream-apps-metadata-store-common + stream-apps-metadata-store-common + + + 1.11.439 + 2.0.0.RELEASE + 1.0.0.RELEASE + 4.0.1 + + + + + + org.springframework.integration + spring-integration-core + + + + + org.springframework.integration + spring-integration-redis + true + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + org.springframework.integration + spring-integration-mongodb + true + + + org.springframework.boot + spring-boot-starter-data-mongodb + true + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + + + org.springframework.boot + spring-boot-starter-logging + true + + + org.apache.logging.log4j + log4j-to-slf4j + + + + + org.springframework.integration + spring-integration-gemfire + true + + + org.springframework.data + spring-data-gemfire + + + + + + org.springframework.data + spring-data-geode + true + + + + + + org.springframework.integration + spring-integration-jdbc + true + + + org.springframework.boot + spring-boot-starter-jdbc + true + + + org.hsqldb + hsqldb + test + + + + + org.springframework.integration + spring-integration-zookeeper + true + + + org.apache.curator + curator-test + ${curator.version} + test + + + + + org.springframework.integration + spring-integration-hazelcast + ${spring-integration-hazelcast.version} + true + + + + + org.springframework.integration + spring-integration-aws + ${spring-integration-aws.version} + true + + + com.amazonaws + aws-java-sdk-dynamodb + ${aws-java-sdk.version} + true + + + org.springframework.integration + spring-integration-test + test + + + + diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/ClientCacheAutoConfiguration.java b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/ClientCacheAutoConfiguration.java new file mode 100644 index 00000000..72831fc1 --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/ClientCacheAutoConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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.metadata; + +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.client.ClientCache; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.gemfire.client.ClientCacheFactoryBean; +import org.springframework.data.gemfire.config.annotation.ClientCacheApplication; +import org.springframework.data.gemfire.config.annotation.EnablePdx; + +/** + * TODO + * + * This is the copy of {@code org.springframework.boot.data.geode.autoconfigure.ClientCacheAutoConfiguration} + * until {@code geode-spring-boot-starter} is released. + * + * @author John Blum + */ +@Configuration +@ConditionalOnClass({ ClientCacheFactoryBean.class, ClientCache.class }) +@ConditionalOnMissingBean(GemFireCache.class) +@ClientCacheApplication +@EnablePdx +public class ClientCacheAutoConfiguration { + +} diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfiguration.java b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfiguration.java new file mode 100644 index 00000000..8a29af6a --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfiguration.java @@ -0,0 +1,234 @@ +/* + * Copyright 2018 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.metadata; + +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.CuratorFrameworkFactory; +import org.apache.curator.retry.RetryForever; +import org.apache.geode.cache.GemFireCache; +import org.apache.geode.cache.Region; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.aws.core.region.RegionProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.gemfire.client.ClientRegionFactoryBean; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.integration.aws.metadata.DynamoDbMetadataStore; +import org.springframework.integration.gemfire.metadata.GemfireMetadataStore; +import org.springframework.integration.hazelcast.metadata.HazelcastMetadataStore; +import org.springframework.integration.jdbc.metadata.JdbcMetadataStore; +import org.springframework.integration.metadata.ConcurrentMetadataStore; +import org.springframework.integration.metadata.MetadataStoreListener; +import org.springframework.integration.metadata.SimpleMetadataStore; +import org.springframework.integration.mongodb.metadata.MongoDbMetadataStore; +import org.springframework.integration.redis.metadata.RedisMetadataStore; +import org.springframework.integration.zookeeper.metadata.ZookeeperMetadataStore; +import org.springframework.jdbc.core.JdbcTemplate; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsyncClientBuilder; +import com.hazelcast.core.HazelcastInstance; + +/** + * @author Artem Bilan + * + * @since 2.0.2 + */ +@Configuration +@ConditionalOnClass(ConcurrentMetadataStore.class) +@EnableConfigurationProperties(MetadataStoreProperties.class) +public class MetadataStoreAutoConfiguration { + + @ConditionalOnClass(RedisMetadataStore.class) + @ConditionalOnBean(RedisTemplate.class) + static class Redis { + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore redisMetadataStore(RedisTemplate redisTemplate, + MetadataStoreProperties metadataStoreProperties) { + + return new RedisMetadataStore(redisTemplate, metadataStoreProperties.getRedis().getKey()); + } + + } + + @ConditionalOnClass(MongoDbMetadataStore.class) + @ConditionalOnBean(MongoTemplate.class) + static class Mongo { + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore mongoDbMetadataStore(MongoTemplate mongoTemplate, + MetadataStoreProperties metadataStoreProperties) { + + return new MongoDbMetadataStore(mongoTemplate, metadataStoreProperties.getMongoDb().getCollection()); + } + + } + + @ConditionalOnClass(GemfireMetadataStore.class) + @Import(ClientCacheAutoConfiguration.class) + static class Gemfire { + + @Bean + @ConditionalOnMissingBean + public ClientRegionFactoryBean gemfireRegion(GemFireCache cache, + MetadataStoreProperties metadataStoreProperties) { + + ClientRegionFactoryBean clientRegionFactoryBean = new ClientRegionFactoryBean<>(); + clientRegionFactoryBean.setCache(cache); + clientRegionFactoryBean.setName(metadataStoreProperties.getGemfire().getRegion()); + return clientRegionFactoryBean; + } + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore gemfireMetadataStore(Region region, + ObjectProvider metadataStoreListenerObjectProvider) { + + @SuppressWarnings("unchecked") + GemfireMetadataStore gemfireMetadataStore = new GemfireMetadataStore((Region) region); + metadataStoreListenerObjectProvider.ifAvailable(gemfireMetadataStore::addListener); + + return gemfireMetadataStore; + } + + } + + @ConditionalOnClass(HazelcastMetadataStore.class) + static class Hazelcast { + + @Bean + @ConditionalOnMissingBean + public HazelcastInstance hazelcastInstance() { + return com.hazelcast.core.Hazelcast.newHazelcastInstance(); + } + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore hazelcastMetadataStore(HazelcastInstance hazelcastInstance, + ObjectProvider metadataStoreListenerObjectProvider) { + + HazelcastMetadataStore hazelcastMetadataStore = new HazelcastMetadataStore(hazelcastInstance); + metadataStoreListenerObjectProvider.ifAvailable(hazelcastMetadataStore::addListener); + return hazelcastMetadataStore; + } + + } + + @ConditionalOnClass({ ZookeeperMetadataStore.class, CuratorFramework.class }) + static class Zookeeper { + + @Bean(initMethod = "start") + @ConditionalOnMissingBean + public CuratorFramework curatorFramework(MetadataStoreProperties metadataStoreProperties) { + MetadataStoreProperties.Zookeeper zookeeperProperties = metadataStoreProperties.getZookeeper(); + return CuratorFrameworkFactory.newClient(zookeeperProperties.getConnectString(), + new RetryForever(zookeeperProperties.getRetryInterval())); + } + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore zookeeperMetadataStore(CuratorFramework curatorFramework, + MetadataStoreProperties metadataStoreProperties, + ObjectProvider metadataStoreListenerObjectProvider) { + + MetadataStoreProperties.Zookeeper zookeeperProperties = metadataStoreProperties.getZookeeper(); + ZookeeperMetadataStore zookeeperMetadataStore = new ZookeeperMetadataStore(curatorFramework); + zookeeperMetadataStore.setEncoding(zookeeperProperties.getEncoding().name()); + zookeeperMetadataStore.setRoot(zookeeperProperties.getRoot()); + metadataStoreListenerObjectProvider.ifAvailable(zookeeperMetadataStore::addListener); + return zookeeperMetadataStore; + } + + } + + @ConditionalOnClass(DynamoDbMetadataStore.class) + @ConditionalOnBean({ AWSCredentialsProvider.class, RegionProvider.class }) + static class DynamoDb { + + @Bean + @ConditionalOnMissingBean + public AmazonDynamoDBAsync dynamoDB(AWSCredentialsProvider awsCredentialsProvider, + RegionProvider regionProvider) { + + return AmazonDynamoDBAsyncClientBuilder.standard() + .withCredentials(awsCredentialsProvider) + .withRegion( + regionProvider.getRegion() + .getName()) + .build(); + } + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore dynamoDbMetadataStore(AmazonDynamoDBAsync dynamoDB, + MetadataStoreProperties metadataStoreProperties) { + + MetadataStoreProperties.DynamoDb dynamoDbProperties = metadataStoreProperties.getDynamoDb(); + + DynamoDbMetadataStore dynamoDbMetadataStore = + new DynamoDbMetadataStore(dynamoDB, dynamoDbProperties.getTable()); + + dynamoDbMetadataStore.setReadCapacity(dynamoDbProperties.getReadCapacity()); + dynamoDbMetadataStore.setWriteCapacity(dynamoDbProperties.getWriteCapacity()); + dynamoDbMetadataStore.setCreateTableDelay(dynamoDbProperties.getCreateDelay()); + dynamoDbMetadataStore.setCreateTableRetries(dynamoDbProperties.getCreateRetries()); + if (dynamoDbProperties.getTimeToLive() != null) { + dynamoDbMetadataStore.setTimeToLive(dynamoDbProperties.getTimeToLive()); + } + + return dynamoDbMetadataStore; + } + + } + + @ConditionalOnClass(JdbcMetadataStore.class) + @ConditionalOnBean(JdbcTemplate.class) + static class Jdbc { + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore jdbcMetadataStore(JdbcTemplate jdbcTemplate, + MetadataStoreProperties metadataStoreProperties) { + + MetadataStoreProperties.Jdbc jdbcProperties = metadataStoreProperties.getJdbc(); + + JdbcMetadataStore jdbcMetadataStore = new JdbcMetadataStore(jdbcTemplate); + jdbcMetadataStore.setTablePrefix(jdbcProperties.getTablePrefix()); + jdbcMetadataStore.setRegion(jdbcProperties.getRegion()); + + return jdbcMetadataStore; + } + + } + + @Bean + @ConditionalOnMissingBean + public ConcurrentMetadataStore simpleMetadataStore() { + return new SimpleMetadataStore(); + } + +} diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreProperties.java b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreProperties.java new file mode 100644 index 00000000..263ff4a4 --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/java/org/springframework/cloud/stream/app/metadata/MetadataStoreProperties.java @@ -0,0 +1,290 @@ +/* + * Copyright 2018 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.metadata; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.integration.aws.metadata.DynamoDbMetadataStore; +import org.springframework.integration.gemfire.metadata.GemfireMetadataStore; +import org.springframework.integration.jdbc.metadata.JdbcMetadataStore; +import org.springframework.integration.redis.metadata.RedisMetadataStore; + +/** + * @author Artem Bilan + * + * @since 2.0.2 + */ +@ConfigurationProperties("metadata.store") +public class MetadataStoreProperties { + + private final Mongo mongoDb = new Mongo(); + + private final Gemfire gemfire = new Gemfire(); + + private final Redis redis = new Redis(); + + private final DynamoDb dynamoDb = new DynamoDb(); + + private final Jdbc jdbc = new Jdbc(); + + private final Zookeeper zookeeper = new Zookeeper(); + + public Mongo getMongoDb() { + return this.mongoDb; + } + + public Gemfire getGemfire() { + return this.gemfire; + } + + public Redis getRedis() { + return this.redis; + } + + public DynamoDb getDynamoDb() { + return this.dynamoDb; + } + + public Jdbc getJdbc() { + return this.jdbc; + } + + public Zookeeper getZookeeper() { + return this.zookeeper; + } + + public static class Mongo { + + /** + * MongoDB collection name for metadata. + */ + private String collection = "metadataStore"; + + public String getCollection() { + return this.collection; + } + + public void setCollection(String collection) { + this.collection = collection; + } + + } + + public static class Gemfire { + + /** + * Gemfire region name for metadata. + */ + private String region = GemfireMetadataStore.KEY; + + public String getRegion() { + return this.region; + } + + public void setRegion(String region) { + this.region = region; + } + + } + + public static class Redis { + + /** + * Redis key for metadata. + */ + private String key = RedisMetadataStore.KEY; + + public String getKey() { + return this.key; + } + + public void setKey(String key) { + this.key = key; + } + + } + + public static class DynamoDb { + + /** + * Table name for metadata. + */ + private String table = DynamoDbMetadataStore.DEFAULT_TABLE_NAME; + + /** + * Read capacity on the table. + */ + private long readCapacity = 1L; + + /** + * Write capacity on the table. + */ + private long writeCapacity = 1L; + + /** + * Delay between create table retries. + */ + private int createDelay = 1; + + /** + * Retry number for create table request. + */ + private int createRetries = 25; + + /** + * TTL for table entries. + */ + private Integer timeToLive; + + public String getTable() { + return this.table; + } + + public void setTable(String table) { + this.table = table; + } + + public long getReadCapacity() { + return this.readCapacity; + } + + public void setReadCapacity(long readCapacity) { + this.readCapacity = readCapacity; + } + + public long getWriteCapacity() { + return this.writeCapacity; + } + + public void setWriteCapacity(long writeCapacity) { + this.writeCapacity = writeCapacity; + } + + public int getCreateDelay() { + return this.createDelay; + } + + public void setCreateDelay(int createDelay) { + this.createDelay = createDelay; + } + + public int getCreateRetries() { + return this.createRetries; + } + + public void setCreateRetries(int createRetries) { + this.createRetries = createRetries; + } + + public Integer getTimeToLive() { + return this.timeToLive; + } + + public void setTimeToLive(Integer timeToLive) { + this.timeToLive = timeToLive; + } + + } + + public static class Jdbc { + + /** + * Prefix for the custom table name. + */ + private String tablePrefix = JdbcMetadataStore.DEFAULT_TABLE_PREFIX; + + /** + * Unique grouping identifier for messages persisted with this store. + */ + private String region = "DEFAULT"; + + public String getTablePrefix() { + return this.tablePrefix; + } + + public void setTablePrefix(String tablePrefix) { + this.tablePrefix = tablePrefix; + } + + public String getRegion() { + return this.region; + } + + public void setRegion(String region) { + this.region = region; + } + + } + + public static class Zookeeper { + + /** + * Zookeeper connect string in form HOST:PORT. + */ + private String connectString = "127.0.0.1:2181"; + + /** + * Retry interval for Zookeeper operations in milliseconds. + */ + private int retryInterval = 1000; + + /** + * Encoding to use when storing data in Zookeeper. + */ + private Charset encoding = StandardCharsets.UTF_8; + + /** + * Root node - store entries are children of this node. + */ + private String root = "/SpringIntegration-MetadataStore"; + + public String getConnectString() { + return connectString; + } + + public void setConnectString(String connectString) { + this.connectString = connectString; + } + + public int getRetryInterval() { + return this.retryInterval; + } + + public void setRetryInterval(int retryInterval) { + this.retryInterval = retryInterval; + } + + public Charset getEncoding() { + return this.encoding; + } + + public void setEncoding(Charset encoding) { + this.encoding = encoding; + } + + public String getRoot() { + return this.root; + } + + public void setRoot(String root) { + this.root = root; + } + + } + +} diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/src/main/resources/META-INF/spring.factories b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..adb3df0d --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.cloud.stream.app.metadata.MetadataStoreAutoConfiguration diff --git a/applications/apps-core/common/stream-apps-metadata-store-common/src/test/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfigurationTests.java b/applications/apps-core/common/stream-apps-metadata-store-common/src/test/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfigurationTests.java new file mode 100644 index 00000000..4d888c0e --- /dev/null +++ b/applications/apps-core/common/stream-apps-metadata-store-common/src/test/java/org/springframework/cloud/stream/app/metadata/MetadataStoreAutoConfigurationTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2018 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.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.willReturn; +import static org.mockito.Mockito.mock; + +import java.beans.Introspector; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; + +import org.apache.curator.test.TestingServer; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.test.context.FilteredClassLoader; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.aws.core.region.RegionProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.integration.aws.metadata.DynamoDbMetadataStore; +import org.springframework.integration.gemfire.metadata.GemfireMetadataStore; +import org.springframework.integration.hazelcast.metadata.HazelcastMetadataStore; +import org.springframework.integration.jdbc.metadata.JdbcMetadataStore; +import org.springframework.integration.metadata.ConcurrentMetadataStore; +import org.springframework.integration.metadata.MetadataStore; +import org.springframework.integration.metadata.SimpleMetadataStore; +import org.springframework.integration.mongodb.metadata.MongoDbMetadataStore; +import org.springframework.integration.redis.metadata.RedisMetadataStore; +import org.springframework.integration.zookeeper.metadata.ZookeeperMetadataStore; + +import com.amazonaws.auth.AWSCredentialsProvider; +import com.amazonaws.services.dynamodbv2.AmazonDynamoDBAsync; +import com.amazonaws.services.dynamodbv2.model.DescribeTableRequest; +import com.amazonaws.services.dynamodbv2.model.DescribeTableResult; + +/** + * @author Artem Bilan + * + * @since 2.0.2 + */ +@RunWith(Parameterized.class) +@Ignore +public class MetadataStoreAutoConfigurationTests { + + private final static List> METADATA_STORE_CLASSES = + Arrays.asList( + RedisMetadataStore.class, + MongoDbMetadataStore.class, + GemfireMetadataStore.class, + JdbcMetadataStore.class, + ZookeeperMetadataStore.class, + HazelcastMetadataStore.class, + DynamoDbMetadataStore.class, + SimpleMetadataStore.class + ); + + private static FilteredClassLoader filteredClassLoaderBut(Class classToInclude) { + return new FilteredClassLoader( + METADATA_STORE_CLASSES.stream() + .filter(Predicate.isEqual(classToInclude).negate()) + .toArray(Class[]::new)); + } + + private final ApplicationContextRunner contextRunner; + + private final Class classToInclude; + + + public MetadataStoreAutoConfigurationTests(Class classToInclude) { + this.classToInclude = classToInclude; + this.contextRunner = + new ApplicationContextRunner() + .withUserConfiguration(TestConfiguration.class) + .withClassLoader(filteredClassLoaderBut(classToInclude)); + } + + @Parameterized.Parameters + public static Iterable parameters() { + return METADATA_STORE_CLASSES; + } + + @Test + public void testMetadataStore() { + this.contextRunner + .run(context -> { + assertThat(context.getBeansOfType(MetadataStore.class)).hasSize(1); + + assertThat(context.getBeanNamesForType(this.classToInclude)) + .containsOnlyOnce(Introspector.decapitalize(this.classToInclude.getSimpleName())); + }); + } + + @Configuration + @EnableAutoConfiguration + public static class TestConfiguration { + + @Bean(destroyMethod = "stop") + @ConditionalOnClass(ZookeeperMetadataStore.class) + public static TestingServer zookeeperTestingServer() throws Exception { + TestingServer testingServer = new TestingServer(true); + + System.setProperty("metadata.store.zookeeper.connect-string", testingServer.getConnectString()); + System.setProperty("metadata.store.zookeeper.encoding", StandardCharsets.US_ASCII.name()); + + return testingServer; + } + + @Configuration + @ConditionalOnClass(DynamoDbMetadataStore.class) + protected static class DynamoDbMockConfig { + + @Bean + public static AmazonDynamoDBAsync dynamoDB() { + AmazonDynamoDBAsync dynamoDb = mock(AmazonDynamoDBAsync.class); + willReturn(new DescribeTableResult()) + .given(dynamoDb) + .describeTable(any(DescribeTableRequest.class)); + + return dynamoDb; + } + + @Bean + public static AWSCredentialsProvider awsCredentialsProvider() { + return mock(AWSCredentialsProvider.class); + } + + @Bean + public static RegionProvider regionProvider() { + return mock(RegionProvider.class); + } + + } + + } + +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/pom.xml b/applications/apps-core/common/stream-apps-micrometer-common/pom.xml new file mode 100644 index 00000000..99e9d9ea --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/pom.xml @@ -0,0 +1,35 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + stream-apps-micrometer-common + + + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-influx + test + + + io.pivotal.cfenv + java-cfenv-test-support + test + 2.1.1.RELEASE + + + org.springframework.boot + spring-boot-starter-actuator + + + + diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTags.java b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTags.java new file mode 100644 index 00000000..f4c2eec8 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTags.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 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.micrometer.common; + +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +/** + * Micrometer common tags for Cloud Foundry deployment properties. Based on the CF application environment variables: + * https://docs.cloudfoundry.org/devguide/deploy-apps/environment-variable.html + * + * Tags are set only if the "cloud" Spring profile is set. The "cloud" profile is activated automatically when an + * application is deployed in CF: https://docs.cloudfoundry.org/buildpacks/java/configuring-service-connections/spring-service-bindings.html#cloud-profiles + * + * Use the spring.cloud.stream.app.metrics.cf.tags.enabled=false property to disable inserting those tags. + * + * @author Christian Tzolov + */ +@Configuration +@Profile("cloud") +@ConditionalOnProperty(name = "spring.cloud.stream.app.metrics.cf.tags.enabled", havingValue = "true", matchIfMissing = true) +public class CloudFoundryMicrometerCommonTags { + + @Value("${vcap.application.org_name:default}") + private String organizationName; + + @Value("${vcap.application.space_id:unknown}") + private String spaceId; + + @Value("${vcap.application.space_name:unknown}") + private String spaceName; + + @Value("${vcap.application.application_name:unknown}") + private String applicationName; + + @Value("${vcap.application.application_id:unknown}") + private String applicationId; + + @Value("${vcap.application.application_version:unknown}") + private String applicationVersion; + + @Value("${vcap.application.instance_index:0}") + private String instanceIndex; + + @Bean + public MeterRegistryCustomizer cloudFoundryMetricsCommonTags() { + return registry -> registry.config() + .commonTags("cf.org.name", organizationName) + .commonTags("cf.space.id", spaceId) + .commonTags("cf.space.name", spaceName) + .commonTags("cf.app.id", applicationId) + .commonTags("cf.app.name", applicationName) + .commonTags("cf.app.version", applicationVersion) + .commonTags("cf.instance.index", instanceIndex); + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTags.java b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTags.java new file mode 100644 index 00000000..c2595fbe --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTags.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 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.micrometer.common; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.config.MeterFilter; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Auto configuration extends the micrometer metrics with additional tags such as: stream name, application name, + * instance index and guids. Later are necessary to allow discrimination and aggregation of app metrics by external + * metrics collection and visualizaiton tools. + * + * Use the spring.cloud.stream.app.metrics.common.tags.enabled=false property to disable inserting those tags. + * + * @author Christian Tzolov + */ +@Configuration +@ConditionalOnProperty(name = "spring.cloud.stream.app.metrics.common.tags.enabled", havingValue = "true", matchIfMissing = true) +public class SpringCloudStreamMicrometerCommonTags { + + @Value("${spring.cloud.dataflow.stream.name:unknown}") + private String streamName; + + @Value("${spring.cloud.dataflow.stream.app.label:unknown}") + private String applicationName; + + @Value("${spring.cloud.stream.instanceIndex:0}") + private String instanceIndex; + + @Value("${spring.cloud.application.guid:unknown}") + private String applicationGuid; + + @Value("${spring.cloud.dataflow.stream.app.type:unknown}") + private String applicationType; + + @Bean + public MeterRegistryCustomizer metricsCommonTags() { + return registry -> registry.config() + .commonTags("stream.name", streamName) + .commonTags("application.name", applicationName) + .commonTags("application.type", applicationType) + .commonTags("instance.index", instanceIndex) + .commonTags("application.guid", applicationGuid); + } + + @Bean + public MeterRegistryCustomizer renameNameTag() { + return registry -> { + if (registry.getClass().getCanonicalName().contains("AtlasMeterRegistry")) { + registry.config().meterFilter(MeterFilter.renameTag("spring.integration", "name", "aname")); + } + if (registry.getClass().getCanonicalName().contains("InfluxMeterRegistry")) { + registry.config().meterFilter(MeterFilter.replaceTagValues("application.name", + tagValue -> ("time".equalsIgnoreCase(tagValue)) ? "atime" : tagValue)); + } + }; + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessor.java b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessor.java new file mode 100644 index 00000000..94c56743 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/main/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 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.micrometer.common; + +import java.util.Properties; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; + +/** + * Disables all Micrometer Repositories added as App Starters dependencies by default. + * That means disabling Datadog, Influx and Prometheus. + * + * @author Christian Tzolov + */ +public class SpringCloudStreamMicrometerEnvironmentPostProcessor implements EnvironmentPostProcessor { + + protected static final String PROPERTY_SOURCE_KEY_NAME = SpringCloudStreamMicrometerEnvironmentPostProcessor.class.getName(); + + private final static String METRICS_PROPERTY_NAME_TEMPLATE = "management.metrics.export.%s.enabled"; + + private final static String[] METRICS_REPOSITORY_NAMES = + new String[] { "datadog", "influx", "prometheus", "prometheus.rsocket" }; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + Properties properties = new Properties(); + + if (!environment.containsProperty("management.endpoints.web.exposure.include")) { + properties.setProperty("management.endpoints.web.exposure.include", "prometheus"); + } + + for (String metricsRepositoryName : METRICS_REPOSITORY_NAMES) { + String propertyKey = String.format(METRICS_PROPERTY_NAME_TEMPLATE, metricsRepositoryName); + + // Back off if the property is already set. + if (!environment.containsProperty(propertyKey)) { + properties.setProperty(propertyKey, "false"); + } + } + + // This post-processor is called multiple times but sets the properties only once. + if (!properties.isEmpty()) { + PropertiesPropertySource propertiesPropertySource = + new PropertiesPropertySource(PROPERTY_SOURCE_KEY_NAME, properties); + environment.getPropertySources().addLast(propertiesPropertySource); + } + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/main/resources/META-INF/spring.factories b/applications/apps-core/common/stream-apps-micrometer-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..90d57c3e --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,6 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.stream.app.micrometer.common.SpringCloudStreamMicrometerCommonTags,\ +org.springframework.cloud.stream.app.micrometer.common.CloudFoundryMicrometerCommonTags +org.springframework.boot.env.EnvironmentPostProcessor=\ +org.springframework.cloud.stream.app.micrometer.common.SpringCloudStreamMicrometerEnvironmentPostProcessor + diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/AbstractMicrometerTagTest.java b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/AbstractMicrometerTagTest.java new file mode 100644 index 00000000..c15b1dd6 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/AbstractMicrometerTagTest.java @@ -0,0 +1,96 @@ +/* + * Copyright 2018-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.app.micrometer.common; + +import java.io.IOException; +import java.nio.charset.Charset; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.Meter; +import io.micrometer.core.instrument.simple.SimpleConfig; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.pivotal.cfenv.test.CfEnvTestUtils; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleProperties; +import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimplePropertiesConfigAdapter; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StreamUtils; + +import static org.junit.Assert.assertNotNull; + +/** + * @author Christian Tzolov + * @author Soby Chacko + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = AbstractMicrometerTagTest.AutoConfigurationApplication.class) +public class AbstractMicrometerTagTest { + + @Autowired + protected SimpleMeterRegistry simpleMeterRegistry; + + @Autowired + protected ConfigurableApplicationContext context; + + protected Meter meter; + + @Before + public void before() { + assertNotNull(simpleMeterRegistry); + meter = simpleMeterRegistry.find("jvm.memory.committed").meter(); + assertNotNull("The jvm.memory.committed meter mast be present in SpringBoot apps!", meter); + } + + @BeforeClass + public static void setup() throws IOException { + String serviceJson = StreamUtils.copyToString(new DefaultResourceLoader().getResource( + "classpath:/org/springframework/cloud/stream/app/micrometer/common/pcf-scs-info.json") + .getInputStream(), Charset.forName("UTF-8")); + CfEnvTestUtils.mockVcapServicesFromString(serviceJson); + } + + @SpringBootApplication + @EnableConfigurationProperties(SimpleProperties.class) + public static class AutoConfigurationApplication { + + public static void main(String[] args) { + SpringApplication.run(AutoConfigurationApplication.class, args); + } + + @Bean + public SimpleMeterRegistry simpleMeterRegistry(SimpleConfig config, Clock clock) { + return new SimpleMeterRegistry(config, clock); + } + + @Bean + @ConditionalOnMissingBean + public SimpleConfig simpleConfig(SimpleProperties simpleProperties) { + return new SimplePropertiesConfigAdapter(simpleProperties); + } + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTagsTest.java b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTagsTest.java new file mode 100644 index 00000000..f5b610e0 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/CloudFoundryMicrometerCommonTagsTest.java @@ -0,0 +1,100 @@ +/* + * Copyright 2018 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.micrometer.common; + +import org.hamcrest.Matchers; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.TestPropertySource; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Tzolov + */ +@RunWith(Enclosed.class) +public class CloudFoundryMicrometerCommonTagsTest { + + @ActiveProfiles("cloud") + public static class ActiveCloudProfileDefaultValues extends AbstractMicrometerTagTest { + @Test + public void testDefaultTagValues() { + assertThat(meter.getId().getTag("cf.org.name"), is("default")); + assertThat(meter.getId().getTag("cf.space.id"), is("unknown")); + assertThat(meter.getId().getTag("cf.space.name"), is("unknown")); + assertThat(meter.getId().getTag("cf.app.name"), is("unknown")); + assertThat(meter.getId().getTag("cf.app.id"), is("unknown")); + assertThat(meter.getId().getTag("cf.app.version"), is("unknown")); + assertThat(meter.getId().getTag("cf.instance.index"), is("0")); + } + } + + @TestPropertySource(properties = { + "vcap.application.org_name=PivotalOrg", + "vcap.application.space_id=SpringSpaceId", + "vcap.application.space_name=SpringSpace", + "vcap.application.application_name=App666", + "vcap.application.application_id=666guid", + "vcap.application.application_version=2.0", + "vcap.application.instance_index=123" }) + @ActiveProfiles("cloud") + public static class ActiveCloudProfile extends AbstractMicrometerTagTest { + + @Test + public void testPresetTagValues() { + assertThat(meter.getId().getTag("cf.org.name"), is("PivotalOrg")); + assertThat(meter.getId().getTag("cf.space.id"), is("SpringSpaceId")); + assertThat(meter.getId().getTag("cf.space.name"), is("SpringSpace")); + assertThat(meter.getId().getTag("cf.app.name"), is("App666")); + assertThat(meter.getId().getTag("cf.app.id"), is("666guid")); + assertThat(meter.getId().getTag("cf.app.version"), is("2.0")); + assertThat(meter.getId().getTag("cf.instance.index"), is("123")); + } + } + + @TestPropertySource(properties = { + "vcap.application.org_name=PivotalOrg", + "vcap.application.space_id=SpringSpaceId", + "vcap.application.space_name=SpringSpace", + "vcap.application.application_name=App666", + "vcap.application.application_id=666guid", + "vcap.application.application_version=2.0", + "vcap.application.instance_index=123" }) + public static class InactiveCloudProfile extends AbstractMicrometerTagTest { + + @Test + public void testDisabledTagValues() { + assertThat(meter.getId().getTag("cf.org.name"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.space.id"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.space.name"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.app.name"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.app.id"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.app.version"), is(Matchers.nullValue())); + assertThat(meter.getId().getTag("cf.instance.index"), is(Matchers.nullValue())); + } + } + + @TestPropertySource(properties = { "spring.cloud.stream.app.metrics.cf.tags.enabled=false" }) + @ActiveProfiles("cloud") + public static class ActiveCloudProfileDisabledProperty extends InactiveCloudProfile { + } +} + diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/InfluxReservedKeywordHandlingTest.java b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/InfluxReservedKeywordHandlingTest.java new file mode 100644 index 00000000..c9d06a51 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/InfluxReservedKeywordHandlingTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 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.app.micrometer.common; + +import io.micrometer.core.instrument.Meter; +import io.micrometer.influx.InfluxMeterRegistry; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Tzolov + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = InfluxReservedKeywordHandlingTest.AutoConfigurationApplication.class, + properties = { + "management.metrics.export.influx.enabled=true", + "spring.cloud.dataflow.stream.app.label=time" }) +public class InfluxReservedKeywordHandlingTest { + + @Autowired + protected InfluxMeterRegistry influxMeterRegistry; + + @Test + public void testPresetTagValues() { + assertNotNull(influxMeterRegistry); + Meter meter = influxMeterRegistry.find("jvm.memory.committed").meter(); + assertNotNull("The jvm.memory.committed meter mast be present in SpringBoot apps!", meter); + + assertThat(meter.getId().getTag("application.name"), is("atime")); + } + + @SpringBootApplication + public static class AutoConfigurationApplication { + public static void main(String[] args) { + SpringApplication.run(AutoConfigurationApplication.class, args); + } + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTagsTest.java b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTagsTest.java new file mode 100644 index 00000000..5fef6c98 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerCommonTagsTest.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 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.micrometer.common; + +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import org.springframework.test.context.TestPropertySource; + +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Tzolov + */ +@RunWith(Enclosed.class) +public class SpringCloudStreamMicrometerCommonTagsTest { + + public static class TestDefaultTagValues extends AbstractMicrometerTagTest { + + @Test + public void testDefaultTagValues() { + assertThat(meter.getId().getTag("stream.name"), is("unknown")); + assertThat(meter.getId().getTag("application.name"), is("unknown")); + assertThat(meter.getId().getTag("instance.index"), is("0")); + assertThat(meter.getId().getTag("application.type"), is("unknown")); + assertThat(meter.getId().getTag("application.guid"), is("unknown")); + } + } + + @TestPropertySource(properties = { + "spring.cloud.dataflow.stream.name=myStream", + "spring.cloud.dataflow.stream.app.label=myApp", + "spring.cloud.stream.instanceIndex=666", + "spring.cloud.application.guid=666guid", + "spring.cloud.dataflow.stream.app.type=source" }) + public static class TestPresetTagValues extends AbstractMicrometerTagTest { + + @Test + public void testPresetTagValues() { + assertThat(meter.getId().getTag("stream.name"), is("myStream")); + assertThat(meter.getId().getTag("application.name"), is("myApp")); + assertThat(meter.getId().getTag("instance.index"), is("666")); + assertThat(meter.getId().getTag("application.type"), is("source")); + assertThat(meter.getId().getTag("application.guid"), is("666guid")); + } + } + + @TestPropertySource(properties = { "spring.cloud.stream.app.metrics.common.tags.enabled=false" }) + public static class TestDisabledTagValues extends AbstractMicrometerTagTest { + + @Test + public void testDefaultTagValues() { + assertThat(meter.getId().getTag("stream.name"), is(nullValue())); + assertThat(meter.getId().getTag("application.name"), is(nullValue())); + assertThat(meter.getId().getTag("instance.index"), is(nullValue())); + assertThat(meter.getId().getTag("application.type"), is(nullValue())); + assertThat(meter.getId().getTag("application.guid"), is(nullValue())); + } + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessorTest.java b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessorTest.java new file mode 100644 index 00000000..da340292 --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/java/org/springframework/cloud/stream/app/micrometer/common/SpringCloudStreamMicrometerEnvironmentPostProcessorTest.java @@ -0,0 +1,94 @@ +/* + * Copyright 2018 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.micrometer.common; + +import org.hamcrest.core.Is; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; + +import org.springframework.core.env.PropertySource; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThat; + +/** + * @author Christian Tzolov + */ +@RunWith(Enclosed.class) +public class SpringCloudStreamMicrometerEnvironmentPostProcessorTest { + + public static class TestDefaultMetricsEnabledProperties extends AbstractMicrometerTagTest { + + @Test + public void testDefaultProperties() { + assertNotNull(context); + + PropertySource propertySource = context.getEnvironment().getPropertySources() + .get(SpringCloudStreamMicrometerEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNotNull("Property source " + + SpringCloudStreamMicrometerEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is null", + propertySource); + + assertThat(propertySource.getProperty("management.metrics.export.influx.enabled"), Is.is("false")); + assertThat(propertySource.getProperty("management.metrics.export.prometheus.enabled"), Is.is("false")); + assertThat(propertySource.getProperty("management.metrics.export.prometheus.rsocket.enabled"), Is.is("false")); + assertThat(propertySource.getProperty("management.metrics.export.datadog.enabled"), Is.is("false")); + + assertThat(propertySource.getProperty("management.endpoints.web.exposure.include"), Is.is("prometheus")); + + assertThat(context.getEnvironment().getProperty("management.metrics.export.influx.enabled"), Is.is("false")); + assertThat(context.getEnvironment().getProperty("management.metrics.export.prometheus.enabled"), Is.is("false")); + assertThat(context.getEnvironment().getProperty("management.metrics.export.datadog.enabled"), Is.is("false")); + + assertThat(context.getEnvironment().getProperty("management.endpoints.web.exposure.include"), Is.is("prometheus")); + + } + } + + @TestPropertySource(properties = { + "management.metrics.export.simple.enabled=true", + "management.metrics.export.influx.enabled=true", + "management.metrics.export.prometheus.enabled=true", + "management.metrics.export.prometheus.rsocket.enabled=true", + "management.metrics.export.datadog.enabled=true", + "management.endpoints.web.exposure.include=info,health"}) + public static class TestOverrideMetricsEnabledProperties extends AbstractMicrometerTagTest { + + @Test + public void testOverrideProperties() { + assertNotNull(context); + + PropertySource propertySource = context.getEnvironment().getPropertySources() + .get(SpringCloudStreamMicrometerEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNull("Property source " + + SpringCloudStreamMicrometerEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is not null", + propertySource); + + assertThat(context.getEnvironment().getProperty("management.metrics.export.influx.enabled"), Is.is("true")); + assertThat(context.getEnvironment().getProperty("management.metrics.export.prometheus.enabled"), Is.is("true")); + assertThat(context.getEnvironment().getProperty("management.metrics.export.prometheus.rsocket.enabled"), Is.is("true")); + assertThat(context.getEnvironment().getProperty("management.metrics.export.datadog.enabled"), Is.is("true")); + + assertThat(context.getEnvironment().getProperty("management.endpoints.web.exposure.include"), Is.is("info,health")); + } + } +} diff --git a/applications/apps-core/common/stream-apps-micrometer-common/src/test/resources/org/springframework/cloud/stream/app/micrometer/common/pcf-scs-info.json b/applications/apps-core/common/stream-apps-micrometer-common/src/test/resources/org/springframework/cloud/stream/app/micrometer/common/pcf-scs-info.json new file mode 100644 index 00000000..adede92b --- /dev/null +++ b/applications/apps-core/common/stream-apps-micrometer-common/src/test/resources/org/springframework/cloud/stream/app/micrometer/common/pcf-scs-info.json @@ -0,0 +1,13 @@ +{ + "sso":[{ + "name": "sso", + "label": "sso", + "plan": "notfree", + "tags": ["configuration"], + "credentials":{ + "uri": "https://pivotal.io", + "client_id": "fakeClientId", + "client_secret": "fakeSecret", + "access_token_uri": "token" + } + }]} diff --git a/applications/apps-core/common/stream-apps-postprocessor-common/pom.xml b/applications/apps-core/common/stream-apps-postprocessor-common/pom.xml new file mode 100644 index 00000000..8107c8b5 --- /dev/null +++ b/applications/apps-core/common/stream-apps-postprocessor-common/pom.xml @@ -0,0 +1,22 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-postprocessor-common + stream-apps-postprocessor-common + + + + org.springframework.integration + spring-integration-test + test + + + + diff --git a/applications/apps-core/common/stream-apps-postprocessor-common/src/main/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessor.java b/applications/apps-core/common/stream-apps-postprocessor-common/src/main/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessor.java new file mode 100644 index 00000000..470f6277 --- /dev/null +++ b/applications/apps-core/common/stream-apps-postprocessor-common/src/main/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessor.java @@ -0,0 +1,88 @@ +/* + * Copyright 2018 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.postprocessor; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.cloud.stream.messaging.Sink; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * An {@link EnvironmentPostProcessor} to set the {@code spring.cloud.stream.bindings.{input,output}.contentType} + * channel properties to a default of {@code application/octet-stream} if it has not been set already. + * + * Subclasses may extend this class to change the default content type and channel name(s). + * + * @author Chris Schaefer + */ +public class ContentTypeEnvironmentPostProcessor implements EnvironmentPostProcessor { + private Map channelMap = createChannelMap(); + + private Map createChannelMap() { + Map channelMap = new HashMap<>(); + channelMap.put(Sink.INPUT, "application/octet-stream"); + channelMap.put(Source.OUTPUT, "application/octet-stream"); + + return channelMap; + } + + protected static final String PROPERTY_SOURCE_KEY_NAME = ContentTypeEnvironmentPostProcessor.class.getName(); + protected static final String CONTENT_TYPE_PROPERTY_PREFIX = "spring.cloud.stream.bindings."; + protected static final String CONTENT_TYPE_PROPERTY_SUFFIX = ".contentType"; + + public ContentTypeEnvironmentPostProcessor() { + super(); + } + + protected ContentTypeEnvironmentPostProcessor(Map channelMap) { + this.channelMap = channelMap; + } + + protected ContentTypeEnvironmentPostProcessor(String contentType) { + for (Map.Entry channel : channelMap.entrySet()) { + channel.setValue(contentType); + } + } + + protected ContentTypeEnvironmentPostProcessor(String channelName, String contentType) { + channelMap.put(channelName, contentType); + } + + @Override + public void postProcessEnvironment(ConfigurableEnvironment configurableEnvironment, SpringApplication springApplication) { + Properties properties = new Properties(); + + for (Map.Entry channel : channelMap.entrySet()) { + String propertyKey = CONTENT_TYPE_PROPERTY_PREFIX + channel.getKey() + CONTENT_TYPE_PROPERTY_SUFFIX; + + if (!configurableEnvironment.containsProperty(propertyKey)) { + properties.setProperty(propertyKey, channel.getValue()); + } + } + + if (!properties.isEmpty()) { + PropertiesPropertySource propertiesPropertySource = + new PropertiesPropertySource(PROPERTY_SOURCE_KEY_NAME, properties); + configurableEnvironment.getPropertySources().addLast(propertiesPropertySource); + } + } +} diff --git a/applications/apps-core/common/stream-apps-postprocessor-common/src/test/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessorTests.java b/applications/apps-core/common/stream-apps-postprocessor-common/src/test/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessorTests.java new file mode 100644 index 00000000..bde024cf --- /dev/null +++ b/applications/apps-core/common/stream-apps-postprocessor-common/src/test/java/org/springframework/cloud/stream/app/postprocessor/ContentTypeEnvironmentPostProcessorTests.java @@ -0,0 +1,227 @@ +/* + * Copyright 2018 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.postprocessor; + +import org.junit.Test; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.cloud.stream.messaging.Sink; +import org.springframework.cloud.stream.messaging.Source; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; +import org.springframework.core.env.PropertySource; + +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Test cases for {@ContentTypeEnvironmentPostProcessor}. + * + * @author Chris Schaefer + */ +public class ContentTypeEnvironmentPostProcessorTests { + @Test + public void testPostProcessorDefaults() { + ConfigurableEnvironment configurableEnvironment = getEnvironment(); + + PropertySource propertySource = configurableEnvironment.getPropertySources() + .get(ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNotNull("Property source " + ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is null", + propertySource); + + assertTrue("Unexpected input content type", propertySource.getProperty(getContentTypeProperty(Sink.INPUT)) + .equals("application/octet-stream")); + + assertTrue("Unexpected output content type", propertySource.getProperty(getContentTypeProperty(Source.OUTPUT)) + .equals("application/octet-stream")); + } + + @Test + public void testUserDefinedOutputContentType() { + PropertiesPropertySource testProperties = buildTestProperties(Source.OUTPUT, "text/plain"); + ConfigurableEnvironment configurableEnvironment = getEnvironment(testProperties); + + assertTrue("Output contentType property key not found", + configurableEnvironment.containsProperty(getContentTypeProperty(Source.OUTPUT))); + + assertTrue("Unexpected output content type", configurableEnvironment.getProperty(getContentTypeProperty(Source.OUTPUT)) + .equals("text/plain")); + } + + @Test + public void testUserDefinedInputContentType() { + PropertiesPropertySource testProperties = buildTestProperties(Sink.INPUT, "text/html"); + ConfigurableEnvironment configurableEnvironment = getEnvironment(testProperties); + + assertTrue("Input contentType property key not found", + configurableEnvironment.containsProperty(getContentTypeProperty(Sink.INPUT))); + + assertTrue("Unexpected input content type", configurableEnvironment.getProperty(getContentTypeProperty(Sink.INPUT)) + .equals("text/html")); + } + + @Test + public void testConfigureCustomChannel() { + ConfigurableEnvironment configurableEnvironment = getEnvironment(new CustomChannel()); + + PropertySource propertySource = configurableEnvironment.getPropertySources() + .get(ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNotNull("Property source " + ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is null", + propertySource); + + assertTrue("myChannelName contentType property key not found", + propertySource.containsProperty(getContentTypeProperty("myChannelName"))); + + assertTrue("Unexpected myChannelName content type", propertySource.getProperty(getContentTypeProperty("myChannelName")) + .equals("application/octet-stream")); + } + + public static class CustomChannel extends ContentTypeEnvironmentPostProcessor { + private static final String CHANNEL_NAME = "myChannelName"; + private static final String CONTENT_TYPE = "application/octet-stream"; + + public CustomChannel() { + super(CHANNEL_NAME, CONTENT_TYPE); + } + } + + @Test + public void testConfigureDefaultChannelsCustomContentType() { + ConfigurableEnvironment configurableEnvironment = getEnvironment(new DefaultChannelsCustomContentTypes()); + + PropertySource propertySource = configurableEnvironment.getPropertySources() + .get(ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNotNull("Property source " + ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is null", + propertySource); + + assertTrue("Output contentType property key not found", + propertySource.containsProperty(getContentTypeProperty(Source.OUTPUT))); + + assertTrue("Unexpected output content type", propertySource.getProperty(getContentTypeProperty(Source.OUTPUT)) + .equals("image/jpeg")); + + assertTrue("Input contentType property key not found", + propertySource.containsProperty(getContentTypeProperty(Sink.INPUT))); + + assertTrue("Unexpected input content type", propertySource.getProperty(getContentTypeProperty(Sink.INPUT)) + .equals("image/gif")); + } + + @Test + public void testConfigureDefaultChannelsSameContentType() { + ConfigurableEnvironment configurableEnvironment = getEnvironment(new ChannelSameContentType()); + + PropertySource propertySource = configurableEnvironment.getPropertySources() + .get(ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME); + + assertNotNull("Property source " + ContentTypeEnvironmentPostProcessor.PROPERTY_SOURCE_KEY_NAME + " is null", + propertySource); + + assertTrue("Output contentType property key not found", + propertySource.containsProperty(getContentTypeProperty(Source.OUTPUT))); + + assertTrue("Unexpected output content type", propertySource.getProperty(getContentTypeProperty(Source.OUTPUT)) + .equals("image/jpeg")); + + assertTrue("Input contentType property key not found", + propertySource.containsProperty(getContentTypeProperty(Sink.INPUT))); + + assertTrue("Unexpected input content type", propertySource.getProperty(getContentTypeProperty(Sink.INPUT)) + .equals("image/jpeg")); + } + + public static class ChannelSameContentType extends ContentTypeEnvironmentPostProcessor { + private static final String CONTENT_TYPE = "image/jpeg"; + + public ChannelSameContentType() { + super(CONTENT_TYPE); + } + } + + public static class DefaultChannelsCustomContentTypes extends ContentTypeEnvironmentPostProcessor { + private static Map CHANNEL_MAP = createChannelMap(); + + private static Map createChannelMap() { + Map channelMap = new HashMap<>(); + channelMap.put(Source.OUTPUT, "image/jpeg"); + channelMap.put(Sink.INPUT, "image/gif"); + + return channelMap; + } + + public DefaultChannelsCustomContentTypes() { + super(CHANNEL_MAP); + } + } + + private static String getContentTypeProperty(String channelName) { + return ContentTypeEnvironmentPostProcessor.CONTENT_TYPE_PROPERTY_PREFIX + channelName + + ContentTypeEnvironmentPostProcessor.CONTENT_TYPE_PROPERTY_SUFFIX; + } + + private PropertiesPropertySource buildTestProperties(String channelName, String contentType) { + Properties testProperties = new Properties(); + testProperties.setProperty(getContentTypeProperty(channelName), contentType); + + return new PropertiesPropertySource("test-properties", testProperties); + } + + private ConfigurableEnvironment getEnvironment() { + return getEnvironment(null, null); + } + + private ConfigurableEnvironment getEnvironment(PropertiesPropertySource propertiesPropertySource) { + return getEnvironment(propertiesPropertySource, null); + } + + private ConfigurableEnvironment getEnvironment(EnvironmentPostProcessor environmentPostProcessor) { + return getEnvironment(null, environmentPostProcessor); + } + + private ConfigurableEnvironment getEnvironment(PropertiesPropertySource propertiesPropertySource, + EnvironmentPostProcessor environmentPostProcessor) { + SpringApplication springApplication = new SpringApplicationBuilder() + .sources(ContentTypeEnvironmentPostProcessorTests.class) + .web(WebApplicationType.NONE).build(); + + ConfigurableApplicationContext context = springApplication.run(); + + if (propertiesPropertySource != null) { + context.getEnvironment().getPropertySources().addFirst(propertiesPropertySource); + } + + if (environmentPostProcessor == null) { + environmentPostProcessor = new ContentTypeEnvironmentPostProcessor(); + } + + environmentPostProcessor.postProcessEnvironment(context.getEnvironment(), springApplication); + + ConfigurableEnvironment configurableEnvironment = context.getEnvironment(); + context.close(); + + return configurableEnvironment; + } +} diff --git a/applications/apps-core/common/stream-apps-security-common/README.adoc b/applications/apps-core/common/stream-apps-security-common/README.adoc new file mode 100644 index 00000000..b58001d6 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/README.adoc @@ -0,0 +1,33 @@ +=== `App Starters Security` Common Module + +Spring Boot auto-configuration to manage the web security of the application starters. + +When the `app-starters-security-common` dependency is on the classpath, the `spring.cloud.streamapp.security.enabled` and `spring.cloud.streamapp.security.csrf-enabled` properties control the application security behavior. + +By default the security is enabled allowing unauthorized access only to the `Info` and `Health` endpoints. + +The `spring.cloud.streamapp.security.enabled = false` completely surpass the application security. + +For secured application setting `spring.cloud.streamapp.security.csrf-enabled = false` disables security for the CSRF access. + +With security enabled (`spring.cloud.streamapp.security.enabled = true`) and `actuator` dependency on the classpath, the `(Reactive)ManagementWebSecurityAutoConfiguration` is activated, providing unauthenticated access to the `HealthEndpoint` and `InfoEndpoint`. + +If the user specifies their own `WebSecurityConfigurerAdapter` (for MVC application), this configuration will back-off completely and the user should specify all the bits that they want to configure as part of the custom security configuration. +For reactive (WebFlux) application the same effect can be achieved with a custom `WebFilterChainProxy` bean. + +=== Configuration +To include app starters security management for a stream app, just include a dependency on this module. + +[source,xml] +---- + + org.springframework.cloud.stream.app + app-starters-security-common + +---- + + +All Spring Cloud Stream app starters that inherit form the `core` pom have the `app-starters-security-common` dependency included by default. + +* `spring.cloud.streamapp.security.enabled` (default: `true`). If set to `false` it surpasses the boot security. +* `spring.cloud.streamapp.security.csrf-enabled` (default: `true`). If set to `false`, for secured applications it enables CQRS. diff --git a/applications/apps-core/common/stream-apps-security-common/pom.xml b/applications/apps-core/common/stream-apps-security-common/pom.xml new file mode 100644 index 00000000..1e3a5271 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/pom.xml @@ -0,0 +1,52 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-security-common + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + org.springframework.boot + spring-boot-autoconfigure-processor + true + + + javax.servlet + javax.servlet-api + provided + + + org.springframework.boot + spring-boot-starter-actuator + true + + + org.springframework.boot + spring-boot-starter-webflux + true + + + org.springframework.boot + spring-boot-starter-web + true + test + + + org.springframework.boot + spring-boot-starter-security + + + + diff --git a/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebFluxSecurityAutoConfiguration.java b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebFluxSecurityAutoConfiguration.java new file mode 100644 index 00000000..7d160091 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebFluxSecurityAutoConfiguration.java @@ -0,0 +1,66 @@ +/* + * Copyright 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.app.security.common; + +import org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity; +import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.WebFilterChainProxy; +import org.springframework.web.reactive.config.WebFluxConfigurer; + +import reactor.core.publisher.Flux; + +/** + * @author Artem Bilan + * + * @since 3.0 + */ +@Conditional(OnHttpCsrfOrSecurityDisabled.class) +@Configuration +@ConditionalOnClass({ Flux.class, EnableWebFluxSecurity.class, WebFilterChainProxy.class, WebFluxConfigurer.class }) +@ConditionalOnMissingBean(WebFilterChainProxy.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@AutoConfigureBefore(value = { ReactiveManagementWebSecurityAutoConfiguration.class, + ReactiveSecurityAutoConfiguration.class }) +@EnableConfigurationProperties(AppStarterWebSecurityAutoConfigurationProperties.class) +public class AppStarterWebFluxSecurityAutoConfiguration { + + @Bean + public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, + AppStarterWebSecurityAutoConfigurationProperties securityProperties) { + if (!securityProperties.isCsrfEnabled()) { + http.csrf().disable(); + } + if (!securityProperties.isEnabled()) { + http.authorizeExchange() + .anyExchange() + .permitAll(); + } + return http.build(); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfiguration.java b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfiguration.java new file mode 100644 index 00000000..8e451ceb --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfiguration.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018-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.app.security.common; + +import org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 2.1 + */ +@Conditional(OnHttpCsrfOrSecurityDisabled.class) +@Configuration +@ConditionalOnClass(WebSecurityConfigurerAdapter.class) +@ConditionalOnMissingBean(WebSecurityConfigurerAdapter.class) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) +@AutoConfigureBefore(value = { ManagementWebSecurityAutoConfiguration.class, SecurityAutoConfiguration.class }) +@EnableConfigurationProperties(AppStarterWebSecurityAutoConfigurationProperties.class) +@EnableWebSecurity +public class AppStarterWebSecurityAutoConfiguration { + + @Bean + WebSecurityConfigurerAdapter appStarterWebSecurityConfigurerAdapter( + AppStarterWebSecurityAutoConfigurationProperties securityProperties) { + + + return new WebSecurityConfigurerAdapter() { + + @Override + protected void configure(HttpSecurity http) throws Exception { + super.configure(http); + if (!securityProperties.isCsrfEnabled()) { + http.csrf().disable(); + } + } + + @Override + public void configure(WebSecurity builder) { + if (!securityProperties.isEnabled()) { + builder.ignoring().antMatchers("/**"); + } + } + + }; + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfigurationProperties.java b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfigurationProperties.java new file mode 100644 index 00000000..7ed36d7a --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/AppStarterWebSecurityAutoConfigurationProperties.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 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.security.common; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * {@code AppStarterWebSecurityAutoConfiguration} properties. + * + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 2.1 + */ +@ConfigurationProperties("spring.cloud.streamapp.security") +public class AppStarterWebSecurityAutoConfigurationProperties { + + + /** + * The security enabling flag. + */ + private boolean enabled = true; + + /** + * The security CSRF enabling flag. Makes sense only if security 'enabled` is `true'. + */ + private boolean csrfEnabled = true; + + public boolean isEnabled() { + return this.enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public boolean isCsrfEnabled() { + return this.csrfEnabled; + } + + public void setCsrfEnabled(boolean csrfEnabled) { + this.csrfEnabled = csrfEnabled; + } +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/OnHttpCsrfOrSecurityDisabled.java b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/OnHttpCsrfOrSecurityDisabled.java new file mode 100644 index 00000000..70e0ff22 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/main/java/org/springframework/cloud/stream/app/security/common/OnHttpCsrfOrSecurityDisabled.java @@ -0,0 +1,44 @@ +/* + * Copyright 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.app.security.common; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +/** + * An {@link AnyNestedCondition} to enable app starters-specific security auto-configuration + * overriding out-of-the-box one in Spring Boot. + * + * @author Artem Bilan + * + * @since 3.0 + */ +class OnHttpCsrfOrSecurityDisabled extends AnyNestedCondition { + + OnHttpCsrfOrSecurityDisabled() { + super(ConfigurationPhase.PARSE_CONFIGURATION); + } + + @ConditionalOnProperty(name = "spring.cloud.streamapp.security.enabled", havingValue = "false") + static class SecurityDisabled { + } + + @ConditionalOnProperty(name = "spring.cloud.streamapp.security.csrf-enabled", havingValue = "false") + static class HttpCsrfDisabled { + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/main/resources/META-INF/spring.factories b/applications/apps-core/common/stream-apps-security-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..81c908f4 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.cloud.stream.app.security.common.AppStarterWebSecurityAutoConfiguration,\ +org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/AbstractSecurityCommonTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/AbstractSecurityCommonTests.java new file mode 100644 index 00000000..c9cd2230 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/AbstractSecurityCommonTests.java @@ -0,0 +1,40 @@ +/* + * Copyright 2018-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.app.security.common; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.test.annotation.DirtiesContext; + +/** + * @author Christian Tzolov + * @author Artem Bilan + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@DirtiesContext +public abstract class AbstractSecurityCommonTests { + + @Autowired + protected TestRestTemplate restTemplate; + + @SpringBootApplication + public static class AutoConfigurationApplication { + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityDisabledManagementSecurityEnabledTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityDisabledManagementSecurityEnabledTests.java new file mode 100644 index 00000000..9474e504 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityDisabledManagementSecurityEnabledTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=reactive", + "spring.cloud.streamapp.security.enabled=false", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class ReactiveSecurityDisabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java new file mode 100644 index 00000000..a899752a --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=reactive", + "org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration" + + ",org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class ReactiveSecurityEnabledManagementSecurityDisabledAuthorizedAccessTests extends AbstractSecurityCommonTests { + + @Autowired + private SecurityProperties securityProperties; + + @BeforeEach + public void before() { + restTemplate.getRestTemplate().getInterceptors().add(new BasicAuthenticationInterceptor( + securityProperties.getUser().getName(), securityProperties.getUser().getPassword())); + } + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java new file mode 100644 index 00000000..df88435b --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=reactive", + "spring.autoconfigure.exclude=" + + "org.springframework.boot.actuate.autoconfigure.security.reactive.ReactiveManagementWebSecurityAutoConfiguration" + + ",org.springframework.cloud.stream.app.security.common.AppStarterWebFluxSecurityAutoConfiguration", + "management.endpoints.web.exposure.include=health,info" }) +public class ReactiveSecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityEnabledTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityEnabledTests.java new file mode 100644 index 00000000..256281a1 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/ReactiveSecurityEnabledManagementSecurityEnabledTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=reactive", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class ReactiveSecurityEnabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + // The ManagementWebSecurityAutoConfiguration exposes only Info and Health endpoint not Env! + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityDisabledManagementSecurityEnabledTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityDisabledManagementSecurityEnabledTests.java new file mode 100644 index 00000000..673cd8f5 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityDisabledManagementSecurityEnabledTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=servlet", + "spring.cloud.streamapp.security.enabled=false", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class SecurityDisabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java new file mode 100644 index 00000000..affc6bfb --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.security.SecurityProperties; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.support.BasicAuthenticationInterceptor; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=servlet", + "spring.autoconfigure.exclude=org.springframework.boot.actuate.autoconfigure.security.servlet" + + ".ManagementWebSecurityAutoConfiguration" + + ",org.springframework.cloud.stream.app.security.common.AppStarterWebSecurityAutoConfiguration", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class SecurityEnabledManagementSecurityDisabledAuthorizedAccessTests extends AbstractSecurityCommonTests { + + @Autowired + private SecurityProperties securityProperties; + + @BeforeEach + public void before() { + restTemplate.getRestTemplate().getInterceptors().add(new BasicAuthenticationInterceptor( + securityProperties.getUser().getName(), securityProperties.getUser().getPassword())); + } + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java new file mode 100644 index 00000000..7161bd3e --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2019-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.stream.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=servlet", + "spring.autoconfigure.exclude=" + + "org.springframework.boot.actuate.autoconfigure.security.servlet.ManagementWebSecurityAutoConfiguration" + + ",org.springframework.cloud.stream.app.security.common.AppStarterWebSecurityAutoConfiguration", + "management.endpoints.web.exposure.include=health,info" }) +public class SecurityEnabledManagementSecurityDisabledUnauthorizedAccessTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("Unauthorized", health.get("error")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("Unauthorized", info.get("error")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityEnabledTests.java b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityEnabledTests.java new file mode 100644 index 00000000..155eaaa8 --- /dev/null +++ b/applications/apps-core/common/stream-apps-security-common/src/test/java/org/springframework/cloud/stream/app/security/common/SecurityEnabledManagementSecurityEnabledTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 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.app.security.common; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import java.util.Map; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Christian Tzolov + * @author Artem Bilan + * + * @since 3.0 + */ +@TestPropertySource(properties = { + "spring.main.web-application-type=servlet", + "management.endpoints.web.exposure.include=health,info,env", + "info.name=MY TEST APP" }) +public class SecurityEnabledManagementSecurityEnabledTests extends AbstractSecurityCommonTests { + + @Test + @SuppressWarnings("rawtypes") + public void testHealthEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/health", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map health = response.getBody(); + assertEquals("UP", health.get("status")); + } + + @Test + @SuppressWarnings("rawtypes") + public void testInfoEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/info", Map.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertTrue(response.hasBody()); + Map info = response.getBody(); + assertEquals("MY TEST APP", info.get("name")); + } + + // The ManagementWebSecurityAutoConfiguration exposes only Info and Health endpoint not Env! + @Test + @SuppressWarnings("rawtypes") + public void testEnvEndpoint() { + ResponseEntity response = this.restTemplate.getForEntity("/actuator/env", Map.class); + assertEquals(HttpStatus.UNAUTHORIZED, response.getStatusCode()); + assertTrue(response.hasBody()); + } + +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/README.adoc b/applications/apps-core/common/stream-apps-task-launch-request-common/README.adoc new file mode 100644 index 00000000..3e026aa9 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/README.adoc @@ -0,0 +1,52 @@ +=== `Data Flow Task Launch Request Support` Common Module + +This artifact contains a Spring Boot auto-configuration providing components to produce a Task Launch Request payload. +In theory any message source can be used to launch a Task. The task launch request is compatible with the +task-launcher-dataflow sink. + + +==== Data Flow Task Launch Request + +Data Flow Task Launch Request requests require a Data Flow server with an existing task definition. +The task launch request contains the name of the task to launch. This must the name of a defined task in Data Flow. +You may optionally provide command line arguments and deployment properties. + +===== Task Name + +The task name is a required field. This may be statically configured by setting `task.launch.request.task-name`, +extracted from the Message by setting `task.launch.request.task-name-expression`. +You may also override the default implementation of the `TaskNameMessageMapper` bean to enable more complex runtime task name mappings. + + +===== Task Command Line Arguments + +The launched task often requires additional data which may be passed as command line arguments. +The `task.launch.request.args` property accepts a comma delimited string of key-value pairs, for example +`key1=val1,key2=val2`. In addition, a `task.launch.request.arg-expressions` allows you to use SpEL expressions to evaluate +message contents to provide command line arguments. +For example, `task.launch.request.arg-expressions=foo=payload.toUpperCase(),bar=payload.substring(0,2)`. + +You may also provide override implementation of the `CommandLineArgumentsMessageMapper` bean to implement more complex logic. + +===== Task Deployment Properties + +Deployment properties are platform-specific configuration used by the `TaskLauncher` and are always statically configured by +setting `task.launch.request.deployment-properties` and apply to every task launch request. + +=== Configuration +To enable any stream app to transform its output to a Task Launch Request, include a dependency on this module + +[source,xml] +---- + + org.springframework.cloud.stream.app + app-starters-task-launch-request-common + +---- + +Setting the application property `spring.cloud.stream.function.definition=taskLaunchRequest` is required to execute the transformation. +You may safely add the above dependency to applications which optionally produce a task launch request. + + +`TaskLaunchRequestIntegrationTests` provides some configuration examples. + diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/pom.xml b/applications/apps-core/common/stream-apps-task-launch-request-common/pom.xml new file mode 100644 index 00000000..4e785243 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/pom.xml @@ -0,0 +1,45 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-task-launch-request-common + stream-apps-task-launch-request-common + + + + org.springframework.cloud.stream.app + stream-apps-file-common + ${project.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-stream + test-jar + test + test-binder + + + org.springframework.cloud + spring-cloud-stream + + + + org.springframework.cloud + spring-cloud-stream-test-support + test + + + + diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequest.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequest.java new file mode 100644 index 00000000..199106c7 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequest.java @@ -0,0 +1,64 @@ +/* + * Copyright 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.app.tasklaunchrequest; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DataFlowTaskLaunchRequest { + @JsonProperty("args") + private List commandlineArguments = new ArrayList<>(); + + @JsonProperty("deploymentProps") + private Map deploymentProperties = new HashMap<>(); + + @JsonProperty("name") + private String taskName; + + public void setCommandlineArguments(List commandlineArguments) { + this.commandlineArguments = new ArrayList<>(commandlineArguments); + } + + public List getCommandlineArguments() { + return this.commandlineArguments; + } + + public void setDeploymentProperties(Map deploymentProperties) { + this.deploymentProperties = deploymentProperties; + } + + public Map getDeploymentProperties() { + return this.deploymentProperties; + } + + public void setTaskName(String taskName) { + this.taskName = taskName; + } + + public String getTaskName() { + return this.taskName; + } + + public DataFlowTaskLaunchRequest addCommmandLineArguments(Collection args) { + this.commandlineArguments.addAll(args); + return this; + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequestAutoConfiguration.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequestAutoConfiguration.java new file mode 100644 index 00000000..06b72528 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataFlowTaskLaunchRequestAutoConfiguration.java @@ -0,0 +1,162 @@ +/* + * Copyright 2018-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.app.tasklaunchrequest; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import javax.annotation.PostConstruct; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.CommandLineArgumentsMessageMapper; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskLaunchRequestSupplier; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskNameMessageMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.expression.ExpressionUtils; +import org.springframework.messaging.Message; +import org.springframework.util.StringUtils; + + +/** + * @author David Turanski + **/ +@Configuration +@EnableConfigurationProperties(DataflowTaskLaunchRequestProperties.class) +public class DataFlowTaskLaunchRequestAutoConfiguration { + + @Autowired + private BeanFactory beanFactory; + + private EvaluationContext evaluationContext; + + public final static String TASK_LAUNCH_REQUEST_FUNCTION_NAME = "taskLaunchRequest"; + + /** + * A {@link java.util.function.Function} to transform a {@link Message} payload to a {@link DataFlowTaskLaunchRequest}. + * + * @param taskLaunchRequestMessageProcessor a {@link TaskLaunchRequestMessageProcessor}. + * + * @return a {code DataFlowTaskLaunchRequest} Message. + */ + @Bean(name = TASK_LAUNCH_REQUEST_FUNCTION_NAME) + @ConditionalOnMissingBean(TaskLaunchRequestFunction.class) + public TaskLaunchRequestFunction taskLaunchRequest(TaskLaunchRequestMessageProcessor taskLaunchRequestMessageProcessor) { + return message -> taskLaunchRequestMessageProcessor.postProcessMessage(message); + } + + @Bean + @ConditionalOnMissingBean(TaskNameMessageMapper.class) + public TaskNameMessageMapper taskNameMessageMapper(DataflowTaskLaunchRequestProperties taskLaunchRequestProperties) { + if (StringUtils.hasText(taskLaunchRequestProperties.getTaskNameExpression())) { + SpelExpressionParser expressionParser = new SpelExpressionParser(); + Expression taskNameExpression = expressionParser.parseExpression(taskLaunchRequestProperties.getTaskNameExpression()); + return new ExpressionEvaluatingTaskNameMessageMapper(taskNameExpression, this.evaluationContext); + } + + return message -> taskLaunchRequestProperties.getTaskName(); + } + + @Bean + @ConditionalOnMissingBean(CommandLineArgumentsMessageMapper.class) + public CommandLineArgumentsMessageMapper commandLineArgumentsMessageMapper( + DataflowTaskLaunchRequestProperties dataflowTaskLaunchRequestProperties){ + + return new ExpressionEvaluatingCommandLineArgsMapper(dataflowTaskLaunchRequestProperties.getArgExpressions(), + this.evaluationContext); + } + + @Bean + public TaskLaunchRequestSupplier taskLaunchRequestInitializer( + DataflowTaskLaunchRequestProperties taskLaunchRequestProperties){ + return new DataflowTaskLaunchRequestPropertiesInitializer(taskLaunchRequestProperties); + } + + @Bean + public TaskLaunchRequestMessageProcessor taskLaunchRequestMessageProcessor( + TaskLaunchRequestSupplier taskLaunchRequestInitializer, + TaskNameMessageMapper taskNameMessageMapper, + CommandLineArgumentsMessageMapper commandLineArgumentsMessageMapper) { + + return new TaskLaunchRequestMessageProcessor(taskLaunchRequestInitializer, + taskNameMessageMapper, + commandLineArgumentsMessageMapper); + } + + static class DataflowTaskLaunchRequestPropertiesInitializer extends TaskLaunchRequestSupplier { + DataflowTaskLaunchRequestPropertiesInitializer( + DataflowTaskLaunchRequestProperties taskLaunchRequestProperties){ + + this.commandLineArgumentSupplier( + () -> new ArrayList<>(taskLaunchRequestProperties.getArgs()) + ); + + this.deploymentPropertiesSupplier( + () -> KeyValueListParser.parseCommaDelimitedKeyValuePairs( + taskLaunchRequestProperties.getDeploymentProperties()) + ); + + this.taskNameSupplier(()->taskLaunchRequestProperties.getTaskName()); + } + } + + static class ExpressionEvaluatingCommandLineArgsMapper implements CommandLineArgumentsMessageMapper { + private final Map argExpressionsMap; + private final EvaluationContext evaluationContext; + + ExpressionEvaluatingCommandLineArgsMapper(String argExpressions, EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + this.argExpressionsMap = new HashMap<>(); + if (StringUtils.hasText(argExpressions)) { + SpelExpressionParser expressionParser = new SpelExpressionParser(); + + KeyValueListParser.parseCommaDelimitedKeyValuePairs(argExpressions).forEach( + (k,v)-> argExpressionsMap.put(k, expressionParser.parseExpression(v))); + } + + } + + @Override + public Collection processMessage(Message message) { + return evaluateArgExpressions(message); + } + + private Collection evaluateArgExpressions(Message message) { + List results = new LinkedList<>(); + this.argExpressionsMap.forEach((k, expression) -> + results.add(String.format("%s=%s", k, expression.getValue(this.evaluationContext, message)))); + return results; + } + } + + @PostConstruct + public void createEvaluationContext(){ + this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.beanFactory); + } + +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataflowTaskLaunchRequestProperties.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataflowTaskLaunchRequestProperties.java new file mode 100644 index 00000000..2003c530 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/DataflowTaskLaunchRequestProperties.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018-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.app.tasklaunchrequest; + +import java.util.ArrayList; +import java.util.List; +import javax.validation.constraints.AssertFalse; +import javax.validation.constraints.NotNull; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +/** + * Base Properties to create a {@link DataFlowTaskLaunchRequest}. + * + * @author Chris Schaefer + * @author David Turanski + */ +@Validated +@ConfigurationProperties("task.launch.request") +public class DataflowTaskLaunchRequestProperties { + + /** + * Comma separated list of optional args in key=value format. + */ + private List args = new ArrayList<>(); + + /** + * Comma separated list of option args as SpEL expressions in key=value format. + */ + private String argExpressions = ""; + + /** + * Comma delimited list of deployment properties to be applied to the + * TaskLaunchRequest. + */ + private String deploymentProperties = ""; + + /** + * The Data Flow task name. + */ + private String taskName; + + + /** + * A SpEL expression to extract the task name from each Message, using the Message as the evaluation context. + */ + private String taskNameExpression; + + @NotNull + public List getArgs() { + return this.args; + } + + public void setArgs(List args) { + this.args = new ArrayList<>(args); + } + + @NotNull + public String getDeploymentProperties() { + return this.deploymentProperties; + } + + public void setDeploymentProperties(String deploymentProperties) { + this.deploymentProperties = deploymentProperties; + } + + public String getTaskName() { + return taskName; + } + + public void setTaskName(String taskName) { + this.taskName = taskName; + } + + public String getTaskNameExpression() { + return taskNameExpression; + } + + public void setTaskNameExpression(String taskNameExpression) { + this.taskNameExpression = taskNameExpression; + } + + public String getArgExpressions() { + return argExpressions; + } + + public void setArgExpressions(String argExpressions) { + this.argExpressions = argExpressions; + } + + @AssertFalse(message = "Cannot specify both 'taskName' and 'taskNameExpression'.") + public boolean isTaskNameAndTaskNameExpressionSet() { + return StringUtils.hasText(this.taskName) && StringUtils.hasText(this.taskNameExpression); + } + +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/ExpressionEvaluatingTaskNameMessageMapper.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/ExpressionEvaluatingTaskNameMessageMapper.java new file mode 100644 index 00000000..b38d35e3 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/ExpressionEvaluatingTaskNameMessageMapper.java @@ -0,0 +1,38 @@ +/* + * Copyright 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.app.tasklaunchrequest; + +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskNameMessageMapper; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.messaging.Message; + +public class ExpressionEvaluatingTaskNameMessageMapper implements TaskNameMessageMapper { + + private final Expression expression; + private final EvaluationContext evaluationContext; + + public ExpressionEvaluatingTaskNameMessageMapper(Expression expression, EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + this.expression = expression; + } + + @Override + public String processMessage(Message message) { + return expression.getValue(evaluationContext, message).toString(); + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParser.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParser.java new file mode 100644 index 00000000..ee71ebba --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParser.java @@ -0,0 +1,66 @@ +/* + * Copyright 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.app.tasklaunchrequest; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.StringUtils; + +/** + * Parses a comma delimited list of key value pairs in which the values can contain commas as well. + * + * @author Chris Schaeffer + * @author David Turanski + **/ +abstract class KeyValueListParser { + + static Map parseCommaDelimitedKeyValuePairs(String value) { + Map keyValuePairs = new HashMap<>(); + + if (StringUtils.isEmpty(value)) { + return keyValuePairs; + } + + ArrayList pairs = new ArrayList<>(); + + String[] candidates = StringUtils.commaDelimitedListToStringArray(value); + + for (int i = 0; i < candidates.length; i++) { + if (i > 0 && !candidates[i].contains("=")) { + pairs.add(pairs.get(pairs.size() - 1) + "," + candidates[i]); + } + else { + pairs.add(candidates[i]); + } + } + + for (String pair : pairs) { + addKeyValuePair(pair, keyValuePairs); + } + + return keyValuePairs; + } + + private static void addKeyValuePair(String pair, Map properties) { + int firstEquals = pair.indexOf('='); + if (firstEquals != -1) { + properties.put(pair.substring(0, firstEquals).trim(), pair.substring(firstEquals + 1).trim()); + } + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestFunction.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestFunction.java new file mode 100644 index 00000000..611dc9c1 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestFunction.java @@ -0,0 +1,31 @@ +/* + * Copyright 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.app.tasklaunchrequest; + +import java.util.function.Function; + +import org.springframework.messaging.Message; + +/** + * A marker interface useful for unambiguous dependency injection of this Function. + * + * @author David Turanski + **/ +@FunctionalInterface +public interface TaskLaunchRequestFunction extends Function, Message> { + +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestMessageProcessor.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestMessageProcessor.java new file mode 100644 index 00000000..f53209c8 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestMessageProcessor.java @@ -0,0 +1,70 @@ +/* + * Copyright 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.app.tasklaunchrequest; + +import org.springframework.cloud.stream.app.tasklaunchrequest.support.CommandLineArgumentsMessageMapper; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskLaunchRequestSupplier; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskNameMessageMapper; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.core.MessagePostProcessor; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.Assert; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.StringUtils; + +public class TaskLaunchRequestMessageProcessor implements MessagePostProcessor { + + private final TaskNameMessageMapper taskNameMessageMapper; + private final CommandLineArgumentsMessageMapper commandLineArgumentsMessageMapper; + private final TaskLaunchRequestSupplier taskLaunchRequestInitializer; + + public TaskLaunchRequestMessageProcessor(TaskLaunchRequestSupplier taskLaunchRequestInitializer, + TaskNameMessageMapper taskNameMessageMapper, + CommandLineArgumentsMessageMapper commandLIneArgumentsMessageMapper) { + + this.taskLaunchRequestInitializer = taskLaunchRequestInitializer; + + this.taskNameMessageMapper = taskNameMessageMapper; + + this.commandLineArgumentsMessageMapper = commandLIneArgumentsMessageMapper; + + } + + @Override + public Message postProcessMessage(Message message) { + DataFlowTaskLaunchRequest taskLaunchRequest = taskLaunchRequestInitializer.get(); + + if (!StringUtils.hasText(taskLaunchRequest.getTaskName())) { + taskLaunchRequest.setTaskName(taskNameMessageMapper.processMessage(message)); + Assert.hasText(taskLaunchRequest.getTaskName(), ()-> + "'taskName' is required in " + DataFlowTaskLaunchRequest.class.getName()); + } + + taskLaunchRequest.addCommmandLineArguments(commandLineArgumentsMessageMapper.processMessage(message)); + + MessageBuilder builder + = MessageBuilder.withPayload(taskLaunchRequest).copyHeaders(message.getHeaders()); + return adjustHeaders(builder).build(); + } + + + private MessageBuilder adjustHeaders(MessageBuilder builder) { + builder.setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON); + return builder; + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/CommandLineArgumentsMessageMapper.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/CommandLineArgumentsMessageMapper.java new file mode 100644 index 00000000..12f4a5af --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/CommandLineArgumentsMessageMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 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.app.tasklaunchrequest.support; + +import java.util.Collection; +import org.springframework.integration.handler.MessageProcessor; + +public interface CommandLineArgumentsMessageMapper extends MessageProcessor> { +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskLaunchRequestSupplier.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskLaunchRequestSupplier.java new file mode 100644 index 00000000..9f8a3bb2 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskLaunchRequestSupplier.java @@ -0,0 +1,65 @@ +/* + * Copyright 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.app.tasklaunchrequest.support; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; +import org.springframework.cloud.stream.app.tasklaunchrequest.DataFlowTaskLaunchRequest; +import org.springframework.util.Assert; + +public class TaskLaunchRequestSupplier implements Supplier { + + private Supplier taskNameSupplier; + private Supplier> commandLineArgumentsSupplier; + private Supplier> deploymentPropertiesSupplier; + + + public TaskLaunchRequestSupplier taskNameSupplier(Supplier taskNameSupplier) { + this.taskNameSupplier = taskNameSupplier; + return this; + } + + public TaskLaunchRequestSupplier commandLineArgumentSupplier(Supplier> commandLineArgumentsSupplier) { + this.commandLineArgumentsSupplier = commandLineArgumentsSupplier; + return this; + } + + public TaskLaunchRequestSupplier deploymentPropertiesSupplier(Supplier> deploymentPropertiesSupplier) { + this.deploymentPropertiesSupplier = deploymentPropertiesSupplier; + return this; + } + + @Override + public DataFlowTaskLaunchRequest get() { + + Assert.notNull(this.taskNameSupplier, "'taskNameSupplier' is required."); + + DataFlowTaskLaunchRequest dataFlowTaskLaunchRequest = new DataFlowTaskLaunchRequest(); + dataFlowTaskLaunchRequest.setTaskName(this.taskNameSupplier.get()); + + if (this.commandLineArgumentsSupplier != null) { + dataFlowTaskLaunchRequest.setCommandlineArguments(this.commandLineArgumentsSupplier.get()); + } + + if (this.deploymentPropertiesSupplier != null) { + dataFlowTaskLaunchRequest.setDeploymentProperties(this.deploymentPropertiesSupplier.get()); + } + + return dataFlowTaskLaunchRequest; + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskNameMessageMapper.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskNameMessageMapper.java new file mode 100644 index 00000000..0997a72b --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/java/org/springframework/cloud/stream/app/tasklaunchrequest/support/TaskNameMessageMapper.java @@ -0,0 +1,23 @@ +/* + * Copyright 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.app.tasklaunchrequest.support; + +import org.springframework.integration.handler.MessageProcessor; + +@FunctionalInterface +public interface TaskNameMessageMapper extends MessageProcessor { +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring-configuration-metadata-whitelist.properties b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..0fbbde32 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring-configuration-metadata-whitelist.properties @@ -0,0 +1,2 @@ +configuration-properties.classes=\ + org.springframework.cloud.stream.app.tasklaunchrequest.DataflowTaskLaunchRequestProperties diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring.factories b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..3e0260f2 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + org.springframework.cloud.stream.app.tasklaunchrequest.DataFlowTaskLaunchRequestAutoConfiguration diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParserTests.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParserTests.java new file mode 100644 index 00000000..abb59d0a --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/KeyValueListParserTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2018 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.tasklaunchrequest; + +import java.util.Map; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +/** + * @author Chris Schaefer + * @author David Turanski + */ +public class KeyValueListParserTests { + + @Test + public void testParseSimpleDeploymentProperty() { + Map deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs( + "app.sftp.param=value"); + assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(), + deploymentProperties.size() == 1); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.param")); + assertEquals("Invalid deployment value", "value", deploymentProperties.get("app.sftp.param")); + } + + @Test + public void testParseSimpleDeploymentPropertyMultipleValues() { + Map deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs( + "app.sftp.param=value1,value2,value3"); + + assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(), + deploymentProperties.size() == 1); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.param")); + assertEquals("Invalid deployment value", "value1,value2,value3", deploymentProperties.get("app.sftp.param")); + } + + @Test + public void testParseSpelExpressionMultipleValues() { + Map argExpressions = KeyValueListParser.parseCommaDelimitedKeyValuePairs( + "arg1=payload.substr(0,2),arg2=headers['foo'],arg3=headers['bar']==false"); + + assertTrue("Invalid number of deployment properties: " + argExpressions.size(), + argExpressions.size() == 3); + assertTrue("Expected deployment key not found", argExpressions.containsKey("arg1")); + assertEquals("Invalid deployment value", "payload.substr(0,2)", argExpressions.get("arg1")); + + assertTrue("Expected deployment key not found", argExpressions.containsKey("arg2")); + assertEquals("Invalid deployment value", "headers['foo']", argExpressions.get("arg2")); + + assertTrue("Expected deployment key not found", argExpressions.containsKey("arg3")); + assertEquals("Invalid deployment value", "headers['bar']==false", argExpressions.get("arg3")); + } + + @Test + public void testParseMultipleDeploymentPropertiesSingleValue() { + Map deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs( + "app.sftp.param=value1,app.sftp.other.param=value2"); + + assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(), + deploymentProperties.size() == 2); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.param")); + assertEquals("Invalid deployment value", "value1", deploymentProperties.get("app.sftp.param")); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.other.param")); + assertEquals("Invalid deployment value", "value2", deploymentProperties.get("app.sftp.other.param")); + } + + @Test + public void testParseMultipleDeploymentPropertiesMultipleValues() { + DataflowTaskLaunchRequestProperties taskLaunchRequestProperties = new DataflowTaskLaunchRequestProperties(); + + Map deploymentProperties = KeyValueListParser.parseCommaDelimitedKeyValuePairs( + "app.sftp.param=value1,value2,app.sftp.other.param=other1,other2"); + + assertTrue("Invalid number of deployment properties: " + deploymentProperties.size(), + deploymentProperties.size() == 2); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.param")); + assertEquals("Invalid deployment value", "value1,value2", deploymentProperties.get("app.sftp.param")); + assertTrue("Expected deployment key not found", deploymentProperties.containsKey("app.sftp.other.param")); + assertEquals("Invalid deployment value", "other1,other2", deploymentProperties.get("app.sftp.other.param")); + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestIntegrationTests.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestIntegrationTests.java new file mode 100644 index 00000000..0aaaa167 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestIntegrationTests.java @@ -0,0 +1,313 @@ +/* + * Copyright 2018 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.tasklaunchrequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +import java.io.IOException; +import java.util.Collections; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.stream.annotation.EnableBinding; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.CommandLineArgumentsMessageMapper; +import org.springframework.cloud.stream.app.tasklaunchrequest.support.TaskNameMessageMapper; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; +import org.springframework.cloud.stream.messaging.Processor; +import org.springframework.cloud.stream.test.binder.MessageCollectorAutoConfiguration; +import org.springframework.cloud.stream.test.binder.TestSupportBinderAutoConfiguration; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; +import org.springframework.messaging.support.MessageBuilder; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author David Turanski + **/ +public class TaskLaunchRequestIntegrationTests { + + private ApplicationContextRunner applicationContextRunner; + + @Before + public void setUp() { + applicationContextRunner = + new ApplicationContextRunner().withUserConfiguration(TestChannelBinderConfiguration.class, TestApp.class); + } + + @Test + public void noTaskLaunchRequestPropertiesAreRequired() { + + applicationContextRunner.withPropertyValues("spring.jmx.enabled=false") + .run(context -> { + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + Message message = + MessageBuilder.withPayload("hello".getBytes()).build(); + input.send(message); + + Message response = target.receive(1000); + assertThat(response.getPayload()).isEqualTo(message.getPayload()); + }); + } + + @Test + public void simpleDataflowTaskLaunchRequest() { + + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", + "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name=foo") + .run(context -> { + DataFlowTaskLaunchRequest dataFlowTaskLaunchRequest = verifyAndreceiveDataFlowTaskLaunchRequest(context); + + assertThat(dataFlowTaskLaunchRequest.getTaskName()).isEqualTo("foo"); + assertThat(dataFlowTaskLaunchRequest.getCommandlineArguments()).hasSize(0); + assertThat(dataFlowTaskLaunchRequest.getDeploymentProperties()).hasSize(0); + }); + } + + @Test + public void dataflowTaskLaunchRequestWithArgsAndDeploymentProperties() { + + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name=foo", "task.launch.request.args=foo=bar,baz=boo", + "task.launch.request.deploymentProperties=count=3") + .run(context -> { + DataFlowTaskLaunchRequest dataFlowTaskLaunchRequest = verifyAndreceiveDataFlowTaskLaunchRequest(context); + + assertThat(dataFlowTaskLaunchRequest.getTaskName()).isEqualTo("foo"); + assertThat(dataFlowTaskLaunchRequest.getCommandlineArguments()).containsExactlyInAnyOrder("foo=bar", + "baz=boo"); + assertThat(dataFlowTaskLaunchRequest.getDeploymentProperties()).containsOnly(entry("count", "3")); + }); + } + + @Test + public void dataflowTaskLaunchRequestWithCommandLineArgsMessageMapper() { + + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name=foo", "enhanceTLRArgs=true") + .run(context -> { + + DataFlowTaskLaunchRequest dataFlowTaskLaunchRequest = verifyAndreceiveDataFlowTaskLaunchRequest(context); + + assertThat(dataFlowTaskLaunchRequest.getTaskName()).isEqualTo("foo"); + assertThat(dataFlowTaskLaunchRequest.getCommandlineArguments()).hasSize(1); + assertThat(dataFlowTaskLaunchRequest.getCommandlineArguments()).containsExactly("runtimeArg"); + }); + } + + @Test + public void taskLaunchRequestWithArgExpressions() { + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", + "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name=foo", + "task.launch.request.arg-expressions=foo=payload.toUpperCase(),bar=payload.substring(0,2)") + .run(context -> { + + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + Message message = MessageBuilder.withPayload("hello").build(); + + input.send(message); + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + Message response = target.receive(1000); + + assertThat(response).isNotNull(); + + DataFlowTaskLaunchRequest request = objectMapper.readValue(response.getPayload(), + DataFlowTaskLaunchRequest.class); + + assertThat(request.getCommandlineArguments()).containsExactlyInAnyOrder("foo=HELLO", "bar=he"); + + }); + } + + @Test + public void taskLaunchRequestWithIntPayload() { + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name=foo", + "task.launch.request.arg-expressions=i=payload") + .run(context -> { + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + Message message = + MessageBuilder.withPayload(123).build(); + + input.send(message); + + Message response = target.receive(1000); + + assertThat(response).isNotNull(); + + DataFlowTaskLaunchRequest request = objectMapper.readValue(response.getPayload(), + DataFlowTaskLaunchRequest.class); + + assertThat(request.getCommandlineArguments()).containsExactly("i=123"); + + }); + } + + @Test + public void taskNameExpression() { + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "task.launch.request.task-name-expression=payload+'_task'") + .run(context -> { + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + Message message = MessageBuilder.withPayload("foo").build(); + input.send(message); + + Message response = target.receive(1000); + assertThat(response).isNotNull(); + + DataFlowTaskLaunchRequest request = objectMapper.readValue(response.getPayload(), + DataFlowTaskLaunchRequest.class); + + assertThat(request.getTaskName()).isEqualTo("foo_task"); + }); + } + + @Test + public void customTaskNameExtractor() { + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "customTaskNameExtractor=true") + .run(context -> { + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + Message message = MessageBuilder.withPayload("foo").build(); + input.send(message); + + Message response = target.receive(1000); + assertThat(response).isNotNull(); + + DataFlowTaskLaunchRequest request = objectMapper.readValue(response.getPayload(), + DataFlowTaskLaunchRequest.class); + + assertThat(request.getTaskName()).isEqualTo("fooTask"); + }); + //TODO: Workaround for https://github.com/spring-cloud/spring-cloud-stream/issues/1876 + applicationContextRunner.withPropertyValues( + "spring.jmx.enabled=false", "spring.cloud.stream.function.definition=taskLaunchRequest", + "customTaskNameExtractor=true") + .run(context -> { + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + Message message = MessageBuilder.withPayload("bar").build(); + input.send(message); + + Message response = target.receive(1000); + assertThat(response).isNotNull(); + + DataFlowTaskLaunchRequest request = objectMapper.readValue(response.getPayload(), + DataFlowTaskLaunchRequest.class); + + assertThat(request.getTaskName()).isEqualTo("defaultTask"); + }); + } + + private DataFlowTaskLaunchRequest verifyAndreceiveDataFlowTaskLaunchRequest(ApplicationContext context) + throws IOException { + MessageChannel input = context.getBean("input", MessageChannel.class); + + OutputDestination target = context.getBean(OutputDestination.class); + + MessageBuilder builder = MessageBuilder.withPayload(new byte[] {}); + + ObjectMapper objectMapper = context.getBean(ObjectMapper.class); + + input.send(builder.build()); + + Message message = target.receive(1000); + + assertThat(message).isNotNull(); + + return objectMapper.readValue(message.getPayload(), + DataFlowTaskLaunchRequest.class); + } + + @EnableAutoConfiguration(exclude = { TestSupportBinderAutoConfiguration.class, + MessageCollectorAutoConfiguration.class }) + @EnableBinding(Processor.class) + static class TestApp { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + @ConditionalOnProperty("customTaskNameExtractor") + TaskNameMessageMapper taskNameExtractor() { + return message -> ((String)(message.getPayload())).equalsIgnoreCase("foo") ? + "fooTask" : + "defaultTask"; + } + + @Bean + @ConditionalOnProperty("enhanceTLRArgs") + CommandLineArgumentsMessageMapper commandLineArgumentsProvider(){ + return message -> Collections.singletonList("runtimeArg"); + } + + @Bean + public IntegrationFlow flow() { + + return IntegrationFlows.from(Processor.INPUT) + .channel(Processor.OUTPUT).get(); + } + } +} diff --git a/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestPropertiesTests.java b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestPropertiesTests.java new file mode 100644 index 00000000..0753e5e7 --- /dev/null +++ b/applications/apps-core/common/stream-apps-task-launch-request-common/src/test/java/org/springframework/cloud/stream/app/tasklaunchrequest/TaskLaunchRequestPropertiesTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018-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.app.tasklaunchrequest; + +import java.util.List; + +import org.junit.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.integration.config.EnableIntegration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author David Turanski + **/ +public class TaskLaunchRequestPropertiesTests { + + @Test + public void deploymentPropertiesCanBeCustomized() { + DataflowTaskLaunchRequestProperties properties = getBatchProperties( + "task.launch.request.deploymentProperties:prop1=val1,prop2=val2"); + assertThat(properties.getDeploymentProperties()).isEqualTo("prop1=val1,prop2=val2"); + } + + @Test + public void parametersCanBeCustomized() { + DataflowTaskLaunchRequestProperties properties = getBatchProperties( + "task.launch.request.args:jp1=jpv1,jp2=jpv2"); + List args = properties.getArgs(); + + assertThat(args).isNotNull(); + assertThat(args).hasSize(2); + assertThat(args.get(0)).isEqualTo("jp1=jpv1"); + assertThat(args.get(1)).isEqualTo("jp2=jpv2"); + } + + private DataflowTaskLaunchRequestProperties getBatchProperties(String... var) { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + + if (var != null) { + TestPropertyValues.of(var).applyTo(context); + } + + context.register(Conf.class); + context.refresh(); + + return context.getBean(DataflowTaskLaunchRequestProperties.class); + } + + + @Configuration + @EnableIntegration + @EnableConfigurationProperties(DataflowTaskLaunchRequestProperties.class) + @Import(DataFlowTaskLaunchRequestAutoConfiguration.class) + static class Conf { + + } +} diff --git a/applications/apps-core/common/stream-apps-test-support/pom.xml b/applications/apps-core/common/stream-apps-test-support/pom.xml new file mode 100644 index 00000000..975777a2 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/pom.xml @@ -0,0 +1,29 @@ + + + + stream-apps-common + org.springframework.cloud.stream.app + 3.0.0.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-test-support + + + + org.springframework.integration + spring-integration-test + + + org.springframework.integration + spring-integration-test-support + + + org.springframework.boot + spring-boot-starter-data-redis + true + + + + + diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/BinderTestPropertiesInitializer.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/BinderTestPropertiesInitializer.java new file mode 100644 index 00000000..e49ca6e8 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/BinderTestPropertiesInitializer.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016 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.test; + +import java.util.Properties; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.PropertiesPropertySource; + +/** + * Unlike the {@code PropertiesInitializer}, this does not require boot infrastructure + * to add properties to the context. Used for testing generated apps where the + * {@code ApplicationContextInitializer} can't be used. Since it's a BDRPP, it runs + * before any BFPPs - i.e. as early as possible. + * + * @author Gary Russell + * + */ +public class BinderTestPropertiesInitializer implements BeanDefinitionRegistryPostProcessor { + + private final ConfigurableApplicationContext context; + + private final Properties properties; + + public BinderTestPropertiesInitializer(ConfigurableApplicationContext context, Properties properties) { + this.context = context; + this.properties = properties; + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + } + + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + this.context.getEnvironment().getPropertySources() + .addLast(new PropertiesPropertySource("scsAppProperties", properties)); + } + +} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/PropertiesInitializer.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/PropertiesInitializer.java new file mode 100644 index 00000000..145adc5d --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/PropertiesInitializer.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014-2016 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.test; + +import java.util.Properties; + +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.PropertiesPropertySource; + +/** + * @author David Turanski + */ +public class PropertiesInitializer implements ApplicationContextInitializer { + + public static Properties PROPERTIES; + + @Override + public void initialize(ConfigurableApplicationContext configurableApplicationContext) { + configurableApplicationContext.getEnvironment().getPropertySources().addLast(new + PropertiesPropertySource("applicationOptions", PROPERTIES)); + } +} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/file/remote/RemoteFileTestSupport.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/file/remote/RemoteFileTestSupport.java new file mode 100644 index 00000000..dad41cd9 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/file/remote/RemoteFileTestSupport.java @@ -0,0 +1,149 @@ +/* + * Copyright 2015-2016 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.test.file.remote; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.rules.TemporaryFolder; + +/** + * Abstract base class for tests requiring remote file servers, e.g. (S)FTP. + * + * @author Gary Russell + * + */ +public abstract class RemoteFileTestSupport { + + protected static final int port = 0; + + @ClassRule + public static final TemporaryFolder remoteTemporaryFolder = new TemporaryFolder(); + + @ClassRule + public static final TemporaryFolder localTemporaryFolder = new TemporaryFolder(); + + protected volatile File sourceRemoteDirectory; + + protected volatile File targetRemoteDirectory; + + protected volatile File sourceLocalDirectory; + + protected volatile File targetLocalDirectory; + + public File getSourceRemoteDirectory() { + return sourceRemoteDirectory; + } + + public File getTargetRemoteDirectory() { + return targetRemoteDirectory; + } + + public File getSourceLocalDirectory() { + return sourceLocalDirectory; + } + + public File getTargetLocalDirectory() { + return targetLocalDirectory; + } + + /** + * Default implementation creates the following folder structures: + * + *
+	 *  $ tree remoteSource/
+	 *  remoteSource/
+	 *  ├── remoteSource1.txt - contains 'source1'
+	 *  ├── remoteSource2.txt - contains 'source2'
+	 *  remoteTarget/
+	 *  $ tree localSource/
+	 *  localSource/
+	 *  ├── localSource1.txt - contains 'local1'
+	 *  ├── localSource2.txt - contains 'local2'
+	 *  localTarget/
+	 * 
+ * + * The intent is tests retrieve from remoteSource and verify arrival in localTarget or send from localSource and verify + * arrival in remoteTarget. + *

+ * Subclasses can change 'remote' in these names by overriding {@link #prefix()} or override this method completely to + * create a different structure. + *

+ * While a single server exists for all tests, the directory structure is rebuilt for each test. + * @throws IOException IO Exception. + */ + @Before + public void setupFolders() throws IOException { + String prefix = prefix(); + recursiveDelete(new File(remoteTemporaryFolder.getRoot(), prefix + "Source")); + this.sourceRemoteDirectory = remoteTemporaryFolder.newFolder(prefix + "Source"); + recursiveDelete(new File(remoteTemporaryFolder.getRoot(), prefix + "Target")); + this.targetRemoteDirectory = remoteTemporaryFolder.newFolder(prefix + "Target"); + recursiveDelete(new File(localTemporaryFolder.getRoot(), "localSource")); + this.sourceLocalDirectory = localTemporaryFolder.newFolder("localSource"); + recursiveDelete(new File(localTemporaryFolder.getRoot(), "localTarget")); + this.targetLocalDirectory = localTemporaryFolder.newFolder("localTarget"); + File file = new File(sourceRemoteDirectory, prefix + "Source1.txt"); + file.createNewFile(); + FileOutputStream fos = new FileOutputStream(file); + fos.write("source1".getBytes()); + fos.close(); + file = new File(sourceRemoteDirectory, prefix + "Source2.txt"); + file.createNewFile(); + fos = new FileOutputStream(file); + fos.write("source2".getBytes()); + fos.close(); + file = new File(sourceLocalDirectory, "localSource1.txt"); + file.createNewFile(); + fos = new FileOutputStream(file); + fos.write("local1".getBytes()); + fos.close(); + file = new File(sourceLocalDirectory, "localSource2.txt"); + file.createNewFile(); + fos = new FileOutputStream(file); + fos.write("local2".getBytes()); + fos.close(); + } + + public void recursiveDelete(File file) { + if (file != null && file.exists()) { + File[] files = file.listFiles(); + if (files != null) { + for (File fyle : files) { + if (fyle.isDirectory()) { + recursiveDelete(fyle); + } + else { + fyle.delete(); + } + } + } + file.delete(); + } + } + + /** + * Prefix for directory/file structure; default 'remote'. + * @return the prefix. + */ + protected String prefix() { + return "remote"; + } + +} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSinkTestConfiguration.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSinkTestConfiguration.java new file mode 100644 index 00000000..e1de6007 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSinkTestConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.test.ip; + +import java.util.Properties; + +import org.springframework.cloud.stream.app.test.BinderTestPropertiesInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Generated app test configuration for the IP (TCP) sink. + * + * @author Gary Russell + * + */ +@Configuration +public class IpSinkTestConfiguration { + + @Bean + public static BinderTestPropertiesInitializer loadProps(ConfigurableApplicationContext context) { + // minimal properties for the context to load + Properties properties = new Properties(); + properties.put("host", "localhost"); + return new BinderTestPropertiesInitializer(context, properties); + } + +} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSourceTestConfiguration.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSourceTestConfiguration.java new file mode 100644 index 00000000..babe0127 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/ip/IpSourceTestConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016 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.test.ip; + +import java.util.Properties; + +import org.springframework.cloud.stream.app.test.BinderTestPropertiesInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Generated app test configuration for IP (TCP, UDP) sources. + * + * @author Gary Russell + * + */ +@Configuration +public class IpSourceTestConfiguration { + + @Bean + public static BinderTestPropertiesInitializer loadProps(ConfigurableApplicationContext context) { + // minimal properties for the context to load + Properties properties = new Properties(); + properties.put("port", 0); + return new BinderTestPropertiesInitializer(context, properties); + } + +} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/redis/RedisTestSupport.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/redis/RedisTestSupport.java new file mode 100644 index 00000000..321d9499 --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/redis/RedisTestSupport.java @@ -0,0 +1,43 @@ +///* +// * Copyright 2016 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.test.redis; +// +//import org.springframework.cloud.stream.test.junit.AbstractExternalResourceTestSupport; +//import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +// +///** +// * Porting from https://github.com/spring-cloud/spring-cloud-stream/blob/1.0.x/spring-cloud-stream-test-support-internal +// * +// * @author Soby Chacko +// */ +//public class RedisTestSupport extends AbstractExternalResourceTestSupport { +// +// public RedisTestSupport() { +// super("REDIS"); +// } +// @Override +// protected void cleanupResource() throws Exception { +// resource.destroy(); +// } +// +// @Override +// protected void obtainResource() throws Exception { +// resource = new LettuceConnectionFactory(); +// resource.afterPropertiesSet(); +// resource.getConnection().close(); +// } +//} diff --git a/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/script/ScriptableTestConfiguration.java b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/script/ScriptableTestConfiguration.java new file mode 100644 index 00000000..c677dc5d --- /dev/null +++ b/applications/apps-core/common/stream-apps-test-support/src/main/java/org/springframework/cloud/stream/app/test/script/ScriptableTestConfiguration.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016 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.test.script; + +import java.util.Properties; + +import org.springframework.cloud.stream.app.test.BinderTestPropertiesInitializer; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Test configuration for generated scriptable apps. + * + * @author Gary Russell + * + */ +@Configuration +public class ScriptableTestConfiguration { + + @Bean + public BinderTestPropertiesInitializer loadProps(ConfigurableApplicationContext context) { + // minimal properties for the context to load + Properties properties = new Properties(); + properties.put("script", "foo"); + properties.put("language", "ruby"); + return new BinderTestPropertiesInitializer(context, properties); + } + +} \ No newline at end of file diff --git a/applications/apps-core/pom.xml b/applications/apps-core/pom.xml new file mode 100644 index 00000000..37f50ab8 --- /dev/null +++ b/applications/apps-core/pom.xml @@ -0,0 +1,348 @@ + + + 4.0.0 + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + stream-apps-parent + Infrastructure for stream apps + pom + + + org.springframework.boot + spring-boot-starter-parent + 2.3.0.M4 + + + + 1.8 + 3.0.0.M2 + 2.3.0.M4 + 2.0.1.M1 + 2.0.0.M2 + 3.0.0.M1 + 3.0.2.RELEASE + 3.0.2.RELEASE + Hoxton.SR2 + Horsham.SR2 + 2.1.1.RELEASE + 1.0.0.BUILD-SNAPSHOT + 0.9.0 + + + + common + + + + + + org.springframework.cloud + spring-cloud-stream-dependencies + ${spring-cloud-stream-dependencies.version} + pom + import + + + + + + + org.springframework.cloud + spring-cloud-stream + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud + spring-cloud-stream + test-jar + test-binder + test + + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + Copyright 2014-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 + + http://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. + + + + scm:git:git://github.com/pivotal/java-functions.git + scm:git:ssh://git@github.com/pivotal/java-functions.git + https://github.com/pivotal/java-functions + + + + + repo.spring.io + Spring Release Repository + https://repo.spring.io/libs-release-local + + + repo.spring.io + Spring Snapshot Repository + https://repo.spring.io/libs-snapshot-local + + + + + + milestone + + + repo.spring.io + Spring Milestone Repository + https://repo.spring.io/libs-milestone-local + + + + + central + + + + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven-surefire-plugin.version} + + + **/*Tests.java + **/*Test.java + + + **/Abstract*.java + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + ${stream-apps-docs-plugin.version} + + + generate-documentation + verify + + generate-documentation + + + + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + ${scst-app-maven-plugin.version} + + + app-gen + package + + generate-app + + + + + ${basedir}/apps + 1.8 + ${spring-boot.version} + + + kafka + rabbit + + + + org.springframework.cloud + spring-cloud-stream-dependencies + ${spring-cloud-stream-dependencies.version} + + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud-function-dependencies.version} + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud-dependencies.version} + + + + + org.springframework.cloud.stream.app + stream-apps-security-common + ${stream-apps-core.version} + + + org.springframework.cloud.stream.app + stream-apps-micrometer-common + ${stream-apps-core.version} + + + io.micrometer + micrometer-registry-influx + + + io.micrometer + micrometer-registry-prometheus + + + io.micrometer.prometheus + prometheus-rsocket-spring + ${prometheus-rsocket.version} + + + io.micrometer + micrometer-registry-datadog + + + io.pivotal.cfenv + java-cfenv-boot + ${java-cfenv-boot.version} + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-logging + + + org.springframework.boot + spring-boot-starter-security + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + false + + spring-releases + Spring Releases + https://repo.spring.io/release + + + + false + + spring-libs-release + Spring Libs Release + https://repo.spring.io/libs-release + + + + false + + spring-milestone-release + Spring Milestone Release + https://repo.spring.io/libs-milestone + + + + + spring-releases + Spring Releases + https://repo.spring.io/libs-release + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/apps-metadata/CODE_OF_CONDUCT.adoc b/applications/apps-metadata/CODE_OF_CONDUCT.adoc new file mode 100644 index 00000000..17783c7c --- /dev/null +++ b/applications/apps-metadata/CODE_OF_CONDUCT.adoc @@ -0,0 +1,44 @@ += Contributor Code of Conduct + +As contributors and maintainers of this project, and in the interest of fostering an open +and welcoming community, we pledge to respect all people who contribute through reporting +issues, posting feature requests, updating documentation, submitting pull requests or +patches, and other activities. + +We are committed to making participation in this project a harassment-free experience for +everyone, regardless of level of experience, gender, gender identity and expression, +sexual orientation, disability, personal appearance, body size, race, ethnicity, age, +religion, or nationality. + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery +* Personal attacks +* Trolling or insulting/derogatory comments +* Public or private harassment +* Publishing other's private information, such as physical or electronic addresses, + without explicit permission +* Other unethical or unprofessional conduct + +Project maintainers have the right and responsibility to remove, edit, or reject comments, +commits, code, wiki edits, issues, and other contributions that are not aligned to this +Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors +that they deem inappropriate, threatening, offensive, or harmful. + +By adopting this Code of Conduct, project maintainers commit themselves to fairly and +consistently applying these principles to every aspect of managing this project. Project +maintainers who do not follow or enforce the Code of Conduct may be permanently removed +from the project team. + +This Code of Conduct applies both within project spaces and in public spaces when an +individual is representing the project or its community. + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by +contacting a project maintainer at spring-code-of-conduct@pivotal.io . All complaints will +be reviewed and investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. Maintainers are obligated to maintain confidentiality +with regard to the reporter of an incident. + +This Code of Conduct is adapted from the +https://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at +https://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/] diff --git a/applications/apps-metadata/LICENSE b/applications/apps-metadata/LICENSE new file mode 100644 index 00000000..9b259bdf --- /dev/null +++ b/applications/apps-metadata/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + 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. diff --git a/applications/apps-metadata/pom.xml b/applications/apps-metadata/pom.xml new file mode 100644 index 00000000..68a2325a --- /dev/null +++ b/applications/apps-metadata/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + apps-metadata + Fahrenheit.BUILD-SNAPSHOT + pom + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + stream-apps-docs + stream-apps-descriptor + + + + + spring + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + spring-releases + Spring Releases + https://repo.spring.io/release + + false + + + + spring-libs-release + Spring Libs Release + https://repo.spring.io/libs-release + + false + + + + + false + + spring-milestone-release + Spring Milestone Release + https://repo.spring.io/libs-milestone + + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + true + + + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + false + + + + + + + diff --git a/applications/apps-metadata/release-tools/core-tag-next-version.sh b/applications/apps-metadata/release-tools/core-tag-next-version.sh new file mode 100755 index 00000000..6f9a086a --- /dev/null +++ b/applications/apps-metadata/release-tools/core-tag-next-version.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# The script takes two arguments - currently released version for tagging and next version + +if [ "$#" -ne 2 ]; then + echo "Please specify the released version and the next version" + exit +fi + +pushd /tmp + +git clone git@github.com:spring-cloud-stream-app-starters/core.git +cd core + +git tag v$1 +git push origin v$1 + +./mvnw versions:set -DnewVersion=$2 -DgenerateBackupPoms=false +./mvnw versions:set -DnewVersion=$2 -DgenerateBackupPoms=false -pl :app-starters-core-dependencies + +sed -i '' 's/.*/'"$2"'<\/app-starters-core-dependencies.version>/g' pom.xml + +git commit -am"Next version - $2" +git push origin master + +cd .. +rm -rf core + +popd diff --git a/applications/apps-metadata/release-tools/core-version-check.sh b/applications/apps-metadata/release-tools/core-version-check.sh new file mode 100755 index 00000000..7c883647 --- /dev/null +++ b/applications/apps-metadata/release-tools/core-version-check.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +# The script takes one argument - release version + +if [ "$#" -ne 1 ]; then + echo "Please specify the release version" + exit +fi + +pushd /tmp + +git clone git@github.com:spring-cloud-stream-app-starters/core.git +cd core + +./mvnw versions:set -DnewVersion=$1 -DgenerateBackupPoms=false -U +./mvnw versions:set -DnewVersion=$1 -DgenerateBackupPoms=false -pl :app-starters-core-dependencies -U + +sed -i '' 's/.*/'"$1"'<\/app-starters-core-dependencies.version>/g' pom.xml + +snapshotlines=$(find . -type f -name pom.xml | xargs grep SNAPSHOT | wc -l) +rclines=$(find . -type f -name pom.xml | xargs grep .RC | wc -l) +milestonelines=$(find . -type f -name pom.xml | xargs grep version | grep .M | wc -l) + +if [ $snapshotlines -eq 0 ] && [ $rclines -eq 0 ] && [$milestonelines -eq 0 ]; then + echo "All clear" +else + echo "Snapshots found." + find . -type f -name pom.xml | xargs grep SNAPSHOT + echo "SNAPSHOTS: " $snapshotlines + exit 1 +fi + +cd .. +rm -rf core + +popd + + diff --git a/applications/apps-metadata/release-tools/core-version-upgrade.sh b/applications/apps-metadata/release-tools/core-version-upgrade.sh new file mode 100755 index 00000000..60c4a0a3 --- /dev/null +++ b/applications/apps-metadata/release-tools/core-version-upgrade.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# The script takes one argument - release version + +if [ "$#" -ne 1 ]; then + echo "Please specify the release version" + exit +fi + +pushd /tmp + +git clone git@github.com:spring-cloud-stream-app-starters/core.git +cd core + +./mvnw versions:set -DnewVersion=$1 -DgenerateBackupPoms=false -U +./mvnw versions:set -DnewVersion=$1 -DgenerateBackupPoms=false -pl :app-starters-core-dependencies -U + +sed -i '' 's/.*/'"$1"'<\/app-starters-core-dependencies.version>/g' pom.xml + +snapshotlines=$(find . -type f -name pom.xml | xargs grep SNAPSHOT | wc -l) +rclines=$(find . -type f -name pom.xml | xargs grep .RC | wc -l) +milestonelines=$(find . -type f -name pom.xml | xargs grep version | grep .M | wc -l) + +if [ $snapshotlines -eq 0 ] && [ $rclines -eq 0 ] && [$milestonelines -eq 0 ]; then + echo "All clear" + git commit -am"Update version to $1" + git push origin master +else + echo "Snapshots found." + echo "SNAPSHOTS: " $snapshotlines + echo "Milestones: " $milestonelines + echo "RC: " $rclines + exit 1 +fi + +cd .. +rm -rf core + +popd diff --git a/applications/apps-metadata/stream-apps-descriptor/pom.xml b/applications/apps-metadata/stream-apps-descriptor/pom.xml new file mode 100644 index 00000000..2206d721 --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/pom.xml @@ -0,0 +1,140 @@ + + + + stream-apps-release-train + org.springframework.cloud.stream.app + Fahrenheit.BUILD-SNAPSHOT + + 4.0.0 + + stream-apps-descriptor + stream-apps-descriptor + jar + + + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + 3.0.0.BUILD-SNAPSHOT + + + + + + src/main/resources + true + + META-INF/kafka-apps-maven.properties + META-INF/rabbit-apps-maven.properties + META-INF/kafka-apps-docker.properties + META-INF/rabbit-apps-docker.properties + META-INF/kafka-apps-maven-repo-url.properties + META-INF/rabbit-apps-maven-repo-url.properties + + + + + + + org.codehaus.gmaven + gmaven-plugin + 1.5 + + + validate + + execute + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.8 + + + attach-artifacts + package + + attach-artifact + + + + + target/classes/META-INF/kafka-apps-maven.properties + stream-apps-kafka-maven + + + target/classes/META-INF/kafka-apps-docker.properties + stream-apps-kafka-docker + + + target/classes/META-INF/rabbit-apps-maven.properties + stream-apps-rabbit-maven + + + target/classes/META-INF/rabbit-apps-docker.properties + stream-apps-rabbit-docker + + + target/classes/META-INF/kafka-apps-maven-repo-url.properties + kafka-apps-maven-repo-url.properties + + + target/classes/META-INF/rabbit-apps-maven-repo-url.properties + rabbit-apps-maven-repo-url.properties + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-docker.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-docker.properties new file mode 100644 index 00000000..ceeef29d --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-docker.properties @@ -0,0 +1,13 @@ +sink.cassandra=docker:springcloudstream/cassandra-sink-kafka:@cassandra-sink-docker.tag@ +sink.counter=docker:springcloudstream/counter-sink-kafka:@counter-sink-docker.tag@ +sink.jdbc=docker:springcloudstream/jdbc-sink-kafka:@jdbc-sink-docker.tag@ +sink.log=docker:springcloudstream/log-sink-kafka:@log-sink-docker.tag@ +sink.mongodb=docker:springcloudstream/mongodb-sink-kafka:@mongodb-sink-docker.tag@ +sink.rabbit=docker:springcloudstream/rabbit-sink-kafka:@rabbit-sink-docker.tag@ +source.http=docker:springcloudstream/http-source-kafka:@http-source-docker.tag@ +source.jdbc=docker:springcloudstream/jdbc-source-kafka:@jdbc-source-docker.tag@ +source.mongodb=docker:springcloudstream/mongodb-source-kafka:@mongodb-source-docker.tag@ +source.time=docker:springcloudstream/time-source-kafka:@time-source-docker.tag@ +processor.filter=docker:springcloudstream/filter-processor-kafka:@filter-processor-docker.tag@ +processor.splitter=docker:springcloudstream/splitter-processor-kafka:@splitter-processor-docker.tag@ +processor.transform=docker:springcloudstream/transform-processor-kafka:@transform-processor-docker.tag@ diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven-repo-url.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven-repo-url.properties new file mode 100644 index 00000000..140d9a87 --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven-repo-url.properties @@ -0,0 +1,26 @@ +sink.cassandra=https://@repo-spring-io@/org/springframework/cloud/stream/app/cassandra-sink-kafka/@cassandra-sink.version@/cassandra-sink-kafka-@cassandra-sink.version@.jar +sink.cassandra.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/cassandra-sink-kafka/@cassandra-sink.version@/cassandra-sink-kafka-@cassandra-sink.version@-metadata.jar +sink.counter=https://@repo-spring-io@/org/springframework/cloud/stream/app/counter-sink-kafka/@counter-sink.version@/counter-sink-kafka-@counter-sink.version@.jar +sink.counter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/counter-sink-kafka/@counter-sink.version@/counter-sink-kafka-@counter-sink.version@-metadata.jar +sink.jdbc=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-sink-kafka/@jdbc-sink.version@/jdbc-sink-kafka-@jdbc-sink.version@.jar +sink.jdbc.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-sink-kafka/@jdbc-sink.version@/jdbc-sink-kafka-@jdbc-sink.version@-metadata.jar +sink.log=https://@repo-spring-io@/org/springframework/cloud/stream/app/log-sink-kafka/@log-sink.version@/log-sink-kafka-@log-sink.version@.jar +sink.log.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/log-sink-kafka/@log-sink.version@/log-sink-kafka-@log-sink.version@-metadata.jar +sink.mongodb=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-sink-kafka/@mongodb-sink.version@/mongodb-sink-kafka-@mongodb-sink.version@.jar +sink.mongodb.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-sink-kafka/@mongodb-sink.version@/mongodb-sink-kafka-@mongodb-sink.version@-metadata.jar +sink.rabbit=https://@repo-spring-io@/org/springframework/cloud/stream/app/rabbit-sink-kafka/@rabbit-sink.version@/rabbit-sink-kafka-@rabbit-sink.version@.jar +sink.rabbit.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/rabbit-sink-kafka/@rabbit-sink.version@/rabbit-sink-kafka-@rabbit-sink.version@-metadata.jar +source.http=https://@repo-spring-io@/org/springframework/cloud/stream/app/http-source-kafka/@http-source.version@/http-source-kafka-@http-source.version@.jar +source.http.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/http-source-kafka/@http-source.version@/http-source-kafka-@http-source.version@-metadata.jar +source.jdbc=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-source-kafka/@jdb-source.version@/jdbc-source-kafka-@jdbc-source.version@.jar +source.jdbc.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-source-kafka/@jdb-source.version@/jdbc-source-kafka-@jdbc-source.version@-metadata.jar +source.mongodb=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-source-kafka/@mongodb-source.version@/mongodb-source-kafka-@mongodb-source.version@.jar +source.mongodb.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-source-kafka/@mongodb-source.version@/mongodb-source-kafka-@mongodb-source.version@-metadata.jar +source.time=https://@repo-spring-io@/org/springframework/cloud/stream/app/time-source-kafka/@time-source.version@/time-source-kafka-@time-source.version@.jar +source.time.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/time-source-kafka/@time-source.version@/time-source-kafka-@time-source.version@-metadata.jar +processor.filter=https://@repo-spring-io@/org/springframework/cloud/stream/app/filter-processor-kafka/@filter-processor.version@/filter-processor-kafka-@filter-processor.version@.jar +processor.filter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/filter-processor-kafka/@filter-processor.version@/filter-processor-kafka-@filter-processor.version@-metadata.jar +processor.splitter=https://@repo-spring-io@/org/springframework/cloud/stream/app/splitter-processor-kafka/@splitter-processor.version@/splitter-processor-kafka-@splitter-processor.version@.jar +processor.splitter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/splitter-processor-kafka/@splitter-processor.version@/splitter-processor-kafka-@splitter-processor.version@-metadata.jar +processor.transform=https://@repo-spring-io@/org/springframework/cloud/stream/app/transform-processor-kafka/@transform-processor.version@/transform-processor-kafka-@transform-processor.version@.jar +processor.transform.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/transform-processor-kafka/@transform-processor.version@/transform-processor-kafka-@transform-processor.version@-metadata.jar \ No newline at end of file diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven.properties new file mode 100644 index 00000000..8655eac1 --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/kafka-apps-maven.properties @@ -0,0 +1,27 @@ +sink.cassandra=maven://org.springframework.cloud.stream.app:cassandra-sink-kafka:@cassandra-sink.version@ +sink.cassandra.metadata=maven://org.springframework.cloud.stream.app:cassandra-sink-kafka:jar:metadata:@cassandra-sink.version@ +sink.counter=maven://org.springframework.cloud.stream.app:counter-sink-kafka:@counter-sink.version@ +sink.counter.metadata=maven://org.springframework.cloud.stream.app:counter-sink-kafka:jar:metadata:@counter-sink.version@ +sink.jdbc=maven://org.springframework.cloud.stream.app:jdbc-sink-kafka:@jdbc-sink.version@ +sink.jdbc.metadata=maven://org.springframework.cloud.stream.app:jdbc-sink-kafka:jar:metadata:@jdbc-sink.version@ +sink.log=maven://org.springframework.cloud.stream.app:log-sink-kafka:@log-sink.version@ +sink.log.metadata=maven://org.springframework.cloud.stream.app:log-sink-kafka:jar:metadata:@log-sink.version@ +sink.mongodb=maven://org.springframework.cloud.stream.app:mongodb-sink-kafka:@mongodb-sink.version@ +sink.mongodb.metadata=maven://org.springframework.cloud.stream.app:mongodb-sink-kafka:jar:metadata:@mongodb-sink.version@ +sink.rabbit=maven://org.springframework.cloud.stream.app:rabbit-sink-kafka:@rabbit-sink.version@ +sink.rabbit.metadata=maven://org.springframework.cloud.stream.app:rabbit-sink-kafka:jar:metadata:@rabbit-sink.version@ +source.http=maven://org.springframework.cloud.stream.app:http-source-kafka:@http-source.version@ +source.http.metadata=maven://org.springframework.cloud.stream.app:http-source-kafka:jar:metadata:@http-source.version@ +source.jdbc=maven://org.springframework.cloud.stream.app:jdbc-source-kafka:@jdbc-source.version@ +source.jdbc.metadata=maven://org.springframework.cloud.stream.app:jdbc-source-kafka:jar:metadata:@jdbc-source.version@ +source.mongodb=maven://org.springframework.cloud.stream.app:mongodb-source-kafka:@mongodb-source.version@ +source.mongodb.metadata=maven://org.springframework.cloud.stream.app:mongodb-source-kafka:jar:metadata:@mongodb-source.version@ +source.time=maven://org.springframework.cloud.stream.app:time-source-kafka:@time-source.version@ +source.time.metadata=maven://org.springframework.cloud.stream.app:time-source-kafka:jar:metadata:@time-source.version@ +processor.filter=maven://org.springframework.cloud.stream.app:filter-processor-kafka:@filter-processor.version@ +processor.filter.metadata=maven://org.springframework.cloud.stream.app:filter-processor-kafka:jar:metadata:@filter-processor.version@ +processor.splitter=maven://org.springframework.cloud.stream.app:splitter-processor-kafka:@splitter-processor.version@ +processor.splitter.metadata=maven://org.springframework.cloud.stream.app:splitter-processor-kafka:jar:metadata:@splitter-processor.version@ +processor.transform=maven://org.springframework.cloud.stream.app:transform-processor-kafka:@transform-processor.version@ +processor.transform.metadata=maven://org.springframework.cloud.stream.app:transform-processor-kafka:jar:metadata:@transform-processor.version@ + diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-docker.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-docker.properties new file mode 100644 index 00000000..5af47680 --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-docker.properties @@ -0,0 +1,13 @@ +sink.cassandra=docker:springcloudstream/cassandra-sink-rabbit:@cassandra-sink-docker.tag@ +sink.counter=docker:springcloudstream/counter-sink-rabbit:@counter-sink-docker.tag@ +sink.jdbc=docker:springcloudstream/jdbc-sink-rabbit:@jdbc-sink-docker.tag@ +sink.log=docker:springcloudstream/log-sink-rabbit:@log-sink-docker.tag@ +sink.mongodb=docker:springcloudstream/mongodb-sink-rabbit:@mongodb-sink-docker.tag@ +sink.rabbit=docker:springcloudstream/rabbit-sink-rabbit:@rabbit-sink-docker.tag@ +source.http=docker:springcloudstream/http-source-rabbit:@http-source-docker.tag@ +source.jdbc=docker:springcloudstream/jdbc-source-rabbit:@jdbc-source-docker.tag@ +source.mongodb=docker:springcloudstream/mongodb-source-rabbit:@mongodb-source-docker.tag@ +source.time=docker:springcloudstream/time-source-rabbit:@time-source-docker.tag@ +processor.filter=docker:springcloudstream/filter-processor-rabbit:@filter-processor-docker.tag@ +processor.splitter=docker:springcloudstream/splitter-processor-rabbit:@splitter-processor-docker.tag@ +processor.transform=docker:springcloudstream/transform-processor-rabbit:@transform-processor-docker.tag@ diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven-repo-url.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven-repo-url.properties new file mode 100644 index 00000000..b67e0dd8 --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven-repo-url.properties @@ -0,0 +1,26 @@ +sink.cassandra=https://@repo-spring-io@/org/springframework/cloud/stream/app/cassandra-sink-rabbit/@cassandra-sink.version@/cassandra-sink-rabbit-@cassandra-sink.version@.jar +sink.cassandra.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/cassandra-sink-rabbit/@cassandra-sink.version@/cassandra-sink-rabbit-@cassandra-sink.version@-metadata.jar +sink.counter=https://@repo-spring-io@/org/springframework/cloud/stream/app/counter-sink-rabbit/@counter-sink.version@/counter-sink-rabbit-@counter-sink.version@.jar +sink.counter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/counter-sink-rabbit/@counter-sink.version@/counter-sink-rabbit-@counter-sink.version@-metadata.jar +sink.jdbc=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-sink-rabbit/@jdbc-sink.version@/jdbc-sink-rabbit-@jdbc-sink.version@.jar +sink.jdbc.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-sink-rabbit/@jdbc-sink.version@/jdbc-sink-rabbit-@jdbc-sink.version@-metadata.jar +sink.log=https://@repo-spring-io@/org/springframework/cloud/stream/app/log-sink-rabbit/@log-sink.version@/log-sink-rabbit-@log-sink.version@.jar +sink.log.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/log-sink-rabbit/@log-sink.version@/log-sink-rabbit-@log-sink.version@-metadata.jar +sink.mongodb=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-sink-rabbit/@mongodb-sink.version@/mongodb-sink-rabbit-@mongodb-sink.version@.jar +sink.mongodb.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-sink-rabbit/@mongodb-sink.version@/mongodb-sink-rabbit-@mongodb-sink.version@-metadata.jar +sink.rabbit=https://@repo-spring-io@/org/springframework/cloud/stream/app/rabbit-sink-rabbit/@rabbit-sink.version@/rabbit-sink-rabbit-@rabbit-sink.version@.jar +sink.rabbit.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/rabbit-sink-rabbit/@rabbit-sink.version@/rabbit-sink-rabbit-@rabbit-sink.version@-metadata.jar +source.http=https://@repo-spring-io@/org/springframework/cloud/stream/app/http-source-rabbit/@http-source.version@/http-source-rabbit-@http-source.version@.jar +source.http.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/http-source-rabbit/@http-source.version@/http-source-rabbit-@http-source.version@-metadata.jar +source.jdbc=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-source-rabbit/@jdb-source.version@/jdbc-source-rabbit-@jdbc-source.version@.jar +source.jdbc.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/jdbc-source-rabbit/@jdb-source.version@/jdbc-source-rabbit-@jdbc-source.version@-metadata.jar +source.mongodb=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-source-rabbit/@mongodb-source.version@/mongodb-source-rabbit-@mongodb-source.version@.jar +source.mongodb.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/mongodb-source-rabbit/@mongodb-source.version@/mongodb-source-rabbit-@mongodb-source.version@-metadata.jar +source.time=https://@repo-spring-io@/org/springframework/cloud/stream/app/time-source-rabbit/@time-source.version@/time-source-rabbit-@time-source.version@.jar +source.time.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/time-source-rabbit/@time-source.version@/time-source-rabbit-@time-source.version@-metadata.jar +processor.filter=https://@repo-spring-io@/org/springframework/cloud/stream/app/filter-processor-rabbit/@filter-processor.version@/filter-processor-rabbit-@filter-processor.version@.jar +processor.filter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/filter-processor-rabbit/@filter-processor.version@/filter-processor-rabbit-@filter-processor.version@-metadata.jar +processor.splitter=https://@repo-spring-io@/org/springframework/cloud/stream/app/splitter-processor-rabbit/@splitter-processor.version@/splitter-processor-rabbit-@splitter-processor.version@.jar +processor.splitter.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/splitter-processor-rabbit/@splitter-processor.version@/splitter-processor-rabbit-@splitter-processor.version@-metadata.jar +processor.transform=https://@repo-spring-io@/org/springframework/cloud/stream/app/transform-processor-rabbit/@transform-processor.version@/transform-processor-rabbit-@transform-processor.version@.jar +processor.transform.metadata=https://@repo-spring-io@/org/springframework/cloud/stream/app/transform-processor-rabbit/@transform-processor.version@/transform-processor-rabbit-@transform-processor.version@-metadata.jar \ No newline at end of file diff --git a/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven.properties b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven.properties new file mode 100644 index 00000000..f6bfaa8b --- /dev/null +++ b/applications/apps-metadata/stream-apps-descriptor/src/main/resources/META-INF/rabbit-apps-maven.properties @@ -0,0 +1,27 @@ +sink.cassandra=maven://org.springframework.cloud.stream.app:cassandra-sink-rabbit:@cassandra-sink.version@ +sink.cassandra.metadata=maven://org.springframework.cloud.stream.app:cassandra-sink-rabbit:jar:metadata:@cassandra-sink.version@ +sink.counter=maven://org.springframework.cloud.stream.app:counter-sink-rabbit:@counter-sink.version@ +sink.counter.metadata=maven://org.springframework.cloud.stream.app:counter-sink-rabbit:jar:metadata:@counter-sink.version@ +sink.jdbc=maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:@jdbc-sink.version@ +sink.jdbc.metadata=maven://org.springframework.cloud.stream.app:jdbc-sink-rabbit:jar:metadata:@jdbc-sink.version@ +sink.log=maven://org.springframework.cloud.stream.app:log-sink-rabbit:@log-sink.version@ +sink.log.metadata=maven://org.springframework.cloud.stream.app:log-sink-rabbit:jar:metadata:@log-sink.version@ +sink.mongodb=maven://org.springframework.cloud.stream.app:mongodb-sink-rabbit:@mongodb-sink.version@ +sink.mongodb.metadata=maven://org.springframework.cloud.stream.app:mongodb-sink-rabbit:jar:metadata:@mongodb-sink.version@ +sink.rabbit=maven://org.springframework.cloud.stream.app:rabbit-sink-rabbit:@rabbit-sink.version@ +sink.rabbit.metadata=maven://org.springframework.cloud.stream.app:rabbit-sink-rabbit:jar:metadata:@rabbit-sink.version@ +source.http=maven://org.springframework.cloud.stream.app:http-source-rabbit:@http-source.version@ +source.http.metadata=maven://org.springframework.cloud.stream.app:http-source-rabbit:jar:metadata:@http-source.version@ +source.jdbc=maven://org.springframework.cloud.stream.app:jdbc-source-rabbit:@jdbc-source.version@ +source.jdbc.metadata=maven://org.springframework.cloud.stream.app:jdbc-source-rabbit:jar:metadata:@jdbc-source.version@ +source.mongodb=maven://org.springframework.cloud.stream.app:mongodb-source-rabbit:@mongodb-source.version@ +source.mongodb.metadata=maven://org.springframework.cloud.stream.app:mongodb-source-rabbit:jar:metadata:@mongodb-source.version@ +source.time=maven://org.springframework.cloud.stream.app:time-source-rabbit:@time-source.version@ +source.time.metadata=maven://org.springframework.cloud.stream.app:time-source-rabbit:jar:metadata:@time-source.version@ +processor.filter=maven://org.springframework.cloud.stream.app:filter-processor-rabbit:@filter-processor.version@ +processor.filter.metadata=maven://org.springframework.cloud.stream.app:filter-processor-rabbit:jar:metadata:@filter-processor.version@ +processor.splitter=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:@splitter-processor.version@ +processor.splitter.metadata=maven://org.springframework.cloud.stream.app:splitter-processor-rabbit:jar:metadata:@splitter-processor.version@ +processor.transform=maven://org.springframework.cloud.stream.app:transform-processor-rabbit:@transform-processor.version@ +processor.transform.metadata=maven://org.springframework.cloud.stream.app:transform-processor-rabbit:jar:metadata:@transform-processor.version@ + diff --git a/applications/apps-metadata/stream-apps-docs/README.md b/applications/apps-metadata/stream-apps-docs/README.md new file mode 100644 index 00000000..9ce4320c --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/README.md @@ -0,0 +1,2 @@ +# docs +app starter docs aggregator diff --git a/applications/apps-metadata/stream-apps-docs/pom.xml b/applications/apps-metadata/stream-apps-docs/pom.xml new file mode 100644 index 00000000..e72e8bd3 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/pom.xml @@ -0,0 +1,325 @@ + + + 4.0.0 + stream-apps-docs + stream-apps-docs + Stream Apps Docs + + org.springframework.cloud.stream.app + stream-apps-release-train + Fahrenheit.BUILD-SNAPSHOT + + + + 0.2.1.RELEASE + + + + + io.spring.docresources + spring-doc-resources + ${docs.resources.version} + zip + true + + + io.pivotal.java.function + splitter-function + ${java-functions.version} + + + io.pivotal.java.function + spel-function + ${java-functions.version} + + + io.pivotal.java.function + counter-consumer + ${java-functions.version} + + + io.pivotal.java.function + jdbc-consumer + ${java-functions.version} + + + io.pivotal.java.function + mongodb-consumer + ${java-functions.version} + + + io.pivotal.java.function + cassandra-consumer + ${java-functions.version} + + + io.pivotal.java.function + rabbit-consumer + ${java-functions.version} + + + io.pivotal.java.function + log-consumer + ${java-functions.version} + + + io.pivotal.java.function + time-supplier + ${java-functions.version} + + + io.pivotal.java.function + jdbc-supplier + ${java-functions.version} + + + io.pivotal.java.function + http-supplier + ${java-functions.version} + + + io.pivotal.java.function + mongodb-supplier + ${java-functions.version} + + + + + full + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + attach-javadocs + + jar + + prepare-package + + true + + io.pivotal.java.function:* + + false + true + ${basedir}/src/main/javadoc/spring-javadoc.css + + https://docs.spring.io/spring-framework/docs/${spring.version}/javadoc-api/ + https://docs.spring.io/spring-shell/docs/current/api/ + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + + unpack-doc-resources + + unpack-dependencies + + generate-resources + + io.spring.docresources + spring-doc-resources + zip + true + ${project.build.directory}/refdocs/ + + + + + + org.apache.maven.plugins + maven-resources-plugin + + + copy-asciidoc-resources + generate-resources + + copy-resources + + + ${project.build.directory}/refdocs/ + + + src/main/asciidoc + false + + + + + + + + org.asciidoctor + asciidoctor-maven-plugin + 1.5.6 + + + org.asciidoctor + asciidoctorj-pdf + 1.5.0-alpha.16 + + + + + + html + generate-resources + + process-asciidoc + + + html5 + highlight.js + ${project.build.directory}/contents/reference/html + false + + ${project.version} + shared + true + font + true + css/ + spring.css + js/highlight + github + + + + + + pdf + generate-resources + + process-asciidoc + + + pdf + + ${project.build.directory}/contents/reference/pdf + coderay + + + + + + + ${project.build.directory}/refdocs/ + index.adoc + book + + ${project.version} + ${project.name} + ${project.version} + true + 4 + true + ${project.basedir} + + + + + + org.apache.maven.plugins + maven-antrun-plugin + 1.7 + + + ant-contrib + ant-contrib + 1.0b3 + + + ant + ant + + + + + org.apache.ant + ant-nodeps + 1.8.1 + + + org.tigris.antelope + antelopetasks + 3.2.10 + + + + + package-and-attach-docs-zip + package + + run + + + + + + + + + + + + + setup-maven-properties + validate + + run + + + true + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 1.9.1 + + + attach-zip + + attach-artifact + + + + + ${project.build.directory}/${project.artifactId}-${project.version}.zip + zip;zip.type=docs;zip.deployed=false + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/.gitignore b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/.gitignore new file mode 100644 index 00000000..bbc34111 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/.gitignore @@ -0,0 +1,2 @@ +*.html +*.css diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/Guardfile b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/Guardfile new file mode 100644 index 00000000..bdd4d729 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/Guardfile @@ -0,0 +1,20 @@ +require 'asciidoctor' +require 'erb' + +guard 'shell' do + watch(/.*\.adoc$/) {|m| + Asciidoctor.render_file('index.adoc', \ + :in_place => true, \ + :safe => Asciidoctor::SafeMode::UNSAFE, \ + :attributes=> { \ + 'source-highlighter' => 'prettify', \ + 'icons' => 'font', \ + 'linkcss'=> 'true', \ + 'copycss' => 'true', \ + 'doctype' => 'book'}) + } +end + +guard 'livereload' do + watch(%r{^.+\.(css|js|html)$}) +end diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/appendix.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/appendix.adoc new file mode 100644 index 00000000..3027ed6c --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/appendix.adoc @@ -0,0 +1,2 @@ +[[appendix]] += Appendices diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/contributing.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/contributing.adoc new file mode 100644 index 00000000..bcdf6c00 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/contributing.adoc @@ -0,0 +1,42 @@ +[[contributing]] +== Contributing + +Spring Cloud is released under the non-restrictive Apache 2.0 license, +and follows a very standard Github development process, using Github +tracker for issues and merging pull requests into master. If you want +to contribute even something trivial please do not hesitate, but +follow the guidelines below. + +=== Sign the Contributor License Agreement +Before we accept a non-trivial patch or pull request we will need you to sign the +https://support.springsource.com/spring_committer_signup[contributor's agreement]. +Signing the contributor's agreement does not grant anyone commit rights to the main +repository, but it does mean that we can accept your contributions, and you will get an +author credit if we do. Active contributors might be asked to join the core team, and +given the ability to merge pull requests. + +=== Code Conventions and Housekeeping +None of these is essential for a pull request, but they will all help. They can also be +added after the original pull request but before a merge. + +* Use the Spring Framework code format conventions. If you use Eclipse + you can import formatter settings using the + `eclipse-code-formatter.xml` file from the + https://github.com/spring-cloud/build/tree/master/eclipse-coding-conventions.xml[Spring + Cloud Build] project. If using IntelliJ, you can use the + https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter + Plugin] to import the same file. +* Make sure all new `.java` files to have a simple Javadoc class comment with at least an + `@author` tag identifying you, and preferably at least a paragraph on what the class is + for. +* Add the ASF license header comment to all new `.java` files (copy from existing files + in the project) +* Add yourself as an `@author` to the .java files that you modify substantially (more + than cosmetic changes). +* Add some Javadocs and, if you change the namespace, some XSD doc elements. +* A few unit tests would help a lot as well -- someone has to do it. +* If no-one else is using your branch, please rebase it against the current master (or + other target branch in the main project). +* When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], + if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit + message (where XXXX is the issue number). diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/app-starter-naming-conventions.png b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/app-starter-naming-conventions.png new file mode 100644 index 0000000000000000000000000000000000000000..ce630b78b7bfea87e0173ad524cf0def0a5ddb92 GIT binary patch literal 133938 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 literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/logo.png b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bb1f4f9fa3cfb9665bfbe00db0f622a5a13faff6 GIT binary patch literal 19499 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 literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/starters-pom-dependencies.png b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/images/starters-pom-dependencies.png new file mode 100644 index 0000000000000000000000000000000000000000..b456ab53257fab38c5eb4d99d1a630c9701fb05f GIT binary patch literal 142803 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%0A~q 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 RFhRhVoV1cusf6+8{{z2N=0yMi literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index-docinfo.xml b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index-docinfo.xml new file mode 100644 index 00000000..011dc8df --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index-docinfo.xml @@ -0,0 +1,14 @@ +Spring Cloud Data Flow +{project-version} + + 2013-2020 + Pivotal Software, Inc. + + + + Copies of this document may be made for your own use and for distribution to + others, provided that you do not charge any fee for such copies and further + provided that each copy contains this Copyright Notice, whether distributed in + print or electronically. + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index.adoc new file mode 100644 index 00000000..0dd752a2 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/index.adoc @@ -0,0 +1,50 @@ += Spring Cloud Stream Applications Reference Guide +Sabby Anandan; Artem Bilan; Marius Bogoevici; Eric Bottard; Mark Fisher; Ilayaperumal Gopinathan; Gunnar Hillert; Mark Pollack; Patrick Peralta; Glenn Renfro; Gary Russell; Thomas Risberg; David Turanski; Janne Valkealahti; Soby Chacko; Christian Tzolov; +:doctype: book +:toc: left +:toclevels: 4 +:source-highlighter: prettify +:numbered: +:icons: font +:hide-uri-scheme: +:docinfo: shared + +:stream-apps-root: https://raw.githubusercontent.com/spring-cloud-stream-app-starters + +:branch: master + +:stream-apps-asciidoc: https://raw.githubusercontent.com/spring-cloud-stream-app-starters/app-starters-release/master/spring-cloud-stream-app-starters-docs/src/main/asciidoc + +:scst-core-version: 3.0.3.RELEASE + +ifdef::backend-html5[] + +Version {project-version} + +(C) 2012-2020 Pivotal Software, Inc. + +_Copies of this document may be made for your own use and for distribution to +others, provided that you do not charge any fee for such copies and further +provided that each copy contains this Copyright Notice, whether distributed in +print or electronically._ + +endif::backend-html5[] + +// ====================================================================================== + += Reference Guide +include::overview.adoc[] + +[[starters]] += Starters +include::sources.adoc[] + +include::processors.adoc[] + +include::sinks.adoc[] + += Appendices +[appendix] +include::contributing.adoc[] + +// ====================================================================================== diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/overview.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/overview.adoc new file mode 100644 index 00000000..c1d12ae8 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/overview.adoc @@ -0,0 +1,102 @@ +[[overview]] + +This section provides you with a detailed overview of the out-of-the-box Spring Cloud Stream Applications. +It assumes familiarity with general Spring Cloud Stream concepts, which you can find in the Spring Cloud Stream https://cloud.spring.io/spring-cloud-static/spring-cloud-stream/{scst-core-version}/[reference documentation]. + +These Spring Cloud Stream Applications provide you with out-of-the-box Spring Cloud Stream utility applications that you can run independently or with Spring Cloud Data Flow. They include: + +* Connectors (sources, processors, and sinks) for a variety of middleware technologies, including message brokers, storage (relational, non-relational, filesystem). +* Adapters for various network protocols. +* Generic processors that you can customize with https://docs.spring.io/spring/docs/4.2.x/spring-framework-reference/html/expressions.html[Spring Expression Language (SpEL)] or by scripting. + +You can find a detailed listing of all the applications and their options in the <> section of this guide. + +== Pre-built Applications + +Out-of-the-box applications are Spring Boot applications that include a Binder implementation on top of the basic logic of the app (a function for example) -- a fully functional uber-jar. +These https://docs.spring.io/spring-boot/docs/current-SNAPSHOT/reference/html/getting-started.html#getting-started-first-application-executable-jar[uber-jars] include the minimal code required for standalone execution. +For each function application, the project provides a prebuilt version for Apache Kafka and Rabbit MQ Binders. + +[NOTE] +Prebuilt applications are generated according to the https://github.com/spring-cloud/spring-cloud-app-starters-maven-plugins/tree/master/spring-cloud-stream-app-maven-plugin[stream apps generator Maven plugin]. + +[[classification]] +== Classification + +Based on their target application type, they can be either: + +* A _source_ that connects to an external resource to poll and receive data that is published to the default "`output`" channel; +* A _processor_ that receives data from an "`input`" channel and processes it, sending the result on the default "`output`" channel; +* A _sink_ that connects to an external resource to send the received data to the default "`input`" channel. + +The prebuilt applications follow a naming convention: `--`. For example, `rabbit-sink-kafka` is a _Rabbit sink_ that uses the Kafka binder that is running with Kafka as the middleware. + +=== Maven and Docker Access + +The core functionality of the applications is available as functions. +See the https://github.com/pivotal/java-functions[Java Functions] repository for more details. +Prebuilt applications are available as Maven artifacts. +You can download the executable jar artifacts from the Spring Maven repositories. +The root directory of the Maven repository that hosts release versions is https://repo.spring.io/release/org/springframework/cloud/stream/app/. +From there, you can navigate to the latest released version of a specific app -- for example, link:https://repo.spring.io/release/org/springframework/cloud/stream/app/log-sink-rabbit/2.0.2.RELEASE/log-sink-rabbit-1.1.1.RELEASE.jar[log-sink-rabbit-2.0.2.RELEASE.jar]. +You need to use the link:https://repo.spring.io/milestone/org/springframework/cloud/stream/app[Milestone] and link:https://repo.spring.io/snapshot/org/springframework/cloud/stream/app[Snapshot] repository locations for Milestone and Snapshot executable jar artifacts. + +The Docker versions of the applications are available in Docker Hub, at `https://hub.docker.com/r/springcloudstream/`. +Naming and versioning follows the same general conventions as Maven -- for example: + +==== +[source,bash] +---- +docker pull springcloudstream/cassandra-sink-kafka +---- +==== + +The preceding command pulls the latest Docker image of the _Cassandra sink_ with the Kafka binder. + +=== Building the Artifacts + +You can build the project and generate the artifacts (including the prebuilt applications) on your own. +This is useful if you want to deploy the artifacts locally or add additional features. +If you are at the root of the repository, https://github.com/spring-cloud-stream-app-starters/stream-applications[steam-applications], doing a maven build generates the entire binder based apps. +If you do not want to do that and instead only are interested in a certain application, then `cd` into the right module and invoke the build from there. +Then run the following Maven command: + +==== +[source,bash] +---- +mvn clean package +---- +==== + +This command generates the applications. By default, the generated projects are placed under a directory called `apps`. +There, you can find the binder based applications, which you can then build and run. + +== Patching Pre-built Applications + +If you are looking to patch the pre-built applications to accommodate the addition of new dependencies, you can use the following example as the reference. +To add `mysql` driver to `jdbc-sink` application: + +. Clone the GitHub repository at https://github.com/spring-cloud-stream-app-starters/stream-applications + +. Open it in an IDE and make the necessary changes in the right generator project. The repository is organized as `source-apps-generator`, `sink-apps-generator`, and `processor-apps-generator`. ++ +Find the module that you want to patch and make the changes. For example, you can add the following to the generator plugin's configuration in the pom.xml: ++ +==== +[source,xml] +---- + + + mysql + mysql-connector-java + 5.1.37 + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit + + +---- +==== + +. Generate the binder based apps as specified above and build the apps. diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/pom-dependencies.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/pom-dependencies.adoc new file mode 100644 index 00000000..20b30195 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/pom-dependencies.adoc @@ -0,0 +1,23 @@ +[[pom-dependencies]] +== Stream Apps POM Dependencies + +Following diagram highlights some of the important `Stream Apps` POM dependencies. + +image::{stream-apps-asciidoc}/images/starters-pom-dependencies.png[PomDependencies, scaledwidth="100%"] + +The dependencies are grouped in three categories: + +* *Core Spring libraries* - represent the core framework libraries such as `Spring Boot`, `Spring Integration`, +`Spring Cloud`. The "Bill Of Materials" (BOM) patterns is used throughout the stack to decouple the dependency +management from the lifecycle configurations. +The `app-starters-build` parent POM and the `app-starters-core-dependencies` BOM use inherit by all app starters. + +* *App Starters* - libraries that contain the complete configuration of a Spring Cloud Stream application with a specific role +Starters are not executable applications, and are intended to be included in the `Pre-build Apps`, along with a Binder +implementation. +The App Starter root pom (`[my-app-name]-app-starters-build`) inherit all compile-tme configuration for its parent +the core `app-starters-build`. Starer's BOM `[my-app-name]-app-dependencies` is used to manage starter's own dependencies. + +* *Pre-build App* - pre-build Spring Boot applications that include the app starters and a Binder implementation. + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/processors.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/processors.adoc new file mode 100644 index 00000000..a11c52da --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/processors.adoc @@ -0,0 +1,15 @@ +[[spring-cloud-stream-modules-processors]] +== Processors + +:leveloffset: +2 + +[[spring-cloud-stream-modules-filter]] +include::{stream-apps-root}/stream-applications/{branch}/processor/filter-processor/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-splitter]] +include::{stream-apps-root}/stream-applications/{branch}/processor/splitter-processor/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-transform]] +include::{stream-apps-root}/stream-applications/{branch}/processor/transform-processor/README.adoc[tags=ref-doc] + +:leveloffset: -2 + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sinks.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sinks.adoc new file mode 100644 index 00000000..17572625 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sinks.adoc @@ -0,0 +1,19 @@ +[[spring-cloud-stream-modules-sinks]] +== Sinks + +:leveloffset: +2 + +[[spring-cloud-stream-modules-cassandra-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/cassandra-sink/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-counter-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/counter-sink/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-jdbc-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/jdbc-sink/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-log-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/log-sink/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-mongodb-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/mongodb-sink/README.adoc[tags=ref-doc] +[[spring-cloud-stream-modules-rabbit-sink]] +include::{stream-apps-root}/stream-applications/{branch}/sink/rabbit-sink/README.adoc[tags=ref-doc] + +:leveloffset: -2 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sources.adoc b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sources.adoc new file mode 100644 index 00000000..8743618c --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/asciidoc/sources.adoc @@ -0,0 +1,20 @@ +[[sources]] +== Sources + +:leveloffset: +2 + + +[[spring-cloud-stream-modules-http-source]] +include::{stream-apps-root}/stream-applications/{branch}/source/http-source/README.adoc[tags=ref-doc] + +[[spring-cloud-stream-modules-jdbc-source]] +include::{stream-apps-root}/stream-applications/{branch}/source/jdbc-source/README.adoc[tags=ref-doc] + +[[spring-cloud-stream-modules-mongodb-source]] +include::{stream-apps-root}/stream-applications/{branch}/source/mongodb-source/README.adoc[tags=ref-doc] + +[[spring-cloud-stream-modules-time-source]] +include::{stream-apps-root}/stream-applications/{branch}/source/time-source/README.adoc[tags=ref-doc] + +:leveloffset: -2 + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/highlight.css b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/highlight.css new file mode 100644 index 00000000..ffefef72 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/highlight.css @@ -0,0 +1,35 @@ +/* + code highlight CSS resemblign the Eclipse IDE default color schema + @author Costin Leau +*/ + +.hl-keyword { + color: #7F0055; + font-weight: bold; +} + +.hl-comment { + color: #3F5F5F; + font-style: italic; +} + +.hl-multiline-comment { + color: #3F5FBF; + font-style: italic; +} + +.hl-tag { + color: #3F7F7F; +} + +.hl-attribute { + color: #7F007F; +} + +.hl-value { + color: #2A00FF; +} + +.hl-string { + color: #2A00FF; +} \ No newline at end of file diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-multipage.css b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-multipage.css new file mode 100644 index 00000000..0c484531 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-multipage.css @@ -0,0 +1,9 @@ +@IMPORT url("manual.css"); + +body.firstpage { + background: url("../images/background.png") no-repeat center top; +} + +div.part h1 { + border-top: none; +} diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-singlepage.css b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-singlepage.css new file mode 100644 index 00000000..4a7fd140 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual-singlepage.css @@ -0,0 +1,6 @@ +@IMPORT url("manual.css"); + +body { + background: url("../images/background.png") no-repeat center top; +} + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual.css b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual.css new file mode 100644 index 00000000..0ecbe2e8 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/css/manual.css @@ -0,0 +1,344 @@ +@IMPORT url("highlight.css"); + +html { + padding: 0pt; + margin: 0pt; +} + +body { + color: #333333; + margin: 15px 30px; + font-family: Helvetica, Arial, Freesans, Clean, Sans-serif; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +code { + font-size: 16px; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +:not(a)>code { + color: #6D180B; +} + +:not(pre)>code { + background-color: #F2F2F2; + border: 1px solid #CCCCCC; + border-radius: 4px; + padding: 1px 3px 0; + text-shadow: none; + white-space: nowrap; +} + +body>*:first-child { + margin-top: 0 !important; +} + +div { + margin: 0pt; +} + +hr { + border: 1px solid #CCCCCC; + background: #CCCCCC; +} + +h1,h2,h3,h4,h5,h6 { + color: #000000; + cursor: text; + font-weight: bold; + margin: 30px 0 10px; + padding: 0; +} + +h1,h2,h3 { + margin: 40px 0 10px; +} + +h1 { + margin: 70px 0 30px; + padding-top: 20px; +} + +div.part h1 { + border-top: 1px dotted #CCCCCC; +} + +h1,h1 code { + font-size: 32px; +} + +h2,h2 code { + font-size: 24px; +} + +h3,h3 code { + font-size: 20px; +} + +h4,h1 code,h5,h5 code,h6,h6 code { + font-size: 18px; +} + +div.book,div.chapter,div.appendix,div.part,div.preface { + min-width: 300px; + max-width: 1200px; + margin: 0 auto; +} + +p.releaseinfo { + font-weight: bold; + margin-bottom: 40px; + margin-top: 40px; +} + +div.authorgroup { + line-height: 1; +} + +p.copyright { + line-height: 1; + margin-bottom: -5px; +} + +.legalnotice p { + font-style: italic; + font-size: 14px; + line-height: 1; +} + +div.titlepage+p,div.titlepage+p { + margin-top: 0; +} + +pre { + line-height: 1.0; + color: black; +} + +a { + color: #4183C4; + text-decoration: none; +} + +p { + margin: 15px 0; + text-align: left; +} + +ul,ol { + padding-left: 30px; +} + +li p { + margin: 0; +} + +div.table { + margin: 1em; + padding: 0.5em; + text-align: center; +} + +div.table table,div.informaltable table { + display: table; + width: 100%; +} + +div.table td { + padding-left: 7px; + padding-right: 7px; +} + +.sidebar { + line-height: 1.4; + padding: 0 20px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; +} + +.sidebar p.title { + color: #6D180B; +} + +pre.programlisting,pre.screen { + font-size: 15px; + padding: 6px 10px; + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; + clear: both; + overflow: auto; + line-height: 1.4; + font-family: Consolas, "Liberation Mono", Courier, monospace; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px solid #DDDDDD !important; + border-radius: 4px !important; + border-collapse: separate !important; + line-height: 1.6; +} + +table thead { + background: #F5F5F5; +} + +table tr { + border: none; + border-bottom: none; +} + +table th { + font-weight: bold; +} + +table th,table td { + border: none !important; + padding: 6px 13px; +} + +table tr:nth-child(2n) { + background-color: #F8F8F8; +} + +td p { + margin: 0 0 15px 0; +} + +div.table-contents td p { + margin: 0; +} + +div.important *,div.note *,div.tip *,div.warning *,div.navheader *,div.navfooter *,div.calloutlist * + { + border: none !important; + background: none !important; + margin: 0; +} + +div.important p,div.note p,div.tip p,div.warning p { + color: #6F6F6F; + line-height: 1.6; +} + +div.important code,div.note code,div.tip code,div.warning code { + background-color: #F2F2F2 !important; + border: 1px solid #CCCCCC !important; + border-radius: 4px !important; + padding: 1px 3px 0 !important; + text-shadow: none !important; + white-space: nowrap !important; +} + +.note th,.tip th,.warning th { + display: none; +} + +.note tr:first-child td,.tip tr:first-child td,.warning tr:first-child td + { + border-right: 1px solid #CCCCCC !important; + padding-top: 10px; +} + +div.calloutlist p,div.calloutlist td { + padding: 0; + margin: 0; +} + +div.calloutlist>table>tbody>tr>td:first-child { + padding-left: 10px; + width: 30px !important; +} + +div.important,div.note,div.tip,div.warning { + margin-left: 0px !important; + margin-right: 20px !important; + margin-top: 20px; + margin-bottom: 20px; + padding-top: 10px; + padding-bottom: 10px; +} + +div.toc { + line-height: 1.2; +} + +dl,dt { + margin-top: 1px; + margin-bottom: 0; +} + +div.toc>dl>dt { + font-size: 32px; + font-weight: bold; + margin: 30px 0 10px 0; + display: block; +} + +div.toc>dl>dd>dl>dt { + font-size: 24px; + font-weight: bold; + margin: 20px 0 10px 0; + display: block; +} + +div.toc>dl>dd>dl>dd>dl>dt { + font-weight: bold; + font-size: 20px; + margin: 10px 0 0 0; +} + +tbody.footnotes * { + border: none !important; +} + +div.footnote p { + margin: 0; + line-height: 1; +} + +div.footnote p sup { + margin-right: 6px; + vertical-align: middle; +} + +div.navheader { + border-bottom: 1px solid #CCCCCC; +} + +div.navfooter { + border-top: 1px solid #CCCCCC; +} + +.title { + margin-left: -1em; + padding-left: 1em; +} + +.title>a { + position: absolute; + visibility: hidden; + display: block; + font-size: 0.85em; + margin-top: 0.05em; + margin-left: -1em; + vertical-align: text-top; + color: black; +} + +.title>a:before { + content: "\00A7"; +} + +.title:hover>a,.title>a:hover,.title:hover>a:hover { + visibility: visible; +} + +.title:focus>a,.title>a:focus,.title:focus>a:focus { + outline: 0; +} diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/background.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/background.png new file mode 100644 index 0000000000000000000000000000000000000000..d4195e5b32cd5f878652c5195bd674a5a6304855 GIT binary patch literal 10947 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 literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/1.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/1.png new file mode 100644 index 0000000000000000000000000000000000000000..7d473430b7bec514f7de12f5769fe7c5859e8c5d GIT binary patch literal 329 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQC}X^4DKU-G|w_t}fLBA)Suv#nrW z!^h2QnY_`l!BOq-UXEX{m2up>JTQkX)2m zTvF+fTUlI^nXH#utd~++ke^qgmzgTe~DWM4ffP81J literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/10.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/10.png new file mode 100644 index 0000000000000000000000000000000000000000..997bbc8246a316e040e0804174ba260e219d7d33 GIT binary patch literal 361 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQWtZ~+OvdJMW|Y+^UT?O-M{rKJsmzxdayJ{ zDCQA!%%@7Jj$q%-wf8e0_jRx8Dqi$}^?K=?6FriQFLv>>oc^CE+aVHhW3=nZ+fQ4!M=ZC7H>3sl|FJr3LwU zC3?yExf6FO?f@F61vV}-Juk7O6lk8Yg;}bFaZ-|HQc7Azopr01?u8M*si- literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/11.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/11.png new file mode 100644 index 0000000000000000000000000000000000000000..ce47dac3f52ac49017749a3fea53db57d006993c GIT binary patch literal 565 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1SD^YpWXnZI14-?iy0V%N{XE z)7O>#600DeuDZ?5tOl@ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=9KoYUDZ+`aP)jU z&ny=ErrK^#Gw!AcR}pdfMERuV^@&0$@(#^6b8c@rn^6RWX3pUb z4*6@PZ+H0#u=rjsXzS?6n6*sBGbHqGTU%mCsH?n#%j;eD^2}qe=iX*J@VQ3BRpz+u z{PX#N(^9X${`$90+;!pWs>o@z_n8G)7Uo7PJz`jrS+)QE@=PWHmc~UIw=WmUe73o7 z>^bR(M752aYoNg~ozu7U7&{(U>{s!;bn#f?ItjL^o`e{*EOQHqO;ccnz9hLK5@2cAyw@AaPFL~Cp#02|E|4xeQteNtB7waMs QVCXP-y85}Sb4q9e0GRUFb^rhX literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/12.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/12.png new file mode 100644 index 0000000000000000000000000000000000000000..31daf4e2f25b6712499ee32de9c2e3b050b691ca GIT binary patch literal 617 zcmeAS@N?(olHy`uVBq!ia0vp^JRr=$1SD^YpWXnZI14-?iy0V%N{XE z)7O>#600De9$%>2LVd81Yeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=Cn>wTzx1(qV@bS z0hYvspf(--lM>otrqbK$7p{3DzJ|+KN8%5ows)AI?zWk_n>jwEHXrTJecpEW_0xL= z?}N`*R`T~d2{AN${y8T#GEn4hUb&52^}Op@TW4{oc)A6)%$5=G}h# z?O{QLj@aRcAIf&y&OiUN=H2gq=_}V|pWfuReDV|{jwXw~>#w)I|9${XE z)7O>#600Dep5bGK9wD%hYeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=Cn>w>~AWNX^a2R zbkveVY|45D7UnZ&JtjPwvdCCscZp0EA*0()#GOw)UH4-^&)y^E*4%UC)*|J}q_Ss;tN`nd8$>x9$_Xb^O2EpX&@C ZI46EzbLxq-voTO7gQu&X%Q~loCIF_C`w;*D literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/14.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/14.png new file mode 100644 index 0000000000000000000000000000000000000000..64014b75fe2e84d45ed861974c72462727979360 GIT binary patch literal 411 zcmV;M0c8G(P)!ax*-PXaQ9e~6^e1gu=a6a&KSz}bR`+prYG9ayB$BDjWGfIE;t#wl!+ zR3S(jA%y#i_@eOOedXoc%RQe%L;wH~k+s%ZI~)!<=dD%?4MaplaU9QPGski2q3`>r z(}{j@0a$CLl+)={2vLWml*i-oa5#J}DW$gCZB~Z!(!M#)2St|1_V^0qpmCrBof=Y&NUas@LmfSw=)4B4f;8Fu)(eFsv24 zJzXxBrayquXcR?J{XE z)7O>#600De0j~t#c`vY#Yeb1-X-P(Y5yQ%LXFPyHJS9LOm(=3qqRfJl%=|nCVNOM5 zpg0#u+&RCXvM4h>ql94%{~0TwC?8m~C^ZqJRG}m@H-L1 z5L@scq?{XUcxG{OP9jig5ySQaTl#^*93bKF#G<^+ymW>G($Cs~V(bw8rA5i93}62@ zzlJGu&d<$F%`0K}c4pdspcorSSx9C{PAbEScbC)|7#JBmT^vIy=9Eq_Jl&Ka(%QdX zh{H8O%#_7)Tc@t$mM`p4(Ne7omR*~(>gd8_8AZH{=3ms$Fmzm^yL@_+(#aQQ5>7QW z>3g2fIsH(ugM)!V$x4Rr_+!J_XU%4xbz0aE;^N{m@42Z|@0S@TQ=WbP`TMV5Ok;<| z^Ihv+@6tQ{sciRF9dD7Nr=KobwJJ68zJK$<1Pd9rz%4O)*;}Jzj&~nTGMecz>B%lV zK|`fmIc8mp-h8iSXiGFW=C(L+XH4DRxZQX87^-dLuD>odo6YLT@Sw)dfBEIG)v2@6 zR)%mL7GRj1x-&v&+2q@A%a&h0`Lw7|#(w_!tgT!PoJ|+re`lxaY7e*=hH)_rZeB4|imU1$R#1`!P>&$poQl;nzm}mD5ZFopaX|GsS%q*{P~< z;WtmO%lhToBL0i}yfkaOt?EN=nkLNGuU`ywhI5H)L`iUdT1k0gQ7VIjhO(w-Zen_> zZ(@38a<+nro{^q~f~BRtfrY+-p+a&|W^qZSLvCepNoKNMYO!8QX+eHoiC%Jk?!;Y+ zJAlS%fsM;d&r2*R1)67JkeZlkYGj#gX_9E3W@4U_nw*@Ln38B@k(iuhnUeN2eF0kK0(Y1u|9Rc(19XFPiEBhjaDG}zd16s2gM)^$re|(qda7?? zdS-IAf{C7yo`r&?rM`iMzJZ}aa#3b+Nu@(>WpPPnvR-PjUP@^}eqM=Qa(?c_U5Yz^ z#%Y0#%S_KpEGY$=XJL?(l#*ybuErX#^g`ttQfwn3r>K)tuC)r#2`iJ>Prt42#Ndx#Uc~1)>aw z3jE@Q4|!9Z%lVv}- zc=48cF7H)t`(Ck`^+mtha~Np7bBSw2NpOBzNqJ&XDuaWDvZiNlVtT4?VtQtBwt|VC zk)DNurKP@sg}#BILUK`NaY>~^Ze?*vX0l#tv0h4PL4IC|UUGi!#9fLzfW~Qojmu2W zODrh`nrE42VU(7fm~5G9U~HM3l#*m_WNcxOXkuzEX4g z+-vfUhb0A>b04=Im{6XiQd1v%r%>h0$G8U7E1If8OQ!N~xOYY5h0NDT$p9(iZ?Q&e z18-(+l~J8O`)kc}e&uL$eW&>P-#`~Qm$*ih1m~xflqVLYGB{``YkKA;rl!p+yCFkc(+@-h!Xq*<< zxXkpt#FA2=d1VEBsYynrsitN|Y01eJ$;p;U#>wWX2KP5v&I9V=1L+C? fTFYQ)RAFeOZJ=$?lDoSWD8u0C>gTe~DWM4f^}upZ literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/6.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/6.png new file mode 100644 index 0000000000000000000000000000000000000000..0ba694af6c07d947d219b45a629bd32c60a0f5fe GIT binary patch literal 355 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQ*)Bra@SU# zmiz#bR~{$s2si{S(aY|Z}Vd7tb ouUmn-_&~Y>fYve?8dVq?X&Y!8wB+ut1u%w%U~xZhnMEEs6JbBSw2NpOBzNqJ&XDuaWDvZiNlVtT4?VtQtBwt|VC zk)DNurKP@sg}#BILUK`NaY>~^Ze?*vX0l#tv0h4PL4IC|UUGi!#9fLzfW~Qojmu2W zODrh`nrCEbVQgk$XkwI@Y+{_8nv`N>YGIaQkz#0QY@Te9lBQ<)awbq0A4pdK&{_sV bqY6VKZ3AtCmfYR7Kp6&4S3j3^P6u&S`V$cAh@R~F=4@V4jxkzlaQrcFYWK{)(`o5XZnut z=nE4SU2g1ZW%;@@I$>_e3F8a=8WK~|CVXt1DqisQxtIX|`YW_n&?Nh#1gQ}d)$LrYTw(_{nVG)tp2V+#}WG*e^KRLdkoLz7g? qn(IA84Qgo42`r6v<+Hvch>@C7(8A5T-G@yGywn*$#_oy literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/9.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/callouts/9.png new file mode 100644 index 0000000000000000000000000000000000000000..a0676d26cc2ff1de12c4ecdeefb44a0d71bc6bde GIT binary patch literal 357 zcmeAS@N?(olHy`uVBq!ia0vp^JRr;gBp8b2n5}^nQNRqa;^5&H%t0&v*|C|wdb9$wI zR@+N9#RIowg@Uqn&z-__Tzhhz!sG|vTxA7?=O|Y?u(d4T{!RM9c7chr6d%1?R=i16 z?@Ic{f32YJFJnVhX)qGzOMplv!L->5yAlT#}irms+fsQd*FoSE84k zpF44v;trs3T43Wc)AJHbN`dAXo0u6Hr<$gkq?lM38ycjV7+5A5Sr{ayr5c%-n;95g pF*H#D>f!_G3IJNmU}#ifXryhRZP1dtyA~+J;OXk;vd$@?2>@J{cB%jX literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/caution.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/caution.png new file mode 100644 index 0000000000000000000000000000000000000000..8a5e4fca039d0247dcf55ae0e719cdb457509422 GIT binary patch literal 2099 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

)_adS|n?p|6d5ZKl`PzD7od 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 literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..bb1f4f9fa3cfb9665bfbe00db0f622a5a13faff6 GIT binary patch literal 19499 zcmeEt^;etE6K-&~(&AD|3lv)1305dtpg?hVCrEHFRw&XIYmuNuio1IYMHAeuNP}Ta=2avY~S=&8Tb+dA?)3LL%4e%VY0|Ef*W2%bsAN&^&R`xwp zhT25eWBxsy#ak-keqY3Timv8Or&+wtqPsv(HeZ;vVHtJ>^5{{&FJpdVvTm3G8{k#vlWNzgqengG*E#{q;K?S zP*1wdiIzT!K9$3%@sTr6W~o*V&pqd7${=5$s(5G*9%HxU>%aKPgGZT%HNcq4PFJnY zU7R#J%j?~OhCHAqOGPj}NjYC>O}>Ym`#$TOjc z0C2>Ox6TVWV@9DbUh2ys{WY6UX>^Q>e zP)gtRg)q9vmAvt;_` z(M$sm?sVhQK!4-f6&zxUzv~@~jnQf{KR|l%E~rjlsGs^`f~p`u3)u%h_B=E5``ipGx6t+0R%4J%>sdI`ar5Daq_j!gSyHm|Px)~Cd6De5mZ2}f zW{gSQ=BTT;xk|5*B}9V;Z!W0WZfQL=*Z3^p`X~R{t;zC9Z@{P>Ll#@WSBnfA;`fYL zLEkKr&*o}1AM34RVGH;0ML2y!b5GHYJOb~3i?3#6Lru>p!z2A0aW2tKD~$wwBf?pV-?aWcj@_GPl4ca)3e9InN1ci1oT1 zx%k>!JiH39y>Uea zI^g24xk`E^1-ep+GoO6l4@^X}+%`AId90s*&~B49=skmC(c3L|L7I z2msmgqG!c^w#C)@;C*YEqH)M?XYeOcR#A43w?77p2xPs0u8^Ckomuaj*Y_dRkdf`v zz=OXS-_H3kP+?n3!+FDmndMQyN?#jl0Af-49C)wCmm?X<`bYYF!02|HNy!84I~EpL zI)(wEs8oXLUTAB~g#C@L)*l?FIfIHgxzdWwW4OC?z{xLNpDjXIPg}izN}E}gCuCZ5 z>Nv=ixhPrc&c=M}>AlSsoSVfLeOw&tZ^uLk%hSy*KB}rZMQ(1}z95V7gH{@xs4#aP zD|H$V(%=vei&N%V6*2$TK6+~Ib29ij>E&#tUUBc2<6hOhOHBx}fg#a6;h@9RtnEnh ze{G|f{StyhR94D|?&z~$Q~x2vG0E_|1Ts!N@9!|0UXntK{U?>bE=lsE_neF*1g;r) z^omWzPf%rWYlSYci>Wu@(*%5o=Am4W9M_Rfz6AD1+o6~K=1CEOG(*VPz{_Us|ICeZ zoUpn1_s2TvM=(|-bKf8geFOtZyAij}5~%rss>l@PhrRagErTz@9K?#e%^gZFh9?FOm{O7WM#^?9QqdX>T~jSvPqP7;xvrVDTn0y}qN4NL8d zE6hA)rWlz@x>Gqm!$BrPm?_GMDJayggzV4mcE51t5x$Qg`V!}2w&4+G(EjT0rIIb?K-S!Wz+tRX9n8UYjfX5ikqNQ0M~>zY^0 z*p9D&VNL?TLC0a64f+_H09obmq`bM=-(9K=F0C@{yP_9*zr6Kh)!R&aw05-OrmjR2_TdK zJu9I0sBxSx;X9xe%=6|vfMiYP<531NTU*>@q6XAdby<;!;4vxGN$+_1eEegE1duNq zZ#}bVH-Kcr`>^Quc=YS^pIn>k#!7klYa0^2xIwRgIATLmNlhZXC;j;&%iCWMnw;Cx zJTWf)dBT)<%Ssjt_Zb=}69TwW70?EFe-TZ$zx|@Vbm?SaLM)nB)5#Nbny3 z-aiE@6TA;%T+sO3u=ss_d4hnLU^m5I9CW?JvPM%N_5Lb&W;zXn+}Kz+hnUI_&LvMA z9|bTld(aV_;xo}4jVM;%JQ6Ic6(GeHCWQKA^GP+8%s$0qAe=b;g60=#pI+f(lhhJE zur>_Y<+e&uje?Dx1Q*!iIF@y~1d7cN9%>=4f|n-xy?(e1$YU@Y1HZ797wU=-UFs}0 z=rJtv%_v_QP{`7+Esnx*KURc@F+p;Pp>j*!;a3we+6c z)uUmy@_7nuk}Ny9#XDkVyWArSr<^aCJiPtgojI<7l{9;Es#s>O6r5!_8U}5M+(qY) zRE~~fKoch%M1$!SBm0C5o9=(1D~AxunIs8|q`WWunM2aP6ZhY-4b1{!7Y=je0{*83 zMdbO=UxfxLM_k92M-ZE>);Nal6ktDdUNzedxTqWb)r^QLZ-Y-m<4dhPF@F>#b0om# z{_$8<6d~-3-%2Ji6d)CiKz@xul+`B##^(ExS<|EGjeSB-6)E~X%@--K7?kKC?Bd-t z5G-=?35_G;c|2VQ^Y9pf7^eHfTpx>^&+XF8l znaXhhR-?t>I1i0>uq<*rSo#_R=l8+GY%ITHI^$F$sUTWRZkADQuDEamb{X8gJmtLz zg`S=nh%lYdcp?zThGbm1H6$nT?)^J6vQ1muOg6ET`r*V|sEF3Xef6;{rnBt^>hsB6 z%Ju<9#}7EHh2tQe(9eWpd>HL*-S(fy!fli{wXhoiWRZ^RHsI%bXU;Ry%8sGVJ0kpb zO(JiQVjC#hy*xZex~RD{#_Oq$C7K??KIE%PB}g(;q6HH?f)TS5xUsYDOV4Ffh1wTB zwzNIm%ldGo+sXD_5_i-333<%)-)eO+=T^fQxLA@fe}U+X8sk!&U=m3z2DdW5T35YO zT@Cl?>2&=?jXZ*MV=YZnJ@``x(0YIHUvZE@3_Wq^RD&4uQ4|{ioGBe|h_Hpvi))jW z_I>_%k^g+XVio0~O%QU#j|tkb zO5OIm1ItD7mbizA=aoBOxE91m|8Jl3&WUQhwo~sCd1ZP!e<=p_utlby35BMzNf$$Q1QHu9>;9xALAp7=n$F`XQ5B~;Z!odfEEJH6>*poGF9 zu6J`OX3p=gNJ(4oKh$Z@O|wzv<(EGL&RzdKg$F{(`S^R*nJ!wjFN{58Hp*elJpac#x#n6a@K+IAFvUUk;23i0=x+;POh;|eFfJCOID z{7_U`ll;h#6Fy01>3C?@kj6pWWV1&}noH$)GfA&FwtAelivIo0rzd=IWZrUEvDX(G zK`HH_l(+lYn3^EX9iTwumeRoOO})BE3%5MtC(XcgJbNx%eln{j7M|abvajv1(aOjo zf!lA7n<-P;Xx2PQmUrA6@GU{o?<-Q^zmWprCmlKpMPM96(CxK7YMa8UGNppxe?ri>DaSSqe`H|vA{z7S(EK-!hs=9y1wL#LN;;KtC| z#kdv5T!~2q4I_5`vw2t{F4PWXtImqk0zQN>>SV%Z7r%6r^DzQ-J!H(gx{78L)eqMnopb^j#M6AY>- zA>z3Gn8j1&!_=shyJmuNzTv6L2AAT7YRs|9;W_w|DU{CeTl*H|~ zdBkR>ZF9a*=Oee@8-R*`vFjQoc(gC~iG_|G1}=zH-FAQs7QdvaCRlWxGv_RKu6bm~ z@Wy?J;~{RD%an{tOoR9-7Y!4zTpZpdoUssxasgXkHC+t zABtEWQ0$9ng;TySMT(DCubs`QI)iX`@&RPRu*bS>R1{8V_cGP7k|$h3awC*$2k&tI$7dly#afUHL*5W#^LrY z5dqBUC1qhECbJ><&JCYA@OApAW97o_#dG?RECf5{6&+Zq&W!A@l--c8%0R`W)I?aXOV?z`gr z8o5>51Y3fU>r%+Scz`?Mn9Z$X(vi?t4?u$4Rk?z=T9_MSxq_i~7Z%~zd0bo+wlEoH zF7*HIUWjV8g6S+9+%^M|B@Pn8JkUHotVP8)SefxT3b^+BUsp#FLJ8GoK+Ukj?>Ea- z`LXXXik877=0$y_YCYpQ{pmup#1{xhbq}B6SU$@KXl$(CaaXE|1d|4c6+-#%dV*iM z^2;fE=47dG$+3%z?GBz{;uV}6kF&X{oO$U||8cXLm8(Y0zc_#;Wyw(Fff6j0i$1HlG^u;mj(!&DAuT?f8_OE?H8CxUFh z=@*VsK{PzDN|hOx6belScw^ywuVqf(TFPj{-zgs_JKdDO!wI(srjgv(ePD+L)U9XG zlF~B7Ee&7hj4h$Kg$Cy-L*_Jc?_fwI=UT@1@Vv`>MS| z9tl`!%;Q%t&xDKq-eUlnI)nMeA0X4KSm0h+CTuKYe{+Gogmuy$QHzVOer$O3E%ftdncKo zu}p)RK#1-a|CCGUA#4nYO1>0 z4DMX2BnHSEhaUy|H+2cQSsa6Oq@o6e=?>=ak)2sG*x+1 z<3+H=b)AE=bg=dpn~$F5mPacHQ~>UsP}{91qv8F?S1%U;F`HfsxiYJ@%-O{=mh^|$ zMy#Dc5Qr(hedSob#-9f$baKK}$g5-?&bAOhuFcCX?u{^}4uS34<(?~te&3+UVTo|} zXqDXfAzF3Jp`HTB6tuW3)4Fn{<#ZBRi@W@T+b>k^V_IF$4r^sOuRIx*>qmMXJ;xmD z9WTv$x#!jwPn#L7;Y%Q;!l1`_$iJr3(sO7P6aV7G{h;(rlO(D<)!MMWd@PLDl2p$P zv^BHp>CMPtPI^rOR1kVcfBj=pQtjYr&_k4!akA3wbVE6oB*?@xXfp5Q9pl_(VBOOm z%1*uyd{WFP&*1O+7lUTy(vU66`+=XUwW?8MudUdDQ~Lf8iAOzhuhT>J`?1~Zq8hZd zo|&=?1~Cq@-z2W-UhRyGdP9s8E`UDvxPgU7t;OCOKZ>jOxA#AvkdiK|=3mi=#P#eH zb^d)7Abn^!Q>2c15jp@0z+u8_vuore8+hygEpH*Vk8f!3YfnuM?K1IATtBK@yi1}V zc<}xUTH-`v_FIxBE@HbkOi#krlo;CZ7^2Yvh^aXL-CL#*=<>zf6?eCOI#F_WK6ra% zUVqn7T28EH)%#T}a@s8xlMpUDl_iRLEtRb-|C_pIDq6!dZ~7AnEyc7Hk%MRb-1&v; zshd0hmSBTEuO(n3;H_@#c}eAM@?SAV`J0aI+@NpzE=o{b#ErQDIRhW@D+GC;P|w;u zQy%vnWH0v${Ig-R;1BCIyaz$IS71T{3rOEBk6-&&v|fZy3Bf{p>01%im-QPzSLs$M zpPUgTpe*FQ>Ho4{U_t~VOLaG_N-7R{1c~_iX)@98iC2VZgXtFteEZn?ql1ZEMSTPJ zE}iWscNbhqH8GB~)my;@wN!gn!c8lWJrw}^`$90glfpx>;Y;;Um{YyXjd(0#2&SpC ziPh${tF%1N_l}`x;{@paJp8M8dBg3pP;65YaDtyDu%N8Dr{{NfE)+ASxNunblQPvlc<=g z?fI_rNOr6Q#mdRVi%?4H*p}L-%to*F3bP=#E#woC^XT?kK+e8}xk2FVywm#?Aq}&U zK};$xD021TyJSaVEMf#WRI@vQxVT&s!U_y(#2$pUIW*&tMZ+(M1(lQV`5!gPZsqXWb!$S8t*9V$r&@@@>D`#HDE;9ydV`^%~6u+nt zhIQ^b`vblUQ}6wgl+cZsxv2#AnFJO5I7@WL+cmU%p;Sow2K*UIQj6)Xb8;!y@c2he z9}=ock%Y_tOydx7yPSVy`l)M_Es$5S`ElNq^Y1F9*U;z}`;M>@TTG(Ml6Zi~f-5_j zw9^+97O`{CcDbA^bsbJgPF(S<(s@!k12wo-@&xuUh;S-63OJ1GUqL+$$rLi6o>Qn6 zH7S$8HgoxxH%W%6pLkN22jLGC#kIjjwi)jUC*V$v(>au8QgU!2gDGN=v_YGEq-#2M zf-1&JzN`lppUmgpik*5~cy~tw9~@1wBxmRwb=%}^xIt1BFpmq;}*EPng7x#FVk=;^^`MgMD=C>`drJVD9y!rntYZ z+FhNSf{$gtQ6ZLq?lr0Qb246?-GdboAllq`u-8e0Qh1f8?yGcyllHwbm)QuOlzT!d~!N|c`~mVqOS8KT56xOrAG7X@En zIb7#$C+AtC3kM1Yul6(tuKW=xP@j{VD-D78-M>ueje`4M->!TGDE$K-1N~;Y4%fjW zGt?DbY>LOfyj>J&NnlF3^x<^UwWYHB_;Z>JqZgG1k!q>htL-<&Xw-ra+f7o%39u}L z#rji~G}Z+?smCnygP|90+kmnly#1Y8I1US-a6*h5J3)S$r@O^UD|%lEl%Ex^qGysMy`U5%H_Y|Kvuv6kz7G0%t8-c;Q1Q<|2b*a|#~pEqhp{%-k`-KhePF zj$tfVE4tZc(&jjCBIyQ|Q6U%_p~r{fD%9NRtXSZ&A!Z)Cch#u+w{f8CC1X3UrCmF; zQz^%d9xvXWU-JBw2_PPkcJ3C zd8p2(=R~Aa14d2&C|_cc1)0&dRz$YfxVt|o?K_UcaHO&wEXjT z%jL*voAO?g&vv36V0IJ3fAfw#K?aB2W68^>8Ski^e6i;lL3Z4fz#?VHw>{1HJJ`St z?24j=&A#joku%|tX?;#l*H#krPRZop3rMV38Iei3g zhyq*5{ggXN&bC0E*-2BI)1I;QjwP83|)RCg&j}AeNQfB{a9J*lt)f6m}ChdBbHs_av_vs))%>F-Jxwt?ewP1=$;J_KIlP(s4oKqZSNmz3(9vwx#fwnb3IhXAuc35FB`r}#w& zIe+(<2?yVFbqQy;i~Ar5!Ax*?75_(Cg1x>Mk@(D(bQ=;5csMk%?M$0|X0@s8ecq?c z#LdhR5E^tBEpv{@XwOz-E@u+%Q=q}sVi~t@*WfSxmxJYZ;Rd5k`*N~8cb|iO!(d;^ z6ab_mK=>0r$btBKN~~>{Av;@d@gEXqOwaHQGUjwry@Jcm!wo9lZLQLXy(JBKuoxP! z33g#1y?2Gxb2N*tBp-^t zV4CWa?O1fkY{TNj#J+};>=15P*V~8C6MJfj(JU98PfvzJB{h81jN+xY+^F-?KrjQ2 zdO#!zwAIzd_*c%TI+XdJUY#fLHtRFzgC~MKdL%37gy2*Vrq=aRiwWCOWkZ5Tx~B0; z5K}Sx+zuv0lTLtia)_eeD&-%qdDAdw$q{qU!g3QeJ@l7v8Y_}^F1p9=|s)`Nf1+Gmr<*Z!31hKsCs@g$urQa?4 z`~Vh0vB7S&qUUFlStV!4Q*!gKn^ljJIHM_|s2nq&$>Bs2L-%WfM#9?nk1dW+U19<^ zrmKFj>$D|LR*~!4h<0r75al!A)bk6d9II*Zil}O<=XPSdKB#3g=wNz{F{Jnrv#B zxpD#G&a24v`mjg=VF+){JTHwRKu|pH_g$toRjs9lm8}qMpP}+#0v&>bOKdrZ7 z%TH52_})8QK!t}0g<&%%!INiD4@Wtno8^hvtgnbsuJijC?7zzrB}4&W+kYwmX79;} z6Q*KYWH6OL(8aCBsVx=?`0Sygega8GTu~*G=BL!kzf$*pwhdHylE9mw)g} zAtltCFr568W>>Wf&;lN*YrL||HorL7cifD$?Yk8N+O<7J@UX+=mYxaARy~H$irvSy zi$=E;hIMyrA8VwHm1XAP2da+*xj=?9Iy!({av9`LaRP6YFr=E10Pc4A;aRM}&zHyk z(38tqC%5O1ht=ByEUxb%$8|zhhX856P^a%iymBV1f7BS$=FU1_+pdUE?QOr_I9x;r z(sS7l^(azD!B<$t$8}NKx0NH}ET%|@WK+uXudh2IXmTDyUis@fUe~(r^MVK)1wLLr zT>39%^?8%3(1o}U^s&vWFMC%LcdZ>ejS$7pCCX_Lqn~n6V)NUI=r6X6492WNi(ig? z&A8NUOW5>ZY7K8x7YAxj6kz})T)Jc1yOd3PU_m<<{lN6hT$FpB)Fx8 z(*bYoIH1i&aAO8=pZsy{t{h3!6aHpgbxxU>Bvo_ZcW>O(Yq_gYcpuiyf*ID0Gf^y2 z)cKD_wFrjUi}6TuNe=R+0b-_DS#k5n<2ins&nt{@Udv4hzhIJcDsh{W68rnR*OxAW z_bSiiEaGgIGNdtTa~=;1Q2vCw>2=)S9Jlj)t;+*Go5yDbm3a_9rfkW>v3hGG0qp^8 zFWofG=IAqmCwzn{gjk5e0CK3`rA+{8PHVTOZNtYSq&>KMfb(Uh_&j%HA|9(^`kdEt z{ZE<1u8W8>^Z=Q#k37s^>4*u}Bi69sXqOyQIp7xC<&{Kh`GtT@dt-}mHN#nAzER{h zAF26wPx4uw|0pJc_2l2{y3-T~H!XlwAn#UFeh>X4L@dJjk&S zaj3Hkj|}5!Qz28A?OZ}=we!QIL{XObJi1ZYFBzUvjB_5AtpEsI2QPg~&99HyAsn7!p) z@!uA7oLOcoEwH5@+$xqtjOmGodTRstvEfc4XB~6FIj=t+qC^{~O)5gE)=I7ke@-Tj z*VuT=vb;;068SmQs3s1`bD4FK%4KSq-ZR4rT#B>ui{4aDONyLKw8EF3%(WAEL0jL_ zL~cw(0I(tAp&+TO4K}!5cAobCtor$nx_GKPm>WR^yZ0Awy_5-FF92BfSU=x82seiB z-`RXVUXV}|!)H9X)=M2sHHLTwuCT6Fql!oXuVYgOqx+FL_uEN4Jf=stA(xaJ0sra{ z?|KS3DY)jwg62tKzH9|h*Hs+XCW8I#T^eck_V|mD06zuBVp8FCPng1iImMWBo@YHk zfrLXuDJO`Bum2~w&?F=suu>hHYQb?nTN+_5?f?+3P^v_}pI-FJzKmoR`z-jwDgR26 zCJ&3w`l-aOe5=wX8Xm^l`I7D#*Jix}?9;d0cRU*aOa9QU^}7%LU7|o`kI+LE@L5E1 zC?2A)BR&z3s6>QlG=xgb`pEckRgpo{EB1?{C8WkmPu|}*Yav5ZpgtPV}7}Z0Ca*J4vf~psj)AH((CgBYE8LzD(}U9o9exHeSEs7D|#d{xPCz~ znvF|hlLnDQII58I+~4G(9eJE}i)`lo;m+NV23)$;>b(lAL-Ac9iS&|jM)mo9At{LL*+;9|T(!OEi| z*`%G&hQ80qanIGG;-_@>CE)z@z2{AOT-GDUVgwB$pSQ2#zR0$grI?|wK)aX82R3ge z&yzqRYyYki$yWlQD<#2&E272XSea}xPR4rpHxp%@T6lZe-k%a+K=rX102F!EK6D!4 zHnD9sXlGOZ*ysOtG39@KZXQ;JXT~n4mBVA)fVo&iVhDKC0mtjv{hO}{;G##UdMWJl z{8iBDL-6G41v`*u&2c7-q-!phqFgDDKJz=+1_uk_?ja*pG!Q_a&E6p6aw`M1V>;H! zS==0HS2KRcaPVZC@ldA^YY7Y_#{!Q73E(I;&}47mT0N7sdJ?v>Ls;D(g(%g?`}|-V z@z!k8E_S~+Dh-iM4{HnVRJT)V#b{Z6Ho_vU=rJgv z*Cp_dq6&CHQ`^zYiL>IBmh?GkP>l%|w;rtjOxS{52`MRbli5Gv)Nw{0Mdji5W12Qh z78@y=g1^~$C_K-H@v^~- zh3Tc&sw<%#*9@>2$m>+fC+y|(P$#aH@KD{`S8~U0|6r4KtqOpkI26yZNd<@oQhBlL z$0Dh`4%HKPUqY#b7lY0&4mR!QHJ5p6vsU}JJCSev-KBxIkDCWK7Pfl7;vo{18a^mI zRh$y#>Fqgu#+Ca@>Mo4eqC^s{-;exy0wm<1NZK^Nw|kQGt^P5)oJ)d}?zB9V3I96l z?a@|ZDiQQ?=+lecv6D(pT?;bU3t0?Nx4=}T;iXcZ#_tHdC6=px2NM!MQQ#8S&xZt= zZigNHM zSi;FW4GdeL1*aiRoX+~Y|33dyVOo;ViKao$wC=T;alWwhAkD-RAhxI`6^8E-^8&PA znShgzI_aU!N7-?AFjt+70_Mek-o~$iD5|0oOX*)Pyni>Q@)IZ3lOGpzWcj`_uh`?* z@9mg$;%ff+WVCwLrF6>e&r&tP+}%RQI$>m*I0D;_>0B>3u#Uy9%k>}Q&Z9&l@^R`C zIsYB~<1rDs5=`=4uzoA+^~otqkUY?~b9s4gy6<_ zH$ns&8Ao*j*GxGX&5<8Qc+0+X?%2H_#tM`s!ZW78ce4f5q%$gv>tS*;pB8`y%?}Mk z?hurmuAeZmL-l#-IoioD)rVr|tHtmUiR5`7!jqVZLPq8Ht#_;YC}O`bCIPdK>HGM`RWg?v>;;G8j0u^nXUqL>)xP7*n!JeoUN z{lbxqvfORD+xYO4=OQgmBXWK9lNlSB976U5ffpZ$40bADJH$*hjX_e<}+YcT+e3L9<+*hl)@KfiO8Y`1r zf;vz6_)N_ZBK7=$9wO_grNqX-yIOkmkGAC^R2;#LxNS$I&o*=s%kB z)oz#oPB#;8##oXxz-8D}m3y^YG#j)=lUI0jte1c6{a7f)#}xcG2rJNo2Gv+)WbWu) zI{#AHUsN@e>0j@Xv-SN4K{*R|#yog*Sez4ghbqYd&m0PxGEvx-3BTOJTuA?%U}GSL zrk63Y{$#w@J7c>V)W?)$o4Ab#?q3;EWV&P24x&n>mEd}~bKtpwS^Gm?j7%qvwMsnd zyfo>(x@pr3Uqy7N;US!+6X;(Q@FcwPQ)(c}2q7D8rMVRER>LcQ8%l4qwz8c*=r@PF z<1livNpMTSfYK!tH;~<}oBMilpvRJ{w}=OCZ|#xQHLfS7?}&30*vr_ksZWN zFXPtbu7OY?7QEeEuzaz=D$XI>-rBaBIM~ypwQt+{O9Tj00y9BV!|^2Ec&c&`dH?Jz z#jL=$I|tMsI_DGAFxuR+QmmwZV}m^{hKB^ zHsX*5hCC!S2z^*%`Q&h|ocvxzK$2T7LwZnS zu4!rv3m;`GuC_X|=0^_O=dAnSYw!jW00b1fho~Dlm~U!ncB@}B|Jx6%im_`y6!~|R zbPrTk%Ksb#H>&Fr;@BlWjA&WJ?K8s$?`|KYdcqd)g=gra7fJgJ`cLeiqETxJG=7@2 zvXjZaAHroSQlv42gB{0&RVmDYbhu$F%S`OO4=6Tf=YhBc6&5&IxS6y6{7{VHHGpDY z@>6V?nSoOrQnvf-3ph);wRf#C(XE4dtXwyEp>C>m?iBgUHj)x4%OXV@>D+8n=TjDu zj5@oyw)=-Uv#zceoahGLS;IAj%+eHUo{oa&#{E%?0cEW~h8wrR*rv@7p#@iTl5^LJ z7d|?55p52Kmn&W7SFu-q{*ln=u-1%mJ{V5>_?FC)!w2z)oO&9mI00OEAyDw=>W@ZU zqzb-Yu0eI!>PtDeYS;@|?(Ci9zgTUqCBAz-M3YG7hX-X%=xmNN(DgTA_lrHlActp1 zPc=xo*BTGcBAM5U!1T(auChK=R$wcRH=|9BCCGHns{ITGU~9Y=ovOF4&h{JCFNUg>_@I0g$H7vl3>r|Hhh`drWn=^BO(c&UO0I zEZ9`MlJaQKf9wa>UyZBpZE&kqq;lCo$vd%2>Go;45+HE{*L5WG_JvqPgs8n|GM~li z1d7$Ek!hz$xx24QcK<3}k~CDB*RvAy(n9MF+f|uf559}>^?ldsIV;P-sr?HvJtBc0 zC(9DC^3fS|Sf702rJo+pSsqUk8OCiM&zS?ygH<>Tp8=C6>idqx_~`T2Xvtxi4)d=J z=@dN{niUJneaWmy4BnK_!8ok5EQ%rZFbB_wA^pbJ!NnTNX zuy?vp{gyn8hY)35O_21?qTUENIqX69Q9zMP=*@fSwR4+|wIF*{Da6lGmrx#Hv-5Pm zKy}TjhW}2|Nic??XTE!uXmTYRPy^30FFGTcFquq}@m$RUt=436o)T{tHH4<55BWsR}0!e&m*tPD-2tNyVUye-)W(`&IwNUim zQIpLXP6rjaogLoebVq1A#)RsImU-BucR9#3cCbx27?3!0to zgleQKx8L+Uo}26V*C{qM7f0pbO9T~FrFXE9E`uq7^wRyid&DHQwv}@e-b`JRp@JyF z&Q+bOarf>;m>1g{7^nhUf^+tkTQ<9-hAFDT2%!g07t>-FGbRGfB!M6)LLZOt@imp# z!V2wdw>N;f;eLms4qhbA#pmtr2VKY2C2-V6K)Sxqcy&&wFOd``_pgX@EuvU?!qU{d z%5@^254x^;(@!hE-ofs&B3Q-fHM?n%-}tC>yYpuL$#gucbk|yO>+yHV#Dh$!e{3sO zbd8fK1Yuhe(k3sp`)XODWvVH06O>>L3xLqr49(5bbN5tPK6P4kg-|NU!e+#&qJQxl zrDZ48*;~BsQ=>%wso7!dgjvcy`0o(ihWgd$&CnnZnMK2N_nl8Qg>Q}1T(hMf7lPES z&jWO>-Tb(fdG-2Ey=~GuI{kK)1ng~OeOh6PaC0tz*-i}lHYx0w6?Hqf4@>@O&SCPV zz33v`v!iJIuCStDz1gRa0Np`{zPs0k>fDBsqM42OJ=H*m1^aB5zheUt5Uvv z?6z)$0lC_o%;5Umcx1yB&z-f1b3W%#o$s`t3g<)N8?#mU5rv%+FnRFzg~Ce7XcE+oCqG(!oK}-e4#VlFY_%3Jun2jUp zw%Ja~r?nrIb>P_*+R%utOn%F1YV+6wqEB6OvAaC8^NytZ@ZT3Lsb6@A7W(w=6C-X5 zU(eJvWVCvG6)qy4+N?l$D}b?^It;S(mb||`iF*!>n|GSr>ei_-bn;DqUlPhp-1Oe& z9F_N$?`ZDM4fzXKWqkUqQYNs1MF!V_ds(?wc!e?c zC zkP}FKdJ@VJy(YAXjLb0UT7zMUYxpREiUUy3!x!0ZG?ZB!$6XR-;43&bNkd}2Kd_&@ z!VvabIi#(IR=+4*j~~M1OLm%e(ryxp7CUaM>V6-l z5?62>G=&J_Wgdj*Byy#g4$213zQ0whQJ$Od1cks?oX1}D;w5~a@Izn2lHNB*XFReBFeXFC+A>(-c{rBIYW;M1Aw_m2T zgkKofXahd(j4c113kkg+LeuW42HlexnCtcQ+r)PSr-Iyw7Cp^rk+ly1{SeYZ3# zHE?+n0uGtg0Q24T-@MToPsAF-vn0RDbct>EohS2bTotIxT9_+eToo0?3evRlHS_TQ zT>9nTQZ4YejZ3;GuzNQ$1=M8xlV&g9ssAee>T|3-Iq~?2!8_8sr$Ak~24&9up@=@A z=%w-t%_)(`YbiyERES*G5dBx%;+jo2_ya3MZWE3~D)vt>1luPw+$-Pts#pHy z63aNA%FVpm{#4nuHh)M+r|kjRA}`%3QW{`a%7E%_5LI|6+=^)U;Oi-*G1p>fK&KPX zv+%bs2Q!Sd1V^qczJDN0$lF9o&lsJ>o&MmnV7?_aE{?HlxAw@s$3i=y;;7H;Q0U|k+_!JFxVxGO%e{~64#MW6qs+K|(xy{T0S;c{Il?))VV@X+@z z*pEzN>i2o6oXGhlmz50jnzpV`Oa#PNaEd9@Aqk%#KDl1Dct&~YMiIkCpUqmI;jivw zwCBVIuUfu{S#Wu6;gyYZW0Ua;mNNc=1#M$Ha5Ki_{8gV3VtOr{PTmO0ezHGwVX!4?j$+0^E=!LB7JDz^8AV2>8FH5{ z^EL_({Q7(1E;#rV@_Sld1(*yC|>L_sgGd zldffcVcv)*0V9|~S(3aRLpg*~^b5@~q~wRKJfNT9I#d*?fn#)7ad>+qpR1`BwMf`Z z7L6d(;Z&kjdLj8cL}4Gqk}Q|XeRIj>BaDf@%#^WaQ+!xv44?G}e1HD_^8NLkU(Vy4 z^TRoh*Lj|uS4@X?itQH0rK&(^e-(8^kl?h4iab%-^7=7^IS~W5yB8pu{Lt)vbAyC3 zG{kZ9C*!uYAC4J zMkbpt{9&)-k=)ve{^mJ(2OBq>G%dXB=H~ejKha+*{uY#X#5T1cA)oTnp|Jep&2#q3 zVk_3ZJ8?bg>%ZUlF-R9%YX|RZ5_N-?i?>YOTRR_@WG>Xo zG5i*88Z4&n)D^xpkWFMF{myR)W^IN%H#2te-u3g-q60!|dMQvjTT;zxeaeo+FJ@jLsIPo&6-LpK8a zLe^M->*dF=aaL$Bqfx_JZfcLC16`QWcS+-OP2C^-Khoq;$HJb*o?upo<5BAgk9Q|7 zG5eVql%8SDysEBJTVGOVd-w~v;-}2GG`6_fP{|d(%9k98iQYtRQQy044;Vs@ z^H%F#?3x(lyt)UnE^)zyVum{-A*{0Xto@ZqWXYWd-A9IWX4}%~Wl;%2m)N;kQaCC$ z`^*?+uD-V~rbT_ae(C!1ROxg3jXZ`tENKZ+(kOxLdZUV1cs2{>b}LJN_!`ld<(WA% zKD;7+I%Sw$4I;sdEziH4yI9ccRpeX!-9U(o$@JTkqRg1%r%U^pGK%=z1Fztv8#6bY zGR);+UzQ;4qQoU}tWfTLNBYVR)ZL1>h|=43ozK`SKG z2{F3~=c+!K@q5$jd&kEc>p-yft!lz)GRv5H7j;mp)1z(A1lS%<#%Yy3N4|+)x}B&J zf)}qh-4AFW0)IVuRvJ+=KXvC`V{ChCMgSyotgd_c`eN0n8TYE~5%Jp?B#4w^7m?q& zQuYNkHA$d;CoCO6))6rorrC{?8UPn&lIkp|OhQfZ1JH{E!S%dy&X{?Nt5HTw*o+oH z_tXpve^U#!0u>2GYg0O+B!(dQn71iOXrL_(C~=B=ZOvxHP2~&bkAl4y8z1@ll>|m+ z6nB|0#{k%Srn$?vQEQc>HtBXSl)rnxC!;{3SAXU}ks3g{I*3G{s6F1G(I>;DnpFG5 z9}O>94+TgU+I(`t>o3+y(7$=Q*M4=-nnV8Ix@+}ItGZKXT2 zt4&%!(en3GNKqI8XeCw#av-vQ;KXwJFWjkIS#E(T?bmUiYn?=yS{9 zA=Bhbi$*YLObOg2dK3=DEFkdDThsf;n``{*0X@suu&av^5jvea=*?9r?J2T5Ww-`6)2iKm zY2m|t?q7$48R~rK69z<;ET5H}nN7k6vLH6Wl>GFKs<|PVp?w zX8ziALg|peMU?LJf#3ScP2L7w!UKMtRP1n zti#6COa1p*1iQ6&;vqiGMsAqW{b;4}r2|&dJ-|&((8a|ZO03toDoGXz0PQ~5eaI(i z!Kiq*&Z;_TAU~Gqth$?o*j1(vqx!dnq{#uV$xtgxs#c*a*Gz8S$(L{r@x(655x|~$ zQ0J*6l@BQ!Z{B-0*Dgk4Qo=(Dhh%R^(3UU`)Qh*K%Krns) z$3c?^q%8)L=fNXGnSD5boL{!d%iQeU31x{L^wgG65D>Xp|)n;AA+B zPFQIRw&F*=-e!Tj5ulQ%U6Bp?ug{JC@AzM2pnG9k;W)sZxuWg#`ru#1Azifq literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.svg b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.svg new file mode 100644 index 00000000..e4624f71 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/logo.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/note.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/note.png new file mode 100644 index 0000000000000000000000000000000000000000..88d997b17cfe703280d3a33e3f7c285a4ce6c8a6 GIT binary patch literal 2257 zcmbVOc~}!?8jt0%wJO?rR6vCx90KN;$uT)dKypElKnRgUJaCwd5Fm%i5JI?F5NZ{L zHB!K;6~%M$V1?>pq28#7tClK{yXb1Wwu?(E7V(JeM8)nOZvWVMX6F08ci!Lcy`N`3 zRmMkqPWG8hB9T1hF%lKAdbyuT9>n{*eLWY6#T%Du@g&n~JQuNGq$r&!4Flu`Bpp*> zh%RqUHzpvFJTmlZEv{9>@llh3hPZWTc7vHflSqO{yBR^VFdRt3()C6mdFU^lWI(SI zk~M4vs4$DM41J8lf+acP)u|5Q7d9H%x_Cd^XHyaDX=#nXqQj zt>&vFvNyJflaQQ&<7Pgco|~IX%Vp9`mUKGA-Zp( zOJtG50yzv2=0Xrx46(Qj83@V53@*$QjdQ#U%j3e3l*8gOAt(xhqztY^3`s$@h$SN! zBqG*0R&KQ7h!Mrc?dl1;Z?K%-#qz}#48ctnwaJt{-T}%C6K=9*n9P7U2?jzG2&y-_ z1)=T&y^dFcS@bqcC$pFgz^e@N_3!Y2&4rmVrc?^b{#WF$vAX{!YjnaHy1PC8t6j!L zL=U>RZ=0Vuyd59RNX(3d7!LK(`xl6rBPrw5(!bs96XC4+vN`n!pkNhnvKs;x&pmV! z^p5t4F5vmbc<*Tg!?VGlB>@XnzNdTWfhvE25$e1Mo;VNrFPPX7U z(3k?AET0>c;EQjdF;|7qmM;id8Z2DH;$?xdi*jLQS;UTmTiQ;84~KpVQTS}!1G7>???1T1M8Y2Y^v{gyWH4>vrEALt zW@fUDlD9Q{=doHaIiz}LsVtu#Tf|>gNSPn)uUj7xxbS*QsNLaH+;@qq1yM5)eX8Xer{FRzM~ z7xK|ff|w#cs13!6!+4oAeiqo&#|^o&-HT^JJ+1KLT73G&i2y$6Z`@c^KzV`9OsHC8!WcLRbRl_HObYx+233S$HvBP zx5xC8NE3$Tk|?#kKW%jS#1E2l4~Dm9y?iNEMtGE`{31iwDR0{;frQ`P~3kjC$lu_eqZs}wAR(baf^>n-dr`hd)oUmm$gnF zbD^_eYPM#zynGxf-a!tS6u8|n_+WHspz~^faii1kX{e03c}{&}W2fu+^!IEjlU-

MvJZJ$)LTqJA+@mbLxmkyPc#tU=W5xPJ%q2sZXv`v(Ui?>!8Tjh_mSOc$O+ zW<-$ZjJfV@LAsB%Biz5w(;fXV?CW1TB9(ujH2(XqZD*&_2O2L-EZJ~mTUSoq*g)q^ zQ!j3qa>DzQ*dH!xN(0O3n$-7HmkYk_eQXG-gI*K|{dncP!DXswNa?P_Z}nzo#*v#J zQ5S9ROsaZ%ZqC6y?VF!Q1^;o|wu*kH=E=`8B``9)uFtN|s?>Xw*7?*`wfqP}<_A~q zd8VVPq*k-7ZPhbSEogsT%F|x0xuT7xdRv7>Rev?4wv{qrDN}+xS$8V5!!ga&#Y1*BgqL?&c}jPc zG_JlfMSD5I%DQQcHXTbGWQtKpeL6yAB|UI5CQ=~#`}=c}Um;E%R)9u^qI0>&GHQ-g zOm;DCkym+{WF$}@UWrV1mtnTPtu!WtY$r7BOpo|N_#mqWGhK#KR0MD7eW*yPaY&xBTRfcG-E5p&`2dq z875XFdy+3GStd(wD_Mg`dys8Xd_houJju_&*4)mZt2Tk1H)DTRJY^_lf>>*ZU2Th5 zWQ3Ly{;kf91GM2s4Vfv8a-fcsXpb+4t> zmM%11X*>M&PQZNVdARf4d*2x!aq1>jOzQ?>>R)(Ok;sOJ)7jfk$Fdif23? z-}3V78&9qod*O;uGk%fEW^;|k`Lo>bOq2iF72o-IGb2gTw+4B~#iYz(oL}sS7|$R2 zDGfrR{|@~AQJ&v(#4u|ZtJP}t520N48P!$8U;|Vfuq=8>E$w`o2Jf`%eqhqbr%IH1zV?O3uDWqKZId-wMQ*MFefpD5X*w@ zok{kNA?%%$F{M!OUcE^^x{~(wkHK|_*9Yg`KNS88FaVH_sda1Xfs6nE002ovPDHLk FV1jwin)(0$ literal 0 HcmV?d00001 diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/warning.png b/applications/apps-metadata/stream-apps-docs/src/main/docbook/images/warning.png new file mode 100644 index 0000000000000000000000000000000000000000..0d5b5244605adbb7ab05a1549746a9c35490f95b GIT binary patch literal 2130 zcmbVNYg7|w8V(4q($)50y>JmGlLW#g$xLn}Vd2Si)}#|ptPAQp3Bp-3!-lL0;i^LY?;i#f0m5s49g z3h?3rDQg~EDPlm?FKkgKIcWEK-3X6YU0uzs7H|nq84s39r2!5;pF?SI$QqXy^Ko1x zV~zpENvp@<_Bsd`5MabC#3rvCq&$5dg43yGR zAdy0-rWjC#a1N_+kzUMY#pmogD7!DP(9dEKr3c5ngvUq_6>}Y+w-a81v=eSXnIi_+ zI?U>D1q2C!0zHox#XXKH+@|&rPT*OF5yvY$5J|)WwLqnU)c-5;=UChSlQkaY3@^|g z|J5#YBB}=i+n3Ex9bS$P?xJSKLk)-HHII@;3qG#TG^!+aj>0QkO~7kB0+|yM;YrGB zF>Ge@isQ#73%Tp#k_(tInh3U$uJWY-+DND*o}L-CB5g@#9YWWw~pxZ!Obm>yN#ZHFvL0zA3vNCTJ=t?)~`2O1M{L6un=ml<2x zQEYfifmA?Pz0tqRs^2Utt1pU0BQB1eIY0U_I}c1Y#PP7Ci5s7#IJn}xK{G+{_v^Z+1V#$Erodv>d}ew>RP0wzw+GWkGCBlS1Oj5Z!5NK zUuSR6>pa}RSEZ;qE_w1l#`hQX4E67U6NeP zHZ`T&wj0_H)t|Y1thUqnhub?%e)Y07;a^Tq%FO&iQkR&`qG!dh*2WfW(HuRQj*_DI zeCD1bUA|tMwjOb8E|FRIn$pzEU!2j_N}1Z2ig)vbr5xA0rjC70cmI6*%Uf->hWzaZ z{33HABO5q)vRhzDly2l691@+q3O{}NbSzx+I*k@|L4&3leK#$>u+T#TvTELfKb|IH zc23atf3vmfCa0&TWY@iuoA_Pm<0~ttEC*ut`isb^jXS>X^Sp=l?RXN_OJ*&usGXg$ zTlTv;Ch^vhLDf&+62jl64*&D+=+`sN_dap_1W%p|KQVYIdgPL5uRE9(c4On)DXndL zLNq?%!-u*zmMxyYc4m4Nr>>=A=s}PkJkqr&E^b9>5g+}c1%X}3|WKcg&spcJQ)05zI<<5LTBhNKRg$XRTi2{j)yl_ zj4~kKOvr+m(;vw~9zVh(zC-y9jqQnguz5r7FRq^MyKuXAENp(TAzUVtg&Ts+B_s7) zGn+fN0sG>9GjsPLKTRrvCd`71IZulJ1_1jO9KWbD&_@2UC+LTNpxxrdz#E|j|2nA9 zzB!UTyfAEJ&#%$pQ>QX#8vk@_a5iqukMF;8`wRKe{BI}5$H%OROs4J1#j)|?p|YSh z_SpR^e`VE#F52;WL{!+L(yZLRh40*KS;@box;9(-tE)`mcVp27O*>Z{_Lb*5T3cJA yr~0nPHtg2+UHi&&$8ha;`+hiaUmw&!n@8(?5PqF(KE5>Ym)EG)p&uyBP5%a8^# + + + + + + + 1 + 0 + 1 + + + + images/ + .png + + + book toc,title + 3 + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/epub.xsl b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/epub.xsl new file mode 100644 index 00000000..5484bb99 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/epub.xsl @@ -0,0 +1,31 @@ + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-multipage.xsl b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-multipage.xsl new file mode 100644 index 00000000..f9f5b272 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-multipage.xsl @@ -0,0 +1,73 @@ + + + + + + + + + + css/manual-multipage.css + + '5' + '1' + + + + + + + + + + + + + + + + + + + + firstpage + + + + + + + + + + + + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-singlepage.xsl b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-singlepage.xsl new file mode 100644 index 00000000..c7c0381b --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html-singlepage.xsl @@ -0,0 +1,30 @@ + + + + + + + + + + css/manual-singlepage.css + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html.xsl b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html.xsl new file mode 100644 index 00000000..9f036995 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/html.xsl @@ -0,0 +1,141 @@ + + + + + + + + + + + 1 + + + 1 + + + + 120 + images/callouts/ + .png + + + text/css + + text-align: left + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + + + + + + +

+

Authors

+ +
+ + + + + + + + + + + + + + + + + + # + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/pdf.xsl b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/pdf.xsl new file mode 100644 index 00000000..8278faa3 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/pdf.xsl @@ -0,0 +1,591 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + auto + + + + + underline + #204060 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + , + + + + + + + + + + + + + Copyright © + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -5em + -5em + 8pt + + + + + + + + + + + + + + + please define title in your docbook file! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 8pt + + + + + + + + + + + + + + + + + + + + + + + + + please define title in your docbook file! + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + 0 + + + + false + + + Helvetica + 10 + 8 + Helvetica + + + 1.4 + + + + left + bold + + + pt + + + + + + + + + + + + + + + 0.6em + 0.6em + 0.6em + + + pt + + 0.1em + 0.1em + 0.1em + + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + + 0.3em + 0.3em + 0.3em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + + + + + + + + + + + + 7pt + wrap + 1 + + + + 1em + 1em + 1em + 0.1em + 0.1em + 0.1em + + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + 0.1em + + + + 0.5em + 0.5em + 0.5em + 0.1em + 0.1em + 0.1em + + + + #444444 + solid + 0.1pt + #F0F0F0 + + + + + + + normal + italic + + + pt + + false + 0.1em + 0.1em + 0.1em + + + + + + 0 + 1 + + + 90 + + + + + + figure after + example after + equation before + table before + procedure before + + + + 1 + 0pt + + + + + + + + + + + + + + + + + + + + 18pt + + + + 0.1em + 2em + .75pt + solid + #5c5c4f + 0.5em + 1.5em + 1.5em + 1.5em + 1.5em + 1.5em + 1.5em + + + + 10pt + bold + false + always + 0 + + + + 0em + 0em + 0em + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl-config.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl-config.xml new file mode 100644 index 00000000..e354da15 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl-config.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/asciidoc-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/asciidoc-hl.xml new file mode 100644 index 00000000..5478b1d6 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/asciidoc-hl.xml @@ -0,0 +1,41 @@ + + + + + //// + //// + + + // + + + + ^(={1,6} .+)$ + + MULTILINE + + + ^(\.[^\.\s].+)$ + + MULTILINE + + + ^(:!?\w.*?:) + + MULTILINE + + + ^(-|\*{1,5}|\d*\.{1,5})(?= .+$) + + MULTILINE + + + ^(\[.+\])$ + + MULTILINE + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/bourne-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/bourne-hl.xml new file mode 100644 index 00000000..90408acc --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/bourne-hl.xml @@ -0,0 +1,95 @@ + + + + # + + << + ' + " + - + + + + + " + \ + + + ' + \ + + + + 0x + + + + . + + + + + + if + then + else + elif + fi + case + esac + for + while + until + do + done + + exec + shift + exit + times + break + export + trap + continue + readonly + wait + eval + return + + cd + echo + hash + pwd + read + set + test + type + ulimit + umask + unset + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/c-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/c-hl.xml new file mode 100644 index 00000000..7363dba9 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/c-hl.xml @@ -0,0 +1,117 @@ + + + + + /** + */ + + + + + + + + /* + */ + + // + + + # + \ + + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + l + + + + auto + _Bool + break + case + char + _Complex + const + continue + default + do + double + else + enum + extern + float + for + goto + if + _Imaginary + inline + int + long + register + restrict + return + short + signed + sizeof + static + struct + switch + typedef + union + unsigned + void + volatile + while + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/cpp-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/cpp-hl.xml new file mode 100644 index 00000000..b31f7362 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/cpp-hl.xml @@ -0,0 +1,151 @@ + + + + + /** + */ + + + + + + + + /* + */ + + // + + + # + \ + + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + l + + + + + auto + _Bool + break + case + char + _Complex + const + continue + default + do + double + else + enum + extern + float + for + goto + if + _Imaginary + inline + int + long + register + restrict + return + short + signed + sizeof + static + struct + switch + typedef + union + unsigned + void + volatile + while + + asm + dynamic_cast + namespace + reinterpret_cast + try + bool + explicit + new + static_cast + typeid + catch + false + operator + template + typename + class + friend + private + this + using + const_cast + inline + public + throw + virtual + delete + mutable + protected + true + wchar_t + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/csharp-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/csharp-hl.xml new file mode 100644 index 00000000..09373348 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/csharp-hl.xml @@ -0,0 +1,194 @@ + + + + + /** + */ + + + + /// + + + + /* + */ + + // + + + [ + ] + ( + ) + + + + # + \ + + + + + + @" + " + \ + + + + " + \ + + + ' + \ + + + 0x + ul + lu + u + l + + + + . + + e + ul + lu + u + f + d + m + l + + + + abstract + as + base + bool + break + byte + case + catch + char + checked + class + const + continue + decimal + default + delegate + do + double + else + enum + event + explicit + extern + false + finally + fixed + float + for + foreach + goto + if + implicit + in + int + interface + internal + is + lock + long + namespace + new + null + object + operator + out + override + params + private + protected + public + readonly + ref + return + sbyte + sealed + short + sizeof + stackalloc + static + string + struct + switch + this + throw + true + try + typeof + uint + ulong + unchecked + unsafe + ushort + using + virtual + void + volatile + while + + + + add + alias + from + get + global + group + into + join + orderby + partial + remove + select + set + value + where + yield + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/css-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/css-hl.xml new file mode 100644 index 00000000..21439ae6 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/css-hl.xml @@ -0,0 +1,176 @@ + + + + + /* + */ + + + " + \ + + + + ' + \ + + + + . + + + + @charset + @import + @media + @page + + + + - + azimuth + background-attachment + background-color + background-image + background-position + background-repeat + background + border-collapse + border-color + border-spacing + border-style + border-top + border-right + border-bottom + border-left + border-top-color + border-right-color + border-bottom-color + border-left-color + border-top-style + border-right-style + border-bottom-style + border-left-style + border-top-width + border-right-width + border-bottom-width + border-left-width + border-width + border + bottom + caption-side + clear + clip + color + content + counter-increment + counter-reset + cue-after + cue-before + cue + cursor + direction + display + elevation + empty-cells + float + font-family + font-size + font-style + font-variant + font-weight + font + height + left + letter-spacing + line-height + list-style-image + list-style-position + list-style-type + list-style + margin-right + margin-left + margin-top + margin-bottom + margin + max-height + max-width + min-height + min-width + orphans + outline-color + outline-style + outline-width + outline + overflow + padding-top + padding-right + padding-bottom + padding-left + padding + page-break-after + page-break-before + page-break-inside + pause-after + pause-before + pause + pitch-range + pitch + play-during + position + quotes + richness + right + speak-header + speak-numeral + speak-punctuation + speak + speech-rate + stress + table-layout + text-align + text-decoration + text-indent + text-transform + top + unicode-bidi + vertical-align + visibility + voice-family + volume + white-space + widows + width + word-spacing + z-index + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/html-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/html-hl.xml new file mode 100644 index 00000000..366cd46f --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/html-hl.xml @@ -0,0 +1,122 @@ + + + + + + + a + abbr + address + area + article + aside + audio + b + base + bdi + blockquote + body + br + button + caption + canvas + cite + code + command + col + colgroup + dd + del + dialog + div + dl + dt + em + embed + fieldset + figcaption + figure + font + form + footer + h1 + h2 + h3 + h4 + h5 + h6 + head + header + hr + html + i + iframe + img + input + ins + kbd + label + legend + li + link + map + mark + menu + menu + meta + nav + noscript + object + ol + optgroup + option + p + param + pre + q + samp + script + section + select + small + source + span + strong + style + sub + summary + sup + table + tbody + td + textarea + tfoot + th + thead + time + title + tr + track + u + ul + var + video + wbr + xmp + + + + + xsl: + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ini-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ini-hl.xml new file mode 100644 index 00000000..6bd60b3e --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ini-hl.xml @@ -0,0 +1,45 @@ + + + + ; + + + ^(\[.+\]\s*)$ + + MULTILINE + + + + ^(.+)(?==) + + MULTILINE + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/java-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/java-hl.xml new file mode 100644 index 00000000..4fe3c1fe --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/java-hl.xml @@ -0,0 +1,117 @@ + + + + + /** + */ + + + + /* + */ + + // + + " + \ + + + ' + \ + + + @ + ( + ) + + + 0x + + + + . + e + f + d + l + + + + abstract + boolean + break + byte + case + catch + char + class + const + continue + default + do + double + else + extends + final + finally + float + for + goto + if + implements + import + instanceof + int + interface + long + native + new + package + private + protected + public + return + short + static + strictfp + super + switch + synchronized + this + throw + throws + transient + try + void + volatile + while + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/javascript-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/javascript-hl.xml new file mode 100644 index 00000000..91620401 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/javascript-hl.xml @@ -0,0 +1,147 @@ + + + + + /* + */ + + // + + " + \ + + + ' + \ + + + 0x + + + + . + e + + + + break + case + catch + continue + default + delete + do + else + finally + for + function + if + in + instanceof + new + return + switch + this + throw + try + typeof + var + void + while + with + + abstract + boolean + byte + char + class + const + debugger + double + enum + export + extends + final + float + goto + implements + import + int + interface + long + native + package + private + protected + public + short + static + super + synchronized + throws + transient + volatile + + + prototype + + Array + Boolean + Date + Error + EvalError + Function + Math + Number + Object + RangeError + ReferenceError + RegExp + String + SyntaxError + TypeError + URIError + + decodeURI + decodeURIComponent + encodeURI + encodeURIComponent + eval + isFinite + isNaN + parseFloat + parseInt + + Infinity + NaN + undefined + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/json-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/json-hl.xml new file mode 100644 index 00000000..59b9c481 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/json-hl.xml @@ -0,0 +1,37 @@ + + + # + + " + \ + + + ' + \ + + + @ + ( + ) + + + . + e + f + d + l + + + + true + false + + + { + } + , + [ + ] + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/perl-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/perl-hl.xml new file mode 100644 index 00000000..c8a4d590 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/perl-hl.xml @@ -0,0 +1,120 @@ + + + + # + + << + ' + " + + + + " + \ + + + ' + \ + + + + 0x + + + + . + + + + + if + unless + while + until + foreach + else + elsif + for + when + default + given + + caller + continue + die + do + dump + eval + exit + goto + last + next + redo + return + sub + wantarray + + caller + import + local + my + package + use + + do + import + no + package + require + use + + bless + dbmclose + dbmopen + package + ref + tie + tied + untie + use + + and + or + not + eq + ne + lt + gt + le + ge + cmp + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/php-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/php-hl.xml new file mode 100644 index 00000000..3ea15787 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/php-hl.xml @@ -0,0 +1,154 @@ + + + + + /** + */ + + + + + + + + /* + */ + + // + # + + " + \ + + + + ' + \ + + + + <<< + + + 0x + + + + . + e + + + + and + or + xor + __FILE__ + exception + __LINE__ + array + as + break + case + class + const + continue + declare + default + die + do + echo + else + elseif + empty + enddeclare + endfor + endforeach + endif + endswitch + endwhile + eval + exit + extends + for + foreach + function + global + if + include + include_once + isset + list + new + print + require + require_once + return + static + switch + unset + use + var + while + __FUNCTION__ + __CLASS__ + __METHOD__ + final + php_user_filter + interface + implements + extends + public + private + protected + abstract + clone + try + catch + throw + cfunction + old_function + true + false + + namespace + __NAMESPACE__ + goto + __DIR__ + + + + + ?> + <?php + <?= + + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/properties-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/properties-hl.xml new file mode 100644 index 00000000..8205aac3 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/properties-hl.xml @@ -0,0 +1,38 @@ + + + + # + + ^(.+?)(?==|:) + + MULTILINE + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/python-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/python-hl.xml new file mode 100644 index 00000000..72bf0dbb --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/python-hl.xml @@ -0,0 +1,100 @@ + + + + + + @ + ( + ) + + # + + """ + + + + ''' + + + + " + \ + + + ' + \ + + + 0x + l + + + + . + + e + l + + + + and + del + from + not + while + as + elif + global + or + with + assert + else + if + pass + yield + break + except + import + print + class + exec + in + raise + continue + finally + is + return + def + for + lambda + try + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ruby-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ruby-hl.xml new file mode 100644 index 00000000..a2cee724 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/ruby-hl.xml @@ -0,0 +1,109 @@ + + + + # + + << + + + + " + \ + + + %Q{ + } + \ + + + %/ + / + \ + + + ' + \ + + + %q{ + } + \ + + + 0x + + + + . + e + + + + alias + and + BEGIN + begin + break + case + class + def + defined + do + else + elsif + END + end + ensure + false + for + if + in + module + next + nil + not + or + redo + rescue + retry + return + self + super + then + true + undef + unless + until + when + while + yield + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/sql2003-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/sql2003-hl.xml new file mode 100644 index 00000000..65c08a36 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/sql2003-hl.xml @@ -0,0 +1,565 @@ + + + + -- + + /* + */ + + + ' + + + + U' + ' + + + + B' + ' + + + + N' + ' + + + + X' + ' + + + + . + + e + + + + + + A + ABS + ABSOLUTE + ACTION + ADA + ADMIN + AFTER + ALWAYS + ASC + ASSERTION + ASSIGNMENT + ATTRIBUTE + ATTRIBUTES + AVG + BEFORE + BERNOULLI + BREADTH + C + CARDINALITY + CASCADE + CATALOG_NAME + CATALOG + CEIL + CEILING + CHAIN + CHAR_LENGTH + CHARACTER_LENGTH + CHARACTER_SET_CATALOG + CHARACTER_SET_NAME + CHARACTER_SET_SCHEMA + CHARACTERISTICS + CHARACTERS + CHECKED + CLASS_ORIGIN + COALESCE + COBOL + CODE_UNITS + COLLATION_CATALOG + COLLATION_NAME + COLLATION_SCHEMA + COLLATION + COLLECT + COLUMN_NAME + COMMAND_FUNCTION_CODE + COMMAND_FUNCTION + COMMITTED + CONDITION_NUMBER + CONDITION + CONNECTION_NAME + CONSTRAINT_CATALOG + CONSTRAINT_NAME + CONSTRAINT_SCHEMA + CONSTRAINTS + CONSTRUCTORS + CONTAINS + CONVERT + CORR + COUNT + COVAR_POP + COVAR_SAMP + CUME_DIST + CURRENT_COLLATION + CURSOR_NAME + DATA + DATETIME_INTERVAL_CODE + DATETIME_INTERVAL_PRECISION + DEFAULTS + DEFERRABLE + DEFERRED + DEFINED + DEFINER + DEGREE + DENSE_RANK + DEPTH + DERIVED + DESC + DESCRIPTOR + DIAGNOSTICS + DISPATCH + DOMAIN + DYNAMIC_FUNCTION_CODE + DYNAMIC_FUNCTION + EQUALS + EVERY + EXCEPTION + EXCLUDE + EXCLUDING + EXP + EXTRACT + FINAL + FIRST + FLOOR + FOLLOWING + FORTRAN + FOUND + FUSION + G + GENERAL + GO + GOTO + GRANTED + HIERARCHY + IMPLEMENTATION + INCLUDING + INCREMENT + INITIALLY + INSTANCE + INSTANTIABLE + INTERSECTION + INVOKER + ISOLATION + K + KEY_MEMBER + KEY_TYPE + KEY + LAST + LENGTH + LEVEL + LN + LOCATOR + LOWER + M + MAP + MATCHED + MAX + MAXVALUE + MESSAGE_LENGTH + MESSAGE_OCTET_LENGTH + MESSAGE_TEXT + MIN + MINVALUE + MOD + MORE + MUMPS + NAME + NAMES + NESTING + NEXT + NORMALIZE + NORMALIZED + NULLABLE + NULLIF + NULLS + NUMBER + OBJECT + OCTET_LENGTH + OCTETS + OPTION + OPTIONS + ORDERING + ORDINALITY + OTHERS + OVERLAY + OVERRIDING + PAD + PARAMETER_MODE + PARAMETER_NAME + PARAMETER_ORDINAL_POSITION + PARAMETER_SPECIFIC_CATALOG + PARAMETER_SPECIFIC_NAME + PARAMETER_SPECIFIC_SCHEMA + PARTIAL + PASCAL + PATH + PERCENT_RANK + PERCENTILE_CONT + PERCENTILE_DISC + PLACING + PLI + POSITION + POWER + PRECEDING + PRESERVE + PRIOR + PRIVILEGES + PUBLIC + RANK + READ + RELATIVE + REPEATABLE + RESTART + RETURNED_CARDINALITY + RETURNED_LENGTH + RETURNED_OCTET_LENGTH + RETURNED_SQLSTATE + ROLE + ROUTINE_CATALOG + ROUTINE_NAME + ROUTINE_SCHEMA + ROUTINE + ROW_COUNT + ROW_NUMBER + SCALE + SCHEMA_NAME + SCHEMA + SCOPE_CATALOG + SCOPE_NAME + SCOPE_SCHEMA + SECTION + SECURITY + SELF + SEQUENCE + SERIALIZABLE + SERVER_NAME + SESSION + SETS + SIMPLE + SIZE + SOURCE + SPACE + SPECIFIC_NAME + SQRT + STATE + STATEMENT + STDDEV_POP + STDDEV_SAMP + STRUCTURE + STYLE + SUBCLASS_ORIGIN + SUBSTRING + SUM + TABLE_NAME + TABLESAMPLE + TEMPORARY + TIES + TOP_LEVEL_COUNT + TRANSACTION_ACTIVE + TRANSACTION + TRANSACTIONS_COMMITTED + TRANSACTIONS_ROLLED_BACK + TRANSFORM + TRANSFORMS + TRANSLATE + TRIGGER_CATALOG + TRIGGER_NAME + TRIGGER_SCHEMA + TRIM + TYPE + UNBOUNDED + UNCOMMITTED + UNDER + UNNAMED + USAGE + USER_DEFINED_TYPE_CATALOG + USER_DEFINED_TYPE_CODE + USER_DEFINED_TYPE_NAME + USER_DEFINED_TYPE_SCHEMA + VIEW + WORK + WRITE + ZONE + + ADD + ALL + ALLOCATE + ALTER + AND + ANY + ARE + ARRAY + AS + ASENSITIVE + ASYMMETRIC + AT + ATOMIC + AUTHORIZATION + BEGIN + BETWEEN + BIGINT + BINARY + BLOB + BOOLEAN + BOTH + BY + CALL + CALLED + CASCADED + CASE + CAST + CHAR + CHARACTER + CHECK + CLOB + CLOSE + COLLATE + COLUMN + COMMIT + CONNECT + CONSTRAINT + CONTINUE + CORRESPONDING + CREATE + CROSS + CUBE + CURRENT_DATE + CURRENT_DEFAULT_TRANSFORM_GROUP + CURRENT_PATH + CURRENT_ROLE + CURRENT_TIME + CURRENT_TIMESTAMP + CURRENT_TRANSFORM_GROUP_FOR_TYPE + CURRENT_USER + CURRENT + CURSOR + CYCLE + DATE + DAY + DEALLOCATE + DEC + DECIMAL + DECLARE + DEFAULT + DELETE + DEREF + DESCRIBE + DETERMINISTIC + DISCONNECT + DISTINCT + DOUBLE + DROP + DYNAMIC + EACH + ELEMENT + ELSE + END + END-EXEC + ESCAPE + EXCEPT + EXEC + EXECUTE + EXISTS + EXTERNAL + FALSE + FETCH + FILTER + FLOAT + FOR + FOREIGN + FREE + FROM + FULL + FUNCTION + GET + GLOBAL + GRANT + GROUP + GROUPING + HAVING + HOLD + HOUR + IDENTITY + IMMEDIATE + IN + INDICATOR + INNER + INOUT + INPUT + INSENSITIVE + INSERT + INT + INTEGER + INTERSECT + INTERVAL + INTO + IS + ISOLATION + JOIN + LANGUAGE + LARGE + LATERAL + LEADING + LEFT + LIKE + LOCAL + LOCALTIME + LOCALTIMESTAMP + MATCH + MEMBER + MERGE + METHOD + MINUTE + MODIFIES + MODULE + MONTH + MULTISET + NATIONAL + NATURAL + NCHAR + NCLOB + NEW + NO + NONE + NOT + NULL + NUMERIC + OF + OLD + ON + ONLY + OPEN + OR + ORDER + OUT + OUTER + OUTPUT + OVER + OVERLAPS + PARAMETER + PARTITION + PRECISION + PREPARE + PRIMARY + PROCEDURE + RANGE + READS + REAL + RECURSIVE + REF + REFERENCES + REFERENCING + REGR_AVGX + REGR_AVGY + REGR_COUNT + REGR_INTERCEPT + REGR_R2 + REGR_SLOPE + REGR_SXX + REGR_SXY + REGR_SYY + RELEASE + RESULT + RETURN + RETURNS + REVOKE + RIGHT + ROLLBACK + ROLLUP + ROW + ROWS + SAVEPOINT + SCROLL + SEARCH + SECOND + SELECT + SENSITIVE + SESSION_USER + SET + SIMILAR + SMALLINT + SOME + SPECIFIC + SPECIFICTYPE + SQL + SQLEXCEPTION + SQLSTATE + SQLWARNING + START + STATIC + SUBMULTISET + SYMMETRIC + SYSTEM_USER + SYSTEM + TABLE + THEN + TIME + TIMESTAMP + TIMEZONE_HOUR + TIMEZONE_MINUTE + TO + TRAILING + TRANSLATION + TREAT + TRIGGER + TRUE + UESCAPE + UNION + UNIQUE + UNKNOWN + UNNEST + UPDATE + UPPER + USER + USING + VALUE + VALUES + VAR_POP + VAR_SAMP + VARCHAR + VARYING + WHEN + WHENEVER + WHERE + WIDTH_BUCKET + WINDOW + WITH + WITHIN + WITHOUT + YEAR + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/yaml-hl.xml b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/yaml-hl.xml new file mode 100644 index 00000000..a28008ec --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/docbook/xsl/xslthl/yaml-hl.xml @@ -0,0 +1,47 @@ + + + # + + " + \ + + + ' + \ + + + @ + ( + ) + + + . + e + f + d + l + + + + true + false + + + { + } + , + [ + ] + + + + ^(---)$ + + MULTILINE + + + ^(.+?)(?==|:) + + MULTILINE + + diff --git a/applications/apps-metadata/stream-apps-docs/src/main/javadoc/spring-javadoc.css b/applications/apps-metadata/stream-apps-docs/src/main/javadoc/spring-javadoc.css new file mode 100644 index 00000000..06ad4227 --- /dev/null +++ b/applications/apps-metadata/stream-apps-docs/src/main/javadoc/spring-javadoc.css @@ -0,0 +1,599 @@ +/* Javadoc style sheet */ +/* +Overall document style +*/ + +@import url('resources/fonts/dejavu.css'); + +body { + background-color:#ffffff; + color:#353833; + font-family:'DejaVu Sans', Arial, Helvetica, sans-serif; + font-size:14px; + margin:0; +} +a:link, a:visited { + text-decoration:none; + color:#4A6782; +} +a:hover, a:focus { + text-decoration:none; + color:#bb7a2a; +} +a:active { + text-decoration:none; + color:#4A6782; +} +a[name] { + color:#353833; +} +a[name]:hover { + text-decoration:none; + color:#353833; +} +pre { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; +} +h1 { + font-size:20px; +} +h2 { + font-size:18px; +} +h3 { + font-size:16px; + font-style:italic; +} +h4 { + font-size:13px; +} +h5 { + font-size:12px; +} +h6 { + font-size:11px; +} +ul { + list-style-type:disc; +} +code, tt { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; + margin-top:8px; + line-height:1.4em; +} +dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + padding-top:4px; +} +table tr td dt code { + font-family:'DejaVu Sans Mono', monospace; + font-size:14px; + vertical-align:top; + padding-top:4px; +} +sup { + font-size:8px; +} +/* +Document title and Copyright styles +*/ +.clear { + clear:both; + height:0px; + overflow:hidden; +} +.aboutLanguage { + float:right; + padding:0px 21px; + font-size:11px; + z-index:200; + margin-top:-9px; +} +.legalCopy { + margin-left:.5em; +} +.bar a, .bar a:link, .bar a:visited, .bar a:active { + color:#FFFFFF; + text-decoration:none; +} +.bar a:hover, .bar a:focus { + color:#bb7a2a; +} +.tab { + background-color:#0066FF; + color:#ffffff; + padding:8px; + width:5em; + font-weight:bold; +} +/* +Navigation bar styles +*/ +.bar { + background-color:#4D7A97; + color:#FFFFFF; + padding:.8em .5em .4em .8em; + height:auto;/*height:1.8em;*/ + font-size:11px; + margin:0; +} +.topNav { + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.bottomNav { + margin-top:10px; + background-color:#4D7A97; + color:#FFFFFF; + float:left; + padding:0; + width:100%; + clear:right; + height:2.8em; + padding-top:10px; + overflow:hidden; + font-size:12px; +} +.subNav { + background-color:#dee3e9; + float:left; + width:100%; + overflow:hidden; + font-size:12px; +} +.subNav div { + clear:left; + float:left; + padding:0 0 5px 6px; + text-transform:uppercase; +} +ul.navList, ul.subNavList { + float:left; + margin:0 25px 0 0; + padding:0; +} +ul.navList li{ + list-style:none; + float:left; + padding: 5px 6px; + text-transform:uppercase; +} +ul.subNavList li{ + list-style:none; + float:left; +} +.topNav a:link, .topNav a:active, .topNav a:visited, .bottomNav a:link, .bottomNav a:active, .bottomNav a:visited { + color:#FFFFFF; + text-decoration:none; + text-transform:uppercase; +} +.topNav a:hover, .bottomNav a:hover { + text-decoration:none; + color:#bb7a2a; + text-transform:uppercase; +} +.navBarCell1Rev { + background-color:#F8981D; + color:#253441; + margin: auto 5px; +} +.skipNav { + position:absolute; + top:auto; + left:-9999px; + overflow:hidden; +} +/* +Page header and footer styles +*/ +.header, .footer { + clear:both; + margin:0 20px; + padding:5px 0 0 0; +} +.indexHeader { + margin:10px; + position:relative; +} +.indexHeader span{ + margin-right:15px; +} +.indexHeader h1 { + font-size:13px; +} +.title { + color:#2c4557; + margin:10px 0; +} +.subTitle { + margin:5px 0 0 0; +} +.header ul { + margin:0 0 15px 0; + padding:0; +} +.footer ul { + margin:20px 0 5px 0; +} +.header ul li, .footer ul li { + list-style:none; + font-size:13px; +} +/* +Heading styles +*/ +div.details ul.blockList ul.blockList ul.blockList li.blockList h4, div.details ul.blockList ul.blockList ul.blockListLast li.blockList h4 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList ul.blockList li.blockList h3 { + background-color:#dee3e9; + border:1px solid #d0d9e0; + margin:0 0 6px -8px; + padding:7px 5px; +} +ul.blockList ul.blockList li.blockList h3 { + padding:0; + margin:15px 0; +} +ul.blockList li.blockList h2 { + padding:0px 0 20px 0; +} +/* +Page layout container styles +*/ +.contentContainer, .sourceContainer, .classUseContainer, .serializedFormContainer, .constantValuesContainer { + clear:both; + padding:10px 20px; + position:relative; +} +.indexContainer { + margin:10px; + position:relative; + font-size:12px; +} +.indexContainer h2 { + font-size:13px; + padding:0 0 3px 0; +} +.indexContainer ul { + margin:0; + padding:0; +} +.indexContainer ul li { + list-style:none; + padding-top:2px; +} +.contentContainer .description dl dt, .contentContainer .details dl dt, .serializedFormContainer dl dt { + font-size:12px; + font-weight:bold; + margin:10px 0 0 0; + color:#4E4E4E; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + margin:5px 0 10px 0px; + font-size:14px; + font-family:'DejaVu Sans Mono',monospace; +} +.serializedFormContainer dl.nameValue dt { + margin-left:1px; + font-size:1.1em; + display:inline; + font-weight:bold; +} +.serializedFormContainer dl.nameValue dd { + margin:0 0 0 1px; + font-size:1.1em; + display:inline; +} +/* +List styles +*/ +ul.horizontal li { + display:inline; + font-size:0.9em; +} +ul.inheritance { + margin:0; + padding:0; +} +ul.inheritance li { + display:inline; + list-style:none; +} +ul.inheritance li ul.inheritance { + margin-left:15px; + padding-left:15px; + padding-top:1px; +} +ul.blockList, ul.blockListLast { + margin:10px 0 10px 0; + padding:0; +} +ul.blockList li.blockList, ul.blockListLast li.blockList { + list-style:none; + margin-bottom:15px; + line-height:1.4; +} +ul.blockList ul.blockList li.blockList, ul.blockList ul.blockListLast li.blockList { + padding:0px 20px 5px 10px; + border:1px solid #ededed; + background-color:#f8f8f8; +} +ul.blockList ul.blockList ul.blockList li.blockList, ul.blockList ul.blockList ul.blockListLast li.blockList { + padding:0 0 5px 8px; + background-color:#ffffff; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockList { + margin-left:0; + padding-left:0; + padding-bottom:15px; + border:none; +} +ul.blockList ul.blockList ul.blockList ul.blockList li.blockListLast { + list-style:none; + border-bottom:none; + padding-bottom:0; +} +table tr td dl, table tr td dl dt, table tr td dl dd { + margin-top:0; + margin-bottom:1px; +} +/* +Table styles +*/ +.overviewSummary, .memberSummary, .typeSummary, .useSummary, .constantsSummary, .deprecatedSummary { + width:100%; + border-left:1px solid #EEE; + border-right:1px solid #EEE; + border-bottom:1px solid #EEE; +} +.overviewSummary, .memberSummary { + padding:0px; +} +.overviewSummary caption, .memberSummary caption, .typeSummary caption, +.useSummary caption, .constantsSummary caption, .deprecatedSummary caption { + position:relative; + text-align:left; + background-repeat:no-repeat; + color:#253441; + font-weight:bold; + clear:none; + overflow:hidden; + padding:0px; + padding-top:10px; + padding-left:1px; + margin:0px; + white-space:pre; +} +.overviewSummary caption a:link, .memberSummary caption a:link, .typeSummary caption a:link, +.useSummary caption a:link, .constantsSummary caption a:link, .deprecatedSummary caption a:link, +.overviewSummary caption a:hover, .memberSummary caption a:hover, .typeSummary caption a:hover, +.useSummary caption a:hover, .constantsSummary caption a:hover, .deprecatedSummary caption a:hover, +.overviewSummary caption a:active, .memberSummary caption a:active, .typeSummary caption a:active, +.useSummary caption a:active, .constantsSummary caption a:active, .deprecatedSummary caption a:active, +.overviewSummary caption a:visited, .memberSummary caption a:visited, .typeSummary caption a:visited, +.useSummary caption a:visited, .constantsSummary caption a:visited, .deprecatedSummary caption a:visited { + color:#FFFFFF; +} +.overviewSummary caption span, .memberSummary caption span, .typeSummary caption span, +.useSummary caption span, .constantsSummary caption span, .deprecatedSummary caption span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + padding-bottom:7px; + display:inline-block; + float:left; + background-color:#F8981D; + border: none; + height:16px; +} +.memberSummary caption span.activeTableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#F8981D; + height:16px; +} +.memberSummary caption span.tableTab span { + white-space:nowrap; + padding-top:5px; + padding-left:12px; + padding-right:12px; + margin-right:3px; + display:inline-block; + float:left; + background-color:#4D7A97; + height:16px; +} +.memberSummary caption span.tableTab, .memberSummary caption span.activeTableTab { + padding-top:0px; + padding-left:0px; + padding-right:0px; + background-image:none; + float:none; + display:inline; +} +.overviewSummary .tabEnd, .memberSummary .tabEnd, .typeSummary .tabEnd, +.useSummary .tabEnd, .constantsSummary .tabEnd, .deprecatedSummary .tabEnd { + display:none; + width:5px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .activeTableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + float:left; + background-color:#F8981D; +} +.memberSummary .tableTab .tabEnd { + display:none; + width:5px; + margin-right:3px; + position:relative; + background-color:#4D7A97; + float:left; + +} +.overviewSummary td, .memberSummary td, .typeSummary td, +.useSummary td, .constantsSummary td, .deprecatedSummary td { + text-align:left; + padding:0px 0px 12px 10px; + width:100%; +} +th.colOne, th.colFirst, th.colLast, .useSummary th, .constantsSummary th, +td.colOne, td.colFirst, td.colLast, .useSummary td, .constantsSummary td{ + vertical-align:top; + padding-right:0px; + padding-top:8px; + padding-bottom:3px; +} +th.colFirst, th.colLast, th.colOne, .constantsSummary th { + background:#dee3e9; + text-align:left; + padding:8px 3px 3px 7px; +} +td.colFirst, th.colFirst { + white-space:nowrap; + font-size:13px; +} +td.colLast, th.colLast { + font-size:13px; +} +td.colOne, th.colOne { + font-size:13px; +} +.overviewSummary td.colFirst, .overviewSummary th.colFirst, +.overviewSummary td.colOne, .overviewSummary th.colOne, +.memberSummary td.colFirst, .memberSummary th.colFirst, +.memberSummary td.colOne, .memberSummary th.colOne, +.typeSummary td.colFirst{ + width:25%; + vertical-align:top; +} +td.colOne a:link, td.colOne a:active, td.colOne a:visited, td.colOne a:hover, td.colFirst a:link, td.colFirst a:active, td.colFirst a:visited, td.colFirst a:hover, td.colLast a:link, td.colLast a:active, td.colLast a:visited, td.colLast a:hover, .constantValuesContainer td a:link, .constantValuesContainer td a:active, .constantValuesContainer td a:visited, .constantValuesContainer td a:hover { + font-weight:bold; +} +.tableSubHeadingColor { + background-color:#EEEEFF; +} +.altColor { + background-color:#FFFFFF; +} +.rowColor { + background-color:#EEEEEF; +} +/* +Content styles +*/ +.description pre { + margin-top:0; +} +.deprecatedContent { + margin:0; + padding:10px 0; +} +.docSummary { + padding:0; +} + +ul.blockList ul.blockList ul.blockList li.blockList h3 { + font-style:normal; +} + +div.block { + font-size:14px; + font-family:'DejaVu Serif', Georgia, "Times New Roman", Times, serif; +} + +td.colLast div { + padding-top:0px; +} + + +td.colLast a { + padding-bottom:3px; +} +/* +Formatting effect styles +*/ +.sourceLineNo { + color:green; + padding:0 30px 0 0; +} +h1.hidden { + visibility:hidden; + overflow:hidden; + font-size:10px; +} +.block { + display:block; + margin:3px 10px 2px 0px; + color:#474747; +} +.deprecatedLabel, .descfrmTypeLabel, .memberNameLabel, .memberNameLink, +.overrideSpecifyLabel, .packageHierarchyLabel, .paramLabel, .returnLabel, +.seeLabel, .simpleTagLabel, .throwsLabel, .typeNameLabel, .typeNameLink { + font-weight:bold; +} +.deprecationComment, .emphasizedPhrase, .interfaceName { + font-style:italic; +} + +div.block div.block span.deprecationComment, div.block div.block span.emphasizedPhrase, +div.block div.block span.interfaceName { + font-style:normal; +} + +div.contentContainer ul.blockList li.blockList h2{ + padding-bottom:0px; +} + + + +/* +Spring +*/ + +pre.code { + background-color: #F8F8F8; + border: 1px solid #CCCCCC; + border-radius: 3px 3px 3px 3px; + overflow: auto; + padding: 10px; + margin: 4px 20px 2px 0px; +} + +pre.code code, pre.code code * { + font-size: 1em; +} + +pre.code code, pre.code code * { + padding: 0 !important; + margin: 0 !important; +} + diff --git a/applications/pom.xml b/applications/pom.xml new file mode 100644 index 00000000..458eb2d5 --- /dev/null +++ b/applications/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + org.springframework.cloud.stream.app + stream-apps + 3.0.0.BUILD-SNAPSHOT + stream-apps + Infrastructure for stream applications + pom + + + apps-core + apps-metadata + source + sink + processor + + + diff --git a/applications/processor/bridge-processor/README.adoc b/applications/processor/bridge-processor/README.adoc new file mode 100644 index 00000000..82ce758b --- /dev/null +++ b/applications/processor/bridge-processor/README.adoc @@ -0,0 +1,10 @@ +//tag::ref-doc[] += Bridge Processor + +A processor that bridges the input and ouput by simply passing the incoming payload to the outbound. + +=== Payload + +Any + +//end::ref-doc[] diff --git a/applications/processor/bridge-processor/pom.xml b/applications/processor/bridge-processor/pom.xml new file mode 100644 index 00000000..e3b612a0 --- /dev/null +++ b/applications/processor/bridge-processor/pom.xml @@ -0,0 +1,80 @@ + + + 4.0.0 + bridge-processor + bridge-processor + bridge processor apps + 3.0.0.BUILD-SNAPSHOT + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + bridge + processor + ${project.version} + org.springframework.cloud.stream.app.BridgeProcessorConfiguration.class + bridgeFunction + + + + + org.springframework.cloud.stream.app + bridge-processor + ${project.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + diff --git a/applications/processor/bridge-processor/src/main/java/org/springframework/cloud/stream/app/BridgeProcessorConfiguration.java b/applications/processor/bridge-processor/src/main/java/org/springframework/cloud/stream/app/BridgeProcessorConfiguration.java new file mode 100644 index 00000000..22bf47ef --- /dev/null +++ b/applications/processor/bridge-processor/src/main/java/org/springframework/cloud/stream/app/BridgeProcessorConfiguration.java @@ -0,0 +1,34 @@ +/* + * Copyright 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 + * + * http://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; + +import java.util.function.Function; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Soby Chacko + */ +@Configuration +public class BridgeProcessorConfiguration { + + @Bean + public Function bridgeFunction() { + return Function.identity(); + } +} diff --git a/applications/processor/bridge-processor/src/test/java/org/springframework/cloud/stream/app/BridgeProcessorTests.java b/applications/processor/bridge-processor/src/test/java/org/springframework/cloud/stream/app/BridgeProcessorTests.java new file mode 100644 index 00000000..fa011636 --- /dev/null +++ b/applications/processor/bridge-processor/src/test/java/org/springframework/cloud/stream/app/BridgeProcessorTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 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 + * + * http://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; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +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.Import; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +public class BridgeProcessorTests { + + @Test + public void testFilterProcessor() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(BridgeTestAppConfiguration.class)) + .web(WebApplicationType.NONE) + .run()) { + + InputDestination processorInput = context.getBean(InputDestination.class); + OutputDestination processorOutput = context.getBean(OutputDestination.class); + + String inMessage = "hello world"; + processorInput.send(new GenericMessage<>(inMessage.getBytes(StandardCharsets.UTF_8))); + Message sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload())).isEqualTo(inMessage); + } + } + + @EnableAutoConfiguration + @Import({ BridgeProcessorConfiguration.class }) + public static class BridgeTestAppConfiguration { } +} diff --git a/applications/processor/filter-processor/README.adoc b/applications/processor/filter-processor/README.adoc new file mode 100644 index 00000000..ddbe62cd --- /dev/null +++ b/applications/processor/filter-processor/README.adoc @@ -0,0 +1,22 @@ +//tag::ref-doc[] += Filter Processor + +Filter processor enables an application to examine the incoming payload and then applies a predicate against it which decides if the record needs to be continued. +For example, if the incoming payload is of type `String` and you want to filter out anything that has less than five characters, you can run the filter processor as below. + +`java -jar filter-processor-kafka-.jar --spel.function.expression=payload.length() > 4` + +Change kafka to rabbit if you want to run it against RabbitMQ. + +=== Payload + +You can pass any type as payload and then apply SpEL expressions against it to filter. +If the incoming type is `byte[]` and the content type is set to `text/plain` or `application/json`, then the application converts the `byte[]` into `String`. + +== Options + +//tag::configuration-properties[] +$$spel.function.expression$$:: $$A SpEL expression to apply.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/processor/filter-processor/pom.xml b/applications/processor/filter-processor/pom.xml new file mode 100644 index 00000000..5331aff5 --- /dev/null +++ b/applications/processor/filter-processor/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + filter-processor + filter-processor + filter processor apps + 3.0.0.BUILD-SNAPSHOT + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.cloud.function + filter-function + ${java-functions.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-starter-json + test + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + filter + processor + ${project.version} + org.springframework.cloud.fn.filter.FilterFunctionConfiguration.class + byteArrayTextToString|filterFunction + + + + + org.springframework.cloud.fn + filter-function + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + diff --git a/applications/processor/filter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/processor/filter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..ff1bc322 --- /dev/null +++ b/applications/processor/filter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=SpelFunctionProperties \ No newline at end of file diff --git a/applications/processor/filter-processor/src/test/java/org/springframework/cloud/stream/app/filter/processor/FilterProcessorTests.java b/applications/processor/filter-processor/src/test/java/org/springframework/cloud/stream/app/filter/processor/FilterProcessorTests.java new file mode 100644 index 00000000..51e8f5d6 --- /dev/null +++ b/applications/processor/filter-processor/src/test/java/org/springframework/cloud/stream/app/filter/processor/FilterProcessorTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 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 + * + * http://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.filter.processor; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.filter.FilterFunctionConfiguration; +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.Import; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +public class FilterProcessorTests { + + @Test + public void testFilterProcessor() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(FilterProcessorConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=byteArrayTextToString|filterFunction", + "--spel.function.expression=payload.length() > 5")) { + + InputDestination processorInput = context.getBean(InputDestination.class); + OutputDestination processorOutput = context.getBean(OutputDestination.class); + + String longMessage = "hello world message"; + processorInput.send(new GenericMessage<>(longMessage.getBytes(StandardCharsets.UTF_8))); + Message sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload())).isEqualTo(longMessage); + + String shortMessage = "foo"; + processorInput.send(new GenericMessage<>(shortMessage.getBytes(StandardCharsets.UTF_8))); + Message sourceMessage2 = processorOutput.receive(5000); + assertThat(sourceMessage2).isNull(); + } + } + + @EnableAutoConfiguration + @Import({ FilterFunctionConfiguration.class }) + public static class FilterProcessorConfiguration {} + +} diff --git a/applications/processor/pom.xml b/applications/processor/pom.xml new file mode 100644 index 00000000..6390a810 --- /dev/null +++ b/applications/processor/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + org.springframework.cloud.stream.app + processor + 3.0.0.BUILD-SNAPSHOT + pom + + + bridge-processor + splitter-processor + filter-processor + transform-processor + + + diff --git a/applications/processor/splitter-processor/README.adoc b/applications/processor/splitter-processor/README.adoc new file mode 100644 index 00000000..08834790 --- /dev/null +++ b/applications/processor/splitter-processor/README.adoc @@ -0,0 +1,28 @@ +//tag::ref-doc[] += Splitter Processor + +The splitter app builds upon the concept of the same name in Spring Integration and allows the splitting of a single message into several distinct messages. +The processor uses a function that takes a `Message` as input and then produces a `List` as output based on various properties (see below). +You can use a SpEL expression or a delimiter to specify how you want to split the incoming message. + +=== Payload + +* Incoming payload - `Message + +If the incoming type is `byte[]` and the content type is set to `text/plain` or `application/json`, then the application converts the `byte[]` into `String`. + +* Outgoing payload - `List` + + +== Options + +//tag::configuration-properties[] +$$splitter.apply-sequence$$:: $$Add correlation/sequence information in headers to facilitate later aggregation.$$ *($$Boolean$$, default: `$$true$$`)* +$$splitter.charset$$:: $$The charset to use when converting bytes in text-based files to String.$$ *($$String$$, default: `$$$$`)* +$$splitter.delimiters$$:: $$When expression is null, delimiters to use when tokenizing {@link String} payloads.$$ *($$String$$, default: `$$$$`)* +$$splitter.expression$$:: $$A SpEL expression for splitting payloads.$$ *($$String$$, default: `$$$$`)* +$$splitter.file-markers$$:: $$Set to true or false to use a {@code FileSplitter} (to split text-based files by line) that includes (or not) beginning/end of file markers.$$ *($$Boolean$$, default: `$$$$`)* +$$splitter.markers-json$$:: $$When 'fileMarkers == true', specify if they should be produced as FileSplitter.FileMarker objects or JSON.$$ *($$Boolean$$, default: `$$true$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/processor/splitter-processor/pom.xml b/applications/processor/splitter-processor/pom.xml new file mode 100644 index 00000000..2f6f8aaf --- /dev/null +++ b/applications/processor/splitter-processor/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + splitter-processor + 3.0.0.BUILD-SNAPSHOT + splitter-processor + splitter processor apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud.function + splitter-function + ${java-functions.version} + + + io.pivotal.java.function + payload-converter-function + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + splitter + processor + ${project.version} + org.springframework.cloud.fn.splitter.SplitterFunctionConfiguration.class + byteArrayTextToString|splitterFunction + + + + + org.springframework.cloud.fn + payload-converter-function + ${java-functions.version} + + + org.springframework.cloud.fn + splitter-function + ${java-functions.version} + + + + + true + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/processor/splitter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/processor/splitter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..70bc4605 --- /dev/null +++ b/applications/processor/splitter-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.splitter.SplitterFunctionProperties \ No newline at end of file diff --git a/applications/processor/splitter-processor/src/test/java/org/springframework/cloud/stream/app/splitter/processor/SplitterProcessorTests.java b/applications/processor/splitter-processor/src/test/java/org/springframework/cloud/stream/app/splitter/processor/SplitterProcessorTests.java new file mode 100644 index 00000000..e36d4a45 --- /dev/null +++ b/applications/processor/splitter-processor/src/test/java/org/springframework/cloud/stream/app/splitter/processor/SplitterProcessorTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 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 + * + * http://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.splitter.processor; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.splitter.SplitterFunctionConfiguration; +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.Import; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +public class SplitterProcessorTests { + + @Test + public void testSplitterProcessor() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(SplitterProcessorConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=byteArrayTextToString|splitterFunction", + "--splitter.expression=payload.split(',')")) { + + InputDestination processorInput = context.getBean(InputDestination.class); + OutputDestination processorOutput = context.getBean(OutputDestination.class); + + String payload = "hello,world,message"; + processorInput.send(new GenericMessage<>(payload.getBytes(StandardCharsets.UTF_8))); + Message sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload(), StandardCharsets.UTF_8)).isEqualTo("hello"); + sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload(), StandardCharsets.UTF_8)).isEqualTo("world"); + sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload(), StandardCharsets.UTF_8)).isEqualTo("message"); + } + } + + @EnableAutoConfiguration + @Import({ SplitterFunctionConfiguration.class }) + public static class SplitterProcessorConfiguration {} +} diff --git a/applications/processor/splitter-processor/src/test/resources/META-INF/spring.binders b/applications/processor/splitter-processor/src/test/resources/META-INF/spring.binders new file mode 100644 index 00000000..737ae2f4 --- /dev/null +++ b/applications/processor/splitter-processor/src/test/resources/META-INF/spring.binders @@ -0,0 +1,2 @@ +integration:\ +org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration \ No newline at end of file diff --git a/applications/processor/transform-processor/README.adoc b/applications/processor/transform-processor/README.adoc new file mode 100644 index 00000000..2441adf2 --- /dev/null +++ b/applications/processor/transform-processor/README.adoc @@ -0,0 +1,23 @@ +//tag::ref-doc[] += Transform Processor + +Transformer processor allows you to convert the message payload structure based on a SpEL expression. + +Here is an example of how you can run this application. + +`java -jar filter-processor-kafka-.jar --spel.function.expression=toUpperCase()` + +Change kafka to rabbit if you want to run it against RabbitMQ. + + +=== Payload + +Incoming message can contain any type of payload. + +== Options + +//tag::configuration-properties[] +$$spel.function.expression$$:: $$A SpEL expression to apply.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/processor/transform-processor/pom.xml b/applications/processor/transform-processor/pom.xml new file mode 100644 index 00000000..233e2616 --- /dev/null +++ b/applications/processor/transform-processor/pom.xml @@ -0,0 +1,91 @@ + + + 4.0.0 + transform-processor + 3.0.0.BUILD-SNAPSHOT + transform-processor + transform processor apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + org.springframework.cloud.function + spel-function + ${java-functions.version} + + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + transform + processor + ${project.version} + org.springfamework.cloud.fn.spel.SpelFunctionConfiguration.class + byteArrayTextToString|spelFunction + + + + + org.springframework.cloud.fn + spel-function + ${java-functions.version} + + + + + true + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/processor/transform-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/processor/transform-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..aa9720e2 --- /dev/null +++ b/applications/processor/transform-processor/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.spel.SpelFunctionProperties \ No newline at end of file diff --git a/applications/processor/transform-processor/src/test/java/org/springframework/cloud/stream/app/transform/processor/TransformProcessorTests.java b/applications/processor/transform-processor/src/test/java/org/springframework/cloud/stream/app/transform/processor/TransformProcessorTests.java new file mode 100644 index 00000000..b2198dd8 --- /dev/null +++ b/applications/processor/transform-processor/src/test/java/org/springframework/cloud/stream/app/transform/processor/TransformProcessorTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 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 + * + * http://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.transform.processor; + +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.spel.SpelFunctionConfiguration; +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.Import; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +public class TransformProcessorTests { + + @Test + public void testTransformProcessor() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(TransformProcessorConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=byteArrayTextToString|spelFunction", + "--spel.function.expression=payload.toUpperCase()")) { + + InputDestination processorInput = context.getBean(InputDestination.class); + OutputDestination processorOutput = context.getBean(OutputDestination.class); + + String payload = "hello world"; + processorInput.send(new GenericMessage<>(payload.getBytes(StandardCharsets.UTF_8))); + Message sourceMessage = processorOutput.receive(10000); + assertThat(new String(sourceMessage.getPayload())).isEqualTo(payload.toUpperCase()); + } + } + + @EnableAutoConfiguration + @Import({ SpelFunctionConfiguration.class }) + public static class TransformProcessorConfiguration {} + +} diff --git a/applications/sink/cassandra-sink/README.adoc b/applications/sink/cassandra-sink/README.adoc new file mode 100644 index 00000000..add39433 --- /dev/null +++ b/applications/sink/cassandra-sink/README.adoc @@ -0,0 +1,44 @@ +//tag::ref-doc[] += Cassandra Sink + +This sink application writes the content of each message it receives into Cassandra. + +It expects a payload of JSON String and uses it’s properties to map to table columns. + +=== Payload +A JSON String or byte array representing the entity (or a list of entities) to be persisted. + +== Options + +The **$$cassandra$$** $$sink$$ has the following options: + + +//tag::configuration-properties[] +$$cassandra.cluster.create-keyspace$$:: $$Flag to create (or not) keyspace on application startup.$$ *($$Boolean$$, default: `$$false$$`)* +$$cassandra.cluster.entity-base-packages$$:: $$Base packages to scan for entities annotated with Table annotations.$$ *($$String[]$$, default: `$$[]$$`)* +$$cassandra.cluster.init-script$$:: $$Resource with CQL scripts (delimited by ';') to initialize keyspace schema.$$ *($$Resource$$, default: `$$$$`)* +$$cassandra.cluster.skip-ssl-validation$$:: $$Flag to validate the Servers' SSL certs$$ *($$Boolean$$, default: `$$false$$`)* +$$cassandra.consistency-level$$:: $$The consistency level for write operation.$$ *($$ConsistencyLevel$$, default: `$$$$`)* +$$cassandra.ingest-query$$:: $$Ingest Cassandra query.$$ *($$String$$, default: `$$$$`)* +$$cassandra.query-type$$:: $$QueryType for Cassandra Sink.$$ *($$Type$$, default: `$$$$`, possible values: `INSERT`,`UPDATE`,`DELETE`,`STATEMENT`)* +$$cassandra.statement-expression$$:: $$Expression in Cassandra query DSL style.$$ *($$Expression$$, default: `$$$$`)* +$$cassandra.ttl$$:: $$Time-to-live option of WriteOptions.$$ *($$Integer$$, default: `$$0$$`)* +$$spring.data.cassandra.cluster-name$$:: $$$$ *($$String$$, default: `$$$$`)* +$$spring.data.cassandra.compression$$:: $$Compression supported by the Cassandra binary protocol.$$ *($$Compression$$, default: `$$none$$`, possible values: `LZ4`,`SNAPPY`,`NONE`)* +$$spring.data.cassandra.connect-timeout$$:: $$Socket option: connection time out.$$ *($$Duration$$, default: `$$$$`)* +$$spring.data.cassandra.consistency-level$$:: $$Queries consistency level.$$ *($$DefaultConsistencyLevel$$, default: `$$$$`, possible values: `ANY`,`ONE`,`TWO`,`THREE`,`QUORUM`,`ALL`,`LOCAL_ONE`,`LOCAL_QUORUM`,`EACH_QUORUM`,`SERIAL`,`LOCAL_SERIAL`)* +$$spring.data.cassandra.contact-points$$:: $$Cluster node addresses in the form 'host:port'.$$ *($$List$$, default: `$$[127.0.0.1:9042]$$`)* +$$spring.data.cassandra.fetch-size$$:: $$$$ *($$Integer$$, default: `$$$$`)* +$$spring.data.cassandra.keyspace-name$$:: $$Keyspace name to use.$$ *($$String$$, default: `$$$$`)* +$$spring.data.cassandra.local-datacenter$$:: $$Datacenter that is considered "local". Contact points should be from this datacenter.$$ *($$String$$, default: `$$$$`)* +$$spring.data.cassandra.page-size$$:: $$Queries default page size.$$ *($$Integer$$, default: `$$5000$$`)* +$$spring.data.cassandra.password$$:: $$Login password of the server.$$ *($$String$$, default: `$$$$`)* +$$spring.data.cassandra.read-timeout$$:: $$Socket option: read time out.$$ *($$Duration$$, default: `$$$$`)* +$$spring.data.cassandra.schema-action$$:: $$Schema action to take at startup.$$ *($$String$$, default: `$$none$$`)* +$$spring.data.cassandra.serial-consistency-level$$:: $$Queries serial consistency level.$$ *($$DefaultConsistencyLevel$$, default: `$$$$`, possible values: `ANY`,`ONE`,`TWO`,`THREE`,`QUORUM`,`ALL`,`LOCAL_ONE`,`LOCAL_QUORUM`,`EACH_QUORUM`,`SERIAL`,`LOCAL_SERIAL`)* +$$spring.data.cassandra.session-name$$:: $$Name of the Cassandra session.$$ *($$String$$, default: `$$$$`)* +$$spring.data.cassandra.ssl$$:: $$Enable SSL support.$$ *($$Boolean$$, default: `$$false$$`)* +$$spring.data.cassandra.username$$:: $$Login user of the server.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/cassandra-sink/pom.xml b/applications/sink/cassandra-sink/pom.xml new file mode 100644 index 00000000..01cfde05 --- /dev/null +++ b/applications/sink/cassandra-sink/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + cassandra-sink + 3.0.0.BUILD-SNAPSHOT + cassandra-sink + cassandra sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + cassandra-consumer + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + cassandra + sink + ${project.version} + org.springframework.cloud.fn.consumer.cassandra.CassandraConsumerConfiguration.class + + + + + org.springframework.cloud.fn + cassandra-consumer + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/cassandra-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/cassandra-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..38811295 --- /dev/null +++ b/applications/sink/cassandra-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,3 @@ +configuration-properties.classes=CassandraConsumerProperties, \ + CassandraClusterProperties, \ + org.springframework.boot.autoconfigure.cassandra.CassandraProperties \ No newline at end of file diff --git a/applications/sink/counter-sink/README.adoc b/applications/sink/counter-sink/README.adoc new file mode 100644 index 00000000..1d68e25d --- /dev/null +++ b/applications/sink/counter-sink/README.adoc @@ -0,0 +1,60 @@ +//tag::ref-doc[] +:images-asciidoc: https://github.com/spring-cloud-stream-app-starters/stream-applications/raw/master/sink/counter-sink/src/main/resources += Counter Sink + +Counter that compute multiple counters from the received messages. It leverages the Micrometer library and can use various popular TSDB to persist the counter values. + +By default the Counter Sink increments the `message`.`name` counter on every received message. The `message-counter-enabled` allows you to disable this counter when required. + +If tag expressions are provided (via the `counter.tag.expression.= property) then the `name` counter is incremented. Note that each SpEL expression can evaluate into multiple values resulting into multiple counter increments (one fore every value resolved). + +If fixed tags are provided they are include in all message and expression counter increment measurements. + +Counter's implementation is based on the https://micrometer.io/[Micrometer library] which is a Vendor-neutral application metrics facade that supports the most popular monitoring systems. +See the https://micrometer.io/docs[Micrometer documentation] for the list of supported monitoring systems. Starting with Spring Boot 2.0, Micrometer is the instrumentation library powering the delivery of application metrics from Spring Boot. + +All Spring Cloud Stream App Starters are configured to support two of the most popular monitoring systems, Prometheus and InfluxDB. You can declaratively select which monitoring system to use. +If you are not using Prometheus or InfluxDB, you can customise the App starters to use a different monitoring system as well as include your preferred micrometer monitoring system library in your own custom applications. + +https://grafana.com/[Grafana] is a popular platform for building visualization dashboards. + +To enable Micrometer’s Prometheus meter registry for Spring Cloud Stream application starters, set the following properties. + +``` +management.metrics.export.prometheus.enabled=true +management.endpoints.web.exposure.include=prometheus +``` + +and disable the application’s security which allows for a simple Prometheus configuration to scrape counter information by setting the following property. + +``` +spring.cloud.streamapp.security.enabled=false +``` + +To enable Micrometer’s Influx meter registry for Spring Cloud Stream application starters, set the following property. + +``` +management.metrics.export.influx.enabled=true +management.metrics.export.influx.uri={influxdb-server-url} +``` + +NOTE: if the https://docs.spring.io/spring-cloud-dataflow/docs/2.0.0.BUILD-SNAPSHOT/reference/htmlsingle/#streams-monitoring[Data Flow Server metrics] is enabled then the `Counter` will reuse the exiting configurations. + +Following diagram illustrates Counter's information collection and processing flow. + +image::{images-asciidoc}/MicrometerCounterAppStarter.png[Counter Architecture, scaledwidth="70%"] + +=== Payload + +== Options + +//tag::configuration-properties[] +$$counter.amount-expression$$:: $$A SpEL expression (against the incoming Message) to derive the amount to add to the counter. If not set the counter is incremented by 1.0$$ *($$Expression$$, default: `$$$$`)* +$$counter.message-counter-enabled$$:: $$Enables counting the number of messages processed. Uses the 'message.' counter name prefix to distinct it form the expression based counter. The message counter includes the fixed tags when provided.$$ *($$Boolean$$, default: `$$true$$`)* +$$counter.name$$:: $$The name of the counter to increment. The 'name' and 'nameExpression' are mutually exclusive. Only one can be set.$$ *($$String$$, default: `$$$$`)* +$$counter.name-expression$$:: $$A SpEL expression (against the incoming Message) to derive the name of the counter to increment. The 'name' and 'nameExpression' are mutually exclusive. Only one can be set.$$ *($$Expression$$, default: `$$$$`)* +$$counter.tag.expression$$:: $$Computes tags from SpEL expression. Single SpEL expression can produce an array of values, which in turn means distinct name/value tags. Every name/value tag will produce a separate counter increment. Tag expression format is: counter.tag.expression.[tag-name]=[SpEL expression]$$ *($$Map$$, default: `$$$$`)* +$$counter.tag.fixed$$:: $$Custom tags assigned to every counter increment measurements. This is a map so the property convention fixed tags is: counter.tag.fixed.[tag-name]=[tag-value]$$ *($$Map$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/counter-sink/pom.xml b/applications/sink/counter-sink/pom.xml new file mode 100644 index 00000000..d6a6ac9c --- /dev/null +++ b/applications/sink/counter-sink/pom.xml @@ -0,0 +1,98 @@ + + + 4.0.0 + counter-sink + 3.0.0.BUILD-SNAPSHOT + counter-sink + counter sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.cloud.fn + counter-consumer + ${java-functions.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.awaitility + awaitility + test + + + junit + junit + + + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + counter + sink + ${project.version} + org.springframework.cloud.fn.consumer.counter.CounterConsumerConfiguration.class + byteArrayTextToString|counterConsumer + + + + org.springframework.cloud.fn + counter-consumer + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/counter-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/counter-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..66583ecc --- /dev/null +++ b/applications/sink/counter-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,2 @@ +configuration-properties.classes=CounterConsumerProperties, \ + CounterConsumerProperties$MetricsTag diff --git a/applications/sink/counter-sink/src/main/resources/MicrometerCounterAppStarter.png b/applications/sink/counter-sink/src/main/resources/MicrometerCounterAppStarter.png new file mode 100644 index 0000000000000000000000000000000000000000..6dcb5cc300ab992078606a5e48bbe4f82074282b GIT binary patch literal 87567 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

RW_yttyWs#PIY76;=nsG(}>f#V-Ca@6+HQ|KqED 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.getBytes(StandardCharsets.UTF_8))); + + Counter counter = meterRegistry.find("counter666").counter(); + assertThat(counter.count()).isEqualTo(message.length()); + assertThat(counter.getId().getTag("foo")).isEqualTo("bar"); + } + } + + @EnableAutoConfiguration + //@Import({CounterConsumerConfiguration.class, PayloadConverterConfiguration.class}) + @Import({CounterConsumerConfiguration.class}) + public static class CounterSinkConfiguration {} + +} diff --git a/applications/sink/file-sink/README.adoc b/applications/sink/file-sink/README.adoc new file mode 100644 index 00000000..d09c637a --- /dev/null +++ b/applications/sink/file-sink/README.adoc @@ -0,0 +1,28 @@ +//tag::ref-doc[] += File Sink + +The file sink app writes each message it receives to a file. + +=== Payload + +* `java.io.File` +* `java.io.InputStream` +* `byte[]` +* `String` + +== Options + +The `file-sink` has the following options: + +//tag::configuration-properties[] +$$file.consumer.binary$$:: $$A flag to indicate whether adding a newline after the write should be suppressed.$$ *($$Boolean$$, default: `$$false$$`)* +$$file.consumer.charset$$:: $$The charset to use when writing text content.$$ *($$String$$, default: `$$UTF-8$$`)* +$$file.consumer.directory$$:: $$The parent directory of the target file.$$ *($$File$$, default: `$$$$`)* +$$file.consumer.directory-expression$$:: $$The expression to evaluate for the parent directory of the target file.$$ *($$String$$, default: `$$$$`)* +$$file.consumer.mode$$:: $$The FileExistsMode to use if the target file already exists.$$ *($$FileExistsMode$$, default: `$$$$`, possible values: `APPEND`,`APPEND_NO_FLUSH`,`FAIL`,`IGNORE`,`REPLACE`,`REPLACE_IF_MODIFIED`)* +$$file.consumer.name$$:: $$The name of the target file.$$ *($$String$$, default: `$$file-consumer$$`)* +$$file.consumer.name-expression$$:: $$The expression to evaluate for the name of the target file.$$ *($$String$$, default: `$$$$`)* +$$file.consumer.suffix$$:: $$The suffix to append to file name.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/file-sink/pom.xml b/applications/sink/file-sink/pom.xml new file mode 100644 index 00000000..b1a8003c --- /dev/null +++ b/applications/sink/file-sink/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + file-sink + 3.0.0.BUILD-SNAPSHOT + file-sink + file sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + file-consumer + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + file + sink + ${project.version} + org.springframework.cloud.fn.consumer.file.FileConsumerConfiguration.class + + + + org.springframework.cloud.fn + file-consumer + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/file-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/file-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..3a01d9ee --- /dev/null +++ b/applications/sink/file-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.file.FileConsumerProperties \ No newline at end of file diff --git a/applications/sink/file-sink/src/test/java/org/springframework/cloud/stream/app/FileSinkTests.java b/applications/sink/file-sink/src/test/java/org/springframework/cloud/stream/app/FileSinkTests.java new file mode 100644 index 00000000..189d18cb --- /dev/null +++ b/applications/sink/file-sink/src/test/java/org/springframework/cloud/stream/app/FileSinkTests.java @@ -0,0 +1,71 @@ +/* + * Copyright 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 + * + * http://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; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Path; + +import org.springframework.cloud.fn.consumer.file.FileConsumerConfiguration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +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.TestChannelBinderConfiguration; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +public class FileSinkTests { + + @TempDir + static Path tempDir; + + @Test + public void testFileSink() throws Exception { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration + .getCompleteConfiguration(FileSinkConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=fileConsumer", + "--file.consumer.name=test", + "--file.consumer.suffix=txt", + "--file.consumer.directory=" + tempDir.toAbsolutePath().toString())) { + + final Message message = MessageBuilder.withPayload("hello").build(); + InputDestination source = context.getBean(InputDestination.class); + source.send(message); + File file = new File(tempDir.toFile(), "test.txt"); + assertThat(file.exists()).isTrue(); + assertThat("hello" + System.lineSeparator()) + .isEqualTo(FileCopyUtils.copyToString(new FileReader(file))); + } + } + + @EnableAutoConfiguration + @Import(FileConsumerConfiguration.class) + public static class FileSinkConfiguration {} +} diff --git a/applications/sink/jdbc-sink/README.adoc b/applications/sink/jdbc-sink/README.adoc new file mode 100644 index 00000000..53432af7 --- /dev/null +++ b/applications/sink/jdbc-sink/README.adoc @@ -0,0 +1,58 @@ +//tag::ref-doc[] += JDBC Sink + +JDBC sink allows you to persist incoming payload into an RDBMS database. + +The `jdbc.consumer.columns` property represents pairs of `COLUMN_NAME[:EXPRESSION_FOR_VALUE]` where `EXPRESSION_FOR_VALUE` (together with the colon) is optional. +In this case the value is evaluated via generated expression like `payload.COLUMN_NAME`, so this way we have a direct mapping from object properties to the table column. +For example we have a JSON payload like: +``` +{ + "name": "My Name" + "address": { + "city": "Big City", + "street": "Narrow Alley" + } +} +``` +So, we can insert it into the table with `name`, `city` and `street` structure using the configuration: +``` +--jdbc.consumer.columns=name,city:address.city,street:address.street +``` + +This sink supports batch inserts, as far as supported by the underlying JDBC driver. +Batch inserts are configured via the `batch-size` and `idle-timeout` properties: +Incoming messages are aggregated until `batch-size` messages are present, then inserted as a batch. +If `idle-timeout` milliseconds pass with no new messages, the aggregated batch is inserted even if it is smaller than `batch-size`, capping maximum latency. + +NOTE: The module also uses Spring Boot's https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html#boot-features-configure-datasource[DataSource support] for configuring the database connection, so properties like `spring.datasource.url` _etc._ apply. + +== Examples + +``` +java -jar jdbc-sink.jar --jdbc.consumer.tableName=names --jdbc.consumer.columns=name --spring.datasource.driver-class-name=org.mariadb.jdbc.Driver \ +--spring.datasource.url='jdbc:mysql://localhost:3306/test +``` + +=== Payload + +== Options + +The **$$jdbc$$** $$sink$$ has the following options: + +//tag::configuration-properties[] +$$jdbc.consumer.batch-size$$:: $$Threshold in number of messages when data will be flushed to database table.$$ *($$Integer$$, default: `$$1$$`)* +$$jdbc.consumer.columns$$:: $$The comma separated colon-based pairs of column names and SpEL expressions for values to insert/update. Names are used at initialization time to issue the DDL.$$ *($$String$$, default: `$$payload:payload.toString()$$`)* +$$jdbc.consumer.idle-timeout$$:: $$Idle timeout in milliseconds when data is automatically flushed to database table.$$ *($$Long$$, default: `$$-1$$`)* +$$jdbc.consumer.initialize$$:: $$'true', 'false' or the location of a custom initialization script for the table.$$ *($$String$$, default: `$$false$$`)* +$$jdbc.consumer.table-name$$:: $$The name of the table to write into.$$ *($$String$$, default: `$$messages$$`)* +$$spring.datasource.data$$:: $$Data (DML) script resource references.$$ *($$List$$, default: `$$$$`)* +$$spring.datasource.driver-class-name$$:: $$Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.initialization-mode$$:: $$Initialize the datasource with available DDL and DML scripts.$$ *($$DataSourceInitializationMode$$, default: `$$embedded$$`, possible values: `ALWAYS`,`EMBEDDED`,`NEVER`)* +$$spring.datasource.password$$:: $$Login password of the database.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.schema$$:: $$Schema (DDL) script resource references.$$ *($$List$$, default: `$$$$`)* +$$spring.datasource.url$$:: $$JDBC URL of the database.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.username$$:: $$Login username of the database.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/jdbc-sink/pom.xml b/applications/sink/jdbc-sink/pom.xml new file mode 100644 index 00000000..a36c808d --- /dev/null +++ b/applications/sink/jdbc-sink/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + jdbc-sink + 3.0.0.BUILD-SNAPSHOT + jdbc-sink + jdbc sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.cloud.fn + jdbc-consumer + ${java-functions.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-jdbc + test + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + jdbc + sink + ${project.version} + org.springframework.cloud.fn.consumer.jdbc.JdbcConsumerConfiguration.class + + + + org.springframework.cloud.fn + jdbc-consumer + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/jdbc-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/jdbc-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..764ba642 --- /dev/null +++ b/applications/sink/jdbc-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,9 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.jdbc.JdbcConsumerProperties +configuration-properties.names=\ +spring.datasource.url,\ +spring.datasource.driver-class-name,\ +spring.datasource.username,\ +spring.datasource.password,\ +spring.datasource.schema,\ +spring.datasource.data,\ +spring.datasource.initialization-mode \ No newline at end of file diff --git a/applications/sink/jdbc-sink/src/test/java/org/springframework/cloud/stream/app/jdbc/sink/JdbcSinkTests.java b/applications/sink/jdbc-sink/src/test/java/org/springframework/cloud/stream/app/jdbc/sink/JdbcSinkTests.java new file mode 100644 index 00000000..f75156ea --- /dev/null +++ b/applications/sink/jdbc-sink/src/test/java/org/springframework/cloud/stream/app/jdbc/sink/JdbcSinkTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 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 + * + * http://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.jdbc.sink; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.consumer.jdbc.JdbcConsumerConfiguration; +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.support.MessageBuilder; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; + +public class JdbcSinkTests { + + @Test + public void testSimpleInserts() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(JdbcSinkConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=jdbcConsumer")) { + + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + InputDestination source = context.getBean(InputDestination.class); + source.send(message); + + final JdbcOperations jdbcOperations = context.getBean(JdbcOperations.class); + String result = jdbcOperations.queryForObject("select payload from messages", String.class); + assertThat(result).isEqualTo(("hello42")); + } + } + + @EnableAutoConfiguration + @Import(JdbcConsumerConfiguration.class) + public static class JdbcSinkConfiguration {} + + static class Payload { + + private String a; + + private Integer b; + + public Payload() { + } + + public Payload(String a, Integer b) { + this.a = a; + this.b = b; + } + + public String getA() { + return a; + } + + public Integer getB() { + return b; + } + + public void setA(String a) { + this.a = a; + } + + public void setB(Integer b) { + this.b = b; + } + + @Override + public String toString() { + return a + b; + } + + } +} diff --git a/applications/sink/jdbc-sink/src/test/resources/schema.sql b/applications/sink/jdbc-sink/src/test/resources/schema.sql new file mode 100644 index 00000000..65e85283 --- /dev/null +++ b/applications/sink/jdbc-sink/src/test/resources/schema.sql @@ -0,0 +1,7 @@ +-- Run by default by Boot infrastructure + +create table messages( + a varchar(2000), + b VARCHAR (2000), + payload VARCHAR (2000) +); diff --git a/applications/sink/log-sink/README.adoc b/applications/sink/log-sink/README.adoc new file mode 100644 index 00000000..44b7671e --- /dev/null +++ b/applications/sink/log-sink/README.adoc @@ -0,0 +1,21 @@ +//tag::ref-doc[] += Log Sink + +The `log` sink uses the application logger to output the data for inspection. + +Please understand that `log` sink uses type-less handler, which affects how the actual logging will be performed. +This means that if the content-type is textual, then raw payload bytes will be converted to String, otherwise raw bytes will be logged. +Please see more info in the https://docs.spring.io/spring-cloud-stream/docs/Elmhurst.RELEASE/reference/htmlsingle/#_content_type_versus_argument_type[user-guide]. + +== Options + +The **$$log$$** $$sink$$ has the following options: + + +//tag::configuration-properties[] +$$log.expression$$:: $$A SpEL expression (against the incoming message) to evaluate as the logged message.$$ *($$String$$, default: `$$payload$$`)* +$$log.level$$:: $$The level at which to log messages.$$ *($$Level$$, default: `$$$$`, possible values: `FATAL`,`ERROR`,`WARN`,`INFO`,`DEBUG`,`TRACE`)* +$$log.name$$:: $$The name of the logger to use.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/log-sink/pom.xml b/applications/sink/log-sink/pom.xml new file mode 100644 index 00000000..fb24a566 --- /dev/null +++ b/applications/sink/log-sink/pom.xml @@ -0,0 +1,90 @@ + + + 4.0.0 + log-sink + 3.0.0.BUILD-SNAPSHOT + log-sink + log sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + log-consumer + ${java-functions.version} + + + org.awaitility + awaitility + test + + + junit + junit + + + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + log + sink + ${project.version} + org.springframework.cloud.fn.consumer.log.LogConsumerConfiguration.class + byteArrayTextToString|logConsumer + + + + org.springframework.cloud.fn + log-consumer + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/log-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/log-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..89d8345b --- /dev/null +++ b/applications/sink/log-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.log.LogConsumerProperties \ No newline at end of file diff --git a/applications/sink/log-sink/src/test/java/org/springframework/cloud/stream/app/LogSinkTests.java b/applications/sink/log-sink/src/test/java/org/springframework/cloud/stream/app/LogSinkTests.java new file mode 100644 index 00000000..9539e5c4 --- /dev/null +++ b/applications/sink/log-sink/src/test/java/org/springframework/cloud/stream/app/LogSinkTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 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 + * + * http://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; + +import java.nio.charset.StandardCharsets; + +import org.springframework.cloud.fn.consumer.log.LogConsumerConfiguration; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +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.messaging.support.GenericMessage; + +/** + * @author Soby Chacko + */ +@ExtendWith(OutputCaptureExtension.class) +public class LogSinkTests { + + @Test + public void testSourceFromSupplier(CapturedOutput output) { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration.getCompleteConfiguration(LogSinkConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=byteArrayTextToString|logConsumer")) { + + InputDestination source = context.getBean(InputDestination.class); + source.send(new GenericMessage("hello".getBytes(StandardCharsets.UTF_8))); + Awaitility.await().until(output::getOut, value -> value.contains("hello")); + } + } + + @EnableAutoConfiguration + @Import(LogConsumerConfiguration.class) + public static class LogSinkConfiguration {} +} diff --git a/applications/sink/mongodb-sink/README.adoc b/applications/sink/mongodb-sink/README.adoc new file mode 100644 index 00000000..1f78d259 --- /dev/null +++ b/applications/sink/mongodb-sink/README.adoc @@ -0,0 +1,25 @@ +//tag::ref-doc[] += MongoDB Sink + +This sink application ingest incoming data into MongoDB. +This application is fully based on the `MongoDataAutoConfiguration`, so refer to the https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-nosql.html#boot-features-mongodb[Spring Boot MongoDB Support] for more information. + +== Input + +=== Payload + +* Any POJO +* `String` +* `byte[]` + +== Options + +The **$$mongodb$$** $$sink$$ has the following options: + + +//tag::configuration-properties[] +$$mongodb.consumer.collection$$:: $$The MongoDB collection to store data$$ *($$String$$, default: `$$$$`)* +$$mongodb.consumer.collection-expression$$:: $$The SpEL expression to evaluate MongoDB collection$$ *($$Expression$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/mongodb-sink/pom.xml b/applications/sink/mongodb-sink/pom.xml new file mode 100644 index 00000000..9af3540d --- /dev/null +++ b/applications/sink/mongodb-sink/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + mongodb-sink + 3.0.0.BUILD-SNAPSHOT + mongodb-sink + mongodb sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + mongodb-consumer + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + mongodb + sink + ${project.version} + org.springframework.cloud.fn.consumer.mongo.MongoDbConsumerConfiguration.class + + + + org.springframework.cloud.fn + mongodb-consumer + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/mongodb-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/mongodb-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..0570c896 --- /dev/null +++ b/applications/sink/mongodb-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.consumer.mongo.MongoDbConsumerProperties \ No newline at end of file diff --git a/applications/sink/pom.xml b/applications/sink/pom.xml new file mode 100644 index 00000000..d02982b9 --- /dev/null +++ b/applications/sink/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + org.springframework.cloud.stream.app + sink + 3.0.0.BUILD-SNAPSHOT + pom + + + file-sink + rabbit-sink + log-sink + cassandra-sink + mongodb-sink + counter-sink + jdbc-sink + + diff --git a/applications/sink/rabbit-sink/README.adoc b/applications/sink/rabbit-sink/README.adoc new file mode 100644 index 00000000..d2d06bf0 --- /dev/null +++ b/applications/sink/rabbit-sink/README.adoc @@ -0,0 +1,34 @@ +//tag::ref-doc[] += RabbitMQ Sink + +This module sends messages to RabbitMQ. + +== Options + +The **$$rabbit$$** $$sink$$ has the following options: + +(See the Spring Boot documentation for RabbitMQ connection properties) + +//tag::configuration-properties[] +$$rabbit.converter-bean-name$$:: $$The bean name for a custom message converter; if omitted, a SimpleMessageConverter is used. If 'jsonConverter', a Jackson2JsonMessageConverter bean will be created for you.$$ *($$String$$, default: `$$$$`)* +$$rabbit.exchange$$:: $$Exchange name - overridden by exchangeNameExpression, if supplied.$$ *($$String$$, default: `$$$$`)* +$$rabbit.exchange-expression$$:: $$A SpEL expression that evaluates to an exchange name.$$ *($$Expression$$, default: `$$$$`)* +$$rabbit.mapped-request-headers$$:: $$Headers that will be mapped.$$ *($$String[]$$, default: `$$[*]$$`)* +$$rabbit.own-connection$$:: $$When true, use a separate connection based on the boot properties.$$ *($$Boolean$$, default: `$$false$$`)* +$$rabbit.persistent-delivery-mode$$:: $$Default delivery mode when 'amqp_deliveryMode' header is not present, true for PERSISTENT.$$ *($$Boolean$$, default: `$$false$$`)* +$$rabbit.routing-key$$:: $$Routing key - overridden by routingKeyExpression, if supplied.$$ *($$String$$, default: `$$$$`)* +$$rabbit.routing-key-expression$$:: $$A SpEL expression that evaluates to a routing key.$$ *($$Expression$$, default: `$$$$`)* +$$spring.rabbitmq.addresses$$:: $$Comma-separated list of addresses to which the client should connect.$$ *($$String$$, default: `$$$$`)* +$$spring.rabbitmq.connection-timeout$$:: $$Connection timeout. Set it to zero to wait forever.$$ *($$Duration$$, default: `$$$$`)* +$$spring.rabbitmq.host$$:: $$RabbitMQ host.$$ *($$String$$, default: `$$localhost$$`)* +$$spring.rabbitmq.password$$:: $$Login to authenticate against the broker.$$ *($$String$$, default: `$$guest$$`)* +$$spring.rabbitmq.port$$:: $$RabbitMQ port.$$ *($$Integer$$, default: `$$5672$$`)* +$$spring.rabbitmq.publisher-confirm-type$$:: $$Type of publisher confirms to use.$$ *($$ConfirmType$$, default: `$$$$`, possible values: `SIMPLE`,`CORRELATED`,`NONE`)* +$$spring.rabbitmq.publisher-returns$$:: $$Whether to enable publisher returns.$$ *($$Boolean$$, default: `$$false$$`)* +$$spring.rabbitmq.requested-channel-max$$:: $$Number of channels per connection requested by the client. Use 0 for unlimited.$$ *($$Integer$$, default: `$$2047$$`)* +$$spring.rabbitmq.requested-heartbeat$$:: $$Requested heartbeat timeout; zero for none. If a duration suffix is not specified, seconds will be used.$$ *($$Duration$$, default: `$$$$`)* +$$spring.rabbitmq.username$$:: $$Login user to authenticate to the broker.$$ *($$String$$, default: `$$guest$$`)* +$$spring.rabbitmq.virtual-host$$:: $$Virtual host to use when connecting to the broker.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/sink/rabbit-sink/pom.xml b/applications/sink/rabbit-sink/pom.xml new file mode 100644 index 00000000..c3146046 --- /dev/null +++ b/applications/sink/rabbit-sink/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + rabbit-sink + 3.0.0.BUILD-SNAPSHOT + rabbit-sink + rabbit sink apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-starter-amqp + test + + + org.springframework.cloud + spring-cloud-stream-binder-rabbit-test-support + test + + + org.springframework.cloud.fn + rabbit-consumer + ${java-functions.version} + + + org.springframework.cloud + spring-cloud-stream-test-support + test + + + org.testcontainers + testcontainers + 1.9.1 + test + + + org.testcontainers + rabbitmq + 1.12.5 + test + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + rabbit + sink + ${project.version} + org.springframework.cloud.fn.consumer.rabbit.RabbitConsumerConfiguration.class + + + + + org.springframework.cloud.fn + rabbit-consumer + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/sink/rabbit-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/sink/rabbit-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..c052f6e5 --- /dev/null +++ b/applications/sink/rabbit-sink/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,2 @@ +configuration-properties.classes=RabbitConsumerProperties, \ + org.springframework.boot.autoconfigure.amqp.RabbitProperties \ No newline at end of file diff --git a/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/OwnConnectionTest.java b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/OwnConnectionTest.java new file mode 100644 index 00000000..e81d2b8e --- /dev/null +++ b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/OwnConnectionTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2016-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.app.rabbit.sink; + +import java.util.function.Consumer; + +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.ClassRule; +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.Queue; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.testcontainers.containers.GenericContainer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertEquals; + +@TestPropertySource(properties = {"rabbit.routingKey=scsapp-testOwn", + "rabbit.own-connection=true"}) +public class OwnConnectionTest extends RabbitSinkIntegrationTests { + + /** + * RabbitMQ + */ +// @ClassRule +// public static GenericContainer rabbitMq = new GenericContainer("rabbitmq:3.5.3") +// .withExposedPorts(5672); + + + + @Test + public void test() { + this.rabbitAdmin.declareQueue( + new Queue("scsapp-testOwn", false, false, true)); + this.bootFactory.resetConnection(); + this.channels.send(MessageBuilder.withPayload("foo".getBytes()) + .build()); + this.rabbitTemplate.setReceiveTimeout(10000); + Message received = this.rabbitTemplate.receive("scsapp-testOwn"); + assertEquals("foo", new String(received.getBody())); + assertThat(this.bootFactory.getCacheProperties().getProperty("localPort")).isEqualTo("0"); + } +} diff --git a/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkIntegrationTests.java b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkIntegrationTests.java new file mode 100644 index 00000000..9077e8ca --- /dev/null +++ b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkIntegrationTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2016-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.app.rabbit.sink; + +import java.util.function.Consumer; + +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.springframework.amqp.core.Binding; +import org.springframework.amqp.core.BindingBuilder; +import org.springframework.amqp.core.DirectExchange; +import org.springframework.amqp.core.Queue; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitAdmin; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.fn.consumer.rabbit.RabbitConsumerConfiguration; +import org.springframework.cloud.fn.consumer.rabbit.RabbitConsumerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.SubscribableChannel; +import org.springframework.test.annotation.DirtiesContext; +import org.testcontainers.containers.GenericContainer; + +@SpringBootTest( + properties = {"spring.cloud.stream.function.definition=rabbitConsumer"}, + webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +@Import(RabbitSinkIntegrationTests.FooConfiguration.class) +abstract class RabbitSinkIntegrationTests { + + @Qualifier("rabbitConsumer-in-0") + @Autowired + protected SubscribableChannel channels; + + @Autowired + protected RabbitConsumerProperties rabbitSinkProperties; + + @Autowired + protected RabbitTemplate rabbitTemplate; + + @Autowired + protected RabbitAdmin rabbitAdmin; + + @Autowired(required = false) + protected MessageConverter myConverter; + + @Autowired + protected CachingConnectionFactory bootFactory; + + static { + Consumer cmd = e -> e.withPortBindings(new PortBinding(Ports.Binding.bindPort(5672), new ExposedPort(5672))); + + GenericContainer rabbitMq = new GenericContainer("rabbitmq:3.5.3") + .withExposedPorts(5672) + .withCreateContainerCmdModifier(cmd); + rabbitMq.start(); + } + + static class FooConfiguration { + + @Bean + public Queue queue() { + return new Queue("scsapp-testq", false, false, true); + } + + @Bean + public DirectExchange exchange() { + return new DirectExchange("scsapp-testex", false, true); + } + + @Bean + public Binding binding() { + return BindingBuilder.bind(queue()).to(exchange()).with("scsapp-testrk"); + } + + } + + @SpringBootApplication + @Import({RabbitConsumerConfiguration.class}) + public static class RabbitSinkConfiguration {} +} diff --git a/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkInvalidConfigTests.java b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkInvalidConfigTests.java new file mode 100644 index 00000000..f02324ad --- /dev/null +++ b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/RabbitSinkInvalidConfigTests.java @@ -0,0 +1,84 @@ +/* + * Copyright 2016-2018 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. + */ +/* + * Copyright 2016-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.app.rabbit.sink; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.Assert.fail; + +import io.pivotal.java.function.rabbit.consumer.RabbitConsumerProperties; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.bind.validation.BindValidationException; +import org.springframework.boot.context.properties.bind.validation.ValidationErrors; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; +import org.springframework.validation.FieldError; + +/** + * Tests for RabbitSource with invalid config. + * + * @author Gary Russell + * @author Chris Schaefer + */ +public class RabbitSinkInvalidConfigTests { + + @Test + public void testNoRoutingKey() { + try { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(Config.class); + context.refresh(); + context.close(); + fail("BeanCreationException expected"); + } + catch (Exception e) { + assertThat(e, instanceOf(BeanCreationException.class)); + + BindValidationException bindValidationException = (BindValidationException) e.getCause().getCause(); + ValidationErrors validationErrors = bindValidationException.getValidationErrors(); + FieldError fieldError = (FieldError) validationErrors.getAllErrors().get(0); + + assertThat(fieldError.getDefaultMessage(), containsString("routingKey or routingKeyExpression is required")); + } + } + + @Configuration + @EnableConfigurationProperties(RabbitConsumerProperties.class) + static class Config { + + } + +} \ No newline at end of file diff --git a/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/SimpleRoutingKeyAndCustomHeaderTests.java b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/SimpleRoutingKeyAndCustomHeaderTests.java new file mode 100644 index 00000000..872b9ea9 --- /dev/null +++ b/applications/sink/rabbit-sink/src/test/java/org/springframework/cloud/stream/app/rabbit/sink/SimpleRoutingKeyAndCustomHeaderTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-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.app.rabbit.sink; + +import org.junit.jupiter.api.Test; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +@TestPropertySource(properties = {"rabbit.routingKey=scsapp-testq", + "rabbit.persistentDeliveryMode=true", + "rabbit.mappedRequestHeaders=STANDARD_REQUEST_HEADERS,bar"}) +public class SimpleRoutingKeyAndCustomHeaderTests extends RabbitSinkIntegrationTests { + + @Test + public void test() { + this.channels.send(MessageBuilder.withPayload("foo".getBytes()) + .setHeader("bar", "baz") + .setHeader("qux", "fiz") + .build()); + this.rabbitTemplate.setReceiveTimeout(10000); + Message received = this.rabbitTemplate.receive("scsapp-testq"); + assertEquals("foo", new String(received.getBody())); + assertEquals("baz", received.getMessageProperties().getHeaders().get("bar")); + assertNull(received.getMessageProperties().getHeaders().get("qux")); + assertEquals(MessageDeliveryMode.PERSISTENT, received.getMessageProperties().getReceivedDeliveryMode()); + } +} diff --git a/applications/source/file-source/README.adoc b/applications/source/file-source/README.adoc new file mode 100644 index 00000000..66209c20 --- /dev/null +++ b/applications/source/file-source/README.adoc @@ -0,0 +1,33 @@ +//tag::ref-doc[] += File Source + +This application polls a directory and sends new files or their contents to the output channel. +The file source provides the contents of a File as a byte array by default. +However, this can be customized using the --file.supplier.mode option: + +* ref Provides a java.io.File reference + +* lines Will split files line-by-line and emit a new message for each line + +* contents The default. Provides the contents of a file as a byte array + +When using `--file.supplier.mode=lines`, you can also provide the additional option `--file.supplier.withMarkers=true`. +If set to true, the underlying FileSplitter will emit additional start-of-file and end-of-file marker messages before and after the actual data. +The payload of these 2 additional marker messages is of type `FileSplitter.FileMarker`. The option withMarkers defaults to false if not explicitly set. + +== Options + +The **$$file$$** $$source$$ has the following options: + +//tag::configuration-properties[] +$$file.consumer.markers-json$$:: $$When 'fileMarkers == true', specify if they should be produced as FileSplitter.FileMarker objects or JSON.$$ *($$Boolean$$, default: `$$true$$`)* +$$file.consumer.mode$$:: $$The FileReadingMode to use for file reading sources. Values are 'ref' - The File object, 'lines' - a message per line, or 'contents' - the contents as bytes.$$ *($$FileReadingMode$$, default: `$$$$`, possible values: `ref`,`lines`,`contents`)* +$$file.consumer.with-markers$$:: $$Set to true to emit start of file/end of file marker messages before/after the data. Only valid with FileReadingMode 'lines'.$$ *($$Boolean$$, default: `$$$$`)* +$$file.supplier.delay-when-empty$$:: $$Duration of delay when no new files are detected.$$ *($$Duration$$, default: `$$1s$$`)* +$$file.supplier.directory$$:: $$The directory to poll for new files.$$ *($$File$$, default: `$$$$`)* +$$file.supplier.filename-pattern$$:: $$A simple ant pattern to match files.$$ *($$String$$, default: `$$$$`)* +$$file.supplier.filename-regex$$:: $$A regex pattern to match files.$$ *($$Pattern$$, default: `$$$$`)* +$$file.supplier.prevent-duplicates$$:: $$Set to true to include an AcceptOnceFileListFilter which prevents duplicates.$$ *($$Boolean$$, default: `$$true$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/source/file-source/pom.xml b/applications/source/file-source/pom.xml new file mode 100644 index 00000000..7c238071 --- /dev/null +++ b/applications/source/file-source/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + file-source + 3.0.0.BUILD-SNAPSHOT + file-source + file source apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + file-supplier + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + file + source + ${project.version} + org.springframework.cloud.fn.supplier.file.FileSupplierConfiguration.class + + + + org.springframework.cloud.fn + file-supplier + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/source/file-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/source/file-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..f750801d --- /dev/null +++ b/applications/source/file-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,2 @@ +configuration-properties.classes=FileSupplierProperties,\ + FileConsumerProperties \ No newline at end of file diff --git a/applications/source/file-source/src/test/java/org.springframework.cloud.stream.app/FileSourceTests.java b/applications/source/file-source/src/test/java/org.springframework.cloud.stream.app/FileSourceTests.java new file mode 100644 index 00000000..a8554648 --- /dev/null +++ b/applications/source/file-source/src/test/java/org.springframework.cloud.stream.app/FileSourceTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 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 + * + * http://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. + */ + +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.supplier.file.FileSupplierConfiguration; +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.Import; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +public class FileSourceTests { + + @TempDir + static Path tempDir; + + @Test + public void testFileSource() throws Exception { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration + .getCompleteConfiguration(FileSourceConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=fileSupplier", "--file.supplier.directory=" + tempDir.toAbsolutePath().toString())) { + + Path firstFile = tempDir.resolve("test.file"); + Files.write(firstFile, "testing".getBytes()); + + OutputDestination target = context.getBean(OutputDestination.class); + Message sourceMessage = target.receive(10000); + final String actual = new String(sourceMessage.getPayload()); + assertThat(actual).isEqualTo("testing"); + } + } + + @EnableAutoConfiguration + @Import(FileSupplierConfiguration.class) + public static class FileSourceConfiguration {} +} diff --git a/applications/source/http-source/README.adoc b/applications/source/http-source/README.adoc new file mode 100644 index 00000000..143d8d45 --- /dev/null +++ b/applications/source/http-source/README.adoc @@ -0,0 +1,31 @@ +//tag::ref-doc[] += Http Source + +A source application that listens for HTTP requests and emits the body as a message payload. +If the Content-Type matches `text/*` or `application/json`, the payload will be a String, +otherwise the payload will be a byte array. + +==== Payload: + +If content type matches `text/*` or `application/json` + +* `String` + +If content type does not match `text/*` or `application/json` + +* `byte array` + +== Options + +The **$$http$$** $$source$$ supports the following configuration properties: + +//tag::configuration-properties[] +$$http.cors.allow-credentials$$:: $$Whether the browser should include any cookies associated with the domain of the request being annotated.$$ *($$Boolean$$, default: `$$$$`)* +$$http.cors.allowed-headers$$:: $$List of request headers that can be used during the actual request.$$ *($$String[]$$, default: `$$$$`)* +$$http.cors.allowed-origins$$:: $$List of allowed origins, e.g. "http://domain1.com".$$ *($$String[]$$, default: `$$$$`)* +$$http.mapped-request-headers$$:: $$Headers that will be mapped.$$ *($$String[]$$, default: `$$$$`)* +$$http.path-pattern$$:: $$HTTP endpoint path mapping.$$ *($$String$$, default: `$$/$$`)* +$$server.port$$:: $$Server HTTP port.$$ *($$Integer$$, default: `$$8080$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/source/http-source/pom.xml b/applications/source/http-source/pom.xml new file mode 100644 index 00000000..b5f0674e --- /dev/null +++ b/applications/source/http-source/pom.xml @@ -0,0 +1,95 @@ + + + 4.0.0 + http-source + 3.0.0.BUILD-SNAPSHOT + http-source + http source apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + http-supplier + ${java-functions.version} + + + org.springframework.boot + spring-boot-starter-webflux + test + + + io.projectreactor + reactor-test + test + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + http + source + ${project.version} + org.springframework.cloud.fn.supplier.http.HttpSupplierConfiguration.class + + + + + org.springframework.cloud.fn + http-supplier + ${java-functions.version} + + + + + spring.main.web-application-type=reactive + spring.cloud.streamapp.security.enabled=false + spring.cloud.streamapp.security.csrf-enabled=false + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/source/http-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/source/http-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..81cfa8ee --- /dev/null +++ b/applications/source/http-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,3 @@ +configuration-properties.classes=HttpSourceProperties,\ + HttpSourceProperties$Cors +configuration-properties.names=server.port \ No newline at end of file diff --git a/applications/source/http-source/src/test/java/org/springframework/cloud/stream/app/HttpSourceTests.java b/applications/source/http-source/src/test/java/org/springframework/cloud/stream/app/HttpSourceTests.java new file mode 100644 index 00000000..c357a759 --- /dev/null +++ b/applications/source/http-source/src/test/java/org/springframework/cloud/stream/app/HttpSourceTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 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 + * + * http://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; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.cloud.fn.supplier.http.HttpSupplierConfiguration; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration; +import org.springframework.cloud.stream.config.BindingServiceConfiguration; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = {"spring.cloud.function.definition=httpSupplier","debug=true"}) +public class HttpSourceTests { + + @Autowired + private TestRestTemplate testRestTemplate; + + @Autowired + OutputDestination outputDestination; + + @Test + public void testSourceFromSupplier() { + testRestTemplate.postForObject("/", "test1", Object.class); + Message sourceMessage = outputDestination.receive(10000); + final String actual = new String(sourceMessage.getPayload()); + assertThat(actual).isEqualTo("test1"); + } + + @SpringBootApplication + @Import({HttpSupplierConfiguration.class, TestChannelBinderConfiguration.class, BindingServiceConfiguration.class}) + public static class HttpSourceConfiguration {} +} diff --git a/applications/source/jdbc-source/README.adoc b/applications/source/jdbc-source/README.adoc new file mode 100644 index 00000000..78dd7544 --- /dev/null +++ b/applications/source/jdbc-source/README.adoc @@ -0,0 +1,36 @@ +//tag::ref-doc[] += JDBC Source + +This source polls data from an RDBMS. +This source is fully based on the `DataSourceAutoConfiguration`, so refer to the https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-sql.html[Spring Boot JDBC Support] for more information. + +=== Payload + +* `Map` when `jdbc.split == true` (default) and `List>` otherwise + +== Options + +The **$$jdbc$$** $$source$$ has the following options: + +//tag::configuration-properties[] +$$jdbc.supplier.max-rows$$:: $$Max numbers of rows to process for query.$$ *($$Integer$$, default: `$$0$$`)* +$$jdbc.supplier.query$$:: $$The query to use to select data.$$ *($$String$$, default: `$$$$`)* +$$jdbc.supplier.split$$:: $$Whether to split the SQL result as individual messages.$$ *($$Boolean$$, default: `$$true$$`)* +$$jdbc.supplier.update$$:: $$An SQL update statement to execute for marking polled messages as 'seen'.$$ *($$String$$, default: `$$$$`)* +$$spring.cloud.stream.poller.cron$$:: $$Cron expression value for the Cron Trigger.$$ *($$String$$, default: `$$$$`)* +$$spring.cloud.stream.poller.fixed-delay$$:: $$Fixed delay for default poller.$$ *($$Long$$, default: `$$1000$$`)* +$$spring.cloud.stream.poller.initial-delay$$:: $$Initial delay for periodic triggers.$$ *($$Integer$$, default: `$$0$$`)* +$$spring.cloud.stream.poller.max-messages-per-poll$$:: $$Maximum messages per poll for the default poller.$$ *($$Long$$, default: `$$1$$`)* +$$spring.datasource.data$$:: $$Data (DML) script resource references.$$ *($$List$$, default: `$$$$`)* +$$spring.datasource.driver-class-name$$:: $$Fully qualified name of the JDBC driver. Auto-detected based on the URL by default.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.initialization-mode$$:: $$Initialize the datasource with available DDL and DML scripts.$$ *($$DataSourceInitializationMode$$, default: `$$embedded$$`, possible values: `ALWAYS`,`EMBEDDED`,`NEVER`)* +$$spring.datasource.password$$:: $$Login password of the database.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.schema$$:: $$Schema (DDL) script resource references.$$ *($$List$$, default: `$$$$`)* +$$spring.datasource.url$$:: $$JDBC URL of the database.$$ *($$String$$, default: `$$$$`)* +$$spring.datasource.username$$:: $$Login username of the database.$$ *($$String$$, default: `$$$$`)* +//end::configuration-properties[] + +Also see the https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html[Spring Boot Documentation] +for addition `DataSource` properties and `TriggerProperties` and `MaxMessagesProperties` for polling options. + +//end::ref-doc[] diff --git a/applications/source/jdbc-source/pom.xml b/applications/source/jdbc-source/pom.xml new file mode 100644 index 00000000..90ff70c6 --- /dev/null +++ b/applications/source/jdbc-source/pom.xml @@ -0,0 +1,100 @@ + + + 4.0.0 + jdbc-source + 3.0.0.BUILD-SNAPSHOT + jdbc-source + JDBC source apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.cloud.fn + jdbc-supplier + ${java-functions.version} + + + org.springframework.cloud + spring-cloud-stream-test-support + test + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-starter-json + test + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + jdbc + source + ${project.version} + org.springframework.cloud.fn.supplier.jdbc.JdbcSupplierConfiguration.class + + + + + org.springframework.cloud.fn + jdbc-supplier + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/source/jdbc-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/source/jdbc-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..5444c6cd --- /dev/null +++ b/applications/source/jdbc-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,10 @@ +configuration-properties.classes=JdbcSupplierProperties,\ + org.springframework.cloud.stream.config.DefaultPollerProperties +configuration-properties.names=\ +spring.datasource.url,\ +spring.datasource.driver-class-name,\ +spring.datasource.username,\ +spring.datasource.password,\ +spring.datasource.schema,\ +spring.datasource.data,\ +spring.datasource.initialization-mode \ No newline at end of file diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/DefaultBehaviorTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/DefaultBehaviorTests.java new file mode 100644 index 00000000..9324da79 --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/DefaultBehaviorTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016-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.app.jdbc.source; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import static org.assertj.core.api.Assertions.assertThat; + +@TestPropertySource(properties = "jdbc.supplier.query=select id, name from test order by id") +class DefaultBehaviorTests extends JdbcSourceIntegrationTests { + + @Test + void testExtraction() throws Exception { + Message received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + Map payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(1, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(2, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(3, payload.get("ID")); + } +} diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/JdbcSourceIntegrationTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/JdbcSourceIntegrationTests.java new file mode 100644 index 00000000..a1871098 --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/JdbcSourceIntegrationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 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.stream.app.jdbc.source; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.fn.supplier.jdbc.JdbcSupplierConfiguration; +import org.springframework.cloud.stream.test.binder.MessageCollector; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.messaging.MessageChannel; +import org.springframework.test.annotation.DirtiesContext; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@SpringBootTest(properties = "spring.cloud.stream.function.definition=jdbcSupplier") +@DirtiesContext +public class JdbcSourceIntegrationTests { + + @Autowired + protected ObjectMapper objectMapper; + + @Qualifier("jdbcSupplier-out-0") + @Autowired + protected MessageChannel output; + + @Autowired + protected JdbcOperations jdbcOperations; + + @Autowired + protected MessageCollector messageCollector; + + @SpringBootApplication + @Import(JdbcSupplierConfiguration.class) + public static class JdbcSourceConfiguration { } + +} diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/Select2PerPollNoSplitWithUpdateTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/Select2PerPollNoSplitWithUpdateTests.java new file mode 100644 index 00000000..2ad4d592 --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/Select2PerPollNoSplitWithUpdateTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-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.app.jdbc.source; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.databind.type.CollectionLikeType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@TestPropertySource(properties = { + "jdbc.supplier.query=select id, name, tag from test where tag is NULL order by id", + "jdbc.supplier.split=false", + "jdbc.supplier.maxRows=2", + "jdbc.supplier.update=update test set tag='1' where id in (:id)" }) +public class Select2PerPollNoSplitWithUpdateTests extends JdbcSourceIntegrationTests { + + @Test + public void testExtraction() throws Exception { + Message received = this.messageCollector.forChannel(this.output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + CollectionLikeType valueType = TypeFactory.defaultInstance() + .constructCollectionLikeType(List.class, Map.class); + + List> payload = this.objectMapper.readValue((String) received.getPayload(), valueType); + + assertEquals(2, payload.size()); + assertEquals(1, payload.get(0).get("ID")); + assertEquals(2, payload.get(1).get("ID")); + received = this.messageCollector.forChannel(this.output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + payload = this.objectMapper.readValue((String) received.getPayload(), valueType); + assertEquals(1, payload.size()); + assertEquals(3, payload.get(0).get("ID")); + } + +} diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllNoSplitTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllNoSplitTests.java new file mode 100644 index 00000000..e04dde44 --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllNoSplitTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 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.stream.app.jdbc.source; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import com.fasterxml.jackson.databind.type.CollectionLikeType; +import com.fasterxml.jackson.databind.type.TypeFactory; +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@TestPropertySource(properties = { + "jdbc.supplier.query=select id, name, tag from test where tag is NULL order by id", + "jdbc.supplier.split=false" +}) +public class SelectAllNoSplitTests extends JdbcSourceIntegrationTests { + + @Test + public void testExtraction() throws Exception { + Message received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + CollectionLikeType valueType = TypeFactory.defaultInstance() + .constructCollectionLikeType(List.class, Map.class); + + List> payload = this.objectMapper.readValue((String) received.getPayload(), valueType); + + assertEquals(3, payload.size()); + assertEquals(1, payload.get(0).get("ID")); + assertEquals("John", payload.get(2).get("NAME")); + } + +} diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithDelayTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithDelayTests.java new file mode 100644 index 00000000..700ae9fc --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithDelayTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016-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.app.jdbc.source; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +@TestPropertySource(properties = { "jdbc.supplier.query=select id, name from test order by id", "spring.cloud.stream.poller.fixedDelay=60000" }) +public class SelectAllWithDelayTests extends JdbcSourceIntegrationTests { + + @Test + public void testExtraction() throws Exception { + Message received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + Map payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(1, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + assertEquals(2, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + assertEquals(3, payload.get("ID")); + // should not wrap around to the beginning since delay is 60 + received = messageCollector.forChannel(output).poll(1, TimeUnit.SECONDS); + assertNull(received); + } +} diff --git a/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithMinDelayTests.java b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithMinDelayTests.java new file mode 100644 index 00000000..27bbda7a --- /dev/null +++ b/applications/source/jdbc-source/src/test/java/org/springframework/cloud/stream/app/jdbc/source/SelectAllWithMinDelayTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2016-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.app.jdbc.source; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@TestPropertySource(properties = { "jdbc.supplier.query=select id, name from test order by id", "spring.cloud.stream.poller.fixedDelay=1" }) +public class SelectAllWithMinDelayTests extends JdbcSourceIntegrationTests { + + @Test + public void testExtraction() throws Exception { + Message received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + Map payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(1, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(2, payload.get("ID")); + received = messageCollector.forChannel(output).poll(10, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(3, payload.get("ID")); + // should wrap around to the beginning + received = messageCollector.forChannel(output).poll(2, TimeUnit.SECONDS); + assertNotNull(received); + assertThat(received.getPayload().getClass()).isEqualTo(String.class); + + payload = this.objectMapper.readValue((String) received.getPayload(), Map.class); + + assertEquals(1, payload.get("ID")); + } + +} diff --git a/applications/source/jdbc-source/src/test/resources/schema.sql b/applications/source/jdbc-source/src/test/resources/schema.sql new file mode 100644 index 00000000..bb5835cb --- /dev/null +++ b/applications/source/jdbc-source/src/test/resources/schema.sql @@ -0,0 +1,10 @@ +-- Run by default by Boot infrastructure + +create table test( + id bigint, + name varchar (2000), + tag char(1) +); +insert into test values (1, 'Bob', NULL); +insert into test values (2, 'Jane', NULL); +insert into test values (3, 'John', NULL); \ No newline at end of file diff --git a/applications/source/mongodb-source/README.adoc b/applications/source/mongodb-source/README.adoc new file mode 100644 index 00000000..ea65829c --- /dev/null +++ b/applications/source/mongodb-source/README.adoc @@ -0,0 +1,25 @@ +//tag::ref-doc[] += MongoDB Source + +This source polls data from MongoDB. +This source is fully based on the `MongoDataAutoConfiguration`, so refer to the +https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-nosql.html#boot-features-mongodb[Spring Boot MongoDB Support] +for more information. + + +== Options + +The **$$mongodb$$** $$source$$ has the following options: + + +//tag::configuration-properties[] +$$mongodb.supplier.collection$$:: $$The MongoDB collection to query$$ *($$String$$, default: `$$$$`)* +$$mongodb.supplier.query$$:: $$The MongoDB query$$ *($$String$$, default: `$${ }$$`)* +$$mongodb.supplier.query-expression$$:: $$The SpEL expression in MongoDB query DSL style$$ *($$Expression$$, default: `$$$$`)* +$$mongodb.supplier.split$$:: $$Whether to split the query result as individual messages.$$ *($$Boolean$$, default: `$$true$$`)* +//end::configuration-properties[] + +Also see the https://docs.spring.io/spring-boot/docs/current/reference/html/common-application-properties.html[Spring Boot Documentation] for additional `MongoProperties` properties. +See and `TriggerProperties` for polling options. + +//end::ref-doc[] diff --git a/applications/source/mongodb-source/pom.xml b/applications/source/mongodb-source/pom.xml new file mode 100644 index 00000000..8d6c7e1a --- /dev/null +++ b/applications/source/mongodb-source/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + mongodb-source + 3.0.0.BUILD-SNAPSHOT + mongodb-source + mongodb source apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + mongodb-supplier + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + mongodb + source + ${project.version} + org.springframework.cloud.fn.supplier.mongo.MongodbSupplierConfiguration.class + + + + + org.springframework.cloud.fn + mongodb-supplier + ${java-functions.version} + + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/source/mongodb-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/source/mongodb-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..db0d96af --- /dev/null +++ b/applications/source/mongodb-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1 @@ +configuration-properties.classes=org.springframework.cloud.fn.supplier.mongo.MongodbSupplierProperties \ No newline at end of file diff --git a/applications/source/pom.xml b/applications/source/pom.xml new file mode 100644 index 00000000..0cb00b77 --- /dev/null +++ b/applications/source/pom.xml @@ -0,0 +1,19 @@ + + + 4.0.0 + + org.springframework.cloud.stream.app + source + 3.0.0.BUILD-SNAPSHOT + pom + + + file-source + jdbc-source + http-source + time-source + mongodb-source + + diff --git a/applications/source/time-source/README.adoc b/applications/source/time-source/README.adoc new file mode 100644 index 00000000..9358c5ce --- /dev/null +++ b/applications/source/time-source/README.adoc @@ -0,0 +1,18 @@ +//tag::ref-doc[] += Time Source + +The time source will simply emit a String with the current time every so often. + +== Options + +The **$$time$$** $$source$$ has the following options: + +//tag::configuration-properties[] +$$spring.cloud.stream.poller.cron$$:: $$Cron expression value for the Cron Trigger.$$ *($$String$$, default: `$$$$`)* +$$spring.cloud.stream.poller.fixed-delay$$:: $$Fixed delay for default poller.$$ *($$Long$$, default: `$$1000$$`)* +$$spring.cloud.stream.poller.initial-delay$$:: $$Initial delay for periodic triggers.$$ *($$Integer$$, default: `$$0$$`)* +$$spring.cloud.stream.poller.max-messages-per-poll$$:: $$Maximum messages per poll for the default poller.$$ *($$Long$$, default: `$$1$$`)* +$$time.date-format$$:: $$Format for the date value.$$ *($$String$$, default: `$$MM/dd/yy HH:mm:ss$$`)* +//end::configuration-properties[] + +//end::ref-doc[] diff --git a/applications/source/time-source/pom.xml b/applications/source/time-source/pom.xml new file mode 100644 index 00000000..1de53685 --- /dev/null +++ b/applications/source/time-source/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + time-source + 3.0.0.BUILD-SNAPSHOT + time-source + time source apps + jar + + + org.springframework.cloud.stream.app + stream-apps-parent + 3.0.0.BUILD-SNAPSHOT + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.cloud.fn + time-supplier + ${java-functions.version} + + + + + + + org.springframework.cloud + spring-cloud-app-starter-doc-maven-plugin + + + org.springframework.cloud.stream.app.plugin + spring-cloud-stream-app-maven-plugin + + + time + source + ${project.version} + org.springframework.cloud.fn.supplier.time.TimeSupplierConfiguration.class + + + + org.springframework.cloud.fn + time-supplier + ${java-functions.version} + + + + + + + + + + + true + + spring-snapshots + Spring Snapshots + https://repo.spring.io/libs-snapshot-local + + + + false + + spring-milestones + Spring Milestones + https://repo.spring.io/libs-milestone-local + + + + diff --git a/applications/source/time-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties b/applications/source/time-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties new file mode 100644 index 00000000..a405e9c7 --- /dev/null +++ b/applications/source/time-source/src/main/resources/META-INF/dataflow-configuration-metadata-whitelist.properties @@ -0,0 +1,2 @@ +configuration-properties.classes=TimeProperties,\ + org.springframework.cloud.stream.config.DefaultPollerProperties \ No newline at end of file diff --git a/applications/source/time-source/src/test/java/org/springframework/cloud/stream/app/TimeSourceTests.java b/applications/source/time-source/src/test/java/org/springframework/cloud/stream/app/TimeSourceTests.java new file mode 100644 index 00000000..9565462c --- /dev/null +++ b/applications/source/time-source/src/test/java/org/springframework/cloud/stream/app/TimeSourceTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 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 + * + * http://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; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.WebApplicationType; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.cloud.fn.supplier.time.TimeProperties; +import org.springframework.cloud.fn.supplier.time.TimeSupplierConfiguration; +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.Import; +import org.springframework.messaging.Message; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +/** + * @author Soby Chacko + */ +public class TimeSourceTests { + + @Test + public void testSourceFromSupplier() { + try (ConfigurableApplicationContext context = new SpringApplicationBuilder( + TestChannelBinderConfiguration + .getCompleteConfiguration(TimeSourceConfiguration.class)) + .web(WebApplicationType.NONE) + .run("--spring.cloud.function.definition=timeSupplier")) { + + OutputDestination target = context.getBean(OutputDestination.class); + Message sourceMessage = target.receive(10000); + final String actual = new String(sourceMessage.getPayload()); + + TimeProperties timeProperties = context.getBean(TimeProperties.class); + SimpleDateFormat dateFormat = new SimpleDateFormat(timeProperties.getDateFormat()); + assertThatCode(() -> { + Date date = dateFormat.parse(actual); + assertThat(date).isNotNull(); + }).doesNotThrowAnyException(); + } + } + + @EnableAutoConfiguration + @Import(TimeSupplierConfiguration.class) + public static class TimeSourceConfiguration {} +} diff --git a/functions/consumer/cassandra-consumer/.gitignore b/functions/consumer/cassandra-consumer/.gitignore new file mode 100644 index 00000000..3a568a50 --- /dev/null +++ b/functions/consumer/cassandra-consumer/.gitignore @@ -0,0 +1,30 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/functions/consumer/cassandra-consumer/.toDelete b/functions/consumer/cassandra-consumer/.toDelete new file mode 100644 index 00000000..e69de29b diff --git a/functions/consumer/cassandra-consumer/pom.xml b/functions/consumer/cassandra-consumer/pom.xml new file mode 100644 index 00000000..117d5875 --- /dev/null +++ b/functions/consumer/cassandra-consumer/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + cassandra-consumer + 1.0.0.BUILD-SNAPSHOT + cassandra-consumer + Cassandra Consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + 0.8.0.BUILD-SNAPSHOT + 4.3.1.0 + + + + + org.springframework.boot + spring-boot-starter-data-cassandra-reactive + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework.integration + spring-integration-cassandra + ${springIntegrationCassandara.version} + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.cassandraunit + cassandra-unit-spring + ${cassandra-unit-spring.version} + test + + + com.addthis.metrics + reporter-config3 + + + + + io.projectreactor + reactor-test + test + + + org.awaitility + awaitility + test + + + + diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerConfiguration.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerConfiguration.java new file mode 100644 index 00000000..f458f4fb --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerConfiguration.java @@ -0,0 +1,209 @@ +/* + * Copyright 2015-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 + * + * http://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.cassandra; + +import java.sql.Date; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.fn.consumer.cassandra.query.InsertQueryColumnNameExtractor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.cassandra.core.InsertOptions; +import org.springframework.data.cassandra.core.ReactiveCassandraOperations; +import org.springframework.data.cassandra.core.UpdateOptions; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.data.cassandra.core.cql.WriteOptions; +import org.springframework.integration.cassandra.outbound.CassandraMessageHandler; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlowBuilder; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.support.json.Jackson2JsonObjectMapper; +import org.springframework.integration.transformer.AbstractPayloadTransformer; +import org.springframework.messaging.MessageHandler; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import org.springframework.cloud.fn.consumer.cassandra.query.ColumnNameExtractor; +import org.springframework.cloud.fn.consumer.cassandra.query.UpdateQueryColumnNameExtractor; +import reactor.core.publisher.Mono; + +/** + * @author Artem Bilan + * @author Thomas Risberg + * @author Ashu Gairola + * @author Akos Ratku + */ +@Configuration +@EnableConfigurationProperties(CassandraConsumerProperties.class) +public class CassandraConsumerConfiguration { + + @Autowired + private CassandraConsumerProperties cassandraSinkProperties; + + @Bean + public IntegrationFlow cassandraConsumerFlow(MessageHandler cassandraSinkMessageHandler, + ObjectMapper objectMapper) { + IntegrationFlowBuilder integrationFlowBuilder = + IntegrationFlows.from(CassandraConsumerFunction.class); + if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) { + integrationFlowBuilder.transform( + new PayloadToMatrixTransformer(objectMapper, this.cassandraSinkProperties.getIngestQuery(), + CassandraMessageHandler.Type.UPDATE == this.cassandraSinkProperties.getQueryType() + ? new UpdateQueryColumnNameExtractor() + : new InsertQueryColumnNameExtractor())); + } + return integrationFlowBuilder + .handle(cassandraSinkMessageHandler) + .get(); + } + + @Bean + public MessageHandler cassandraSinkMessageHandler(ReactiveCassandraOperations cassandraOperations) { + CassandraMessageHandler cassandraMessageHandler = + this.cassandraSinkProperties.getQueryType() != null + ? new CassandraMessageHandler(cassandraOperations, this.cassandraSinkProperties.getQueryType()) + : new CassandraMessageHandler(cassandraOperations); + cassandraMessageHandler.setProducesReply(true); + cassandraMessageHandler.setAsync(true); + if (this.cassandraSinkProperties.getConsistencyLevel() != null + || this.cassandraSinkProperties.getTtl() > 0) { + + WriteOptions.WriteOptionsBuilder writeOptionsBuilder = WriteOptions.builder(); + + switch (this.cassandraSinkProperties.getQueryType()) { + + case INSERT: + writeOptionsBuilder = InsertOptions.builder(); + break; + case UPDATE: + writeOptionsBuilder = UpdateOptions.builder(); + break; + } + + if (this.cassandraSinkProperties.getConsistencyLevel() != null) { + writeOptionsBuilder.consistencyLevel(this.cassandraSinkProperties.getConsistencyLevel()); + } + + if (this.cassandraSinkProperties.getTtl() > 0) { + writeOptionsBuilder.ttl(this.cassandraSinkProperties.getTtl()); + } + + cassandraMessageHandler.setWriteOptions(writeOptionsBuilder.build()); + } + if (StringUtils.hasText(this.cassandraSinkProperties.getIngestQuery())) { + cassandraMessageHandler.setIngestQuery(this.cassandraSinkProperties.getIngestQuery()); + } + else if (this.cassandraSinkProperties.getStatementExpression() != null) { + cassandraMessageHandler.setStatementExpression(this.cassandraSinkProperties.getStatementExpression()); + } + return cassandraMessageHandler; + } + + private static boolean isUuid(String uuid) { + if (uuid.length() == 36) { + String[] parts = uuid.split("-"); + if (parts.length == 5) { + return (parts[0].length() == 8) && (parts[1].length() == 4) && + (parts[2].length() == 4) && (parts[3].length() == 4) && + (parts[4].length() == 12); + } + } + return false; + } + + + private static class PayloadToMatrixTransformer extends AbstractPayloadTransformer>> { + + private final Jackson2JsonObjectMapper jsonObjectMapper; + + private final List columns = new LinkedList<>(); + + private final ISO8601StdDateFormat dateFormat = new ISO8601StdDateFormat(); + + PayloadToMatrixTransformer(ObjectMapper objectMapper, String query, ColumnNameExtractor columnNameExtractor) { + this.jsonObjectMapper = new Jackson2JsonObjectMapper(objectMapper); + this.columns.addAll(columnNameExtractor.extract(query)); + this.jsonObjectMapper.getObjectMapper() + .configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true); + } + + @Override + @SuppressWarnings("unchecked") + protected List> transformPayload(Object payload) { + if (payload instanceof List) { + return (List>) payload; + } + else { + try { + List> model = this.jsonObjectMapper.fromJson(payload, List.class); + List> data = new ArrayList<>(model.size()); + for (Map entity : model) { + List row = new ArrayList<>(this.columns.size()); + for (String column : this.columns) { + Object value = entity.get(column); + if (value instanceof String) { + String string = (String) value; + if (this.dateFormat.looksLikeISO8601(string)) { + synchronized (this.dateFormat) { + value = new Date(this.dateFormat.parse(string).getTime()).toLocalDate(); + } + } + if (isUuid(string)) { + value = UUID.fromString(string); + } + } + row.add(value); + } + data.add(row); + } + return data; + } + catch (Exception ex) { + throw new IllegalArgumentException("Cannot parse json into matrix", ex); + } + } + } + + } + + /* + * We need this to provide visibility to the protected method. + */ + @SuppressWarnings("serial") + private static class ISO8601StdDateFormat extends StdDateFormat { + + @Override + protected boolean looksLikeISO8601(String dateStr) { + return super.looksLikeISO8601(dateStr); + } + + } + + interface CassandraConsumerFunction extends Function> { + + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerProperties.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerProperties.java new file mode 100644 index 00000000..1af4094d --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerProperties.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019-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 + * + * http://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.cassandra; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.integration.cassandra.outbound.CassandraMessageHandler; + +import com.datastax.oss.driver.api.core.ConsistencyLevel; + +/** + * @author Artem Bilan + * @author Thomas Risberg + */ +@ConfigurationProperties("cassandra") +public class CassandraConsumerProperties { + + /** + * Time-to-live option of WriteOptions. + */ + private int ttl; + + /** + * QueryType for Cassandra Sink. + */ + private CassandraMessageHandler.Type queryType; + + /** + * Ingest Cassandra query. + */ + private String ingestQuery; + + /** + * Expression in Cassandra query DSL style. + */ + private Expression statementExpression; + + /** + * The consistency level for write operation. + */ + private ConsistencyLevel consistencyLevel; + + public int getTtl() { + return this.ttl; + } + + public void setTtl(int ttl) { + this.ttl = ttl; + } + + public CassandraMessageHandler.Type getQueryType() { + return this.queryType; + } + + public void setQueryType(CassandraMessageHandler.Type queryType) { + this.queryType = queryType; + } + + public String getIngestQuery() { + return this.ingestQuery; + } + + public void setIngestQuery(String ingestQuery) { + this.ingestQuery = ingestQuery; + } + + public Expression getStatementExpression() { + return this.statementExpression; + } + + public void setStatementExpression(Expression statementExpression) { + this.statementExpression = statementExpression; + } + + public ConsistencyLevel getConsistencyLevel() { + return this.consistencyLevel; + } + + public void setConsistencyLevel(ConsistencyLevel consistencyLevel) { + this.consistencyLevel = consistencyLevel; + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraAppClusterConfiguration.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraAppClusterConfiguration.java new file mode 100644 index 00000000..06e60ab4 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraAppClusterConfiguration.java @@ -0,0 +1,157 @@ +/* + * Copyright 2019-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 + * + * http://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.cassandra.cluster; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Scanner; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.boot.autoconfigure.cassandra.CassandraProperties; +import org.springframework.boot.autoconfigure.cassandra.CqlSessionBuilderCustomizer; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.domain.EntityScanPackages; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.PropertyMapper; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.context.EnvironmentAware; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.data.cassandra.config.CqlSessionFactoryBean; +import org.springframework.data.cassandra.core.ReactiveCassandraTemplate; +import org.springframework.data.cassandra.core.cql.CqlTemplate; +import org.springframework.data.cassandra.core.cql.ReactiveCqlOperations; +import org.springframework.data.cassandra.core.cql.generator.CreateKeyspaceCqlGenerator; +import org.springframework.data.cassandra.core.cql.keyspace.CreateKeyspaceSpecification; +import org.springframework.util.StringUtils; + +import com.datastax.oss.driver.api.core.CqlSession; +import com.datastax.oss.driver.api.core.CqlSessionBuilder; +import reactor.core.publisher.Flux; + +/** + * @author Artem Bilan + * @author Thomas Risberg + * @author Rob Hardt + */ +@Configuration +@EnableConfigurationProperties(CassandraClusterProperties.class) +@Import(CassandraAppClusterConfiguration.CassandraPackageRegistrar.class) +public class CassandraAppClusterConfiguration { + + @Bean + public CqlSessionBuilderCustomizer clusterBuilderCustomizer( + CassandraClusterProperties cassandraClusterProperties) { + + PropertyMapper map = PropertyMapper.get(); + return builder -> + map.from(cassandraClusterProperties::isSkipSslValidation) + .whenTrue() + .toCall(() -> { + try { + builder.withSslContext(TrustAllSSLContextFactory.getSslContext()); + } + catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new BeanInitializationException( + "Unable to configure a Cassandra cluster using SSL.", e); + } + + }); + } + + @Bean + @ConditionalOnProperty("cassandra.cluster.create-keyspace") + public Object keyspaceCreator(CassandraProperties cassandraProperties, CqlSessionBuilder cqlSessionBuilder) { + CreateKeyspaceSpecification createKeyspaceSpecification = + CreateKeyspaceSpecification + .createKeyspace(cassandraProperties.getKeyspaceName()) + .withSimpleReplication() + .ifNotExists(); + + String createKeySpaceQuery = new CreateKeyspaceCqlGenerator(createKeyspaceSpecification).toCql(); + CqlSession systemSession = + cqlSessionBuilder.withKeyspace(CqlSessionFactoryBean.CASSANDRA_SYSTEM_SESSION).build(); + + CqlTemplate template = new CqlTemplate(systemSession); + template.execute(createKeySpaceQuery); + + return null; + } + + @Bean + @Lazy + @DependsOn("keyspaceCreator") + public CqlSession cassandraSession(CqlSessionBuilder cqlSessionBuilder) { + return cqlSessionBuilder.build(); + } + + + @Bean + @ConditionalOnProperty("cassandra.cluster.init-script") + public Object keyspaceInitializer(CassandraClusterProperties cassandraClusterProperties, + ReactiveCassandraTemplate reactiveCassandraTemplate) throws IOException { + + String scripts = + new Scanner(cassandraClusterProperties.getInitScript().getInputStream(), + StandardCharsets.UTF_8.name()) + .useDelimiter("\\A") + .next(); + + ReactiveCqlOperations reactiveCqlOperations = + reactiveCassandraTemplate.getReactiveCqlOperations(); + + Flux.fromArray(StringUtils.delimitedListToStringArray(scripts, ";", "\r\n\f")) + .filter(StringUtils::hasText) // an empty String after the last ';' + .flatMap(script -> reactiveCqlOperations.execute(script + ";")) + .blockLast(); + + return null; + + } + + static class CassandraPackageRegistrar implements ImportBeanDefinitionRegistrar, EnvironmentAware { + + private Environment environment; + + @Override + public void setEnvironment(Environment environment) { + this.environment = environment; + } + + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, + BeanDefinitionRegistry registry) { + + Binder.get(this.environment) + .bind("cassandra.cluster.entity-base-packages", String[].class) + .map(Arrays::asList) + .ifBound(packagesToScan -> EntityScanPackages.register(registry, packagesToScan)); + } + + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraClusterProperties.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraClusterProperties.java new file mode 100644 index 00000000..67e7e549 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/CassandraClusterProperties.java @@ -0,0 +1,85 @@ +/* + * 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 + * + * http://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.cassandra.cluster; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; + +/** + * Common properties for the cassandra modules. + * + * @author Artem Bilan + * @author Thomas Risberg + * @author Rob Hardt + */ +@ConfigurationProperties("cassandra.cluster") +public class CassandraClusterProperties { + + /** + * Flag to create (or not) keyspace on application startup. + */ + private boolean createKeyspace; + + /** + * Resource with CQL scripts (delimited by ';') to initialize keyspace schema. + */ + private Resource initScript; + + /** + * Flag to validate the Servers' SSL certs + */ + private boolean skipSslValidation; + + /** + * Base packages to scan for entities annotated with Table annotations. + */ + private String[] entityBasePackages = { }; + + + public void setCreateKeyspace(boolean createKeyspace) { + this.createKeyspace = createKeyspace; + } + + public void setInitScript(Resource initScript) { + this.initScript = initScript; + } + + public void setSkipSslValidation(boolean skipSslValidation) { + this.skipSslValidation = skipSslValidation; + } + + public boolean isCreateKeyspace() { + return this.createKeyspace; + } + + public Resource getInitScript() { + return this.initScript; + } + + public boolean isSkipSslValidation() { + return this.skipSslValidation; + } + + public String[] getEntityBasePackages() { + return this.entityBasePackages; + } + + public void setEntityBasePackages(String[] entityBasePackages) { + this.entityBasePackages = entityBasePackages; + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/TrustAllSSLContextFactory.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/TrustAllSSLContextFactory.java new file mode 100644 index 00000000..97535139 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/cluster/TrustAllSSLContextFactory.java @@ -0,0 +1,66 @@ +/* + * Copyright 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 + * + * http://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.cassandra.cluster; + +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + + +/** + * Helper to provide an SSL Context that does not validate + * certificates presented in the SSL handshake. + * + * The usual caveats apply. + * + * @author Rob Hardt + * @author Artem Bilan + */ +class TrustAllSSLContextFactory { + + static SSLContext getSslContext() throws NoSuchAlgorithmException, KeyManagementException { + + TrustManager[] trustAllCerts = new TrustManager[] { + new X509TrustManager() { + + @Override + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) { + } + + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) { + } + + } + }; + + SSLContext sc = SSLContext.getInstance("SSL"); + sc.init(null, trustAllCerts, new SecureRandom()); + return sc; + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/ColumnNameExtractor.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/ColumnNameExtractor.java new file mode 100644 index 00000000..19d0d769 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/ColumnNameExtractor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2017-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 + * + * http://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.cassandra.query; + +import java.util.List; + +/** + * @author Akos Ratku + * @author Artem Bilan + */ +@FunctionalInterface +public interface ColumnNameExtractor { + + List extract(String query); + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/InsertQueryColumnNameExtractor.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/InsertQueryColumnNameExtractor.java new file mode 100644 index 00000000..967fcd0f --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/InsertQueryColumnNameExtractor.java @@ -0,0 +1,56 @@ +/* + * Copyright 2017 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 + * + * http://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.cassandra.query; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.StringUtils; + +/** + * @author Akos Ratku + * @author Artem Bilan + */ +public class InsertQueryColumnNameExtractor implements ColumnNameExtractor { + + private static final Pattern PATTERN = Pattern.compile(".+\\((.+)\\).+(?:values\\s*\\((.+)\\))"); + + @Override + public List extract(String query) { + List extractedColumns = new LinkedList<>(); + Matcher matcher = PATTERN.matcher(query); + if (matcher.matches()) { + String[] columns = StringUtils.delimitedListToStringArray(matcher.group(1), ",", " "); + String[] params = StringUtils.delimitedListToStringArray(matcher.group(2), ",", " "); + for (int i = 0; i < columns.length; i++) { + String param = params[i]; + if (param.equals("?")) { + extractedColumns.add(columns[i]); + } + else if (param.startsWith(":")) { + extractedColumns.add(param.substring(1)); + } + } + } + else { + throw new IllegalArgumentException("Invalid CQL insert query syntax: " + query); + } + return extractedColumns; + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/UpdateQueryColumnNameExtractor.java b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/UpdateQueryColumnNameExtractor.java new file mode 100644 index 00000000..462478b5 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/java/org/springframework/cloud/fn/consumer/cassandra/query/UpdateQueryColumnNameExtractor.java @@ -0,0 +1,58 @@ +/* + * Copyright 2017 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 + * + * http://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.cassandra.query; + +import java.util.LinkedList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.StringUtils; + +/** + * @author Akos Ratku + * @author Artem Bilan + */ +public class UpdateQueryColumnNameExtractor implements ColumnNameExtractor { + + private static final Pattern PATTERN = Pattern.compile("(?i)(?<=set)(.*)(?=where)where(.*)"); + + @Override + public List extract(String query) { + List extractedColumns = new LinkedList<>(); + Matcher matcher = PATTERN.matcher(query); + if (matcher.find()) { + String[] settings = StringUtils.delimitedListToStringArray(matcher.group(1), ",", " "); + String[] where = StringUtils.delimitedListToStringArray(matcher.group(2), ",", " "); + readPairs(extractedColumns, settings); + readPairs(extractedColumns, where); + } + else { + throw new IllegalArgumentException("Invalid CQL update query syntax: " + query); + } + return extractedColumns; + } + + protected void readPairs(List extractedColumns, String[] settings) { + for (String setting : settings) { + String[] columnValuePair = StringUtils.delimitedListToStringArray(setting, "=", " "); + if (columnValuePair[1].startsWith(":") || columnValuePair[1].equals("?")) { + extractedColumns.add(columnValuePair[0]); + } + } + } + +} diff --git a/functions/consumer/cassandra-consumer/src/main/resources/application.properties b/functions/consumer/cassandra-consumer/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerApplicationTests.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerApplicationTests.java new file mode 100644 index 00000000..df0652bd --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraConsumerApplicationTests.java @@ -0,0 +1,103 @@ +/* + * Copyright 2019-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.cassandra; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.function.Function; + +import org.springframework.cloud.fn.consumer.cassandra.domain.Book; +import org.cassandraunit.spring.CassandraUnitDependencyInjectionIntegrationTestExecutionListener; +import org.cassandraunit.spring.EmbeddedCassandra; +import org.cassandraunit.utils.EmbeddedCassandraServerHelper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import reactor.core.publisher.Mono; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.cassandra.core.CassandraOperations; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.TestExecutionListeners; + +/** + * @author Artem Bilan + */ +@TestExecutionListeners(mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS, + listeners = CassandraUnitDependencyInjectionIntegrationTestExecutionListener.class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = { + "spring.data.cassandra.keyspaceName=" + CassandraConsumerApplicationTests.CASSANDRA_KEYSPACE, + "spring.data.cassandra.localDatacenter=datacenter1", + "cassandra.cluster.createKeyspace=true" }) +@EmbeddedCassandra(configuration = EmbeddedCassandraServerHelper.CASSANDRA_RNDPORT_YML_FILE, timeout = 120000) +@DirtiesContext +abstract class CassandraConsumerApplicationTests { + + static final String CASSANDRA_KEYSPACE = "test"; + + @Autowired + protected CassandraOperations cassandraTemplate; + + @Autowired + protected Function> cassandraConsumer; + + @BeforeAll + static void setUp() { + EmbeddedCassandraServerHelper.getSession(); + System.setProperty("spring.data.cassandra.contactPoints", + EmbeddedCassandraServerHelper.getHost() + ':' + EmbeddedCassandraServerHelper.getNativeTransportPort()); + } + + @AfterAll + static void cleanup() { + System.clearProperty("spring.data.cassandra.contactPoints"); + } + + @AfterEach + void tearDown() { + this.cassandraTemplate.truncate(Book.class); + } + + protected static List getBookList(int numBooks) { + + List books = new ArrayList<>(); + + Book b; + for (int i = 0; i < numBooks; i++) { + b = new Book(); + b.setIsbn(UUID.randomUUID()); + b.setTitle("Spring Cloud Data Flow Guide"); + b.setAuthor("SCDF Guru"); + b.setPages(i * 10 + 5); + b.setInStock(true); + b.setSaleDate(LocalDate.now()); + books.add(b); + } + + return books; + } + + @SpringBootApplication + static class TestApplication {} + +} diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraEntityInsertTests.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraEntityInsertTests.java new file mode 100644 index 00000000..9ea74e77 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraEntityInsertTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019-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.cassandra; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDate; +import java.util.UUID; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.cloud.fn.consumer.cassandra.domain.Book; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.test.context.TestPropertySource; + +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * @author Artem Bilan + */ +@DisabledOnOs(OS.WINDOWS) +@TestPropertySource(properties = { + "spring.data.cassandra.schema-action=RECREATE", + "cassandra.cluster.entity-base-packages=io.pivotal.java.function.cassandra.consumer.domain" }) +class CassandraEntityInsertTests extends CassandraConsumerApplicationTests { + + @Test + @Disabled + void testInsert() { + Book book = new Book(); + book.setIsbn(UUID.randomUUID()); + book.setTitle("Spring Integration Cassandra"); + book.setAuthor("Cassandra Guru"); + book.setPages(521); + book.setSaleDate(LocalDate.now()); + book.setInStock(true); + + Mono result = this.cassandraConsumer.apply(book); + + StepVerifier.create(result) + .expectNextCount(1) + .then(() -> + assertThat(this.cassandraTemplate.query(Book.class) + .count()) + .isEqualTo(1)) + .verifyComplete(); + } + +} diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestInsertTests.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestInsertTests.java new file mode 100644 index 00000000..dd3cdabd --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestInsertTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2019-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.cassandra; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.fn.consumer.cassandra.domain.Book; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.integration.support.json.Jackson2JsonObjectMapper; +import org.springframework.test.context.TestPropertySource; + +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * @author Artem Bilan + */ +@DisabledOnOs(OS.WINDOWS) +@TestPropertySource(properties = { + "cassandra.cluster.init-script=init-db.cql", + "cassandra.ingest-query=" + + "insert into book (isbn, title, author, pages, saleDate, inStock) values (?, ?, ?, ?, ?, ?)" }) +class CassandraIngestInsertTests extends CassandraConsumerApplicationTests { + + @Test + void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception { + List books = getBookList(5); + + Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper); + + Mono result = + this.cassandraConsumer.apply(mapper.toJson(books)); + + StepVerifier.create(result) + .expectNextCount(1) + .then(() -> + assertThat(this.cassandraTemplate.query(Book.class) + .count()) + .isEqualTo(5)) + .verifyComplete(); + } + +} diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestNamedParamsTests.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestNamedParamsTests.java new file mode 100644 index 00000000..844a12cc --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestNamedParamsTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019-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.cassandra; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.fn.consumer.cassandra.domain.Book; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.integration.support.json.Jackson2JsonObjectMapper; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * @author Artem Bilan + */ +@DisabledOnOs(OS.WINDOWS) +@TestPropertySource(properties = { + "cassandra.cluster.init-script=init-db.cql", + "cassandra.ingest-query=" + + "insert into book (isbn, title, author, pages, saleDate, inStock) " + + "values (:myIsbn, :myTitle, :myAuthor, ?, ?, ?)" }) +class CassandraIngestNamedParamsTests extends CassandraConsumerApplicationTests { + + @Test + void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception { + List books = getBookList(5); + + Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper); + + String booksJsonWithNamedParams = mapper.toJson(books); + booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "isbn", "myIsbn"); + booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "title", "myTitle"); + booksJsonWithNamedParams = StringUtils.replace(booksJsonWithNamedParams, "author", "myAuthor"); + + Mono result = + this.cassandraConsumer.apply(booksJsonWithNamedParams); + + StepVerifier.create(result) + .expectNextCount(1) + .then(() -> + assertThat(this.cassandraTemplate.query(Book.class) + .count()) + .isEqualTo(5)) + .verifyComplete(); + } + +} diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestUpdateTests.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestUpdateTests.java new file mode 100644 index 00000000..2f64956c --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/CassandraIngestUpdateTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019-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.cassandra; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cloud.fn.consumer.cassandra.domain.Book; +import org.springframework.data.cassandra.core.WriteResult; +import org.springframework.integration.support.json.Jackson2JsonObjectMapper; +import org.springframework.test.context.TestPropertySource; + +import com.fasterxml.jackson.databind.ObjectMapper; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +/** + * @author Artem Bilan + */ +@DisabledOnOs(OS.WINDOWS) +@TestPropertySource(properties = { + "cassandra.cluster.init-script=init-db.cql", + "cassandra.ingest-query=" + + "update book set inStock = :inStock, author = :author, pages = :pages, " + + "saleDate = :saleDate, title = :title where isbn = :isbn", + "cassandra.queryType=UPDATE" }) +class CassandraIngestUpdateTests extends CassandraConsumerApplicationTests { + + @Test + void testIngestQuery(@Autowired ObjectMapper objectMapper) throws Exception { + List books = getBookList(5); + + Jackson2JsonObjectMapper mapper = new Jackson2JsonObjectMapper(objectMapper); + + Mono result = + this.cassandraConsumer.apply(mapper.toJson(books)); + + StepVerifier.create(result) + .expectNextCount(1) + .then(() -> + assertThat(this.cassandraTemplate.query(Book.class) + .count()) + .isEqualTo(5)) + .verifyComplete(); + } + +} diff --git a/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/domain/Book.java b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/domain/Book.java new file mode 100644 index 00000000..f1d30388 --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/java/org/springframework/cloud/fn/consumer/cassandra/domain/Book.java @@ -0,0 +1,146 @@ +/* + * 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 + * + * http://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.cassandra.domain; + +import java.time.LocalDate; +import java.util.UUID; + +import org.springframework.data.cassandra.core.mapping.PrimaryKey; +import org.springframework.data.cassandra.core.mapping.Table; + +/** + * Test POJO + * + * @author David Webb + * @author Artem Bilan + */ +@Table("book") +public class Book { + + @PrimaryKey + private UUID isbn; + + private String title; + + private String author; + + private int pages; + + private LocalDate saleDate; + + private boolean inStock; + + public Book() { + } + + public Book(UUID isbn, String title, String author) { + this.isbn = isbn; + this.title = title; + this.author = author; + } + + /** + * @return Returns the isbn. + */ + public UUID getIsbn() { + return this.isbn; + } + + /** + * @return Returns the saleDate. + */ + public LocalDate getSaleDate() { + return this.saleDate; + } + + /** + * @param saleDate The saleDate to set. + */ + public void setSaleDate(LocalDate saleDate) { + this.saleDate = saleDate; + } + + /** + * @return Returns the inStock. + */ + public boolean isInStock() { + return this.inStock; + } + + /** + * @param inStock The isInStock to set. + */ + public void setInStock(boolean inStock) { + this.inStock = inStock; + } + + /** + * @param isbn The isbn to set. + */ + public void setIsbn(UUID isbn) { + this.isbn = isbn; + } + + /** + * @return Returns the title. + */ + public String getTitle() { + return this.title; + } + + /** + * @param title The title to set. + */ + public void setTitle(String title) { + this.title = title; + } + + /** + * @return Returns the author. + */ + public String getAuthor() { + return this.author; + } + + /** + * @param author The author to set. + */ + public void setAuthor(String author) { + this.author = author; + } + + /** + * @return Returns the pages. + */ + public int getPages() { + return this.pages; + } + + /** + * @param pages The pages to set. + */ + public void setPages(int pages) { + this.pages = pages; + } + + @Override + public String toString() { + return ("isbn -> " + this.isbn) + "\n" + "tile -> " + this.title + "\n" + "author -> " + this.author + + "\n" + "pages -> " + this.pages + "\n"; + } + +} diff --git a/functions/consumer/cassandra-consumer/src/test/resources/init-db.cql b/functions/consumer/cassandra-consumer/src/test/resources/init-db.cql new file mode 100644 index 00000000..6c1397dc --- /dev/null +++ b/functions/consumer/cassandra-consumer/src/test/resources/init-db.cql @@ -0,0 +1,10 @@ +DROP TABLE IF EXISTS book; + +CREATE TABLE book ( + isbn uuid PRIMARY KEY, + author text, + instock boolean, + pages int, + saledate date, + title text +); diff --git a/functions/consumer/counter-consumer/.gitignore b/functions/consumer/counter-consumer/.gitignore new file mode 100644 index 00000000..4a453031 --- /dev/null +++ b/functions/consumer/counter-consumer/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/functions/consumer/counter-consumer/pom.xml b/functions/consumer/counter-consumer/pom.xml new file mode 100644 index 00000000..1030fddf --- /dev/null +++ b/functions/consumer/counter-consumer/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + counter-consumer + 1.0.0.BUILD-SNAPSHOT + counter-consumer + Spring Native Consumer for computing counters + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.cloud.fn + payload-converter-function + ${project.version} + + + org.springframework.boot + spring-boot-starter-integration + + + io.micrometer + micrometer-core + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + diff --git a/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerConfiguration.java b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerConfiguration.java new file mode 100644 index 00000000..09bd9f0d --- /dev/null +++ b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerConfiguration.java @@ -0,0 +1,178 @@ +/* + * Copyright 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.counter; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Collectors; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.context.properties.ConfigurationPropertiesBinding; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.convert.converter.Converter; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.integration.context.IntegrationContextUtils; +import org.springframework.messaging.Message; +import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * + * @author Christian Tzolov + */ +@Configuration +@EnableConfigurationProperties({ CounterConsumerProperties.class }) +public class CounterConsumerConfiguration { + + @Bean + public Function stringToSpelFunction(@Lazy EvaluationContext evaluationContext) { + return new StringToSpelConversionFunction(evaluationContext); + } + + @Bean + @ConfigurationPropertiesBinding + public Converter propertiesSpelConverter(Function stringToSpelFunction) { + return new Converter() { // NOTE Using lambda causes Java Generics issues. + @Override + public Expression convert(String source) { + return stringToSpelFunction.apply(source); + } + }; + } + + @Bean(name = "counterConsumer") + public Consumer> counterConsumer(CounterConsumerProperties properties, MeterRegistry[] meterRegistries, + @Qualifier(IntegrationContextUtils.INTEGRATION_EVALUATION_CONTEXT_BEAN_NAME) EvaluationContext context) { + + return message -> { + + String counterName = properties.getComputedNameExpression().getValue(context, message, CharSequence.class).toString(); + + // All fixed tags together are passed with every counter increment. + Tags fixedTags = this.toTags(properties.getTag().getFixed()); + + double amount = properties.getComputedAmountExpression().getValue(context, message, double.class); + + Map> allGroupedTags = new HashMap<>(); + // Tag Expressions Counter + if (properties.getTag().getExpression() != null) { + + Map> groupedTags = properties.getTag().getExpression().entrySet().stream() + // maps a pair into [, ... ] Tag array. + .map(namedExpression -> + toList(namedExpression.getValue().getValue(context, message)).stream() + .map(tagValue -> Tag.of(namedExpression.getKey(), tagValue)) + .collect(Collectors.toList())).flatMap(List::stream) + .collect(Collectors.groupingBy(tag -> tag.getKey(), Collectors.toList())); + allGroupedTags.putAll(groupedTags); + } + + this.count(meterRegistries, counterName, fixedTags, allGroupedTags, amount); + }; + } + + /** + * Converts a key/value Map into Tag(key,value) list. Filters out the empty key/value pairs. + * @param keyValueMap key/value map to convert into tags. + * @return Returns Tags list representing every non-empty key/value pair. + */ + protected Tags toTags(Map keyValueMap) { + return CollectionUtils.isEmpty(keyValueMap) ? Tags.empty() : + Tags.of(keyValueMap.entrySet().stream() + .filter(e -> StringUtils.hasText(e.getKey()) && StringUtils.hasText(e.getValue())) + .map(e -> Tag.of(e.getKey(), e.getValue())) + .collect(Collectors.toList())); + } + + /** + * Converts the input value into an list of values. If the value is not a collection/array type the result + * is a single element list. For collection/array input value the result is the list of stringifie content of + * this collection. + * @param value input value can be array, collection or single value. + * @return Returns value list. + */ + protected List toList(Object value) { + if (value == null) { + return Collections.emptyList(); + } + + if ((value instanceof Collection) || ObjectUtils.isArray(value)) { + Collection valueCollection = (value instanceof Collection) ? (Collection) value + : Arrays.asList(ObjectUtils.toObjectArray(value)); + + return valueCollection.stream() + .filter(v -> v != null) + .map(Object::toString) + .filter(StringUtils::hasText) + .collect(Collectors.toList()); + } + else { + return Arrays.asList(value.toString()); + } + } + + private void count(MeterRegistry[] meterRegistries, String counterName, Tags fixedTags, Map> groupedTags, double amount) { + if (!CollectionUtils.isEmpty(groupedTags)) { + groupedTags.values().stream().map(List::size).max(Integer::compareTo).ifPresent( + max -> { + for (int i = 0; i < max; i++) { + Tags currentTags = Tags.of(fixedTags); + for (Map.Entry> e : groupedTags.entrySet()) { + currentTags = (e.getValue().size() > i) ? + currentTags.and(e.getValue().get(i)) : + currentTags.and(Tags.of(e.getKey(), "")); + } + + // Increment the counterName increment for every configured MaterRegistry. + for (MeterRegistry meterRegistry : meterRegistries) { + meterRegistry.counter(counterName, currentTags).increment(amount); + } + } + } + ); + } + else { + // Increment the counterName increment for every configured MaterRegistry. + for (MeterRegistry meterRegistry : meterRegistries) { + meterRegistry.counter(counterName, fixedTags).increment(amount); + } + } + } + + @Bean + @ConditionalOnMissingBean + public SimpleMeterRegistry simpleMeterRegistry() { + return new SimpleMeterRegistry(); + } +} diff --git a/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerProperties.java b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerProperties.java new file mode 100644 index 00000000..a3442fd8 --- /dev/null +++ b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerProperties.java @@ -0,0 +1,172 @@ +/* + * Copyright 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.counter; + +import java.util.Map; + +import javax.validation.constraints.AssertTrue; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.validation.annotation.Validated; + +/** + * @author Christian Tzolov + */ +@ConfigurationProperties("counter") +@Validated +public class CounterConsumerProperties { + + /** + * The default name of the increment + */ + @Value("${spring.application.name:counts}") + private String defaultName; + + /** + * The name of the counter to increment. The 'name' and 'nameExpression' are mutually exclusive. + * Only one can be set. + */ + private String name; + + /** + * A SpEL expression (against the incoming Message) to derive the name of the counter to increment. + * The 'name' and 'nameExpression' are mutually exclusive. Only one can be set. + */ + private Expression nameExpression; + + /** + * A SpEL expression (against the incoming Message) to derive the amount to add to the counter. + * If not set the counter is incremented by 1.0 + */ + private Expression amountExpression; + + /** + * Enables counting the number of messages processed. Uses the 'message.' counter name prefix to distinct it + * form the expression based counter. The message counter includes the fixed tags when provided. + */ + private boolean messageCounterEnabled = true; + + /** + * Fixed and computed tags to be assignee with the counter increment measurement. + */ + private MetricsTag tag = new MetricsTag(); + + public static class MetricsTag { + + /** + * Custom tags assigned to every counter increment measurements. + * This is a map so the property convention fixed tags is: counter.tag.fixed.[tag-name]=[tag-value] + */ + private Map fixed; + + /** + * Computes tags from SpEL expression. + * Single SpEL expression can produce an array of values, which in turn means distinct name/value tags. + * Every name/value tag will produce a separate counter increment. + * Tag expression format is: counter.tag.expression.[tag-name]=[SpEL expression] + */ + private Map expression; + + public Map getFixed() { + return fixed; + } + + public void setFixed(Map fixed) { + this.fixed = fixed; + } + + public Map getExpression() { + return expression; + } + + public void setExpression(Map expression) { + this.expression = expression; + } + + @Override + public String toString() { + return "MetricsTag{" + + "fixed=" + fixed + + ", expression=" + expression + + '}'; + } + } + + public MetricsTag getTag() { + return tag; + } + + public String getName() { + if (name == null && nameExpression == null) { + return defaultName; + } + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Expression getNameExpression() { + return nameExpression; + } + + public void setNameExpression(Expression nameExpression) { + this.nameExpression = nameExpression; + } + + public Expression getAmountExpression() { + return amountExpression; + } + + public void setAmountExpression(Expression amountExpression) { + this.amountExpression = amountExpression; + } + + public Expression getComputedAmountExpression() { + return (amountExpression != null ? amountExpression : new LiteralExpression("1.0")); + } + + public Expression getComputedNameExpression() { + return (nameExpression != null ? nameExpression : new LiteralExpression(getName())); + } + + public boolean isMessageCounterEnabled() { + return messageCounterEnabled; + } + + public void setMessageCounterEnabled(boolean messageCounterEnabled) { + this.messageCounterEnabled = messageCounterEnabled; + } + + @AssertTrue(message = "exactly one of 'name' and 'nameExpression' must be set") + public boolean isExclusiveOptions() { + return getName() != null ^ getNameExpression() != null; + } + + @Override + public String toString() { + return "CounterFunctionProperties{" + + "defaultName='" + defaultName + '\'' + + ", name=" + name + + ", tag=" + tag + + '}'; + } +} diff --git a/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/StringToSpelConversionFunction.java b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/StringToSpelConversionFunction.java new file mode 100644 index 00000000..a8a5e99f --- /dev/null +++ b/functions/consumer/counter-consumer/src/main/java/org/springframework/cloud/fn/consumer/counter/StringToSpelConversionFunction.java @@ -0,0 +1,61 @@ +/* + * Copyright 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.counter; + +import java.util.function.Function; + +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ParseException; +import org.springframework.expression.spel.standard.SpelExpression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Converter from String to Spring Expression. + * + * TODO: This could be a top level project. + */ +public class StringToSpelConversionFunction implements Function { + + private final SpelExpressionParser parser; + + private final EvaluationContext evaluationContext; + + public StringToSpelConversionFunction(EvaluationContext evaluationContext) { + this(new SpelExpressionParser(), evaluationContext); + } + + public StringToSpelConversionFunction(SpelExpressionParser parser, EvaluationContext evaluationContext) { + this.evaluationContext = evaluationContext; + this.parser = parser; + } + + @Override + public Expression apply(String source) { + try { + Expression expression = parser.parseExpression(source); + if (expression instanceof SpelExpression) { + ((SpelExpression) expression).setEvaluationContext(evaluationContext); + } + return expression; + } + catch (ParseException e) { + throw new IllegalArgumentException(String.format( + "Could not convert '%s' into a SpEL expression", source), e); + } + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ConverterFunctionAdapter.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ConverterFunctionAdapter.java new file mode 100644 index 00000000..14f21775 --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ConverterFunctionAdapter.java @@ -0,0 +1,22 @@ +package org.springframework.cloud.fn.consumer.counter; + +import java.util.function.Function; + +import org.springframework.core.convert.converter.Converter; + +/** + * @author Christian Tzolov + */ +public class ConverterFunctionAdapter implements Converter { + + private Function function; + + public ConverterFunctionAdapter(Function function) { + this.function = function; + } + + @Override + public T convert(S s) { + return this.function.apply(s); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CountWithAmountTest.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CountWithAmountTest.java new file mode 100644 index 00000000..92ef115c --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CountWithAmountTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 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.counter; + +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name=counter666", + "counter.tag.expression.foo='bar'", + "counter.amount-expression=payload.length()" +}) +class CountWithAmountTest extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + String message = "hello world message"; + double messageSize = Long.valueOf(message.length()).doubleValue(); + counterConsumer.accept(new GenericMessage(message)); + assertThat(meterRegistry.find("counter666").counter().count()).isEqualTo(messageSize); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerParentTest.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerParentTest.java new file mode 100644 index 00000000..78f4790f --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/CounterConsumerParentTest.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.counter; + +import java.util.function.Consumer; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@DirtiesContext +public class CounterConsumerParentTest { + + @Autowired + protected SimpleMeterRegistry meterRegistry; + + @Autowired + protected Consumer> counterConsumer; + + protected Message message(String payload) { + return MessageBuilder.withPayload(payload.getBytes()).build(); + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/EmptyTagsTests.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/EmptyTagsTests.java new file mode 100644 index 00000000..898e67ac --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/EmptyTagsTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.counter; + +import java.util.Collection; + +import io.micrometer.core.instrument.Counter; +import org.junit.jupiter.api.Test; + +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name=counter666", + "counter.tag.fixed.foo=", + "counter.tag.expression.tag666=#jsonPath(payload,'$..noField')", + "counter.tag.expression.test=#jsonPath(payload,'$..test')", +}) +class EmptyTagsTests extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + + counterConsumer.accept(message("{\"test\": \"Bar\"}")); + + Collection fixedTagsCounters = meterRegistry.find("counter666").tagKeys("foo").counters(); + assertThat(fixedTagsCounters.size()).isEqualTo(0); + + Collection expressionTagsCounters = meterRegistry.find("counter666").tagKeys("tag666").counters(); + assertThat(expressionTagsCounters.size()).isEqualTo(0); + + Collection testExpTagsCounters = meterRegistry.find("counter666").tagKeys("test").counters(); + assertThat(testExpTagsCounters.size()).isEqualTo(1); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ExpressionCounterNameTests.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ExpressionCounterNameTests.java new file mode 100644 index 00000000..99d5c881 --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/ExpressionCounterNameTests.java @@ -0,0 +1,41 @@ +/* + * Copyright 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.counter; + +import java.util.stream.IntStream; + +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name-expression=payload" +}) +public class ExpressionCounterNameTests extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello"))); + assertThat(meterRegistry.find("hello").counter().count()).isEqualTo(13.0); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/FixedTagsTests.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/FixedTagsTests.java new file mode 100644 index 00000000..a2debb41 --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/FixedTagsTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 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.counter; + +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + +import io.micrometer.core.instrument.Meter; +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name=counter666", + "counter.tag.fixed.foo=bar", + "counter.tag.fixed.gork=bork" +}) +public class FixedTagsTests extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello"))); + Meter counterMeter = meterRegistry.find("counter666").meter(); + assertThat(StreamSupport.stream(counterMeter.measure().spliterator(), false) + .mapToDouble(m -> m.getValue()).sum()).isEqualTo(13.0); + + assertThat(counterMeter.getId().getTags().size()).isEqualTo(2); + assertThat(counterMeter.getId().getTag("foo")).isEqualTo("bar"); + assertThat(counterMeter.getId().getTag("gork")).isEqualTo("bork"); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/LiteralTagExpressionsTests.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/LiteralTagExpressionsTests.java new file mode 100644 index 00000000..63a783e8 --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/LiteralTagExpressionsTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 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.counter; + +import java.util.stream.IntStream; + +import io.micrometer.core.instrument.Counter; +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name=counter666", + "counter.tag.expression.foo='bar'", + "counter.tag.expression.gork='bork'" +}) +public class LiteralTagExpressionsTests extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + + IntStream.range(0, 13).forEach(i -> counterConsumer.accept(new GenericMessage("hello"))); + + Counter fooCounter = meterRegistry.find("counter666").tag("foo", "bar").counter(); + assertThat(fooCounter.count()).isEqualTo(13.0); + + Counter gorkCounter = meterRegistry.find("counter666").tag("gork", "bork").counter(); + assertThat(gorkCounter.count()).isEqualTo(13.0); + + assertThat(fooCounter.getId()).isEqualTo(gorkCounter.getId()); + } +} diff --git a/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/NullTagsTests.java b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/NullTagsTests.java new file mode 100644 index 00000000..79c75708 --- /dev/null +++ b/functions/consumer/counter-consumer/src/test/java/org/springframework/cloud/fn/consumer/counter/NullTagsTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.counter; + +import java.util.Collection; + +import io.micrometer.core.instrument.Counter; +import org.junit.jupiter.api.Test; + +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Tzolov + */ +@TestPropertySource(properties = { + "counter.name=counter666", + "counter.tag.fixed.foo=", + "counter.tag.expression.tag666=#jsonPath(payload,'$..noField')", + "counter.tag.expression.test=#jsonPath(payload,'$..test')", +}) +public class NullTagsTests extends CounterConsumerParentTest { + + @Test + void testCounterSink() { + + counterConsumer.accept(message("{\"test\": null}")); + + Collection fixedTagsCounters = meterRegistry.find("counter666").tagKeys("foo").counters(); + assertThat(fixedTagsCounters.size()).isEqualTo(0); + + Collection expressionTagsCounters = meterRegistry.find("counter666").tagKeys("tag666").counters(); + assertThat(expressionTagsCounters.size()).isEqualTo(0); + + Collection testExpTagsCounters = meterRegistry.find("counter666").tagKeys("test").counters(); + assertThat(testExpTagsCounters.size()).isEqualTo(0); + } +} diff --git a/functions/consumer/file-consumer/pom.xml b/functions/consumer/file-consumer/pom.xml new file mode 100644 index 00000000..9ab3918d --- /dev/null +++ b/functions/consumer/file-consumer/pom.xml @@ -0,0 +1,41 @@ + + + 4.0.0 + file-consumer + 1.0.0.BUILD-SNAPSHOT + file-consumer + file consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-file + + + 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 + + + + diff --git a/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerConfiguration.java b/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerConfiguration.java new file mode 100644 index 00000000..d00f57f5 --- /dev/null +++ b/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerConfiguration.java @@ -0,0 +1,73 @@ +/* + * 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.file; + +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.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.file.DefaultFileNameGenerator; +import org.springframework.integration.file.FileNameGenerator; +import org.springframework.integration.file.FileWritingMessageHandler; +import org.springframework.messaging.Message; + +/** + * @author Mark Fisher + * @author Artem Bilan + * @author Soby Chacko + */ +@Configuration +@EnableConfigurationProperties(FileConsumerProperties.class) +public class FileConsumerConfiguration { + + private static final ExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); + + private final FileConsumerProperties properties; + + public FileConsumerConfiguration(FileConsumerProperties properties) { + this.properties = properties; + } + + @Bean + public Consumer> fileConsumer() { + return fileWritingMessageHandler()::handleMessage; + } + + @Bean + public FileWritingMessageHandler fileWritingMessageHandler() { + FileWritingMessageHandler handler = (properties.getDirectoryExpression() != null) + ? new FileWritingMessageHandler(EXPRESSION_PARSER.parseExpression(properties.getDirectoryExpression())) + : new FileWritingMessageHandler(properties.getDirectory()); + handler.setAutoCreateDirectory(true); + handler.setAppendNewLine(!properties.isBinary()); + handler.setCharset(properties.getCharset()); + handler.setExpectReply(false); + handler.setFileExistsMode(properties.getMode()); + handler.setFileNameGenerator(fileNameGenerator()); + return handler; + } + + @Bean + public FileNameGenerator fileNameGenerator() { + DefaultFileNameGenerator fileNameGenerator = new DefaultFileNameGenerator(); + fileNameGenerator.setExpression(properties.getNameExpression()); + return fileNameGenerator; + } +} diff --git a/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerProperties.java b/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerProperties.java new file mode 100644 index 00000000..eb861a61 --- /dev/null +++ b/functions/consumer/file-consumer/src/main/java/org/springframework/cloud/fn/consumer/file/FileConsumerProperties.java @@ -0,0 +1,162 @@ +/* + * 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.file; + +import java.io.File; + +import javax.validation.constraints.AssertTrue; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.integration.file.support.FileExistsMode; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; + +/** + * Properties for the file sink. + * + * @author Mark Fisher + * @author Gary Russell + */ +@ConfigurationProperties("file.consumer") +@Validated +public class FileConsumerProperties { + + static final String DEFAULT_DIR = System.getProperty("java.io.tmpdir") + "file-consumer"; + + private static final String DEFAULT_NAME = "file-consumer"; + + /** + * A flag to indicate whether adding a newline after the write should be suppressed. + */ + private boolean binary = false; + + /** + * The charset to use when writing text content. + */ + private String charset = "UTF-8"; + + /** + * The parent directory of the target file. + */ + private File directory = new File(DEFAULT_DIR); + + /** + * The expression to evaluate for the parent directory of the target file. + */ + private String directoryExpression; + + /** + * The FileExistsMode to use if the target file already exists. + */ + private FileExistsMode mode = FileExistsMode.APPEND; + + /** + * The name of the target file. + */ + private String name = DEFAULT_NAME; + + /** + * The expression to evaluate for the name of the target file. + */ + private String nameExpression; + + /** + * The suffix to append to file name. + */ + private String suffix = ""; + + public boolean isBinary() { + return binary; + } + + public void setBinary(boolean binary) { + this.binary = binary; + } + + public String getCharset() { + return charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public File getDirectory() { + return directory; + } + + public void setDirectory(File directory) { + this.directory = directory; + } + + public String getDirectoryExpression() { + return directoryExpression; + } + + public void setDirectoryExpression(String directoryExpression) { + this.directoryExpression = directoryExpression; + } + + public FileExistsMode getMode() { + return mode; + } + + public void setMode(FileExistsMode mode) { + this.mode = mode; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getNameExpression() { + return (nameExpression != null) + ? nameExpression + " + '" + getSuffix() + "'" + : "'" + name + getSuffix() + "'"; + } + + public void setNameExpression(String nameExpression) { + this.nameExpression = nameExpression; + } + + public String getSuffix() { + String suffixWithDotIfNecessary = ""; + if (StringUtils.hasText(suffix)) { + suffixWithDotIfNecessary = suffix.startsWith(".") ? suffix : "." + suffix; + } + return suffixWithDotIfNecessary; + } + + public void setSuffix(String suffix) { + this.suffix = suffix; + } + + @AssertTrue(message = "Exactly one of 'name' or 'nameExpression' must be set") + public boolean isMutuallyExclusiveNameAndNameExpression() { + return DEFAULT_NAME.equals(name) || nameExpression == null; + } + + @AssertTrue(message = "Exactly one of 'directory' or 'directoryExpression' must be set") + public boolean isMutuallyExclusiveDirectoryAndDirectoryExpression() { + return new File(DEFAULT_DIR).equals(directory) || directoryExpression == null; + } + +} diff --git a/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/AbstractFileConsumerTests.java b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/AbstractFileConsumerTests.java new file mode 100644 index 00000000..e886e5ec --- /dev/null +++ b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/AbstractFileConsumerTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 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.file; + +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; + +/** + * @author Soby Chacko + */ +@SpringBootTest +@DirtiesContext +public class AbstractFileConsumerTests { + + @TempDir + static Path tempDir; + + @Autowired + Consumer> fileConsumer; + + @BeforeAll + public static void beforeAll() { + System.setProperty("file.consumer.directory", tempDir.toAbsolutePath().toString()); + } + + @AfterAll + public static void afterAll() { + System.clearProperty("file.consumer.directory"); + } + + @SpringBootApplication + static class TestApplication { + } +} diff --git a/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/BinaryFileTests.java b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/BinaryFileTests.java new file mode 100644 index 00000000..15b4ac02 --- /dev/null +++ b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/BinaryFileTests.java @@ -0,0 +1,45 @@ +/* + * 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.file; + +import java.io.File; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = "file.consumer.binary = true") +public class BinaryFileTests extends AbstractFileConsumerTests { + + @Test + public void test() throws Exception { + fileConsumer.accept(MessageBuilder.withPayload("hello file-consumer".getBytes()).build()); + File file = new File(tempDir.toFile(), "file-consumer"); + assertThat(file.exists()).isTrue(); + byte[] results = FileCopyUtils.copyToByteArray(file); + assertThat("hello file-consumer".getBytes()).isEqualTo(results); + } + +} diff --git a/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/ExpressionTests.java b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/ExpressionTests.java new file mode 100644 index 00000000..5eeb4cfd --- /dev/null +++ b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/ExpressionTests.java @@ -0,0 +1,64 @@ +/* + * 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.file; + +import java.io.File; +import java.io.FileReader; +import java.nio.file.Path; +import java.util.function.Consumer; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Artem Bilan + * @author Soby Chacko + * + * We don't need a separate SpringBootApplication for this test as there is already one available in this package. + * {@link AbstractFileConsumerTests}. + */ +@SpringBootTest(properties = {"file.consumer.nameExpression = payload.substring(0, 4)", + "file.consumer.directoryExpression = '${java.io.tmpdir}'+'/'+headers.dir", + "file.consumer.suffix=out"}) +@DirtiesContext +public class ExpressionTests { + + @TempDir + static Path tempDir; + + @Autowired + Consumer> fileConsumer; + + @Test + public void test() throws Exception { + fileConsumer.accept(MessageBuilder.withPayload("this is something").setHeader("dir", "expression").build()); + File file = new File(System.getProperty("java.io.tmpdir") + File.separator + "expression", "this.out"); + file.deleteOnExit(); + assertThat(file.exists()).isTrue(); + assertThat("this is something" + System.lineSeparator()) + .isEqualTo(FileCopyUtils.copyToString(new FileReader(file))); + } +} diff --git a/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/TextFileTests.java b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/TextFileTests.java new file mode 100644 index 00000000..38aa77a0 --- /dev/null +++ b/functions/consumer/file-consumer/src/test/java/org/springframework/cloud/fn/consumer/file/TextFileTests.java @@ -0,0 +1,46 @@ +/* + * 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.file; + +import java.io.File; +import java.io.FileReader; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; +import org.springframework.util.FileCopyUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Mark Fisher + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = {"file.consumer.name = test", "file.consumer.suffix=txt"}) +public class TextFileTests extends AbstractFileConsumerTests { + + @Test + public void test() throws Exception { + fileConsumer.accept(MessageBuilder.withPayload("hello file-consumer").build()); + File file = new File(tempDir.toFile(), "test.txt"); + assertThat(file.exists()).isTrue(); + assertThat("hello file-consumer" + System.lineSeparator()) + .isEqualTo(FileCopyUtils.copyToString(new FileReader(file))); + } + +} diff --git a/functions/consumer/jdbc-consumer/pom.xml b/functions/consumer/jdbc-consumer/pom.xml new file mode 100644 index 00000000..fbd5fd1c --- /dev/null +++ b/functions/consumer/jdbc-consumer/pom.xml @@ -0,0 +1,63 @@ + + + 4.0.0 + jdbc-consumer + 1.0.0.BUILD-SNAPSHOT + jdbc-consumer + jdbc consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-jdbc + + + org.springframework.boot + spring-boot-starter-json + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.h2database + h2 + test + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.awaitility + awaitility + test + + + junit + junit + + + + + + diff --git a/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/DefaultInitializationScriptResource.java b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/DefaultInitializationScriptResource.java new file mode 100644 index 00000000..90d671c4 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/DefaultInitializationScriptResource.java @@ -0,0 +1,60 @@ +/* + * Copyright 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.jdbc; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.io.ByteArrayResource; + +/** + * An in-memory script crafted for dropping-creating the table we're working with. + * All columns are created as VARCHAR(2000). + * + * @author Eric Bottard + * @author Thomas Risberg + */ +public class DefaultInitializationScriptResource extends ByteArrayResource { + + private static final Log logger = LogFactory.getLog(DefaultInitializationScriptResource.class); + + public DefaultInitializationScriptResource(String tableName, Collection columns) { + super(scriptFor(tableName, columns).getBytes(StandardCharsets.UTF_8)); + } + + private static String scriptFor(String tableName, Collection columns) { + StringBuilder result = new StringBuilder("DROP TABLE "); + result.append(tableName).append(";\n\n"); + + result.append("CREATE TABLE ").append(tableName).append('('); + int i = 0; + for (String column : columns) { + if (i++ > 0) { + result.append(", "); + } + result.append(column).append(" VARCHAR(2000)"); + } + result.append(");\n"); + logger.debug(String.format("Generated the following initializing script for table %s:\n%s", tableName, + result.toString())); + return result.toString(); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerConfiguration.java b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerConfiguration.java new file mode 100644 index 00000000..ebe29278 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerConfiguration.java @@ -0,0 +1,314 @@ +/* + * Copyright 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.jdbc; + +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import javax.sql.DataSource; + +import com.fasterxml.jackson.databind.JsonNode; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.ResourceLoader; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.EvaluationException; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.SpelParseException; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.integration.aggregator.DefaultAggregatingMessageGroupProcessor; +import org.springframework.integration.aggregator.MessageCountReleaseStrategy; +import org.springframework.integration.config.AggregatorFactoryBean; +import org.springframework.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlowBuilder; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.expression.ExpressionUtils; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.jdbc.JdbcMessageHandler; +import org.springframework.integration.jdbc.SqlParameterSourceFactory; +import org.springframework.integration.json.JsonPropertyAccessor; +import org.springframework.integration.store.MessageGroupStore; +import org.springframework.integration.store.SimpleMessageStore; +import org.springframework.integration.support.MutableMessage; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.datasource.init.DataSourceInitializer; +import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MimeTypeUtils; +import org.springframework.util.MultiValueMap; + +/** + * + * @author Eric Bottard + * @author Thomas Risberg + * @author Robert St. John + * @author Oliver Flasch + * @author Artem Bilan + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@Configuration +@EnableConfigurationProperties(JdbcConsumerProperties.class) +public class JdbcConsumerConfiguration { + + private static final Log logger = LogFactory.getLog(JdbcConsumerConfiguration.class); + + private static final Object NOT_SET = new Object(); + + private final JdbcConsumerProperties properties; + + private SpelExpressionParser spelExpressionParser = new SpelExpressionParser(); + + private EvaluationContext evaluationContext; + + public JdbcConsumerConfiguration(JdbcConsumerProperties properties, BeanFactory beanFactory) { + this.properties = properties; + this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); + StandardEvaluationContext standardEvaluationContext = (StandardEvaluationContext) this.evaluationContext; + standardEvaluationContext.addPropertyAccessor(new JsonPropertyAccessor()); + } + + @Bean + IntegrationFlow jdbcConsumerFlow(@Qualifier("aggregator") MessageHandler aggregator, + JdbcMessageHandler jdbcMessageHandler) { + + final IntegrationFlowBuilder builder = + IntegrationFlows.from(Consumer.class, gateway -> gateway.beanName("jdbcConsumer")); + if (properties.getBatchSize() > 1 || properties.getIdleTimeout() > 0) { + builder.handle(aggregator); + } + return builder.handle(jdbcMessageHandler).get(); + } + + @Bean + FactoryBean aggregator(MessageGroupStore messageGroupStore) { + AggregatorFactoryBean aggregatorFactoryBean = new AggregatorFactoryBean(); + aggregatorFactoryBean.setCorrelationStrategy(message -> message.getPayload().getClass().getName()); + aggregatorFactoryBean.setReleaseStrategy(new MessageCountReleaseStrategy(this.properties.getBatchSize())); + if (this.properties.getIdleTimeout() >= 0) { + aggregatorFactoryBean.setGroupTimeoutExpression(new ValueExpression<>(this.properties.getIdleTimeout())); + } + aggregatorFactoryBean.setMessageStore(messageGroupStore); + aggregatorFactoryBean.setProcessorBean(new DefaultAggregatingMessageGroupProcessor()); + aggregatorFactoryBean.setExpireGroupsUponCompletion(true); + aggregatorFactoryBean.setSendPartialResultOnExpiry(true); + return aggregatorFactoryBean; + } + + @Bean + MessageGroupStore messageGroupStore() { + SimpleMessageStore messageGroupStore = new SimpleMessageStore(); + messageGroupStore.setTimeoutOnIdle(true); + messageGroupStore.setCopyOnGet(false); + return messageGroupStore; + } + + @Bean + public JdbcMessageHandler jdbcMessageHandler(DataSource dataSource) { + final MultiValueMap columnExpressionVariations = new LinkedMultiValueMap<>(); + for (Map.Entry entry : this.properties.getColumnsMap().entrySet()) { + String value = entry.getValue(); + columnExpressionVariations.add(entry.getKey(), this.spelExpressionParser.parseExpression(value)); + if (!value.startsWith("payload")) { + String qualified = "payload." + value; + try { + columnExpressionVariations.add(entry.getKey(), + this.spelExpressionParser.parseExpression(qualified)); + } + catch (SpelParseException e) { + logger.info("failed to parse qualified fallback expression " + qualified + + "; be sure your expression uses the 'payload.' prefix where necessary"); + } + } + } + JdbcMessageHandler jdbcMessageHandler = new JdbcMessageHandler(dataSource, + generateSql(this.properties.getTableName(), columnExpressionVariations.keySet())) { + + @Override + protected void handleMessageInternal(final Message message) { + Message convertedMessage = message; + if (message.getPayload() instanceof byte[] || message.getPayload() instanceof Iterable) { + + final String contentType = message.getHeaders().containsKey(MessageHeaders.CONTENT_TYPE) + ? message.getHeaders().get(MessageHeaders.CONTENT_TYPE).toString() + : MimeTypeUtils.APPLICATION_JSON_VALUE; + if (message.getPayload() instanceof Iterable) { + Stream messageStream = + StreamSupport.stream(((Iterable) message.getPayload()).spliterator(), false) + .map(payload -> { + if (payload instanceof byte[]) { + return convertibleContentType(contentType) ? + new String(((byte[]) payload)) : payload; + } + else { + return payload; + } + }); + convertedMessage = new MutableMessage<>(messageStream.collect(Collectors.toList()), + message.getHeaders()); + } + else { + if (convertibleContentType(contentType)) { + convertedMessage = new MutableMessage<>(new String(((byte[]) message.getPayload())), + message.getHeaders()); + } + } + } + super.handleMessageInternal(convertedMessage); + } + }; + SqlParameterSourceFactory parameterSourceFactory = + new ParameterFactory(columnExpressionVariations, this.evaluationContext); + jdbcMessageHandler.setSqlParameterSourceFactory(parameterSourceFactory); + return jdbcMessageHandler; + } + + @ConditionalOnProperty("jdbc.consumer.initialize") + @Bean + public DataSourceInitializer nonBootDataSourceInitializer(DataSource dataSource, ResourceLoader resourceLoader) { + DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); + dataSourceInitializer.setDataSource(dataSource); + ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator(); + databasePopulator.setIgnoreFailedDrops(true); + dataSourceInitializer.setDatabasePopulator(databasePopulator); + if ("true".equals(properties.getInitialize())) { + databasePopulator.addScript( + new DefaultInitializationScriptResource(this.properties.getTableName(), + this.properties.getColumnsMap().keySet())); + } + else { + databasePopulator.addScript(resourceLoader.getResource(this.properties.getInitialize())); + } + return dataSourceInitializer; + } + + @Bean + public static ShorthandMapConverter shorthandMapConverter() { + return new ShorthandMapConverter(); + } + + private static boolean convertibleContentType(String contentType) { + return contentType.contains("text") || contentType.contains("json") || contentType.contains("x-spring-tuple"); + } + + private static String generateSql(String tableName, Set columns) { + StringBuilder builder = new StringBuilder("INSERT INTO "); + StringBuilder questionMarks = new StringBuilder(") VALUES ("); + builder.append(tableName).append("("); + int i = 0; + + for (String column : columns) { + if (i++ > 0) { + builder.append(", "); + questionMarks.append(", "); + } + builder.append(column); + questionMarks.append(':').append(column); + } + builder.append(questionMarks).append(")"); + return builder.toString(); + } + + private static final class ParameterFactory implements SqlParameterSourceFactory { + + private final MultiValueMap columnExpressions; + + private final EvaluationContext context; + + ParameterFactory(MultiValueMap columnExpressions, EvaluationContext context) { + this.columnExpressions = columnExpressions; + this.context = context; + } + + @Override + public SqlParameterSource createParameterSource(Object o) { + if (!(o instanceof Message)) { + throw new IllegalArgumentException("Unable to handle type " + o.getClass().getName()); + } + Message message = (Message) o; + MapSqlParameterSource parameterSource = new MapSqlParameterSource(); + for (Map.Entry> entry : this.columnExpressions.entrySet()) { + String key = entry.getKey(); + List spels = entry.getValue(); + Object value = NOT_SET; + EvaluationException lastException = null; + for (Expression spel : spels) { + try { + value = spel.getValue(context, message); + break; + } + catch (EvaluationException e) { + lastException = e; + } + } + if (value == NOT_SET) { + if (lastException != null) { + logger.info("Could not find value for column '" + key + "': " + lastException.getMessage()); + } + parameterSource.addValue(key, null); + } + else { + if (value instanceof JsonPropertyAccessor.ToStringFriendlyJsonNode) { + // Need to do some reflection until we have a getter for the Node + DirectFieldAccessor dfa = new DirectFieldAccessor(value); + JsonNode node = (JsonNode) dfa.getPropertyValue("node"); + Object valueToUse; + if (node == null || node.isNull()) { + valueToUse = null; + } + else if (node.isNumber()) { + valueToUse = node.numberValue(); + } + else if (node.isBoolean()) { + valueToUse = node.booleanValue(); + } + else { + valueToUse = node.textValue(); + } + parameterSource.addValue(key, valueToUse); + } + else { + parameterSource.addValue(key, value); + } + } + } + return parameterSource; + } + + } + +} diff --git a/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerProperties.java b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerProperties.java new file mode 100644 index 00000000..3afae4a1 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerProperties.java @@ -0,0 +1,109 @@ +/* + * Copyright 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.jdbc; + +import java.util.Map; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * @author Eric Bottard + * @author Artem Bilan + * @author Oliver Flasch + */ +@ConfigurationProperties("jdbc.consumer") +public class JdbcConsumerProperties { + + @Autowired + private ShorthandMapConverter shorthandMapConverter; + + /** + * The name of the table to write into. + */ + private String tableName = "messages"; + + /** + * The comma separated colon-based pairs of column names and SpEL expressions for values to insert/update. + * Names are used at initialization time to issue the DDL. + */ + private String columns = "payload:payload.toString()"; + + /** + * 'true', 'false' or the location of a custom initialization script for the table. + */ + private String initialize = "false"; + + /** + * Threshold in number of messages when data will be flushed to database table. + */ + private int batchSize = 1; + + /** + * Idle timeout in milliseconds when data is automatically flushed to database table. + */ + private long idleTimeout = -1L; + + private Map columnsMap; + + public String getTableName() { + return this.tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getColumns() { + return this.columns; + } + + public void setColumns(String columns) { + this.columns = columns; + } + + public String getInitialize() { + return this.initialize; + } + + public void setInitialize(String initialize) { + this.initialize = initialize; + } + + public int getBatchSize() { + return this.batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + public long getIdleTimeout() { + return this.idleTimeout; + } + + public void setIdleTimeout(long idleTimeout) { + this.idleTimeout = idleTimeout; + } + + Map getColumnsMap() { + if (this.columnsMap == null) { + this.columnsMap = this.shorthandMapConverter.convert(this.columns); + } + return this.columnsMap; + } +} diff --git a/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/ShorthandMapConverter.java b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/ShorthandMapConverter.java new file mode 100644 index 00000000..ec925b85 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/main/java/org/springframework/cloud/fn/consumer/jdbc/ShorthandMapConverter.java @@ -0,0 +1,61 @@ +/* + * Copyright 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.jdbc; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.util.Assert; + +/** + * A Converter from String to Map that accepts csv {@literal key:value} pairs + * (similar to what comes out of the box in Spring Core) but also simple + * {@literal key} items, in which case the value is assumed to be equal to the key. + *

+ *

Additionally, commas and colons can be escaped by using a backslash, which is + * useful if said mappings are to be used for SpEL for example.

+ * + * @author Eric Bottard + * @author Artem Bilan + */ +public class ShorthandMapConverter implements Converter> { + + @Override + public Map convert(String source) { + Map result = new LinkedHashMap<>(); + + // Split on comma if not preceded by backslash + String[] mappings = source.split("(? message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + } + Awaitility.await().until(() -> jdbcOperations + .queryForObject("select count(*) from messages", Integer.class), value -> value == numberOfInserts); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/DataReceivedAsByteArrayTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/DataReceivedAsByteArrayTests.java new file mode 100644 index 00000000..f7857f93 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/DataReceivedAsByteArrayTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a") +public class DataReceivedAsByteArrayTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertionWhenDataReceivedAsByteArray() { + String hello = "{\"a\": \"hello\"}"; + final Message message = MessageBuilder.withPayload(hello.getBytes()).build(); + jdbcConsumer.accept(message); + final Integer count = + jdbcOperations.queryForObject("select count(*) from messages where a = ?", Integer.class, "hello"); + assertThat(count).isEqualTo(1); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ExplicitTableCreationTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ExplicitTableCreationTests.java new file mode 100644 index 00000000..375db87a --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ExplicitTableCreationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.jdbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = { "jdbc.consumer.tableName=foobar", + "jdbc.consumer.initialize=classpath:explicit-script.sql", + "jdbc.consumer.columns=a,b" }) +public class ExplicitTableCreationTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + Payload result = + jdbcOperations.query("select a, b from foobar", new BeanPropertyRowMapper<>(Payload.class)) + .get(0); + assertThat(result).isEqualToComparingFieldByField(sent); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/HeaderInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/HeaderInsertTests.java new file mode 100644 index 00000000..c5cc13f8 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/HeaderInsertTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 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.jdbc; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a: headers[foo]") +public class HeaderInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testHeaderInsertion() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent) + .setHeader("foo", "bar").build(); + jdbcConsumer.accept(message); + assertThat(jdbcOperations.queryForObject("select count(*) from messages where a = ?", + Integer.class, "bar")).isEqualTo(1); + } +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ImplicitTableCreationTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ImplicitTableCreationTests.java new file mode 100644 index 00000000..3915f29f --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/ImplicitTableCreationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 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.jdbc; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = { + "jdbc.consumer.tableName=no_script", + "jdbc.consumer.initialize=true", + "jdbc.consumer.columns=a,b" }) +public class ImplicitTableCreationTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + Payload result = jdbcOperations + .query("select a, b from no_script", new BeanPropertyRowMapper<>(Payload.class)).get(0); + assertThat(result).isEqualToComparingFieldByField(sent); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerApplicationTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerApplicationTests.java new file mode 100644 index 00000000..e4bff00a --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JdbcConsumerApplicationTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 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.jdbc; + +import java.util.function.Consumer; + +import org.junit.jupiter.api.AfterEach; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; + +/** + * @author Soby Chacko + */ +@SpringBootTest +@DirtiesContext +public class JdbcConsumerApplicationTests { + + @Autowired + Consumer> jdbcConsumer; + + @Autowired + JdbcOperations jdbcOperations; + + @Autowired + JdbcTemplate jdbcTemplate; + + @AfterEach + public void cleanup() { + jdbcOperations.execute("DROP TABLE MESSAGES IF EXISTS"); + } + + static class Payload { + + private String a; + + private Integer b; + + public Payload() { + } + + public Payload(String a, Integer b) { + this.a = a; + this.b = b; + } + + public String getA() { + return a; + } + + public Integer getB() { + return b; + } + + public void setA(String a) { + this.a = a; + } + + public void setB(Integer b) { + this.b = b; + } + + @Override + public String toString() { + return a + b; + } + + } + + @SpringBootApplication + static class TestApplication {} + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JsonStringPayloadInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JsonStringPayloadInsertTests.java new file mode 100644 index 00000000..0ffcc35e --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/JsonStringPayloadInsertTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a,b") +public class JsonStringPayloadInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + String stringA = "{\"a\": \"hello1\", \"b\": 42}"; + String stringB = "{\"a\": \"hello2\", \"b\": null}"; + String stringC = "{\"a\": \"hello3\"}"; + final Message message1 = MessageBuilder.withPayload(stringA).build(); + jdbcConsumer.accept(message1); + final Message message2 = MessageBuilder.withPayload(stringB).build(); + jdbcConsumer.accept(message2); + final Message message3 = MessageBuilder.withPayload(stringC).build(); + jdbcConsumer.accept(message3); + assertThat(jdbcOperations.queryForObject( + "select count(*) from messages where a = ? and b = ?", + Integer.class, "hello1", 42)).isEqualTo(1); + assertThat(jdbcOperations.queryForObject( + "select count(*) from messages where a = ? and b IS NULL", + Integer.class, "hello2")).isEqualTo(1); + assertThat(jdbcOperations.queryForObject( + "select count(*) from messages where a = ? and b IS NULL", + Integer.class, "hello3")).isEqualTo(1); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/MapPayloadInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/MapPayloadInsertTests.java new file mode 100644 index 00000000..e5b38384 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/MapPayloadInsertTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a,b") +public class MapPayloadInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + NamedParameterJdbcOperations namedParameterJdbcOperations = new NamedParameterJdbcTemplate(jdbcOperations); + Map mapA = new HashMap<>(); + mapA.put("a", "hello1"); + mapA.put("b", 42); + Map mapB = new HashMap<>(); + mapB.put("a", "hello2"); + mapB.put("b", null); + Map mapC = new HashMap<>(); + mapC.put("a", "hello3"); + final Message> message1 = MessageBuilder.withPayload(mapA).build(); + jdbcConsumer.accept(message1); + final Message> message2 = MessageBuilder.withPayload(mapB).build(); + jdbcConsumer.accept(message2); + final Message> message3 = MessageBuilder.withPayload(mapC).build(); + jdbcConsumer.accept(message3); + assertThat(namedParameterJdbcOperations.queryForObject( + "select count(*) from messages where a = :a and b = :b", mapA, Integer.class)).isEqualTo(1); + assertThat(namedParameterJdbcOperations.queryForObject( + "select count(*) from messages where a = :a and b IS NULL", mapB, Integer.class)).isEqualTo(1); + assertThat(namedParameterJdbcOperations.queryForObject( + "select count(*) from messages where a = :a and b IS NULL", mapC, Integer.class)).isEqualTo(1); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleBatchInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleBatchInsertTests.java new file mode 100644 index 00000000..ecf8e287 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleBatchInsertTests.java @@ -0,0 +1,51 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.batchSize=1000") +public class SimpleBatchInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testBatchInsertion() { + final int numberOfInserts = 5000; + Payload sent = new Payload("hello", 42); + for (int i = 0; i < numberOfInserts; i++) { + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + } + int result = jdbcOperations.queryForObject("select count(*) from messages", Integer.class); + assertThat(result).isEqualTo(numberOfInserts); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleInsertTests.java new file mode 100644 index 00000000..f0826573 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleInsertTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +public class SimpleInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testSimpleInsert() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + String result = jdbcOperations.queryForObject("select payload from messages", String.class); + assertThat(result).isEqualTo(("hello42")); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleMappingTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleMappingTests.java new file mode 100644 index 00000000..428a266b --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SimpleMappingTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a,b") +public class SimpleMappingTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + Payload result = jdbcOperations + .query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class)).get(0); + assertThat(result).isEqualToComparingFieldByField(sent); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SpELTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SpELTests.java new file mode 100644 index 00000000..9eb8fd9b --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/SpELTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +// annotation below relies on java.util.Properties so backslash needs to be doubled +@TestPropertySource(properties = "jdbc.consumer.columns=a: a.substring(0\\\\, 4), b: b + 624") +public class SpELTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + Payload sent = new Payload("hello", 42); + final Message message = MessageBuilder.withPayload(sent).build(); + jdbcConsumer.accept(message); + Payload expected = new Payload("hell", 666); + Payload result = jdbcOperations + .query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class)).get(0); + assertThat(result).isEqualToComparingFieldByField(expected); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/UnqualifiableColumnExpressionTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/UnqualifiableColumnExpressionTests.java new file mode 100644 index 00000000..9fb69a97 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/UnqualifiableColumnExpressionTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a: new StringBuilder(payload.a).reverse().toString(), b") +public class UnqualifiableColumnExpressionTests extends JdbcConsumerApplicationTests { + + @Test + public void doesNotFailParsingUnqualifiableExpression() { + // if the app initializes, the test condition passes, but go ahead and apply the column expression anyway + jdbcConsumer.accept(MessageBuilder.withPayload(new Payload("desrever", 123)).build()); + assertThat(jdbcOperations.queryForObject("select count(*) from messages where a = ? and b = ?", + Integer.class, "reversed", 123)).isEqualTo(1); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/VaryingInsertTests.java b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/VaryingInsertTests.java new file mode 100644 index 00000000..bb48f807 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/java/org/springframework/cloud/fn/consumer/jdbc/VaryingInsertTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 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.jdbc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.integration.support.MessageBuilder; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; + +/** + * @author Eric Bottard + * @author Thomas Risberg + * @author Artem Bilan + * @author Robert St. John + * @author Oliver Flasch + * @author Soby Chacko + * @author Szabolcs Stremler + */ +@TestPropertySource(properties = "jdbc.consumer.columns=a,b") +public class VaryingInsertTests extends JdbcConsumerApplicationTests { + + @Test + public void testInsertion() { + Payload a = new Payload("hello", 42); + Payload b = new Payload("world", 12); + Payload c = new Payload("bonjour", null); + Payload d = new Payload(null, 22); + final Message message1 = MessageBuilder.withPayload(a).build(); + jdbcConsumer.accept(message1); + final Message message2 = MessageBuilder.withPayload(b).build(); + jdbcConsumer.accept(message2); + final Message message3 = MessageBuilder.withPayload(c).build(); + jdbcConsumer.accept(message3); + final Message message4 = MessageBuilder.withPayload(d).build(); + jdbcConsumer.accept(message4); + List result = jdbcOperations + .query("select a, b from messages", new BeanPropertyRowMapper<>(Payload.class)); + Assertions.assertThat(result).extracting("a").containsExactly("hello", "world", "bonjour", null); + Assertions.assertThat(result).extracting("b").contains(42, 12, 22, null); + } + +} diff --git a/functions/consumer/jdbc-consumer/src/test/resources/explicit-script.sql b/functions/consumer/jdbc-consumer/src/test/resources/explicit-script.sql new file mode 100644 index 00000000..ac07d2d0 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/resources/explicit-script.sql @@ -0,0 +1,6 @@ +-- Used in test for explicit script + +create table foobar( + a varchar(2000), + b VARCHAR (2000) +); diff --git a/functions/consumer/jdbc-consumer/src/test/resources/schema.sql b/functions/consumer/jdbc-consumer/src/test/resources/schema.sql new file mode 100644 index 00000000..65e85283 --- /dev/null +++ b/functions/consumer/jdbc-consumer/src/test/resources/schema.sql @@ -0,0 +1,7 @@ +-- Run by default by Boot infrastructure + +create table messages( + a varchar(2000), + b VARCHAR (2000), + payload VARCHAR (2000) +); diff --git a/functions/consumer/log-consumer/.gitignore b/functions/consumer/log-consumer/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/functions/consumer/log-consumer/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/functions/consumer/log-consumer/pom.xml b/functions/consumer/log-consumer/pom.xml new file mode 100644 index 00000000..384dd2c7 --- /dev/null +++ b/functions/consumer/log-consumer/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + log-consumer + 1.0.0.BUILD-SNAPSHOT + log-consumer + Log Consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + io.pivotal.java.function + payload-converter-function + ${project.version} + + + + org.springframework.boot + spring-boot-starter-integration + + + + org.hibernate.validator + hibernate-validator + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.integration + spring-integration-test + test + + + + diff --git a/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerConfiguration.java b/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerConfiguration.java new file mode 100644 index 00000000..3b17411e --- /dev/null +++ b/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerConfiguration.java @@ -0,0 +1,52 @@ +/* + * Copyright 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 + * + * http://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.log; + +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.integration.dsl.IntegrationFlow; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.messaging.Message; + +/** + * The Configuration class for {@link Consumer} which logs incoming data. + * For the logging logic a Spring Integration {@link org.springframework.integration.handler.LoggingHandler} + * is used. + * If incoming payload is a {@code byte[]} and incoming message {@code contentType} header is text-compatible + * (e.g. {@code application/json}), it is converted into a {@link String}. + * Otherwise the payload is passed to logger as is. + * + * @author Artem Bilan + */ +@Configuration +@EnableConfigurationProperties(LogConsumerProperties.class) +public class LogConsumerConfiguration { + + @Bean + IntegrationFlow logConsumerFlow(LogConsumerProperties logSinkProperties) { + return IntegrationFlows.from(MessageConsumer.class, (gateway) -> gateway.beanName("logConsumer")) + .handle((payload, headers) -> payload) + .log(logSinkProperties.getLevel(), logSinkProperties.getName(), logSinkProperties.getExpression()) + .get(); + } + + private interface MessageConsumer extends Consumer> {} + +} diff --git a/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerProperties.java b/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerProperties.java new file mode 100644 index 00000000..ff9b9528 --- /dev/null +++ b/functions/consumer/log-consumer/src/main/java/org/springframework/cloud/fn/consumer/log/LogConsumerProperties.java @@ -0,0 +1,84 @@ +/* + * Copyright 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 + * + * http://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.log; + +import static org.springframework.integration.handler.LoggingHandler.Level.INFO; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.integration.handler.LoggingHandler; +import org.springframework.validation.annotation.Validated; + +/** + * Configuration properties for the Log Sink app. + * + * @author Gary Russell + * @author Eric Bottard + * @author Chris Schaefer + * @author Artem Bilan + */ +@ConfigurationProperties("log") +@Validated +public class LogConsumerProperties { + + /** + * The name of the logger to use. + */ + @Value("${spring.application.name:log.consumer}") + private String name; + + /** + * A SpEL expression (against the incoming message) to evaluate as the logged message. + */ + private String expression = "payload"; + + /** + * The level at which to log messages. + */ + private LoggingHandler.Level level = INFO; + + @NotBlank + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @NotBlank + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + @NotNull + public LoggingHandler.Level getLevel() { + return level; + } + + public void setLevel(LoggingHandler.Level level) { + this.level = level; + } + +} diff --git a/functions/consumer/log-consumer/src/test/java/org/springframework/cloud/fn/consumer/log/LogConsumerApplicationTests.java b/functions/consumer/log-consumer/src/test/java/org/springframework/cloud/fn/consumer/log/LogConsumerApplicationTests.java new file mode 100644 index 00000000..cf2b837f --- /dev/null +++ b/functions/consumer/log-consumer/src/test/java/org/springframework/cloud/fn/consumer/log/LogConsumerApplicationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 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 + * + * http://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.log; + +import java.util.function.Consumer; + +import org.apache.commons.logging.Log; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.integration.handler.LoggingHandler; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.test.util.TestUtils; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.util.MimeType; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * @author Artem Bilan + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@SpringBootTest({ "log.name=foo", "log.level=warn", "log.expression=payload.toUpperCase()" }) +class LogConsumerApplicationTests { + + @Autowired + private Consumer> logConsumer; + + @Autowired + @Qualifier("logConsumerFlow.logging-channel-adapter#0") + private LoggingHandler loggingHandler; + + @Test + public void testJsonContentType() { + Message message = MessageBuilder.withPayload("{\"foo\":\"bar\"}") + .setHeader("contentType", new MimeType("json")) + .build(); + testMessage(message, "{\"foo\":\"bar\"}"); + } + + private void testMessage(Message message, String expectedPayload) { + assertThat(this.loggingHandler.getLevel()).isEqualTo(LoggingHandler.Level.WARN); + Log logger = TestUtils.getPropertyValue(this.loggingHandler, "messageLogger", Log.class); + assertThat(TestUtils.getPropertyValue(logger, "logger.name")).isEqualTo("foo"); + logger = spy(logger); + new DirectFieldAccessor(this.loggingHandler).setPropertyValue("messageLogger", logger); + this.logConsumer.accept(message); + ArgumentCaptor captor = ArgumentCaptor.forClass(Object.class); + verify(logger).warn(captor.capture()); + assertThat(captor.getValue()).isEqualTo(expectedPayload.toUpperCase()); + this.loggingHandler.setLogExpressionString("#this"); + this.logConsumer.accept(message); + verify(logger, times(2)).warn(captor.capture()); + + Message captorMessage = (Message) captor.getAllValues().get(2); + assertThat(captorMessage.getPayload()).isEqualTo(expectedPayload); + + MessageHeaders messageHeaders = captorMessage.getHeaders(); + assertThat(messageHeaders).hasSize(3); + + assertThat(messageHeaders) + .containsEntry(MessageHeaders.CONTENT_TYPE, message.getHeaders().get(MessageHeaders.CONTENT_TYPE)); + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/consumer/mongodb-consumer/.gitignore b/functions/consumer/mongodb-consumer/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/functions/consumer/mongodb-consumer/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/functions/consumer/mongodb-consumer/pom.xml b/functions/consumer/mongodb-consumer/pom.xml new file mode 100644 index 00000000..c4cca0ed --- /dev/null +++ b/functions/consumer/mongodb-consumer/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + mongodb-consumer + 1.0.0.BUILD-SNAPSHOT + mongodb-consumer + Mongo DB consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-mongodb + + + org.mongodb + mongodb-driver-reactivestreams + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + diff --git a/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerConfiguration.java b/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerConfiguration.java new file mode 100644 index 00000000..e2315858 --- /dev/null +++ b/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerConfiguration.java @@ -0,0 +1,70 @@ +/* + * 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.consumer.mongo; + +import java.util.function.Function; + +import reactor.core.publisher.Mono; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.mongodb.outbound.ReactiveMongoDbStoringMessageHandler; +import org.springframework.messaging.Message; +import org.springframework.messaging.ReactiveMessageHandler; + +/** + * A configuration for MongoDB Consumer function. Uses a + * {@link ReactiveMongoDbStoringMessageHandler} to save payload contents to Mongo DB. + * + * @author Artem Bilan + * @author David Turanski + * + */ +@Configuration +@EnableConfigurationProperties({ MongoDbConsumerProperties.class }) +public class MongoDbConsumerConfiguration { + + private final MongoDbConsumerProperties properties; + + private final ReactiveMongoTemplate mongoTemplate; + + public MongoDbConsumerConfiguration(MongoDbConsumerProperties properties, ReactiveMongoTemplate mongoTemplate) { + this.properties = properties; + this.mongoTemplate = mongoTemplate; + } + + @Bean + public Function, Mono> mongodbConsumer(ReactiveMessageHandler mongoConsumerMessageHandler) { + return mongoConsumerMessageHandler::handleMessage; + } + + @Bean + public ReactiveMessageHandler mongoConsumerMessageHandler() { + ReactiveMongoDbStoringMessageHandler mongoDbMessageHandler = new ReactiveMongoDbStoringMessageHandler( + this.mongoTemplate); + Expression collectionExpression = this.properties.getCollectionExpression(); + if (collectionExpression == null) { + collectionExpression = new LiteralExpression(this.properties.getCollection()); + } + mongoDbMessageHandler.setCollectionNameExpression(collectionExpression); + return mongoDbMessageHandler; + } +} diff --git a/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerProperties.java b/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerProperties.java new file mode 100644 index 00000000..838cbf4c --- /dev/null +++ b/functions/consumer/mongodb-consumer/src/main/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerProperties.java @@ -0,0 +1,66 @@ +/* + * Copyright 2019-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.mongo; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.util.StringUtils; +import org.springframework.validation.annotation.Validated; +/** + * @author Artem Bilan + * @author David Turanski + * + */ +@ConfigurationProperties("mongodb.consumer") +@Validated +public class MongoDbConsumerProperties { + + /** + * The MongoDB collection to store data + */ + private String collection; + + /** + * The SpEL expression to evaluate MongoDB collection + */ + private Expression collectionExpression; + + public void setCollection(String collection) { + this.collection = collection; + } + + public String getCollection() { + return this.collection; + } + + public void setCollectionExpression(Expression collectionExpression) { + this.collectionExpression = collectionExpression; + } + + public Expression getCollectionExpression() { + return collectionExpression; + } + + @AssertTrue(message = "One of 'collection' or 'collectionExpression' is required") + private boolean isValid() { + return StringUtils.hasText(this.collection) || this.collectionExpression != null; + } +} diff --git a/functions/consumer/mongodb-consumer/src/test/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerApplicationTests.java b/functions/consumer/mongodb-consumer/src/test/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerApplicationTests.java new file mode 100644 index 00000000..d3c1a3fa --- /dev/null +++ b/functions/consumer/mongodb-consumer/src/test/java/org/springframework/cloud/fn/consumer/mongo/MongoDbConsumerApplicationTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019-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.mongo; + +import java.time.Duration; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; + +import java.util.function.Function; +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author David Turanski + */ +@SpringBootTest(properties = { + "spring.data.mongodb.port=0", + "mongodb.consumer.collection=testing"}) +class MongoDbConsumerApplicationTests { + + @Autowired + private MongoDbConsumerProperties properties; + + @Autowired + private Function, Mono> mongoDbConsumer; + + @Autowired + private ReactiveMongoTemplate mongoTemplate; + + @Test + void testMongodbConsumer() { + Map data1 = new HashMap<>(); + data1.put("foo", "bar"); + + Map data2 = new HashMap<>(); + data2.put("firstName", "Foo"); + data2.put("lastName", "Bar"); + + Flux> messages = Flux.just( + new GenericMessage<>(data1), + new GenericMessage<>(data2), + new GenericMessage<>("{\"my_data\": \"THE DATA\"}") + ); + + messages.flatMap(mongoDbConsumer::apply).blockLast(Duration.ofSeconds(10)); + + StepVerifier.create(this.mongoTemplate.findAll(Document.class, properties.getCollection()) + .sort(Comparator.comparing(d -> d.get("_id").toString()))) + .assertNext(document -> { + assertThat(document.get("foo")).isEqualTo("bar"); + }) + .assertNext(document-> { + assertThat(document.get("firstName")).isEqualTo("Foo"); + assertThat(document.get("lastName")).isEqualTo("Bar"); + }) + .assertNext(document-> { + assertThat(document.get("my_data")).isEqualTo("THE DATA"); + }) + .verifyComplete(); + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/consumer/rabbit-consumer/pom.xml b/functions/consumer/rabbit-consumer/pom.xml new file mode 100644 index 00000000..4443782d --- /dev/null +++ b/functions/consumer/rabbit-consumer/pom.xml @@ -0,0 +1,40 @@ + + + 4.0.0 + rabbit-consumer + 1.0.0.BUILD-SNAPSHOT + rabbit-consumer + Rabbit consumer + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-amqp + + + org.springframework.boot + spring-boot-starter-amqp + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + javax.validation + validation-api + + + + diff --git a/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerConfiguration.java b/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerConfiguration.java new file mode 100644 index 00000000..dd89486e --- /dev/null +++ b/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerConfiguration.java @@ -0,0 +1,150 @@ +/* + * Copyright 2019-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.rabbit; + +import java.util.function.Function; + +import org.springframework.amqp.core.MessageDeliveryMode; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.connection.ConnectionNameStrategy; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.amqp.support.converter.MessageConverter; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration; +import org.springframework.boot.autoconfigure.amqp.RabbitProperties; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.Expression; +import org.springframework.integration.amqp.dsl.Amqp; +import org.springframework.integration.amqp.dsl.AmqpOutboundChannelAdapterSpec; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHandler; + +@EnableConfigurationProperties(RabbitConsumerProperties.class) +@Configuration +public class RabbitConsumerConfiguration implements DisposableBean { + + @Autowired + private RabbitProperties bootProperties; + + @Autowired + private ObjectProvider connectionNameStrategy; + + @Autowired + private RabbitConsumerProperties properties; + + @Value("#{${rabbit.converterBeanName:null}}") + private MessageConverter messageConverter; + + private CachingConnectionFactory ownConnectionFactory; + + @Bean + public Function, Object> rabbitConsumer(@Qualifier("amqpChannelAdapter") MessageHandler messageHandler) { + return o -> { + messageHandler.handleMessage(o); + return ""; + }; + } + + @Bean + public MessageHandler amqpChannelAdapter(ConnectionFactory rabbitConnectionFactory) + throws Exception { + + AmqpOutboundChannelAdapterSpec handler = Amqp + .outboundAdapter(rabbitTemplate(this.properties.isOwnConnection() + ? buildLocalConnectionFactory() : rabbitConnectionFactory)) + .mappedRequestHeaders(properties.getMappedRequestHeaders()) + .defaultDeliveryMode(properties.getPersistentDeliveryMode() + ? MessageDeliveryMode.PERSISTENT + : MessageDeliveryMode.NON_PERSISTENT); + + Expression exchangeExpression = this.properties.getExchangeExpression(); + if (exchangeExpression != null) { + handler.exchangeNameExpression(exchangeExpression); + } + else { + handler.exchangeName(this.properties.getExchange()); + } + + Expression routingKeyExpression = this.properties.getRoutingKeyExpression(); + if (routingKeyExpression != null) { + handler.routingKeyExpression(routingKeyExpression); + } + else { + handler.routingKey(this.properties.getRoutingKey()); + } + return handler.get(); + } + + private ConnectionFactory buildLocalConnectionFactory() throws Exception { + this.ownConnectionFactory = new AutoConfig.Creator().rabbitConnectionFactory( + this.bootProperties, this.connectionNameStrategy); + return this.ownConnectionFactory; + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory rabbitConnectionFactory) { + RabbitTemplate rabbitTemplate = new RabbitTemplate(rabbitConnectionFactory); + if (this.messageConverter != null) { + rabbitTemplate.setMessageConverter(this.messageConverter); + } + return rabbitTemplate; + } + + @Bean + @ConditionalOnProperty(name = "rabbit.converterBeanName", + havingValue = RabbitConsumerProperties.JSON_CONVERTER) + public Jackson2JsonMessageConverter jsonConverter() { + return new Jackson2JsonMessageConverter(); + } + + @Override + public void destroy() { + if (this.ownConnectionFactory != null) { + this.ownConnectionFactory.destroy(); + } + } + +} + +class AutoConfig extends RabbitAutoConfiguration { + + static class Creator extends RabbitConnectionFactoryCreator { + + @Override + public CachingConnectionFactory rabbitConnectionFactory(RabbitProperties config, + ObjectProvider connectionNameStrategy) + throws Exception { + CachingConnectionFactory cf = super.rabbitConnectionFactory(config, + connectionNameStrategy); + cf.setConnectionNameStrategy( + connectionFactory -> "rabbit.sink.own.connection"); + cf.afterPropertiesSet(); + return cf; + } + + } + +} diff --git a/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerProperties.java b/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerProperties.java new file mode 100644 index 00000000..654a9c2b --- /dev/null +++ b/functions/consumer/rabbit-consumer/src/main/java/org/springframework/cloud/fn/consumer/rabbit/RabbitConsumerProperties.java @@ -0,0 +1,142 @@ +/* + * Copyright 2019-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.rabbit; + +import javax.validation.constraints.AssertTrue; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.validation.annotation.Validated; + +@ConfigurationProperties("rabbit") +@Validated +public class RabbitConsumerProperties { + + public static final String JSON_CONVERTER = "jsonConverter"; + + /** + * Exchange name - overridden by exchangeNameExpression, if supplied. + */ + private String exchange = ""; + + /** + * A SpEL expression that evaluates to an exchange name. + */ + private Expression exchangeExpression; + + /** + * Routing key - overridden by routingKeyExpression, if supplied. + */ + private String routingKey; + + /** + * A SpEL expression that evaluates to a routing key. + */ + private Expression routingKeyExpression; + + /** + * Default delivery mode when 'amqp_deliveryMode' header is not present, + * true for PERSISTENT. + */ + private boolean persistentDeliveryMode; + + /** + * Headers that will be mapped. + */ + private String[] mappedRequestHeaders = { "*" }; + + /** + * The bean name for a custom message converter; if omitted, a SimpleMessageConverter is used. + * If 'jsonConverter', a Jackson2JsonMessageConverter bean will be created for you. + */ + private String converterBeanName; + + /** + * When true, use a separate connection based on the boot properties. + */ + private boolean ownConnection; + + public String getExchange() { + return this.exchange; + } + + public void setExchange(String exchange) { + this.exchange = exchange; + } + + public Expression getExchangeExpression() { + return this.exchangeExpression; + } + + public void setExchangeExpression(Expression exchangeExpression) { + this.exchangeExpression = exchangeExpression; + } + + public String getRoutingKey() { + return this.routingKey; + } + + public void setRoutingKey(String routingKey) { + this.routingKey = routingKey; + } + + public Expression getRoutingKeyExpression() { + return this.routingKeyExpression; + } + + public void setRoutingKeyExpression(Expression routingKeyExpression) { + this.routingKeyExpression = routingKeyExpression; + } + + public boolean getPersistentDeliveryMode() { + return this.persistentDeliveryMode; + } + + public void setPersistentDeliveryMode(boolean persistentDeliveryMode) { + this.persistentDeliveryMode = persistentDeliveryMode; + } + + public String[] getMappedRequestHeaders() { + return this.mappedRequestHeaders; + } + + public void setMappedRequestHeaders(String[] mappedRequestHeaders) { + this.mappedRequestHeaders = mappedRequestHeaders; + } + + public String getConverterBeanName() { + return this.converterBeanName; + } + + public void setConverterBeanName(String converterBeanName) { + this.converterBeanName = converterBeanName; + } + + @AssertTrue(message = "routingKey or routingKeyExpression is required") + public boolean isRoutingKeyProvided() { + return this.routingKey != null || this.routingKeyExpression != null; + } + + public boolean isOwnConnection() { + return this.ownConnection; + } + + public void setOwnConnection(boolean ownConnection) { + this.ownConnection = ownConnection; + } + +} \ No newline at end of file diff --git a/functions/function/filter-function/.gitignore b/functions/function/filter-function/.gitignore new file mode 100644 index 00000000..4a453031 --- /dev/null +++ b/functions/function/filter-function/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/functions/function/filter-function/pom.xml b/functions/function/filter-function/pom.xml new file mode 100644 index 00000000..5e5b72d5 --- /dev/null +++ b/functions/function/filter-function/pom.xml @@ -0,0 +1,36 @@ + + + 4.0.0 + filter-function + 1.0.0.BUILD-SNAPSHOT + filter-function + Spring Native Function for applying filter SpEL expressions + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.cloud.fn + spel-function + 1.0.0.BUILD-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + diff --git a/functions/function/filter-function/src/main/java/org/springframework/cloud/fn/filter/FilterFunctionConfiguration.java b/functions/function/filter-function/src/main/java/org/springframework/cloud/fn/filter/FilterFunctionConfiguration.java new file mode 100644 index 00000000..9859446c --- /dev/null +++ b/functions/function/filter-function/src/main/java/org/springframework/cloud/fn/filter/FilterFunctionConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.filter; + +import java.util.Optional; +import java.util.function.Function; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.cloud.fn.spel.SpelFunctionConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.messaging.Message; + +@Configuration +@Import(SpelFunctionConfiguration.class) +public class FilterFunctionConfiguration { + + @Bean + public Function, Message> filterFunction( + @Qualifier("spelFunction") Function, Message> spelFunction) { + + return message -> + Optional.of(message) + .filter(m -> (Boolean) spelFunction.apply(m).getPayload()) + .orElse(null); + } + +} diff --git a/functions/function/filter-function/src/main/resources/application.properties b/functions/function/filter-function/src/main/resources/application.properties new file mode 100644 index 00000000..86e445ae --- /dev/null +++ b/functions/function/filter-function/src/main/resources/application.properties @@ -0,0 +1 @@ +spel.function.expression=true diff --git a/functions/function/filter-function/src/test/java/org/springframework/cloud/fn/filter/FilterFunctionApplicationTests.java b/functions/function/filter-function/src/test/java/org/springframework/cloud/fn/filter/FilterFunctionApplicationTests.java new file mode 100644 index 00000000..23afb9be --- /dev/null +++ b/functions/function/filter-function/src/test/java/org/springframework/cloud/fn/filter/FilterFunctionApplicationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(properties = "spel.function.expression=payload.length() > 5") +@DirtiesContext +public class FilterFunctionApplicationTests { + + @Autowired + @Qualifier("filterFunction") + Function, Message> filter; + + @Test + public void testFilter() { + Message filtered = this.filter.apply(new GenericMessage<>("hello")); + assertThat(filtered).isNull(); + filtered = this.filter.apply(new GenericMessage<>("hello world")); + assertThat(filtered).isNotNull() + .extracting(Message::getPayload) + .isEqualTo("hello world"); + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/function/payload-converter-function/pom.xml b/functions/function/payload-converter-function/pom.xml new file mode 100644 index 00000000..846b9c54 --- /dev/null +++ b/functions/function/payload-converter-function/pom.xml @@ -0,0 +1,37 @@ + + + 4.0.0 + payload-converter-function + 1.0.0.BUILD-SNAPSHOT + payload-converter-function + Utility message conversion functions + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework + spring-messaging + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + + diff --git a/functions/function/payload-converter-function/src/main/java/functions/ByteArrayTextToString.java b/functions/function/payload-converter-function/src/main/java/functions/ByteArrayTextToString.java new file mode 100644 index 00000000..2aecc905 --- /dev/null +++ b/functions/function/payload-converter-function/src/main/java/functions/ByteArrayTextToString.java @@ -0,0 +1,51 @@ +/* + * Copyright 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 functions; + +import java.util.function.Function; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.util.MimeTypeUtils; + +/** + * + * @author Christian Tzolov + */ +public class ByteArrayTextToString implements Function, Message> { + + @Override + public Message apply(Message message) { + + if (message.getPayload() instanceof byte[]) { + final MessageHeaders headers = message.getHeaders(); + String contentType = headers.containsKey(MessageHeaders.CONTENT_TYPE) + ? headers.get(MessageHeaders.CONTENT_TYPE).toString() + : MimeTypeUtils.APPLICATION_JSON_VALUE; + + if (contentType.contains("text") || contentType.contains("json") || contentType.contains("x-spring-tuple")) { + message = MessageBuilder.withPayload(new String(((byte[]) message.getPayload()))) + .copyHeaders(message.getHeaders()) + .build(); + } + } + + return message; + } +} + diff --git a/functions/function/payload-converter-function/src/test/java/functions/ByteArrayTextToStringTests.java b/functions/function/payload-converter-function/src/test/java/functions/ByteArrayTextToStringTests.java new file mode 100644 index 00000000..24b8a93e --- /dev/null +++ b/functions/function/payload-converter-function/src/test/java/functions/ByteArrayTextToStringTests.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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 functions; + +import java.util.Collections; +import java.util.function.Function; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ByteArrayTextToStringTests { + + private static final String MESSAGE = "hello world"; + private static Function, Message> converter; + + @BeforeAll + static void before() { + converter = new ByteArrayTextToString(); + } + + @Test + public void testDefaultNoContentType() { + Message converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes())); + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE); + + converted = converter.apply(new GenericMessage<>(MESSAGE)); // String + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE); + } + + @Test + public void testApplicationJsonContentType() { + Message converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(), + Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON_VALUE))); + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo("hello world"); + } + + @Test + public void testPlainTextContentType() { + Message converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(), + Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN_VALUE))); + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE); + } + + @Test + public void testOctetContentType() { + Message converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(), + Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE))); + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE.getBytes()); + } + + @Test + public void testRandomNonTextContentType() { + Message converted = converter.apply(new GenericMessage<>(MESSAGE.getBytes(), + Collections.singletonMap(MessageHeaders.CONTENT_TYPE, "Random Content Type"))); + assertThat(converted).isNotNull().extracting(Message::getPayload).isEqualTo(MESSAGE.getBytes()); + } + +} diff --git a/functions/function/spel-function/.gitignore b/functions/function/spel-function/.gitignore new file mode 100644 index 00000000..4a453031 --- /dev/null +++ b/functions/function/spel-function/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/functions/function/spel-function/pom.xml b/functions/function/spel-function/pom.xml new file mode 100644 index 00000000..f11c25e7 --- /dev/null +++ b/functions/function/spel-function/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + spel-function + 1.0.0.BUILD-SNAPSHOT + spel-function + Spring Native Function for applying SpEL expressions + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.cloud.function + payload-converter-function + ${project.version} + + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + diff --git a/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionConfiguration.java b/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionConfiguration.java new file mode 100644 index 00000000..9e682b1e --- /dev/null +++ b/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.spel; + +import java.util.function.Function; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.transformer.ExpressionEvaluatingTransformer; +import org.springframework.messaging.Message; + +@Configuration +@EnableConfigurationProperties(SpelFunctionProperties.class) +public class SpelFunctionConfiguration { + + @Bean + public Function, Message> spelFunction( + ExpressionEvaluatingTransformer expressionEvaluatingTransformer) { + + return message -> expressionEvaluatingTransformer.transform(message); + } + + @Bean + public ExpressionEvaluatingTransformer expressionEvaluatingTransformer( + SpelFunctionProperties spelFunctionProperties) { + + return new ExpressionEvaluatingTransformer(new SpelExpressionParser() + .parseExpression(spelFunctionProperties.getExpression())); + } + +} diff --git a/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionProperties.java b/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionProperties.java new file mode 100644 index 00000000..89bfec3f --- /dev/null +++ b/functions/function/spel-function/src/main/java/org/springframework/cloud/fn/spel/SpelFunctionProperties.java @@ -0,0 +1,47 @@ +/* + * Copyright 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 + * + * http://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.spel; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; + +/** + * Configuration properties for the SpEL function. + * + * @author Gary Russell + * @author Artem Bilan + */ +@ConfigurationProperties("spel.function") +public class SpelFunctionProperties { + + private static final Expression DEFAULT_EXPRESSION = new SpelExpressionParser().parseExpression("payload"); + + /** + * A SpEL expression to apply. + */ + private String expression = DEFAULT_EXPRESSION.getExpressionString(); + + public void setExpression(String expression) { + this.expression = expression; + } + + public String getExpression() { + return this.expression; + } + +} diff --git a/functions/function/spel-function/src/test/java/org/springframework/cloud/fn/spel/SpelFunctionApplicationTests.java b/functions/function/spel-function/src/test/java/org/springframework/cloud/fn/spel/SpelFunctionApplicationTests.java new file mode 100644 index 00000000..1ef016c3 --- /dev/null +++ b/functions/function/spel-function/src/test/java/org/springframework/cloud/fn/spel/SpelFunctionApplicationTests.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.spel; + +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.util.MimeTypeUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(properties = "spel.function.expression=payload.toUpperCase()") +@DirtiesContext +public class SpelFunctionApplicationTests { + + @Autowired + Function, Message> transformer; + + @Test + public void testTransform() { + final Message transformed = this.transformer.apply(new GenericMessage<>("hello,world")); + assertThat(transformed.getPayload()).isEqualTo("HELLO,WORLD"); + } + + @Test + public void testJson() { + Message message = MessageBuilder.withPayload("{\"foo\":\"bar\"}") + .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON).build(); + final Message transformed = this.transformer.apply(message); + assertThat(transformed.getPayload()).isEqualTo("{\"FOO\":\"BAR\"}"); + } + + @SpringBootApplication + static class TestApplication { + + } +} diff --git a/functions/function/splitter-function/.gitignore b/functions/function/splitter-function/.gitignore new file mode 100644 index 00000000..4a453031 --- /dev/null +++ b/functions/function/splitter-function/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/functions/function/splitter-function/pom.xml b/functions/function/splitter-function/pom.xml new file mode 100644 index 00000000..fcdeaf2d --- /dev/null +++ b/functions/function/splitter-function/pom.xml @@ -0,0 +1,50 @@ + + + 4.0.0 + splitter-function + 1.0.0.BUILD-SNAPSHOT + splitter-function + Spring Native Function for Splitter + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springframework.integration + spring-integration-file + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + diff --git a/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionConfiguration.java b/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionConfiguration.java new file mode 100644 index 00000000..7d1897c7 --- /dev/null +++ b/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionConfiguration.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.splitter; + +import java.nio.charset.Charset; +import java.util.List; +import java.util.function.Function; + +import org.reactivestreams.Publisher; + +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.integration.channel.ReactiveStreamsSubscribableChannel; +import org.springframework.integration.file.splitter.FileSplitter; +import org.springframework.integration.splitter.AbstractMessageSplitter; +import org.springframework.integration.splitter.DefaultMessageSplitter; +import org.springframework.integration.splitter.ExpressionEvaluatingSplitter; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageChannel; + +import reactor.core.publisher.Flux; + +@Configuration +@EnableConfigurationProperties(SplitterFunctionProperties.class) +public class SplitterFunctionConfiguration { + + @Bean + public Function, List>> splitterFunction(AbstractMessageSplitter messageSplitter, + SplitterFunctionProperties splitterFunctionProperties) { + + messageSplitter.setApplySequence(splitterFunctionProperties.isApplySequence()); + ThreadLocalFluxSinkMessageChannel outputChannel = new ThreadLocalFluxSinkMessageChannel(); + messageSplitter.setOutputChannel(outputChannel); + return message -> { + messageSplitter.handleMessage(message); + return outputChannel.publisherThreadLocal.get(); + }; + } + + @Bean + @ConditionalOnProperty(prefix = "splitter", name = "expression") + public AbstractMessageSplitter expressionSplitter(SplitterFunctionProperties splitterFunctionProperties) { + return new ExpressionEvaluatingSplitter( + new SpelExpressionParser() + .parseExpression(splitterFunctionProperties.getExpression())); + } + + @Bean + @ConditionalOnMissingBean + @Conditional(FileSplitterCondition.class) + public AbstractMessageSplitter fileSplitter(SplitterFunctionProperties splitterFunctionProperties) { + Boolean markers = splitterFunctionProperties.getFileMarkers(); + String charset = splitterFunctionProperties.getCharset(); + if (markers == null) { + markers = false; + } + FileSplitter fileSplitter = new FileSplitter(true, markers, splitterFunctionProperties.getMarkersJson()); + if (charset != null) { + fileSplitter.setCharset(Charset.forName(charset)); + } + return fileSplitter; + } + + + @Bean + @ConditionalOnMissingBean + public AbstractMessageSplitter defaultSplitter(SplitterFunctionProperties splitterFunctionProperties) { + DefaultMessageSplitter defaultMessageSplitter = new DefaultMessageSplitter(); + defaultMessageSplitter.setDelimiters(splitterFunctionProperties.getDelimiters()); + return defaultMessageSplitter; + } + + static class FileSplitterCondition extends AnyNestedCondition { + + FileSplitterCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(prefix = "splitter", name = "charset") + static class Charset { } + + @ConditionalOnProperty(prefix = "splitter", name = "fileMarkers") + static class FileMarkers { } + + } + + private static final class ThreadLocalFluxSinkMessageChannel + implements MessageChannel, ReactiveStreamsSubscribableChannel { + + private final ThreadLocal>> publisherThreadLocal = new ThreadLocal<>(); + + @Override + @SuppressWarnings("unchecked") + public void subscribeTo(Publisher> publisher) { + this.publisherThreadLocal.set(Flux.from(publisher).collectList().cast(List.class).block()); + } + + @Override + public boolean send(Message message, long l) { + throw new UnsupportedOperationException("This channel only supports a reactive 'subscribeTo()' "); + } + + } + +} diff --git a/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionProperties.java b/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionProperties.java new file mode 100644 index 00000000..9749f95f --- /dev/null +++ b/functions/function/splitter-function/src/main/java/org/springframework/cloud/fn/splitter/SplitterFunctionProperties.java @@ -0,0 +1,128 @@ +/* + * Copyright 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 + * + * http://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.splitter; + +import javax.validation.constraints.AssertTrue; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Configuration properties for the Splitter Processor app. + * + * @author Gary Russell + * @author Artem Bilan + */ +@ConfigurationProperties("splitter") +@Validated +public class SplitterFunctionProperties { + + /** + * A SpEL expression for splitting payloads. + */ + private String expression; + + /** + * When expression is null, delimiters to use when tokenizing + * {@link String} payloads. + */ + private String delimiters; + + /** + * Set to true or false to use a {@code FileSplitter} (to split + * text-based files by line) that includes + * (or not) beginning/end of file markers. + */ + private Boolean fileMarkers; + + /** + * When 'fileMarkers == true', specify if they should be produced + * as FileSplitter.FileMarker objects or JSON. + */ + private boolean markersJson = true; + + /** + * The charset to use when converting bytes in text-based files + * to String. + */ + private String charset; + + /** + * Add correlation/sequence information in headers to facilitate later + * aggregation. + */ + private boolean applySequence = true; + + public void setExpression(String expression) { + this.expression = expression; + } + + public String getExpression() { + return this.expression; + } + + public String getDelimiters() { + return this.delimiters; + } + + public void setDelimiters(String delimiters) { + this.delimiters = delimiters; + } + + public Boolean getFileMarkers() { + return this.fileMarkers; + } + + public void setFileMarkers(Boolean fileMarkers) { + this.fileMarkers = fileMarkers; + } + + public boolean getMarkersJson() { + return this.markersJson; + } + + public void setMarkersJson(boolean markersJson) { + this.markersJson = markersJson; + } + + public String getCharset() { + return this.charset; + } + + public void setCharset(String charset) { + this.charset = charset; + } + + public boolean isApplySequence() { + return this.applySequence; + } + + public void setApplySequence(boolean applySequence) { + this.applySequence = applySequence; + } + + @AssertTrue(message = "'delimiters' is not allowed when an 'expression' is provided") + public boolean isDelimitersAllowed() { + return this.expression == null || this.delimiters == null; + } + + @AssertTrue(message = "File properties are not allowed when an 'expression' or 'delimiters' property is provided") + public boolean isFilePropsAllowed() { + return !(this.expression != null || this.delimiters != null) || this.fileMarkers == null && this.charset == null; + } + +} diff --git a/functions/function/splitter-function/src/test/java/org/springframework/cloud/fn/splitter/SplitterFunctionApplicationTests.java b/functions/function/splitter-function/src/test/java/org/springframework/cloud/fn/splitter/SplitterFunctionApplicationTests.java new file mode 100644 index 00000000..6a8a5f94 --- /dev/null +++ b/functions/function/splitter-function/src/test/java/org/springframework/cloud/fn/splitter/SplitterFunctionApplicationTests.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2011-2020 Pivotal Software Inc, All Rights Reserved. + * + * 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.splitter; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.function.Function; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.messaging.support.GenericMessage; +import org.springframework.test.annotation.DirtiesContext; + +@SpringBootTest(properties = "splitter.expression=payload.split(',')") +@DirtiesContext +public class SplitterFunctionApplicationTests { + + @Autowired + Function, List>> splitter; + + @Test + public void testExpressionSplitter() { + List> messageList = this.splitter.apply(new GenericMessage<>("hello,world")); + assertThat(messageList).extracting(m -> m.getPayload().toString()).contains("hello", "world"); + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/pom.xml b/functions/pom.xml new file mode 100644 index 00000000..cfd3fd98 --- /dev/null +++ b/functions/pom.xml @@ -0,0 +1,205 @@ + + + 4.0.0 + org.springframework.cloud.fn + java-functions-parent + 1.0.0.BUILD-SNAPSHOT + java-functions-parent + Pivotal Java Functions Parent + pom + + + 1.8 + 3.1.1 + 3.2.1 + 2.22.2 + UTF-8 + UTF-8 + ${java.version} + ${java.version} + + + + consumer/cassandra-consumer + consumer/counter-consumer + consumer/file-consumer + consumer/jdbc-consumer + consumer/log-consumer + consumer/mongodb-consumer + consumer/rabbit-consumer + + function/filter-function + function/spel-function + function/payload-converter-function + function/splitter-function + + supplier/file-supplier + supplier/http-supplier + supplier/jdbc-supplier + supplier/mongodb-supplier + supplier/time-supplier + + spring-functions-parent + + + + + Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0 + Copyright 2014-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 + + http://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. + + + + scm:git:git://github.com/pivotal/java-functions.git + scm:git:ssh://git@github.com/pivotal/java-functions.git + https://github.com/pivotal/java-functions + + + + + maven-javadoc-plugin + ${maven-javadoc-plugin.version} + + + javadoc + package + + jar + + + + + true + + + + maven-source-plugin + ${maven-source-plugin.version} + + + attach-sources + package + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + **/*Tests.java + **/*Test.java + + + **/Abstract*.java + + + + + + + + repo.spring.io + Spring Release Repository + https://repo.spring.io/libs-release-local + + + repo.spring.io + Spring Snapshot Repository + https://repo.spring.io/libs-snapshot-local + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + true + + + + + + + + milestone + + + repo.spring.io + Spring Milestone Repository + https://repo.spring.io/libs-milestone-local + + + + + central + + + + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + + + + sonatype-nexus-staging + Nexus Release Repository + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + sonatype-nexus-snapshots + Sonatype Nexus Snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + + + + diff --git a/functions/spring-functions-parent/pom.xml b/functions/spring-functions-parent/pom.xml new file mode 100644 index 00000000..b645f407 --- /dev/null +++ b/functions/spring-functions-parent/pom.xml @@ -0,0 +1,40 @@ + + + + java-functions-parent + org.springframework.cloud.fn + 1.0.0.BUILD-SNAPSHOT + .. + + 4.0.0 + + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + pom + + + 2.3.0.M4 + 3.0.3.RELEASE + + + + + org.springframework.boot + spring-boot-starter-parent + ${spring-boot.version} + import + pom + + + org.springframework.cloud + spring-cloud-function-dependencies + ${spring-cloud-function.version} + import + pom + + + + + \ No newline at end of file diff --git a/functions/supplier/file-supplier/pom.xml b/functions/supplier/file-supplier/pom.xml new file mode 100644 index 00000000..43d085de --- /dev/null +++ b/functions/supplier/file-supplier/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + file-supplier + 1.0.0.BUILD-SNAPSHOT + file-supplier + file supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-file + + + org.springframework.boot + spring-boot-starter-integration + + + org.springframework.boot + spring-boot-starter-json + true + + + 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 + + + + diff --git a/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileConsumerProperties.java b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileConsumerProperties.java new file mode 100644 index 00000000..b5c08234 --- /dev/null +++ b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileConsumerProperties.java @@ -0,0 +1,84 @@ +/* + * Copyright 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.file; + +import javax.validation.constraints.AssertTrue; +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * TODO: This will be used in other apps like (S)FTP and S3. Therefore, it might be moved to a common place. + * + * @author David Turanski + * @author Artem Bilan + */ +@ConfigurationProperties("file.consumer") +@Validated +public class FileConsumerProperties { + + /** + * The FileReadingMode to use for file reading sources. + * Values are 'ref' - The File object, + * 'lines' - a message per line, or + * 'contents' - the contents as bytes. + */ + private FileReadingMode mode = FileReadingMode.contents; + + /** + * Set to true to emit start of file/end of file marker messages before/after the data. + * Only valid with FileReadingMode 'lines'. + */ + private Boolean withMarkers = null; + + /** + * When 'fileMarkers == true', specify if they should be produced + * as FileSplitter.FileMarker objects or JSON. + */ + private boolean markersJson = true; + + @NotNull + public FileReadingMode getMode() { + return this.mode; + } + + public void setMode(FileReadingMode mode) { + this.mode = mode; + } + + public Boolean getWithMarkers() { + return this.withMarkers; + } + + public void setWithMarkers(Boolean withMarkers) { + this.withMarkers = withMarkers; + } + + public boolean getMarkersJson() { + return this.markersJson; + } + + public void setMarkersJson(boolean markersJson) { + this.markersJson = markersJson; + } + + @AssertTrue(message = "withMarkers can only be supplied when FileReadingMode is 'lines'") + public boolean isWithMarkersValid() { + return this.withMarkers == null || FileReadingMode.lines == this.mode; + } +} diff --git a/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileReadingMode.java b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileReadingMode.java new file mode 100644 index 00000000..3d357733 --- /dev/null +++ b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileReadingMode.java @@ -0,0 +1,29 @@ +/* + * Copyright 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.file; + +/** + * Defines the supported modes of reading and processing files. + * + * @author Gunnar Hillert + * @author David Turanski + */ +public enum FileReadingMode { + ref, + lines, + contents; +} diff --git a/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierConfiguration.java b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierConfiguration.java new file mode 100644 index 00000000..ff4139a6 --- /dev/null +++ b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 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.file; + +import java.util.function.Supplier; + +import org.reactivestreams.Publisher; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.integration.dsl.IntegrationFlowBuilder; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.file.FileReadingMessageSource; +import org.springframework.integration.file.dsl.FileInboundChannelAdapterSpec; +import org.springframework.integration.file.dsl.Files; +import org.springframework.messaging.Message; +import org.springframework.util.StringUtils; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +/** + * @author Artem Bilan + * @author Soby Chacko + */ +@Configuration +@EnableConfigurationProperties({FileSupplierProperties.class, FileConsumerProperties.class}) +public class FileSupplierConfiguration { + + private final FileSupplierProperties fileSupplierProperties; + + private final FileConsumerProperties fileConsumerProperties; + + @Autowired + @Lazy + @Qualifier("fileMessageSource") + private FileReadingMessageSource fileMessageSource; + + public FileSupplierConfiguration(FileSupplierProperties fileSupplierProperties, + FileConsumerProperties fileConsumerProperties) { + this.fileSupplierProperties = fileSupplierProperties; + this.fileConsumerProperties = fileConsumerProperties; + } + + @Bean + public FileInboundChannelAdapterSpec fileMessageSource() { + final FileInboundChannelAdapterSpec fileInboundChannelAdapterSpec = + Files.inboundAdapter(this.fileSupplierProperties.getDirectory()); + if (StringUtils.hasText(this.fileSupplierProperties.getFilenamePattern())) { + fileInboundChannelAdapterSpec.patternFilter(this.fileSupplierProperties.getFilenamePattern()); + } + else if (this.fileSupplierProperties.getFilenameRegex() != null) { + fileInboundChannelAdapterSpec.regexFilter(this.fileSupplierProperties.getFilenameRegex().pattern()); + } + fileInboundChannelAdapterSpec.preventDuplicates(this.fileSupplierProperties.isPreventDuplicates()); + return fileInboundChannelAdapterSpec; + } + + @Bean + public Flux> fileMessageFlux() { + return Mono.>create(monoSink -> + monoSink.onRequest(value -> + monoSink.success(this.fileMessageSource.receive()))) + .subscribeOn(Schedulers.boundedElastic()) + .repeatWhenEmpty(it -> it.delayElements(this.fileSupplierProperties.getDelayWhenEmpty())) + .repeat(); + } + + @Bean + @ConditionalOnExpression("environment['file.consumer.mode'] != 'ref'") + public Publisher> fileReadingFlow() { + IntegrationFlowBuilder flowBuilder = IntegrationFlows.from(fileMessageFlux()); + return FileUtils.enhanceFlowForReadingMode(flowBuilder, this.fileConsumerProperties) + .toReactivePublisher(); + } + + @Bean + public Supplier>> fileSupplier() { + if (this.fileConsumerProperties.getMode() == FileReadingMode.ref) { + return this::fileMessageFlux; + } + else { + return () -> Flux.from(fileReadingFlow()); + } + } +} diff --git a/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierProperties.java b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierProperties.java new file mode 100644 index 00000000..3e14582b --- /dev/null +++ b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileSupplierProperties.java @@ -0,0 +1,112 @@ +/* + * Copyright 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.file; + +import java.io.File; +import java.time.Duration; +import java.util.regex.Pattern; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * Properties for the file supplier. + * + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@ConfigurationProperties("file.supplier") +@Validated +public class FileSupplierProperties { + + private static final String DEFAULT_DIR = System.getProperty("java.io.tmpdir") + + File.separator + "file-supplier"; + + /** + * The directory to poll for new files. + */ + private File directory = new File(DEFAULT_DIR); + + /** + * Set to true to include an AcceptOnceFileListFilter which prevents duplicates. + */ + private boolean preventDuplicates = true; + + /** + * A simple ant pattern to match files. + */ + private String filenamePattern; + + /** + * A regex pattern to match files. + */ + private Pattern filenameRegex; + + /** + * Duration of delay when no new files are detected. + */ + private Duration delayWhenEmpty = Duration.ofSeconds(1); + + public File getDirectory() { + return this.directory; + } + + public void setDirectory(File directory) { + this.directory = directory; + } + + public boolean isPreventDuplicates() { + return this.preventDuplicates; + } + + public void setPreventDuplicates(boolean preventDuplicates) { + this.preventDuplicates = preventDuplicates; + } + + public String getFilenamePattern() { + return this.filenamePattern; + } + + public void setFilenamePattern(String filenamePattern) { + this.filenamePattern = filenamePattern; + } + + public Pattern getFilenameRegex() { + return this.filenameRegex; + } + + public void setFilenameRegex(Pattern filenameRegex) { + this.filenameRegex = filenameRegex; + } + + //@AssertTrue(message = "filenamePattern and filenameRegex are mutually exclusive") + + public boolean isExclusivePatterns() { + return !(this.filenamePattern != null && this.filenameRegex != null); + } + + public Duration getDelayWhenEmpty() { + return delayWhenEmpty; + } + + public void setDelayWhenEmpty(Duration delayWhenEmpty) { + this.delayWhenEmpty = delayWhenEmpty; + } + + +} diff --git a/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileUtils.java b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileUtils.java new file mode 100644 index 00000000..7e1e04e0 --- /dev/null +++ b/functions/supplier/file-supplier/src/main/java/org/springframework/cloud/fn/supplier/file/FileUtils.java @@ -0,0 +1,103 @@ +/* + * Copyright 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.file; + +import java.util.Collections; + +import org.springframework.integration.dsl.IntegrationFlowBuilder; +import org.springframework.integration.file.splitter.FileSplitter; +import org.springframework.integration.file.transformer.FileToByteArrayTransformer; +import org.springframework.integration.transformer.StreamTransformer; +import org.springframework.messaging.MessageHeaders; +import org.springframework.util.MimeTypeUtils; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Christian Tzolov + * + */ +public class FileUtils { + + /** + * Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on + * {@link FileConsumerProperties}. + * @param flowBuilder the flow builder. + * @param fileConsumerProperties the properties. + * @return the updated flow builder. + */ + public static IntegrationFlowBuilder enhanceFlowForReadingMode(IntegrationFlowBuilder flowBuilder, + FileConsumerProperties fileConsumerProperties) { + switch (fileConsumerProperties.getMode()) { + case contents: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)) + .transform(new FileToByteArrayTransformer()); + break; + case lines: + Boolean withMarkers = fileConsumerProperties.getWithMarkers(); + if (withMarkers == null) { + withMarkers = false; + } + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.TEXT_PLAIN_VALUE)) + .split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson())); + break; + case ref: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_JSON_VALUE)); + break; + default: + throw new IllegalArgumentException(fileConsumerProperties.getMode().name() + + " is not a supported file reading mode."); + } + return flowBuilder; + } + + /** + * Enhance an {@link IntegrationFlowBuilder} to add flow snippets, depending on + * {@link FileConsumerProperties}; used for streaming sources. + * @param flowBuilder the flow builder. + * @param fileConsumerProperties the properties. + * @return the updated flow builder. + */ + public static IntegrationFlowBuilder enhanceStreamFlowForReadingMode(IntegrationFlowBuilder flowBuilder, + FileConsumerProperties fileConsumerProperties) { + switch (fileConsumerProperties.getMode()) { + case contents: + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE)) + .transform(new StreamTransformer()); + break; + case lines: + Boolean withMarkers = fileConsumerProperties.getWithMarkers(); + if (withMarkers == null) { + withMarkers = false; + } + flowBuilder.enrichHeaders(Collections.singletonMap(MessageHeaders.CONTENT_TYPE, + MimeTypeUtils.TEXT_PLAIN_VALUE)) + .split(new FileSplitter(true, withMarkers, fileConsumerProperties.getMarkersJson())); + break; + case ref: + default: + throw new IllegalArgumentException(fileConsumerProperties.getMode().name() + + " is not a supported file reading mode when streaming."); + } + return flowBuilder; + } + +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/AbstractFileSupplierTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/AbstractFileSupplierTests.java new file mode 100644 index 00000000..56c3f4e8 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/AbstractFileSupplierTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 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.file; + +import java.nio.file.Path; +import java.util.function.Supplier; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.io.TempDir; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; +import reactor.core.publisher.Flux; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@SpringBootTest +@DirtiesContext +public class AbstractFileSupplierTests { + + @TempDir + static Path tempDir; + + @Autowired + Supplier>> fileSupplier; + + @BeforeAll + public static void beforeAll() { + System.setProperty("file.supplier.directory", tempDir.toAbsolutePath().toString()); + } + + @AfterAll + public static void afterAll() { + System.clearProperty("file.supplier.directory"); + } + + @SpringBootApplication + static class TestApplication { + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/DefaultFileSupplierTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/DefaultFileSupplierTests.java new file mode 100644 index 00000000..b933e9a3 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/DefaultFileSupplierTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.file.FileHeaders; +import org.springframework.messaging.Message; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +public class DefaultFileSupplierTests extends AbstractFileSupplierTests { + + @Test + public void testBasicFlow() throws IOException { + + Path firstFile = tempDir.resolve("first.file"); + Files.write(firstFile, "first.file".getBytes()); + + final Flux> messageFlux = fileSupplier.get(); + + //create file after subscription + Path tempFile = tempDir.resolve("test.file"); + + StepVerifier stepVerifier = + StepVerifier.create(messageFlux) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo("first.file".getBytes()); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.FILENAME, "first.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.RELATIVE_PATH, "first.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.ORIGINAL_FILE, firstFile.toFile()); + } + ) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo("testing".getBytes()); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.FILENAME, "test.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.RELATIVE_PATH, "test.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.ORIGINAL_FILE, tempFile.toFile()); + }) + .thenCancel() + .verifyLater(); + Files.write(tempFile, "testing".getBytes()); + stepVerifier.verify(); + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FileModeRefTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FileModeRefTests.java new file mode 100644 index 00000000..c420f74b --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FileModeRefTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.file.FileHeaders; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = "file.consumer.mode=ref") +public class FileModeRefTests extends AbstractFileSupplierTests { + + @Test + public void testBasicFlow() throws IOException { + + Path firstFile = tempDir.resolve("first.file"); + Files.write(firstFile, "first.file".getBytes()); + + final Flux> messageFlux = fileSupplier.get(); + + StepVerifier.create(messageFlux) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo(firstFile.toAbsolutePath().toFile()); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.FILENAME, "first.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.RELATIVE_PATH, "first.file"); + assertThat(message.getHeaders()) + .containsEntry(FileHeaders.ORIGINAL_FILE, firstFile.toFile()); + } + ) + .thenCancel() + .verify(); + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithPatternTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithPatternTests.java new file mode 100644 index 00000000..e1c81697 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithPatternTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = {"file.consumer.mode=ref", "file.supplier.filenamePattern = *.txt"}) +public class FilePayloadWithPatternTests extends AbstractFileSupplierTests { + + @Test + public void testPattern() throws IOException { + + Path txtFile1 = tempDir.resolve("test1.txt"); + Files.write(txtFile1, "one".getBytes()); + Path nonTxtExtension = tempDir.resolve("hello.bin"); + Files.write(nonTxtExtension, ByteBuffer.allocate(4).putInt(1).array()); + Path txtFile2 = tempDir.resolve("test2.txt"); + Files.write(txtFile2, "two".getBytes()); + + final Flux> messageFlux = fileSupplier.get(); + + StepVerifier.create(messageFlux) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo(txtFile1.toAbsolutePath().toFile()); + } + ) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo(txtFile2.toAbsolutePath().toFile()); + }) + .expectNoEvent(Duration.ofSeconds(1)) + .thenCancel() + .verify(); + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithRegexTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithRegexTests.java new file mode 100644 index 00000000..0bf999c1 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/FilePayloadWithRegexTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = {"file.consumer.mode=ref", "file.supplier.filenameRegex = t.*.txt"}) +public class FilePayloadWithRegexTests extends AbstractFileSupplierTests { + + @Test + public void testRegexPattern() throws IOException { + + Path txtFile1 = tempDir.resolve("test1.txt"); + Files.write(txtFile1, "one".getBytes()); + Path nonTxtExtension = tempDir.resolve("hello.bin"); + Files.write(nonTxtExtension, ByteBuffer.allocate(4).putInt(1).array()); + Path txtFile2 = tempDir.resolve("abc.txt"); + Files.write(txtFile2, "two".getBytes()); + + final Flux> messageFlux = fileSupplier.get(); + + StepVerifier stepVerifier = + StepVerifier.create(messageFlux) + .assertNext((message) -> { + assertThat(message.getPayload()) + .isEqualTo(txtFile1.toAbsolutePath().toFile()); + } + ) + .expectNoEvent(Duration.ofSeconds(1)) + .expectNoEvent(Duration.ofSeconds(1)) + .thenCancel() + .verifyLater(); + + stepVerifier.verify(); + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesAndMarkersAsJsonPayloadTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesAndMarkersAsJsonPayloadTests.java new file mode 100644 index 00000000..f03bea85 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesAndMarkersAsJsonPayloadTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Test; +import org.springframework.integration.file.splitter.FileSplitter; +import org.springframework.integration.json.JsonPathUtils; +import org.springframework.integration.support.json.JsonObjectMapperProvider; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = {"file.consumer.mode=lines", "file.consumer.withMarkers = true"}) +public class LinesAndMarkersAsJsonPayloadTests extends AbstractFileSupplierTests { + + @Test + public void testLinesWithMarkers() throws Exception { + Path firstFile = tempDir.resolve("test.file"); + Files.write(firstFile, "first line\n".getBytes()); + Files.write(firstFile, "second line\n".getBytes(), StandardOpenOption.APPEND); + + final Flux> messageFlux = fileSupplier.get(); + + StepVerifier.create(messageFlux) + .assertNext((message) -> { + try { + final Object evaluate = JsonPathUtils.evaluate(message.getPayload(), "$.mark"); + assertThat(evaluate).isEqualTo(FileSplitter.FileMarker.Mark.START.name()); + } catch (IOException e) { + // passt through + } + } + ) + .assertNext((message) -> assertThat(message.getPayload()).isEqualTo("first line")) + .assertNext((message) -> assertThat(message.getPayload()).isEqualTo("second line")) + .assertNext((message) -> { + try { + final Object fileMarker = JsonPathUtils.evaluate(message.getPayload(), "$.mark"); + assertThat(fileMarker).isEqualTo(FileSplitter.FileMarker.Mark.END.name()); + FileSplitter.FileMarker fileMarker1 = JsonObjectMapperProvider.newInstance() + .fromJson(fileMarker, FileSplitter.FileMarker.class); + assertThat(FileSplitter.FileMarker.Mark.END).isEqualTo(fileMarker1.getMark()); + assertThat(firstFile.toAbsolutePath()).isEqualTo(fileMarker1.getFilePath()); + assertThat(fileMarker1.getLineCount()).isEqualTo(2); + } catch (IOException e) { + // passt through + } + } + ) + .thenCancel() + .verify(); + } +} diff --git a/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesPayloadTests.java b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesPayloadTests.java new file mode 100644 index 00000000..a70a0841 --- /dev/null +++ b/functions/supplier/file-supplier/src/test/java/org/springframework/cloud/fn/supplier/file/LinesPayloadTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 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.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; + +import org.junit.jupiter.api.Test; +import org.springframework.messaging.Message; +import org.springframework.test.context.TestPropertySource; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Gary Russell + * @author Artem Bilan + * @author Soby Chacko + */ +@TestPropertySource(properties = "file.consumer.mode=lines") +public class LinesPayloadTests extends AbstractFileSupplierTests { + + @Test + public void testLines() throws IOException { + Path firstFile = tempDir.resolve("test.file"); + Files.write(firstFile, "first line\n".getBytes()); + Files.write(firstFile, "second line\n".getBytes(), StandardOpenOption.APPEND); + + final Flux> messageFlux = fileSupplier.get(); + + StepVerifier.create(messageFlux) + .assertNext((message) -> assertThat(message.getPayload()).isEqualTo("first line")) + .assertNext((message) -> assertThat(message.getPayload()).isEqualTo("second line")) + .thenCancel() + .verify(); + + } +} diff --git a/functions/supplier/http-supplier/.gitignore b/functions/supplier/http-supplier/.gitignore new file mode 100644 index 00000000..4a453031 --- /dev/null +++ b/functions/supplier/http-supplier/.gitignore @@ -0,0 +1,28 @@ +/target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +/build/ + +### VS Code ### +.vscode/ diff --git a/functions/supplier/http-supplier/pom.xml b/functions/supplier/http-supplier/pom.xml new file mode 100644 index 00000000..0e4ab57b --- /dev/null +++ b/functions/supplier/http-supplier/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + http-supplier + 1.0.0.BUILD-SNAPSHOT + http-supplier + HTTP Supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.boot + spring-boot-starter-webflux + + + + org.springframework.integration + spring-integration-webflux + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + io.projectreactor + reactor-test + test + + + javax.validation + validation-api + + + org.hibernate.validator + hibernate-validator + 6.1.0.Final + + + + diff --git a/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSourceProperties.java b/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSourceProperties.java new file mode 100644 index 00000000..4210d5bb --- /dev/null +++ b/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSourceProperties.java @@ -0,0 +1,120 @@ +/* + * Copyright 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 + * + * http://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.http; + +import javax.validation.constraints.NotEmpty; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.integration.http.support.DefaultHttpHeaderMapper; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.cors.CorsConfiguration; + +/** + * Configuration properties for the HTTP Supplier. + * + * @author Artem Bilan + */ +@ConfigurationProperties("http") +@Validated +public class HttpSourceProperties { + + /** + * HTTP endpoint path mapping. + */ + private String pathPattern = "/"; + + /** + * Headers that will be mapped. + */ + private String[] mappedRequestHeaders = { DefaultHttpHeaderMapper.HTTP_REQUEST_HEADER_NAME_PATTERN }; + + /** + * CORS properties. + */ + private Cors cors = new Cors(); + + @NotEmpty + public String getPathPattern() { + return this.pathPattern; + } + + public void setPathPattern(String pathPattern) { + this.pathPattern = pathPattern; + } + + public String[] getMappedRequestHeaders() { + return this.mappedRequestHeaders; + } + + public void setMappedRequestHeaders(String[] mappedRequestHeaders) { + this.mappedRequestHeaders = mappedRequestHeaders; + } + + public Cors getCors() { + return this.cors; + } + + public void setCors(Cors cors) { + this.cors = cors; + } + + public static class Cors { + + /** + * List of allowed origins, e.g. "http://domain1.com". + */ + private String[] allowedOrigins = { CorsConfiguration.ALL }; + + /** + * List of request headers that can be used during the actual request. + */ + private String[] allowedHeaders = { CorsConfiguration.ALL }; + + /** + * Whether the browser should include any cookies associated with the domain of the request being annotated. + */ + private Boolean allowCredentials; + + @NotEmpty + public String[] getAllowedOrigins() { + return this.allowedOrigins; + } + + public void setAllowedOrigins(String[] allowedOrigins) { + this.allowedOrigins = allowedOrigins; + } + + @NotEmpty + public String[] getAllowedHeaders() { + return this.allowedHeaders; + } + + public void setAllowedHeaders(String[] allowedHeaders) { + this.allowedHeaders = allowedHeaders; + } + + public Boolean getAllowCredentials() { + return allowCredentials; + } + + public void setAllowCredentials(Boolean allowCredentials) { + this.allowCredentials = allowCredentials; + } + + } + +} diff --git a/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSupplierConfiguration.java b/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSupplierConfiguration.java new file mode 100644 index 00000000..a59bd7d6 --- /dev/null +++ b/functions/supplier/http-supplier/src/main/java/org/springframework/cloud/fn/supplier/http/HttpSupplierConfiguration.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2011-2018 Pivotal Software Inc, All Rights Reserved. + * + * 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.http; + +import java.util.function.Supplier; + +import org.reactivestreams.Publisher; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.integration.dsl.IntegrationFlows; +import org.springframework.integration.expression.ValueExpression; +import org.springframework.integration.http.support.DefaultHttpHeaderMapper; +import org.springframework.integration.mapping.HeaderMapper; +import org.springframework.integration.webflux.dsl.WebFlux; +import org.springframework.integration.webflux.inbound.WebFluxInboundEndpoint; +import org.springframework.messaging.Message; + +import reactor.core.publisher.Flux; + +/** + * Configuration for the HTTP Supplier. + * + * @author Artem Bilan + */ +@EnableConfigurationProperties(HttpSourceProperties.class) +@Configuration +public class HttpSupplierConfiguration { + + @Bean + public Publisher> httpSupplierFlow(HttpSourceProperties httpSourceProperties) { + return IntegrationFlows.from( + WebFlux.inboundChannelAdapter(httpSourceProperties.getPathPattern()) + .requestPayloadType(byte[].class) + .statusCodeExpression(new ValueExpression<>(HttpStatus.ACCEPTED)) + .mappedRequestHeaders(httpSourceProperties.getMappedRequestHeaders()) + .crossOrigin(crossOrigin -> + crossOrigin.origin(httpSourceProperties.getCors().getAllowedOrigins()) + .allowedHeaders(httpSourceProperties.getCors().getAllowedHeaders()) + .allowCredentials(httpSourceProperties.getCors().getAllowCredentials())) + .autoStartup(false)) + .toReactivePublisher(); + } + + @Bean + public HeaderMapper httpHeaderMapper() { + return DefaultHttpHeaderMapper.inboundMapper(); + } + + @Bean + public Supplier>> httpSupplier( + Publisher> httpRequestPublisher, + WebFluxInboundEndpoint webFluxInboundEndpoint) { + + return () -> Flux.from(httpRequestPublisher) + .doOnSubscribe((subscription) -> webFluxInboundEndpoint.start()) + .doOnTerminate(webFluxInboundEndpoint::stop); + } + +} diff --git a/functions/supplier/http-supplier/src/main/resources/application.properties b/functions/supplier/http-supplier/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/functions/supplier/http-supplier/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/functions/supplier/http-supplier/src/test/java/org/springframework/cloud/fn/supplier/http/HttpSupplierApplicationTests.java b/functions/supplier/http-supplier/src/test/java/org/springframework/cloud/fn/supplier/http/HttpSupplierApplicationTests.java new file mode 100644 index 00000000..c7d229c7 --- /dev/null +++ b/functions/supplier/http-supplier/src/test/java/org/springframework/cloud/fn/supplier/http/HttpSupplierApplicationTests.java @@ -0,0 +1,123 @@ +package org.springframework.cloud.fn.supplier.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.web.server.LocalServerPort; +import org.springframework.http.MediaType; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; +import org.springframework.integration.http.HttpHeaders; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.web.reactive.function.client.WebClient; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.SslProvider; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import reactor.core.publisher.Flux; +import reactor.netty.http.client.HttpClient; +import reactor.test.StepVerifier; + +/** + * The test for HTTP Supplier. + * + * @author Artem Bilan + */ +@SpringBootTest( + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "server.ssl.key-store=classpath:test.jks", + "server.ssl.key-password=password", + "server.ssl.trust-store=classpath:test.jks", + "server.ssl.client-auth=want" + }) +public class HttpSupplierApplicationTests { + + @Autowired + private Supplier>> httpSupplier; + + @LocalServerPort + private int port; + + @Test + public void testHttpSupplier() { + Flux> messageFlux = this.httpSupplier.get(); + + StepVerifier stepVerifier = + StepVerifier.create(messageFlux) + .assertNext((message) -> + assertThat(message) + .satisfies((msg) -> assertThat(msg) + .extracting(Message::getPayload) + .isEqualTo("test1".getBytes())) + .satisfies((msg) -> assertThat(msg.getHeaders()) + .containsEntry(MessageHeaders.CONTENT_TYPE, + new MediaType("text", "plain", StandardCharsets.UTF_8)) + .extractingByKey(HttpHeaders.REQUEST_URL).asString() + .startsWith("https://")) + ) + .assertNext((message) -> + assertThat(message) + .extracting(Message::getPayload) + .isEqualTo("{\"name\":\"test2\"}".getBytes())) + .assertNext((message) -> + assertThat(message) + .extracting(Message::getPayload) + .isEqualTo("{\"name\":\"test3\"}".getBytes())) + .thenCancel() + .verifyLater(); + + HttpClient httpClient = + HttpClient.create() + .secure(sslSpec -> + sslSpec.sslContext(SslContextBuilder.forClient() + .sslProvider(SslProvider.JDK) + .trustManager(InsecureTrustManagerFactory.INSTANCE))); + + WebClient webClient = + WebClient.builder() + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .baseUrl("https://localhost:" + port) + .build(); + + WebClient.RequestBodySpec requestBodySpec = webClient.post().uri("/"); + requestBodySpec.bodyValue("test1").exchange().block(Duration.ofSeconds(10)); + requestBodySpec.bodyValue(new TestPojo("test2")).exchange().block(Duration.ofSeconds(10)); + requestBodySpec.bodyValue(new TestPojo("test3")).exchange().block(Duration.ofSeconds(10)); + + stepVerifier.verify(); + } + + private static class TestPojo { + + private String name; + + public TestPojo() { + } + + public TestPojo(String name) { + this.name = name; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + } + + @SpringBootApplication + static class TestApplication { } + +} diff --git a/functions/supplier/http-supplier/src/test/resources/test.jks b/functions/supplier/http-supplier/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..0fc3e802f75461dd074facb9611d350db4d5960f GIT binary patch literal 1276 zcmezO_TO6u1_mZ5W@O+hNi8nXP0YzmEM{O}OjTL=XFE`?-k{cikBv*4jgf^>i%F1? zk(GfZ`?F{4vBFug6<%MmmXJ+)mEgQoslXV6!5_H^yana6V1?kw1w zbE0Bi&GHlLHzfosgjrwL)qKcc5I;l0LF?W2lpQg%-cHp$l(#o)?Jkath1@gQN{hG8 zig@wKv#0R7vd_QC=JG%%Ffy=4=$RT=0v*d`(8R=M(8RcU0W%XL6BCP-)w&Y~JZv0V zZ64=rS(uqv84M~6g$xAPm_u3EggJBalM{0?@{3DgVjNh+*s+LlVG-lTBF2m)W*{fd zYiMC$VQ64zW@K(?5e4L0B5?=MWswHLZ0z7LVq$~_7BeF|vl9agPmO-znfkD()@R+bGrT$(AXmN24#zv*XRX z#$UNu(Lmln78u;Jd@N!tBKmU@J0!OJc3G%!N>OO@P1n+F-CorAVRmOQaA8six!iWP z)M3lXpnJ*TI=kIlH(Yxia-ls?xvctEx&P5B6()tKm`>%bo~@fX9{j%TtMU1G!|pw& zZ6BRjIqQ^`bIxR@OmMno&8^H%tpq36Esh&T(+MJ_la+#pV>+3scS% + + 4.0.0 + jdbc-supplier + 1.0.0.BUILD-SNAPSHOT + jdbc-supplier + JDBC supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-jdbc + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.cloud + spring-cloud-function-context + ${spring-cloud-function.version} + + + io.pivotal.java.function + splitter-function + ${project.version} + + + org.springframework.boot + spring-boot-configuration-processor + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + com.h2database + h2 + test + + + io.projectreactor + reactor-test + test + + + + diff --git a/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierConfiguration.java b/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierConfiguration.java new file mode 100644 index 00000000..ec6a59ee --- /dev/null +++ b/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierConfiguration.java @@ -0,0 +1,84 @@ +/* + * Copyright 2019-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.jdbc; + +import io.pivotal.java.function.splitter.function.SplitterFunctionConfiguration; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; +import javax.sql.DataSource; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.function.context.PollableBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.jdbc.JdbcPollingChannelAdapter; +import org.springframework.messaging.Message; +import reactor.core.publisher.Flux; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@Configuration +@EnableConfigurationProperties(JdbcSupplierProperties.class) +@Import(SplitterFunctionConfiguration.class) +public class JdbcSupplierConfiguration { + + private final JdbcSupplierProperties properties; + + private final DataSource dataSource; + + public JdbcSupplierConfiguration(JdbcSupplierProperties properties, DataSource dataSource) { + this.properties = properties; + this.dataSource = dataSource; + } + + @Bean + public MessageSource jdbcMessageSource() { + JdbcPollingChannelAdapter jdbcPollingChannelAdapter = + new JdbcPollingChannelAdapter(this.dataSource, this.properties.getQuery()); + jdbcPollingChannelAdapter.setMaxRows(this.properties.getMaxRows()); + jdbcPollingChannelAdapter.setUpdateSql(this.properties.getUpdate()); + return jdbcPollingChannelAdapter; + } + + @Bean(name = "jdbcSupplier") + @PollableBean(splittable = true) + @ConditionalOnProperty(prefix = "jdbc.supplier", name = "split", matchIfMissing = true) + public Supplier>> splittedSupplier(Function, List>> splitterFunction) { + return () -> { + Message received = jdbcMessageSource().receive(); + if (received != null) { + return Flux.fromIterable(splitterFunction.apply(received)); // multiple Message> + } + else { + return Flux.empty(); + } + }; + } + + @Bean + @ConditionalOnProperty(prefix = "jdbc.supplier", name = "split", havingValue = "false") + public Supplier> jdbcSupplier() { + return () -> jdbcMessageSource().receive(); + } + +} diff --git a/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierProperties.java b/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierProperties.java new file mode 100644 index 00000000..6b6d14fc --- /dev/null +++ b/functions/supplier/jdbc-supplier/src/main/java/org/springframework/cloud/fn/supplier/jdbc/JdbcSupplierProperties.java @@ -0,0 +1,85 @@ +/* + * Copyright 2019-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 + * + * http://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.jdbc; + +import javax.validation.constraints.NotNull; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@ConfigurationProperties("jdbc.supplier") +@Validated +public class JdbcSupplierProperties { + + /** + * The query to use to select data. + */ + private String query; + + /** + * An SQL update statement to execute for marking polled messages as 'seen'. + */ + private String update; + + /** + * Whether to split the SQL result as individual messages. + */ + private boolean split = true; + + /** + * Max numbers of rows to process for query. + */ + private int maxRows = 0; + + @NotNull + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public String getUpdate() { + return update; + } + + public void setUpdate(String update) { + this.update = update; + } + + public boolean isSplit() { + return split; + } + + public void setSplit(boolean split) { + this.split = split; + } + + public int getMaxRows() { + return maxRows; + } + + public void setMaxRows(int maxRows) { + this.maxRows = maxRows; + } + +} diff --git a/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/DefaultJdbcSupplierTests.java b/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/DefaultJdbcSupplierTests.java new file mode 100644 index 00000000..49e3a24a --- /dev/null +++ b/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/DefaultJdbcSupplierTests.java @@ -0,0 +1,81 @@ +/* + * Copyright 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.jdbc; + +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = "jdbc.supplier.query=select id, name from test order by id") +@DirtiesContext +public class DefaultJdbcSupplierTests { + + @Autowired + Supplier>> jdbcSupplier; + + @Test + void testExtraction() { + final Flux> messageFlux = jdbcSupplier.get(); + StepVerifier stepVerifier = + StepVerifier.create(messageFlux) + .assertNext((message) -> + assertThat(message) + .satisfies((msg) -> assertThat(msg) + .extracting(Message::getPayload) + .matches(o -> { + Map map = (Map)o; + return map.get("ID").equals(1L) && map.get("NAME").equals("Bob"); + }) + )) + .assertNext((message) -> + assertThat(message) + .satisfies((msg) -> assertThat(msg) + .extracting(Message::getPayload) + .matches(o -> { + Map map = (Map)o; + return map.get("ID").equals(2L) && map.get("NAME").equals("Jane"); + }) + )) + .assertNext((message) -> + assertThat(message) + .satisfies((msg) -> assertThat(msg) + .extracting(Message::getPayload) + .matches(o -> { + Map map = (Map)o; + return map.get("ID").equals(3L) && map.get("NAME").equals("John"); + }) + )) + .thenCancel() + .verifyLater(); + stepVerifier.verify(); + } + + @SpringBootApplication + static class TestApplication { + } +} diff --git a/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/NonSplitJdbcSupplierTests.java b/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/NonSplitJdbcSupplierTests.java new file mode 100644 index 00000000..60e000b0 --- /dev/null +++ b/functions/supplier/jdbc-supplier/src/test/java/org/springframework/cloud/fn/supplier/jdbc/NonSplitJdbcSupplierTests.java @@ -0,0 +1,62 @@ +/* + * Copyright 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.jdbc; + +import java.util.List; +import java.util.Map; +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import org.springframework.test.annotation.DirtiesContext; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, + properties = {"jdbc.supplier.query=select id, name from test order by id", "jdbc.supplier.split=false"}) +@DirtiesContext +public class NonSplitJdbcSupplierTests { + + @Autowired + Supplier> jdbcSupplier; + + @Test + void testExtraction() { + final Message message = jdbcSupplier.get(); + final List> payload = (List>) message.getPayload(); + assertThat(payload.size()).isEqualTo(3); + Map map = payload.get(0); + assertThat(map.get("ID")).isEqualTo(1L); + assertThat(map.get("NAME")).isEqualTo("Bob"); + map = payload.get(1); + assertThat(map.get("ID")).isEqualTo(2L); + assertThat(map.get("NAME")).isEqualTo("Jane"); + map = payload.get(2); + assertThat(map.get("ID")).isEqualTo(3L); + assertThat(map.get("NAME")).isEqualTo("John"); + } + + @SpringBootApplication + static class TestApplication { + } +} diff --git a/functions/supplier/jdbc-supplier/src/test/resources/schema.sql b/functions/supplier/jdbc-supplier/src/test/resources/schema.sql new file mode 100644 index 00000000..bb5835cb --- /dev/null +++ b/functions/supplier/jdbc-supplier/src/test/resources/schema.sql @@ -0,0 +1,10 @@ +-- Run by default by Boot infrastructure + +create table test( + id bigint, + name varchar (2000), + tag char(1) +); +insert into test values (1, 'Bob', NULL); +insert into test values (2, 'Jane', NULL); +insert into test values (3, 'John', NULL); \ No newline at end of file diff --git a/functions/supplier/mongodb-supplier/.gitignore b/functions/supplier/mongodb-supplier/.gitignore new file mode 100644 index 00000000..a2a3040a --- /dev/null +++ b/functions/supplier/mongodb-supplier/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/functions/supplier/mongodb-supplier/pom.xml b/functions/supplier/mongodb-supplier/pom.xml new file mode 100644 index 00000000..15845cc1 --- /dev/null +++ b/functions/supplier/mongodb-supplier/pom.xml @@ -0,0 +1,68 @@ + + + 4.0.0 + mongodb-supplier + 1.0.0.BUILD-SNAPSHOT + mongodb-supplier + Mongo DB supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.integration + spring-integration-mongodb + + + org.mongodb + mongodb-driver-sync + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.cloud + spring-cloud-function-context + ${spring-cloud-function.version} + + + org.springframework.cloud.fn + splitter-function + ${project.version} + + + org.springframework.boot + spring-boot-starter-test + test + + + org.junit.vintage + junit-vintage-engine + + + + + io.projectreactor + reactor-test + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + diff --git a/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierConfiguration.java b/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierConfiguration.java new file mode 100644 index 00000000..a82a8c68 --- /dev/null +++ b/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierConfiguration.java @@ -0,0 +1,99 @@ +/* + * Copyright 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.fn.supplier.mongo; + +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.cloud.fn.splitter.SplitterFunctionConfiguration; +import org.springframework.context.annotation.Configuration; +import reactor.core.publisher.Flux; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.function.context.PollableBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.expression.Expression; +import org.springframework.expression.common.LiteralExpression; +import org.springframework.integration.mongodb.inbound.MongoDbMessageSource; +import org.springframework.messaging.Message; + +/** + * A configuration for MongoDB Source applications. Produces + * {@link MongoDbMessageSource} which polls collection with the query after startup + * according to the polling properties. + * + * @author Adam Zwickey + * @author Artem Bilan + * @author David Turanski + * + */ +@Configuration +@EnableConfigurationProperties({ MongodbSupplierProperties.class }) +@Import(SplitterFunctionConfiguration.class) +public class MongodbSupplierConfiguration { + + private final MongodbSupplierProperties properties; + + private final MongoTemplate mongoTemplate; + + public MongodbSupplierConfiguration(MongodbSupplierProperties properties, MongoTemplate mongoTemplate) { + this.properties = properties; + this.mongoTemplate = mongoTemplate; + } + + @Bean(name = "mongodbSupplier") + @PollableBean(splittable = true) + @ConditionalOnProperty(prefix = "mongodb", name = "split", matchIfMissing = true) + public Supplier>> splittedSupplier(Function, List>> splitterFunction) { + return () -> { + Message received = mongoSource().receive(); + if (received != null) { + return Flux.fromIterable(splitterFunction.apply(received)); // multiple Message> + } + else { + return Flux.empty(); + } + }; + } + + @Bean + @ConditionalOnProperty(prefix = "mongodb", name = "split", havingValue = "false") + public Supplier> mongodbSupplier() { + return () -> mongoSource().receive(); + } + + /** + * The inheritors can consider to override this method for their purpose or just adjust + * options for the returned instance + * @return a {@link MongoDbMessageSource} instance + */ + @Bean + public MongoDbMessageSource mongoSource() { + Expression queryExpression = (this.properties.getQueryExpression() != null + ? this.properties.getQueryExpression() + : new LiteralExpression(this.properties.getQuery())); + MongoDbMessageSource mongoDbMessageSource = new MongoDbMessageSource(this.mongoTemplate, queryExpression); + mongoDbMessageSource.setCollectionNameExpression(new LiteralExpression(this.properties.getCollection())); + mongoDbMessageSource.setEntityClass(String.class); + return mongoDbMessageSource; + } + +} diff --git a/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierProperties.java b/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierProperties.java new file mode 100644 index 00000000..31ba05e6 --- /dev/null +++ b/functions/supplier/mongodb-supplier/src/main/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierProperties.java @@ -0,0 +1,91 @@ +/* + * Copyright 2016-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.fn.supplier.mongo; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotEmpty; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.expression.Expression; +import org.springframework.validation.annotation.Validated; + +/** + * @author Adam Zwickey + * @author Artem Bilan + * @author Chris Schaefer + * @author David Turanski + * + */ +@ConfigurationProperties("mongodb.supplier") +@Validated +public class MongodbSupplierProperties { + + /** + * The MongoDB collection to query + */ + private String collection; + + /** + * The MongoDB query + */ + private String query = "{ }"; + + /** + * The SpEL expression in MongoDB query DSL style + */ + private Expression queryExpression; + + /** + * Whether to split the query result as individual messages. + */ + private boolean split = true; + + @NotEmpty(message = "Query is required") + public String getQuery() { + return query; + } + + public void setQuery(String query) { + this.query = query; + } + + public Expression getQueryExpression() { + return queryExpression; + } + + public void setQueryExpression(Expression queryExpression) { + this.queryExpression = queryExpression; + } + + public void setCollection(String collection) { + this.collection = collection; + } + + @NotBlank(message = "Collection name is required") + public String getCollection() { + return collection; + } + + public boolean isSplit() { + return split; + } + + public void setSplit(boolean split) { + this.split = split; + } + +} diff --git a/functions/supplier/mongodb-supplier/src/main/resources/application.properties b/functions/supplier/mongodb-supplier/src/main/resources/application.properties new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/functions/supplier/mongodb-supplier/src/main/resources/application.properties @@ -0,0 +1 @@ + diff --git a/functions/supplier/mongodb-supplier/src/test/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierApplicationTests.java b/functions/supplier/mongodb-supplier/src/test/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierApplicationTests.java new file mode 100644 index 00000000..0c332137 --- /dev/null +++ b/functions/supplier/mongodb-supplier/src/test/java/org/springframework/cloud/fn/supplier/mongo/MongodbSupplierApplicationTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019-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.mongo; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Supplier; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.messaging.Message; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.entry; + +@SpringBootTest(properties = { + "spring.data.mongodb.port=0", + "mongodb.supplier.collection=testing"}) +class MongodbSupplierApplicationTests { + + private ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired + private Supplier>> mongodbSupplier; + + @Autowired + private MongoClient mongo; + + @BeforeEach + public void setUp() { + MongoDatabase database = this.mongo.getDatabase("test"); + database.createCollection("testing"); + MongoCollection collection = database.getCollection("testing"); + collection.insertOne( + new Document("greeting", "hello") + .append("name", "foo")); + collection.insertOne( + new Document("greeting", "hola") + .append("name", "bar")); + } + + @Test + void testMongodbSupplier() { + Flux> messageFlux = this.mongodbSupplier.get(); + StepVerifier.create(messageFlux) + .assertNext((message) -> + assertThat(payload(message)).contains( + entry("greeting","hello"), + entry("name", "foo"))) + .assertNext((message) -> + assertThat(payload(message)).contains( + entry("greeting","hola"), + entry("name", "bar"))) + .thenCancel() + .verify(); + } + + private Map payload(Message message) { + Map map = null; + try { + map = objectMapper.readValue(message.getPayload().toString(),HashMap.class); + } + catch (Exception e) { + e.printStackTrace(); + } + return map; + } + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/supplier/time-supplier/pom.xml b/functions/supplier/time-supplier/pom.xml new file mode 100644 index 00000000..6b871e62 --- /dev/null +++ b/functions/supplier/time-supplier/pom.xml @@ -0,0 +1,45 @@ + + + 4.0.0 + time-supplier + 1.0.0.BUILD-SNAPSHOT + time-supplier + time supplier + + + org.springframework.cloud.fn + spring-functions-parent + 1.0.0.BUILD-SNAPSHOT + ../../spring-functions-parent + + + + + org.springframework.boot + spring-boot-starter + + + org.apache.commons + commons-lang3 + + + + org.hibernate.validator + hibernate-validator + true + + + + org.springframework.boot + spring-boot-configuration-processor + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/DateFormat.java b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/DateFormat.java new file mode 100644 index 00000000..77ff2986 --- /dev/null +++ b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/DateFormat.java @@ -0,0 +1,81 @@ +/* + * Copyright 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 + * + * http://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.time; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.text.SimpleDateFormat; + +import javax.validation.Constraint; +import javax.validation.ConstraintValidator; +import javax.validation.ConstraintValidatorContext; +import javax.validation.Payload; + +/** + * The annotated String must be a valid {@link java.text.SimpleDateFormat} pattern. + * + * @author Eric Bottard + * @author Soby Chacko + */ +@Target({ ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, + ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Constraint(validatedBy = { DateFormat.DateFormatValidator.class }) +public @interface DateFormat { + + String DEFAULT_MESSAGE = ""; + + String message() default DEFAULT_MESSAGE; + + Class[] groups() default { }; + + Class[] payload() default { }; + + public static class DateFormatValidator implements ConstraintValidator { + + private String message; + + @Override + public void initialize(DateFormat constraintAnnotation) { + this.message = constraintAnnotation.message(); + } + + @Override + public boolean isValid(CharSequence value, ConstraintValidatorContext context) { + if (value == null) { + return true; + } + try { + new SimpleDateFormat(value.toString()); + } + catch (IllegalArgumentException e) { + if (DEFAULT_MESSAGE.equals(this.message)) { + context.disableDefaultConstraintViolation(); + context.buildConstraintViolationWithTemplate(e.getMessage()).addConstraintViolation(); + } + return false; + } + return true; + } + + } + +} diff --git a/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeProperties.java b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeProperties.java new file mode 100644 index 00000000..3c59377c --- /dev/null +++ b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeProperties.java @@ -0,0 +1,43 @@ +/* + * Copyright 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 + * + * http://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.time; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.validation.annotation.Validated; + +/** + * @author Soby Chacko + */ +@ConfigurationProperties("time") +@Validated +public class TimeProperties { + + /** + * Format for the date value. + */ + private String dateFormat = "MM/dd/yy HH:mm:ss"; + + @DateFormat + public String getDateFormat() { + return this.dateFormat; + } + + public void setDateFormat(String dateFormat) { + this.dateFormat = dateFormat; + } + +} diff --git a/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeSupplierConfiguration.java b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeSupplierConfiguration.java new file mode 100644 index 00000000..40224405 --- /dev/null +++ b/functions/supplier/time-supplier/src/main/java/org/springframework/cloud/fn/supplier/time/TimeSupplierConfiguration.java @@ -0,0 +1,41 @@ +/* + * Copyright 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 + * + * http://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.time; + +import java.util.Date; +import java.util.function.Supplier; + +import org.apache.commons.lang3.time.FastDateFormat; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Soby Chacko + */ +@Configuration +@EnableConfigurationProperties(TimeProperties.class) +public class TimeSupplierConfiguration { + + @Bean + public Supplier timeSupplier(TimeProperties timeProperties) { + FastDateFormat fastDateFormat = FastDateFormat.getInstance(timeProperties.getDateFormat()); + return () -> fastDateFormat.format(new Date()); + } + +} diff --git a/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/SimpleTimeSupplierTests.java b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/SimpleTimeSupplierTests.java new file mode 100644 index 00000000..4e426863 --- /dev/null +++ b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/SimpleTimeSupplierTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 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 + * + * http://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.time; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +public class SimpleTimeSupplierTests extends TimeSupplierApplicationTests { + + @Test + public void testTimeSupplier() { + final String time = timeSupplier.get(); + SimpleDateFormat dateFormat = new SimpleDateFormat(new TimeProperties().getDateFormat()); + assertThatCode(() -> { + Date date = dateFormat.parse(time); + assertThat(date).isNotNull(); + }).doesNotThrowAnyException(); + } + +} diff --git a/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/TimeSupplierApplicationTests.java b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/TimeSupplierApplicationTests.java new file mode 100644 index 00000000..df379308 --- /dev/null +++ b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/TimeSupplierApplicationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 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 + * + * http://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.time; + +import java.util.function.Supplier; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@SpringBootTest +public abstract class TimeSupplierApplicationTests { + + @Autowired + Supplier timeSupplier; + + @Autowired + TimeProperties timeProperties; + + protected abstract void testTimeSupplier(); + + @SpringBootApplication + static class TestApplication {} +} diff --git a/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/VariationToSimpleTests.java b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/VariationToSimpleTests.java new file mode 100644 index 00000000..bcbe99d8 --- /dev/null +++ b/functions/supplier/time-supplier/src/test/java/org/springframework/cloud/fn/supplier/time/VariationToSimpleTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 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 + * + * http://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.time; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +/** + * @author Soby Chacko + * @author Artem Bilan + */ +@SpringBootTest({ "time.dateFormat=MMddyyyy HH:mm:ss" }) +public class VariationToSimpleTests extends TimeSupplierApplicationTests { + + @Test + public void testTimeSupplier() { + final String time = timeSupplier.get(); + SimpleDateFormat dateFormat = new SimpleDateFormat(timeProperties.getDateFormat()); + assertThatCode(() -> { + Date date = dateFormat.parse(time); + assertThat(date).isNotNull(); + }).doesNotThrowAnyException(); + } + + @Test + public void testInvalidDateFormat() { + TimeProperties timeProperties = new TimeProperties(); + timeProperties.setDateFormat("AA/dd/yyyy HH:mm:ss"); + assertThatIllegalArgumentException().isThrownBy(() -> new SimpleDateFormat(timeProperties.getDateFormat())); + } + +} diff --git a/mvnw b/mvnw new file mode 100755 index 00000000..a08b219e --- /dev/null +++ b/mvnw @@ -0,0 +1,253 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven2 Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # + # Look for the Apple JDKs first to preserve the existing behaviour, and then look + # for the new JDKs provided by Oracle. + # + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then + # + # Apple JDKs + # + export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then + # + # Oracle JDKs + # + export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home + fi + + if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then + # + # Apple JDKs + # + export JAVA_HOME=`/usr/libexec/java_home` + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Migwn, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" + # TODO classpath? +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + local basedir=$(pwd) + local wdir=$(pwd) + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + wdir=$(cd "$wdir/.."; pwd) + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +echo "Running version check" +VERSION=$( sed '\!//' -e 's!.*$!!' ) +echo "The found version is [${VERSION}]" + +if echo $VERSION | egrep -q 'M|RC'; then + echo Activating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pmilestone" +else + echo Deactivating \"milestone\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pmilestone//') +fi + +if echo $VERSION | egrep -q 'RELEASE'; then + echo Activating \"central\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q milestone || MAVEN_ARGS="$MAVEN_ARGS -Pcentral" +else + echo Deactivating \"central\" profile for version=\"$VERSION\" + echo $MAVEN_ARGS | grep -q central && MAVEN_ARGS=$(echo $MAVEN_ARGS | sed -e 's/-Pcentral//') +fi + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} ${MAVEN_ARGS} "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 00000000..b26ab24f --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM http://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven2 Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..93cb7271 --- /dev/null +++ b/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + org.springframework.cloud.stream.app + stream-applications + 3.0.0.BUILD-SNAPSHOT + stream-applications + Functions and Infrastructure for stream applications + pom + + + functions + applications + + +