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 a3e5d09fb2 TIKA-4918 - clean up pf4j loading (#3226)
a3e5d09fb2 is described below
commit a3e5d09fb2927d779448e5d58aa9983693bdd9fb
Author: Tim Allison <[email protected]>
AuthorDate: Tue Oct 6 12:37:43 2026 -0400
TIKA-4918 - clean up pf4j loading (#3226)
---
CHANGES.txt | 9 +++
.../ROOT/pages/pipes/plugins/writing-a-plugin.adoc | 6 ++
.../ROOT/pages/using-tika/java-api/pipes.adoc | 49 ++++++++-------
tika-e2e-tests/tika-grpc/pom.xml | 33 ++++++++++
.../org/apache/tika/pipes/ExternalTestBase.java | 16 ++++-
.../tika/pipes/filesystem/HandlerTypeTest.java | 2 +-
.../tika/pipes/ignite/IgniteConfigStoreTest.java | 2 +-
tika-grpc/pom.xml | 5 --
...PipesBiDirectionalStreamingIntegrationTest.java | 2 +
.../tika/pipes/core/AbstractComponentManager.java | 23 +++----
.../tika/pipes/core/config/ConfigMerger.java | 15 +++++
.../tika/pipes/core/config/ConfigOverrides.java | 17 ++++++
.../tika/pipes/core/config/DefaultPluginsDir.java | 43 +++++++++----
.../tika/pipes/core/config/ConfigMergerTest.java | 20 +++++++
.../pipes/core/config/DefaultPluginsDirTest.java | 9 +++
tika-pipes/tika-pipes-fork-parser/pom.xml | 2 +
.../apache/tika/pipes/fork/PipesForkParser.java | 35 ++++++++---
.../tika/pipes/fork/ClasspathPluginsTest.java | 70 ++++++++++++++++++++++
.../tika/pipes/fork/PipesForkParserTest.java | 30 ++++++++++
.../test/resources/configs/tika-config-http.json | 2 +-
.../apache/tika/plugins/PluginComponentLoader.java | 23 +++----
.../org/apache/tika/plugins/TikaPluginManager.java | 67 +++++++++++++++++----
.../apache/tika/plugins/TestExtensionFactory.java | 30 ++++++++++
.../java/org/apache/tika/plugins/TestPlugin.java | 23 +++++++
.../apache/tika/plugins/TikaPluginManagerTest.java | 22 +++++++
25 files changed, 463 insertions(+), 92 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index 8cf92ee9a8..b08bbc4361 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,14 @@
Release 4.2.0 - unreleased
+ * Pipes plugins load only from the zips in plugin-roots; a plugin jar on
the classpath
+ needs -Dtika.plugins.classpath=true, which PipesForkParser sets on its
fork when it
+ has no plugins directory. Dev mode loads an exploded target/classes
(TIKA-4918).
+
+ * GeoGebraParser reads structure.json and inline text content with
jackson-core's
+ streaming parser; tika-parser-miscoffice-module and
tika-parser-cad-module no
+ longer depend on jackson-databind, and OSGi deployments of
tika-bundle-standard
+ need only jackson-core (TIKA-4956).
+
* Forked pipes servers get their config and auth token over stdin; nothing
is written
to the temp dir. Server-manager and PipesServer signatures change
accordingly (TIKA-4952).
diff --git a/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
b/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
index aeca158585..ce36b0c7ec 100644
--- a/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
+++ b/docs/modules/ROOT/pages/pipes/plugins/writing-a-plugin.adoc
@@ -316,6 +316,12 @@ all fill it in when you do not: a `plugins` directory
beside the running jar, el
working directory. Loading through `TikaPluginManager` directly with no
`plugin-roots` fails with
`plugin-roots must be specified`.
+Zips in `plugin-roots` are the only source of plugins. A plugin jar on the
application classpath
+is not scanned, even though it carries the same `META-INF/extensions.idx`,
unless the JVM runs
+with `-Dtika.plugins.classpath=true`. That switch exists so `PipesForkParser`
can embed without a
+plugins directory; it drops the classloader isolation for every plugin found
that way, and a
+process started with it owns whatever its classpath does.
+
=== tika-config.json
The JSON never names a class. It names your factory's `getName()`.
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 b7516d78ce..d0e30f1d6a 100644
--- a/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
+++ b/docs/modules/ROOT/pages/using-tika/java-api/pipes.adoc
@@ -50,31 +50,36 @@ API; use the classes above.
== Where plugins come from
-Fetchers, emitters, iterators and reporters are
xref:pipes/plugins/index.adoc[PF4J plugins]. An
-embedding application can supply them two ways:
-
-* **As Maven dependencies.** A plugin jar on the application's classpath is
discovered through
- its `META-INF/extensions.idx`, and the forked JVM inherits the parent's
classpath.
- `tika-pipes-fork-parser` already depends on `tika-pipes-file-system`, so
`PipesForkParser`
- works with no further setup. For another source or destination, add that
plugin's jar
- (`tika-pipes-s3`, `tika-pipes-opensearch`, ...) as an ordinary dependency.
-* **As zips in a plugins directory.** This is the distribution layout:
`tika-app` and
- `tika-server` ship a `plugins/` directory of plugin zips, and `plugin-roots`
points at it.
- Get the zips from the distribution zips on the
https://tika.apache.org/download.html[download
- page]; they are not on Maven Central. A zip plugin takes precedence over a
classpath plugin of
- the same name.
-
-Either way, `plugin-roots` must be present in the JSON config for
`PipesParser` and
-`AsyncProcessor`, even if the directory is empty. `PipesForkParser` fills it
in for you (a
-`plugins` directory beside the jar, else one in the working directory) and
logs a warning when it
-finds none; with the plugins on the classpath the warning is harmless.
-`PipesForkParserConfig.setPluginsDir(Path)` sets it explicitly.
+Fetchers, emitters, iterators and reporters are
xref:pipes/plugins/index.adoc[PF4J plugins], and
+Tika loads them from **zip files in the directories named by `plugin-roots`**.
A plugin jar on
+your application's classpath is not scanned; the only thing a Maven dependency
on a plugin gives
+you is its classes to call directly. The zips are distributed from the
+https://tika.apache.org/download.html[Apache download page], and the
`tika-app` and `tika-server`
+distributions carry them in `plugins/`.
+
+----
+/opt/tika/plugins/
+ tika-pipes-file-system-{tika-version}.zip
+ tika-pipes-s3-{tika-version}.zip
+----
+
+`PipesParser` and `AsyncProcessor` require `plugin-roots` in the JSON config.
`PipesForkParser`
+resolves it: an explicit `PipesForkParserConfig.setPluginsDir(Path)`, else a
`plugins` directory
+beside the jar or in the working directory. When there is neither, which is
the plain Maven case,
+it starts its fork with `-Dtika.plugins.classpath=true` so the file-system
plugin it needs comes
+from the classpath, where the `tika-pipes-fork-parser` dependency put it.
+
+That property is the one deliberate exception. Set on any Tika process, it
makes pf4j also
+accept factories found through `META-INF/extensions.idx` on the classpath,
running in the
+application classloader with none of the plugin isolation; a zip in
`plugin-roots` still wins over
+a classpath copy of the same plugin. It is there for embedding, not for
installing plugins, and
+what happens on a classpath assembled that way is yours to own.
== Dependencies
[source,xml,subs=attributes+]
----
-<!-- PipesForkParser; pulls in tika-pipes-core, the file-system plugin and the
standard parsers -->
+<!-- PipesForkParser; pulls in tika-pipes-core and the standard parsers -->
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-pipes-fork-parser</artifactId>
@@ -95,8 +100,8 @@ finds none; with the plugins on the classpath the warning is
harmless.
</dependency>
----
-The forked JVM is launched with the parent's classpath, so the parsers and
plugins your application
-depends on are the ones the fork uses.
+The forked JVM is launched with the parent's classpath, so the parsers your
application depends on
+are the parsers the fork uses.
== PipesForkParser: one document, result returned
diff --git a/tika-e2e-tests/tika-grpc/pom.xml b/tika-e2e-tests/tika-grpc/pom.xml
index e4c91463e4..9805ec6a6a 100644
--- a/tika-e2e-tests/tika-grpc/pom.xml
+++ b/tika-e2e-tests/tika-grpc/pom.xml
@@ -63,6 +63,14 @@
<artifactId>tika-pipes-core</artifactId>
<version>${tika.version}</version>
</dependency>
+ <!-- The local-server tests hand tika-grpc this zip through -
-plugin-roots. -->
+ <dependency>
+ <groupId>org.apache.tika</groupId>
+ <artifactId>tika-pipes-file-system</artifactId>
+ <version>${tika.version}</version>
+ <scope>test</scope>
+ <type>zip</type>
+ </dependency>
<!-- Jackson for JSON -->
<dependency>
@@ -123,6 +131,31 @@
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>copy-plugins</id>
+ <phase>process-test-resources</phase>
+ <goals>
+ <goal>copy</goal>
+ </goals>
+ <configuration>
+
<outputDirectory>${project.build.directory}/plugins</outputDirectory>
+ <artifactItems>
+ <artifactItem>
+ <groupId>org.apache.tika</groupId>
+
<artifactId>tika-pipes-file-system</artifactId>
+ <version>${tika.version}</version>
+ <type>zip</type>
+ <overWrite>true</overWrite>
+ </artifactItem>
+ </artifactItems>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ExternalTestBase.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ExternalTestBase.java
index 8985c1d00b..5ebe3d569d 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ExternalTestBase.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ExternalTestBase.java
@@ -88,11 +88,25 @@ public abstract class ExternalTestBase {
}
}
+ /**
+ * The checked-in configs name the Docker image's plugins dir; the local
server gets a copy
+ * pointed at the zip this module copies into target/plugins.
+ */
+ public static Path localConfig(String name) throws IOException {
+ Path pluginsDir = Path.of("target/plugins").toAbsolutePath();
+ String json = Files.readString(Path.of("src/test/resources", name));
+ String local = json.replace("/var/cache/tika/plugins",
+ pluginsDir.toString().replace("\\", "\\\\"));
+ Path out = Path.of("target", name).toAbsolutePath();
+ Files.writeString(out, local);
+ return out;
+ }
+
private static void startLocalGrpcServer() throws Exception {
LOG.info("Starting local tika-grpc server using Maven exec");
Path tikaGrpcDir = findTikaGrpcDirectory();
- Path configFile =
Path.of("src/test/resources/tika-config.json").toAbsolutePath();
+ Path configFile = localConfig("tika-config.json");
if (!Files.exists(configFile)) {
throw new IllegalStateException("Config file not found: " +
configFile);
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
index ece3f35bdd..d78fe6ab0a 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/filesystem/HandlerTypeTest.java
@@ -119,7 +119,7 @@ class HandlerTypeTest {
}
Path tikaGrpcDir = tikaRootDir.resolve("tika-grpc");
- Path configFile =
Path.of("src/test/resources/tika-config-ignite-handlertype.json").toAbsolutePath();
+ Path configFile =
ExternalTestBase.localConfig("tika-config-ignite-handlertype.json");
if (!Files.exists(configFile)) {
throw new IllegalStateException("Config file not found: " +
configFile);
}
diff --git
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
index 43342dbfab..d8c5101089 100644
---
a/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
+++
b/tika-e2e-tests/tika-grpc/src/test/java/org/apache/tika/pipes/ignite/IgniteConfigStoreTest.java
@@ -131,7 +131,7 @@ class IgniteConfigStoreTest {
}
String configFileName = "tika-config-ignite-local.json";
- Path configFile = Path.of("src/test/resources/" +
configFileName).toAbsolutePath();
+ Path configFile = ExternalTestBase.localConfig(configFileName);
if (!Files.exists(configFile)) {
throw new IllegalStateException("Config file not found: " +
configFile);
diff --git a/tika-grpc/pom.xml b/tika-grpc/pom.xml
index 988723ec22..ce2fa7c438 100644
--- a/tika-grpc/pom.xml
+++ b/tika-grpc/pom.xml
@@ -211,11 +211,6 @@
<artifactId>tika-pipes-http</artifactId>
<version>${project.version}</version>
</dependency>
- <dependency>
- <groupId>org.apache.tika</groupId>
- <artifactId>tika-pipes-file-system</artifactId>
- <version>${project.version}</version>
- </dependency>
<dependency>
<groupId>org.apache.tika</groupId>
<artifactId>tika-pipes-file-system</artifactId>
diff --git
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
index 6cc673de4a..a2dfb97679 100644
---
a/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
+++
b/tika-grpc/src/test/java/org/apache/tika/pipes/grpc/PipesBiDirectionalStreamingIntegrationTest.java
@@ -103,6 +103,8 @@ class PipesBiDirectionalStreamingIntegrationTest {
// Parse it as JSON to inject the correct javaPath
@SuppressWarnings("unchecked")
Map<String, Object> configMap = OBJECT_MAPPER.readValue(configContent,
Map.class);
+ configMap.put("plugin-roots",
+
Paths.get("target").toAbsolutePath().resolve("plugins").toString());
// Get or create the pipes section
@SuppressWarnings("unchecked")
diff --git
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/AbstractComponentManager.java
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/AbstractComponentManager.java
index 2d27dd3a7a..e93b49a7c9 100644
---
a/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/AbstractComponentManager.java
+++
b/tika-pipes/tika-pipes-core/src/main/java/org/apache/tika/pipes/core/AbstractComponentManager.java
@@ -207,26 +207,19 @@ public abstract class AbstractComponentManager<T extends
TikaExtension,
Map<String, F> factories = new HashMap<>();
for (F factory : pluginManager.getExtensions(getFactoryClass())) {
- String name = factory.getName();
- ClassLoader cl = factory.getClass().getClassLoader();
- boolean isFromPlugin = cl instanceof org.pf4j.PluginClassLoader;
-
- F existing = factories.get(name);
- if (existing != null) {
- boolean existingIsFromPlugin =
existing.getClass().getClassLoader()
- instanceof org.pf4j.PluginClassLoader;
- if (isFromPlugin && !existingIsFromPlugin) {
- // Replace classpath version with plugin version
- factories.put(name, factory);
- }
- // Otherwise skip duplicate (keep existing)
- continue;
+ F existing = factories.get(factory.getName());
+ // a plugin wins over the same factory found on the classpath
(tika.plugins.classpath)
+ if (existing == null || (isFromPlugin(factory) &&
!isFromPlugin(existing))) {
+ factories.put(factory.getName(), factory);
}
- factories.put(name, factory);
}
return factories;
}
+ private static boolean isFromPlugin(Object factory) {
+ return factory.getClass().getClassLoader() instanceof
org.pf4j.PluginClassLoader;
+ }
+
private static String toJsonString(final JsonNode node) throws
TikaConfigException {
try {
return
TikaObjectMapperFactory.getMapper().writeValueAsString(node);
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 69a85940a2..458448956b 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
@@ -38,6 +38,7 @@ import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.config.loader.TikaObjectMapperFactory;
import org.apache.tika.exception.TikaConfigException;
import org.apache.tika.pipes.api.ComponentIds;
+import org.apache.tika.plugins.TikaPluginManager;
/**
* Utility for merging configuration overrides with existing Tika JSON
configuration.
@@ -210,6 +211,20 @@ public class ConfigMerger {
LOG.debug("Applied pipes config: numClients={}",
pc.getNumClients());
}
+ // Appended, not set: the user config's forkedJvmArgs must survive the
opt-in
+ if (overrides.isClasspathPlugins()) {
+ ObjectNode pipesNode = getOrCreateObject(mapper, root, "pipes");
+ ArrayNode argsArray = pipesNode.withArray("forkedJvmArgs");
+ String arg = "-D" + TikaPluginManager.CLASSPATH_PLUGINS_PROPERTY +
"=true";
+ boolean present = false;
+ for (JsonNode existing : argsArray) {
+ present |= arg.equals(existing.asText());
+ }
+ if (!present) {
+ argsArray.add(arg);
+ }
+ }
+
// Apply emit strategy
if (overrides.getEmitStrategy() != null) {
ObjectNode pipesNode = getOrCreateObject(mapper, root, "pipes");
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 779217b29a..40caad25f9 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
@@ -51,6 +51,7 @@ public class ConfigOverrides {
private final String pluginRoots;
private final EmitStrategy emitStrategy;
private final TimeoutLimits timeoutLimits;
+ private final boolean classpathPlugins;
private ConfigOverrides(Builder builder) {
this.fetchers = Collections.unmodifiableList(new
ArrayList<>(builder.fetchers));
@@ -59,6 +60,7 @@ public class ConfigOverrides {
this.pluginRoots = builder.pluginRoots;
this.emitStrategy = builder.emitStrategy;
this.timeoutLimits = builder.timeoutLimits;
+ this.classpathPlugins = builder.classpathPlugins;
}
public static Builder builder() {
@@ -89,6 +91,11 @@ public class ConfigOverrides {
return timeoutLimits;
}
+ /** Whether the fork is told to load plugins from its classpath; see
ConfigMerger. */
+ public boolean isClasspathPlugins() {
+ return classpathPlugins;
+ }
+
/**
* Represents a fetcher configuration override.
*/
@@ -207,10 +214,20 @@ public class ConfigOverrides {
private String pluginRoots;
private EmitStrategy emitStrategy;
private TimeoutLimits timeoutLimits;
+ private boolean classpathPlugins;
private Builder() {
}
+ /**
+ * Appends {@code -Dtika.plugins.classpath=true} to the fork's JVM
args, keeping whatever
+ * forkedJvmArgs the existing config or {@link #setPipesConfig}
already carry.
+ */
+ public Builder setClasspathPlugins(boolean classpathPlugins) {
+ this.classpathPlugins = classpathPlugins;
+ return this;
+ }
+
/**
* Add a fetcher configuration.
*
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
index 8e08f07f18..bc4400541a 100644
---
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
@@ -18,6 +18,7 @@ package org.apache.tika.pipes.core.config;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -61,15 +62,26 @@ public final class DefaultPluginsDir {
* none exists yet
*/
public static String resolve(Class<?> anchor) {
- Path codeSourceDir = null;
+ return resolve(codeSourceDir(anchor), Path.of("")).toString();
+ }
+
+ /**
+ * The same probe as {@link #resolve(Class)}, but empty rather than a
warning and a
+ * fallback path when no {@code plugins} directory exists.
+ */
+ public static Optional<Path> find(Class<?> anchor) {
+ return find(codeSourceDir(anchor), Path.of(""));
+ }
+
+ private static Path codeSourceDir(Class<?> anchor) {
try {
- codeSourceDir =
Path.of(anchor.getProtectionDomain().getCodeSource().getLocation()
+ return
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 null;
}
- return resolve(codeSourceDir, Path.of("")).toString();
}
/**
@@ -80,24 +92,33 @@ public final class DefaultPluginsDir {
* @return the absolute path of the resolved directory
*/
public static Path resolve(Path codeSourceDir, Path cwd) {
+ Path fallback = cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
+ return find(codeSourceDir, cwd).orElseGet(() -> {
+ LOG.warn("no plugins directory found in the install layout or at
{}; "
+ + "pipes plugins will not load unless plugin-roots is
configured", fallback);
+ return fallback;
+ });
+ }
+
+ /**
+ * @return the first existing {@code plugins} directory: beside the code
source, beside its
+ * parent, or in {@code cwd}; empty when there is none
+ */
+ public static Optional<Path> find(Path codeSourceDir, Path cwd) {
if (codeSourceDir != null) {
Path nextToJar = codeSourceDir.resolve(PLUGINS_DIR_NAME);
if (Files.isDirectory(nextToJar)) {
- return nextToJar.toAbsolutePath();
+ return Optional.of(nextToJar.toAbsolutePath());
}
Path parent = codeSourceDir.getParent();
if (parent != null) {
Path nextToParent = parent.resolve(PLUGINS_DIR_NAME);
if (Files.isDirectory(nextToParent)) {
- return nextToParent.toAbsolutePath();
+ return Optional.of(nextToParent.toAbsolutePath());
}
}
}
- Path fallback = cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
- if (!Files.isDirectory(fallback)) {
- LOG.warn("no plugins directory found in the install layout or at
{}; "
- + "pipes plugins will not load unless plugin-roots is
configured", fallback);
- }
- return fallback;
+ Path inCwd = cwd.resolve(PLUGINS_DIR_NAME).toAbsolutePath();
+ return Files.isDirectory(inCwd) ? Optional.of(inCwd) :
Optional.empty();
}
}
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 c7f551ed17..9d88fd17c3 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
@@ -422,4 +422,24 @@ public class ConfigMergerTest {
Files.deleteIfExists(result.configPath());
}
+
+ /** The classpath opt-in is appended; the user config's own JVM args
survive. */
+ @Test
+ public void testClasspathPluginsKeepsUserJvmArgs() throws IOException {
+ Path userConfig = tempDir.resolve("user-config.json");
+ Files.writeString(userConfig,
"{\"pipes\":{\"forkedJvmArgs\":[\"-Xmx4g\"]}}");
+ ConfigOverrides overrides = ConfigOverrides.builder()
+ .setPipesConfig(1, 100, null)
+ .setClasspathPlugins(true)
+ .build();
+
+ ConfigMerger.MergeResult result =
ConfigMerger.mergeOrCreate(userConfig, overrides);
+
+ JsonNode args = new
ObjectMapper().readTree(result.configPath().toFile())
+ .get("pipes").get("forkedJvmArgs");
+ assertEquals(List.of("-Xmx4g", "-Dtika.plugins.classpath=true"),
+ new ObjectMapper().convertValue(args, List.class));
+
+ Files.deleteIfExists(result.configPath());
+ }
}
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
index fbe1beee39..a7df4d1a6c 100644
---
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
@@ -51,6 +51,15 @@ public class DefaultPluginsDirTest {
DefaultPluginsDir.resolve(install.resolve("lib"), cwd));
}
+ @Test
+ public void findIsEmptyWhenNothingExists(@TempDir Path install, @TempDir
Path cwd)
+ throws Exception {
+ assertTrue(DefaultPluginsDir.find(install.resolve("lib"),
cwd).isEmpty());
+ Path plugins = Files.createDirectories(cwd.resolve("plugins"));
+ assertEquals(plugins.toAbsolutePath(),
+ DefaultPluginsDir.find(install.resolve("lib"), cwd).get());
+ }
+
@Test
public void missingPluginsDirStaysAbsolute(@TempDir Path cwd) {
//the forked pipes server must not re-resolve the path against its own
cwd
diff --git a/tika-pipes/tika-pipes-fork-parser/pom.xml
b/tika-pipes/tika-pipes-fork-parser/pom.xml
index 938a7aae0c..eca9b8f00f 100644
--- a/tika-pipes/tika-pipes-fork-parser/pom.xml
+++ b/tika-pipes/tika-pipes-fork-parser/pom.xml
@@ -48,6 +48,8 @@
<artifactId>tika-pipes-core</artifactId>
<version>${project.version}</version>
</dependency>
+ <!-- On the classpath on purpose: a Maven consumer has no plugins
directory, and the fork is
+ told to load this one plugin from the classpath
(tika.plugins.classpath). -->
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>tika-pipes-file-system</artifactId>
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 9e9e893570..0454973652 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
@@ -21,8 +21,12 @@ import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
+import java.util.Optional;
import java.util.UUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import org.apache.tika.config.EmbeddedLimits;
import org.apache.tika.config.loader.TikaJsonConfig;
import org.apache.tika.exception.TikaConfigException;
@@ -120,6 +124,8 @@ public class PipesForkParser implements Closeable {
public static final String DEFAULT_FETCHER_NAME = "fs";
+ private static final Logger LOG =
LoggerFactory.getLogger(PipesForkParser.class);
+
private final PipesForkParserConfig config;
private final PipesParser pipesParser;
private final String internalFetcherId;
@@ -417,6 +423,23 @@ public class PipesForkParser implements Closeable {
private ConfigMerger.MergedConfig createTikaConfig() throws IOException {
PipesConfig pc = config.getPipesConfig();
+ String pluginRoots;
+ boolean classpathPlugins = false;
+ if (config.getPluginsDir() != null) {
+ pluginRoots = config.getPluginsDir().toAbsolutePath().toString();
+ } else {
+ Optional<Path> found =
DefaultPluginsDir.find(PipesForkParser.class);
+ if (found.isPresent()) {
+ pluginRoots = found.get().toString();
+ } else {
+ // No install layout, so this is a Maven consumer: the
file-system plugin this
+ // parser needs is on the classpath, and only the fork is told
to look there.
+ pluginRoots =
Path.of(DefaultPluginsDir.PLUGINS_DIR_NAME).toAbsolutePath().toString();
+ classpathPlugins = true;
+ LOG.info("no plugins directory found; the fork loads plugins
from its classpath");
+ }
+ }
+
// Build configuration overrides
ConfigOverrides.Builder builder = ConfigOverrides.builder()
// Add internal fetcher with UUID-based name to avoid conflicts
@@ -431,22 +454,16 @@ public class PipesForkParser implements Closeable {
pc.getForkedJvmArgs(),
pc.getSocketTimeoutMillis(),
pc.getJavaPath())
+ .setClasspathPlugins(classpathPlugins)
// Use PASSBACK_ALL strategy - results returned through socket
- .setEmitStrategy(EmitStrategy.PASSBACK_ALL);
+ .setEmitStrategy(EmitStrategy.PASSBACK_ALL)
+ .setPluginRoots(pluginRoots);
// Set timeout limits if configured
if (config.getTimeoutLimits() != null) {
builder.setTimeoutLimits(config.getTimeoutLimits());
}
- // plugin-roots is mandatory downstream (TikaPluginManager.load throws
without it), so an
- // unset pluginsDir has to resolve to something rather than fail
construction.
- if (config.getPluginsDir() != null) {
-
builder.setPluginRoots(config.getPluginsDir().toAbsolutePath().toString());
- } else {
-
builder.setPluginRoots(DefaultPluginsDir.resolve(PipesForkParser.class));
- }
-
ConfigOverrides overrides = builder.build();
// Merge with user config if provided, otherwise create new
diff --git
a/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/ClasspathPluginsTest.java
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/ClasspathPluginsTest.java
new file mode 100644
index 0000000000..50ea30a509
--- /dev/null
+++
b/tika-pipes/tika-pipes-fork-parser/src/test/java/org/apache/tika/pipes/fork/ClasspathPluginsTest.java
@@ -0,0 +1,70 @@
+/*
+ * 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.fork;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.nio.file.Path;
+import java.util.Collections;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import org.pf4j.DefaultPluginManager;
+
+import org.apache.tika.pipes.fetcher.fs.FileSystemFetcherFactory;
+import org.apache.tika.plugins.TikaExtensionFactory;
+import org.apache.tika.plugins.TikaPluginManager;
+
+/**
+ * The file-system plugin jar is a compile dependency here, so its
extensions.idx sits on the
+ * application classpath exactly the way a Maven consumer of PipesForkParser
sees it.
+ */
+public class ClasspathPluginsTest {
+
+ @Test
+ public void classpathExtensionsAreNotDiscovered(@TempDir Path emptyRoot)
throws Exception {
+ DefaultPluginManager stock = new DefaultPluginManager(emptyRoot);
+ stock.loadPlugins();
+ stock.startPlugins();
+ assertFalse(stock.getExtensions(TikaExtensionFactory.class).isEmpty(),
+ "pf4j's stock manager sees the index; the test setup is wrong
otherwise");
+
+ TikaPluginManager manager = new
TikaPluginManager(Collections.singletonList(emptyRoot));
+ manager.loadPlugins();
+ manager.startPlugins();
+ assertTrue(manager.getExtensions(TikaExtensionFactory.class).isEmpty(),
+ "a factory on the application classpath must not be
discovered");
+ }
+
+ @Test
+ public void classpathExtensionsDiscoveredWhenOptedIn(@TempDir Path
emptyRoot) throws Exception {
+ System.setProperty(TikaPluginManager.CLASSPATH_PLUGINS_PROPERTY,
"true");
+ try {
+ TikaPluginManager manager =
+ new
TikaPluginManager(Collections.singletonList(emptyRoot));
+ manager.loadPlugins();
+ manager.startPlugins();
+ List<TikaExtensionFactory> found =
manager.getExtensions(TikaExtensionFactory.class);
+ assertTrue(found.stream().anyMatch(f -> f instanceof
FileSystemFetcherFactory),
+ "found: " + found);
+ } finally {
+ System.clearProperty(TikaPluginManager.CLASSPATH_PLUGINS_PROPERTY);
+ }
+ }
+}
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 be5b24e640..7e9dd55c0a 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
@@ -44,6 +44,7 @@ import org.apache.tika.metadata.Metadata;
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;
@@ -75,6 +76,35 @@ public class PipesForkParserTest {
return zipPath;
}
+ @Test
+ public void testClasspathPluginsOnlyWhenOptedIn() throws Exception {
+ Path testFile = tempDir.resolve("test.txt");
+ Files.writeString(testFile, "classpath plugin");
+ Path noPlugins =
Files.createDirectories(tempDir.resolve("no-plugins"));
+
+ PipesForkParserConfig optedIn = new PipesForkParserConfig()
+ .setPluginsDir(noPlugins)
+ .addJvmArg("-Dtika.plugins.classpath=true")
+ .addJvmArg("-Xmx256m");
+ try (PipesForkParser parser = new PipesForkParser(optedIn)) {
+ PipesForkResult result = parser.parse(testFile);
+ assertTrue(result.isSuccess(), "Status: " + result.getStatus()
+ + ", message: " + result.getMessage());
+ assertTrue(result.getContent().contains("classpath plugin"));
+ }
+
+ PipesForkParserConfig zipsOnly = new PipesForkParserConfig()
+ .setPluginsDir(noPlugins)
+ .addJvmArg("-Xmx256m");
+ try (PipesForkParser parser = new PipesForkParser(zipsOnly)) {
+ PipesForkResult result = parser.parse(testFile);
+ assertFalse(result.isSuccess(), "the file-system plugin jar is on
the classpath, "
+ + "but without the opt-in the fork must not find it: " +
result.getStatus());
+ } catch (PipesForkParserException | PipesException e) {
+ // fork refused to start without its fetcher: also the right answer
+ }
+ }
+
@Test
public void testParseTextFile() throws Exception {
// Create a simple test file
diff --git
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/resources/configs/tika-config-http.json
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/resources/configs/tika-config-http.json
index 7ebb4dd77c..dd8ac7ad06 100644
---
a/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/resources/configs/tika-config-http.json
+++
b/tika-pipes/tika-pipes-plugins/tika-pipes-http/src/test/resources/configs/tika-config-http.json
@@ -17,5 +17,5 @@
}
}
},
- "plugin-roots": "target/plugins"
+ "plugin-roots": "target/classes"
}
diff --git
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginComponentLoader.java
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginComponentLoader.java
index 4b6809b6b5..0188495889 100644
---
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginComponentLoader.java
+++
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/PluginComponentLoader.java
@@ -218,26 +218,19 @@ public class PluginComponentLoader {
Map<String, TikaExtensionFactory<T>> factories = new HashMap<>();
for (TikaExtensionFactory<T> factory :
pluginManager.getExtensions(factoryClass)) {
- String name = factory.getName();
- ClassLoader cl = factory.getClass().getClassLoader();
- boolean isFromPlugin = cl instanceof PluginClassLoader;
-
- TikaExtensionFactory<T> existing = factories.get(name);
- if (existing != null) {
- boolean existingIsFromPlugin =
existing.getClass().getClassLoader()
- instanceof PluginClassLoader;
- if (isFromPlugin && !existingIsFromPlugin) {
- // Replace classpath version with plugin version
- factories.put(name, factory);
- }
- // Otherwise skip duplicate (keep existing)
- continue;
+ TikaExtensionFactory<T> existing =
factories.get(factory.getName());
+ // a plugin wins over the same factory found on the classpath
(tika.plugins.classpath)
+ if (existing == null || (isFromPlugin(factory) &&
!isFromPlugin(existing))) {
+ factories.put(factory.getName(), factory);
}
- factories.put(name, factory);
}
return factories;
}
+ private static boolean isFromPlugin(Object factory) {
+ return factory.getClass().getClassLoader() instanceof
PluginClassLoader;
+ }
+
private static String extractTypeName(JsonNode wrapper, String contextName)
throws TikaConfigException {
Iterator<String> fieldNames = wrapper.fieldNames();
diff --git
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/TikaPluginManager.java
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/TikaPluginManager.java
index 423b84b97f..030a3c02fc 100644
---
a/tika-plugins-core/src/main/java/org/apache/tika/plugins/TikaPluginManager.java
+++
b/tika-plugins-core/src/main/java/org/apache/tika/plugins/TikaPluginManager.java
@@ -20,15 +20,25 @@ import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+import org.pf4j.BasePluginLoader;
+import org.pf4j.ClassLoadingStrategy;
import org.pf4j.DefaultExtensionFinder;
import org.pf4j.DefaultPluginManager;
import org.pf4j.ExtensionFinder;
+import org.pf4j.IndexedExtensionFinder;
+import org.pf4j.PluginClassLoader;
+import org.pf4j.PluginClasspath;
+import org.pf4j.PluginDescriptor;
+import org.pf4j.PluginLoader;
import org.pf4j.RuntimeMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,6 +57,14 @@ public class TikaPluginManager extends DefaultPluginManager {
private static final Logger LOG =
LoggerFactory.getLogger(TikaPluginManager.class);
private static final String DEV_MODE_PROPERTY = "tika.plugin.dev.mode";
+
+ /**
+ * System property: {@code true} also accepts extension factories found
through
+ * {@code META-INF/extensions.idx} on the application classpath. Those run
in the application
+ * classloader with none of the plugin isolation; whoever sets it owns the
consequences.
+ * {@code PipesForkParser} sets it on its fork when there is no plugins
directory.
+ */
+ public static final String CLASSPATH_PLUGINS_PROPERTY =
"tika.plugins.classpath";
private static final String DEV_MODE_ENV = "TIKA_PLUGIN_DEV_MODE";
//we're only using this to convert a single path or a list of paths to a
list
@@ -231,20 +249,27 @@ public class TikaPluginManager extends
DefaultPluginManager {
}
/**
- * Override to disable classpath scanning for extensions.
- * By default, PF4J's DefaultExtensionFinder scans both plugins AND the
classpath:
- * - LegacyExtensionFinder scans for extensions.idx files (causes errors
for unpackaged JARs)
- * - ServiceProviderExtensionFinder scans META-INF/services (finds Lombok
and other libs)
- *
- * We only want to discover extensions from the configured plugin
directories,
- * not from the application classpath. The DefaultExtensionFinder without
any
- * additional finders will only scan the loaded plugins.
+ * Extensions come from the loaded plugins only, unless {@link
#CLASSPATH_PLUGINS_PROPERTY}
+ * opts in to pf4j's stock finder, which also reads every {@code
META-INF/extensions.idx} on
+ * the application classpath.
*/
@Override
protected ExtensionFinder createExtensionFinder() {
- // Return a DefaultExtensionFinder without any classpath-scanning
finders.
- // This will only discover extensions within the loaded plugin JARs.
- return new DefaultExtensionFinder(this);
+ if (Boolean.getBoolean(CLASSPATH_PLUGINS_PROPERTY)) {
+ return new DefaultExtensionFinder(this);
+ }
+ return new DefaultExtensionFinder(this) {
+ {
+ // the constructor registers the classpath-scanning finder;
replace it
+ finders.clear();
+ finders.add(new IndexedExtensionFinder(TikaPluginManager.this)
{
+ @Override
+ public Map<String, Set<String>> readClasspathStorages() {
+ return Collections.emptyMap();
+ }
+ });
+ }
+ };
}
/**
@@ -267,6 +292,26 @@ public class TikaPluginManager extends
DefaultPluginManager {
return super.createPluginRepository();
}
+ /**
+ * In development mode each root is an exploded classes directory (e.g.
{@code target/classes}),
+ * so the plugin's classpath is the root itself rather than pf4j's Maven
module layout, and
+ * classes resolve application-first so a test in the same JVM shares the
plugin's types.
+ */
+ @Override
+ protected PluginLoader createPluginLoader() {
+ if (getRuntimeMode() == RuntimeMode.DEVELOPMENT) {
+ return new BasePluginLoader(this, new
PluginClasspath().addClassesDirectories("")) {
+ @Override
+ protected PluginClassLoader createPluginClassLoader(Path
pluginPath,
+
PluginDescriptor descriptor) {
+ return new PluginClassLoader(pluginManager, descriptor,
+ getClass().getClassLoader(),
ClassLoadingStrategy.APD);
+ }
+ };
+ }
+ return super.createPluginLoader();
+ }
+
/**
* Override to use PropertiesPluginDescriptorFinder in development mode.
* In development mode, plugins are in target/classes with
plugin.properties,
diff --git
a/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestExtensionFactory.java
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestExtensionFactory.java
new file mode 100644
index 0000000000..8aaaa18ab2
--- /dev/null
+++
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestExtensionFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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.plugins;
+
+public class TestExtensionFactory implements
TikaExtensionFactory<TikaExtension> {
+
+ @Override
+ public String getName() {
+ return "test-extension-factory";
+ }
+
+ @Override
+ public TikaExtension buildExtension(ExtensionConfig extensionConfig) {
+ return null;
+ }
+}
diff --git
a/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestPlugin.java
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestPlugin.java
new file mode 100644
index 0000000000..56f8287ec7
--- /dev/null
+++ b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TestPlugin.java
@@ -0,0 +1,23 @@
+/*
+ * 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.plugins;
+
+import org.pf4j.Plugin;
+
+/** Named by plugin.properties in the development-mode test. */
+public class TestPlugin extends Plugin {
+}
diff --git
a/tika-plugins-core/src/test/java/org/apache/tika/plugins/TikaPluginManagerTest.java
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TikaPluginManagerTest.java
index ca492d0550..58574e23ab 100644
---
a/tika-plugins-core/src/test/java/org/apache/tika/plugins/TikaPluginManagerTest.java
+++
b/tika-plugins-core/src/test/java/org/apache/tika/plugins/TikaPluginManagerTest.java
@@ -18,6 +18,7 @@ package org.apache.tika.plugins;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
@@ -54,4 +55,25 @@ public class TikaPluginManagerTest {
System.clearProperty("tika.plugin.dev.mode");
}
}
+
+ @Test
+ public void developmentModeLoadsAnExplodedClassesDirectory(@TempDir Path
classes)
+ throws Exception {
+ // the documented recipe: plugin-roots points at target/classes, no zip
+ Files.writeString(classes.resolve("plugin.properties"),
+ "plugin.id=exploded-test\nplugin.class=" +
TestPlugin.class.getName()
+ + "\nplugin.version=1\n");
+ Files.createDirectories(classes.resolve("META-INF"));
+ Files.writeString(classes.resolve("META-INF/extensions.idx"),
+ TestExtensionFactory.class.getName() + "\n");
+ System.setProperty("tika.plugin.dev.mode", "true");
+ try {
+ TikaPluginManager manager = new
TikaPluginManager(Collections.singletonList(classes));
+ manager.loadPlugins();
+ manager.startPlugins();
+ assertEquals(1,
manager.getExtensions(TikaExtensionFactory.class).size());
+ } finally {
+ System.clearProperty("tika.plugin.dev.mode");
+ }
+ }
}