getQueue() {
+ return this.queue;
+ }
+
+ public void closeStream() {
+ this.twitterStream.shutdown();
+ }
+}
diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterIO.java b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterIO.java
new file mode 100644
index 000000000000..f194859bbe8f
--- /dev/null
+++ b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterIO.java
@@ -0,0 +1,95 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * 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.apache.beam.examples.complete.twitterstreamgenerator;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.transforms.PTransform;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.values.PBegin;
+import org.apache.beam.sdk.values.PCollection;
+
+/**
+ * An unbounded source for twitter
+ * stream. PTransforms for streaming live tweets from twitter. Reading from Twitter is supported by
+ * read()
+ *
+ * Standard Twitter API can be read using a list of Twitter Config
+ * readStandardStream(List)
+ *
+ * It allow multiple Twitter configurations to demonstrate how multiple twitter streams can be
+ * combined in a single pipeline.
+ *
+ *
{@code
+ * PCollection weatherData = pipeline.apply(
+ * TwitterIO.readStandardStream(
+ * Arrays.asList(
+ * new TwitterConfig.Builder()
+ * .setKey("")
+ * .setSecret("")
+ * .setToken("")
+ * .setTokenSecret("")
+ * .setFilters(Arrays.asList("", ""))
+ * .setLanguage("en")
+ * .setTweetsCount(10L)
+ * .setMinutesToRun(1)
+ * .build())));
+ * }
+ */
+public class TwitterIO {
+
+ /**
+ * Initializes the stream by converting input to a Twitter connection configuration.
+ *
+ * @param twitterConfigs list of twitter config
+ * @return PTransform of statuses
+ */
+ public static PTransform> readStandardStream(
+ List twitterConfigs) {
+ return new TwitterIO.Read.Builder().setTwitterConfig(twitterConfigs).build();
+ }
+
+ /** A {@link PTransform} to read from Twitter stream. usage and configuration. */
+ private static class Read extends PTransform> {
+ private final List twitterConfigs;
+
+ private Read(Builder builder) {
+ this.twitterConfigs = builder.twitterConfigs;
+ }
+
+ @Override
+ public PCollection expand(PBegin input) throws IllegalArgumentException {
+ return input.apply(Create.of(this.twitterConfigs)).apply(ParDo.of(new ReadFromTwitterDoFn()));
+ }
+
+ private static class Builder {
+ List twitterConfigs = new ArrayList<>();
+
+ TwitterIO.Read.Builder setTwitterConfig(final List twitterConfigs) {
+ this.twitterConfigs = twitterConfigs;
+ return this;
+ }
+
+ TwitterIO.Read build() {
+ return new TwitterIO.Read(this);
+ }
+ }
+ }
+}
diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterStream.java b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterStream.java
new file mode 100644
index 000000000000..d1f8b2566c2b
--- /dev/null
+++ b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/TwitterStream.java
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * 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.apache.beam.examples.complete.twitterstreamgenerator;
+
+import java.util.Arrays;
+import org.apache.beam.sdk.Pipeline;
+import org.apache.beam.sdk.transforms.DoFn;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.transforms.windowing.AfterFirst;
+import org.apache.beam.sdk.transforms.windowing.AfterPane;
+import org.apache.beam.sdk.transforms.windowing.AfterProcessingTime;
+import org.apache.beam.sdk.transforms.windowing.FixedWindows;
+import org.apache.beam.sdk.transforms.windowing.Repeatedly;
+import org.apache.beam.sdk.transforms.windowing.Window;
+import org.apache.beam.sdk.values.PCollection;
+import org.joda.time.Duration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@link TwitterStream} pipeline is a streaming pipeline which ingests data in JSON format from
+ * Twitter, and outputs the resulting records to console. Stream configurations are specified by the
+ * user as template parameters.
+ *
+ * Concepts: API connectors and streaming; splittable Dofn and watermarking ; logging
+ *
+ *
To execute this pipeline locally, specify key, secret, token, token-secret and filters to
+ * filter stream with, for your twitter streaming app.You can also set number of tweets ( use set
+ * TweetsCount - default Long.MAX_VALUE ) you wish to stream and/or the number of minutes to run the
+ * pipeline ( use set MinutesToRun: default Integer.MAX_VALUE ) :
+ *
+ *
{@code
+ * new TwitterConfig
+ * .Builder()
+ * .setKey("")
+ * .setSecret("")
+ * .setToken("")
+ * .setTokenSecret("")
+ * .setFilters(Arrays.asList("", "")).build()
+ * }
+ *
+ * To change the runner( does not works on Dataflow ), specify:
+ *
+ *
{@code
+ * --runner=YOUR_SELECTED_RUNNER
+ * }
+ *
+ * See examples/java/README.md for instructions about how to configure different runners.
+ */
+public class TwitterStream {
+
+ private static final Logger LOG = LoggerFactory.getLogger(TwitterStream.class);
+
+ /**
+ * Main entry point for pipeline execution.
+ *
+ * @param args Command line arguments to the pipeline.
+ */
+ public static void main(String[] args) {
+ Pipeline pipeline = Pipeline.create();
+ Window.configure()
+ .triggering(
+ Repeatedly.forever(
+ AfterFirst.of(
+ AfterPane.elementCountAtLeast(10),
+ AfterProcessingTime.pastFirstElementInPane()
+ .plusDelayOf(Duration.standardMinutes(2)))));
+ PCollection tweetStream =
+ pipeline
+ .apply(
+ "Create Twitter Connection Configuration",
+ TwitterIO.readStandardStream(
+ Arrays.asList(
+ new TwitterConfig.Builder()
+ .setKey("")
+ .setSecret("")
+ .setToken("")
+ .setTokenSecret("")
+ .setFilters(Arrays.asList("", ""))
+ .setLanguage("en")
+ .setTweetsCount(10L)
+ .setMinutesToRun(1)
+ .build())))
+ .apply(Window.into(FixedWindows.of(Duration.standardSeconds(1))));
+ tweetStream.apply(
+ "Output Tweets to console",
+ ParDo.of(
+ new DoFn() {
+ @ProcessElement
+ public void processElement(@Element String element, OutputReceiver receiver) {
+ LOG.debug("Output tweets: " + element);
+ receiver.output(element);
+ }
+ }));
+
+ pipeline.run();
+ }
+}
diff --git a/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/package-info.java b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/package-info.java
new file mode 100644
index 000000000000..87cdf672b868
--- /dev/null
+++ b/examples/java/src/main/java/org/apache/beam/examples/complete/twitterstreamgenerator/package-info.java
@@ -0,0 +1,20 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * 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.
+ */
+
+/** Kafka to Pubsub template. */
+package org.apache.beam.examples.complete.twitterstreamgenerator;
diff --git a/examples/java/src/test/java/org/apache/beam/examples/complete/twitterstreamgenerator/ReadFromTwitterDoFnTest.java b/examples/java/src/test/java/org/apache/beam/examples/complete/twitterstreamgenerator/ReadFromTwitterDoFnTest.java
new file mode 100644
index 000000000000..23e768e2f603
--- /dev/null
+++ b/examples/java/src/test/java/org/apache/beam/examples/complete/twitterstreamgenerator/ReadFromTwitterDoFnTest.java
@@ -0,0 +1,136 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * 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.apache.beam.examples.complete.twitterstreamgenerator;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.stream.IntStream;
+import org.apache.beam.sdk.testing.PAssert;
+import org.apache.beam.sdk.testing.TestPipeline;
+import org.apache.beam.sdk.transforms.Create;
+import org.apache.beam.sdk.transforms.ParDo;
+import org.apache.beam.sdk.values.PCollection;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import twitter4j.Status;
+
+@RunWith(JUnit4.class)
+public class ReadFromTwitterDoFnTest {
+ @Rule public final transient TestPipeline pipeline = TestPipeline.create();
+ @Rule public final ExpectedException expectedException = ExpectedException.none();
+ @Mock TwitterConnection twitterConnection1;
+ @Mock TwitterConnection twitterConnection2;
+ @Mock Status status1;
+ @Mock Status status2;
+ @Mock Status status3;
+ @Mock Status status4;
+ @Mock Status status5;
+ LinkedBlockingQueue queue1 = new LinkedBlockingQueue<>();
+ LinkedBlockingQueue queue2 = new LinkedBlockingQueue<>();
+
+ @Before
+ public void setUp() throws JsonProcessingException {
+ MockitoAnnotations.initMocks(this);
+ when(status1.getText()).thenReturn("Breaking News1");
+ when(status1.getCreatedAt()).thenReturn(new Date());
+ when(status2.getText()).thenReturn("Breaking News2");
+ when(status2.getCreatedAt()).thenReturn(new Date());
+ when(status3.getText()).thenReturn("Breaking News3");
+ when(status3.getCreatedAt()).thenReturn(new Date());
+ when(status4.getText()).thenReturn("Breaking News4");
+ when(status4.getCreatedAt()).thenReturn(new Date());
+ when(status5.getText()).thenReturn("Breaking News5");
+ when(status5.getCreatedAt()).thenReturn(new Date());
+ queue1.offer(status1);
+ queue1.offer(status2);
+ queue1.offer(status3);
+ queue2.offer(status4);
+ queue2.offer(status5);
+ }
+
+ @Test
+ public void testTwitterRead() {
+ TwitterConfig twitterConfig = new TwitterConfig.Builder().setTweetsCount(3L).build();
+ TwitterConnection.INSTANCE_MAP.put(twitterConfig, twitterConnection1);
+ when(twitterConnection1.getQueue()).thenReturn(queue1);
+ PCollection result =
+ pipeline
+ .apply("Create Twitter Connection Configuration", Create.of(twitterConfig))
+ .apply(ParDo.of(new ReadFromTwitterDoFn()));
+ PAssert.that(result)
+ .satisfies(
+ pcollection -> {
+ List output = new ArrayList<>();
+ pcollection.forEach(output::add);
+ String[] expected = {"Breaking News1", "Breaking News2", "Breaking News3"};
+ String[] actual = new String[output.size()];
+ IntStream.range(0, output.size()).forEach((i) -> actual[i] = output.get(i));
+ assertArrayEquals("Mismatch found in output", actual, expected);
+ return null;
+ });
+ pipeline.run();
+ }
+
+ @Test
+ public void testMultipleTwitterConfigs() {
+ TwitterConfig twitterConfig1 = new TwitterConfig.Builder().setTweetsCount(3L).build();
+ TwitterConfig twitterConfig2 = new TwitterConfig.Builder().setTweetsCount(2L).build();
+ TwitterConnection.INSTANCE_MAP.put(twitterConfig1, twitterConnection1);
+ TwitterConnection.INSTANCE_MAP.put(twitterConfig2, twitterConnection2);
+ when(twitterConnection1.getQueue()).thenReturn(queue1);
+ when(twitterConnection2.getQueue()).thenReturn(queue2);
+ PCollection result =
+ pipeline
+ .apply(
+ "Create Twitter Connection Configuration",
+ Create.of(twitterConfig1, twitterConfig2))
+ .apply(ParDo.of(new ReadFromTwitterDoFn()));
+ PAssert.that(result)
+ .satisfies(
+ pcollection -> {
+ List output = new ArrayList<>();
+ pcollection.forEach(output::add);
+ String[] expected = {
+ "Breaking News1",
+ "Breaking News2",
+ "Breaking News3",
+ "Breaking News4",
+ "Breaking News5"
+ };
+ String[] actual = new String[output.size()];
+ Collections.sort(output);
+ IntStream.range(0, output.size()).forEach((i) -> actual[i] = output.get(i));
+ assertArrayEquals("Mismatch found in output", actual, expected);
+ return null;
+ });
+ pipeline.run();
+ }
+}