diff --git a/.gitignore b/.gitignore index ba594ff03d4c4..04f8feed0ad70 100644 --- a/.gitignore +++ b/.gitignore @@ -50,4 +50,4 @@ docs/generated/ kafkatest.egg-info/ systest/ - +*.swp diff --git a/build.gradle b/build.gradle index 69f560ec38fef..cd68cab5d1ec7 100644 --- a/build.gradle +++ b/build.gradle @@ -124,7 +124,8 @@ if (new File('.git').exists()) { '**/id_rsa', '**/id_rsa.pub', 'checkstyle/suppressions.xml', - 'streams/quickstart/java/src/test/resources/projects/basic/goal.txt' + 'streams/quickstart/java/src/test/resources/projects/basic/goal.txt', + 'streams/streams-scala/logs/*' ]) } } @@ -518,7 +519,7 @@ for ( sv in availableScalaVersions ) { } def connectPkgs = ['connect:api', 'connect:runtime', 'connect:transforms', 'connect:json', 'connect:file'] -def pkgs = ['clients', 'examples', 'log4j-appender', 'tools', 'streams', 'streams:test-utils', 'streams:examples'] + connectPkgs +def pkgs = ['clients', 'examples', 'log4j-appender', 'tools', 'streams', 'streams:streams-scala', 'streams:test-utils', 'streams:examples'] + connectPkgs /** Create one task per default Scala version */ def withDefScalaVersions(taskName) { @@ -733,6 +734,8 @@ project(':core') { from(project(':connect:file').configurations.runtime) { into("libs/") } from(project(':streams').jar) { into("libs/") } from(project(':streams').configurations.runtime) { into("libs/") } + from(project(':streams:streams-scala').jar) { into("libs/") } + from(project(':streams:streams-scala').configurations.runtime) { into("libs/") } from(project(':streams:test-utils').jar) { into("libs/") } from(project(':streams:test-utils').configurations.runtime) { into("libs/") } from(project(':streams:examples').jar) { into("libs/") } @@ -965,6 +968,46 @@ project(':streams') { } } +project(':streams:streams-scala') { + println "Building project 'streams-scala' with Scala version ${versions.scala}" + apply plugin: 'scala' + archivesBaseName = "kafka-streams-scala" + + dependencies { + compile project(':streams') + + compile libs.scalaLibrary + + testCompile project(':core') + testCompile project(':core').sourceSets.test.output + testCompile project(':streams').sourceSets.test.output + testCompile project(':clients').sourceSets.test.output + testCompile libs.scalaLogging + + testCompile libs.junit + testCompile libs.scalatest + + testRuntime libs.slf4jlog4j + } + + javadoc { + include "**/org/apache/kafka/streams/scala/**" + } + + tasks.create(name: "copyDependantLibs", type: Copy) { + from (configurations.runtime) { + exclude('kafka-streams*') + } + into "$buildDir/dependant-libs-${versions.scala}" + duplicatesStrategy 'exclude' + } + + jar { + dependsOn 'copyDependantLibs' + } + +} + project(':streams:test-utils') { archivesBaseName = "kafka-streams-test-utils" diff --git a/docs/api.html b/docs/api.html index 97771861883dd..015d5140bd00a 100644 --- a/docs/api.html +++ b/docs/api.html @@ -78,6 +78,19 @@
+ When using Scala you may optionally include the kafka-streams-scala library. Additional documentation on using the Kafka Streams DSL for Scala is available in the developer guide.
+
+ To use Kafka Streams DSL for Scala for Scala 2.11 you can use the following maven dependency: + +
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka-streams-scala_2.11</artifactId>
+ <version>{{fullDotVersion}}</version>
+ </dependency>
+
+
SerDes specified in the Streams configuration via StreamsConfig are used as the default in your Kafka Streams application.
If you need to implement custom SerDes, your best starting point is to take a look at the source code references of - existing SerDes (see previous section). Typically, your workflow will be similar to:
-T by implementing
- org.apache.kafka.common.serialization.Serializer.T by implementing
- org.apache.kafka.common.serialization.Deserializer.T by implementing
- org.apache.kafka.common.serialization.Serde,
- which you either do manually (see existing SerDes in the previous section) or by leveraging helper functions in
- Serdes
- such as Serdes.serdeFrom(Serializer<T>, Deserializer<T>).If you need to implement custom SerDes, your best starting point is to take a look at the source code references of + existing SerDes (see previous section). Typically, your workflow will be similar to:
+T by implementing
+ org.apache.kafka.common.serialization.Serializer.T by implementing
+ org.apache.kafka.common.serialization.Deserializer.T by implementing
+ org.apache.kafka.common.serialization.Serde,
+ which you either do manually (see existing SerDes in the previous section) or by leveraging helper functions in
+ Serdes
+ such as Serdes.serdeFrom(Serializer<T>, Deserializer<T>).When using the Kafka Streams DSL for Scala you're not required to configure a default SerDes. In fact, it's not supported. SerDes are instead provided implicitly by default implementations for common primitive datatypes. See the Implicit SerDes and User-Defined SerDes sections in the DSL API documentation for details
+test-utils module to help you test your application here.
+ The Kafka Streams DSL Java APIs are based on the Builder design pattern, which allows users to incrementally build the target functionality using lower level compositional fluent APIs. These APIs can be called from Scala, but there are several issues:
+The Kafka Streams DSL for Scala library is a wrapper over the existing Java APIs for Kafka Streams DSL that addresses the concerns raised above. + It does not attempt to provide idiomatic Scala APIs that one would implement in a Scala library developed from scratch. The intention is to make the Java APIs more usable in Scala through better type inferencing, enhanced expressiveness, and lesser boilerplates. +
+The library wraps Java Stream DSL APIs in Scala thereby providing:
+All functionality provided by Kafka Streams DSL for Scala are under the root package name of org.apache.kafka.streams.scala.
Many of the public facing types from the Java API are wrapped. The following Scala abstractions are available to the user:
+org.apache.kafka.streams.scala.StreamsBuilderorg.apache.kafka.streams.scala.kstream.KStreamorg.apache.kafka.streams.scala.kstream.KTableorg.apache.kafka.streams.scala.kstream.KGroupedStreamorg.apache.kafka.streams.scala.kstream.KGroupedTableorg.apache.kafka.streams.scala.kstream.SessionWindowedKStreamorg.apache.kafka.streams.scala.kstream.TimeWindowedKStreamThe library also has several utility abstractions and modules that the user needs to use for proper semantics.
+org.apache.kafka.streams.scala.ImplicitConversions: Module that brings into scope the implicit conversions between the Scala and Java classes.org.apache.kafka.streams.scala.DefaultSerdes: Module that brings into scope the implicit values of all primitive SerDes.org.apache.kafka.streams.scala.ScalaSerde: Base abstraction that can be used to implement custom SerDes in a type safe way.The library is cross-built with Scala 2.11 and 2.12. To reference the library compiled against Scala 2.11 include the following in your maven pom.xml add the following:
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka-streams-scala_2.11</artifactId>
+ <version>{{fullDotVersion}}</version>
+ </dependency>
+
+ To use the library compiled against Scala 2.12 replace the artifactId with kafka-streams-scala_2.12.
When using SBT then you can reference the correct library using the following:
+
+ libraryDependencies += "org.apache.kafka" %% "kafka-streams-scala" % "{{fullDotVersion}}"
+
+ The library works by wrapping the original Java abstractions of Kafka Streams within a Scala wrapper object and then using implicit conversions between them. All the Scala abstractions are named identically as the corresponding Java abstraction, but they reside in a different package of the library e.g. the Scala class org.apache.kafka.streams.scala.StreamsBuilder is a wrapper around org.apache.kafka.streams.StreamsBuilder, org.apache.kafka.streams.scala.kstream.KStream is a wrapper around org.apache.kafka.streams.kstream.KStream, and so on.
Here's an example of the classic WordCount program that uses the Scala StreamsBuilder that builds an instance of KStream which is a wrapper around Java KStream. Then we reify to a table and get a KTable, which, again is a wrapper around Java KTable.
The net result is that the following code is structured just like using the Java API, but with Scala and with far fewer type annotations compared to using the Java API directly from Scala. The difference in type annotation usage is more obvious when given an example. Below is an example WordCount implementation that will be used to demonstrate the differences between the Scala and Java API.
+
+import java.util.Properties
+import java.util.concurrent.TimeUnit
+
+import org.apache.kafka.streams.kstream.Materialized
+import org.apache.kafka.streams.{KafkaStreams, StreamsConfig}
+
+object WordCountApplication extends App {
+ import DefaultSerdes._
+ import ImplicitConversions._
+
+ val config: Properties = {
+ val p = new Properties()
+ p.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application")
+ p.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092")
+ p
+ }
+
+ val builder = new StreamsBuilder()
+ val textLines = builder.stream[String, String]("TextLinesTopic")
+ val wordCounts = textLines
+ .flatMapValues(textLine => textLine.toLowerCase.split("\\W+"))
+ .groupBy((_, word) => word)
+ .count(Materialized.as("counts-store"))
+ wordCounts.toStream.to("WordsWithCountsTopic")
+
+ val streams: KafkaStreams = new KafkaStreams(builder.build(), config)
+ streams.start()
+
+ sys.ShutdownHookThread {
+ streams.close(10, TimeUnit.SECONDS)
+ }
+}
+
+ In the above code snippet, we don't have to provide any SerDes, Serialized, Produced, Consumed or Joined explicitly. They will also not be dependent on any SerDes specified in the config. In fact all SerDes specified in the config will be ignored by the Scala APIs. All SerDes and Serialized, Produced, Consumed or Joined will be handled through implicit SerDes as discussed later in the Implicit SerDes section. The complete independence from configuration based SerDes is what makes this library completely typesafe. Any missing instances of SerDes, Serialized, Produced, Consumed or Joined will be flagged as a compile time error.
One of the common complaints of Scala users with the Java API has been the repetitive usage of the SerDes in API invocations. Many of the APIs need to take the SerDes through abstractions like Serialized, Produced, Consumed or Joined. And the user has to supply them every time through the with function of these classes.
The library uses the power of Scala implicit parameters to alleviate this concern. As a user you can provide implicit SerDes or implicit values of Serialized, Produced, Consumed or Joined once and make your code less verbose. In fact you can just have the implicit SerDes in scope and the library will make the instances of Serialized, Produced, Consumed or Joined available in scope.
The library also bundles all implicit SerDes of the commonly used primitive types in a Scala module - so just import the module vals and have all SerDes in scope. A similar strategy of modular implicits can be adopted for any user-defined SerDes as well (User-defined SerDes are discussed in the next section).
+Here's an example:
++// DefaultSerdes brings into scope implicit SerDes (mostly for primitives) +// that will set up all Serialized, Produced, Consumed and Joined instances. +// So all APIs below that accept Serialized, Produced, Consumed or Joined will +// get these instances automatically +import DefaultSerdes._ + +val builder = new StreamsBuilder() + +val userClicksStream: KStream[String, Long] = builder.stream(userClicksTopic) + +val userRegionsTable: KTable[String, String] = builder.table(userRegionsTopic) + +// The following code fragment does not have a single instance of Serialized, +// Produced, Consumed or Joined supplied explicitly. +// All of them are taken care of by the implicit SerDes imported by DefaultSerdes +val clicksPerRegion: KTable[String, Long] = + userClicksStream + .leftJoin(userRegionsTable, + (clicks: Long, region: String) => + (if (region == null) "UNKNOWN" else region, clicks)) + .map((_, regionWithClicks) => regionWithClicks) + .groupByKey + .reduce(_ + _) + +clicksPerRegion.toStream.to(outputTopic) ++
Quite a few things are going on in the above code snippet that may warrant a few lines of elaboration:
+import DefaultSerdes._ brings all necessary SerDes in scope.When the default primitive SerDes are not enough and we need to define custom SerDes, the usage is exactly the same as above. Just define the implicit SerDes and start building the stream transformation. Here's an example with AvroSerde:
+// domain object as a case class +case class UserClicks(clicks: Long) + +// An implicit Serde implementation for the values we want to +// serialize as avro +implicit val userClicksSerde: Serde[UserClicks] = new AvroSerde + +// Primitive SerDes +import DefaultSerdes._ + +// And then business as usual .. + +val userClicksStream: KStream[String, UserClicks] = builder.stream(userClicksTopic) + +val userRegionsTable: KTable[String, String] = builder.table(userRegionsTopic) + +// Compute the total per region by summing the individual click counts per region. +val clicksPerRegion: KTable[String, Long] = + userClicksStream + + // Join the stream against the table. + .leftJoin(userRegionsTable, (clicks: UserClicks, region: String) => (if (region == null) "UNKNOWN" else region, clicks.clicks)) + + // Change the stream from+-> to -> + .map((_, regionWithClicks) => regionWithClicks) + + // Compute the total per region by summing the individual click counts per region. + .groupByKey + .reduce(_ + _) + +// Write the (continuously updating) results to the output topic. +clicksPerRegion.toStream.to(outputTopic) +
A complete example of user-defined SerDes can be found in a test class within the library.
+Any Java application that makes use of the Kafka Streams library is considered a Kafka Streams application. +
Any Java or Scala application that makes use of the Kafka Streams library is considered a Kafka Streams application. The computational logic of a Kafka Streams application is defined as a processor topology, which is a graph of stream processors (nodes) and streams (edges).
You can define the processor topology with the Kafka Streams APIs:
map, filter, join, and aggregations out of the box. The DSL is the recommended starting point for developers new to Kafka Streams, and should cover many use cases and stream processing needs.map, filter, join, and aggregations out of the box. The DSL is the recommended starting point for developers new to Kafka Streams, and should cover many use cases and stream processing needs. If you're writing a Scala application then you can use the Kafka Streams DSL for Scala library which removes much of the Java/Scala interoperability boilerplate as opposed to working directly with the Java DSL.org.apache.kafkakafka-streamskafka-streams{{fullDotVersion}}{{fullDotVersion}}org.apache.kafkakafka-streams-scala{{fullDotVersion}}_2.11, _2.12)See the section Data Types and Serialization for more information about Serializers/Deserializers.
Example pom.xml snippet when using Maven:
<dependency>
- <groupId>org.apache.kafka</groupId>
- <artifactId>kafka-streams</artifactId>
- <version>{{fullDotVersion}}</version>
-</dependency>
-<dependency>
- <groupId>org.apache.kafka</groupId>
- <artifactId>kafka-clients</artifactId>
- <version>{{fullDotVersion}}</version>
-</dependency>
-
-++ +org.apache.kafka +kafka-streams +{{fullDotVersion}} ++ + +org.apache.kafka +kafka-clients +{{fullDotVersion}} ++ +org.apache.kafka +kafka-streams-scala_2.11 +{{fullDotVersion}} +
Rabobank is one of the 3 largest banks in the Netherlands. Its digital nervous system, the Business Event Bus, is powered by Apache Kafka. It is used by an increasing amount of financial processes and services, one of which is Rabo Alerts. This service alerts customers in real-time upon financial events and is built using Kafka Streams.
The code example below implements a WordCount application that is elastic, highly scalable, fault-tolerant, stateful, and ready to run in production at large scale
- +
import org.apache.kafka.common.serialization.Serdes;
@@ -190,7 +190,7 @@ Hello Kafka Streams
}
import org.apache.kafka.common.serialization.Serdes;
@@ -208,16 +208,16 @@ Hello Kafka Streams
import java.util.Arrays;
import java.util.Properties;
-
+
public class WordCountApplication {
-
+
public static void main(final String[] args) throws Exception {
Properties config = new Properties();
config.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application");
config.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092");
config.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass());
config.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass());
-
+
StreamsBuilder builder = new StreamsBuilder();
KStream<String, String> textLines = builder.stream("TextLinesTopic");
KTable<String, Long> wordCounts = textLines
@@ -237,67 +237,59 @@ Hello Kafka Streams
wordCounts.toStream().to("WordsWithCountsTopic", Produced.with(Serdes.String(), Serdes.Long()));
-
+
KafkaStreams streams = new KafkaStreams(builder.build(), config);
streams.start();
}
-
+
}
- import java.lang.Long
- import java.util.Properties
- import java.util.concurrent.TimeUnit
-
- import org.apache.kafka.common.serialization._
- import org.apache.kafka.common.utils.Bytes
- import org.apache.kafka.streams._
- import org.apache.kafka.streams.kstream.{KStream, KTable, Materialized, Produced}
- import org.apache.kafka.streams.state.KeyValueStore
-
- import scala.collection.JavaConverters.asJavaIterableConverter
-
- object WordCountApplication {
-
- def main(args: Array[String]) {
- val config: Properties = {
- val p = new Properties()
- p.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application")
- p.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092")
- p.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass)
- p.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass)
- p
- }
-
- val builder: StreamsBuilder = new StreamsBuilder()
- val textLines: KStream[String, String] = builder.stream("TextLinesTopic")
- val wordCounts: KTable[String, Long] = textLines
- .flatMapValues(textLine => textLine.toLowerCase.split("\\W+").toIterable.asJava)
- .groupBy((_, word) => word)
- .count(Materialized.as("counts-store").asInstanceOf[Materialized[String, Long, KeyValueStore[Bytes, Array[Byte]]]])
- wordCounts.toStream().to("WordsWithCountsTopic", Produced.`with`(Serdes.String(), Serdes.Long()))
-
- val streams: KafkaStreams = new KafkaStreams(builder.build(), config)
- streams.start()
-
- Runtime.getRuntime.addShutdownHook(new Thread(() => {
- streams.close(10, TimeUnit.SECONDS)
- }))
- }
+import java.util.Properties
+import java.util.concurrent.TimeUnit
- }
+import org.apache.kafka.streams.kstream.Materialized
+import org.apache.kafka.streams.scala.kstream._
+import org.apache.kafka.streams.{KafkaStreams, StreamsConfig}
+
+object WordCountApplication extends App {
+ import DefaultSerdes._
+ import ImplicitConversions._
+
+ val config: Properties = {
+ val p = new Properties()
+ p.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-application")
+ p.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-broker1:9092")
+ p
+ }
+
+ val builder: StreamsBuilder = new StreamsBuilder()
+ val textLines: KStream[String, String] = builder.stream[String, String]("TextLinesTopic")
+ val wordCounts: KTable[String, Long] = textLines
+ .flatMapValues(textLine => textLine.toLowerCase.split("\\W+"))
+ .groupBy((_, word) => word)
+ .count(Materialized.as("counts-store"))
+ wordCounts.toStream.to("WordsWithCountsTopic")
+
+ val streams: KafkaStreams = new KafkaStreams(builder.build(), config)
+ streams.start()
+
+ sys.ShutdownHookThread {
+ streams.close(10, TimeUnit.SECONDS)
+ }
+}
StreamsConfig we have also added default.windowed.key.serde.inner and default.windowed.value.serde.inner
to let users specify inner serdes if the default serde classes are windowed serdes.
For more details, see KIP-265.
- /- -
+
+
We have deprecated constructors of KafkaStreams that take a StreamsConfig as parameter.
Please use the other corresponding constructors that accept java.util.Properties instead.
For more details, see KIP-245.
@@ -127,6 +126,14 @@
+ Kafka Streams DSL for Scala is a new Kafka Streams client library available for developers authoring Kafka Streams applications in Scala. It wraps core Kafka Streams DSL types to make it easier to call when + interoperating with Scala code. For example, it includes higher order functions as parameters for transformations avoiding the need anonymous classes in Java 7 or experimental SAM type conversions in Scala 2.11, automatic conversion between Java and Scala collection types, a way + to implicitly provide SerDes to reduce boilerplate from your application and make it more typesafe, and more! For more information see the + Kafka Streams DSL for Scala documentation and + KIP-270. +
+
We have added support for methods in
+ * Bring them in scope for default serializers / de-serializers to work.
+ */
+object DefaultSerdes {
+ implicit val stringSerde: Serde[String] = Serdes.String()
+ implicit val longSerde: Serde[Long] = Serdes.Long().asInstanceOf[Serde[Long]]
+ implicit val byteArraySerde: Serde[Array[Byte]] = Serdes.ByteArray()
+ implicit val bytesSerde: Serde[Bytes] = Serdes.Bytes()
+ implicit val floatSerde: Serde[Float] = Serdes.Float().asInstanceOf[Serde[Float]]
+ implicit val doubleSerde: Serde[Double] = Serdes.Double().asInstanceOf[Serde[Double]]
+ implicit val integerSerde: Serde[Int] = Serdes.Integer().asInstanceOf[Serde[Int]]
+ implicit val shortSerde: Serde[Short] = Serdes.Short().asInstanceOf[Serde[Short]]
+ implicit val byteBufferSerde: Serde[ByteBuffer] = Serdes.ByteBuffer()
+
+ implicit def timeWindowedSerde[T]: WindowedSerdes.TimeWindowedSerde[T] = new WindowedSerdes.TimeWindowedSerde[T]()
+ implicit def sessionWindowedSerde[T]: WindowedSerdes.SessionWindowedSerde[T] = new WindowedSerdes.SessionWindowedSerde[T]()
+}
diff --git a/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/FunctionConversions.scala b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/FunctionConversions.scala
new file mode 100644
index 0000000000000..9ce9838c7c9a0
--- /dev/null
+++ b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/FunctionConversions.scala
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 Lightbend Inc.
+ * For Scala 2.11, most of these conversions need to be invoked explicitly, as Scala 2.11 does not
+ * have full support for SAM types.
+ */
+object FunctionConversions {
+
+ implicit class PredicateFromFunction[K, V](val p: (K, V) => Boolean) extends AnyVal {
+ def asPredicate: Predicate[K, V] = new Predicate[K, V] {
+ override def test(key: K, value: V): Boolean = p(key, value)
+ }
+ }
+
+ implicit class MapperFromFunction[T, U, VR](val f:(T,U) => VR) extends AnyVal {
+ def asKeyValueMapper: KeyValueMapper[T, U, VR] = new KeyValueMapper[T, U, VR] {
+ override def apply(key: T, value: U): VR = f(key, value)
+ }
+ def asValueJoiner: ValueJoiner[T, U, VR] = new ValueJoiner[T, U, VR] {
+ override def apply(value1: T, value2: U): VR = f(value1, value2)
+ }
+ }
+
+ implicit class KeyValueMapperFromFunction[K, V, KR, VR](val f:(K,V) => (KR, VR)) extends AnyVal {
+ def asKeyValueMapper: KeyValueMapper[K, V, KeyValue[KR, VR]] = new KeyValueMapper[K, V, KeyValue[KR, VR]] {
+ override def apply(key: K, value: V): KeyValue[KR, VR] = {
+ val (kr, vr) = f(key, value)
+ KeyValue.pair(kr, vr)
+ }
+ }
+ }
+
+ implicit class ValueMapperFromFunction[V, VR](val f: V => VR) extends AnyVal {
+ def asValueMapper: ValueMapper[V, VR] = new ValueMapper[V, VR] {
+ override def apply(value: V): VR = f(value)
+ }
+ }
+
+ implicit class FlatValueMapperFromFunction[V, VR](val f: V => Iterable[VR]) extends AnyVal {
+ def asValueMapper: ValueMapper[V, JIterable[VR]] = new ValueMapper[V, JIterable[VR]] {
+ override def apply(value: V): JIterable[VR] = f(value).asJava
+ }
+ }
+
+ implicit class ValueMapperWithKeyFromFunction[K, V, VR](val f: (K, V) => VR) extends AnyVal {
+ def asValueMapperWithKey: ValueMapperWithKey[K, V, VR] = new ValueMapperWithKey[K, V, VR] {
+ override def apply(readOnlyKey: K, value: V): VR = f(readOnlyKey, value)
+ }
+ }
+
+ implicit class FlatValueMapperWithKeyFromFunction[K, V, VR](val f: (K, V) => Iterable[VR]) extends AnyVal {
+ def asValueMapperWithKey: ValueMapperWithKey[K, V, JIterable[VR]] = new ValueMapperWithKey[K, V, JIterable[VR]] {
+ override def apply(readOnlyKey: K, value: V): JIterable[VR] = f(readOnlyKey, value).asJava
+ }
+ }
+
+ implicit class AggregatorFromFunction[K, V, VA](val f: (K, V, VA) => VA) extends AnyVal {
+ def asAggregator: Aggregator[K, V, VA] = new Aggregator[K, V, VA] {
+ override def apply(key: K, value: V, aggregate: VA): VA = f(key, value, aggregate)
+ }
+ }
+
+ implicit class MergerFromFunction[K,VR](val f: (K, VR, VR) => VR) extends AnyVal {
+ def asMerger: Merger[K, VR] = new Merger[K, VR] {
+ override def apply(aggKey: K, aggOne: VR, aggTwo: VR): VR = f(aggKey, aggOne, aggTwo)
+ }
+ }
+
+ implicit class ReducerFromFunction[V](val f: (V, V) => V) extends AnyVal {
+ def asReducer: Reducer[V] = new Reducer[V] {
+ override def apply(value1: V, value2: V): V = f(value1, value2)
+ }
+ }
+
+ implicit class InitializerFromFunction[VA](val f: () => VA) extends AnyVal {
+ def asInitializer: Initializer[VA] = new Initializer[VA] {
+ override def apply(): VA = f()
+ }
+ }
+}
diff --git a/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/ImplicitConversions.scala b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/ImplicitConversions.scala
new file mode 100644
index 0000000000000..000690ba0ab4c
--- /dev/null
+++ b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/ImplicitConversions.scala
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2018 Lightbend Inc.
+ * The `implicit Consumed` instance provides the values of `auto.offset.reset` strategy, `TimestampExtractor`,
+ * key and value deserializers etc. If the implicit is not found in scope, compiler error will result.
+ *
+ * A convenient alternative is to have the necessary implicit serdes in scope, which will be implicitly
+ * converted to generate an instance of `Consumed`. @see [[ImplicitConversions]].
+ * {{{
+ * // Brings all implicit conversions in scope
+ * import ImplicitConversions._
+ *
+ * // Bring implicit default serdes in scope
+ * import DefaultSerdes._
+ *
+ * val builder = new StreamsBuilder()
+ *
+ * // stream function gets the implicit Consumed which is constructed automatically
+ * // from the serdes through the implicits in ImplicitConversions#consumedFromSerde
+ * val userClicksStream: KStream[String, Long] = builder.stream(userClicksTopic)
+ * }}}
+ *
+ * @param topic the topic name
+ * @return a [[kstream.KStream]] for the specified topic
+ */
+ def stream[K, V](topic: String)(implicit consumed: Consumed[K, V]): KStream[K, V] =
+ inner.stream[K, V](topic, consumed)
+
+ /**
+ * Create a [[kstream.KStream]] from the specified topics.
+ *
+ * @param topics the topic names
+ * @return a [[kstream.KStream]] for the specified topics
+ * @see #stream(String)
+ * @see `org.apache.kafka.streams.StreamsBuilder#stream`
+ */
+ def stream[K, V](topics: List[String])(implicit consumed: Consumed[K, V]): KStream[K, V] =
+ inner.stream[K, V](topics.asJava, consumed)
+
+ /**
+ * Create a [[kstream.KStream]] from the specified topic pattern.
+ *
+ * @param topics the topic name pattern
+ * @return a [[kstream.KStream]] for the specified topics
+ * @see #stream(String)
+ * @see `org.apache.kafka.streams.StreamsBuilder#stream`
+ */
+ def stream[K, V](topicPattern: Pattern)(implicit consumed: Consumed[K, V]): KStream[K, V] =
+ inner.stream[K, V](topicPattern, consumed)
+
+ /**
+ * Create a [[kstream.KTable]] from the specified topic.
+ *
+ * The `implicit Consumed` instance provides the values of `auto.offset.reset` strategy, `TimestampExtractor`,
+ * key and value deserializers etc. If the implicit is not found in scope, compiler error will result.
+ *
+ * A convenient alternative is to have the necessary implicit serdes in scope, which will be implicitly
+ * converted to generate an instance of `Consumed`. @see [[ImplicitConversions]].
+ * {{{
+ * // Brings all implicit conversions in scope
+ * import ImplicitConversions._
+ *
+ * // Bring implicit default serdes in scope
+ * import DefaultSerdes._
+ *
+ * val builder = new StreamsBuilder()
+ *
+ * // stream function gets the implicit Consumed which is constructed automatically
+ * // from the serdes through the implicits in ImplicitConversions#consumedFromSerde
+ * val userClicksStream: KTable[String, Long] = builder.table(userClicksTopic)
+ * }}}
+ *
+ * @param topic the topic name
+ * @return a [[kstream.KTable]] for the specified topic
+ * @see `org.apache.kafka.streams.StreamsBuilder#table`
+ */
+ def table[K, V](topic: String)(implicit consumed: Consumed[K, V]): KTable[K, V] =
+ inner.table[K, V](topic, consumed)
+
+ /**
+ * Create a [[kstream.KTable]] from the specified topic.
+ *
+ * @param topic the topic name
+ * @param materialized the instance of `Materialized` used to materialize a state store
+ * @return a [[kstream.KTable]] for the specified topic
+ * @see #table(String)
+ * @see `org.apache.kafka.streams.StreamsBuilder#table`
+ */
+ def table[K, V](topic: String, materialized: Materialized[K, V, ByteArrayKeyValueStore])
+ (implicit consumed: Consumed[K, V]): KTable[K, V] =
+ inner.table[K, V](topic, consumed, materialized)
+
+ /**
+ * Create a `GlobalKTable` from the specified topic. The serializers from the implicit `Consumed`
+ * instance will be used. Input records with `null` key will be dropped.
+ *
+ * @param topic the topic name
+ * @return a `GlobalKTable` for the specified topic
+ * @see `org.apache.kafka.streams.StreamsBuilder#globalTable`
+ */
+ def globalTable[K, V](topic: String)(implicit consumed: Consumed[K, V]): GlobalKTable[K, V] =
+ inner.globalTable(topic, consumed)
+
+ /**
+ * Create a `GlobalKTable` from the specified topic. The resulting `GlobalKTable` will be materialized
+ * in a local `KeyValueStore` configured with the provided instance of `Materialized`. The serializers
+ * from the implicit `Consumed` instance will be used.
+ *
+ * @param topic the topic name
+ * @param materialized the instance of `Materialized` used to materialize a state store
+ * @return a `GlobalKTable` for the specified topic
+ * @see `org.apache.kafka.streams.StreamsBuilder#globalTable`
+ */
+ def globalTable[K, V](topic: String, materialized: Materialized[K, V, ByteArrayKeyValueStore])
+ (implicit consumed: Consumed[K, V]): GlobalKTable[K, V] =
+ inner.globalTable(topic, consumed, materialized)
+
+ /**
+ * Adds a state store to the underlying `Topology`. The store must still be "connected" to a `Processor`,
+ * `Transformer`, or `ValueTransformer` before it can be used.
+ *
+ * @param builder the builder used to obtain this state store `StateStore` instance
+ * @return the underlying Java abstraction `StreamsBuilder` after adding the `StateStore`
+ * @throws TopologyException if state store supplier is already added
+ * @see `org.apache.kafka.streams.StreamsBuilder#addStateStore`
+ */
+ def addStateStore(builder: StoreBuilder[_ <: StateStore]): StreamsBuilderJ = inner.addStateStore(builder)
+
+ /**
+ * Adds a global `StateStore` to the topology. Global stores should not be added to `Processor, `Transformer`,
+ * or `ValueTransformer` (in contrast to regular stores).
+ *
+ * @see `org.apache.kafka.streams.StreamsBuilder#addGlobalStore`
+ */
+ def addGlobalStore(storeBuilder: StoreBuilder[_ <: StateStore],
+ topic: String,
+ consumed: Consumed[_, _],
+ stateUpdateSupplier: ProcessorSupplier[_, _]): StreamsBuilderJ =
+ inner.addGlobalStore(storeBuilder, topic, consumed, stateUpdateSupplier)
+
+ def build(): Topology = inner.build()
+}
diff --git a/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KGroupedStream.scala b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KGroupedStream.scala
new file mode 100644
index 0000000000000..8f0ae93c1491d
--- /dev/null
+++ b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KGroupedStream.scala
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2018 Lightbend Inc.
+ * The function `mapper` passed is applied to every record and results in the generation of a new
+ * key `KR`. The function outputs a new [[KStream]] where each record has this new key.
+ *
+ * @param mapper a function `(K, V) => KR` that computes a new key for each record
+ * @return a [[KStream]] that contains records with new key (possibly of different type) and unmodified value
+ * @see `org.apache.kafka.streams.kstream.KStream#selectKey`
+ */
+ def selectKey[KR](mapper: (K, V) => KR): KStream[KR, V] = {
+ inner.selectKey[KR](mapper.asKeyValueMapper)
+ }
+
+ /**
+ * Transform each record of the input stream into a new record in the output stream (both key and value type can be
+ * altered arbitrarily).
+ *
+ * The provided `mapper`, a function `(K, V) => (KR, VR)` is applied to each input record and computes a new output record.
+ *
+ * @param mapper a function `(K, V) => (KR, VR)` that computes a new output record
+ * @return a [[KStream]] that contains records with new key and value (possibly both of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#map`
+ */
+ def map[KR, VR](mapper: (K, V) => (KR, VR)): KStream[KR, VR] = {
+ val kvMapper = mapper.tupled andThen tuple2ToKeyValue
+ inner.map[KR, VR](((k: K, v: V) => kvMapper(k, v)).asKeyValueMapper)
+ }
+
+ /**
+ * Transform the value of each input record into a new value (with possible new type) of the output record.
+ *
+ * The provided `mapper`, a function `V => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `V => VR` that computes a new output value
+ * @return a [[KStream]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#mapValues`
+ */
+ def mapValues[VR](mapper: V => VR): KStream[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapper)
+ }
+
+ /**
+ * Transform the value of each input record into a new value (with possible new type) of the output record.
+ *
+ * The provided `mapper`, a function `(K, V) => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `(K, V) => VR` that computes a new output value
+ * @return a [[KStream]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#mapValues`
+ */
+ def mapValues[VR](mapper: (K, V) => VR): KStream[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapperWithKey)
+ }
+
+ /**
+ * Transform each record of the input stream into zero or more records in the output stream (both key and value type
+ * can be altered arbitrarily).
+ *
+ * The provided `mapper`, function `(K, V) => Iterable[(KR, VR)]` is applied to each input record and computes zero or more output records.
+ *
+ * @param mapper function `(K, V) => Iterable[(KR, VR)]` that computes the new output records
+ * @return a [[KStream]] that contains more or less records with new key and value (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#flatMap`
+ */
+ def flatMap[KR, VR](mapper: (K, V) => Iterable[(KR, VR)]): KStream[KR, VR] = {
+ val kvMapper = mapper.tupled andThen (iter => iter.map(tuple2ToKeyValue).asJava)
+ inner.flatMap[KR, VR](((k: K, v: V) => kvMapper(k , v)).asKeyValueMapper)
+ }
+
+ /**
+ * Create a new [[KStream]] by transforming the value of each record in this stream into zero or more values
+ * with the same key in the new stream.
+ *
+ * Transform the value of each input record into zero or more records with the same (unmodified) key in the output
+ * stream (value type can be altered arbitrarily).
+ * The provided `mapper`, a function `V => Iterable[VR]` is applied to each input record and computes zero or more output values.
+ *
+ * @param mapper a function `V => Iterable[VR]` that computes the new output values
+ * @return a [[KStream]] that contains more or less records with unmodified keys and new values of different type
+ * @see `org.apache.kafka.streams.kstream.KStream#flatMapValues`
+ */
+ def flatMapValues[VR](mapper: V => Iterable[VR]): KStream[K, VR] = {
+ inner.flatMapValues[VR](mapper.asValueMapper)
+ }
+
+ /**
+ * Create a new [[KStream]] by transforming the value of each record in this stream into zero or more values
+ * with the same key in the new stream.
+ *
+ * Transform the value of each input record into zero or more records with the same (unmodified) key in the output
+ * stream (value type can be altered arbitrarily).
+ * The provided `mapper`, a function `(K, V) => Iterable[VR]` is applied to each input record and computes zero or more output values.
+ *
+ * @param mapper a function `(K, V) => Iterable[VR]` that computes the new output values
+ * @return a [[KStream]] that contains more or less records with unmodified keys and new values of different type
+ * @see `org.apache.kafka.streams.kstream.KStream#flatMapValues`
+ */
+ def flatMapValues[VR](mapper: (K, V) => Iterable[VR]): KStream[K, VR] = {
+ inner.flatMapValues[VR](mapper.asValueMapperWithKey)
+ }
+
+ /**
+ * Print the records of this KStream using the options provided by `Printed`
+ *
+ * @param printed options for printing
+ * @see `org.apache.kafka.streams.kstream.KStream#print`
+ */
+ def print(printed: Printed[K, V]): Unit = inner.print(printed)
+
+ /**
+ * Perform an action on each record of 'KStream`
+ *
+ * @param action an action to perform on each record
+ * @see `org.apache.kafka.streams.kstream.KStream#foreach`
+ */
+ def foreach(action: (K, V) => Unit): Unit = {
+ inner.foreach((k: K, v: V) => action(k, v))
+ }
+
+ /**
+ * Creates an array of {@code KStream} from this stream by branching the records in the original stream based on
+ * the supplied predicates.
+ *
+ * @param predicates the ordered list of functions that return a Boolean
+ * @return multiple distinct substreams of this [[KStream]]
+ * @see `org.apache.kafka.streams.kstream.KStream#branch`
+ */
+ def branch(predicates: ((K, V) => Boolean)*): Array[KStream[K, V]] = {
+ inner.branch(predicates.map(_.asPredicate): _*).map(kstream => wrapKStream(kstream))
+ }
+
+ /**
+ * Materialize this stream to a topic and creates a new [[KStream]] from the topic using the `Produced` instance for
+ * configuration of the `Serde key serde`, `Serde value serde`, and `StreamPartitioner`
+ *
+ * The user can either supply the `Produced` instance as an implicit in scope or she can also provide implicit
+ * key and value serdes that will be converted to a `Produced` instance implicitly.
+ *
+ * {{{
+ * Example:
+ *
+ * // brings implicit serdes in scope
+ * import DefaultSerdes._
+ *
+ * //..
+ * val clicksPerRegion: KTable[String, Long] = //..
+ *
+ * // Implicit serdes in scope will generate an implicit Produced instance, which
+ * // will be passed automatically to the call of through below
+ * clicksPerRegion.through(topic)
+ *
+ * // Similarly you can create an implicit Produced and it will be passed implicitly
+ * // to the through call
+ * }}}
+ *
+ * @param topic the topic name
+ * @param (implicit) produced the instance of Produced that gives the serdes and `StreamPartitioner`
+ * @return a [[KStream]] that contains the exact same (and potentially repartitioned) records as this [[KStream]]
+ * @see `org.apache.kafka.streams.kstream.KStream#through`
+ */
+ def through(topic: String)(implicit produced: Produced[K, V]): KStream[K, V] =
+ inner.through(topic, produced)
+
+ /**
+ * Materialize this stream to a topic using the `Produced` instance for
+ * configuration of the `Serde key serde`, `Serde value serde`, and `StreamPartitioner`
+ *
+ * The user can either supply the `Produced` instance as an implicit in scope or she can also provide implicit
+ * key and value serdes that will be converted to a `Produced` instance implicitly.
+ *
+ * {{{
+ * Example:
+ *
+ * // brings implicit serdes in scope
+ * import DefaultSerdes._
+ *
+ * //..
+ * val clicksPerRegion: KTable[String, Long] = //..
+ *
+ * // Implicit serdes in scope will generate an implicit Produced instance, which
+ * // will be passed automatically to the call of through below
+ * clicksPerRegion.to(topic)
+ *
+ * // Similarly you can create an implicit Produced and it will be passed implicitly
+ * // to the through call
+ * }}}
+ *
+ * @param topic the topic name
+ * @param (implicit) produced the instance of Produced that gives the serdes and `StreamPartitioner`
+ * @see `org.apache.kafka.streams.kstream.KStream#to`
+ */
+ def to(topic: String)(implicit produced: Produced[K, V]): Unit =
+ inner.to(topic, produced)
+
+ /**
+ * Transform each record of the input stream into zero or more records in the output stream (both key and value type
+ * can be altered arbitrarily).
+ * A `Transformer` (provided by the given `TransformerSupplier`) is applied to each input record and
+ * computes zero or more output records. In order to assign a state, the state must be created and registered
+ * beforehand via stores added via `addStateStore` or `addGlobalStore` before they can be connected to the `Transformer`
+ *
+ * @param transformerSupplier a instance of `TransformerSupplier` that generates a `Transformer`
+ * @param stateStoreNames the names of the state stores used by the processor
+ * @return a [[KStream]] that contains more or less records with new key and value (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#transform`
+ */
+ def transform[K1, V1](transformerSupplier: Transformer[K, V, (K1, V1)],
+ stateStoreNames: String*): KStream[K1, V1] = {
+ val transformerSupplierJ: TransformerSupplier[K, V, KeyValue[K1, V1]] = new TransformerSupplier[K, V, KeyValue[K1, V1]] {
+ override def get(): Transformer[K, V, KeyValue[K1, V1]] = {
+ new Transformer[K, V, KeyValue[K1, V1]] {
+ override def transform(key: K, value: V): KeyValue[K1, V1] = {
+ transformerSupplier.transform(key, value) match {
+ case (k1, v1) => KeyValue.pair(k1, v1)
+ case _ => null
+ }
+ }
+
+ override def init(context: ProcessorContext): Unit = transformerSupplier.init(context)
+
+ @deprecated ("Please use Punctuator functional interface at https://kafka.apache.org/10/javadoc/org/apache/kafka/streams/processor/Punctuator.html instead", "0.1.3") // scalastyle:ignore
+ override def punctuate(timestamp: Long): KeyValue[K1, V1] = {
+ transformerSupplier.punctuate(timestamp) match {
+ case (k1, v1) => KeyValue.pair[K1, V1](k1, v1)
+ case _ => null
+ }
+ }
+
+ override def close(): Unit = transformerSupplier.close()
+ }
+ }
+ }
+ inner.transform(transformerSupplierJ, stateStoreNames: _*)
+ }
+
+ /**
+ * Transform the value of each input record into a new value (with possible new type) of the output record.
+ * A `ValueTransformer` (provided by the given `ValueTransformerSupplier`) is applied to each input
+ * record value and computes a new value for it.
+ * In order to assign a state, the state must be created and registered
+ * beforehand via stores added via `addStateStore` or `addGlobalStore` before they can be connected to the `Transformer`
+ *
+ * @param valueTransformerSupplier a instance of `ValueTransformerSupplier` that generates a `ValueTransformer`
+ * @param stateStoreNames the names of the state stores used by the processor
+ * @return a [[KStream]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#transformValues`
+ */
+ def transformValues[VR](valueTransformerSupplier: ValueTransformerSupplier[V, VR],
+ stateStoreNames: String*): KStream[K, VR] = {
+ inner.transformValues[VR](valueTransformerSupplier, stateStoreNames: _*)
+ }
+
+ /**
+ * Transform the value of each input record into a new value (with possible new type) of the output record.
+ * A `ValueTransformer` (provided by the given `ValueTransformerSupplier`) is applied to each input
+ * record value and computes a new value for it.
+ * In order to assign a state, the state must be created and registered
+ * beforehand via stores added via `addStateStore` or `addGlobalStore` before they can be connected to the `Transformer`
+ *
+ * @param valueTransformerSupplier a instance of `ValueTransformerWithKeySupplier` that generates a `ValueTransformerWithKey`
+ * @param stateStoreNames the names of the state stores used by the processor
+ * @return a [[KStream]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KStream#transformValues`
+ */
+ def transformValues[VR](valueTransformerWithKeySupplier: ValueTransformerWithKeySupplier[K, V, VR],
+ stateStoreNames: String*): KStream[K, VR] = {
+ inner.transformValues[VR](valueTransformerWithKeySupplier, stateStoreNames: _*)
+ }
+
+ /**
+ * Process all records in this stream, one record at a time, by applying a `Processor` (provided by the given
+ * `processorSupplier`).
+ * In order to assign a state, the state must be created and registered
+ * beforehand via stores added via `addStateStore` or `addGlobalStore` before they can be connected to the `Transformer`
+ *
+ * @param processorSupplier a function that generates a [[org.apache.kafka.stream.Processor]]
+ * @param stateStoreNames the names of the state store used by the processor
+ * @see `org.apache.kafka.streams.kstream.KStream#process`
+ */
+ def process(processorSupplier: () => Processor[K, V],
+ stateStoreNames: String*): Unit = {
+
+ val processorSupplierJ: ProcessorSupplier[K, V] = new ProcessorSupplier[K, V] {
+ override def get(): Processor[K, V] = processorSupplier()
+ }
+ inner.process(processorSupplierJ, stateStoreNames: _*)
+ }
+
+ /**
+ * Group the records by their current key into a [[KGroupedStream]]
+ *
+ * The user can either supply the `Serialized` instance as an implicit in scope or she can also provide an implicit
+ * serdes that will be converted to a `Serialized` instance implicitly.
+ *
+ * {{{
+ * Example:
+ *
+ * // brings implicit serdes in scope
+ * import DefaultSerdes._
+ *
+ * val clicksPerRegion: KTable[String, Long] =
+ * userClicksStream
+ * .leftJoin(userRegionsTable, (clicks: Long, region: String) => (if (region == null) "UNKNOWN" else region, clicks))
+ * .map((_, regionWithClicks) => regionWithClicks)
+ *
+ * // the groupByKey gets the Serialized instance through an implicit conversion of the
+ * // serdes brought into scope through the import DefaultSerdes._ above
+ * .groupByKey
+ * .reduce(_ + _)
+ *
+ * // Similarly you can create an implicit Serialized and it will be passed implicitly
+ * // to the groupByKey call
+ * }}}
+ *
+ * @param (implicit) serialized the instance of Serialized that gives the serdes
+ * @return a [[KGroupedStream]] that contains the grouped records of the original [[KStream]]
+ * @see `org.apache.kafka.streams.kstream.KStream#groupByKey`
+ */
+ def groupByKey(implicit serialized: Serialized[K, V]): KGroupedStream[K, V] =
+ inner.groupByKey(serialized)
+
+ /**
+ * Group the records of this [[KStream]] on a new key that is selected using the provided key transformation function
+ * and the `Serialized` instance.
+ *
+ * The user can either supply the `Serialized` instance as an implicit in scope or she can also provide an implicit
+ * serdes that will be converted to a `Serialized` instance implicitly.
+ *
+ * {{{
+ * Example:
+ *
+ * // brings implicit serdes in scope
+ * import DefaultSerdes._
+ *
+ * val textLines = streamBuilder.stream[String, String](inputTopic)
+ *
+ * val pattern = Pattern.compile("\\W+", Pattern.UNICODE_CHARACTER_CLASS)
+ *
+ * val wordCounts: KTable[String, Long] =
+ * textLines.flatMapValues(v => pattern.split(v.toLowerCase))
+ *
+ * // the groupBy gets the Serialized instance through an implicit conversion of the
+ * // serdes brought into scope through the import DefaultSerdes._ above
+ * .groupBy((k, v) => v)
+ *
+ * .count()
+ * }}}
+ *
+ * @param selector a function that computes a new key for grouping
+ * @return a [[KGroupedStream]] that contains the grouped records of the original [[KStream]]
+ * @see `org.apache.kafka.streams.kstream.KStream#groupBy`
+ */
+ def groupBy[KR](selector: (K, V) => KR)(implicit serialized: Serialized[KR, V]): KGroupedStream[KR, V] =
+ inner.groupBy(selector.asKeyValueMapper, serialized)
+
+ /**
+ * Join records of this stream with another [[KStream]]'s records using windowed inner equi join with
+ * serializers and deserializers supplied by the implicit `Joined` instance.
+ *
+ * @param otherStream the [[KStream]] to be joined with this stream
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param windows the specification of the `JoinWindows`
+ * @param joined an implicit `Joined` instance that defines the serdes to be used to serialize/deserialize
+ * inputs and outputs of the joined streams. Instead of `Joined`, the user can also supply
+ * key serde, value serde and other value serde in implicit scope and they will be
+ * converted to the instance of `Joined` through implicit conversion
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one for each matched record-pair with the same key and within the joining window intervals
+ * @see `org.apache.kafka.streams.kstream.KStream#join`
+ */
+ def join[VO, VR](otherStream: KStream[K, VO],
+ joiner: (V, VO) => VR,
+ windows: JoinWindows)(implicit joined: Joined[K, V, VO]): KStream[K, VR] =
+ inner.join[VO, VR](otherStream.inner, joiner.asValueJoiner, windows, joined)
+
+ /**
+ * Join records of this stream with another [[KTable]]'s records using inner equi join with
+ * serializers and deserializers supplied by the implicit `Joined` instance.
+ *
+ * @param table the [[KTable]] to be joined with this stream
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param joined an implicit `Joined` instance that defines the serdes to be used to serialize/deserialize
+ * inputs and outputs of the joined streams. Instead of `Joined`, the user can also supply
+ * key serde, value serde and other value serde in implicit scope and they will be
+ * converted to the instance of `Joined` through implicit conversion
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KStream#join`
+ */
+ def join[VT, VR](table: KTable[K, VT],
+ joiner: (V, VT) => VR)(implicit joined: Joined[K, V, VT]): KStream[K, VR] =
+ inner.join[VT, VR](table.inner, joiner.asValueJoiner, joined)
+
+ /**
+ * Join records of this stream with `GlobalKTable`'s records using non-windowed inner equi join.
+ *
+ * @param globalKTable the `GlobalKTable` to be joined with this stream
+ * @param keyValueMapper a function used to map from the (key, value) of this stream
+ * to the key of the `GlobalKTable`
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one output for each input [[KStream]] record
+ * @see `org.apache.kafka.streams.kstream.KStream#join`
+ */
+ def join[GK, GV, RV](globalKTable: GlobalKTable[GK, GV],
+ keyValueMapper: (K, V) => GK,
+ joiner: (V, GV) => RV): KStream[K, RV] =
+ inner.join[GK, GV, RV](
+ globalKTable,
+ ((k: K, v: V) => keyValueMapper(k, v)).asKeyValueMapper,
+ ((v: V, gv: GV) => joiner(v, gv)).asValueJoiner
+ )
+
+ /**
+ * Join records of this stream with another [[KStream]]'s records using windowed left equi join with
+ * serializers and deserializers supplied by the implicit `Joined` instance.
+ *
+ * @param otherStream the [[KStream]] to be joined with this stream
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param windows the specification of the `JoinWindows`
+ * @param joined an implicit `Joined` instance that defines the serdes to be used to serialize/deserialize
+ * inputs and outputs of the joined streams. Instead of `Joined`, the user can also supply
+ * key serde, value serde and other value serde in implicit scope and they will be
+ * converted to the instance of `Joined` through implicit conversion
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one for each matched record-pair with the same key and within the joining window intervals
+ * @see `org.apache.kafka.streams.kstream.KStream#leftJoin`
+ */
+ def leftJoin[VO, VR](otherStream: KStream[K, VO],
+ joiner: (V, VO) => VR,
+ windows: JoinWindows)(implicit joined: Joined[K, V, VO]): KStream[K, VR] =
+ inner.leftJoin[VO, VR](otherStream.inner, joiner.asValueJoiner, windows, joined)
+
+ /**
+ * Join records of this stream with another [[KTable]]'s records using left equi join with
+ * serializers and deserializers supplied by the implicit `Joined` instance.
+ *
+ * @param table the [[KTable]] to be joined with this stream
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param joined an implicit `Joined` instance that defines the serdes to be used to serialize/deserialize
+ * inputs and outputs of the joined streams. Instead of `Joined`, the user can also supply
+ * key serde, value serde and other value serde in implicit scope and they will be
+ * converted to the instance of `Joined` through implicit conversion
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KStream#leftJoin`
+ */
+ def leftJoin[VT, VR](table: KTable[K, VT],
+ joiner: (V, VT) => VR)(implicit joined: Joined[K, V, VT]): KStream[K, VR] =
+ inner.leftJoin[VT, VR](table.inner, joiner.asValueJoiner, joined)
+
+ /**
+ * Join records of this stream with `GlobalKTable`'s records using non-windowed left equi join.
+ *
+ * @param globalKTable the `GlobalKTable` to be joined with this stream
+ * @param keyValueMapper a function used to map from the (key, value) of this stream
+ * to the key of the `GlobalKTable`
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one output for each input [[KStream]] record
+ * @see `org.apache.kafka.streams.kstream.KStream#leftJoin`
+ */
+ def leftJoin[GK, GV, RV](globalKTable: GlobalKTable[GK, GV],
+ keyValueMapper: (K, V) => GK,
+ joiner: (V, GV) => RV): KStream[K, RV] = {
+
+ inner.leftJoin[GK, GV, RV](globalKTable, keyValueMapper.asKeyValueMapper, joiner.asValueJoiner)
+ }
+
+ /**
+ * Join records of this stream with another [[KStream]]'s records using windowed outer equi join with
+ * serializers and deserializers supplied by the implicit `Joined` instance.
+ *
+ * @param otherStream the [[KStream]] to be joined with this stream
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param windows the specification of the `JoinWindows`
+ * @param joined an implicit `Joined` instance that defines the serdes to be used to serialize/deserialize
+ * inputs and outputs of the joined streams. Instead of `Joined`, the user can also supply
+ * key serde, value serde and other value serde in implicit scope and they will be
+ * converted to the instance of `Joined` through implicit conversion
+ * @return a [[KStream]] that contains join-records for each key and values computed by the given `joiner`,
+ * one for each matched record-pair with the same key and within the joining window intervals
+ * @see `org.apache.kafka.streams.kstream.KStream#outerJoin`
+ */
+ def outerJoin[VO, VR](otherStream: KStream[K, VO],
+ joiner: (V, VO) => VR,
+ windows: JoinWindows)(implicit joined: Joined[K, V, VO]): KStream[K, VR] =
+ inner.outerJoin[VO, VR](otherStream.inner, joiner.asValueJoiner, windows, joined)
+
+ /**
+ * Merge this stream and the given stream into one larger stream.
+ *
+ * There is no ordering guarantee between records from this `KStream` and records from the provided `KStream`
+ * in the merged stream. Relative order is preserved within each input stream though (ie, records within
+ * one input stream are processed in order).
+ *
+ * @param stream a stream which is to be merged into this stream
+ * @return a merged stream containing all records from this and the provided [[KStream]]
+ * @see `org.apache.kafka.streams.kstream.KStream#merge`
+ */
+ def merge(stream: KStream[K, V]): KStream[K, V] = inner.merge(stream.inner)
+
+ /**
+ * Perform an action on each record of {@code KStream}.
+ *
+ * Peek is a non-terminal operation that triggers a side effect (such as logging or statistics collection)
+ * and returns an unchanged stream.
+ *
+ * @param action an action to perform on each record
+ * @see `org.apache.kafka.streams.kstream.KStream#peek`
+ */
+ def peek(action: (K, V) => Unit): KStream[K, V] = {
+ inner.peek((k: K, v: V) => action(k, v))
+ }
+}
diff --git a/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KTable.scala b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KTable.scala
new file mode 100644
index 0000000000000..0369ee5aa589d
--- /dev/null
+++ b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/KTable.scala
@@ -0,0 +1,292 @@
+/*
+ * Copyright (C) 2018 Lightbend Inc.
+ * The provided `mapper`, a function `V => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `V => VR` that computes a new output value
+ * @return a [[KTable]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KTable#mapValues`
+ */
+ def mapValues[VR](mapper: V => VR): KTable[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapper)
+ }
+
+ /**
+ * Create a new [[KTable]] by transforming the value of each record in this [[KTable]] into a new value
+ * (with possible new type) in the new [[KTable]].
+ *
+ * The provided `mapper`, a function `V => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `V => VR` that computes a new output value
+ * @param materialized a `Materialized` that describes how the `StateStore` for the resulting [[KTable]]
+ * should be materialized.
+ * @return a [[KTable]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KTable#mapValues`
+ */
+ def mapValues[VR](mapper: V => VR,
+ materialized: Materialized[K, VR, ByteArrayKeyValueStore]): KTable[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapper, materialized)
+ }
+
+ /**
+ * Create a new [[KTable]] by transforming the value of each record in this [[KTable]] into a new value
+ * (with possible new type) in the new [[KTable]].
+ *
+ * The provided `mapper`, a function `(K, V) => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `(K, V) => VR` that computes a new output value
+ * @return a [[KTable]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KTable#mapValues`
+ */
+ def mapValues[VR](mapper: (K, V) => VR): KTable[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapperWithKey)
+ }
+
+ /**
+ * Create a new [[KTable]] by transforming the value of each record in this [[KTable]] into a new value
+ * (with possible new type) in the new [[KTable]].
+ *
+ * The provided `mapper`, a function `(K, V) => VR` is applied to each input record value and computes a new value for it
+ *
+ * @param mapper, a function `(K, V) => VR` that computes a new output value
+ * @param materialized a `Materialized` that describes how the `StateStore` for the resulting [[KTable]]
+ * should be materialized.
+ * @return a [[KTable]] that contains records with unmodified key and new values (possibly of different type)
+ * @see `org.apache.kafka.streams.kstream.KTable#mapValues`
+ */
+ def mapValues[VR](mapper: (K, V) => VR,
+ materialized: Materialized[K, VR, ByteArrayKeyValueStore]): KTable[K, VR] = {
+ inner.mapValues[VR](mapper.asValueMapperWithKey)
+ }
+
+ /**
+ * Convert this changelog stream to a [[KStream]].
+ *
+ * @return a [[KStream]] that contains the same records as this [[KTable]]
+ * @see `org.apache.kafka.streams.kstream.KTable#toStream`
+ */
+ def toStream: KStream[K, V] = inner.toStream
+
+ /**
+ * Convert this changelog stream to a [[KStream]] using the given key/value mapper to select the new key
+ *
+ * @param mapper a function that computes a new key for each record
+ * @return a [[KStream]] that contains the same records as this [[KTable]]
+ * @see `org.apache.kafka.streams.kstream.KTable#toStream`
+ */
+ def toStream[KR](mapper: (K, V) => KR): KStream[KR, V] = {
+ inner.toStream[KR](mapper.asKeyValueMapper)
+ }
+
+ /**
+ * Re-groups the records of this [[KTable]] using the provided key/value mapper
+ * and `Serde`s as specified by `Serialized`.
+ *
+ * @param selector a function that computes a new grouping key and value to be aggregated
+ * @param serialized the `Serialized` instance used to specify `Serdes`
+ * @return a [[KGroupedTable]] that contains the re-grouped records of the original [[KTable]]
+ * @see `org.apache.kafka.streams.kstream.KTable#groupBy`
+ */
+ def groupBy[KR, VR](selector: (K, V) => (KR, VR))(implicit serialized: Serialized[KR, VR]): KGroupedTable[KR, VR] = {
+ inner.groupBy(selector.asKeyValueMapper, serialized)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed inner equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#join`
+ */
+ def join[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR): KTable[K, VR] = {
+
+ inner.join[VO, VR](other.inner, joiner.asValueJoiner)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed inner equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param materialized a `Materialized` that describes how the `StateStore` for the resulting [[KTable]]
+ * should be materialized.
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#join`
+ */
+ def join[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR,
+ materialized: Materialized[K, VR, ByteArrayKeyValueStore]): KTable[K, VR] = {
+
+ inner.join[VO, VR](other.inner, joiner.asValueJoiner, materialized)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed left equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#leftJoin`
+ */
+ def leftJoin[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR): KTable[K, VR] = {
+
+ inner.leftJoin[VO, VR](other.inner, joiner.asValueJoiner)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed left equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param materialized a `Materialized` that describes how the `StateStore` for the resulting [[KTable]]
+ * should be materialized.
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#leftJoin`
+ */
+ def leftJoin[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR,
+ materialized: Materialized[K, VR, ByteArrayKeyValueStore]): KTable[K, VR] = {
+
+ inner.leftJoin[VO, VR](other.inner, joiner.asValueJoiner, materialized)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed outer equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#leftJoin`
+ */
+ def outerJoin[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR): KTable[K, VR] = {
+
+ inner.outerJoin[VO, VR](other.inner, joiner.asValueJoiner)
+ }
+
+ /**
+ * Join records of this [[KTable]] with another [[KTable]]'s records using non-windowed outer equi join.
+ *
+ * @param other the other [[KTable]] to be joined with this [[KTable]]
+ * @param joiner a function that computes the join result for a pair of matching records
+ * @param materialized a `Materialized` that describes how the `StateStore` for the resulting [[KTable]]
+ * should be materialized.
+ * @return a [[KTable]] that contains join-records for each key and values computed by the given joiner,
+ * one for each matched record-pair with the same key
+ * @see `org.apache.kafka.streams.kstream.KTable#leftJoin`
+ */
+ def outerJoin[VO, VR](other: KTable[K, VO],
+ joiner: (V, VO) => VR,
+ materialized: Materialized[K, VR, ByteArrayKeyValueStore]): KTable[K, VR] = {
+
+ inner.outerJoin[VO, VR](other.inner, joiner.asValueJoiner, materialized)
+ }
+
+ /**
+ * Get the name of the local state store used that can be used to query this [[KTable]].
+ *
+ * @return the underlying state store name, or `null` if this [[KTable]] cannot be queried.
+ */
+ def queryableStoreName: String =
+ inner.queryableStoreName
+}
diff --git a/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/SessionWindowedKStream.scala b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/SessionWindowedKStream.scala
new file mode 100644
index 0000000000000..7e9fa0713fe29
--- /dev/null
+++ b/streams/streams-scala/src/main/scala/org/apache/kafka/streams/scala/kstream/SessionWindowedKStream.scala
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2018 Lightbend Inc.
+ * The suite contains the test case using Scala APIs `testShouldCountClicksPerRegion` and the same test case using the
+ * Java APIs `testShouldCountClicksPerRegionJava`. The idea is to demonstrate that both generate the same result.
+ *
+ * Note: In the current project settings SAM type conversion is turned off as it's experimental in Scala 2.11.
+ * Hence the native Java API based version is more verbose.
+ */
+class StreamToTableJoinScalaIntegrationTestImplicitSerdes extends JUnitSuite
+ with StreamToTableJoinTestData with LazyLogging {
+
+ private val privateCluster: EmbeddedKafkaCluster = new EmbeddedKafkaCluster(1)
+
+ @Rule def cluster: EmbeddedKafkaCluster = privateCluster
+
+ final val alignedTime = (System.currentTimeMillis() / 1000 + 1) * 1000
+ val mockTime: MockTime = cluster.time
+ mockTime.setCurrentTimeMs(alignedTime)
+
+ val tFolder: TemporaryFolder = new TemporaryFolder(TestUtils.tempDirectory())
+ @Rule def testFolder: TemporaryFolder = tFolder
+
+ @Before
+ def startKafkaCluster(): Unit = {
+ cluster.createTopic(userClicksTopic)
+ cluster.createTopic(userRegionsTopic)
+ cluster.createTopic(outputTopic)
+ cluster.createTopic(userClicksTopicJ)
+ cluster.createTopic(userRegionsTopicJ)
+ cluster.createTopic(outputTopicJ)
+ }
+
+ @Test def testShouldCountClicksPerRegion(): Unit = {
+
+ // DefaultSerdes brings into scope implicit serdes (mostly for primitives) that will set up all Serialized, Produced,
+ // Consumed and Joined instances. So all APIs below that accept Serialized, Produced, Consumed or Joined will
+ // get these instances automatically
+ import DefaultSerdes._
+
+ val streamsConfiguration: Properties = getStreamsConfiguration()
+
+ val builder = new StreamsBuilder()
+
+ val userClicksStream: KStream[String, Long] = builder.stream(userClicksTopic)
+
+ val userRegionsTable: KTable[String, String] = builder.table(userRegionsTopic)
+
+ // Compute the total per region by summing the individual click counts per region.
+ val clicksPerRegion: KTable[String, Long] =
+ userClicksStream
+
+ // Join the stream against the table.
+ .leftJoin(userRegionsTable, (clicks: Long, region: String) => (if (region == null) "UNKNOWN" else region, clicks))
+
+ // Change the stream from
+ * The suite contains the test case using Scala APIs `testShouldCountWords` and the same test case using the
+ * Java APIs `testShouldCountWordsJava`. The idea is to demonstrate that both generate the same result.
+ *
+ * Note: In the current project settings SAM type conversion is turned off as it's experimental in Scala 2.11.
+ * Hence the native Java API based version is more verbose.
+ */
+class WordCountTest extends JUnitSuite with WordCountTestData with LazyLogging {
+
+ private val privateCluster: EmbeddedKafkaCluster = new EmbeddedKafkaCluster(1)
+
+ @Rule def cluster: EmbeddedKafkaCluster = privateCluster
+
+ final val alignedTime = (System.currentTimeMillis() / 1000 + 1) * 1000
+ val mockTime: MockTime = cluster.time
+ mockTime.setCurrentTimeMs(alignedTime)
+
+
+ val tFolder: TemporaryFolder = new TemporaryFolder(TestUtils.tempDirectory())
+ @Rule def testFolder: TemporaryFolder = tFolder
+
+
+ @Before
+ def startKafkaCluster(): Unit = {
+ cluster.createTopic(inputTopic)
+ cluster.createTopic(outputTopic)
+ cluster.createTopic(inputTopicJ)
+ cluster.createTopic(outputTopicJ)
+ }
+
+ @Test def testShouldCountWords(): Unit = {
+
+ import DefaultSerdes._
+
+ val streamsConfiguration = getStreamsConfiguration()
+
+ val streamBuilder = new StreamsBuilder
+ val textLines = streamBuilder.stream[String, String](inputTopic)
+
+ val pattern = Pattern.compile("\\W+", Pattern.UNICODE_CHARACTER_CLASS)
+
+ // generate word counts
+ val wordCounts: KTable[String, Long] =
+ textLines.flatMapValues(v => pattern.split(v.toLowerCase))
+ .groupBy((k, v) => v)
+ .count()
+
+ // write to output topic
+ wordCounts.toStream.to(outputTopic)
+
+ val streams: KafkaStreams = new KafkaStreams(streamBuilder.build(), streamsConfiguration)
+ streams.start()
+
+ // produce and consume synchronously
+ val actualWordCounts: java.util.List[KeyValue[String, Long]] = produceNConsume(inputTopic, outputTopic)
+
+ streams.close()
+
+ import collection.JavaConverters._
+ assertEquals(actualWordCounts.asScala.take(expectedWordCounts.size).sortBy(_.key), expectedWordCounts.sortBy(_.key))
+ }
+
+ @Test def testShouldCountWordsJava(): Unit = {
+
+ import org.apache.kafka.streams.{KafkaStreams => KafkaStreamsJ, StreamsBuilder => StreamsBuilderJ}
+ import org.apache.kafka.streams.kstream.{KTable => KTableJ, KStream => KStreamJ, KGroupedStream => KGroupedStreamJ, _}
+ import collection.JavaConverters._
+
+ val streamsConfiguration = getStreamsConfiguration()
+ streamsConfiguration.put(StreamsConfig.DEFAULT_KEY_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName())
+ streamsConfiguration.put(StreamsConfig.DEFAULT_VALUE_SERDE_CLASS_CONFIG, Serdes.String().getClass().getName())
+
+ val streamBuilder = new StreamsBuilderJ
+ val textLines: KStreamJ[String, String] = streamBuilder.stream[String, String](inputTopicJ)
+
+ val pattern = Pattern.compile("\\W+", Pattern.UNICODE_CHARACTER_CLASS)
+
+ val splits: KStreamJ[String, String] = textLines.flatMapValues {
+ new ValueMapper[String, java.lang.Iterable[String]] {
+ def apply(s: String): java.lang.Iterable[String] = pattern.split(s.toLowerCase).toIterable.asJava
+ }
+ }
+
+ val grouped: KGroupedStreamJ[String, String] = splits.groupBy {
+ new KeyValueMapper[String, String, String] {
+ def apply(k: String, v: String): String = v
+ }
+ }
+
+ val wordCounts: KTableJ[String, java.lang.Long] = grouped.count()
+
+ wordCounts.toStream.to(outputTopicJ, Produced.`with`(Serdes.String(), Serdes.Long()))
+
+ val streams: KafkaStreamsJ = new KafkaStreamsJ(streamBuilder.build(), streamsConfiguration)
+ streams.start()
+
+ val actualWordCounts: java.util.List[KeyValue[String, Long]] = produceNConsume(inputTopicJ, outputTopicJ)
+
+ streams.close()
+
+ assertEquals(actualWordCounts.asScala.take(expectedWordCounts.size).sortBy(_.key), expectedWordCounts.sortBy(_.key))
+ }
+
+ private def getStreamsConfiguration(): Properties = {
+ val streamsConfiguration: Properties = new Properties()
+
+ streamsConfiguration.put(StreamsConfig.APPLICATION_ID_CONFIG, "wordcount-test")
+ streamsConfiguration.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.bootstrapServers())
+ streamsConfiguration.put(StreamsConfig.COMMIT_INTERVAL_MS_CONFIG, "10000")
+ streamsConfiguration.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
+ streamsConfiguration.put(StreamsConfig.STATE_DIR_CONFIG, testFolder.getRoot().getPath())
+ streamsConfiguration
+ }
+
+ private def getProducerConfig(): Properties = {
+ val p = new Properties()
+ p.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.bootstrapServers())
+ p.put(ProducerConfig.ACKS_CONFIG, "all")
+ p.put(ProducerConfig.RETRIES_CONFIG, "0")
+ p.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])
+ p.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, classOf[StringSerializer])
+ p
+ }
+
+ private def getConsumerConfig(): Properties = {
+ val p = new Properties()
+ p.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, cluster.bootstrapServers())
+ p.put(ConsumerConfig.GROUP_ID_CONFIG, "wordcount-scala-integration-test-standard-consumer")
+ p.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest")
+ p.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, classOf[StringDeserializer])
+ p.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, classOf[LongDeserializer])
+ p
+ }
+
+ private def produceNConsume(inputTopic: String, outputTopic: String): java.util.List[KeyValue[String, Long]] = {
+
+ val linesProducerConfig: Properties = getProducerConfig()
+
+ import collection.JavaConverters._
+ IntegrationTestUtils.produceValuesSynchronously(inputTopic, inputValues.asJava, linesProducerConfig, mockTime)
+
+ val consumerConfig = getConsumerConfig()
+
+ IntegrationTestUtils.waitUntilMinKeyValueRecordsReceived(consumerConfig, outputTopic, expectedWordCounts.size)
+ }
+}
+
+trait WordCountTestData {
+ val inputTopic = s"inputTopic"
+ val outputTopic = s"outputTopic"
+ val inputTopicJ = s"inputTopicJ"
+ val outputTopicJ = s"outputTopicJ"
+
+ val inputValues = List(
+ "Hello Kafka Streams",
+ "All streams lead to Kafka",
+ "Join Kafka Summit",
+ "И теперь пошли русские слова"
+ )
+
+ val expectedWordCounts: List[KeyValue[String, Long]] = List(
+ new KeyValue("hello", 1L),
+ new KeyValue("all", 1L),
+ new KeyValue("streams", 2L),
+ new KeyValue("lead", 1L),
+ new KeyValue("to", 1L),
+ new KeyValue("join", 1L),
+ new KeyValue("kafka", 3L),
+ new KeyValue("summit", 1L),
+ new KeyValue("и", 1L),
+ new KeyValue("теперь", 1L),
+ new KeyValue("пошли", 1L),
+ new KeyValue("русские", 1L),
+ new KeyValue("слова", 1L)
+ )
+}
+
ReadOnlyWindowStore which allows for querying WindowStores without the necessity of providing keys.
diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle
index d6beba958ddaa..0d05da3764886 100644
--- a/gradle/dependencies.gradle
+++ b/gradle/dependencies.gradle
@@ -60,6 +60,7 @@ versions += [
log4j: "1.2.17",
scalaLogging: "3.8.0",
jaxb: "2.3.0",
+ jfreechart: "1.0.0",
jopt: "5.0.4",
junit: "4.12",
kafka_0100: "0.10.0.1",
@@ -68,6 +69,7 @@ versions += [
kafka_0110: "0.11.0.2",
kafka_10: "1.0.1",
lz4: "1.4.1",
+ mavenArtifact: "3.5.3",
metrics: "2.2.0",
// PowerMock 1.x doesn't support Java 9, so use PowerMock 2.0.0 beta
powermock: "2.0.0-beta.5",
@@ -78,14 +80,11 @@ versions += [
slf4j: "1.7.25",
snappy: "1.1.7.1",
zkclient: "0.10",
- zookeeper: "3.4.10",
- jfreechart: "1.0.0",
- mavenArtifact: "3.5.3"
+ zookeeper: "3.4.10"
]
libs += [
activation: "javax.activation:activation:$versions.activation",
- argparse4j: "net.sourceforge.argparse4j:argparse4j:$versions.argparse4j",
apacheda: "org.apache.directory.api:api-all:$versions.apacheda",
apachedsCoreApi: "org.apache.directory.server:apacheds-core-api:$versions.apacheds",
apachedsInterceptorKerberos: "org.apache.directory.server:apacheds-interceptor-kerberos:$versions.apacheds",
@@ -95,6 +94,7 @@ libs += [
apachedsLdifPartition: "org.apache.directory.server:apacheds-ldif-partition:$versions.apacheds",
apachedsMavibotPartition: "org.apache.directory.server:apacheds-mavibot-partition:$versions.apacheds",
apachedsJdbmPartition: "org.apache.directory.server:apacheds-jdbm-partition:$versions.apacheds",
+ argparse4j: "net.sourceforge.argparse4j:argparse4j:$versions.argparse4j",
bcpkix: "org.bouncycastle:bcpkix-jdk15on:$versions.bcpkix",
easymock: "org.easymock:easymock:$versions.easymock",
jacksonDatabind: "com.fasterxml.jackson.core:jackson-databind:$versions.jackson",
@@ -105,6 +105,7 @@ libs += [
jettyServlet: "org.eclipse.jetty:jetty-servlet:$versions.jetty",
jettyServlets: "org.eclipse.jetty:jetty-servlets:$versions.jetty",
jerseyContainerServlet: "org.glassfish.jersey.containers:jersey-container-servlet:$versions.jersey",
+ jfreechart: "1.0.0",
jmhCore: "org.openjdk.jmh:jmh-core:$versions.jmh",
jmhCoreBenchmarks: "org.openjdk.jmh:jmh-core-benchmarks:$versions.jmh",
jmhGeneratorAnnProcess: "org.openjdk.jmh:jmh-generator-annprocess:$versions.jmh",
diff --git a/gradle/findbugs-exclude.xml b/gradle/findbugs-exclude.xml
index 33702056d83f6..f36f32376ab17 100644
--- a/gradle/findbugs-exclude.xml
+++ b/gradle/findbugs-exclude.xml
@@ -305,4 +305,19 @@ For a detailed description of findbugs bug categories, see http://findbugs.sourc