This is an automated email from the ASF dual-hosted git repository.
tallison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/main by this push:
new 04d00a0ae TIKA-3820 add tika pipes kafka support (#616)
04d00a0ae is described below
commit 04d00a0aecd0dc4c0f4250fa972d407face06bb5
Author: Nicholas DiPiazza <[email protected]>
AuthorDate: Mon Jul 25 11:52:34 2022 -0500
TIKA-3820 add tika pipes kafka support (#616)
* wip - add kafka support
* wip - add kafka support
* finish setting up intiial kafka commit - tests pass.
* fix readme
* remove dead file, rename good test
* remove stuff from earlier test attempts
* deleted wrong file previously.
* bump version after merge.
* add an emit max in case the documents keep coming into the pipe iterator
forever.
* clean up javadoc.
* fix a few issues with the test.
* remove commented out line.
* no need to create the topics manually after i fixed issue with
conflicting group ids
* remove create topic code.
* add javadoc prefix
* looks like a refactor renamed a couple files
* improve messages
* fix poor message in javadoc.
* apply change suggested in https://stackoverflow.com/a/73024959/1174024
* clarify
* fix message
* fix annoying IDE warnings
---
tika-bom/pom.xml | 10 +
tika-integration-tests/pom.xml | 1 +
.../tika-pipes-kafka-integration-tests/README.md | 7 +
.../pom.xml | 50 ++--
.../tika/pipes/kafka/tests/TikaPipesKafkaTest.java | 215 ++++++++++++++++
.../src/test/resources/log4j2.xml | 32 +++
.../resources/pipes-fork-server-custom-log4j2.xml | 32 +++
.../src/test/resources/tika-config-kafka.xml | 118 +++++++++
.../pipes/solr/tests/TikaPipesSolrTestBase.java | 1 -
tika-parent/pom.xml | 1 +
tika-pipes/tika-emitters/pom.xml | 1 +
.../tika-emitters/tika-emitter-kafka/pom.xml | 132 ++++++++++
.../tika/pipes/emitter/kafka/KafkaEmitter.java | 284 +++++++++++++++++++++
tika-pipes/tika-pipes-iterators/pom.xml | 1 +
.../tika-pipes-iterator-kafka/pom.xml | 125 +++++++++
.../pipesiterator/kafka/KafkaPipesIterator.java | 174 +++++++++++++
.../kafka/TestKafkaPipesIterator.java | 98 +++++++
.../src/test/resources/log4j2.xml | 32 +++
18 files changed, 1294 insertions(+), 20 deletions(-)
diff --git a/tika-bom/pom.xml b/tika-bom/pom.xml
index 5226c95fd..c969b7654 100644
--- a/tika-bom/pom.xml
+++ b/tika-bom/pom.xml
@@ -318,6 +318,11 @@
<artifactId>tika-emitter-s3</artifactId>
<version>2.4.2-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-emitter-kafka</artifactId>
+ <version>2.4.2-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-emitter-solr</artifactId>
@@ -360,6 +365,11 @@
<artifactId>tika-pipes-iterator-s3</artifactId>
<version>2.4.2-SNAPSHOT</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-pipes-iterator-kafka</artifactId>
+ <version>2.4.2-SNAPSHOT</version>
+ </dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-pipes-iterator-solr</artifactId>
diff --git a/tika-integration-tests/pom.xml b/tika-integration-tests/pom.xml
index aecdef34c..f358458d9 100644
--- a/tika-integration-tests/pom.xml
+++ b/tika-integration-tests/pom.xml
@@ -36,6 +36,7 @@
<module>tika-pipes-opensearch-integration-tests</module>
<module>tika-pipes-s3-integration-tests</module>
<module>tika-resource-loading-tests</module>
+ <module>tika-pipes-kafka-integration-tests</module>
</modules>
<dependencies>
diff --git
a/tika-integration-tests/tika-pipes-kafka-integration-tests/README.md
b/tika-integration-tests/tika-pipes-kafka-integration-tests/README.md
new file mode 100644
index 000000000..2bc76c8fb
--- /dev/null
+++ b/tika-integration-tests/tika-pipes-kafka-integration-tests/README.md
@@ -0,0 +1,7 @@
+# Tika Pipes - Kafka - Integration tests
+
+The following test uses TestContainers to test the Tika Pipes Kafka modules:
+
+* Kafka Pipe Iterator
+* Kafka Emitter
+
diff --git a/tika-integration-tests/pom.xml
b/tika-integration-tests/tika-pipes-kafka-integration-tests/pom.xml
similarity index 57%
copy from tika-integration-tests/pom.xml
copy to tika-integration-tests/tika-pipes-kafka-integration-tests/pom.xml
index aecdef34c..e7447c98b 100644
--- a/tika-integration-tests/pom.xml
+++ b/tika-integration-tests/tika-pipes-kafka-integration-tests/pom.xml
@@ -17,28 +17,30 @@
specific language governing permissions and limitations
under the License.
-->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
+ <artifactId>tika-integration-tests</artifactId>
<groupId>org.apache.tika</groupId>
- <artifactId>tika-parent</artifactId>
<version>2.4.2-SNAPSHOT</version>
- <relativePath>../tika-parent/pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>tika-integration-tests</artifactId>
- <name>Apache Tika integration tests</name>
-
- <packaging>pom</packaging>
-
- <modules>
- <module>tika-pipes-solr-integration-tests</module>
- <module>tika-pipes-opensearch-integration-tests</module>
- <module>tika-pipes-s3-integration-tests</module>
- <module>tika-resource-loading-tests</module>
- </modules>
+ <artifactId>tika-pipes-kafka-integration-tests</artifactId>
+ <name>Apache Tika Kafka pipes integration tests</name>
<dependencies>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>testcontainers</artifactId>
+ <version>${test.containers.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.testcontainers</groupId>
+ <artifactId>kafka</artifactId>
+ <version>${test.containers.version}</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>tika-core</artifactId>
@@ -47,15 +49,25 @@
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
- <artifactId>tika-serialization</artifactId>
+ <artifactId>tika-pipes-iterator-kafka</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-emitter-kafka</artifactId>
+ <version>${project.version}</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-app</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
- <!-- after we migrate everything to junit5, we can get rid of this -->
<dependency>
- <groupId>org.junit.vintage</groupId>
- <artifactId>junit-vintage-engine</artifactId>
- <version>${junit5.version}</version>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j-impl</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
diff --git
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
new file mode 100644
index 000000000..18a3cbf45
--- /dev/null
+++
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/java/org/apache/tika/pipes/kafka/tests/TikaPipesKafkaTest.java
@@ -0,0 +1,215 @@
+/*
+ * 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.tika.pipes.kafka.tests;
+
+import java.io.File;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Stopwatch;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.clients.producer.KafkaProducer;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.apache.tika.cli.TikaCLI;
+import org.apache.tika.pipes.HandlerConfig;
+import org.jetbrains.annotations.NotNull;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.testcontainers.containers.KafkaContainer;
+import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
+import org.testcontainers.utility.DockerImageName;
+
+/**
+ * Test will emit some documents into a Kafka "pipe_iterator_topic", then
kafka pipe iterator will
+ * poll those documents and send them to tika pipes. Tika pipes will then use
a file fetcher to fetch/parse, then
+ * the kafka emitter will send the now-parsed output to the "emitter_topic".
+ * Will then wait for the messages to come from the emitter and assert they
are correct.
+ */
+public class TikaPipesKafkaTest {
+ private static final Logger LOG =
LoggerFactory.getLogger(TikaPipesKafkaTest.class);
+
+ public static final String PIPE_ITERATOR_TOPIC = "pipe_iterator_topic";
+ public static final String EMITTER_TOPIC = "emitter_topic";
+ /**
+ * Wait up to this many minutes before you give up waiting for the emitted
documents to poll from the
+ * emitter_topic and fail the test.
+ */
+ public static final int WAIT_FOR_EMITTED_DOCS_TIMEOUT_MINUTES = 2;
+ private final int numDocs = 42;
+
+ private final ObjectMapper objectMapper = new ObjectMapper();
+
+ private final File testFileFolder = new File("target", "test-files");
+
+ private final Set<String> waitingFor = new HashSet<>();
+
+ private void createTestFiles() throws Exception {
+ if (testFileFolder.mkdirs()) {
+ LOG.info("Created test folder: {}", testFileFolder);
+ }
+ for (int i = 0; i < numDocs; ++i) {
+ String nextFileName = "test-" + i + ".html";
+ FileUtils.writeStringToFile(new File(testFileFolder, nextFileName),
+ "<html><body>body-" + i + "</body></html>",
StandardCharsets.UTF_8);
+ waitingFor.add(nextFileName);
+ }
+ }
+
+ KafkaContainer kafka = new
KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:6.2.1"));
+
+ @Before
+ public void before() {
+ kafka.start();
+ }
+
+ @After
+ public void after() {
+ kafka.close();
+ }
+
+ @Test
+ public void testKafkaPipeIteratorAndEmitter()
+ throws Exception {
+ createTestFiles();
+ File tikaConfigFile = new File("target", "ta.xml");
+ File log4jPropFile = new File("target", "tmp-log4j2.xml");
+ try (InputStream is = this.getClass()
+ .getResourceAsStream("/pipes-fork-server-custom-log4j2.xml")) {
+ assert is != null;
+ FileUtils.copyInputStreamToFile(is, log4jPropFile);
+ }
+ String tikaConfigTemplateXml;
+ try (InputStream is = this.getClass()
+ .getResourceAsStream("/tika-config-kafka.xml")) {
+ assert is != null;
+ tikaConfigTemplateXml = IOUtils.toString(is,
StandardCharsets.UTF_8);
+ }
+
+ Properties consumerProps = new Properties();
+ consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
kafka.getBootstrapServers());
+ consumerProps.put("group.id", UUID.randomUUID().toString());
+ consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
+ consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
StringDeserializer.class.getName());
+ consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
+
+ Properties producerProps = new Properties();
+ producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
kafka.getBootstrapServers());
+ producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
+ producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
StringSerializer.class.getName());
+
+ KafkaConsumer<String, String> consumer = new
KafkaConsumer<>(consumerProps);
+ LOG.info("Listening to EMITTER_TOPIC={}", EMITTER_TOPIC);
+ consumer.subscribe(Collections.singletonList(EMITTER_TOPIC));
+
+ ExecutorService es = Executors.newCachedThreadPool();
+
+ try (KafkaProducer<String, String> producer = new
KafkaProducer<>(producerProps)) {
+ int numSent = 0;
+ for (int i = 0; i < numDocs; ++i) {
+ File nextFile = new File(testFileFolder, "test-" + i +
".html");
+ Map<String, Object> meta = new HashMap<>();
+ meta.put("name", nextFile.getName());
+ meta.put("path", nextFile.getAbsolutePath());
+ meta.put("totalSpace", nextFile.getTotalSpace());
+ try {
+ producer.send(new ProducerRecord<>(PIPE_ITERATOR_TOPIC,
+ nextFile.getAbsolutePath(),
+ objectMapper.writeValueAsString(meta))).get();
+ LOG.info("Sent fetch request : {}",
nextFile.getAbsolutePath());
+ ++numSent;
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+ LOG.info("Producer is now complete - sent {}.", numSent);
+ }
+
+ es.execute(() -> {
+ try {
+ String tikaConfigXml =
+ createTikaConfigXml(tikaConfigFile, log4jPropFile,
tikaConfigTemplateXml
+ );
+
+ FileUtils.writeStringToFile(tikaConfigFile, tikaConfigXml,
StandardCharsets.UTF_8);
+ TikaCLI.main(new String[] {"-a", "--config=" +
tikaConfigFile.getAbsolutePath()});
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ });
+
+ LOG.info("Tika pipes have been started. See if we can pull the
response messages from the EMITTER_TOPIC={}", EMITTER_TOPIC);
+
+ Stopwatch stopwatch = Stopwatch.createStarted();
+ while (!waitingFor.isEmpty()) {
+ Assert.assertFalse("Timed out after " +
WAIT_FOR_EMITTED_DOCS_TIMEOUT_MINUTES + " minutes waiting for the emitted docs",
+ stopwatch.elapsed(TimeUnit.MINUTES) >
WAIT_FOR_EMITTED_DOCS_TIMEOUT_MINUTES);
+ try {
+ ConsumerRecords<String, String> records =
consumer.poll(Duration.ofSeconds(1));
+ for (ConsumerRecord<String, String> record : records) {
+ String val = record.value();
+ Map<String, Object> valMap = objectMapper.readValue(val,
new TypeReference<Map<String, Object>>() {});
+ waitingFor.remove(FilenameUtils.getName(record.key()));
+ Assert.assertNotNull(valMap.get("content_s"));
+ Assert.assertNotNull(valMap.get("mime_s"));
+ Assert.assertNotNull(valMap.get("length_i"));
+ LOG.info("Received message key={}, offset={}",
record.key(), record.offset());
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ LOG.info("Done");
+ }
+
+ @NotNull
+ private String createTikaConfigXml(File tikaConfigFile, File log4jPropFile,
+ String tikaConfigTemplateXml) {
+ return tikaConfigTemplateXml.replace("{TIKA_CONFIG}",
tikaConfigFile.getAbsolutePath())
+ .replace("{LOG4J_PROPERTIES_FILE}",
log4jPropFile.getAbsolutePath())
+ .replace("{PATH_TO_DOCS}",
testFileFolder.getAbsolutePath())
+ .replace("{PARSE_MODE}",
HandlerConfig.PARSE_MODE.RMETA.name())
+ .replace("{PIPE_ITERATOR_TOPIC}", PIPE_ITERATOR_TOPIC)
+ .replace("{EMITTER_TOPIC}", EMITTER_TOPIC)
+ .replace("{BOOTSTRAP_SERVERS}",
kafka.getBootstrapServers());
+ }
+}
diff --git
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/log4j2.xml
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/log4j2.xml
new file mode 100644
index 000000000..c88e66e99
--- /dev/null
+++
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/log4j2.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+
+<!--
+ 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.
+-->
+<Configuration status="WARN">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_ERR">
+ <PatternLayout pattern="%-5p [%t] %d{HH:mm:ss,SSS} %c %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file
diff --git
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/pipes-fork-server-custom-log4j2.xml
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/pipes-fork-server-custom-log4j2.xml
new file mode 100644
index 000000000..5f946e6e5
--- /dev/null
+++
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/pipes-fork-server-custom-log4j2.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<Configuration status="INFO">
+ <Appenders>
+ <Console name="console" target="SYSTEM_ERR">
+ <PatternLayout
+ pattern="%-5p [%t] %d{HH:mm:ss,SSS} %c %m%n" />
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="info" additivity="false">
+ <AppenderRef ref="console" />
+ </Root>
+ </Loggers>
+</Configuration>
diff --git
a/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/tika-config-kafka.xml
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/tika-config-kafka.xml
new file mode 100644
index 000000000..f15faf262
--- /dev/null
+++
b/tika-integration-tests/tika-pipes-kafka-integration-tests/src/test/resources/tika-config-kafka.xml
@@ -0,0 +1,118 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+ 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.
+-->
+<properties>
+ <parsers>
+ <parser class="org.apache.tika.parser.DefaultParser">
+ <parser-exclude
class="org.apache.tika.parser.ocr.TesseractOCRParser"/>
+ <parser-exclude class="org.apache.tika.parser.pdf.PDFParser"/>
+ <parser-exclude
class="org.apache.tika.parser.microsoft.ooxml.OOXMLParser"/>
+ <parser-exclude
class="org.apache.tika.parser.microsoft.OfficeParser"/>
+ </parser>
+ <parser class="org.apache.tika.parser.pdf.PDFParser">
+ <params>
+ <param name="extractActions" type="bool">true</param>
+ <param name="checkExtractAccessPermissions"
type="bool">true</param>
+ </params>
+ </parser>
+ <parser class="org.apache.tika.parser.microsoft.ooxml.OOXMLParser">
+ <params>
+ <param name="includeDeletedContent" type="bool">true</param>
+ <param name="includeMoveFromContent" type="bool">true</param>
+ <param name="extractMacros" type="bool">true</param>
+ </params>
+ </parser>
+ <parser class="org.apache.tika.parser.microsoft.OfficeParser">
+ <params>
+ <param name="extractMacros" type="bool">true</param>
+ </params>
+ </parser>
+ </parsers>
+ <metadataFilters>
+ <!-- depending on the file format, some dates do not have a timezone.
This
+ filter arbitrarily assumes dates have a UTC timezone and will
format all
+ dates as yyyy-MM-dd'T'HH:mm:ss'Z' whether or not they actually
have a timezone.
+ -->
+ <metadataFilter
class="org.apache.tika.metadata.filter.DateNormalizingMetadataFilter"/>
+ <metadataFilter
class="org.apache.tika.metadata.filter.FieldNameMappingFilter">
+ <params>
+ <excludeUnmapped>true</excludeUnmapped>
+ <mappings>
+ <mapping from="X-TIKA:content" to="content_s"/>
+ <mapping from="Content-Length" to="length_i"/>
+ <mapping from="dc:creator" to="creators_ss"/>
+ <mapping from="dc:title" to="title_s"/>
+ <mapping from="Content-Type" to="mime_s"/>
+ <mapping from="X-TIKA:EXCEPTION:container_exception"
to="tika_exception_s"/>
+ </mappings>
+ </params>
+ </metadataFilter>
+ </metadataFilters>
+ <async>
+ <params>
+ <maxForEmitBatchBytes>10000</maxForEmitBatchBytes>
+ <emitMaxEstimatedBytes>100000</emitMaxEstimatedBytes>
+ <emitWithinMillis>10</emitWithinMillis>
+ <numEmitters>1</numEmitters>
+ <numClients>1</numClients>
+ <tikaConfig>{TIKA_CONFIG}</tikaConfig>
+ <forkedJvmArgs>
+ <arg>-Xmx1g</arg>
+ <arg>-XX:ParallelGCThreads=2</arg>
+ <arg>-XX:+ExitOnOutOfMemoryError</arg>
+ <arg>-Dlog4j.configurationFile={LOG4J_PROPERTIES_FILE}</arg>
+ </forkedJvmArgs>
+ <timeoutMillis>60000</timeoutMillis>
+ </params>
+ </async>
+ <fetchers>
+ <fetcher class="org.apache.tika.pipes.fetcher.fs.FileSystemFetcher">
+ <params>
+ <name>fsf</name>
+ <basePath>{PATH_TO_DOCS}</basePath>
+ </params>
+ </fetcher>
+ </fetchers>
+ <emitters>
+ <emitter class="org.apache.tika.pipes.emitter.kafka.KafkaEmitter">
+ <params>
+ <name>ke</name>
+ <topic>{EMITTER_TOPIC}</topic>
+ <bootstrapServers>{BOOTSTRAP_SERVERS}</bootstrapServers>
+ </params>
+ </emitter>
+ <emitter class="org.apache.tika.pipes.emitter.fs.FileSystemEmitter">
+ <params>
+ <name>fse</name>
+ <basePath>/path/to/extracts</basePath>
+ </params>
+ </emitter>
+ </emitters>
+ <pipesIterator
class="org.apache.tika.pipes.pipesiterator.kafka.KafkaPipesIterator">
+ <params>
+ <topic>{PIPE_ITERATOR_TOPIC}</topic>
+ <bootstrapServers>{BOOTSTRAP_SERVERS}</bootstrapServers>
+ <groupId>grpid</groupId>
+ <autoOffsetReset>earliest</autoOffsetReset>
+ <pollDelayMs>1000</pollDelayMs>
+ <fetcherName>fsf</fetcherName>
+ <emitterName>ke</emitterName>
+ </params>
+ </pipesIterator>
+</properties>
\ No newline at end of file
diff --git
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
index 49e51c9ff..c82323bd9 100644
---
a/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
+++
b/tika-integration-tests/tika-pipes-solr-integration-tests/src/test/java/org/apache/tika/pipes/solr/tests/TikaPipesSolrTestBase.java
@@ -30,7 +30,6 @@ import org.apache.http.impl.client.HttpClients;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrQuery;
import org.apache.solr.client.solrj.impl.LBHttpSolrClient;
-import org.apache.solr.client.solrj.response.UpdateResponse;
import org.apache.solr.common.SolrInputDocument;
import org.apache.tika.cli.TikaCLI;
import org.apache.tika.pipes.HandlerConfig;
diff --git a/tika-parent/pom.xml b/tika-parent/pom.xml
index be096a65f..c17065c89 100644
--- a/tika-parent/pom.xml
+++ b/tika-parent/pom.xml
@@ -389,6 +389,7 @@
<xerces.version>2.12.2</xerces.version>
<xmpcore.version>6.1.11</xmpcore.version>
<zstd.version>1.5.2-3</zstd.version>
+ <kafka.version>3.2.0</kafka.version>
<netty.version>4.1.79.Final</netty.version>
</properties>
diff --git a/tika-pipes/tika-emitters/pom.xml b/tika-pipes/tika-emitters/pom.xml
index 887aad93b..c5777b20b 100644
--- a/tika-pipes/tika-emitters/pom.xml
+++ b/tika-pipes/tika-emitters/pom.xml
@@ -34,6 +34,7 @@
<modules>
<module>tika-emitter-fs</module>
<module>tika-emitter-s3</module>
+ <module>tika-emitter-kafka</module>
<module>tika-emitter-solr</module>
<module>tika-emitter-opensearch</module>
<module>tika-emitter-gcs</module>
diff --git a/tika-pipes/tika-emitters/tika-emitter-kafka/pom.xml
b/tika-pipes/tika-emitters/tika-emitter-kafka/pom.xml
new file mode 100644
index 000000000..24d959503
--- /dev/null
+++ b/tika-pipes/tika-emitters/tika-emitter-kafka/pom.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <artifactId>tika-emitters</artifactId>
+ <groupId>org.apache.tika</groupId>
+ <version>2.4.2-SNAPSHOT</version>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>tika-emitter-kafka</artifactId>
+ <name>Apache Tika Kafka emitter</name>
+
+
+ <dependencies>
+ <!-- should serialization be provided or bundled? -->
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-serialization</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka-clients</artifactId>
+ <version>${kafka.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j-impl</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+
<Automatic-Module-Name>org.apache.tika.pipes.emitter.kafka</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${maven.shade.version}</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <createDependencyReducedPom>
+ false
+ </createDependencyReducedPom>
+ <!-- <filters> -->
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*</exclude>
+ <exclude>LICENSE.txt</exclude>
+ <exclude>NOTICE.txt</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/LICENSE</resource>
+ <file>target/classes/META-INF/LICENSE</file>
+ </transformer>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/NOTICE</resource>
+ <file>target/classes/META-INF/NOTICE</file>
+ </transformer>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/DEPENDENCIES</resource>
+ <file>target/classes/META-INF/DEPENDENCIES</file>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+
+ <scm>
+ <tag>2.2.1-rc2</tag>
+ </scm>
+</project>
\ No newline at end of file
diff --git
a/tika-pipes/tika-emitters/tika-emitter-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitter.java
b/tika-pipes/tika-emitters/tika-emitter-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitter.java
new file mode 100644
index 000000000..aeee40049
--- /dev/null
+++
b/tika-pipes/tika-emitters/tika-emitter-kafka/src/main/java/org/apache/tika/pipes/emitter/kafka/KafkaEmitter.java
@@ -0,0 +1,284 @@
+/*
+ * 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.tika.pipes.emitter.kafka;
+
+import static org.apache.tika.config.TikaConfig.mustNotBeEmpty;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.kafka.clients.producer.KafkaProducer;
+import org.apache.kafka.clients.producer.Producer;
+import org.apache.kafka.clients.producer.ProducerConfig;
+import org.apache.kafka.clients.producer.ProducerRecord;
+import org.apache.kafka.common.serialization.StringSerializer;
+import org.apache.tika.config.Field;
+import org.apache.tika.config.Initializable;
+import org.apache.tika.config.InitializableProblemHandler;
+import org.apache.tika.config.Param;
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.pipes.emitter.AbstractEmitter;
+import org.apache.tika.pipes.emitter.TikaEmitterException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Emits the now-parsed documents into a specified Apache Kafka topic.
+ */
+public class KafkaEmitter extends AbstractEmitter implements Initializable {
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KafkaEmitter.class);
+
+ private static final ObjectMapper OM = new ObjectMapper();
+
+ String topic;
+ String bootstrapServers;
+
+ String acks = "all";
+ int lingerMs = 5000;
+ int batchSize = 16384;
+ int bufferMemory = 32 * 1024 * 1024;
+ String compressionType = "none";
+ int connectionsMaxIdleMs = 9 * 60 * 1000;
+ int deliveryTimeoutMs = 120 * 1000;
+ boolean enableIdempotence = false;
+ String interceptorClasses;
+ int maxBlockMs = 60 * 1000;
+ int maxInFlightRequestsPerConnection = 5;
+ int maxRequestSize = 1024 * 1024;
+ int metadataMaxAgeMs = 5 * 60 * 1000;
+ int requestTimeoutMs = 30 * 1000;
+ int retries = Integer.MAX_VALUE;
+ int retryBackoffMs = 100;
+ int transactionTimeoutMs = 60000;
+ String transactionalId;
+ String clientId;
+ String keySerializer;
+ String valueSerializer;
+
+ private Producer<String, String> producer;
+
+ @Field
+ public void setBootstrapServers(String bootstrapServers) {
+ this.bootstrapServers = bootstrapServers;
+ }
+
+ @Field
+ public void setAcks(String acks) {
+ this.acks = acks;
+ }
+
+ @Field
+ public void setLingerMs(int lingerMs) {
+ this.lingerMs = lingerMs;
+ }
+
+ public void setBatchSize(int batchSize) {
+ this.batchSize = batchSize;
+ }
+
+ @Field
+ public void setBufferMemory(int bufferMemory) {
+ this.bufferMemory = bufferMemory;
+ }
+
+ @Field
+ public void setClientId(String clientId) {
+ this.clientId = clientId;
+ }
+
+ @Field
+ public void setCompressionType(String compressionType) {
+ this.compressionType = compressionType;
+ }
+
+ @Field
+ public void setConnectionsMaxIdleMs(int connectionsMaxIdleMs) {
+ this.connectionsMaxIdleMs = connectionsMaxIdleMs;
+ }
+
+ @Field
+ public void setDeliveryTimeoutMs(int deliveryTimeoutMs) {
+ this.deliveryTimeoutMs = deliveryTimeoutMs;
+ }
+
+ @Field
+ public void setEnableIdempotence(boolean enableIdempotence) {
+ this.enableIdempotence = enableIdempotence;
+ }
+
+ @Field
+ public void setInterceptorClasses(String interceptorClasses) {
+ this.interceptorClasses = interceptorClasses;
+ }
+
+ @Field
+ public void setMaxBlockMs(int maxBlockMs) {
+ this.maxBlockMs = maxBlockMs;
+ }
+
+ @Field
+ public void setMaxInFlightRequestsPerConnection(int
maxInFlightRequestsPerConnection) {
+ this.maxInFlightRequestsPerConnection =
maxInFlightRequestsPerConnection;
+ }
+
+ @Field
+ public void setMaxRequestSize(int maxRequestSize) {
+ this.maxRequestSize = maxRequestSize;
+ }
+
+ @Field
+ public void setMetadataMaxAgeMs(int metadataMaxAgeMs) {
+ this.metadataMaxAgeMs = metadataMaxAgeMs;
+ }
+
+ @Field
+ public void setRequestTimeoutMs(int requestTimeoutMs) {
+ this.requestTimeoutMs = requestTimeoutMs;
+ }
+
+ @Field
+ public void setRetries(int retries) {
+ this.retries = retries;
+ }
+
+ @Field
+ public void setRetryBackoffMs(int retryBackoffMs) {
+ this.retryBackoffMs = retryBackoffMs;
+ }
+
+ @Field
+ public void setTransactionTimeoutMs(int transactionTimeoutMs) {
+ this.transactionTimeoutMs = transactionTimeoutMs;
+ }
+
+ @Field
+ public void setTransactionalId(String transactionalId) {
+ this.transactionalId = transactionalId;
+ }
+
+ @Field
+ public void setKeySerializer(String keySerializer) {
+ this.keySerializer = keySerializer;
+ }
+
+ @Field
+ public void setValueSerializer(String valueSerializer) {
+ this.valueSerializer = valueSerializer;
+ }
+
+ @Field
+ public void setTopic(String topic) {
+ this.topic = topic;
+ }
+
+ @Override
+ public void emit(String emitKey, List<Metadata> metadataList)
+ throws IOException, TikaEmitterException {
+ if (metadataList == null || metadataList.isEmpty()) {
+ throw new TikaEmitterException("metadata list must not be null or
of size 0");
+ }
+ for (Metadata metadata : metadataList) {
+ LOGGER.debug("about to emit to target topic: ({}) path:({})",
topic, emitKey);
+
+ Map<String, Object> fields = new HashMap<>();
+ for (String n : metadata.names()) {
+ String[] vals = metadata.getValues(n);
+ if (vals.length > 1) {
+ LOGGER.warn("Can only write the first value for key {}. I
see {} values.",
+ n,
+ vals.length);
+ }
+ fields.put(n, vals[0]);
+ }
+
+ producer.send(new ProducerRecord<>(topic, emitKey,
OM.writeValueAsString(fields)));
+ }
+ }
+
+ private void safePut(Properties props, String key, Object val) {
+ if (val != null) {
+ props.put(key, val);
+ }
+ }
+
+ @Override
+ public void initialize(Map<String, Param> params) throws
TikaConfigException {
+
+ // create instance for properties to access producer configs
+ Properties props = new Properties();
+
+ //Assign localhost id
+ safePut(props, ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
bootstrapServers);
+
+ //Set acknowledgements for producer requests.
+ safePut(props, ProducerConfig.ACKS_CONFIG, acks);
+
+ //If the request fails, the producer can automatically retry,
+ safePut(props, ProducerConfig.RETRIES_CONFIG, retries);
+
+ //Specify buffer size in config
+ safePut(props, ProducerConfig.BATCH_SIZE_CONFIG, batchSize);
+
+ //Reduce the no of requests less than 0
+ safePut(props, ProducerConfig.LINGER_MS_CONFIG, lingerMs);
+
+ //The buffer.memory controls the total amount of memory available to
the producer for buffering.
+ safePut(props, ProducerConfig.BUFFER_MEMORY_CONFIG, bufferMemory);
+
+ safePut(props, ProducerConfig.CLIENT_ID_CONFIG, clientId);
+ safePut(props, ProducerConfig.COMPRESSION_TYPE_CONFIG,
compressionType);
+ safePut(props, ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG,
deliveryTimeoutMs);
+ safePut(props, ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG,
enableIdempotence);
+ safePut(props, ProducerConfig.INTERCEPTOR_CLASSES_CONFIG,
interceptorClasses);
+ safePut(props, ProducerConfig.MAX_BLOCK_MS_CONFIG, maxBlockMs);
+ safePut(props, ProducerConfig.MAX_IN_FLIGHT_REQUESTS_PER_CONNECTION,
maxInFlightRequestsPerConnection);
+ safePut(props, ProducerConfig.MAX_REQUEST_SIZE_CONFIG, maxRequestSize);
+ safePut(props, ProducerConfig.METADATA_MAX_AGE_CONFIG,
metadataMaxAgeMs);
+ safePut(props, ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG,
requestTimeoutMs);
+ safePut(props, ProducerConfig.RETRY_BACKOFF_MS_CONFIG, retryBackoffMs);
+ safePut(props, ProducerConfig.TRANSACTION_TIMEOUT_CONFIG,
transactionTimeoutMs);
+ safePut(props, ProducerConfig.TRANSACTIONAL_ID_CONFIG,
transactionalId);
+
+ safePut(props, ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
serializerClass(keySerializer, StringSerializer.class));
+ safePut(props, ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG,
serializerClass(valueSerializer, StringSerializer.class));
+
+ producer = new KafkaProducer<>(props);
+ }
+
+ private Object serializerClass(String className, Class defaultClass) {
+ try {
+ return className == null ? defaultClass : Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ LOGGER.error("Could not find key serializer class: {}", className);
+ return null;
+ }
+ }
+
+ @Override
+ public void checkInitialization(InitializableProblemHandler problemHandler)
+ throws TikaConfigException {
+ mustNotBeEmpty("topic", this.topic);
+ mustNotBeEmpty("server", this.bootstrapServers);
+ }
+
+}
diff --git a/tika-pipes/tika-pipes-iterators/pom.xml
b/tika-pipes/tika-pipes-iterators/pom.xml
index 3f3415be8..d5ace7056 100644
--- a/tika-pipes/tika-pipes-iterators/pom.xml
+++ b/tika-pipes/tika-pipes-iterators/pom.xml
@@ -37,6 +37,7 @@
<module>tika-pipes-iterator-csv</module>
<module>tika-pipes-iterator-jdbc</module>
<module>tika-pipes-iterator-s3</module>
+ <module>tika-pipes-iterator-kafka</module>
<module>tika-pipes-iterator-solr</module>
<module>tika-pipes-iterator-gcs</module>
<module>tika-pipes-iterator-az-blob</module>
diff --git a/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/pom.xml
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/pom.xml
new file mode 100644
index 000000000..086ec23f6
--- /dev/null
+++ b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-pipes-iterators</artifactId>
+ <version>2.4.2-SNAPSHOT</version>
+ <relativePath>../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>tika-pipes-iterator-kafka</artifactId>
+
+ <name>Apache Tika Pipes Iterator - Kafka</name>
+ <url>https://tika.apache.org/</url>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>tika-core</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.kafka</groupId>
+ <artifactId>kafka-clients</artifactId>
+ <version>${kafka.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-slf4j-impl</artifactId>
+ <version>${log4j2.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <configuration>
+ <archive>
+ <manifestEntries>
+
<Automatic-Module-Name>org.apache.tika.pipes.pipesiterator.kafka</Automatic-Module-Name>
+ </manifestEntries>
+ </archive>
+ </configuration>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <artifactId>maven-shade-plugin</artifactId>
+ <version>${maven.shade.version}</version>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>shade</goal>
+ </goals>
+ <configuration>
+ <createDependencyReducedPom>
+ false
+ </createDependencyReducedPom>
+ <!-- <filters> -->
+ <filters>
+ <filter>
+ <artifact>*:*</artifact>
+ <excludes>
+ <exclude>META-INF/*</exclude>
+ <exclude>LICENSE.txt</exclude>
+ <exclude>NOTICE.txt</exclude>
+ </excludes>
+ </filter>
+ </filters>
+ <transformers>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/LICENSE</resource>
+ <file>target/classes/META-INF/LICENSE</file>
+ </transformer>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/NOTICE</resource>
+ <file>target/classes/META-INF/NOTICE</file>
+ </transformer>
+ <transformer
implementation="org.apache.maven.plugins.shade.resource.IncludeResourceTransformer">
+ <resource>META-INF/DEPENDENCIES</resource>
+ <file>target/classes/META-INF/DEPENDENCIES</file>
+ </transformer>
+ </transformers>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+
+ </plugins>
+ </build>
+
+ <scm>
+ <tag>2.2.1-rc2</tag>
+ </scm>
+</project>
diff --git
a/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/main/java/org/apache/tika/pipes/pipesiterator/kafka/KafkaPipesIterator.java
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/main/java/org/apache/tika/pipes/pipesiterator/kafka/KafkaPipesIterator.java
new file mode 100644
index 000000000..5cb917144
--- /dev/null
+++
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/main/java/org/apache/tika/pipes/pipesiterator/kafka/KafkaPipesIterator.java
@@ -0,0 +1,174 @@
+/*
+ * 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.tika.pipes.pipesiterator.kafka;
+
+import java.time.Duration;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.TimeoutException;
+
+import org.apache.kafka.clients.consumer.ConsumerConfig;
+import org.apache.kafka.clients.consumer.ConsumerRecord;
+import org.apache.kafka.clients.consumer.ConsumerRecords;
+import org.apache.kafka.clients.consumer.KafkaConsumer;
+import org.apache.kafka.common.serialization.StringDeserializer;
+import org.apache.tika.config.Field;
+import org.apache.tika.config.Initializable;
+import org.apache.tika.config.InitializableProblemHandler;
+import org.apache.tika.config.Param;
+import org.apache.tika.config.TikaConfig;
+import org.apache.tika.exception.TikaConfigException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.pipes.FetchEmitTuple;
+import org.apache.tika.pipes.HandlerConfig;
+import org.apache.tika.pipes.emitter.EmitKey;
+import org.apache.tika.pipes.fetcher.FetchKey;
+import org.apache.tika.pipes.pipesiterator.PipesIterator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class KafkaPipesIterator extends PipesIterator implements Initializable
{
+
+ private static final Logger LOGGER =
LoggerFactory.getLogger(KafkaPipesIterator.class);
+ String topic;
+ String bootstrapServers;
+ String keySerializer;
+ String valueSerializer;
+ String groupId;
+ String autoOffsetReset = "earliest";
+ int pollDelayMs = 100;
+ int emitMax = -1;
+ int groupInitialRebalanceDelayMs = 3000;
+
+ private Properties props;
+ private KafkaConsumer<String, String> consumer;
+
+
+ @Field
+ public void setTopic(String topic) {
+ this.topic = topic;
+ }
+
+ @Field
+ public void setGroupId(String groupId) {
+ this.groupId = groupId;
+ }
+
+ @Field
+ public void setBootstrapServers(String bootstrapServers) {
+ this.bootstrapServers = bootstrapServers;
+ }
+
+ @Field
+ public void setKeySerializer(String keySerializer) {
+ this.keySerializer = keySerializer;
+ }
+
+ @Field
+ public void setAutoOffsetReset(String autoOffsetReset) {
+ this.autoOffsetReset = autoOffsetReset;
+ }
+
+ @Field
+ public void setValueSerializer(String valueSerializer) {
+ this.valueSerializer = valueSerializer;
+ }
+
+ @Field
+ public void setPollDelayMs(int pollDelayMs) {
+ this.pollDelayMs = pollDelayMs;
+ }
+
+ @Field
+ public void setGroupInitialRebalanceDelayMs(int
groupInitialRebalanceDelayMs) {
+ this.groupInitialRebalanceDelayMs = groupInitialRebalanceDelayMs;
+ }
+
+ /**
+ * If the kafka pipe iterator will keep polling for more documents until
it returns an empty result.
+ * If you set emitMax is set to > 0, it will stop polling if the number of
documents you
+ * have emitted so far > emitMax.
+ */
+ @Field
+ public void setEmitMax(int emitMax) {
+ this.emitMax = emitMax;
+ }
+
+ private void safePut(Properties props, String key, Object val) {
+ if (val != null) {
+ props.put(key, val);
+ }
+ }
+
+ @Override
+ public void initialize(Map<String, Param> params) {
+ props = new Properties();
+ safePut(props, ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,
bootstrapServers);
+ safePut(props, ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG,
serializerClass(keySerializer, StringDeserializer.class));
+ safePut(props, ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG,
serializerClass(valueSerializer, StringDeserializer.class));
+ safePut(props, ConsumerConfig.GROUP_ID_CONFIG, groupId);
+ safePut(props, ConsumerConfig.AUTO_OFFSET_RESET_CONFIG,
autoOffsetReset);
+ safePut(props, "group.inital.rebalance.delay.ms",
groupInitialRebalanceDelayMs);
+ consumer = new KafkaConsumer<>(props);
+ consumer.subscribe(Arrays.asList(topic));
+ }
+
+ private Object serializerClass(String className, Class defaultClass) {
+ try {
+ return className == null ? defaultClass : Class.forName(className);
+ } catch (ClassNotFoundException e) {
+ LOGGER.error("Could not find key serializer class: {}", className);
+ return null;
+ }
+ }
+
+ @Override
+ public void checkInitialization(InitializableProblemHandler problemHandler)
+ throws TikaConfigException {
+ super.checkInitialization(problemHandler);
+ TikaConfig.mustNotBeEmpty("bootstrapServers", this.bootstrapServers);
+ TikaConfig.mustNotBeEmpty("topic", this.topic);
+ }
+
+ @Override
+ protected void enqueue() throws InterruptedException, TimeoutException {
+ String fetcherName = getFetcherName();
+ String emitterName = getEmitterName();
+ long start = System.currentTimeMillis();
+ int count = 0;
+ HandlerConfig handlerConfig = getHandlerConfig();
+ ConsumerRecords<String, String> records;
+
+ do {
+ records = consumer.poll(Duration.ofMillis(pollDelayMs));
+ for (ConsumerRecord<String, String> r : records) {
+ long elapsed = System.currentTimeMillis() - start;
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("adding ({}) {} in {} ms", count, r.key(),
elapsed);
+ }
+ tryToAdd(new FetchEmitTuple(r.key(), new FetchKey(fetcherName,
+ r.key()),
+ new EmitKey(emitterName, r.key()), new Metadata(),
handlerConfig,
+ getOnParseException()));
+ ++count;
+ }
+ } while ((emitMax > 0 || count < emitMax) && !records.isEmpty());
+ long elapsed = System.currentTimeMillis() - start;
+ LOGGER.info("Finished enqueuing {} files in {} ms", count, elapsed);
+ }
+}
diff --git
a/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/java/org/apache/tika/pipes/pipesiterator/kafka/TestKafkaPipesIterator.java
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/java/org/apache/tika/pipes/pipesiterator/kafka/TestKafkaPipesIterator.java
new file mode 100644
index 000000000..152c5101b
--- /dev/null
+++
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/java/org/apache/tika/pipes/pipesiterator/kafka/TestKafkaPipesIterator.java
@@ -0,0 +1,98 @@
+/*
+ * 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.tika.pipes.pipesiterator.kafka;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorCompletionService;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.tika.pipes.FetchEmitTuple;
+import org.apache.tika.pipes.pipesiterator.PipesIterator;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+@Disabled("turn into an actual unit test")
+public class TestKafkaPipesIterator {
+
+ @Test
+ public void testSimple() throws Exception {
+ KafkaPipesIterator it = new KafkaPipesIterator();
+ it.setFetcherName("kafka");
+ it.setGroupId("");//find one
+ it.setBootstrapServers("");//use one
+ it.setTopic("");//select one
+ it.initialize(Collections.EMPTY_MAP);
+ int numConsumers = 6;
+ ArrayBlockingQueue<FetchEmitTuple> queue = new
ArrayBlockingQueue<>(10);
+
+ ExecutorService es = Executors.newFixedThreadPool(numConsumers + 1);
+ ExecutorCompletionService c = new ExecutorCompletionService(es);
+ List<MockFetcher> fetchers = new ArrayList<>();
+ for (int i = 0; i < numConsumers; i++) {
+ MockFetcher fetcher = new MockFetcher(queue);
+ fetchers.add(fetcher);
+ c.submit(fetcher);
+ }
+ for (FetchEmitTuple t : it) {
+ queue.offer(t);
+ }
+ for (int i = 0; i < numConsumers; i++) {
+ queue.offer(PipesIterator.COMPLETED_SEMAPHORE);
+ }
+ int finished = 0;
+ int completed = 0;
+ try {
+ while (finished++ < numConsumers + 1) {
+ Future<Integer> f = c.take();
+ completed += f.get();
+ }
+ } finally {
+ es.shutdownNow();
+ }
+ assertEquals(20000, completed);
+
+ }
+
+ private static class MockFetcher implements Callable<Integer> {
+ private final ArrayBlockingQueue<FetchEmitTuple> queue;
+ private final List<FetchEmitTuple> pairs = new ArrayList<>();
+
+ private MockFetcher(ArrayBlockingQueue<FetchEmitTuple> queue) {
+ this.queue = queue;
+ }
+
+ @Override
+ public Integer call() throws Exception {
+ while (true) {
+ FetchEmitTuple t = queue.poll(1, TimeUnit.HOURS);
+ if (t == PipesIterator.COMPLETED_SEMAPHORE) {
+ return pairs.size();
+ }
+ pairs.add(t);
+ }
+ }
+ }
+}
diff --git
a/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/resources/log4j2.xml
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/resources/log4j2.xml
new file mode 100644
index 000000000..c88e66e99
--- /dev/null
+++
b/tika-pipes/tika-pipes-iterators/tika-pipes-iterator-kafka/src/test/resources/log4j2.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
+
+<!--
+ 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.
+-->
+<Configuration status="WARN">
+ <Appenders>
+ <Console name="Console" target="SYSTEM_ERR">
+ <PatternLayout pattern="%-5p [%t] %d{HH:mm:ss,SSS} %c %m%n"/>
+ </Console>
+ </Appenders>
+ <Loggers>
+ <Root level="info">
+ <AppenderRef ref="Console"/>
+ </Root>
+ </Loggers>
+</Configuration>
\ No newline at end of file