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");

Reply via email to