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 3b6fd2436b TIKA-4864: resolve the default plugins dir against the 
install layout and make it absolute (#3107)
3b6fd2436b is described below

commit 3b6fd2436b70cb9216bfcbd07694d6c492ec8ce6
Author: Dominik Schmidt <[email protected]>
AuthorDate: Mon Aug 31 20:22:15 2026 +0200

    TIKA-4864: resolve the default plugins dir against the install layout and 
make it absolute (#3107)
---
 CHANGES.txt                                        |  5 ++
 .../org/apache/tika/async/cli/PluginsWriter.java   |  3 +-
 .../org/apache/tika/async/cli/TikaAsyncCLI.java    | 35 +-------
 .../tika/pipes/core/config/DefaultPluginsDir.java  | 93 ++++++++++++++++++++++
 .../pipes/core/config/DefaultPluginsDirTest.java   | 59 ++++++++++++++
 .../apache/tika/pipes/fork/PipesForkParser.java    | 29 +------
 tika-server/docker-build/full/Dockerfile           |  3 -
 tika-server/docker-build/minimal/Dockerfile        |  3 -
 .../apache/tika/server/core/TikaServerProcess.java | 31 +-------
 9 files changed, 165 insertions(+), 96 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index 3880a79543..3cf56dbe65 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,10 @@
 Release 4.1.0 - unreleased
 
+   * The default plugins directory is resolved against the install layout
+     (next to the jar, or next to its lib/ directory) and always as an
+     absolute path, shared by tika-server, PipesForkParser and the async
+     CLI; it no longer depends on the working directory (TIKA-4864).
+
    * tika-server error bodies (the 422/500 exception mapper, /meta/{field})
      now honor the exception-reporting policy; /meta/{field} returns the
      already-formatted container exception instead of re-wrapping it with
diff --git 
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/PluginsWriter.java
 
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/PluginsWriter.java
index 23d0547ca0..8b4785d41e 100644
--- 
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/PluginsWriter.java
+++ 
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/PluginsWriter.java
@@ -28,6 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ObjectNode;
 
 import org.apache.tika.config.loader.TikaObjectMapperFactory;
+import org.apache.tika.pipes.core.config.DefaultPluginsDir;
 import org.apache.tika.sax.BasicContentHandlerFactory;
 import org.apache.tika.utils.StringUtils;
 
@@ -73,7 +74,7 @@ public class PluginsWriter {
                     pluginString = plugins.toAbsolutePath().toString();
                 }
             } else {
-                pluginString = TikaAsyncCLI.resolveDefaultPluginsDir();
+                pluginString = DefaultPluginsDir.resolve(PluginsWriter.class);
             }
             root.put("plugin-roots", pluginString);
 
diff --git 
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
 
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
index 0e9f00c98e..eaccde098d 100644
--- 
a/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
+++ 
b/tika-pipes/tika-async-cli/src/main/java/org/apache/tika/async/cli/TikaAsyncCLI.java
@@ -47,6 +47,7 @@ import org.apache.tika.pipes.api.emitter.EmitKey;
 import org.apache.tika.pipes.api.fetcher.FetchKey;
 import org.apache.tika.pipes.api.pipesiterator.PipesIterator;
 import org.apache.tika.pipes.core.async.AsyncProcessor;
+import org.apache.tika.pipes.core.config.DefaultPluginsDir;
 import org.apache.tika.pipes.core.extractor.UnpackConfig;
 import org.apache.tika.pipes.core.pipesiterator.PipesIteratorManager;
 import org.apache.tika.plugins.ExtensionConfig;
@@ -403,38 +404,6 @@ public class TikaAsyncCLI {
         parseContext.set(UnpackConfig.class, config);
     }
 
-    private static final String DEFAULT_PLUGINS_DIR = "plugins";
-
-    /**
-     * Resolves the default plugins directory. Looks for a "plugins" directory
-     * next to the running jar first, then falls back to the current working 
directory.
-     *
-     * @return the resolved plugins directory path, or "plugins" if neither 
location exists
-     */
-    static String resolveDefaultPluginsDir() {
-        try {
-            Path jarPath = Paths.get(
-                    
TikaAsyncCLI.class.getProtectionDomain().getCodeSource().getLocation().toURI());
-            Path jarDir = jarPath.getParent();
-            if (jarDir != null) {
-                // The jar is typically in lib/, so look for plugins/ as a 
sibling of lib/
-                Path parent = jarDir.getParent();
-                if (parent != null) {
-                    Path pluginsDir = parent.resolve(DEFAULT_PLUGINS_DIR);
-                    if (Files.isDirectory(pluginsDir)) {
-                        return pluginsDir.toAbsolutePath().toString();
-                    }
-                }
-            }
-        } catch (Exception e) {
-            // Fall through to cwd-relative
-        }
-        Path cwdPlugins = Paths.get(DEFAULT_PLUGINS_DIR);
-        if (Files.isDirectory(cwdPlugins)) {
-            return cwdPlugins.toAbsolutePath().toString();
-        }
-        return DEFAULT_PLUGINS_DIR;
-    }
 
     /**
      * Ensures plugin-roots is set in the config. If missing, creates a merged 
config
@@ -462,7 +431,7 @@ public class TikaAsyncCLI {
             pluginString = Files.isDirectory(plugins) ?
                     plugins.toAbsolutePath().toString() : pluginsDir;
         } else {
-            pluginString = resolveDefaultPluginsDir();
+            pluginString = DefaultPluginsDir.resolve(TikaAsyncCLI.class);
         }
         mutableRoot.put("plugin-roots", pluginString);
 
diff --git 
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/DefaultPluginsDir.java
 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/DefaultPluginsDir.java
new file mode 100644
index 0000000000..913fe49e69
--- /dev/null
+++ 
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/config/DefaultPluginsDir.java
@@ -0,0 +1,93 @@
+/*
+ * 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.core.config;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * Resolves the default {@code plugins} directory when {@code plugin-roots}
+ * is not configured (TIKA-4864). The probe order matches the install
+ * layouts Tika ships:
+ * <ol>
+ *   <li>next to the jar the anchor class was loaded from (a flat install,
+ *       tika-grpc's docker image);</li>
+ *   <li>next to that directory's parent (the unpacked distributions and the
+ *       tika-server docker image load classes from {@code lib/}, the plugins
+ *       sit beside {@code lib/});</li>
+ *   <li>a {@code plugins} directory in the current working directory.</li>
+ * </ol>
+ * The result is always absolute: the forked pipes server resolves the
+ * configured value against its own working directory, which need not be the
+ * parent's, so a relative default would make the two processes disagree.
+ */
+public final class DefaultPluginsDir {
+
+    /**
+     * The directory name probed in each location.
+     */
+    public static final String PLUGINS_DIR_NAME = "plugins";
+
+    private DefaultPluginsDir() {
+    }
+
+    /**
+     * Resolves the default plugins directory for the install layout of the
+     * given class.
+     *
+     * @param anchor the class whose code source anchors the probe, usually
+     *               the caller
+     * @return the absolute path of the first {@code plugins} directory found,
+     * or the absolute path of {@code plugins} in the working directory if
+     * none exists yet
+     */
+    public static String resolve(Class<?> anchor) {
+        Path codeSourceDir = null;
+        try {
+            codeSourceDir = 
Path.of(anchor.getProtectionDomain().getCodeSource().getLocation()
+                    .toURI()).getParent();
+        } catch (Exception e) {
+            //no code source (e.g. a repacked classloader): probe the working
+            //directory only
+        }
+        return resolve(codeSourceDir, Path.of("")).toString();
+    }
+
+    /**
+     * The probe itself, separated from the code-source lookup for testing.
+     *
+     * @param codeSourceDir the directory holding the anchor's jar, or null
+     * @param cwd           the working directory to fall back to
+     * @return the absolute path of the resolved directory
+     */
+    public static Path resolve(Path codeSourceDir, Path cwd) {
+        if (codeSourceDir != null) {
+            Path nextToJar = codeSourceDir.resolve(PLUGINS_DIR_NAME);
+            if (Files.isDirectory(nextToJar)) {
+                return nextToJar.toAbsolutePath();
+            }
+            Path parent = codeSourceDir.getParent();
+            if (parent != null) {
+                Path nextToParent = parent.resolve(PLUGINS_DIR_NAME);
+                if (Files.isDirectory(nextToParent)) {
+                    return nextToParent.toAbsolutePath();
+                }
+            }
+        }
+        return cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
+    }
+}
diff --git 
a/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/DefaultPluginsDirTest.java
 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/DefaultPluginsDirTest.java
new file mode 100644
index 0000000000..fbe1beee39
--- /dev/null
+++ 
b/tika-pipes/tika-pipes-core/src/test/java/org/apache/tika/pipes/core/config/DefaultPluginsDirTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.core.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+public class DefaultPluginsDirTest {
+
+    @Test
+    public void pluginsDirNextToTheCodeSourceJar(@TempDir Path install) throws 
Exception {
+        Path plugins = Files.createDirectories(install.resolve("plugins"));
+        assertEquals(plugins.toAbsolutePath(),
+                DefaultPluginsDir.resolve(install, Path.of("")));
+    }
+
+    @Test
+    public void pluginsDirBesideTheLibDirectory(@TempDir Path install) throws 
Exception {
+        //the resolving class lives in lib/, the plugins next to it
+        Path lib = Files.createDirectories(install.resolve("lib"));
+        Path plugins = Files.createDirectories(install.resolve("plugins"));
+        assertEquals(plugins.toAbsolutePath(),
+                DefaultPluginsDir.resolve(lib, Path.of("")));
+    }
+
+    @Test
+    public void pluginsDirFromTheWorkingDirectory(@TempDir Path install, 
@TempDir Path cwd)
+            throws Exception {
+        Path plugins = Files.createDirectories(cwd.resolve("plugins"));
+        assertEquals(plugins.toAbsolutePath(),
+                DefaultPluginsDir.resolve(install.resolve("lib"), cwd));
+    }
+
+    @Test
+    public void missingPluginsDirStaysAbsolute(@TempDir Path cwd) {
+        //the forked pipes server must not re-resolve the path against its own 
cwd
+        assertTrue(DefaultPluginsDir.resolve(null, cwd).isAbsolute());
+    }
+}
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 a2e362c747..cabf5bd43f 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
@@ -41,6 +41,7 @@ import org.apache.tika.pipes.core.PipesException;
 import org.apache.tika.pipes.core.PipesParser;
 import org.apache.tika.pipes.core.config.ConfigMerger;
 import org.apache.tika.pipes.core.config.ConfigOverrides;
+import org.apache.tika.pipes.core.config.DefaultPluginsDir;
 import org.apache.tika.pipes.core.fetcher.BytesFetcher;
 import org.apache.tika.pipes.core.fetcher.InlineBytes;
 import org.apache.tika.pipes.core.fetcher.PayloadRouter;
@@ -426,7 +427,7 @@ public class PipesForkParser implements Closeable {
         if (config.getPluginsDir() != null) {
             
builder.setPluginRoots(config.getPluginsDir().toAbsolutePath().toString());
         } else {
-            builder.setPluginRoots(resolveDefaultPluginsDir());
+            
builder.setPluginRoots(DefaultPluginsDir.resolve(PipesForkParser.class));
         }
 
         ConfigOverrides overrides = builder.build();
@@ -435,31 +436,5 @@ public class PipesForkParser implements Closeable {
         return ConfigMerger.mergeOrCreate(config.getUserConfigPath(), 
overrides);
     }
 
-    private static final String DEFAULT_PLUGINS_DIR = "plugins";
-
-    /**
-     * Mirrors tika-server's resolution: a "plugins" directory beside the 
running jar, else one in
-     * the working directory, else the bare name so pf4j reports the missing 
root itself.
-     */
-    private static String resolveDefaultPluginsDir() {
-        try {
-            Path jarPath = Path.of(PipesForkParser.class.getProtectionDomain()
-                    .getCodeSource().getLocation().toURI());
-            Path jarDir = jarPath.getParent();
-            if (jarDir != null) {
-                Path pluginsNextToJar = jarDir.resolve(DEFAULT_PLUGINS_DIR);
-                if (Files.isDirectory(pluginsNextToJar)) {
-                    return pluginsNextToJar.toAbsolutePath().toString();
-                }
-            }
-        } catch (Exception e) {
-            // fall through to the working directory
-        }
-        Path cwdPlugins = Path.of(DEFAULT_PLUGINS_DIR);
-        if (Files.isDirectory(cwdPlugins)) {
-            return cwdPlugins.toAbsolutePath().toString();
-        }
-        return DEFAULT_PLUGINS_DIR;
-    }
 
 }
diff --git a/tika-server/docker-build/full/Dockerfile 
b/tika-server/docker-build/full/Dockerfile
index 15f9f276ed..5582de7597 100644
--- a/tika-server/docker-build/full/Dockerfile
+++ b/tika-server/docker-build/full/Dockerfile
@@ -77,9 +77,6 @@ ENV TIKA_VERSION=$TIKA_VERSION
 ENV OMP_THREAD_LIMIT=1
 
 COPY --from=fetch_tika /opt/tika-server /opt/tika-server
-# WORKDIR sets the CWD so tika-server's plugin-root fallback resolves
-# `plugins/` relative to /opt/tika-server (its `getCodeSource()` returns a
-# lib/* path, not the top-level jar, so the "next-to-jar" resolution misses).
 WORKDIR /opt/tika-server
 USER $UID_GID
 
diff --git a/tika-server/docker-build/minimal/Dockerfile 
b/tika-server/docker-build/minimal/Dockerfile
index 9b6bf14a4c..115554e587 100644
--- a/tika-server/docker-build/minimal/Dockerfile
+++ b/tika-server/docker-build/minimal/Dockerfile
@@ -68,9 +68,6 @@ RUN set -eux \
 ARG TIKA_VERSION
 ENV TIKA_VERSION=$TIKA_VERSION
 COPY --from=fetch_tika /opt/tika-server /opt/tika-server
-# WORKDIR sets the CWD so tika-server's plugin-root fallback resolves
-# `plugins/` relative to /opt/tika-server (its `getCodeSource()` returns a
-# lib/* path, not the top-level jar, so the "next-to-jar" resolution misses).
 WORKDIR /opt/tika-server
 USER $UID_GID
 EXPOSE 9998
diff --git 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
index 0de1e88ceb..264ce728bc 100644
--- 
a/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
+++ 
b/tika-server/tika-server-core/src/main/java/org/apache/tika/server/core/TikaServerProcess.java
@@ -70,6 +70,7 @@ import org.apache.tika.pipes.core.PipesConfig;
 import org.apache.tika.pipes.core.PipesParser;
 import org.apache.tika.pipes.core.config.ConfigMerger;
 import org.apache.tika.pipes.core.config.ConfigOverrides;
+import org.apache.tika.pipes.core.config.DefaultPluginsDir;
 import org.apache.tika.server.core.metrics.MetricsServer;
 import org.apache.tika.server.core.metrics.TikaMetricsFilter;
 import org.apache.tika.server.core.metrics.TikaServerMetrics;
@@ -723,34 +724,6 @@ public class TikaServerProcess {
         return helper;
     }
 
-    private static final String DEFAULT_PLUGINS_DIR = "plugins";
-
-    /**
-     * Resolves the default plugins directory. Looks for a "plugins" directory
-     * next to the running jar first, then falls back to the current working 
directory.
-     */
-    private static String resolveDefaultPluginsDir() {
-        try {
-            Path jarPath = Path.of(
-                    TikaServerProcess.class.getProtectionDomain()
-                            .getCodeSource().getLocation().toURI());
-            Path jarDir = jarPath.getParent();
-            if (jarDir != null) {
-                Path pluginsNextToJar = jarDir.resolve(DEFAULT_PLUGINS_DIR);
-                if (Files.isDirectory(pluginsNextToJar)) {
-                    return pluginsNextToJar.toAbsolutePath().toString();
-                }
-            }
-        } catch (Exception e) {
-            // Fall through to cwd-relative
-        }
-        Path cwdPlugins = Path.of(DEFAULT_PLUGINS_DIR);
-        if (Files.isDirectory(cwdPlugins)) {
-            return cwdPlugins.toAbsolutePath().toString();
-        }
-        return DEFAULT_PLUGINS_DIR;
-    }
-
     /**
      * Creates or merges server configuration using ConfigMerger.
      * <p>
@@ -783,7 +756,7 @@ public class TikaServerProcess {
                 // Use PASSBACK_ALL strategy - results returned through socket
                 .setEmitStrategy(EmitStrategy.PASSBACK_ALL)
                 // Set plugin roots
-                .setPluginRoots(resolveDefaultPluginsDir());
+                
.setPluginRoots(DefaultPluginsDir.resolve(TikaServerProcess.class));
 
         // Only set default pipes config if there's no existing config
         // This allows user-provided config to specify their own numClients, 
etc.

Reply via email to