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 ebc845c390 TIKA-4931: fix overrides in PipesForkParser (#3257)
ebc845c390 is described below

commit ebc845c390ab375722e3dd7e9b50b92cf88b99aa
Author: Tim Allison <[email protected]>
AuthorDate: Fri Sep 25 12:38:21 2026 -0400

    TIKA-4931: fix overrides in PipesForkParser (#3257)
---
 CHANGES.txt                                        |  4 ++
 .../tika/pipes/core/config/ConfigMerger.java       |  7 +++
 .../tika/pipes/core/config/ConfigOverrides.java    | 44 ++++++++++++++++-
 .../tika/pipes/core/config/ConfigMergerTest.java   | 35 +++++++++++++
 .../apache/tika/pipes/fork/PipesForkParser.java    |  7 ++-
 .../tika/pipes/fork/PipesForkParserConfig.java     |  6 +++
 .../tika/pipes/fork/PipesForkParserTest.java       | 57 ++++++++++++++++++++++
 7 files changed, 157 insertions(+), 3 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 3348b3049e..8625c7f0ef 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -89,6 +89,10 @@ Release 4.1.0 - 9/25/2026
      crashes; tika-eval Profile/Compare read that ledger and the run-info
      json to classify NO_EXTRACT_FILE by cause (TIKA-4846, TIKA-4847).
 
+   * PipesForkParser no longer drops javaPath and socketTimeoutMillis set in
+     code; the fork ran java from the PATH with a 60s socket timeout
+     regardless (TIKA-4931).
+
   EXTRACTION AND FORMATS
 
    * Improve extraction of tagged PDFs. New tika-eval-structure tool compares
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
index 57d92625b5..f10ddd7a28 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigMerger.java
@@ -168,6 +168,13 @@ public class ConfigMerger {
                 pipesNode.put("maxFilesProcessedPerProcess", 
pc.getMaxFilesProcessedPerProcess());
             }
 
+            if (pc.getSocketTimeoutMillis() > 0) {
+                pipesNode.put("socketTimeoutMillis", 
pc.getSocketTimeoutMillis());
+            }
+            if (pc.getJavaPath() != null) {
+                pipesNode.put("javaPath", pc.getJavaPath());
+            }
+
             // Apply forked JVM args
             List<String> jvmArgs = pc.getForkedJvmArgs();
             if (jvmArgs != null && !jvmArgs.isEmpty()) {
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
index 2c9e100ad5..779217b29a 100644
--- 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/ConfigOverrides.java
@@ -150,14 +150,30 @@ public class ConfigOverrides {
         private final int numClients;
         private final int maxFilesProcessedPerProcess;
         private final List<String> forkedJvmArgs;
+        private final long socketTimeoutMillis;
+        private final String javaPath;
 
         public PipesConfigOverride(int numClients,
                                    int maxFilesProcessedPerProcess,
                                    List<String> forkedJvmArgs) {
+            this(numClients, maxFilesProcessedPerProcess, forkedJvmArgs, -1, 
null);
+        }
+
+        /**
+         * @param socketTimeoutMillis written when positive; -1 leaves the 
existing config's value
+         * @param javaPath written when non-null; null leaves the existing 
config's value
+         */
+        public PipesConfigOverride(int numClients,
+                                   int maxFilesProcessedPerProcess,
+                                   List<String> forkedJvmArgs,
+                                   long socketTimeoutMillis,
+                                   String javaPath) {
             this.numClients = numClients;
             this.maxFilesProcessedPerProcess = maxFilesProcessedPerProcess;
             this.forkedJvmArgs = forkedJvmArgs != null ?
                     new ArrayList<>(forkedJvmArgs) : new ArrayList<>();
+            this.socketTimeoutMillis = socketTimeoutMillis;
+            this.javaPath = javaPath;
         }
 
         public int getNumClients() {
@@ -171,6 +187,14 @@ public class ConfigOverrides {
         public List<String> getForkedJvmArgs() {
             return forkedJvmArgs;
         }
+
+        public long getSocketTimeoutMillis() {
+            return socketTimeoutMillis;
+        }
+
+        public String getJavaPath() {
+            return javaPath;
+        }
     }
 
     /**
@@ -238,8 +262,26 @@ public class ConfigOverrides {
         public Builder setPipesConfig(int numClients,
                                       int maxFilesProcessedPerProcess,
                                       List<String> forkedJvmArgs) {
+            return setPipesConfig(numClients, maxFilesProcessedPerProcess, 
forkedJvmArgs,
+                    -1, null);
+        }
+
+        /**
+         * Set pipes configuration including the parent-side process settings.
+         *
+         * @param socketTimeoutMillis socket read timeout for the forked 
process, -1 to leave
+         *                            the existing config's value
+         * @param javaPath java executable for the forked process, null to 
leave the existing
+         *                 config's value
+         * @return this builder
+         */
+        public Builder setPipesConfig(int numClients,
+                                      int maxFilesProcessedPerProcess,
+                                      List<String> forkedJvmArgs,
+                                      long socketTimeoutMillis,
+                                      String javaPath) {
             this.pipesConfig = new PipesConfigOverride(numClients,
-                    maxFilesProcessedPerProcess, forkedJvmArgs);
+                    maxFilesProcessedPerProcess, forkedJvmArgs, 
socketTimeoutMillis, javaPath);
             return this;
         }
 
diff --git 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
index 69101bfa19..c7f551ed17 100644
--- 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
+++ 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/ConfigMergerTest.java
@@ -387,4 +387,39 @@ public class ConfigMergerTest {
 
         Files.deleteIfExists(result.configPath());
     }
+
+    /** TIKA-4931: socketTimeoutMillis and javaPath set in code reach the 
merged file. */
+    @Test
+    public void testSocketTimeoutAndJavaPathWritten() throws IOException {
+        ConfigOverrides overrides = ConfigOverrides.builder()
+                .setPipesConfig(1, 100, List.of("-Xmx512m"), 1234, 
"/opt/jdk/bin/java")
+                .build();
+
+        ConfigMerger.MergeResult result = ConfigMerger.mergeOrCreate(null, 
overrides);
+
+        JsonNode pipes = new 
ObjectMapper().readTree(result.configPath().toFile()).get("pipes");
+        assertEquals(1234, pipes.get("socketTimeoutMillis").asLong());
+        assertEquals("/opt/jdk/bin/java", pipes.get("javaPath").asText());
+
+        Files.deleteIfExists(result.configPath());
+    }
+
+    /** Unset (-1 / null) leaves the user config file's values alone. */
+    @Test
+    public void testSocketTimeoutAndJavaPathUnsetKeepUserValues() throws 
IOException {
+        Path userConfig = tempDir.resolve("user-config.json");
+        Files.writeString(userConfig,
+                
"{\"pipes\":{\"socketTimeoutMillis\":4321,\"javaPath\":\"/file/java\"}}");
+        ConfigOverrides overrides = ConfigOverrides.builder()
+                .setPipesConfig(1, 100, null, -1, null)
+                .build();
+
+        ConfigMerger.MergeResult result = 
ConfigMerger.mergeOrCreate(userConfig, overrides);
+
+        JsonNode pipes = new 
ObjectMapper().readTree(result.configPath().toFile()).get("pipes");
+        assertEquals(4321, pipes.get("socketTimeoutMillis").asLong());
+        assertEquals("/file/java", pipes.get("javaPath").asText());
+
+        Files.deleteIfExists(result.configPath());
+    }
 }
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 cabf5bd43f..f9a6c35da4 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
@@ -409,11 +409,14 @@ public class PipesForkParser implements Closeable {
                 // Use null ID to trigger UUID generation
                 .addFetcher(null, "file-system-fetcher",
                         Map.of("allowAbsolutePaths", true))
-                // Set pipes configuration
+                // socketTimeoutMillis/javaPath start unset in 
PipesForkParserConfig, so a
+                // value here was set in code and overrides the user config 
(TIKA-4931)
                 .setPipesConfig(
                         pc.getNumClients(),
                         pc.getMaxFilesProcessedPerProcess(),
-                        pc.getForkedJvmArgs())
+                        pc.getForkedJvmArgs(),
+                        pc.getSocketTimeoutMillis(),
+                        pc.getJavaPath())
                 // Use PASSBACK_ALL strategy - results returned through socket
                 .setEmitStrategy(EmitStrategy.PASSBACK_ALL);
 
diff --git 
a/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParserConfig.java
 
b/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParserConfig.java
index 8c498d2ad1..4e0ae1f4ce 100644
--- 
a/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParserConfig.java
+++ 
b/tika-pipes/tika-pipes-fork-parser/src/main/java/org/apache/tika/pipes/fork/PipesForkParserConfig.java
@@ -50,6 +50,10 @@ public class PipesForkParserConfig {
                 BasicContentHandlerFactory.HANDLER_TYPE.TEXT, -1);
         // Default to single client for simple fork parser use case
         this.pipesConfig.setNumClients(1);
+        // Unset, not defaulted: anything still null/-1 at merge time is left 
to the user
+        // config file (else PipesConfig's default); anything else was set in 
code and wins.
+        this.pipesConfig.setJavaPath(null);
+        this.pipesConfig.setSocketTimeoutMillis(-1);
     }
 
     /**
@@ -228,6 +232,8 @@ public class PipesForkParserConfig {
     /**
      * Set the Java executable path.
      *
+     * Unset by default: the user config file's {@code javaPath}, else {@code 
java}, applies.
+     *
      * @param javaPath path to the java executable
      * @return this config for chaining
      */
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 832255c917..731cf9a1b8 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
@@ -107,6 +107,63 @@ public class PipesForkParserTest {
         }
     }
 
+    /** TIKA-4931: javaPath set in code must reach the process that starts the 
fork. */
+    @Test
+    public void testJavaPathReachesFork() throws Exception {
+        Path testFile = tempDir.resolve("test.txt");
+        Files.writeString(testFile, "hello");
+        PipesForkParserConfig config = new PipesForkParserConfig()
+                .setPluginsDir(PLUGINS_DIR)
+                .setJavaPath(tempDir.resolve("no-such-java").toString());
+
+        try (PipesForkParser parser = new PipesForkParser(config);
+             TikaInputStream tis = TikaInputStream.get(testFile)) {
+            PipesForkParserException e =
+                    assertThrows(PipesForkParserException.class, () -> 
parser.parse(tis));
+            assertTrue(e.getMessage().contains("no-such-java"), 
e.getMessage());
+        }
+    }
+
+    /** TIKA-4931: a code setting equal to the default still overrides the 
user config file. */
+    @Test
+    public void testExplicitDefaultJavaPathBeatsUserConfig() throws Exception {
+        Path userConfig = tempDir.resolve("user-config.json");
+        Files.writeString(userConfig, "{\"pipes\":{\"javaPath\":\""
+                + tempDir.resolve("no-such-java").toString().replace("\\", 
"\\\\") + "\"}}");
+        Path testFile = tempDir.resolve("test.txt");
+        Files.writeString(testFile, "hello");
+        PipesForkParserConfig config = new PipesForkParserConfig()
+                .setPluginsDir(PLUGINS_DIR)
+                .setUserConfigPath(userConfig)
+                .setJavaPath("java");
+
+        try (PipesForkParser parser = new PipesForkParser(config);
+             TikaInputStream tis = TikaInputStream.get(testFile)) {
+            PipesForkResult result = parser.parse(tis);
+            assertTrue(result.isSuccess(), "status: " + result.getStatus());
+        }
+    }
+
+    /** TIKA-4931: javaPath never set in code leaves the user config file's 
value in force. */
+    @Test
+    public void testUnsetJavaPathLeavesUserConfig() throws Exception {
+        Path userConfig = tempDir.resolve("user-config.json");
+        Files.writeString(userConfig, "{\"pipes\":{\"javaPath\":\""
+                + tempDir.resolve("no-such-java").toString().replace("\\", 
"\\\\") + "\"}}");
+        Path testFile = tempDir.resolve("test.txt");
+        Files.writeString(testFile, "hello");
+        PipesForkParserConfig config = new PipesForkParserConfig()
+                .setPluginsDir(PLUGINS_DIR)
+                .setUserConfigPath(userConfig);
+
+        try (PipesForkParser parser = new PipesForkParser(config);
+             TikaInputStream tis = TikaInputStream.get(testFile)) {
+            PipesForkParserException e =
+                    assertThrows(PipesForkParserException.class, () -> 
parser.parse(tis));
+            assertTrue(e.getMessage().contains("no-such-java"), 
e.getMessage());
+        }
+    }
+
     /**
      * The inline payload is request-owned: a caller reusing the ParseContext 
must not have this
      * document's bytes retained and re-serialized into later requests.

Reply via email to