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.