From e46cbfdcb4f0fe1bcb8efa2f29454adefaa360f6 Mon Sep 17 00:00:00 2001 From: Soby Chacko Date: Mon, 18 Mar 2013 23:31:53 -0400 Subject: [PATCH] INTEXT-44 - Add Kafka Support * Add Inbound adapter * Add Outbound adapter * Add serialization * Initial Documentation * Add unit tests * Add async producer * Support Kafka 0.8 * Add Kafka Sample See: https://jira.springsource.org/browse/INTEXT-44 --- samples/kafka/.gitignore | 2 + samples/kafka/README.md | 5 + samples/kafka/build.gradle | 46 ++ samples/kafka/gradle.properties | 7 + .../kafka/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 39860 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + samples/kafka/gradlew | 164 +++++++ samples/kafka/gradlew.bat | 90 ++++ .../samples/kafka/inbound/InboundRunner.java | 27 ++ .../kafka/outbound/CustomPartitioner.java | 37 ++ .../kafka/outbound/OutboundRunner.java | 55 +++ .../outbound/PartitionlessTransformer.java | 56 +++ .../kafka/src/main/resources/log4j.properties | 10 + ...kafkaInboundAdapterParserTests-context.xml | 56 +++ ...afkaOutboundAdapterParserTests-context.xml | 45 ++ spring-integration-kafka/.gitignore | 8 + spring-integration-kafka/README.md | 294 ++++++++++++ spring-integration-kafka/build.gradle | 304 ++++++++++++ spring-integration-kafka/gradle.properties | 1 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 39770 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + spring-integration-kafka/gradlew | 164 +++++++ spring-integration-kafka/gradlew.bat | 90 ++++ spring-integration-kafka/publish-maven.gradle | 61 +++ .../src/api/overview.html | 22 + spring-integration-kafka/src/dist/license.txt | 201 ++++++++ spring-integration-kafka/src/dist/notice.txt | 21 + .../xml/KafkaConsumerContextParser.java | 122 +++++ .../xml/KafkaInboundChannelAdapterParser.java | 49 ++ .../config/xml/KafkaNamespaceHandler.java | 38 ++ .../KafkaOutboundChannelAdapterParser.java | 45 ++ .../xml/KafkaProducerContextParser.java | 89 ++++ .../config/xml/ZookeeperConnectParser.java | 46 ++ .../kafka/config/xml/package-info.java | 4 + .../kafka/core/KafkaConsumerDefaults.java | 40 ++ .../kafka/core/ZookeeperConnectDefaults.java | 31 ++ .../integration/kafka/core/package-info.java | 4 + .../KafkaHighLevelConsumerMessageSource.java | 47 ++ .../kafka/inbound/package-info.java | 4 + .../outbound/KafkaProducerMessageHandler.java | 41 ++ .../integration/kafka/package-info.java | 4 + .../avro/AvroBackedKafkaDecoder.java | 54 +++ .../avro/AvroBackedKafkaEncoder.java | 52 ++ .../kafka/serializer/avro/AvroSerializer.java | 52 ++ .../avro/AvroSpecificDatumSerializer.java | 52 ++ .../serializer/common/StringEncoder.java | 41 ++ .../support/ConsumerConfigFactoryBean.java | 59 +++ .../kafka/support/ConsumerConfiguration.java | 209 ++++++++ .../support/ConsumerConnectionProvider.java | 35 ++ .../kafka/support/ConsumerMetadata.java | 174 +++++++ .../kafka/support/DefaultPartitioner.java | 36 ++ .../kafka/support/KafkaConsumerContext.java | 76 +++ .../kafka/support/KafkaProducerContext.java | 63 +++ .../kafka/support/MessageLeftOverTracker.java | 44 ++ .../kafka/support/ProducerConfiguration.java | 102 ++++ .../kafka/support/ProducerFactoryBean.java | 74 +++ .../kafka/support/ProducerMetadata.java | 139 ++++++ .../kafka/support/ZookeeperConnect.java | 60 +++ .../kafka/support/package-info.java | 4 + .../main/resources/META-INF/spring.handlers | 1 + .../main/resources/META-INF/spring.schemas | 2 + .../main/resources/META-INF/spring.tooling | 4 + .../xml/spring-integration-kafka-1.0.xsd | 451 ++++++++++++++++++ .../config/xml/spring-integration-kafka.gif | Bin 0 -> 572 bytes .../docbook/SIAdapterLowerPrefix.xml | 72 +++ .../src/reference/docbook/history.xml | 4 + .../src/reference/docbook/images/logo.png | Bin 0 -> 9627 bytes .../src/reference/docbook/index.xml | 67 +++ .../src/reference/docbook/resources.xml | 15 + .../src/reference/docbook/whats-new.xml | 8 + .../xml/KafkaConsumerContextParserTests.java | 47 ++ .../xml/KafkaInboundAdapterParserTests.java | 45 ++ .../xml/KafkaOutboundAdapterParserTests.java | 46 ++ .../xml/KafkaProducerContextParserTests.java | 73 +++ .../xml/ZookeeperConnectParserTests.java | 59 +++ .../AvroBackedKafkaSerializerTest.java | 59 +++ .../support/ConsumerConfigurationTests.java | 306 ++++++++++++ .../support/KafkaConsumerContextTest.java | 80 ++++ .../support/ProducerConfigurationTests.java | 303 ++++++++++++ .../support/ProducerFactoryBeanTests.java | 63 +++ .../test/utils/NonSerializableTestKey.java | 35 ++ .../utils/NonSerializableTestPayload.java | 34 ++ .../integration/kafka/test/utils/TestKey.java | 38 ++ .../kafka/test/utils/TestObject.java | 38 ++ .../kafka/test/utils/TestPayload.java | 38 ++ .../src/test/resources/log4j.properties | 8 + ...afkaConsumerContextParserTests-context.xml | 30 ++ .../xml/kafkaInboundAdapterCommon-context.xml | 17 + ...kafkaInboundAdapterParserTests-context.xml | 40 ++ ...afkaOutboundAdapterParserTests-context.xml | 44 ++ ...afkaProducerContextParserTests-context.xml | 26 + .../zookeeperConnectParserTests-context.xml | 14 + 92 files changed, 5735 insertions(+) create mode 100644 samples/kafka/.gitignore create mode 100644 samples/kafka/README.md create mode 100644 samples/kafka/build.gradle create mode 100644 samples/kafka/gradle.properties create mode 100644 samples/kafka/gradle/wrapper/gradle-wrapper.jar create mode 100644 samples/kafka/gradle/wrapper/gradle-wrapper.properties create mode 100755 samples/kafka/gradlew create mode 100644 samples/kafka/gradlew.bat create mode 100644 samples/kafka/src/main/java/org/springframework/integration/samples/kafka/inbound/InboundRunner.java create mode 100644 samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/CustomPartitioner.java create mode 100644 samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/OutboundRunner.java create mode 100644 samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/PartitionlessTransformer.java create mode 100644 samples/kafka/src/main/resources/log4j.properties create mode 100644 samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/inbound/kafkaInboundAdapterParserTests-context.xml create mode 100644 samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/outbound/kafkaOutboundAdapterParserTests-context.xml create mode 100644 spring-integration-kafka/.gitignore create mode 100644 spring-integration-kafka/README.md create mode 100644 spring-integration-kafka/build.gradle create mode 100644 spring-integration-kafka/gradle.properties create mode 100644 spring-integration-kafka/gradle/wrapper/gradle-wrapper.jar create mode 100644 spring-integration-kafka/gradle/wrapper/gradle-wrapper.properties create mode 100755 spring-integration-kafka/gradlew create mode 100644 spring-integration-kafka/gradlew.bat create mode 100644 spring-integration-kafka/publish-maven.gradle create mode 100644 spring-integration-kafka/src/api/overview.html create mode 100644 spring-integration-kafka/src/dist/license.txt create mode 100644 spring-integration-kafka/src/dist/notice.txt create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParser.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaInboundChannelAdapterParser.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaNamespaceHandler.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaOutboundChannelAdapterParser.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParser.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParser.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/package-info.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/KafkaConsumerDefaults.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/ZookeeperConnectDefaults.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/package-info.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/KafkaHighLevelConsumerMessageSource.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/package-info.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/outbound/KafkaProducerMessageHandler.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/package-info.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaDecoder.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaEncoder.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSerializer.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSpecificDatumSerializer.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/common/StringEncoder.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfigFactoryBean.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfiguration.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConnectionProvider.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerMetadata.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/DefaultPartitioner.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaConsumerContext.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaProducerContext.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/MessageLeftOverTracker.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerConfiguration.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerFactoryBean.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerMetadata.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ZookeeperConnect.java create mode 100644 spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/package-info.java create mode 100644 spring-integration-kafka/src/main/resources/META-INF/spring.handlers create mode 100644 spring-integration-kafka/src/main/resources/META-INF/spring.schemas create mode 100644 spring-integration-kafka/src/main/resources/META-INF/spring.tooling create mode 100644 spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd create mode 100644 spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka.gif create mode 100644 spring-integration-kafka/src/reference/docbook/SIAdapterLowerPrefix.xml create mode 100644 spring-integration-kafka/src/reference/docbook/history.xml create mode 100644 spring-integration-kafka/src/reference/docbook/images/logo.png create mode 100644 spring-integration-kafka/src/reference/docbook/index.xml create mode 100644 spring-integration-kafka/src/reference/docbook/resources.xml create mode 100644 spring-integration-kafka/src/reference/docbook/whats-new.xml create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParserTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundAdapterParserTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParserTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParserTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/serializer/AvroBackedKafkaSerializerTest.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/KafkaConsumerContextTest.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerConfigurationTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerFactoryBeanTests.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestKey.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestPayload.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestKey.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestObject.java create mode 100644 spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestPayload.java create mode 100644 spring-integration-kafka/src/test/resources/log4j.properties create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaConsumerContextParserTests-context.xml create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterCommon-context.xml create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterParserTests-context.xml create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaOutboundAdapterParserTests-context.xml create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaProducerContextParserTests-context.xml create mode 100644 spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/zookeeperConnectParserTests-context.xml diff --git a/samples/kafka/.gitignore b/samples/kafka/.gitignore new file mode 100644 index 0000000..38b0df2 --- /dev/null +++ b/samples/kafka/.gitignore @@ -0,0 +1,2 @@ +derby.log + diff --git a/samples/kafka/README.md b/samples/kafka/README.md new file mode 100644 index 0000000..d4cb9b3 --- /dev/null +++ b/samples/kafka/README.md @@ -0,0 +1,5 @@ +Spring Integration Kafka Sample +================================ + +This sample demonstrates the usage of the *[Spring Integration Kafka][]* adapters. + diff --git a/samples/kafka/build.gradle b/samples/kafka/build.gradle new file mode 100644 index 0000000..12748bf --- /dev/null +++ b/samples/kafka/build.gradle @@ -0,0 +1,46 @@ +description = 'Spring Integration Kafka Sample' +apply plugin: 'base' +apply plugin: 'java' +apply plugin: 'eclipse' +apply plugin: 'application' +apply plugin: 'idea' +apply plugin: 'maven' + +repositories { + mavenLocal() + mavenCentral() + maven { url "http://repo.springsource.org/libs-snapshot" } + maven { url 'http://repo.springsource.org/plugins-release' } +} + +dependencies { + compile "org.springframework:spring-beans:3.1.3.RELEASE" + compile "org.springframework:spring-context:3.1.3.RELEASE" + compile "org.springframework:spring-expression:3.1.3.RELEASE" + compile "org.springframework.integration:spring-integration-stream:2.2.0.RELEASE" + compile("org.springframework.integration:spring-integration-kafka:0.5.0.BUILD-SNAPSHOT") { + exclude module: 'log4j' + } +compile("log4j:log4j:1.2.15") { + exclude module: 'jms' + exclude module: 'jmx' + exclude module: 'jmxtools' + exclude module: 'jmxri' + + } + + compile "commons-logging:commons-logging:1.1.1" +} + +task wrapper(type: Wrapper) { + description = 'Generates gradlew[.bat] scripts' + gradleVersion = '1.3' +} + +eclipse { + project { + name = "spring-integration-kafka-sample" + } +} + +defaultTasks 'clean', 'build' diff --git a/samples/kafka/gradle.properties b/samples/kafka/gradle.properties new file mode 100644 index 0000000..d03e972 --- /dev/null +++ b/samples/kafka/gradle.properties @@ -0,0 +1,7 @@ +springVersion = 3.1.3.RELEASE +springIntegrationVersion = 2.2.0.RELEASE +springIntegrationKafkaVersion = 0.5.0.BUILD-SNAPSHOT +version = 0.5.0.BUILD-SNAPSHOT + + + diff --git a/samples/kafka/gradle/wrapper/gradle-wrapper.jar b/samples/kafka/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e3b3376e6002f9d44543761c8d3dcd199eff1d82 GIT binary patch literal 39860 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%0A3!wsG4uToG`VrIW5Id|4fPyx?=aMz8G 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/samples/kafka/gradlew.bat b/samples/kafka/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/samples/kafka/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/inbound/InboundRunner.java b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/inbound/InboundRunner.java new file mode 100644 index 0000000..0fdf9a8 --- /dev/null +++ b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/inbound/InboundRunner.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2013 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.integration.samples.kafka.inbound; + +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class InboundRunner { + private static final String CONFIG = "kafkaInboundAdapterParserTests-context.xml"; + + public static void main(final String args[]) { + final ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONFIG, InboundRunner.class); + ctx.start(); + } +} diff --git a/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/CustomPartitioner.java b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/CustomPartitioner.java new file mode 100644 index 0000000..eff5c22 --- /dev/null +++ b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/CustomPartitioner.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2013 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.integration.samples.kafka.outbound; + +import kafka.producer.Partitioner; + +/** + * @author Soby Chacko + * + * This class is for internal use only and therefore is at default access level + */ +class CustomPartitioner implements Partitioner { + /** + * Uses the key to calculate a partition bucket id for routing + * the data to the appropriate broker partition + * @return an integer between 0 and numPartitions-1 + */ + @Override + public int partition(final T key, final int numPartitions) { + final String s = (String) key; + final Integer i = Integer.parseInt(s); + return i % numPartitions; + } +} diff --git a/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/OutboundRunner.java b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/OutboundRunner.java new file mode 100644 index 0000000..fcb1c35 --- /dev/null +++ b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/OutboundRunner.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2013 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.integration.samples.kafka.outbound; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.integration.MessageChannel; +import org.springframework.integration.support.MessageBuilder; + +public class OutboundRunner { + private static final String CONFIG = "kafkaOutboundAdapterParserTests-context.xml"; + private static final Log LOG = LogFactory.getLog(OutboundRunner.class); + + public static void main(final String args[]) { + final ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(CONFIG, OutboundRunner.class); + ctx.start(); + + final MessageChannel channel = ctx.getBean("inputToKafka", MessageChannel.class); + LOG.info(channel.getClass()); + + //sending 100,000 messages to Kafka server for topic test1 + for (int i = 0; i < 500; i++) { + channel.send( + MessageBuilder.withPayload("hello Fom ob adapter test1 - " + i) + .setHeader("messageKey", String.valueOf(i)) + .setHeader("topic", "test1").build()); + + LOG.info("message sent " + i); + } + + //sending 5,000 messages to kafka server for topic test2 + for (int i = 0; i < 50; i++) { + channel.send( + MessageBuilder.withPayload("hello Fom ob adapter test2 - " + i) + .setHeader("messageKey", String.valueOf(i)) + .setHeader("topic", "test2").build()); + + LOG.info("message sent " + i); + } + } +} diff --git a/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/PartitionlessTransformer.java b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/PartitionlessTransformer.java new file mode 100644 index 0000000..49470ad --- /dev/null +++ b/samples/kafka/src/main/java/org/springframework/integration/samples/kafka/outbound/PartitionlessTransformer.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2013 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.integration.samples.kafka.outbound; + +import org.springframework.integration.Message; +import org.springframework.integration.support.MessageBuilder; +import org.springframework.integration.transformer.Transformer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class PartitionlessTransformer implements Transformer { + @Override + @SuppressWarnings("unchecked") + public Message transform(final Message message) { + + final Map>> origData = + (Map>>) message.getPayload(); + + final Map> nonPartitionedData = new HashMap>(); + + for(final String topic : origData.keySet()) { + final Map> partitionedData = origData.get(topic); + final Collection> nonPartitionedDataFromTopic = partitionedData.values(); + + final List mergedList = new ArrayList(); + + for (final List l : nonPartitionedDataFromTopic){ + mergedList.addAll(l); + } + + nonPartitionedData.put(topic, mergedList); + } + + return MessageBuilder.withPayload(nonPartitionedData).build(); + } +} diff --git a/samples/kafka/src/main/resources/log4j.properties b/samples/kafka/src/main/resources/log4j.properties new file mode 100644 index 0000000..3c84b67 --- /dev/null +++ b/samples/kafka/src/main/resources/log4j.properties @@ -0,0 +1,10 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m%n + +#log4j.category.org.springframework.integration=WARN +log4j.category.org.springframework.integration.kafka=INFO +log4j.category.org.springframework.integration.samples.kafka=INFO +log4j.category.kafka.consumer=ERROR diff --git a/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/inbound/kafkaInboundAdapterParserTests-context.xml b/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/inbound/kafkaInboundAdapterParserTests-context.xml new file mode 100644 index 0000000..a6e97d2 --- /dev/null +++ b/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/inbound/kafkaInboundAdapterParserTests-context.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/outbound/kafkaOutboundAdapterParserTests-context.xml b/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/outbound/kafkaOutboundAdapterParserTests-context.xml new file mode 100644 index 0000000..2c3926b --- /dev/null +++ b/samples/kafka/src/main/resources/org/springframework/integration/samples/kafka/outbound/kafkaOutboundAdapterParserTests-context.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/.gitignore b/spring-integration-kafka/.gitignore new file mode 100644 index 0000000..0c7fdb9 --- /dev/null +++ b/spring-integration-kafka/.gitignore @@ -0,0 +1,8 @@ +.classpath +.project +.settings/ +.gradle +build +target/ +bin/ +derby.log diff --git a/spring-integration-kafka/README.md b/spring-integration-kafka/README.md new file mode 100644 index 0000000..f2cf2d4 --- /dev/null +++ b/spring-integration-kafka/README.md @@ -0,0 +1,294 @@ +Spring Integration Kafka Adapter +================================================= + + +Welcome to the *Spring Integration Kafka adapter*. Apache Kafka is a distributed publish-subscribe messaging system that is designed for handling terra bytes of high throughput +data at constant time. For more information on Kafka and its design goals, please see [Kafka main page](http://kafka.apache.org/) + +Spring Integration Kafka adapters are built for Kafka 0.8 and since 0.8 is not backward compatible with any previous versions, Spring Integration will not +support any Kafka versions prior to 0.8. As of this writing, Kafka 0.8 is still WIP. + +Checking out and building +----------------------------- + +Currently Spring Integration Kafka adapter is built against kafka 0.8 that is backed by +Scala 2.9.2. + +In order to build the project: + + ./gradlew build + +In order to install this into your local maven cache: + + ./gradlew install + +Spring Integration Kafka project currently supports the following two components. Please keep in mind that +this is very early stage in development and do not yet fully make use of all the features that Kafka provides. + +* Outbound Channel Adapter +* Inbound Channel Adapter based on the High level consumer API + +Outbound Channel Adapter: +-------------------------------------------- + +The Outbound channel adapter is used to send messages to Kafka. Messages are read from a Spring Integration channel. One can specify this channel in the application context and then wire +this in the application where messages are sent to kafka. + +Once a channel is configured, then messages can be sent to Kafka through this channel. Obviously, Spring Integration specific messages are sent to the adapter and then it will +internally get converted into Kafka messages before sending. In the current version of the outbound adapter, +you have to specify a message key and the topic as header values and the message to send as the payload. +Here is an example. + +```java + final MessageChannel channel = ctx.getBean("inputToKafka", MessageChannel.class); + + channel.send( + MessageBuilder.withPayload(payload). + setHeader("messageKey", "key") + .setHeader("topic", "test").build()); +``` + +This would create a message with a payload. In addition to this, it also creates two header entries as key/value pairs - one for +the message key and another for the topic that this message belongs to. + +Here is how kafka outbound channel adapter is configured: + +```xml + + + +``` + +The key aspect in this configuration is the producer-context-ref. Producer context contains all the producer configuration for all the topics that this adapter is expected to handle. +A channel in which messages are arriving is configured with the adapter and therefore +any message sent to that channel will be handled by this adapter. You can also configure a poller +depending on the +type of the channel used. For example, in the above configuration, we use a queue based channel +and thus a poller is configured with a task executor. If no messages are available in the queue it will timeout immediately because of +the receive-timeout configuration. Then it will poll again with a delay of 1 second. + +Producer context is at the heart of the kafka outbound adapter. Here is an example of how it is configured. + +```xml + + + + + + +``` + +There are a few things going on here. So, lets go one by one. First of all, producer context is simply a holder of, as the name +indicates, a context for the Kafa producer. It contains one ore more producer configurations. Each producer configuration +is ultimately gets translated into a Kafka native producer. Each producer configuration is per topic based right now. +If you go by the above example, there are two producers generated from this configuration - one for topic named +test1 and another for test2. Each producer can take the following: + + broker-list list of comma separated brokers that this producer connects to + topic topic name + compression-codec any compression to be used. Default is no compression. Supported compression codec are gzip and snappy. Anything else would + result in no compression + value-encoder serializer to be used for encoding messages. + key-encoder serializer to be used for encoding the partition key + key-class-type Type of the key class. This will be ignored if no key-encoder is provided + value-class-type The type of the value class. This will be ignored if no value-encoder is provided. + partitioner custom implementation of a Kafka Partitioner interface. + async true/false - default is false. Setting this to true would make the Kafka producer to use + an async producer + batch-num-messages number of messages to batch at the producer. If async is false, then this has no effect. + +The value-encoder and key-encoder are referring to other spring beans. They are essentially implementations of an +interface provided by Kafka, the Encoder interface. Similarly, partitioner also refers a Spring bean which implements +the Kafka Partitioner interface. + +Here is an example of configuring an encoder. + +```xml + + + +``` + +Spring Integration Kafaka adapter provides Apache Avro backed encoders out of the box, as this is a popular choice +for serialization in the big data spectrum. If no encoders are specified as beans, the default encoders provided +by Kafka will be used. On that not, if the encoder is configured only for the message and not for the key, the same encoder +will be used for both. These are standard Kafka behaviors. Spring Integration Kafka adapter does simply enforce those behaviours. +Kafka default encoder expects the data to come as byte arrays and it is a no-op encoder, i.e. it just takes the byte array as it is. +When default encoders are used, there are two ways a message can be sent. +Either, the sender of the message to the channel +can simply put byte arrays as message key and payload. Or, the key and value can be sent as Java Serializable object. +In the latter case, the Kafka adapter will automatically convert them to byte arrays before sending to Kafka broker. +If the encoders are default and the objets sent are not serializalbe, then that would cause an error. By providing explicit encoders +it is totally up to the developer to configure how the objects are serialized. In that case, the objects may or may not implement +the Serializable interface. + +Encoding String for key and value is a very common use case and Kafka provides a StringEncoder out of the box. It takes a Kafka specific VerifiableProperties object + along with its +constructor that wraps a regular Java.util.Properties object. The StringEncoder is great when writing a + direct Java client that talks to Kafka. +However, when using Spring Integration Kafka adapter, it introduces unnecessary steps to create these +properties objects. Therefore, we provide a wrapper class for this same StringEncoder as part of the SI kafka support, which makes +using it from Spring a bit easier. You can inject +any properties to it in the Spring way. Kafka StringEncoder looks at a specific property for the type of encoding scheme used. +In the wrapper bean provided, this property can simply be injected as a value without constructing any other objects. Spring Integration provided StringEncoder is available +in the package org.springframework.integration.kafka.serializer.common.StringEncoder. The avro support for serialization is +also available in a package called avro under serializer. + +Inbound Channel Adapter: +-------------------------------------------- + +The Inbound channel adapter is used to consume messages from Kafka. These messages will be placed into a channel as Spring Integration specific Messages. +Kafka provides two types of consumer API's primarily. One is called the high level consumer and the other is the Simple Consumer. Highlevel consumer is +pretty complex inside. Nonetheless, for the client, using the high level API is straightforward. Although easy to use, High level consumer +does not provide any offset management. So, if you want to rewind and re-fetch messages, it is not possible to do so using the +high level consumer API. Offsets are managed by the Zookeeper internally in the high level consumer. If your use case does not require any offset management +or re-reading messages from the same consumer, then high level consumer is a perfect fit. Spring Integration Kafka inbound channel adapter +currently supports only the high level consumer. Here are the details of configuring one. + +```xml + + + +``` + +Since this inbound channel adapter uses a Polling Channel under the hood, it must be configured with a Poller. A notable difference +between the poller configured with this inbound adapter and other pollers is that the receive-timeout specified here +does not have any effect. The reason for this is because of the way Kafka implements iterators on the consumer stream. +It is using a BlockingQueue internally and thus it would wait indefinitely. Instead of interrupting the underlying thread, +we are leveraging on direct Kafka support for consumer time out. It is configured on the consumer context. Everything else + is pretty much the same as in a regular inbound adapter. Any messages that it receives will be sent to the channel configured with it. + +Inbound Kafka Adapter must specify a kafka-consumer-context-ref element and here is how it is configured: + +```xml + + + + + + + + +``` + +Consumer context requires a reference to a zookeeper-connect which dictates all the zookeeper specific configuration details. +Here is how a zookeeper-connect is configured. + +```xml + +``` + +zk-connect attribute is where you would specify the zookeeper connection. All the other attributes get translated into their +zookeeper counter-part attributes by the consumer. + +In the above consumer context, you can also specify a consumer-timeout value which would be used to +timeout the consumer in case of no messages to consume. +This timeout would be applicable to all the streams (threads) in the consumer. +The default value for this in Kafka is -1 which would make it wait +indefinitely. However, Sping Integration overrides it to be 5 seconds in order to make sure that no +threads are blocking indefinitely in the lifecycle of the application and thereby +giving them a chance to free up any resources or locks that they hold. It is recommended to +override this value so as to meet any specific use case requirements. +By providing a reasonable consumer-timeout and a fixed-delay value on the poller, +this inbound adapter is capable of simulating a message driven behaviour. + +consumer context takes consumer-configurations which are at the center piece of the inbound adapter. It is a group of one or more +consumer-configuration elements which consists of a consumer group dictated by the group-id. Each consumer-configuration +can be configured with one or more kafka-topic. + +In the above example provided, we have a single consumer-configuration that consumes messages from two topics each having 4 streams. + These streams are fundamentally same as the number of partitions that a topic is configured + with in the producer. For instance, if you configure your topic with +4 partitions, then the maximum number of streams that you may have in the consumer is also 4. +Any more than this would be a no-op. +If you have less number of streams than the available partitions, then messages from +multiple partitions will be sent to available streams. +Therefore, it is a good practice to limit the number of streams for a topic in the consumer +configuration to the number of partitions configured for the topic. There may be situations +in which a partition may be gone during runtime and in that case the stream receiving +data from the partition will simply timeout and whenever this partition comes back, +it would start read data from it again. + +Consumer configuration can also be configured with optional decoders for key and value. +The default ones provided by Kafka are basically no-ops and would consume as byte arrays. +If you provide an encoder for key/value in the producer, then it is recommended to provide +corresponding decoders. +Spring Integration Kafka adapter gives Apache Avro based data serialization components +out of the box. You can use any serialization component for this purpose. +Here is how you would configure a kafka decoder bean that is Avro backed. + +```xml + + + +``` + +Another important attribute for the consumer-configuration is the max-messages. +Please note that this is different from the max-messages-per-poll configured on the inbound adapter +element. +There it means the number of times the receive method called on the adapter. +The max-messages on consumer configuration is different. Kafka is used mainly for big data purposes +and usually that means the influx of large amount of data constantly. Because of this, +each time a receive is invoked on the adapter, you would basically get a collection of messages. +The maximum number of messages to retrieve for a topic in each execution of the +receive is what configured through the max-messages attribute on the consumer-configuration. +Basically, if the use case is to receive a constant stream of +large number of data, simply specifying a receive-timeout alone would not be enough. +You would also need to specify the max number of messages to receive. + +The type of the payload of the Message returned by the adapter is the following: + +```java +Map>> +``` + +It is a java.util.Map that contains the topic string consumed as the key and another Map as the value. +The inner map's key is the stream (partition) number and value is a list of message payloads. +The reason for this complex return type is +due to the way Kafka orders the messages. In the high level consumer, +all the messages received in a single stream for a single partition +are guaranteed to be in order. For example, if I have a topic named test configured with +4 partitions and I have 4 corresponding streams +in the consumer, then I would receive data in all the consumer streams in the same order +as they were put in the corresponding partitions. This is another reason to set the number of +consumer streams for a topic same +as the number of broker partitions configured for that topic. Lets say that the number of streams +are less than the number of partitions. Then, normally, there is no +guarantee for any order other than just the fact that a single stream will contain messages +from multiple partitions and the messages from a given single partition received will +still be kept contiguously. By that time probably there is no way to find out which set of messages came from which partition. +By providing this complex map that contains the partition information for the topic, we make sure that the order sent by the producer +is preserved even if the number of streams used was less than the number of broker partitions. + +A downstream component which receives the data from the inbound adapter can cast the SI payload to the above +Map. + +If your use case does not require ordering of messages during consumption, then you can easily pass this +payload to a standard SI transformer and just get a full dump of the actual payload sent by Kafka. + + + + + diff --git a/spring-integration-kafka/build.gradle b/spring-integration-kafka/build.gradle new file mode 100644 index 0000000..b4da5f7 --- /dev/null +++ b/spring-integration-kafka/build.gradle @@ -0,0 +1,304 @@ +description = 'Spring Integration Kafka Adapter' + +buildscript { + repositories { + maven { url 'https://repo.springsource.org/plugins-snapshot' } + } + dependencies { + classpath 'org.springframework.build.gradle:docbook-reference-plugin:0.1.5' + } +} + +apply plugin: 'java' +apply from: "${rootProject.projectDir}/publish-maven.gradle" +apply plugin: 'eclipse' +apply plugin: 'idea' + +group = 'org.springframework.integration' + +repositories { + mavenLocal() + mavenCentral() + maven { url 'http://repo.springsource.org/libs-milestone' } + maven { url 'http://repo.springsource.org/plugins-release' } + maven { url 'https://repo.springsource.org/libs-snapshot' } +} + +sourceCompatibility=1.6 +targetCompatibility=1.6 + +ext { + junitVersion = '4.10' + log4jVersion = '1.2.12' + mockitoVersion = '1.9.0' + springVersion = '3.1.3.RELEASE' + springIntegrationVersion = '2.2.0.RELEASE' + + idPrefix = 'kafka' +} + +eclipse { + project { + natures += 'org.springframework.ide.eclipse.core.springnature' + } +} + +sourceSets { + test { + resources { + srcDirs = ['src/test/resources', 'src/test/java'] + } + } +} + +// See http://www.gradle.org/docs/current/userguide/dependency_management.html#sub:configurations +// and http://www.gradle.org/docs/current/dsl/org.gradle.api.artifacts.ConfigurationContainer.html +configurations { + jacoco //Configuration Group used by Sonar to provide Code Coverage using JaCoCo +} + +dependencies { + compile "org.springframework:spring-beans:$springVersion" + compile "org.springframework:spring-context:$springVersion" + compile "org.springframework:spring-expression:$springVersion" + compile "org.springframework.integration:spring-integration-core:$springIntegrationVersion" + compile("org.apache:kafka_2.9.2:0.8.0-SNAPSHOT") { + exclude module: 'log4j' + } + compile("org.apache.zookeeper:zookeeper:3.3.4") { + exclude module: 'log4j' + exclude module: 'jline' + } + compile "org.scala-lang:scala-library:2.9.2" + compile "org.scala-lang:scala-compiler:2.9.2" + compile("log4j:log4j:1.2.15") { + exclude module: 'jms' + exclude module: 'jmx' + exclude module: 'jmxtools' + exclude module: 'jmxri' + + } + compile "org.slf4j:slf4j-simple:1.6.4" + compile "net.sf.jopt-simple:jopt-simple:3.2" + compile "org.xerial.snappy:snappy-java:1.0.4.1" + //runtime "zkclient:zkclient:20120522" + + runtime "com.101tec:zkclient:0.2" + runtime "com.yammer.metrics:metrics-core:2.2.0" + runtime "com.yammer.metrics:metrics-annotation:2.2.0" + + compile "org.apache.avro:avro:1.7.3" + compile "org.apache.avro:avro-compiler:1.7.3" + + testCompile "org.springframework.integration:spring-integration-test:$springIntegrationVersion" + testCompile "junit:junit-dep:$junitVersion" + testCompile("log4j:log4j:$log4jVersion") { + exclude module: 'jms' + exclude module: 'jmx' + exclude module: 'jmxtools' + exclude module: 'jmxri' +} + testCompile "org.mockito:mockito-all:$mockitoVersion" + testCompile "org.springframework:spring-test:$springVersion" + testCompile "org.springframework.integration:spring-integration-stream:$springIntegrationVersion" + jacoco group: "org.jacoco", name: "org.jacoco.agent", version: "0.5.6.201201232323", classifier: "runtime" +} + + +// enable all compiler warnings; individual projects may customize further +ext.xLintArg = '-Xlint:all' +[compileJava, compileTestJava]*.options*.compilerArgs = [xLintArg] + +test { + // suppress all console output during testing unless running `gradle -i` + logging.captureStandardOutput(LogLevel.INFO) + jvmArgs "-javaagent:${configurations.jacoco.asPath}=destfile=${buildDir}/jacoco.exec,includes=*" +} + +task sourcesJar(type: Jar) { + classifier = 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + classifier = 'javadoc' + from javadoc +} + +artifacts { + archives sourcesJar + archives javadocJar +} + +apply plugin: 'docbook-reference' + +reference { + sourceDir = file('src/reference/docbook') +} + +apply plugin: 'sonar' + +sonar { + + if (rootProject.hasProperty('sonarHostUrl')) { + server.url = rootProject.sonarHostUrl + } + + database { + if (rootProject.hasProperty('sonarJdbcUrl')) { + url = rootProject.sonarJdbcUrl + } + if (rootProject.hasProperty('sonarJdbcDriver')) { + driverClassName = rootProject.sonarJdbcDriver + } + if (rootProject.hasProperty('sonarJdbcUsername')) { + username = rootProject.sonarJdbcUsername + } + if (rootProject.hasProperty('sonarJdbcPassword')) { + password = rootProject.sonarJdbcPassword + } + } + + project { + dynamicAnalysis = "reuseReports" + withProjectProperties { props -> + props["sonar.core.codeCoveragePlugin"] = "jacoco" + props["sonar.jacoco.reportPath"] = "${buildDir.name}/jacoco.exec" + } + } + + logger.info("Sonar parameters used: server.url='${server.url}'; database.url='${database.url}'; database.driverClassName='${database.driverClassName}'; database.username='${database.username}'") +} + +task api(type: Javadoc) { + group = 'Documentation' + description = 'Generates the Javadoc API documentation.' + title = "${rootProject.description} ${version} API" + options.memberLevel = org.gradle.external.javadoc.JavadocMemberLevel.PROTECTED + options.author = true + options.header = rootProject.description + options.overview = 'src/api/overview.html' + + source = sourceSets.main.allJava + classpath = project.sourceSets.main.compileClasspath + destinationDir = new File(buildDir, "api") +} + +task schemaZip(type: Zip) { + group = 'Distribution' + classifier = 'schema' + description = "Builds -${classifier} archive containing all " + + "XSDs for deployment at static.springframework.org/schema." + + def Properties schemas = new Properties(); + def shortName = idPrefix.replaceFirst("${idPrefix}-", '') + + project.sourceSets.main.resources.find { + it.path.endsWith('META-INF/spring.schemas') + }?.withInputStream { schemas.load(it) } + + for (def key : schemas.keySet()) { + File xsdFile = project.sourceSets.main.resources.find { + it.path.endsWith(schemas.get(key)) + } + assert xsdFile != null + into ("integration/${shortName}") { + from xsdFile.path + } + } + +} + +task docsZip(type: Zip) { + group = 'Distribution' + classifier = 'docs' + description = "Builds -${classifier} archive containing api and reference " + + "for deployment at static.springframework.org/spring-integration/docs." + + from('src/dist') { + include 'changelog.txt' + } + + from (api) { + into 'api' + } + + from (reference) { + into 'reference' + } +} + +task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) { + group = 'Distribution' + classifier = 'dist' + description = "Builds -${classifier} archive, containing all jars and docs, " + + "suitable for community download page." + + ext.baseDir = "${project.name}-${project.version}"; + + from('src/dist') { + include 'readme.txt' + include 'license.txt' + include 'notice.txt' + into "${baseDir}" + } + + from(zipTree(docsZip.archivePath)) { + into "${baseDir}/docs" + } + + from(zipTree(schemaZip.archivePath)) { + into "${baseDir}/schema" + } + + into ("${baseDir}/libs") { + from project.jar + from project.sourcesJar + from project.javadocJar + } +} + +// Create an optional "with dependencies" distribution. +// Not published by default; only for use when building from source. +task depsZip(type: Zip, dependsOn: distZip) { zipTask -> + group = 'Distribution' + classifier = 'dist-with-deps' + description = "Builds -${classifier} archive, containing everything " + + "in the -${distZip.classifier} archive plus all dependencies." + + from zipTree(distZip.archivePath) + + gradle.taskGraph.whenReady { taskGraph -> + if (taskGraph.hasTask(":${zipTask.name}")) { + def projectName = rootProject.name + def artifacts = new HashSet() + + rootProject.configurations.runtime.resolvedConfiguration.resolvedArtifacts.each { artifact -> + def dependency = artifact.moduleVersion.id + if (!projectName.equals(dependency.name)) { + artifacts << artifact.file + } + } + + zipTask.from(artifacts) { + into "${distZip.baseDir}/deps" + } + } + } +} + +artifacts { + archives distZip + archives docsZip + archives schemaZip +} + +task dist(dependsOn: assemble) { + group = 'Distribution' + description = 'Builds -dist, -docs and -schema distribution archives.' +} + +task wrapper(type: Wrapper) { + description = 'Generates gradlew[.bat] scripts' + gradleVersion = '1.3' +} diff --git a/spring-integration-kafka/gradle.properties b/spring-integration-kafka/gradle.properties new file mode 100644 index 0000000..5364470 --- /dev/null +++ b/spring-integration-kafka/gradle.properties @@ -0,0 +1 @@ +version=0.5.0.BUILD-SNAPSHOT diff --git a/spring-integration-kafka/gradle/wrapper/gradle-wrapper.jar b/spring-integration-kafka/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..81dcde627d5cfbc12c96dc5113aac79b66a15118 GIT binary patch literal 39770 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 \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" +APP_HOME="`pwd -P`" +cd "$SAVED" + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +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 + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query businessSystem maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/spring-integration-kafka/gradlew.bat b/spring-integration-kafka/gradlew.bat new file mode 100644 index 0000000..8a0b282 --- /dev/null +++ b/spring-integration-kafka/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/spring-integration-kafka/publish-maven.gradle b/spring-integration-kafka/publish-maven.gradle new file mode 100644 index 0000000..5532ec4 --- /dev/null +++ b/spring-integration-kafka/publish-maven.gradle @@ -0,0 +1,61 @@ +apply plugin: 'maven' + +ext.optionalDeps = [] +ext.providedDeps = [] + +ext.optional = { optionalDeps << it } +ext.provided = { providedDeps << it } + +install { + repositories.mavenInstaller { + customizePom(pom, project) + } +} + +def customizePom(pom, gradleProject) { + pom.whenConfigured { generatedPom -> + // respect 'optional' and 'provided' dependencies + gradleProject.optionalDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.optional = true + } + gradleProject.providedDeps.each { dep -> + generatedPom.dependencies.find { it.artifactId == dep.name }?.scope = 'provided' + } + + // eliminate test-scoped dependencies (no need in maven central poms) + generatedPom.dependencies.removeAll { dep -> + dep.scope == 'test' + } + + // add all items necessary for maven central publication + generatedPom.project { + name = gradleProject.description + description = gradleProject.description + url = 'https://github.com/SpringSource/spring-integration-extensions' + organization { + name = 'SpringSource' + url = 'http://springsource.org' + } + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/licenses/LICENSE-2.0.txt' + distribution 'repo' + } + } + scm { + url = 'https://github.com/SpringSource/spring-integration-extensions' + connection = 'scm:git:git://github.com/SpringSource/spring-integration-extensions' + developerConnection = 'scm:git:git://github.com/SpringSource/spring-integration-extensions' + } + + developers { + developer { + id = 'not specified' + name = 'Soby Chacko' + email = 'not specified' + } + } + } + } +} diff --git a/spring-integration-kafka/src/api/overview.html b/spring-integration-kafka/src/api/overview.html new file mode 100644 index 0000000..d9e5faa --- /dev/null +++ b/spring-integration-kafka/src/api/overview.html @@ -0,0 +1,22 @@ + + +This document is the API specification for Spring Integration Kafka Extension +
+
+

