This is an automated email from the ASF dual-hosted git repository.
tballison 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 0569279eab TIKA-4981: PipesForkParser keeps per-call ParseMode,
handler factory and embedded limits; never writes the caller's ParseContext
(#3372)
0569279eab is described below
commit 0569279eabf8caabe3cbfe0aba2969f804eb032f
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 19 20:08:07 2026 -0400
TIKA-4981: PipesForkParser keeps per-call ParseMode, handler factory and
embedded limits; never writes the caller's ParseContext (#3372)
Generated-by: Claude Opus 5.5
---
CHANGES.txt | 4 ++
.../ROOT/pages/using-tika/java-api/pipes.adoc | 4 ++
.../apache/tika/pipes/fork/PipesForkParser.java | 50 ++++++++++++----------
.../tika/pipes/fork/PipesForkParserTest.java | 40 +++++++++++++++++
4 files changed, 75 insertions(+), 23 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index be595e6e8f..efd3ca6dc0 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,9 @@
Release 4.2.0 - unreleased
+ * PipesForkParser: a ParseMode, ContentHandlerFactory or EmbeddedLimits set
in the
+ ParseContext passed to parse() now overrides PipesForkParserConfig
instead of being
+ overwritten by it, and the caller's ParseContext is no longer modified
(TIKA-4981).
+
* Frictionless unpack: the package's metadata.json is renamed rmeta.json,
is listed in
datapackage.json as a resource (size, sha256), and now gets the metadata
filters and
user metadata the emitted list gets; resources are in embedded-id order.
A document
diff --git a/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
b/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
index d0e30f1d6a..a2976e7cf4 100644
--- a/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
+++ b/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
@@ -146,6 +146,10 @@ try (PipesForkParser parser = new PipesForkParser(config))
{
Prefer `parse(Path)` when you already have a file. `result.getContent()` is
the container's text
only; iterate `getMetadataList()` for embedded documents.
+The config's parse mode, handler type and embedded limits are defaults. To
change one for a
+single call, set it in the `ParseContext` you pass to `parse`; Tika works on a
copy and leaves
+your context unchanged.
+
See xref:using-tika/java-api/getting-started.adoc[Getting Started with the
Java API] and
https://github.com/apache/tika/blob/main/tika-example/src/main/java/org/apache/tika/example/PipesForkParserExample.java[PipesForkParserExample.java]
for parse modes, handler types, content-type hints and error handling.
diff --git
a/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParser.java
b/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParser.java
index 26f94cf821..7418c4f9df 100644
---
a/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParser.java
+++
b/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParser.java
@@ -213,7 +213,8 @@ public class PipesForkParser implements Closeable {
*
* @param path the path to the file to parse
* @param metadata initial metadata (e.g., content type hint)
- * @param parseContext the parse context
+ * @param parseContext the parse context; a ContentHandlerFactory,
ParseMode or EmbeddedLimits
+ * set here overrides the config's. It is not modified.
* @return the parse result containing metadata and content
* @throws IOException if an I/O error occurs
* @throws InterruptedException if the parsing is interrupted
@@ -223,7 +224,7 @@ public class PipesForkParser implements Closeable {
*/
public PipesForkResult parse(Path path, Metadata metadata, ParseContext
parseContext)
throws IOException, InterruptedException, PipesException,
TikaException {
- return parseInternal(path, metadata, parseContext);
+ return parseInternal(path, metadata, withDefaults(parseContext));
}
/**
@@ -270,7 +271,8 @@ public class PipesForkParser implements Closeable {
* file, it will be spooled to a temporary file. The caller
must keep
* the TikaInputStream open until this method returns.
* @param metadata initial metadata (e.g., content type hint)
- * @param parseContext the parse context
+ * @param parseContext the parse context; a ContentHandlerFactory,
ParseMode or EmbeddedLimits
+ * set here overrides the config's. It is not modified.
* @return the parse result containing metadata and content
* @throws IOException if an I/O error occurs
* @throws InterruptedException if the parsing is interrupted
@@ -280,24 +282,35 @@ public class PipesForkParser implements Closeable {
*/
public PipesForkResult parse(TikaInputStream tis, Metadata metadata,
ParseContext parseContext)
throws IOException, InterruptedException, PipesException,
TikaException {
+ ParseContext context = withDefaults(parseContext);
// A stream already backed by a file keeps it; otherwise small content
rides inline and
// only large content is written out. Shared with tika-server so both
use one threshold.
try (PayloadRouter.Routed routed = PayloadRouter.route(tis,
config.getPipesConfig().getMaxInlineBytes(),
() -> Files.createTempFile("tika-fork-", ".tmp"))) {
if (routed.isInline()) {
- parseContext.set(InlineBytes.class, routed.inlineBytes());
- try {
- String name =
metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY);
- return parseWithKey(new FetchKey(BytesFetcher.FETCHER_ID,
name == null ? "" : name),
- UUID.randomUUID().toString(), metadata,
parseContext);
- } finally {
- // The payload is request-owned; a caller reusing this
context must not
- // retain (and re-serialize) this document's bytes on
later parses.
- parseContext.set(InlineBytes.class, null);
- }
+ context.set(InlineBytes.class, routed.inlineBytes());
+ String name =
metadata.get(TikaCoreProperties.RESOURCE_NAME_KEY);
+ return parseWithKey(new FetchKey(BytesFetcher.FETCHER_ID, name
== null ? "" : name),
+ UUID.randomUUID().toString(), metadata, context);
}
- return parseInternal(routed.path(), metadata, parseContext);
+ return parseInternal(routed.path(), metadata, context);
+ }
+ }
+
+ /** A copy of the caller's context, never written back; the config fills
only what it leaves unset. */
+ private ParseContext withDefaults(ParseContext parseContext) {
+ ParseContext context = new ParseContext();
+ context.copyFrom(parseContext);
+ setIfAbsent(context, ContentHandlerFactory.class,
config.getContentHandlerFactory());
+ setIfAbsent(context, ParseMode.class, config.getParseMode());
+ setIfAbsent(context, EmbeddedLimits.class, config.getEmbeddedLimits());
+ return context;
+ }
+
+ private static <T> void setIfAbsent(ParseContext context, Class<T> key, T
value) {
+ if (value != null && context.get(key) == null) {
+ context.set(key, value);
}
}
@@ -317,15 +330,6 @@ public class PipesForkParser implements Closeable {
throws IOException, InterruptedException, PipesException,
TikaException {
EmitKey emitKey = new EmitKey("", id); // Empty emitter name since
we're using PASSBACK_ALL
- // Add content handler factory and parse mode to parse context
- parseContext.set(ContentHandlerFactory.class,
config.getContentHandlerFactory());
- parseContext.set(ParseMode.class, config.getParseMode());
-
- // Add embedded limits if configured
- if (config.getEmbeddedLimits() != null) {
- parseContext.set(EmbeddedLimits.class, config.getEmbeddedLimits());
- }
-
FetchEmitTuple tuple = new FetchEmitTuple(id, fetchKey, emitKey,
metadata, parseContext);
PipesResult result = pipesParser.parse(tuple);
diff --git
a/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
index 46ed124171..2a79e9c42d 100644
---
a/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
+++
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/PipesForkParserTest.java
@@ -42,12 +42,14 @@ import org.junit.jupiter.api.io.TempDir;
import org.apache.tika.io.TikaInputStream;
import org.apache.tika.metadata.HttpHeaders;
import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.pipes.api.ParseMode;
import org.apache.tika.pipes.api.PipesResult;
import org.apache.tika.pipes.core.PipesException;
import org.apache.tika.pipes.core.fetcher.InlineBytes;
import org.apache.tika.sax.BasicContentHandlerFactory;
+import org.apache.tika.sax.ContentHandlerFactory;
public class PipesForkParserTest {
@@ -313,6 +315,44 @@ public class PipesForkParserTest {
}
}
+ // TIKA-4981: per-call values beat the config's defaults, and the caller's
context is not written
+ @Test
+ public void testPerCallParseModeOverridesConfig() throws Exception {
+ Path testZip = createZipWithEmbeddedFiles("per_call_mode.zip",
+ "embedded1.txt", "Content from first embedded file",
+ "embedded2.txt", "Content from second embedded file");
+ ParseContext parseContext = new ParseContext();
+ parseContext.set(ParseMode.class, ParseMode.CONCATENATE);
+
+ PipesForkResult result = defaultParser.parse(testZip, new Metadata(),
parseContext);
+
+ assertTrue(result.isSuccess(), "Parse should succeed. Status: " +
result.getStatus());
+ assertEquals(1, result.getMetadataList().size(), "per-call CONCATENATE
was ignored");
+ assertTrue(result.getContent().contains("second embedded"),
result.getContent());
+ assertNull(parseContext.get(ContentHandlerFactory.class),
+ "the config's handler factory was written into the caller's
context");
+ }
+
+ @Test
+ public void testPerCallHandlerFactoryOverridesConfig() throws Exception {
+ ParseContext parseContext = new ParseContext();
+ parseContext.set(ContentHandlerFactory.class,
+ new
BasicContentHandlerFactory(BasicContentHandlerFactory.HANDLER_TYPE.XML, -1));
+ byte[] html = "<html><body><p>per
call</p></body></html>".getBytes(StandardCharsets.UTF_8);
+ Metadata metadata = new Metadata();
+ metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, "per_call.html");
+
+ try (TikaInputStream tis = TikaInputStream.get(new
ByteArrayInputStream(html))) {
+ PipesForkResult result = defaultParser.parse(tis, metadata,
parseContext);
+
+ assertTrue(result.isSuccess(), "Parse should succeed. Status: " +
result.getStatus());
+ assertTrue(result.getContent().contains("<p>per call</p>"),
+ "per-call XML handler was ignored; content: " +
result.getContent());
+ }
+ assertNull(parseContext.get(ParseMode.class),
+ "the config's parse mode was written into the caller's
context");
+ }
+
@Test
public void testParseWithMetadata() throws Exception {
Path testFile = tempDir.resolve("test.html");