+ For further API reference and developer documentation, see the + Spring + Integration reference documentation. + That documentation contains more detailed, developer-targeted + descriptions, with conceptual overviews, definitions of terms, + workarounds, and working code examples. +

+ +

+ If you are interested in commercial training, consultancy, and + support for Spring Integration, please visit + http://www.springsource.com +

+
+ + diff --git a/spring-integration-kafka/src/dist/license.txt b/spring-integration-kafka/src/dist/license.txt new file mode 100644 index 0000000..c94ec85 --- /dev/null +++ b/spring-integration-kafka/src/dist/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://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 testData1) 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 testData1 and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [testData1 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 + + 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. diff --git a/spring-integration-kafka/src/dist/notice.txt b/spring-integration-kafka/src/dist/notice.txt new file mode 100644 index 0000000..f62045a --- /dev/null +++ b/spring-integration-kafka/src/dist/notice.txt @@ -0,0 +1,21 @@ + ======================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, in this case for the Spring Integration distribution. == + ======================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternatively, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring Integration" must + not be used to endorse or promote products derived from this software + without prior written permission. For written permission, please contact + enquiries@springsource.com. diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParser.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParser.java new file mode 100644 index 0000000..3eac811 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParser.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.kafka.support.ConsumerConfigFactoryBean; +import org.springframework.integration.kafka.support.ConsumerConfiguration; +import org.springframework.integration.kafka.support.ConsumerConnectionProvider; +import org.springframework.integration.kafka.support.ConsumerMetadata; +import org.springframework.integration.kafka.support.KafkaConsumerContext; +import org.springframework.integration.kafka.support.MessageLeftOverTracker; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +import java.util.HashMap; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class KafkaConsumerContextParser extends AbstractSingleBeanDefinitionParser { + + @Override + protected Class getBeanClass(final Element element) { + return KafkaConsumerContext.class; + } + + @Override + protected void doParse(final Element element, final ParserContext parserContext, final BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + final Element consumerConfigurations = DomUtils.getChildElementByTagName(element, "consumer-configurations"); + parseConsumerConfigurations(consumerConfigurations, parserContext, builder, element); + } + + private void parseConsumerConfigurations(final Element consumerConfigurations, final ParserContext parserContext, + final BeanDefinitionBuilder builder, final Element parentElem) { + for (final Element consumerConfiguration : DomUtils.getChildElementsByTagName(consumerConfigurations, "consumer-configuration")) { + final BeanDefinitionBuilder consumerConfigurationBuilder = BeanDefinitionBuilder.genericBeanDefinition(ConsumerConfiguration.class); + final BeanDefinitionBuilder consumerMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(ConsumerMetadata.class); + + IntegrationNamespaceUtils.setValueIfAttributeDefined(consumerMetadataBuilder, consumerConfiguration, "group-id"); + + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(consumerMetadataBuilder, consumerConfiguration, "value-decoder"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(consumerMetadataBuilder, consumerConfiguration, "key-decoder"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(consumerMetadataBuilder, consumerConfiguration, "key-class-type"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(consumerMetadataBuilder, consumerConfiguration, "value-class-type"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(consumerConfigurationBuilder, consumerConfiguration, "max-messages"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(consumerMetadataBuilder, parentElem, "consumer-timeout"); + + final Map topicStreamsMap = new HashMap(); + + for (final Element topicConfiguration : DomUtils.getChildElementsByTagName(consumerConfiguration, "topic")) { + final String topic = topicConfiguration.getAttribute("id"); + final String streams = topicConfiguration.getAttribute("streams"); + final Integer streamsInt = Integer.valueOf(streams); + topicStreamsMap.put(topic, streamsInt); + } + + consumerMetadataBuilder.addPropertyValue("topicStreamMap", topicStreamsMap); + + final BeanDefinition consumerMetadataBeanDef = consumerMetadataBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(consumerMetadataBeanDef, "consumerMetadata_" + consumerConfiguration.getAttribute("group-id")), + parserContext.getRegistry()); + + final String zookeeperConnectBean = parentElem.getAttribute("zookeeper-connect"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, parentElem, zookeeperConnectBean); + + final BeanDefinitionBuilder consumerConfigFactoryBuilder = BeanDefinitionBuilder.genericBeanDefinition(ConsumerConfigFactoryBean.class); + consumerConfigFactoryBuilder.addConstructorArgReference("consumerMetadata_" + consumerConfiguration.getAttribute("group-id")); + + if (StringUtils.hasText(zookeeperConnectBean)) { + consumerConfigFactoryBuilder.addConstructorArgReference(zookeeperConnectBean); + } + + final BeanDefinition consumerConfigFactoryBuilderBeanDefinition = consumerConfigFactoryBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(consumerConfigFactoryBuilderBeanDefinition, "consumerConfigFactory_" + consumerConfiguration.getAttribute("group-id")), parserContext.getRegistry()); + + final BeanDefinitionBuilder consumerConnectionProviderBuilder = BeanDefinitionBuilder.genericBeanDefinition(ConsumerConnectionProvider.class); + consumerConnectionProviderBuilder.addConstructorArgReference("consumerConfigFactory_" + consumerConfiguration.getAttribute("group-id")); + + final BeanDefinition consumerConnectionProviderBuilderBeanDefinition = consumerConnectionProviderBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(consumerConnectionProviderBuilderBeanDefinition, "consumerConnectionProvider_" + consumerConfiguration.getAttribute("group-id")), parserContext.getRegistry()); + + + final BeanDefinitionBuilder messageLeftOverBeanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MessageLeftOverTracker.class); + final BeanDefinition messageLeftOverBeanDefinition = messageLeftOverBeanDefinitionBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(messageLeftOverBeanDefinition, "messageLeftOver_" + consumerConfiguration.getAttribute("group-id")), + parserContext.getRegistry()); + + consumerConfigurationBuilder.addConstructorArgReference("consumerMetadata_" + consumerConfiguration.getAttribute("group-id")); + consumerConfigurationBuilder.addConstructorArgReference("consumerConnectionProvider_" + consumerConfiguration.getAttribute("group-id")); + consumerConfigurationBuilder.addConstructorArgReference("messageLeftOver_" + consumerConfiguration.getAttribute("group-id")); + + final AbstractBeanDefinition consumerConfigurationBeanDefinition = consumerConfigurationBuilder.getBeanDefinition(); + + final String consumerConfigurationBeanName = "consumerConfiguration_" + consumerConfiguration.getAttribute("group-id"); + registerBeanDefinition(new BeanDefinitionHolder(consumerConfigurationBeanDefinition, consumerConfigurationBeanName), + parserContext.getRegistry()); + } + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaInboundChannelAdapterParser.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaInboundChannelAdapterParser.java new file mode 100644 index 0000000..cc52b1e --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaInboundChannelAdapterParser.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.beans.BeanMetadataElement; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractPollingInboundChannelAdapterParser; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.kafka.inbound.KafkaHighLevelConsumerMessageSource; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * The Kafka Inbound Channel adapter parser + * + * @author Soby Chacko + * + */ +public class KafkaInboundChannelAdapterParser extends AbstractPollingInboundChannelAdapterParser { + @Override + protected BeanMetadataElement parseSource(final Element element, final ParserContext parserContext) { + final BeanDefinitionBuilder highLevelConsumerMessageSourceBuilder = + BeanDefinitionBuilder.genericBeanDefinition(KafkaHighLevelConsumerMessageSource.class); + + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(highLevelConsumerMessageSourceBuilder, element, "kafka-decoder"); + + final String kafkaConsumerContext = element.getAttribute("kafka-consumer-context-ref"); + + if (StringUtils.hasText(kafkaConsumerContext)) { + highLevelConsumerMessageSourceBuilder.addConstructorArgReference(kafkaConsumerContext); + } + + return highLevelConsumerMessageSourceBuilder.getBeanDefinition(); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaNamespaceHandler.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaNamespaceHandler.java new file mode 100644 index 0000000..92d7823 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaNamespaceHandler.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.integration.config.xml.AbstractIntegrationNamespaceHandler; + +/** + * The namespace handler for the Kafka namespace + * + * @author Soby Chacko + * + */ +public class KafkaNamespaceHandler extends AbstractIntegrationNamespaceHandler { + /* (non-Javadoc) + * @see org.springframework.beans.factory.xml.NamespaceHandler#init() + */ + @Override + public void init() { + registerBeanDefinitionParser("zookeeper-connect", new ZookeeperConnectParser()); + registerBeanDefinitionParser("inbound-channel-adapter", new KafkaInboundChannelAdapterParser()); + registerBeanDefinitionParser("outbound-channel-adapter", new KafkaOutboundChannelAdapterParser()); + registerBeanDefinitionParser("producer-context", new KafkaProducerContextParser()); + registerBeanDefinitionParser("consumer-context", new KafkaConsumerContextParser()); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaOutboundChannelAdapterParser.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaOutboundChannelAdapterParser.java new file mode 100644 index 0000000..714dad3 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaOutboundChannelAdapterParser.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.AbstractOutboundChannelAdapterParser; +import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +/** + * + * @author Soby Chacko + * + */ +public class KafkaOutboundChannelAdapterParser extends AbstractOutboundChannelAdapterParser { + @Override + protected AbstractBeanDefinition parseConsumer(final Element element, final ParserContext parserContext) { + final BeanDefinitionBuilder kafkaProducerMessageHandlerBuilder = + BeanDefinitionBuilder.genericBeanDefinition(KafkaProducerMessageHandler.class); + + final String kafkaServerBeanName = element.getAttribute("kafka-producer-context-ref"); + + if (StringUtils.hasText(kafkaServerBeanName)) { + kafkaProducerMessageHandlerBuilder.addConstructorArgReference(kafkaServerBeanName); + } + + return kafkaProducerMessageHandlerBuilder.getBeanDefinition(); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParser.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParser.java new file mode 100644 index 0000000..02ed893 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParser.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.kafka.support.KafkaProducerContext; +import org.springframework.integration.kafka.support.ProducerConfiguration; +import org.springframework.integration.kafka.support.ProducerFactoryBean; +import org.springframework.integration.kafka.support.ProducerMetadata; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + * @author Soby Chacko + */ +public class KafkaProducerContextParser extends AbstractSimpleBeanDefinitionParser { + @Override + protected Class getBeanClass(final Element element) { + return KafkaProducerContext.class; + } + + @Override + protected void doParse(final Element element, final ParserContext parserContext, final BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + final Element topics = DomUtils.getChildElementByTagName(element, "producer-configurations"); + parseProducerConfigurations(topics, parserContext); + } + + private void parseProducerConfigurations(final Element topics, final ParserContext parserContext) { + for (final Element producerConfiguration : DomUtils.getChildElementsByTagName(topics, "producer-configuration")){ + final BeanDefinitionBuilder producerConfigurationBuilder = BeanDefinitionBuilder.genericBeanDefinition(ProducerConfiguration.class); + + final BeanDefinitionBuilder producerMetadataBuilder = BeanDefinitionBuilder.genericBeanDefinition(ProducerMetadata.class); + producerMetadataBuilder.addConstructorArgValue(producerConfiguration.getAttribute("topic")); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "value-encoder"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "key-encoder"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "key-class-type"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "value-class-type"); + IntegrationNamespaceUtils.setReferenceIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "partitioner"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "compression-codec"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "async"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(producerMetadataBuilder, producerConfiguration, "batch-num-messages"); + + final BeanDefinition producerMetadataBeanDef = producerMetadataBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(producerMetadataBeanDef, "producerMetadata_" + producerConfiguration.getAttribute("topic")), + parserContext.getRegistry()); + + final BeanDefinitionBuilder producerFactoryBuilder = BeanDefinitionBuilder.genericBeanDefinition(ProducerFactoryBean.class); + producerFactoryBuilder.addConstructorArgReference("producerMetadata_" + producerConfiguration.getAttribute("topic")); + + final String brokerList = producerConfiguration.getAttribute("broker-list"); + if (StringUtils.hasText(brokerList)) { + producerFactoryBuilder.addConstructorArgValue(producerConfiguration.getAttribute("broker-list")); + } + + final BeanDefinition producerfactoryBeanDefinition = producerFactoryBuilder.getBeanDefinition(); + registerBeanDefinition(new BeanDefinitionHolder(producerfactoryBeanDefinition, "prodFactory_" + producerConfiguration.getAttribute("topic")), parserContext.getRegistry()); + + producerConfigurationBuilder.addConstructorArgReference("producerMetadata_" + producerConfiguration.getAttribute("topic")); + producerConfigurationBuilder.addConstructorArgReference("prodFactory_" + producerConfiguration.getAttribute("topic")); + + final AbstractBeanDefinition producerConfigurationBeanDefinition = producerConfigurationBuilder.getBeanDefinition(); + final String producerConfigurationBeanName = "producerConfiguration_" + producerConfiguration.getAttribute("topic"); + registerBeanDefinition(new BeanDefinitionHolder(producerConfigurationBeanDefinition, producerConfigurationBeanName), + parserContext.getRegistry()); + } + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParser.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParser.java new file mode 100644 index 0000000..cac01e9 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSimpleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.integration.config.xml.IntegrationNamespaceUtils; +import org.springframework.integration.kafka.support.ZookeeperConnect; +import org.w3c.dom.Element; + +/** + * @author Soby Chacko + */ +public class ZookeeperConnectParser extends AbstractSimpleBeanDefinitionParser { + @Override + protected Class getBeanClass(final Element element) { + return ZookeeperConnect.class; + } + + @Override + protected void doParse(final Element element, final ParserContext parserContext, final BeanDefinitionBuilder builder) { + super.doParse(element, parserContext, builder); + + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, + BeanDefinitionParserDelegate.SCOPE_ATTRIBUTE); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "zk-connect"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "zk-connection-timeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "zk-session-timeout"); + IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "zk-sync-time"); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/package-info.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/package-info.java new file mode 100644 index 0000000..3289f40 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/config/xml/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides parser classes to provide Xml namespace support for the Kafka components. + */ +package org.springframework.integration.kafka.config.xml; diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/KafkaConsumerDefaults.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/KafkaConsumerDefaults.java new file mode 100644 index 0000000..212678f --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/KafkaConsumerDefaults.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2013 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.integration.kafka.core; + +/** + * Kafka adapter specific message headers. + * + * @author Soby Chacko + */ +public class KafkaConsumerDefaults { + //High level consumer + public static final String GROUP_ID = "groupid"; + public static final String SOCKET_TIMEOUT = "30000"; + public static final String SOCKET_BUFFER_SIZE = "64*1024"; + public static final String FETCH_SIZE = "300 * 1024"; + public static final String BACKOFF_INCREMENT = "1000"; + public static final String QUEUED_CHUNKS_MAX = "100"; + public static final String AUTO_COMMIT_ENABLE = "true"; + public static final String AUTO_COMMIT_INTERVAL = "10000"; + public static final String AUTO_OFFSET_RESET = "smallest"; + //Overriding the default value of -1, which will make the consumer to wait indefinitely + public static final String CONSUMER_TIMEOUT = "5000"; + public static final String REBALANCE_RETRIES_MAX = "4"; + + private KafkaConsumerDefaults() { + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/ZookeeperConnectDefaults.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/ZookeeperConnectDefaults.java new file mode 100644 index 0000000..11138d1 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/ZookeeperConnectDefaults.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2013 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.integration.kafka.core; + +/** + * + * @author Soby Chacko + * + */ +public class ZookeeperConnectDefaults { + public static final String ZK_CONNECT = "localhost:2181"; + public static final String ZK_CONNECTION_TIMEOUT = "6000"; + public static final String ZK_SESSION_TIMEOUT = "6000"; + public static final String ZK_SYNC_TIME = "2000"; + + private ZookeeperConnectDefaults() { + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/package-info.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/package-info.java new file mode 100644 index 0000000..f404ec8 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/core/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides core classes of the Kafka module. + */ +package org.springframework.integration.kafka.core; diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/KafkaHighLevelConsumerMessageSource.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/KafkaHighLevelConsumerMessageSource.java new file mode 100644 index 0000000..acab37b --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/KafkaHighLevelConsumerMessageSource.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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.integration.kafka.inbound; + +import org.springframework.integration.Message; +import org.springframework.integration.context.IntegrationObjectSupport; +import org.springframework.integration.core.MessageSource; +import org.springframework.integration.kafka.support.KafkaConsumerContext; + +import java.util.List; +import java.util.Map; + +/** + * @author Soby Chacko + * + */ +public class KafkaHighLevelConsumerMessageSource extends IntegrationObjectSupport implements MessageSource>>> { + + private final KafkaConsumerContext kafkaConsumerContext; + + public KafkaHighLevelConsumerMessageSource(final KafkaConsumerContext kafkaConsumerContext) { + this.kafkaConsumerContext = kafkaConsumerContext; + } + + @Override + public Message>>> receive() { + return kafkaConsumerContext.receive(); + } + + @Override + public String getComponentType() { + return "kafka:inbound-channel-adapter"; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/package-info.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/package-info.java new file mode 100644 index 0000000..2688d7b --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/inbound/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides inbound Spring Integration Kafka components. + */ +package org.springframework.integration.kafka.inbound; diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/outbound/KafkaProducerMessageHandler.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/outbound/KafkaProducerMessageHandler.java new file mode 100644 index 0000000..ee4923b --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/outbound/KafkaProducerMessageHandler.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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.integration.kafka.outbound; + +import org.springframework.integration.Message; +import org.springframework.integration.handler.AbstractMessageHandler; +import org.springframework.integration.kafka.support.KafkaProducerContext; + +/** + * @author Soby Chacko + */ +public class KafkaProducerMessageHandler extends AbstractMessageHandler { + + private final KafkaProducerContext kafkaProducerContext; + + public KafkaProducerMessageHandler(final KafkaProducerContext kafkaProducerContext) { + this.kafkaProducerContext = kafkaProducerContext; + } + + public KafkaProducerContext getKafkaProducerContext() { + return kafkaProducerContext; + } + + @Override + protected void handleMessageInternal(final Message message) throws Exception { + kafkaProducerContext.send(message); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/package-info.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/package-info.java new file mode 100644 index 0000000..4847590 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/package-info.java @@ -0,0 +1,4 @@ +/** + * Root package of the Kafka Module. + */ +package org.springframework.integration.kafka; diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaDecoder.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaDecoder.java new file mode 100644 index 0000000..f131659 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaDecoder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer.avro; + + +import kafka.serializer.Decoder; +import org.apache.avro.Schema; +import org.apache.avro.reflect.ReflectData; + +import java.io.IOException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Soby Chacko + */ +public class AvroBackedKafkaDecoder implements Decoder { + private static final Log LOG = LogFactory.getLog(AvroBackedKafkaDecoder.class); + + private final Class clazz; + + public AvroBackedKafkaDecoder(final Class clazz) { + this.clazz = clazz; + } + + @Override + @SuppressWarnings("unchecked") + public T fromBytes(final byte[] bytes) { + final Schema schema = ReflectData.get().getSchema(clazz); + final AvroSerializer avroSerializer = new AvroSerializer(); + + try { + return (T) avroSerializer.deserialize(bytes, schema); + } catch (IOException e) { + LOG.error("Failed to decode byte array for schema: " + schema.getFullName(), e); + } + + return null; + } +} + diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaEncoder.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaEncoder.java new file mode 100644 index 0000000..8bc426b --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroBackedKafkaEncoder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer.avro; + +import kafka.serializer.Encoder; +import org.apache.avro.Schema; +import org.apache.avro.reflect.ReflectData; + +import java.io.IOException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Soby Chacko + */ +public class AvroBackedKafkaEncoder implements Encoder { + private static final Log LOG = LogFactory.getLog(AvroBackedKafkaEncoder.class); + + private final Class clazz; + + public AvroBackedKafkaEncoder(final Class clazz) { + this.clazz = clazz; + } + + @Override + @SuppressWarnings("unchecked") + public byte[] toBytes(final T source) { + final Schema schema = ReflectData.get().getSchema(clazz); + final AvroSerializer avroSerializer = new AvroSerializer(); + + try { + return avroSerializer.serialize(source, schema); + } catch (IOException e) { + LOG.error("Failed to encode source for schema: " + schema.getFullName()); + } + + return null; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSerializer.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSerializer.java new file mode 100644 index 0000000..c3e751c --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer.avro; + +import org.apache.avro.Schema; +import org.apache.avro.io.DatumReader; +import org.apache.avro.io.DatumWriter; +import org.apache.avro.io.Decoder; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.reflect.ReflectDatumReader; +import org.apache.avro.reflect.ReflectDatumWriter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author Soby Chacko + */ +public class AvroSerializer { + public T deserialize(final byte[] bytes, final Schema schema) throws IOException { + final Decoder decoder = DecoderFactory.get().binaryDecoder(bytes, null); + final DatumReader reader = new ReflectDatumReader(schema); + + return reader.read(null, decoder); + } + + public byte[] serialize(final T input, final Schema schema) throws IOException { + final DatumWriter writer = new ReflectDatumWriter(schema); + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + final Encoder encoder = EncoderFactory.get().binaryEncoder(stream, null); + writer.write(input, encoder); + encoder.flush(); + + return stream.toByteArray(); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSpecificDatumSerializer.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSpecificDatumSerializer.java new file mode 100644 index 0000000..12fb60d --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/avro/AvroSpecificDatumSerializer.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer.avro; + +import org.apache.avro.Schema; +import org.apache.avro.io.DatumReader; +import org.apache.avro.io.DatumWriter; +import org.apache.avro.io.Decoder; +import org.apache.avro.io.DecoderFactory; +import org.apache.avro.io.Encoder; +import org.apache.avro.io.EncoderFactory; +import org.apache.avro.specific.SpecificDatumReader; +import org.apache.avro.specific.SpecificDatumWriter; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +/** + * @author Soby Chacko + */ +public class AvroSpecificDatumSerializer { + public T deserialize(final byte[] bytes, final Schema schema) throws IOException { + final Decoder decoder = DecoderFactory.get().binaryDecoder(bytes, null); + final DatumReader reader = new SpecificDatumReader(schema); + + return reader.read(null, decoder); + } + + public byte[] serialize(final T input, final Schema schema) throws IOException { + final DatumWriter writer = new SpecificDatumWriter(schema); + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + final Encoder encoder = EncoderFactory.get().binaryEncoder(stream, null); + writer.write(input, encoder); + encoder.flush(); + + return stream.toByteArray(); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/common/StringEncoder.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/common/StringEncoder.java new file mode 100644 index 0000000..ce5e0f3 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/serializer/common/StringEncoder.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer.common; + +import kafka.serializer.Encoder; +import kafka.utils.VerifiableProperties; + +import java.util.Properties; + +/** + * @author Soby Chacko + */ +public class StringEncoder implements Encoder { + private String encoding = "UTF8"; + + public void setEncoding(final String encoding){ + this.encoding = encoding; + } + + @Override + public byte[] toBytes(final Object o) { + final Properties props = new Properties(); + props.put("serializer.encoding", encoding); + + final VerifiableProperties verifiableProperties = new VerifiableProperties(props); + return new kafka.serializer.StringEncoder(verifiableProperties).toBytes((String)o); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfigFactoryBean.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfigFactoryBean.java new file mode 100644 index 0000000..bd27da7 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfigFactoryBean.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.consumer.ConsumerConfig; +import org.springframework.beans.factory.FactoryBean; + +import java.util.Properties; + +/** + * @author Soby Chacko + */ +public class ConsumerConfigFactoryBean implements FactoryBean { + + private final ConsumerMetadata consumerMetadata; + private final ZookeeperConnect zookeeperConnect; + + public ConsumerConfigFactoryBean(final ConsumerMetadata consumerMetadata, + final ZookeeperConnect zookeeperConnect){ + this.consumerMetadata = consumerMetadata; + this.zookeeperConnect = zookeeperConnect; + } + + @Override + public ConsumerConfig getObject() throws Exception { + final Properties properties = new Properties(); + properties.put("zookeeper.connect", zookeeperConnect.getZkConnect()); + properties.put("zookeeper.session.timeout.ms", zookeeperConnect.getZkSessionTimeout()); + properties.put("zookeeper.sync.time.ms", zookeeperConnect.getZkSyncTime()); + properties.put("auto.commit.interval.ms", consumerMetadata.getAutoCommitInterval()); + properties.put("consumer.timeout.ms", consumerMetadata.getConsumerTimeout()); + properties.put("group.id", consumerMetadata.getGroupId()); + + return new ConsumerConfig(properties); + } + + @Override + public Class getObjectType() { + return ConsumerConfig.class; + } + + @Override + public boolean isSingleton() { + return true; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfiguration.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfiguration.java new file mode 100644 index 0000000..c7bb878 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConfiguration.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.consumer.ConsumerTimeoutException; +import kafka.consumer.KafkaStream; +import kafka.javaapi.consumer.ConsumerConnector; +import kafka.message.MessageAndMetadata; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.integration.MessagingException; + +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +/** + * @author Soby Chacko + */ +public class ConsumerConfiguration { + private static final Log LOGGER = LogFactory.getLog(ConsumerConfiguration.class); + + private final ConsumerMetadata consumerMetadata; + private final ConsumerConnectionProvider consumerConnectionProvider; + private final MessageLeftOverTracker messageLeftOverTracker; + private ConsumerConnector consumerConnector; + private volatile int count = 0; + private int maxMessages = 1; + + private ExecutorService executorService = Executors.newCachedThreadPool(); + + public ConsumerConfiguration(final ConsumerMetadata consumerMetadata, + final ConsumerConnectionProvider consumerConnectionProvider, + final MessageLeftOverTracker messageLeftOverTracker) { + this.consumerMetadata = consumerMetadata; + this.consumerConnectionProvider = consumerConnectionProvider; + this.messageLeftOverTracker = messageLeftOverTracker; + } + + public ConsumerMetadata getConsumerMetadata() { + return consumerMetadata; + } + + public Map>> receive() { + count = messageLeftOverTracker.getCurrentCount(); + + final List>> tasks = new LinkedList>>(); + final Object lock = new Object(); + + final Map>> consumerMap = getConsumerMapWithMessageStreams(); + for (final List> streams : consumerMap.values()) { + for (final KafkaStream stream : streams) { + tasks.add(new Callable>() { + @Override + public List call() throws Exception { + final List rawMessages = new ArrayList(); + try { + while (count < maxMessages) { + final MessageAndMetadata messageAndMetadata = stream.iterator().next(); + synchronized (lock) { + if (count < maxMessages) { + rawMessages.add(messageAndMetadata); + count++; + } else { + messageLeftOverTracker.addMessageAndMetadata(messageAndMetadata); + } + } + } + } catch (ConsumerTimeoutException cte) { + LOGGER.info("Consumer timed out"); + } + return rawMessages; + } + }); + } + } + + return executeTasks(tasks); + } + + private Map>> executeTasks(final List>> tasks) { + + final Map>> messages = new ConcurrentHashMap>>(); + messages.putAll(getLeftOverMessageMap()); + + try { + for (final Future> result : executorService.invokeAll(tasks)) { + if (!result.get().isEmpty()) { + final String topic = result.get().get(0).topic(); + if (!messages.containsKey(topic)) { + messages.put(topic, getPayload(result.get())); + } else { + + final Map> existingPayloadMap = messages.get(topic); + getPayload(result.get(), existingPayloadMap); + } + } + } + } catch (Exception e) { + throw new MessagingException("Consuming from Kafka failed", e); + } + + if (messages.isEmpty()) { + return null; + } + + return messages; + } + + private Map>> getLeftOverMessageMap() { + + final Map>> messages = new ConcurrentHashMap>>(); + + for (final MessageAndMetadata mamd : messageLeftOverTracker.getMessageLeftOverFromPreviousPoll()) { + final String topic = mamd.topic(); + + if (!messages.containsKey(topic)) { + final List l = new ArrayList(); + l.add(mamd); + messages.put(topic, getPayload(l)); + } else { + final Map> existingPayloadMap = messages.get(topic); + final List l = new ArrayList(); + l.add(mamd); + getPayload(l, existingPayloadMap); + } + } + messageLeftOverTracker.clearMessagesLeftOver(); + return messages; + } + + private Map> getPayload(final List messageAndMetadatas) { + final Map> payloadMap = new ConcurrentHashMap>(); + + for (final MessageAndMetadata messageAndMetadata : messageAndMetadatas) { + if (!payloadMap.containsKey(messageAndMetadata.partition())) { + final List payload = new ArrayList(); + payload.add(messageAndMetadata.message()); + payloadMap.put(messageAndMetadata.partition(), payload); + } else { + final List payload = payloadMap.get(messageAndMetadata.partition()); + payload.add(messageAndMetadata.message()); + } + + } + + return payloadMap; + } + + private void getPayload(final List messageAndMetadatas, final Map> existingPayloadMap) { + for (final MessageAndMetadata messageAndMetadata : messageAndMetadatas) { + if (!existingPayloadMap.containsKey(messageAndMetadata.partition())) { + final List payload = new ArrayList(); + payload.add(messageAndMetadata.message()); + existingPayloadMap.put(messageAndMetadata.partition(), payload); + } else { + final List payload = existingPayloadMap.get(messageAndMetadata.partition()); + payload.add(messageAndMetadata.message()); + } + } + } + + @SuppressWarnings("unchecked") + public Map>> getConsumerMapWithMessageStreams() { + if (consumerMetadata.getValueDecoder() != null) { + return getConsumerConnector().createMessageStreams( + consumerMetadata.getTopicStreamMap(), + consumerMetadata.getValueDecoder(), + consumerMetadata.getValueDecoder()); + } + + return getConsumerConnector().createMessageStreams(consumerMetadata.getTopicStreamMap()); + } + + public int getMaxMessages() { + return maxMessages; + } + + public void setMaxMessages(final int maxMessages) { + this.maxMessages = maxMessages; + } + + public ConsumerConnector getConsumerConnector() { + if (consumerConnector == null) { + consumerConnector = consumerConnectionProvider.getConsumerConnector(); + } + + return consumerConnector; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConnectionProvider.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConnectionProvider.java new file mode 100644 index 0000000..d2287fd --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerConnectionProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.consumer.ConsumerConfig; +import kafka.javaapi.consumer.ConsumerConnector; + +/** + * @author Soby Chacko + */ +public class ConsumerConnectionProvider { + + private final ConsumerConfig consumerConfig; + + public ConsumerConnectionProvider(final ConsumerConfig consumerConfig) { + this.consumerConfig = consumerConfig; + } + + public ConsumerConnector getConsumerConnector() { + return kafka.consumer.Consumer.createJavaConsumerConnector(consumerConfig); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerMetadata.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerMetadata.java new file mode 100644 index 0000000..eb4cac5 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ConsumerMetadata.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.serializer.Decoder; +import org.springframework.integration.kafka.core.KafkaConsumerDefaults; + +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class ConsumerMetadata { + + //High level consumer defaults + private String groupId = KafkaConsumerDefaults.GROUP_ID; + private String socketTimeout = KafkaConsumerDefaults.SOCKET_TIMEOUT; + private String socketBufferSize = KafkaConsumerDefaults.SOCKET_BUFFER_SIZE; + private String fetchSize = KafkaConsumerDefaults.FETCH_SIZE; + private String backoffIncrement = KafkaConsumerDefaults.BACKOFF_INCREMENT; + private String queuedChunksMax = KafkaConsumerDefaults.QUEUED_CHUNKS_MAX; + private String autoCommitEnable = KafkaConsumerDefaults.AUTO_COMMIT_ENABLE; + private String autoCommitInterval = KafkaConsumerDefaults.AUTO_COMMIT_INTERVAL; + private String autoOffsetReset = KafkaConsumerDefaults.AUTO_OFFSET_RESET; + private String rebalanceRetriesMax = KafkaConsumerDefaults.REBALANCE_RETRIES_MAX; + private String consumerTimeout = KafkaConsumerDefaults.CONSUMER_TIMEOUT; + + private String topic; + private int streams; + private Decoder valueDecoder; + private Decoder keyDecoder; + private Map topicStreamMap; + + public String getGroupId() { + return groupId; + } + + public void setGroupId(final String groupId) { + this.groupId = groupId; + } + + public String getSocketTimeout() { + return socketTimeout; + } + + public void setSocketTimeout(final String socketTimeout) { + this.socketTimeout = socketTimeout; + } + + public String getSocketBufferSize() { + return socketBufferSize; + } + + public void setSocketBufferSize(final String socketBufferSize) { + this.socketBufferSize = socketBufferSize; + } + + public String getFetchSize() { + return fetchSize; + } + + public void setFetchSize(final String fetchSize) { + this.fetchSize = fetchSize; + } + + public String getBackoffIncrement() { + return backoffIncrement; + } + + public void setBackoffIncrement(final String backoffIncrement) { + this.backoffIncrement = backoffIncrement; + } + + public String getQueuedChunksMax() { + return queuedChunksMax; + } + + public void setQueuedChunksMax(final String queuedChunksMax) { + this.queuedChunksMax = queuedChunksMax; + } + + public String getAutoCommitEnable() { + return autoCommitEnable; + } + + public void setAutoCommitEnable(final String autoCommitEnable) { + this.autoCommitEnable = autoCommitEnable; + } + + public String getAutoCommitInterval() { + return autoCommitInterval; + } + + public void setAutoCommitInterval(final String autoCommitInterval) { + this.autoCommitInterval = autoCommitInterval; + } + + public String getAutoOffsetReset() { + return autoOffsetReset; + } + + public void setAutoOffsetReset(final String autoOffsetReset) { + this.autoOffsetReset = autoOffsetReset; + } + + public String getRebalanceRetriesMax() { + return rebalanceRetriesMax; + } + + public void setRebalanceRetriesMax(final String rebalanceRetriesMax) { + this.rebalanceRetriesMax = rebalanceRetriesMax; + } + + public String getConsumerTimeout() { + return consumerTimeout; + } + + public void setConsumerTimeout(final String consumerTimeout) { + this.consumerTimeout = consumerTimeout; + } + + public String getTopic() { + return topic; + } + + public void setTopic(final String topic) { + this.topic = topic; + } + + public int getStreams() { + return streams; + } + + public void setStreams(final int streams) { + this.streams = streams; + } + + public Decoder getValueDecoder() { + return valueDecoder; + } + + public void setValueDecoder(final Decoder valueDecoder) { + this.valueDecoder = valueDecoder; + } + + public Decoder getKeyDecoder() { + return keyDecoder; + } + + public void setKeyDecoder(final Decoder keyDecoder) { + this.keyDecoder = keyDecoder; + } + + public Map getTopicStreamMap() { + return topicStreamMap; + } + + public void setTopicStreamMap(final Map topicStreamMap) { + this.topicStreamMap = topicStreamMap; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/DefaultPartitioner.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/DefaultPartitioner.java new file mode 100644 index 0000000..3147b9d --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/DefaultPartitioner.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.producer.Partitioner; +import kafka.utils.Utils; + +/** + * @author Soby Chacko + * + * This class is for internal use only and therefore is at default access level + */ +class DefaultPartitioner implements Partitioner { + /** + * Uses the key to calculate a partition bucket id for routing + * the data to the appropriate broker partition + * @return an integer between 0 and numPartitions-1 + */ + @Override + public int partition(final T key, final int numPartitions) { + return Utils.abs(key.hashCode()) % numPartitions; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaConsumerContext.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaConsumerContext.java new file mode 100644 index 0000000..a42e458 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaConsumerContext.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.integration.Message; +import org.springframework.integration.kafka.core.KafkaConsumerDefaults; +import org.springframework.integration.support.MessageBuilder; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class KafkaConsumerContext implements BeanFactoryAware { + private Map consumerConfigurations; + private String consumerTimeout = KafkaConsumerDefaults.CONSUMER_TIMEOUT; + private ZookeeperConnect zookeeperConnect; + + public Collection getConsumerConfigurations() { + return consumerConfigurations.values(); + } + + @Override + public void setBeanFactory(final BeanFactory beanFactory) throws BeansException { + consumerConfigurations = ((ListableBeanFactory) beanFactory).getBeansOfType(ConsumerConfiguration.class); + } + + public Message>>> receive() { + final Map>> consumedData = new HashMap>>(); + + for (final ConsumerConfiguration consumerConfiguration : getConsumerConfigurations()) { + final Map>> messages = consumerConfiguration.receive(); + + if (messages != null){ + consumedData.putAll(messages); + } + } + return MessageBuilder.withPayload(consumedData).build(); + } + + public String getConsumerTimeout() { + return consumerTimeout; + } + + public void setConsumerTimeout(final String consumerTimeout) { + this.consumerTimeout = consumerTimeout; + } + + public ZookeeperConnect getZookeeperConnect() { + return zookeeperConnect; + } + + public void setZookeeperConnect(final ZookeeperConnect zookeeperConnect) { + this.zookeeperConnect = zookeeperConnect; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaProducerContext.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaProducerContext.java new file mode 100644 index 0000000..8a815d5 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/KafkaProducerContext.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.integration.Message; + +import java.util.Collection; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class KafkaProducerContext implements BeanFactoryAware { + private Map topicsConfiguration; + + @SuppressWarnings("unchecked") + public void send(final Message message) throws Exception { + final ProducerConfiguration producerConfiguration = + getTopicConfiguration(message.getHeaders().get("topic", String.class)); + + if (producerConfiguration != null) { + producerConfiguration.send(message); + } + } + + private ProducerConfiguration getTopicConfiguration(final String topic){ + final Collection topics = topicsConfiguration.values(); + + for (final ProducerConfiguration producerConfiguration : topics){ + if (producerConfiguration.getProducerMetadata().getTopic().equals(topic)){ + return producerConfiguration; + } + } + + return null; + } + + public Map getTopicsConfiguration() { + return topicsConfiguration; + } + + @Override + public void setBeanFactory(final BeanFactory beanFactory) throws BeansException { + topicsConfiguration = ((ListableBeanFactory)beanFactory).getBeansOfType(ProducerConfiguration.class); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/MessageLeftOverTracker.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/MessageLeftOverTracker.java new file mode 100644 index 0000000..a7d23e0 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/MessageLeftOverTracker.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.message.MessageAndMetadata; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Soby Chacko + */ +public class MessageLeftOverTracker { + private final List messageLeftOverFromPreviousPoll = new ArrayList(); + + public void addMessageAndMetadata(final MessageAndMetadata messageAndMetadata){ + messageLeftOverFromPreviousPoll.add(messageAndMetadata); + } + + public List getMessageLeftOverFromPreviousPoll(){ + return messageLeftOverFromPreviousPoll; + } + + public void clearMessagesLeftOver(){ + messageLeftOverFromPreviousPoll.clear(); + } + + public int getCurrentCount() { + return messageLeftOverFromPreviousPoll.size(); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerConfiguration.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerConfiguration.java new file mode 100644 index 0000000..93e5c66 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerConfiguration.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; +import kafka.serializer.DefaultEncoder; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.springframework.integration.Message; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; + +/** + * @author Soby Chacko + */ +public class ProducerConfiguration { + private final Producer producer; + private final ProducerMetadata producerMetadata; + + public ProducerConfiguration(final ProducerMetadata producerMetadata, final Producer producer){ + this.producerMetadata = producerMetadata; + this.producer = producer; + } + + public ProducerMetadata getProducerMetadata() { + return producerMetadata; + } + + public void send(final Message message) throws Exception { + final V v = getPayload(message); + + if (message.getHeaders().containsKey("messageKey")) { + producer.send(new KeyedMessage(producerMetadata.getTopic(), getKey(message), v)); + } else { + producer.send(new KeyedMessage(producerMetadata.getTopic(), v)); + } + } + + @SuppressWarnings("unchecked") + private V getPayload(final Message message) throws Exception { + if (producerMetadata.getValueEncoder().getClass().isAssignableFrom(DefaultEncoder.class)) { + return (V) getByteStream(message.getPayload()); + } else if (message.getPayload().getClass().isAssignableFrom(producerMetadata.getValueClassType())) { + return producerMetadata.getValueClassType().cast(message.getPayload()); + } + + throw new Exception("Message payload type is not matching with what is configured"); + } + + @SuppressWarnings("unchecked") + private K getKey(final Message message) throws Exception { + final Object key = message.getHeaders().get("messageKey"); + + if (producerMetadata.getKeyEncoder().getClass().isAssignableFrom(DefaultEncoder.class)) { + return (K) getByteStream(key); + } + + return message.getHeaders().get("messageKey", producerMetadata.getKeyClassType()); + } + + private static boolean isRawByteArray(final Object obj){ + return obj instanceof byte[]; + } + + private static byte[] getByteStream(final Object obj) throws IOException { + if (isRawByteArray(obj)){ + return (byte[])obj; + } + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + final ObjectOutputStream os = new ObjectOutputStream(out); + os.writeObject(obj); + + return out.toByteArray(); + } + + @Override + public boolean equals(final Object obj){ + return EqualsBuilder.reflectionEquals(this, obj); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerFactoryBean.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerFactoryBean.java new file mode 100644 index 0000000..801c8e5 --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerFactoryBean.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.javaapi.producer.Producer; +import kafka.producer.ProducerConfig; +import kafka.producer.ProducerPool; +import kafka.producer.async.DefaultEventHandler; +import kafka.producer.async.EventHandler; +import org.springframework.beans.factory.FactoryBean; +import scala.collection.mutable.HashMap; + +import java.util.Properties; + +/** + * @author Soby Chacko + */ +public class ProducerFactoryBean implements FactoryBean> { + + private final String brokerList; + private final ProducerMetadata producerMetadata; + + public ProducerFactoryBean(final ProducerMetadata producerMetadata, final String brokerList){ + this.producerMetadata = producerMetadata; + this.brokerList = brokerList; + } + + @Override + public Producer getObject() throws Exception { + final Properties props = new Properties(); + props.put("metadata.broker.list", brokerList); + props.put("compression.codec", producerMetadata.getCompressionCodec()); + + if (producerMetadata.isAsync()){ + props.put("producer.type", "async"); + if (producerMetadata.getBatchNumMessages() != null){ + props.put("batch.num.messages", producerMetadata.getBatchNumMessages()); + } + } + + final ProducerConfig config = new ProducerConfig(props); + final EventHandler eventHandler = new DefaultEventHandler(config, + producerMetadata.getPartitioner() == null ? new DefaultPartitioner() : producerMetadata.getPartitioner(), + producerMetadata.getValueEncoder(), producerMetadata.getKeyEncoder(), + new ProducerPool(config), new HashMap()); + + final kafka.producer.Producer prod = new kafka.producer.Producer(config, + eventHandler); + return new Producer(prod); + } + + @Override + public Class getObjectType() { + return Producer.class; + } + + @Override + public boolean isSingleton() { + return true; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerMetadata.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerMetadata.java new file mode 100644 index 0000000..a10a4fa --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ProducerMetadata.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.producer.Partitioner; +import kafka.serializer.DefaultEncoder; +import kafka.serializer.Encoder; +import org.apache.commons.lang.builder.EqualsBuilder; +import org.apache.commons.lang.builder.HashCodeBuilder; +import org.springframework.beans.factory.InitializingBean; + +/** + * @author Soby Chacko + */ +public class ProducerMetadata implements InitializingBean { + private Encoder keyEncoder; + private Encoder valueEncoder; + private Class keyClassType; + private Class valueClassType; + private final String topic; + private String compressionCodec = "default"; + private Partitioner partitioner; + private boolean async = false; + private String batchNumMessages; + + public ProducerMetadata(final String topic) { + this.topic = topic; + } + + public String getTopic() { + return topic; + } + + public Encoder getKeyEncoder() { + return keyEncoder; + } + + public void setKeyEncoder(final Encoder keyEncoder) { + this.keyEncoder = keyEncoder; + } + + public Encoder getValueEncoder() { + return valueEncoder; + } + + public void setValueEncoder(final Encoder valueEncoder) { + this.valueEncoder = valueEncoder; + } + + public Class getKeyClassType() { + return keyClassType; + } + + public void setKeyClassType(final Class keyClassType) { + this.keyClassType = keyClassType; + } + + public Class getValueClassType() { + return valueClassType; + } + + public void setValueClassType(final Class valueClassType) { + this.valueClassType = valueClassType; + } + + //TODO: Use an enum + public String getCompressionCodec() { + if (compressionCodec.equalsIgnoreCase("gzip")) { + return "1"; + } else if (compressionCodec.equalsIgnoreCase("snappy")) { + return "2"; + } + + return "0"; + } + + public void setCompressionCodec(final String compressionCodec) { + this.compressionCodec = compressionCodec; + } + + public Partitioner getPartitioner() { + return partitioner; + } + + public void setPartitioner(final Partitioner partitioner) { + this.partitioner = partitioner; + } + + @Override + @SuppressWarnings("unchecked") + public void afterPropertiesSet() throws Exception { + if (valueEncoder == null) { + setValueEncoder((Encoder) new DefaultEncoder(null)); + } + + if (keyEncoder == null) { + setKeyEncoder((Encoder) getValueEncoder()); + } + } + + public boolean isAsync() { + return async; + } + + public void setAsync(final boolean async) { + this.async = async; + } + + public String getBatchNumMessages() { + return batchNumMessages; + } + + public void setBatchNumMessages(final String batchNumMessages) { + this.batchNumMessages = batchNumMessages; + } + + @Override + public boolean equals(final Object obj){ + return EqualsBuilder.reflectionEquals(this, obj); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ZookeeperConnect.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ZookeeperConnect.java new file mode 100644 index 0000000..aa31a4d --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/ZookeeperConnect.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import org.springframework.integration.kafka.core.ZookeeperConnectDefaults; + +/** + * @author Soby Chacko + */ +public class ZookeeperConnect { + private String zkConnect = ZookeeperConnectDefaults.ZK_CONNECT; + private String zkConnectionTimeout = ZookeeperConnectDefaults.ZK_CONNECTION_TIMEOUT; + private String zkSessionTimeout = ZookeeperConnectDefaults.ZK_SESSION_TIMEOUT; + private String zkSyncTime = ZookeeperConnectDefaults.ZK_SYNC_TIME; + + public String getZkConnect() { + return zkConnect; + } + + public void setZkConnect(final String zkConnect) { + this.zkConnect = zkConnect; + } + + public String getZkConnectionTimeout() { + return zkConnectionTimeout; + } + + public void setZkConnectionTimeout(final String zkConnectionTimeout) { + this.zkConnectionTimeout = zkConnectionTimeout; + } + + public String getZkSessionTimeout() { + return zkSessionTimeout; + } + + public void setZkSessionTimeout(final String zkSessionTimeout) { + this.zkSessionTimeout = zkSessionTimeout; + } + + public String getZkSyncTime() { + return zkSyncTime; + } + + public void setZkSyncTime(final String zkSyncTime) { + this.zkSyncTime = zkSyncTime; + } +} diff --git a/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/package-info.java b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/package-info.java new file mode 100644 index 0000000..0ec591d --- /dev/null +++ b/spring-integration-kafka/src/main/java/org/springframework/integration/kafka/support/package-info.java @@ -0,0 +1,4 @@ +/** + * Provides various support classes used across Spring Integration Kafka Components. + */ +package org.springframework.integration.kafka.support; diff --git a/spring-integration-kafka/src/main/resources/META-INF/spring.handlers b/spring-integration-kafka/src/main/resources/META-INF/spring.handlers new file mode 100644 index 0000000..1bee2b0 --- /dev/null +++ b/spring-integration-kafka/src/main/resources/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/integration/kafka=org.springframework.integration.kafka.config.xml.KafkaNamespaceHandler diff --git a/spring-integration-kafka/src/main/resources/META-INF/spring.schemas b/spring-integration-kafka/src/main/resources/META-INF/spring.schemas new file mode 100644 index 0000000..b5240f8 --- /dev/null +++ b/spring-integration-kafka/src/main/resources/META-INF/spring.schemas @@ -0,0 +1,2 @@ +http\://www.springframework.org/schema/integration/kafka/spring-integration-kafka-1.0.xsd=org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd +http\://www.springframework.org/schema/integration/kafka/spring-integration-kafka.xsd=org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd diff --git a/spring-integration-kafka/src/main/resources/META-INF/spring.tooling b/spring-integration-kafka/src/main/resources/META-INF/spring.tooling new file mode 100644 index 0000000..d407981 --- /dev/null +++ b/spring-integration-kafka/src/main/resources/META-INF/spring.tooling @@ -0,0 +1,4 @@ +# Tooling related information for the integration Kafka namespace +http\://www.springframework.org/schema/integration/kafka@name=integration Kafka Namespace +http\://www.springframework.org/schema/integration/kafka@prefix=int-kafka +http\://www.springframework.org/schema/integration/kafka@icon=org/springframework/integration/config/xml/spring-integration-kafka.gif diff --git a/spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd b/spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd new file mode 100644 index 0000000..1c716fe --- /dev/null +++ b/spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka-1.0.xsd @@ -0,0 +1,451 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The topic configured by this configuration. + + + + + + + + + + + + + + + + + Custom implemenation of a Kafka Encoder for encoding message values. + + + + + + + Custom implemenation of a Kafka Encoder for encoding message keys. + + + + + + + Class type used for the key + + + + + + + Class type used for the value + + + + + + + + + + + + + + + + + Custom Kafka key partitioner. + + + + + + + Indicates if this producer is async or not. + + + + + + + number of messages to batch at this producer. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kafka Server Bean Name + + + + + + + Kafka Server Bean Name + + + + + + + + + + + + + + + + + + + + + + + + + Kafka Server Bean Name + + + + + + + + + + The definition for the Spring Integration Kafka + Inbound Channel Adapter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Kafka Server Bean Name + + + + + + + + + + + Identifies the underlying Spring bean definition, which is an + instance of either 'EventDrivenConsumer' or 'PollingConsumer', + depending on whether the component's input channel is a + 'SubscribableChannel' or 'PollableChannel'. + + + + + + + Flag to indicate that the component should start automatically + on startup (default true). + + + + + + + + + + + + Defines kafka outbound channel adapter that writes the contents of the + Message to kafka broker. + + + + + + + + + + + Identifies the underlying Spring bean definition, which is an + instance of either 'EventDrivenConsumer' or 'PollingConsumer', + depending on whether the component's input channel is a + 'SubscribableChannel' or 'PollableChannel'. + + + + + + + Flag to indicate that the component should start automatically + on startup (default true). + + + + + + + + + + Kafka producer context reference. + + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka.gif b/spring-integration-kafka/src/main/resources/org/springframework/integration/config/xml/spring-integration-kafka.gif new file mode 100644 index 0000000000000000000000000000000000000000..41b369fece0e576ecd920400b221fd33abbaf5c4 GIT binary patch literal 572 zcmZ?wbhEHb6krfwc*Xz%|NsBj^POrMHnn5wI!WKA$M#t>l?H4BPzO?b&{cRWSFFSB< z^|`x}P0QyVy|U`SiF40CoO%A?(2mZdyE;$q?mn}(`^J&Ji~DRU^KH0`_S=*f zu`(xWd1~172%niTfzv{QrUeE70}32q)Fc#tvM@3*q%i1!3doJ z$QZroAYKk;o)!}g_c$BZP}??kHkSD;%JZ2d6u9}BT8+&ujM`ePV%-d#6T`zi);F^_ zIR!9@O3I6^oV(z-tdN`(pX{#l&0dZgror~-%^VCYeSN$`0@iCL`334n#7Fs`w{uBO x(B)aWd{Qfm5=#n`xT2uIN}aG328D}QI)&6(d>(9QbkfS;6w=UeP!nLV1^^EL*)#wE literal 0 HcmV?d00001 diff --git a/spring-integration-kafka/src/reference/docbook/SIAdapterLowerPrefix.xml b/spring-integration-kafka/src/reference/docbook/SIAdapterLowerPrefix.xml new file mode 100644 index 0000000..2ff42a4 --- /dev/null +++ b/spring-integration-kafka/src/reference/docbook/SIAdapterLowerPrefix.xml @@ -0,0 +1,72 @@ + + + Kafka Adapter + + The Spring Integration Kafka Adapter provides... + + + + Outbound Channel adapter + + + Outbound Gateway + + + Inbound Channel Adapter + + + +
+ Java Implementation + Each of the provided components will use the + org.springframework.integration.kafka.core.KafkaExecutor + class... + +
+
+ Common Configuration Attributes + + Certain configuration parameters are shared amongst all Kafka + components and are described below: + + + auto-startup + + Lifecycle attribute signaling if this component should + be started during Application Context startup. + Defaults to true. + Optional. + + + id + + Identifies the underlying Spring bean definition, which + is an instance of either EventDrivenConsumer + or PollingConsumer. + Optional. + + +
+ +
+ Outbound Channel Adapter + + The Kafka Outbound channel adapter allows you to... + +
+
+ Outbound Gateway + + Outbound gateways are similar to outbound channel adapters except that it can also be used to + get a result on the reply channel after performing + the given... + +
+
+ Inbound Channel Adapter + + An inbound channel adapter is used to execute... + +
+ +
diff --git a/spring-integration-kafka/src/reference/docbook/history.xml b/spring-integration-kafka/src/reference/docbook/history.xml new file mode 100644 index 0000000..71b4f74 --- /dev/null +++ b/spring-integration-kafka/src/reference/docbook/history.xml @@ -0,0 +1,4 @@ + + + Change History + diff --git a/spring-integration-kafka/src/reference/docbook/images/logo.png b/spring-integration-kafka/src/reference/docbook/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e1c2a0e3c7864b4c2495fa75fed9733af2558ff9 GIT binary patch literal 9627 zcmV;MC1l!(P)004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z001Q+Nkla zCi#~HZyJIZTG9$KUioZd`!}Or{bFjSBIkwcKiM>eFRp0xnmJn*9lLahrv&$&JY*67 z(+68b0=h~6HOW6Ax03H2xpI^z2mp-7iNjZqC1fNu+21DlheV}SfldP$0RVvGj83D` zYn$+AUU*(tq&lBlvd6#{HbJLRBr3Q=M7thPUIL6BRHe~vdxZOizF0p2YxMY+VVRtw z{J)a#lxtKTPHuxb_A@@`c+JC{*?A>UhwVMG&v8YnL)_!_GJc*Os9;lPd)Kb(oX2nMYQd4K`Wf?m5|;z!G; ze2igOjZf*AoLf&~Sgsra>*+M1rTgM02>#EC?fUQN%~LCOuelO`6<7lZ#UPSCLnRB0 zVZo%2R!sTC><1OfKUlsT#{~dXPVp`b@D}bLV%R^&L?VFPqP#+7adCMG!x#X=0D!=7 z2dTZCSmx={Oe_@DW9fK=u;(mGBp@kkd!=U#LP}8oJ1oay7*620C7P?r?}qX!HFga9Wg zJ4Z*Uy^pJxpWJU)K-+_7)B*3q(xm05>e zO}M%8_w|ZWg}u~%Zp@7S5xr}ymM3{>hp(NuoN(i5i2~!;u5G(58NWb42>{^O%~Kop zZz|VmUF@A_jd-hHyPlO@-LwAqm811{6(K|tgomSh8~@NDQT@Ywf~~@$fn}~HT;G4? z*n=kz^UDg$wW>m85r)-As=i@T%?*rd~tH;*t-EcMW7OzzUARs6p2*3%bI@~-5 zJP#}bJO>2Tu2s9O^EP%2Y*)3Nu`Ax#eegS=h4nf$06Y&E1_Ij0!*}va<7N(d-Oa(p z?7KzfC0*Yem=u2lXd&qJUM*T*TXWLOrJ3=6&)mK6^?@xX;?5h?N?;klZ~z31;hTlq zXAXTmtspb{-TwJcl7JS1PVXPw{_2{O?hdZT&yHL9=C22~KfkL80nY1>0QDiNU5oHLn=aX+|H`$hd7D4{?)dir z0AiUr>ct?J`+WWOmuA72Q}Rnj;bsD-v6`eKj$wjy#RieU+s$rJqa#nHRnPzS4t+f_^HF}z6R>v#3my-XvTAvC4diH4}l93Zv&)od`I>1Sw#g*EQW5J*Yod$Fo&})Fv z1JBjy6VTGs%tyIJ1qz7nFd!+A*d?UgdwI$Dul9XoZqyK9)r^5iC``;u?lXVbu46k` zjt7wxaJ;hT2mzz_be02v-k=ATG5r^YgI1%~scr5nU^s}Sde*RF=h8V_7I8HO+v5BoZACZhst4M48Z8cj&82@E{;+M9LG40 zFDx(0Da->^DNwY@j$i-=t-P6kGbgutwFVD*of)G>kS1+v6k4oj3?FP-KoZ2<5z}i- zJ608dlprxHdEVB=$>|S4Vqax#^T08{7yuwRH1e%MlK=q2uxitfVfA@$hzEu-Z-Ox# z2m*|L8$p6d_Ra4bLYfE89P)atsnkzhJL}^0lOVOP7R7KZum<2b;Ca)+tMJWeG1RR} zpO3%9tmNUb<5JQdn76qV1BBz#22GNfiySkGHfi*aNkaA<#f;O%sw>&WJH4G>znwNp z&*uBPzTc40>}hG?nuD7GP~{})G_6{+o;7S*WUKZbj&9C&PB?~hJomIzk?}O^{*wnM zZk@Z85?87!Wm#5c=e&H%CvseD=%gp`8YJyLeV@=Cj?aPhL0x z61z(9vIgMT0N;>)?R&QJYGL$T)jCafVJ?p0EXQs+{(bJ#EFef*@RwbfJ2G}$%H#X> zf)9Y_?HpW(bbQH-ub_>4T6UbkE%HK0T%29>y_?-2U)R|WvSx6cvZco-7)IHKzXur! zY3WH&u_O$-F712nUHrX=qkD}v{XBg9JbZeFcbz?Ada zw~ObFc^k~y{l8M;HXPjc+n;-Yc{Z;kr7bUAuw!X-NTigO)Mc`ZN}d8im^7AF?d0M) zb>PG){l;0rgq0nV~sS;ae(^OqsS*f!>mbJKxiz-ZkN zk73w?(Q|8quZB2D9s0KGIlB9Z=Ya1Se_{4HMN-khk-Is?eF2Rs)$W%2Kee+cq!(O8|S9|~PVqsH|a16p;3 zC;A{gsi?fjL|+u#@5+t=|+VYMq+ty3d}8b z#Wbj6-&v#HsZ@caATcZDa>9-JX3e1cWYVG~6Bc6sY*)L6L;@)=sX&4(EXpf?2FJ?@ zH;aq%E7=pRdTifUW1;HJa2|!wg%}c-6K@*nyX6%Dz*2@G;tmd zF$i&h5$TGJW!l@;wiD(PM2KaT5ERmD_imiA;l0${w10Tl)%&B~c6Rdyjx&1qyjpqg z?!`La)@z6M92q-q{y)D|TrDHijxulBtN4r%PW>Se>Q0` zj{gg#>CQ4IT0mFgtcvN2&q%7INIA~KwOMpXWW&GFOjHt|p0N4o4(mv5f{af~U|5E# zo_A+?3IMVRe}Xb?oPjNLkVGoVP7pY))WqeL{O;vE+44rjLea8`3x_|xuU3_s))G#D zNK#f_a_IcNLuY@Z?3`TfotwEd3-bx?(6UW*aAb2gFK0Wazhi161KaOFJOBc~fFSdW z3mA^A9gPBO=ois5-0$Bh&aKN`(;4)os!~%_4gfso@8K&EivJe;Z9cj~TdH_|%v&9Q z37N^c#rZyN-qk&@W>z|X73&V?cvWHfIag`lr)9?gFbpHIOMY;(A0i;Fh#~s4?eX)% z?QecHL!rn4T4EYOoG@mk7=u2yATu{N?biJ(-valsEDtF8zrHzDPY5Qkxu){tFTe%ji0hIbx#`m1BZ z`-~<@5;RIvd4bWy5Cl*{pv9&?l1O+QzkS~~F|lL2y*Kc5+=a%RLY_AlY1LLiyxh?P z{(DkA5r7m>CgRwtf`0Dtq(R01!z7!<@y%sd(!04e`c!YsK2ogIE z!R0&W2Y)!?(_dEoJ$4S!0-CCWlI0QqUjrWkR4rn|@C-$Eim@mH&RVE%TZ_B^k*b(%PhAx%by8G{%cqg1+Nwf>Q9two1K zZg_iD!nSLFe6eroo1ez?858Img5e}+RG`yoMl#d*#D|f1SP5#mjGKaR*mTYrR1gENV;uG7BPlyVVx@IA4@`DPN!bI z|LeYOyI1IH922G_$q&1il4?`pydmG0RZWeV*A!pk&DE51cQpZV*|nd)wn zt6hJQ(2ut%hlXKriG0VkW`RYOL(Cqz{969{>AX8Wkq>K`HgIU?>|Xk zITkPscn*`;`Fr@L3dfkdW>#Hhfig1)Wyn>LyrYM+T*kTMLNK$50{uoB8 z^eG6jjXu6OhIa~$`gZR6o$r245roOsU>NZHU&(O@8pKzp6=YsXxcLl!JSnk@4vl)T z@YmWe7%D*Pk~14@%GQZX_)+g=;v@>vg-%?RmcANKynrSc*TLUi&P}K@>p> zDWPqBU9*3)zDm?0AYDA2CIIl(2ErJIbDWCd%Y~F%MIMDmdVs$%d#2H8O4Z8BwGN0P zDf1*0LFN|b#HT0L-{7s3yW0+J15#kp={n78y+_k8)G1W};N#}qwncc=LY~8y4*ghv zArC-m?vwn&{CZ3%UW3M~8gttyLeZ@U*A8DlS@l^nhcQw*#Ck6fLx=+e0>ZgU9@#pt zH@h(Rz~#diA6zX}mp%7ohGUog^3~&vM4+A}a#+U4&5Jb4m5@|XiDL9;lkPmeUsr>V zvQsConqF33V!|Jzw|Dg%-F*bSXhI8wW4et1j;lllhD}>HZ~eioN{zDi`v#WTclk*F zk49zYrvX7(6#rHR*cfKu7_C8TOZhQ|^E~(E-i?DJU#i$hi{rGX<7-K|3jq_@#y^2g zDeHmeWHBE&JuGjK>ea&IthBO}ceA{_0w(gQLVw?jjHK3U-j#Bc2Bg z%PTIpl@ga%oR75XYTKC#+5o?>fgStSs~IH4%HnswnZJ7fH=Tnb!+e89v?wt%`R1eB z3F!&IvL^5WfL7Bou-*KZXTggpt`07rjC+6Dm(wc?qX-fR^7g} zt-ZbT&Ue3dcXWdnR$lKv;l`soJN9n^v8=MTS11BN!1H>&Mz2!?h5;pjioIs2a{mg3 zVfNLxuvPQ_+O=pyk(6mx5T99?OS8T6Gn>`Z*RDJ#9oM03bx*M4)HOI`J3TsvRv5nr4fn zlCx6-y#m1IbRm*FUQv~j6{|d1tQ=Dq^r)5&2mbUCyID`k)_N9j$un) zTM!Y^0~iBn%7J46p?cj%1PFmn3I=^-c$YuEJo?ViH|j@Tkays`(R09{1GWxIh+{#m zZ0Q&Jd+g8SdX6-Up=zZGIBxl2Rsu=@CVc_G^B|VWYHXIMz#QVS`gdnm0vJ{0&1<7p zLW_U=VDpN1R=GR7gIWm&9UvZzE1}|#Wk6LX6-j4}dPho&fj5yV1dfOK2Gxnb(B?sH z{ab^vhJd0jKfK}Y=q7P;2iuZ;6m>i0-lYfEsy=g(^|2GTMZARs9-;`xa(x2Jl>y*9 zVuY03s)H-r1%zK%ef;N-e;hw>qPw#kcn%B((5gYL1fANn|6FJMrv?~?Ajp1E1Ah2u z=jGKWyN7jpo`L6RO1S2&&o;lmN$%XtRMVhUgH8il6==#quXVC_o;~jUKUW^>eI@?Ul-&Z=;F~FlqI0kKzXT$tEZ1^W&K-3%eG+9gH{S! zHRzen!5zuxA=}s8TTF{gqfuV!?(XAcuFi!bs0E|vTwQ;5$+XX-T1V3Yk!k57WK?qg z-YsXm{LYz`hhm}!mnch2>JTB3fbJ3!Rj2>wV(uo*{S&@1%=8Iqi&`M(pl!%qM3h0%Ql^ZBYoVxDT{xsv4!b_ zrwr>n=-7=vj^8?6P*kAjScyRB>FU)rBx-2q0nJ^#>S@8#S^o9Rm238FP#JVmf#B^C zGyE(*!!$tdyK%;sn-6YLF$SrSe(#kz&D|=$>#c#4vK86qZvP1w9^x0WVDy~lHYj)U z*f3+orbFMV^%|*2`rga4JnF9V^OyU^j-NmO^>?@xk-7c%hta8_1qEDsxr7WiD_?F&k@l>OePrdr{m7|4DSL0k&SdTNu?KLl&DIp zc`c-~y*R?MbR0s6F@g^PD-w9R%KA4}C`S?`EfCTIq2=JS%<05!03QG}WWMY9EKLcm z1z!U@Qz0MjT&5~jn83#x>|MS4NA#|dJ}{FQnHMv|!Nn6e7KEZ32{&<^=oa1u7@bYC zK`6?KYF`Q<6NTZJz%nKod*%uowU~a(6=S(bW{0W@4yr_gn29cn-++j@9y#Duv z;n;;cmM;4C6Cf#5Mi=pfNbciJh^jk1|ao2? z*Eq?uKKuzt!N!>@<#JzO+4$6Z_tO&Qj+kDZZ?teN=!2WLs=cET1Kjd50*=?X$ok10 zUT&J;|E!d%$`^; zF5hze`$>Jr`1*%g2SJ1Ypi@wX+Jq|0mm{!2wE~RA9IXb!D+;Fyq z+^<%AR2r8)PGd;J>#hYvh(*8$xXt%=p7LKC?!A|f{B~i#L8s{y+G*a%882KVbE8X^ zBB%Gl;i*ZtK_meLz%f8lUEB8feDdP%VV&#$tflua+)qpRFB{zb}#t0cEJV9mBnR@ zQ|lksbrw0t>nK9j(j&I%cEbM!vE}|ZMOjh5g(L1I-UN}X7Kev{rmSm3pTnQ-HhV-! zYQgfXq8$V-GN(Qf$El>Ekd_{?4&siD{!^249w%p|6swgqCGcE;n`$$&J2BYxYQoLoOU6Sl5p*Lj+`nRNBezoS_4!~Z z^q`;vKm^c3+$VkYwxX=i+IDgZb4MtDNo*57@w zY0PJnmCEAE3MM*DM0l4U=WlK9W`-^)EdMh(e+fc-3MLOBmP1;pFvQRGZCAU2e}|Hf z-L-PZ?u`K819J|YmN^Xc=IePPyeT+Bs7Q+B1=q>K}POlhj8cVUyM&Q zI_>N0=0G%bnAL}2S*u!Gy6a2m4DW7RT=8V%p>5C^R{p-h+Kz?K=my;>Y=F~O`@SyI zl-n>wlUKhAonY$PSw%|IL?mV=pZM#H&Y-iZozr*DLr)?+4eqQ*Uz047G8sNBI?Rz-M%s4J(+B*rkAF#aANVp*2d8Fg`auMb2 z=lX72-(5Z~(6s4veVugcQnCO6?s2w*x@5z|%?E!%5QFNzxUwX(PIA z;^`tk5WDL}((S)e;_jy2KY9Dyv2%N_CjND4^@&hx?scI^0gzCvs(Fq=p|S`e9=CF@ zzH!@8w$s-Qh;L^%X19NQe=Z5K@tQYzPYQHmrJu}hmj(ZAh8gxT|oHX zf74+p33_aJ>2T*=V=k zi-n@K)4%B2y0fA%&->Z+$p9c0_=*L-@UO}rx6keTBf)_h!5;6jybq+Q9ACT!A--Y%7eI(dGXMCUw^pQd|^jkDi zQED~0m6G!$Y+4uSf4CNpbLIG^`5m&9l8F8!?0%1 z1%sCy@PRIPZ}Ipqr`7c`v1G}T|My9r&cTr_0)klpsetxyaB1JF{l}9RFPX5=q3R{J zmWh+haZHaGXBWBJpmh;RJBCFsdt=#xS7zC;$A`d)0TF!yd|L4sArXq?P9Ab6ce%ZD zpALQ7_*-|t@VzGwWfIxMzGDTHjb1fHkVB$g^7jc+>a`BEL~ifWrET}G=WU2;6@goM zgF&EI!0@iaL{d9F%eXr@d)T>j4(rsj4zq4c{;f%xq|r=&(-1VtzbOAV0DzS)V@r=* R@&Et;07*qoM6N<$f(p*0Lx literal 0 HcmV?d00001 diff --git a/spring-integration-kafka/src/reference/docbook/index.xml b/spring-integration-kafka/src/reference/docbook/index.xml new file mode 100644 index 0000000..bdb072f --- /dev/null +++ b/spring-integration-kafka/src/reference/docbook/index.xml @@ -0,0 +1,67 @@ + + + + Spring Integration Kafka Adapter + Kafka Adapter ${version} + Spring Integration + ${version} + + + + + + + + + + + + + + Soby Chacko + + + © SpringSource Inc., 2012 + + + + + + + What's new? + + + For those who are already familiar with Spring Integration, this + chapter + provides a brief overview of the new features of version 2.2. If you are + interested in the changes and features, that were introduced in + earlier + versions, please take a look at chapter: + + + + + + + + + + Integration Adapters + + This section covers the various Channel Adapters and Messaging + Gateways provided + by Spring Integration to support Message-based communication with + external systems. + + + + + + Appendices + + Advanced Topics and Additional Resources + + + + diff --git a/spring-integration-kafka/src/reference/docbook/resources.xml b/spring-integration-kafka/src/reference/docbook/resources.xml new file mode 100644 index 0000000..456ebba --- /dev/null +++ b/spring-integration-kafka/src/reference/docbook/resources.xml @@ -0,0 +1,15 @@ + + + Additional Resources + +
+ Spring Integration Home + + The definitive source of information about Spring Integration is the + Spring Integration Home at + http://www.springsource.org. That site serves as a hub of + information and is the best place to find up-to-date announcements about the project as well as links to + articles, blogs, and new sample applications. + +
+
diff --git a/spring-integration-kafka/src/reference/docbook/whats-new.xml b/spring-integration-kafka/src/reference/docbook/whats-new.xml new file mode 100644 index 0000000..ddf2480 --- /dev/null +++ b/spring-integration-kafka/src/reference/docbook/whats-new.xml @@ -0,0 +1,8 @@ + + + What's new? + + This chapter provides an overview of the new features and improvements + that have been added to the Kafka Adapter: + + diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParserTests.java new file mode 100644 index 0000000..50ae6fc --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaConsumerContextParserTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.kafka.support.ConsumerMetadata; +import org.springframework.integration.kafka.support.KafkaConsumerContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Soby Chacko + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class KafkaConsumerContextParserTests { + + @Autowired + private ApplicationContext appContext; + + @Test + @SuppressWarnings("unchecked") + public void testConsumerContextConfiguration() { + final KafkaConsumerContext consumerContext = appContext.getBean("consumerContext", KafkaConsumerContext.class); + Assert.assertNotNull(consumerContext); + + final ConsumerMetadata cm = appContext.getBean("consumerMetadata_default1", ConsumerMetadata.class); + Assert.assertNotNull(cm); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundAdapterParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundAdapterParserTests.java new file mode 100644 index 0000000..e7525b9 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaInboundAdapterParserTests.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.endpoint.SourcePollingChannelAdapter; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class KafkaInboundAdapterParserTests { + + @Autowired + private ApplicationContext appContext; + + /** + * Test method for {@link org.springframework.integration.kafka.config.xml.KafkaInboundChannelAdapterParser#parseSource(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)}. + */ + @Test + public void testParseSourceElementParserContext() throws Exception { + final SourcePollingChannelAdapter adapter = appContext.getBean("kafkaInboundChannelAdapter", + SourcePollingChannelAdapter.class); + + Assert.assertNotNull(adapter); + Assert.assertFalse(adapter.isAutoStartup()); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java new file mode 100644 index 0000000..3c69fcf --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaOutboundAdapterParserTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.endpoint.PollingConsumer; +import org.springframework.integration.kafka.outbound.KafkaProducerMessageHandler; +import org.springframework.integration.kafka.support.KafkaProducerContext; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class KafkaOutboundAdapterParserTests { + + @Autowired + private ApplicationContext appContext; + + @Test + public void testOutboundAdapterConfiguration(){ + final PollingConsumer pollingConsumer = appContext.getBean("kafkaOutboundChannelAdapter", PollingConsumer.class); + final KafkaProducerMessageHandler messageHandler = appContext.getBean(KafkaProducerMessageHandler.class); + Assert.assertNotNull(pollingConsumer); + Assert.assertNotNull(messageHandler); + final KafkaProducerContext producerContext = messageHandler.getKafkaProducerContext(); + Assert.assertNotNull(producerContext); + Assert.assertEquals(producerContext.getTopicsConfiguration().size(), 2); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParserTests.java new file mode 100644 index 0000000..ddf9718 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/KafkaProducerContextParserTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import junit.framework.Assert; +import kafka.javaapi.producer.Producer; +import kafka.serializer.Encoder; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.kafka.support.KafkaProducerContext; +import org.springframework.integration.kafka.support.ProducerConfiguration; +import org.springframework.integration.kafka.support.ProducerMetadata; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.Map; + +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class KafkaProducerContextParserTests { + + @Autowired + private ApplicationContext appContext; + + @Test + @SuppressWarnings("unchecked") + public void testProducerContextConfiguration(){ + final KafkaProducerContext producerContext = appContext.getBean("producerContext", KafkaProducerContext.class); + Assert.assertNotNull(producerContext); + + final Map topicConfigurations = producerContext.getTopicsConfiguration(); + Assert.assertEquals(topicConfigurations.size(), 2); + + final ProducerConfiguration producerConfigurationTest1 = topicConfigurations.get("producerConfiguration_test1"); + Assert.assertNotNull(producerConfigurationTest1); + final ProducerMetadata producerMetadataTest1 = producerConfigurationTest1.getProducerMetadata(); + Assert.assertEquals(producerMetadataTest1.getTopic(), "test1"); + Assert.assertEquals(producerMetadataTest1.getCompressionCodec(), "0"); + Assert.assertEquals(producerMetadataTest1.getKeyClassType(), java.lang.String.class); + Assert.assertEquals(producerMetadataTest1.getValueClassType(), java.lang.String.class); + + final Encoder valueEncoder = appContext.getBean("valueEncoder", Encoder.class); + Assert.assertEquals(producerMetadataTest1.getValueEncoder(), valueEncoder); + Assert.assertEquals(producerMetadataTest1.getKeyEncoder(), valueEncoder); + + final Producer producerTest1 = appContext.getBean("prodFactory_test1", Producer.class); + Assert.assertEquals(producerConfigurationTest1, new ProducerConfiguration(producerMetadataTest1, producerTest1)); + + final ProducerConfiguration producerConfigurationTest2 = topicConfigurations.get("producerConfiguration_" + "test2"); + Assert.assertNotNull(producerConfigurationTest2); + final ProducerMetadata producerMetadataTest2 = producerConfigurationTest2.getProducerMetadata(); + Assert.assertEquals(producerMetadataTest2.getTopic(), "test2"); + Assert.assertEquals(producerMetadataTest2.getCompressionCodec(), "0"); + + final Producer producerTest2 = appContext.getBean("prodFactory_test2", Producer.class); + Assert.assertEquals(producerConfigurationTest2, new ProducerConfiguration(producerMetadataTest2, producerTest2)); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParserTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParserTests.java new file mode 100644 index 0000000..6acfa87 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/config/xml/ZookeeperConnectParserTests.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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.integration.kafka.config.xml; + +import junit.framework.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.integration.kafka.core.ZookeeperConnectDefaults; +import org.springframework.integration.kafka.support.ZookeeperConnect; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +/** + * @author Soby Chacko + * @since 1.0 + * + */ +@RunWith(SpringJUnit4ClassRunner.class) +@ContextConfiguration +public class ZookeeperConnectParserTests { + + @Autowired + private ApplicationContext appContext; + + @Test + public void testCustomKafkaBrokerConfiguration() { + final ZookeeperConnect broker = appContext.getBean("zookeeperConnect", ZookeeperConnect.class); + + Assert.assertEquals("localhost:2181", broker.getZkConnect()); + Assert.assertEquals("10000", broker.getZkConnectionTimeout()); + Assert.assertEquals("10000", broker.getZkSessionTimeout()); + Assert.assertEquals("200", broker.getZkSyncTime()); + } + + @Test + public void testDefaultKafkaBrokerConfiguration() { + final ZookeeperConnect broker = appContext.getBean("defaultZookeeperConnect", ZookeeperConnect.class); + + Assert.assertEquals(ZookeeperConnectDefaults.ZK_CONNECT, broker.getZkConnect()); + Assert.assertEquals(ZookeeperConnectDefaults.ZK_CONNECTION_TIMEOUT, broker.getZkConnectionTimeout()); + Assert.assertEquals(ZookeeperConnectDefaults.ZK_SESSION_TIMEOUT, broker.getZkSessionTimeout()); + Assert.assertEquals(ZookeeperConnectDefaults.ZK_SYNC_TIME, broker.getZkSyncTime()); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/serializer/AvroBackedKafkaSerializerTest.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/serializer/AvroBackedKafkaSerializerTest.java new file mode 100644 index 0000000..a12e908 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/serializer/AvroBackedKafkaSerializerTest.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2013 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.integration.kafka.serializer; + +import junit.framework.Assert; +import org.junit.Test; +import org.springframework.integration.kafka.serializer.avro.AvroBackedKafkaDecoder; +import org.springframework.integration.kafka.serializer.avro.AvroBackedKafkaEncoder; +import org.springframework.integration.kafka.test.utils.TestObject; + +/** + * @author Soby Chacko + * @since 1.0 + */ +public class AvroBackedKafkaSerializerTest { + @Test + @SuppressWarnings("unchecked") + public void testDecodePlainSchema() { + final AvroBackedKafkaEncoder avroBackedKafkaEncoder = new AvroBackedKafkaEncoder(TestObject.class); + + final TestObject testObject = new TestObject(); + testObject.setTestData1("\"Test Data1\""); + testObject.setTestData2(1); + + final byte[] data = avroBackedKafkaEncoder.toBytes(testObject); + + final AvroBackedKafkaDecoder avroBackedKafkaDecoder = new AvroBackedKafkaDecoder(TestObject.class); + final TestObject decodedFbu = (TestObject) avroBackedKafkaDecoder.fromBytes(data); + + Assert.assertEquals(testObject.getTestData1(), decodedFbu.getTestData1()); + Assert.assertEquals(testObject.getTestData2(), decodedFbu.getTestData2()); + } + + @Test + @SuppressWarnings("unchecked") + public void anotherTest() { + final AvroBackedKafkaEncoder avroBackedKafkaEncoder = new AvroBackedKafkaEncoder(java.lang.String.class); + final String testString = "Testing Avro"; + final byte[] data = avroBackedKafkaEncoder.toBytes(testString); + + final AvroBackedKafkaDecoder avroBackedKafkaDecoder = new AvroBackedKafkaDecoder(java.lang.String.class); + final String decodedS = (String) avroBackedKafkaDecoder.fromBytes(data); + + Assert.assertEquals(testString, decodedS); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java new file mode 100644 index 0000000..f41f18f --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ConsumerConfigurationTests.java @@ -0,0 +1,306 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import junit.framework.Assert; +import kafka.consumer.ConsumerIterator; +import kafka.consumer.KafkaStream; +import kafka.javaapi.consumer.ConsumerConnector; +import kafka.message.MessageAndMetadata; +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class ConsumerConfigurationTests { + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForSingleTopicFromSingleStream() { + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(1); + + final KafkaStream stream = Mockito.mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic", streams); + + Mockito.when(consumerConfiguration.getConsumerMapWithMessageStreams()).thenReturn(messageStreams); + final ConsumerIterator iterator = Mockito.mock(ConsumerIterator.class); + Mockito.when(stream.iterator()).thenReturn(iterator); + final MessageAndMetadata messageAndMetadata = Mockito.mock(MessageAndMetadata.class); + Mockito.when(iterator.next()).thenReturn(messageAndMetadata); + Mockito.when(messageAndMetadata.message()).thenReturn("got message"); + Mockito.when(messageAndMetadata.topic()).thenReturn("topic"); + Mockito.when(messageAndMetadata.partition()).thenReturn(1); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(messages.size(), 1); + Assert.assertEquals(messages.get("topic").size(), 1); + Assert.assertEquals(messages.get("topic").get(1).get(0), "got message"); + + Mockito.verify(stream, Mockito.times(1)).iterator(); + Mockito.verify(iterator, Mockito.times(1)).next(); + Mockito.verify(messageAndMetadata, Mockito.times(1)).message(); + Mockito.verify(messageAndMetadata, Mockito.times(1)).topic(); + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForSingleTopicFromMultipleStreams() { + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(3); + + final KafkaStream stream1 = Mockito.mock(KafkaStream.class); + final KafkaStream stream2 = Mockito.mock(KafkaStream.class); + final KafkaStream stream3 = Mockito.mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream1); + streams.add(stream2); + streams.add(stream3); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic", streams); + + Mockito.when(consumerConfiguration.getConsumerMapWithMessageStreams()).thenReturn(messageStreams); + final ConsumerIterator iterator1 = Mockito.mock(ConsumerIterator.class); + final ConsumerIterator iterator2 = Mockito.mock(ConsumerIterator.class); + final ConsumerIterator iterator3 = Mockito.mock(ConsumerIterator.class); + + Mockito.when(stream1.iterator()).thenReturn(iterator1); + Mockito.when(stream2.iterator()).thenReturn(iterator2); + Mockito.when(stream3.iterator()).thenReturn(iterator3); + final MessageAndMetadata messageAndMetadata1 = Mockito.mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata2 = Mockito.mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata3 = Mockito.mock(MessageAndMetadata.class); + + Mockito.when(iterator1.next()).thenReturn(messageAndMetadata1); + Mockito.when(iterator2.next()).thenReturn(messageAndMetadata2); + Mockito.when(iterator3.next()).thenReturn(messageAndMetadata3); + + Mockito.when(messageAndMetadata1.message()).thenReturn("got message"); + Mockito.when(messageAndMetadata1.topic()).thenReturn("topic"); + Mockito.when(messageAndMetadata1.partition()).thenReturn(1); + + Mockito.when(messageAndMetadata2.message()).thenReturn("got message"); + Mockito.when(messageAndMetadata2.topic()).thenReturn("topic"); + Mockito.when(messageAndMetadata2.partition()).thenReturn(2); + + Mockito.when(messageAndMetadata3.message()).thenReturn("got message"); + Mockito.when(messageAndMetadata3.topic()).thenReturn("topic"); + Mockito.when(messageAndMetadata3.partition()).thenReturn(3); + + final Map>> messages = consumerConfiguration.receive(); + Assert.assertEquals(messages.size(), 1); + int sum = 0; + + final Map> values = messages.get("topic"); + + for (final List l : values.values()) { + sum += l.size(); + } + + Assert.assertEquals(sum, 3); + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageForMultipleTopicsFromMultipleStreams() { + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(9); + + final KafkaStream stream1 = Mockito.mock(KafkaStream.class); + final KafkaStream stream2 = Mockito.mock(KafkaStream.class); + final KafkaStream stream3 = Mockito.mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream1); + streams.add(stream2); + streams.add(stream3); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic1", streams); + messageStreams.put("topic2", streams); + messageStreams.put("topic3", streams); + + Mockito.when(consumerConfiguration.getConsumerMapWithMessageStreams()).thenReturn(messageStreams); + final ConsumerIterator iterator1 = Mockito.mock(ConsumerIterator.class); + final ConsumerIterator iterator2 = Mockito.mock(ConsumerIterator.class); + final ConsumerIterator iterator3 = Mockito.mock(ConsumerIterator.class); + + Mockito.when(stream1.iterator()).thenReturn(iterator1); + Mockito.when(stream2.iterator()).thenReturn(iterator2); + Mockito.when(stream3.iterator()).thenReturn(iterator3); + final MessageAndMetadata messageAndMetadata1 = Mockito.mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata2 = Mockito.mock(MessageAndMetadata.class); + final MessageAndMetadata messageAndMetadata3 = Mockito.mock(MessageAndMetadata.class); + + Mockito.when(iterator1.next()).thenReturn(messageAndMetadata1); + Mockito.when(iterator2.next()).thenReturn(messageAndMetadata2); + Mockito.when(iterator3.next()).thenReturn(messageAndMetadata3); + + Mockito.when(messageAndMetadata1.message()).thenReturn("got message1"); + Mockito.when(messageAndMetadata1.topic()).thenReturn("topic1"); + Mockito.when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + Mockito.when(messageAndMetadata2.message()).thenReturn("got message2"); + Mockito.when(messageAndMetadata2.topic()).thenReturn("topic2"); + Mockito.when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + Mockito.when(messageAndMetadata3.message()).thenReturn("got message3"); + Mockito.when(messageAndMetadata3.topic()).thenReturn("topic3"); + Mockito.when(messageAndMetadata1.partition()).thenAnswer(getAnswer()); + + final Map>> messages = consumerConfiguration.receive(); + int sum = 0; + + final Collection>> values = messages.values(); + + for (final Map> m : values) { + for (final List l : m.values()) { + sum += l.size(); + } + } + + Assert.assertEquals(sum, 9); + } + + private Answer getAnswer() { + return new Answer() { + private int count = 0; + + @Override + public Object answer(final InvocationOnMock invocation) throws Throwable { + if (count++ == 1) { + return 1; + } else if (count++ == 2) { + return 2; + } + + return 3; + } + }; + } + + @Test + @SuppressWarnings("unchecked") + public void testReceiveMessageAndVerifyMessageLeftoverFromPreviousPollAreTakenFirst() { + final ConsumerMetadata consumerMetadata = Mockito.mock(ConsumerMetadata.class); + final ConsumerConnectionProvider consumerConnectionProvider = + Mockito.mock(ConsumerConnectionProvider.class); + final MessageLeftOverTracker messageLeftOverTracker = Mockito.mock(MessageLeftOverTracker.class); + final ConsumerConnector consumerConnector = Mockito.mock(ConsumerConnector.class); + + Mockito.when(messageLeftOverTracker.getCurrentCount()).thenReturn(3); + final MessageAndMetadata m1 = new MessageAndMetadata("key1", "value1", "topic1", 1, 1L); + final MessageAndMetadata m2 = new MessageAndMetadata("key2", "value2", "topic2", 1, 1L); + final MessageAndMetadata m3 = new MessageAndMetadata("key1", "value3", "topic3", 1, 1L); + + final List mList = new ArrayList(); + mList.add(m1); + mList.add(m2); + mList.add(m3); + + Mockito.when(messageLeftOverTracker.getMessageLeftOverFromPreviousPoll()).thenReturn(mList); + + Mockito.when(consumerConnectionProvider.getConsumerConnector()).thenReturn(consumerConnector); + + final ConsumerConfiguration consumerConfiguration = new ConsumerConfiguration(consumerMetadata, + consumerConnectionProvider, messageLeftOverTracker); + consumerConfiguration.setMaxMessages(5); + + final KafkaStream stream = Mockito.mock(KafkaStream.class); + final List> streams = new ArrayList>(); + streams.add(stream); + final Map>> messageStreams = new HashMap>>(); + messageStreams.put("topic1", streams); + + Mockito.when(consumerConfiguration.getConsumerMapWithMessageStreams()).thenReturn(messageStreams); + final ConsumerIterator iterator = Mockito.mock(ConsumerIterator.class); + Mockito.when(stream.iterator()).thenReturn(iterator); + final MessageAndMetadata messageAndMetadata = Mockito.mock(MessageAndMetadata.class); + Mockito.when(iterator.next()).thenReturn(messageAndMetadata); + Mockito.when(messageAndMetadata.message()).thenReturn("got message"); + Mockito.when(messageAndMetadata.topic()).thenReturn("topic1"); + Mockito.when(messageAndMetadata.partition()).thenReturn(1); + + final Map>> messages = consumerConfiguration.receive(); + int sum = 0; + + final Collection>> values = messages.values(); + + for (final Map> m : values) { + for (final List l : m.values()) { + sum += l.size(); + } + + } + Assert.assertEquals(sum, 5); + + Assert.assertTrue(messages.containsKey("topic1")); + Assert.assertTrue(messages.containsKey("topic2")); + Assert.assertTrue(messages.containsKey("topic3")); + + Assert.assertTrue(valueFound(messages.get("topic1").get(1), "value1")); + Assert.assertTrue(valueFound(messages.get("topic2").get(1), "value2")); + Assert.assertTrue(valueFound(messages.get("topic3").get(1), "value3")); + } + + private boolean valueFound(final List l, final String value){ + for (final Object o : l){ + if (value.equals(o)){ + return true; + } + } + + return false; + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/KafkaConsumerContextTest.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/KafkaConsumerContextTest.java new file mode 100644 index 0000000..12bc189 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/KafkaConsumerContextTest.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.integration.Message; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * @author Soby Chacko + */ +public class KafkaConsumerContextTest { + + @Test + public void testMergeResultsFromMultipleConsumerConfiguration() { + final KafkaConsumerContext kafkaConsumerContext = new KafkaConsumerContext(); + final ListableBeanFactory beanFactory = Mockito.mock(ListableBeanFactory.class); + final ConsumerConfiguration consumerConfiguration1 = Mockito.mock(ConsumerConfiguration.class); + final ConsumerConfiguration consumerConfiguration2 = Mockito.mock(ConsumerConfiguration.class); + + final Map map = new HashMap(); + map.put("config1", consumerConfiguration1); + map.put("config2", consumerConfiguration2); + + Mockito.when(beanFactory.getBeansOfType(ConsumerConfiguration.class)).thenReturn(map); + kafkaConsumerContext.setBeanFactory(beanFactory); + + final Map>> result1 = new HashMap>>(); + final List l1 = new ArrayList(); + l1.add("got message1 - l1"); + l1.add("got message2 - l1"); + final Map> innerMap1 = new HashMap>(); + innerMap1.put(1, l1); + result1.put("topic1", innerMap1); + + final Map>> result2 = new HashMap>>(); + final List l2 = new ArrayList(); + l2.add("got message1 - l2"); + l2.add("got message2 - l2"); + l2.add("got message3 - l2"); + + final Map> innerMap2 = new HashMap>(); + innerMap2.put(1, l2); + result1.put("topic2", innerMap2); + + Mockito.when(consumerConfiguration1.receive()).thenReturn(result1); + Mockito.when(consumerConfiguration2.receive()).thenReturn(result2); + + final Message>>> messages = kafkaConsumerContext.receive(); + Assert.assertEquals(messages.getPayload().size(), 2); + Assert.assertEquals(messages.getPayload().get("topic1").size(), 1); + Assert.assertEquals(messages.getPayload().get("topic1").get(1).get(0), "got message1 - l1"); + Assert.assertEquals(messages.getPayload().get("topic1").get(1).get(1), "got message2 - l1"); + + Assert.assertEquals(messages.getPayload().get("topic2").size(), 1); + Assert.assertEquals(messages.getPayload().get("topic2").get(1).get(0), "got message1 - l2"); + Assert.assertEquals(messages.getPayload().get("topic2").get(1).get(1), "got message2 - l2"); + Assert.assertEquals(messages.getPayload().get("topic2").get(1).get(2), "got message3 - l2"); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerConfigurationTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerConfigurationTests.java new file mode 100644 index 0000000..2fa2021 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerConfigurationTests.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import kafka.javaapi.producer.Producer; +import kafka.producer.KeyedMessage; +import kafka.serializer.DefaultEncoder; +import kafka.serializer.StringEncoder; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.springframework.integration.Message; +import org.springframework.integration.kafka.serializer.avro.AvroBackedKafkaEncoder; +import org.springframework.integration.kafka.test.utils.NonSerializableTestKey; +import org.springframework.integration.kafka.test.utils.NonSerializableTestPayload; +import org.springframework.integration.kafka.test.utils.TestKey; +import org.springframework.integration.kafka.test.utils.TestPayload; +import org.springframework.integration.support.MessageBuilder; + +import java.io.ByteArrayInputStream; +import java.io.NotSerializableException; +import java.io.ObjectInputStream; + +/** + * @author Soby Chacko + */ +public class ProducerConfigurationTests { + @Test + @SuppressWarnings("unchecked") + public void testSendMessageWithNonDefaultKeyAndValueEncoders() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new StringEncoder(null)); + producerMetadata.setKeyEncoder(new StringEncoder(null)); + producerMetadata.setKeyClassType(String.class); + producerMetadata.setValueClassType(String.class); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload("test message"). + setHeader("messageKey", "key") + .setHeader("topic", "test").build(); + + configuration.send(message); + + Mockito.verify(producer, Mockito.times(1)).send(Mockito.any(KeyedMessage.class)); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(KeyedMessage.class); + Mockito.verify(producer).send(argument.capture()); + + final KeyedMessage capturedKeyMessage = argument.getValue(); + + Assert.assertEquals(capturedKeyMessage.key(), "key"); + Assert.assertEquals(capturedKeyMessage.message(), "test message"); + Assert.assertEquals(capturedKeyMessage.topic(), "test"); + } + + /** + * User does not set an explicit key/value encoder, but send a serializable object for both key/value + */ + @Test + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyAndValueEncodersAndCustomSerializableKeyAndPayloadObject() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload(new TestPayload("part1", "part2")). + setHeader("messageKey", new TestKey("compositePart1", "compositePart2")) + .setHeader("topic", "test").build(); + + configuration.send(message); + + Mockito.verify(producer, Mockito.times(1)).send(Mockito.any(KeyedMessage.class)); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(KeyedMessage.class); + Mockito.verify(producer).send(argument.capture()); + + final KeyedMessage capturedKeyMessage = argument.getValue(); + + final byte[] keyBytes = (byte[])capturedKeyMessage.key(); + + final ByteArrayInputStream keyInputStream = new ByteArrayInputStream (keyBytes); + final ObjectInputStream keyObjectInputStream = new ObjectInputStream (keyInputStream); + final Object keyObj = keyObjectInputStream.readObject(); + + final TestKey tk = (TestKey)keyObj; + + Assert.assertEquals(tk.getKeyPart1(), "compositePart1"); + Assert.assertEquals(tk.getKeyPart2(), "compositePart2"); + + final byte[] messageBytes = (byte[])capturedKeyMessage.message(); + + final ByteArrayInputStream messageInputStream = new ByteArrayInputStream (messageBytes); + final ObjectInputStream messageObjectInputStream = new ObjectInputStream (messageInputStream); + final Object messageObj = messageObjectInputStream.readObject(); + + final TestPayload tp = (TestPayload)messageObj; + + Assert.assertEquals(tp.getPart1(), "part1"); + Assert.assertEquals(tp.getPart2(), "part2"); + + Assert.assertEquals(capturedKeyMessage.topic(), "test"); + } + + /** + * User does not set an explicit key encoder, but a value encoder, and sends the corresponding data + */ + @Test + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyEncoderAndNonDefaultValueEncoderAndCorrespondingData() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + final AvroBackedKafkaEncoder encoder = new AvroBackedKafkaEncoder(TestPayload.class); + producerMetadata.setValueEncoder(encoder); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + producerMetadata.setValueClassType(TestPayload.class); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + final TestPayload tp = new TestPayload("part1", "part2"); + final Message message = MessageBuilder.withPayload(tp). + setHeader("messageKey", "key") + .setHeader("topic", "test").build(); + + configuration.send(message); + + Mockito.verify(producer, Mockito.times(1)).send(Mockito.any(KeyedMessage.class)); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(KeyedMessage.class); + Mockito.verify(producer).send(argument.capture()); + + final KeyedMessage capturedKeyMessage = argument.getValue(); + + final byte[] keyBytes = (byte[])capturedKeyMessage.key(); + + final ByteArrayInputStream keyInputStream = new ByteArrayInputStream (keyBytes); + final ObjectInputStream keyObjectInputStream = new ObjectInputStream (keyInputStream); + final Object keyObj = keyObjectInputStream.readObject(); + + Assert.assertEquals("key", keyObj); + Assert.assertEquals(capturedKeyMessage.message(), tp); + + Assert.assertEquals(capturedKeyMessage.topic(), "test"); + } + + /** + * User does set an explicit key encoder, but not a value encoder, and sends the corresponding data + */ + @Test + @SuppressWarnings("unchecked") + public void testSendMessageWithNonDefaultKeyEncoderAndDefaultValueEncoderAndCorrespondingData() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + final AvroBackedKafkaEncoder encoder = new AvroBackedKafkaEncoder(TestKey.class); + producerMetadata.setKeyEncoder(encoder); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyClassType(TestKey.class); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + final TestKey tk = new TestKey("part1", "part2"); + final Message message = MessageBuilder.withPayload("test message"). + setHeader("messageKey", tk) + .setHeader("topic", "test").build(); + + configuration.send(message); + + Mockito.verify(producer, Mockito.times(1)).send(Mockito.any(KeyedMessage.class)); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(KeyedMessage.class); + Mockito.verify(producer).send(argument.capture()); + + final KeyedMessage capturedKeyMessage = argument.getValue(); + + Assert.assertEquals(capturedKeyMessage.key(), tk); + + final byte[] payloadBytes = (byte[])capturedKeyMessage.message(); + + final ByteArrayInputStream payloadBis = new ByteArrayInputStream (payloadBytes); + final ObjectInputStream payloadOis = new ObjectInputStream (payloadBis); + final Object payloadObj = payloadOis.readObject(); + + Assert.assertEquals("test message", payloadObj); + + Assert.assertEquals(capturedKeyMessage.topic(), "test"); + } + + /** + * User does not set an explicit key/value encoder, but send a serializable String key/value pair + */ + @Test + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyAndValueEncodersAndStringKeyAndValue() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload("test message"). + setHeader("messageKey", "key") + .setHeader("topic", "test").build(); + + configuration.send(message); + + Mockito.verify(producer, Mockito.times(1)).send(Mockito.any(KeyedMessage.class)); + + final ArgumentCaptor argument = ArgumentCaptor.forClass(KeyedMessage.class); + Mockito.verify(producer).send(argument.capture()); + + final KeyedMessage capturedKeyMessage = argument.getValue(); + final byte[] keyBytes = (byte[])capturedKeyMessage.key(); + + final ByteArrayInputStream keyBis = new ByteArrayInputStream (keyBytes); + final ObjectInputStream keyOis = new ObjectInputStream (keyBis); + final Object keyObj = keyOis.readObject(); + + Assert.assertEquals("key", keyObj); + + final byte[] payloadBytes = (byte[])capturedKeyMessage.message(); + + final ByteArrayInputStream payloadBis = new ByteArrayInputStream (payloadBytes); + final ObjectInputStream payloadOis = new ObjectInputStream (payloadBis); + final Object payloadObj = payloadOis.readObject(); + + Assert.assertEquals("test message", payloadObj); + Assert.assertEquals(capturedKeyMessage.topic(), "test"); + } + + /** + * User does not set an explicit key/value encoder, but send non-serializable object for both key/value + */ + @Test(expected = NotSerializableException.class) + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyAndValueEncodersButNonSerializableKeyAndValue() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload(new NonSerializableTestPayload("part1", "part2")). + setHeader("messageKey", new NonSerializableTestKey("compositePart1", "compositePart2")) + .setHeader("topic", "test").build(); + configuration.send(message); + } + + /** + * User does not set an explicit key/value encoder, but send non-serializable key and serializable value + */ + @Test(expected = NotSerializableException.class) + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyAndValueEncodersButNonSerializableKeyAndSerializableValue() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload(new TestPayload("part1", "part2")). + setHeader("messageKey", new NonSerializableTestKey("compositePart1", "compositePart2")) + .setHeader("topic", "test").build(); + configuration.send(message); + } + + /** + * User does not set an explicit key/value encoder, but send serializable key and non-serializable value + */ + @Test(expected = NotSerializableException.class) + @SuppressWarnings("unchecked") + public void testSendMessageWithDefaultKeyAndValueEncodersButSerializableKeyAndNonSerializableValue() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setValueEncoder(new DefaultEncoder(null)); + producerMetadata.setKeyEncoder(new DefaultEncoder(null)); + final Producer producer = Mockito.mock(Producer.class); + + final ProducerConfiguration configuration = new ProducerConfiguration(producerMetadata, producer); + + final Message message = MessageBuilder.withPayload(new NonSerializableTestPayload("part1", "part2")). + setHeader("messageKey", new TestKey("compositePart1", "compositePart2")) + .setHeader("topic", "test").build(); + configuration.send(message); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerFactoryBeanTests.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerFactoryBeanTests.java new file mode 100644 index 0000000..0089e73 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/support/ProducerFactoryBeanTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2013 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.integration.kafka.support; + +import junit.framework.Assert; +import kafka.javaapi.producer.Producer; +import org.junit.Test; +import org.mockito.Mockito; + +/** + * @author Soby Chacko + */ +public class ProducerFactoryBeanTests { + + @Test + public void createProducerWithDefaultMetadata() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + final ProducerMetadata tm = Mockito.spy(producerMetadata); + final ProducerFactoryBean producerFactoryBean = new ProducerFactoryBean(tm, "localhost:9092"); + final Producer producer = producerFactoryBean.getObject(); + + Assert.assertTrue(producer != null); + + Mockito.verify(tm, Mockito.times(1)).getPartitioner(); + Mockito.verify(tm, Mockito.times(1)).getCompressionCodec(); + Mockito.verify(tm, Mockito.times(1)).getValueEncoder(); + Mockito.verify(tm, Mockito.times(1)).getKeyEncoder(); + Mockito.verify(tm, Mockito.times(1)).isAsync(); + Mockito.verify(tm, Mockito.times(0)).getBatchNumMessages(); + } + + @Test + public void createProducerWithAsyncFeatures() throws Exception { + final ProducerMetadata producerMetadata = new ProducerMetadata("test"); + producerMetadata.setAsync(true); + producerMetadata.setBatchNumMessages("300"); + final ProducerMetadata tm = Mockito.spy(producerMetadata); + final ProducerFactoryBean producerFactoryBean = new ProducerFactoryBean(tm, "localhost:9092"); + final Producer producer = producerFactoryBean.getObject(); + + Assert.assertTrue(producer != null); + + Mockito.verify(tm, Mockito.times(1)).getPartitioner(); + Mockito.verify(tm, Mockito.times(1)).getCompressionCodec(); + Mockito.verify(tm, Mockito.times(1)).getValueEncoder(); + Mockito.verify(tm, Mockito.times(1)).getKeyEncoder(); + Mockito.verify(tm, Mockito.times(1)).isAsync(); + Mockito.verify(tm, Mockito.times(2)).getBatchNumMessages(); + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestKey.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestKey.java new file mode 100644 index 0000000..eef22df --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestKey.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2013 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.integration.kafka.test.utils; + +public class NonSerializableTestKey { + private final String keyPart1; + private final String keyPart2; + + public NonSerializableTestKey(final String keyPart1, final String keyPart2) { + this.keyPart1 = keyPart1; + this.keyPart2 = keyPart2; + } + + public String getKeyPart1() { + return keyPart1; + } + + public String getKeyPart2() { + return keyPart2; + } + +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestPayload.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestPayload.java new file mode 100644 index 0000000..6b2bb2f --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/NonSerializableTestPayload.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2013 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.integration.kafka.test.utils; + +public class NonSerializableTestPayload { + private final String part1; + private final String part2; + + public NonSerializableTestPayload(final String part1, final String part2) { + this.part1 = part1; + this.part2 = part2; + } + + public String getPart1() { + return part1; + } + + public String getPart2() { + return part2; + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestKey.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestKey.java new file mode 100644 index 0000000..10e0480 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestKey.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 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.integration.kafka.test.utils; + +import java.io.Serializable; + +public class TestKey implements Serializable { + private static final long serialVersionUID = -6415387283545560656L; + + private final String keyPart1; + private final String keyPart2; + + public TestKey(final String keyPart1, final String keyPart2) { + this.keyPart1 = keyPart1; + this.keyPart2 = keyPart2; + } + + public String getKeyPart1() { + return keyPart1; + } + + public String getKeyPart2() { + return keyPart2; + } +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestObject.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestObject.java new file mode 100644 index 0000000..42c2ec1 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestObject.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 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.integration.kafka.test.utils; + +public class TestObject { + public String testData1; + public int testData2; + + public String getTestData1() { + return testData1; + } + + public void setTestData1(final String testData1) { + this.testData1 = testData1; + } + + public int getTestData2() { + return testData2; + } + + public void setTestData2(final int testData2) { + this.testData2 = testData2; + } + +} diff --git a/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestPayload.java b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestPayload.java new file mode 100644 index 0000000..3ac94f8 --- /dev/null +++ b/spring-integration-kafka/src/test/java/org/springframework/integration/kafka/test/utils/TestPayload.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2013 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.integration.kafka.test.utils; + +import java.io.Serializable; + +public class TestPayload implements Serializable { + private static final long serialVersionUID = -8560378224929007403L; + + private final String part1; + private final String part2; + + public TestPayload(final String part1, final String part2){ + this.part1 = part1; + this.part2 = part2; + } + + public String getPart1() { + return part1; + } + + public String getPart2() { + return part2; + } +} diff --git a/spring-integration-kafka/src/test/resources/log4j.properties b/spring-integration-kafka/src/test/resources/log4j.properties new file mode 100644 index 0000000..f428439 --- /dev/null +++ b/spring-integration-kafka/src/test/resources/log4j.properties @@ -0,0 +1,8 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss.SSS} %-5p [%t][%c] %m%n + +log4j.category.org.springframework.integration=WARN +log4j.category.org.springframework.integration.kafka=INFO diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaConsumerContextParserTests-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaConsumerContextParserTests-context.xml new file mode 100644 index 0000000..e0978bb --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaConsumerContextParserTests-context.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterCommon-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterCommon-context.xml new file mode 100644 index 0000000..f7fecae --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterCommon-context.xml @@ -0,0 +1,17 @@ + + + + + + + + + diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterParserTests-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterParserTests-context.xml new file mode 100644 index 0000000..70266d3 --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaInboundAdapterParserTests-context.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaOutboundAdapterParserTests-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaOutboundAdapterParserTests-context.xml new file mode 100644 index 0000000..52eef96 --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaOutboundAdapterParserTests-context.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaProducerContextParserTests-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaProducerContextParserTests-context.xml new file mode 100644 index 0000000..8f9750a --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/kafkaProducerContextParserTests-context.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/zookeeperConnectParserTests-context.xml b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/zookeeperConnectParserTests-context.xml new file mode 100644 index 0000000..f5d0784 --- /dev/null +++ b/spring-integration-kafka/src/test/resources/org/springframework/integration/kafka/config/xml/zookeeperConnectParserTests-context.xml @@ -0,0 +1,14 @@ + + + + + + +