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 8fdf05c014 TIKA-4942: parsing and detection through the OSGi services
no longer recurse (#3275)
8fdf05c014 is described below
commit 8fdf05c01456ecef996f447def456916fc1a4ff7
Author: Tim Allison <[email protected]>
AuthorDate: Mon Oct 5 14:46:15 2026 -0400
TIKA-4942: parsing and detection through the OSGi services no longer
recurse (#3275)
---
CHANGES.txt | 5 +
.../tika/bundle/internal/BundleActivator.java | 10 +-
.../test/java/org/apache/tika/bundle/BundleIT.java | 55 +++++++-
.../org/apache/tika/detect/DefaultDetector.java | 4 +
.../java/org/apache/tika/parser/DefaultParser.java | 16 ++-
.../tika/config/DynamicDefaultProviderTest.java | 149 +++++++++++++++++++++
6 files changed, 229 insertions(+), 10 deletions(-)
diff --git a/CHANGES.txt b/CHANGES.txt
index cba1dc4dae..ee096f0e9b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -10,6 +10,11 @@ Release 4.2.0 - unreleased
partial dates (2019 -> 2019-01-01T12:00:00Z) so date fields never
reject a document (TIKA-4917).
+ * Fix a StackOverflowError when parsing or detecting through the Parser and
Detector
+ services tika-bundle-standard registers in OSGi: the registered
DefaultParser and
+ DefaultDetector no longer consume dynamic services, and a dynamic
DefaultParser or
+ DefaultDetector no longer nests another dynamic one (TIKA-4942).
+
* XmlSecurityContractTest pins XMLReaderUtils' XXE and entity-expansion
defenses
forbidden-apis now rejects building JAXP or Commons Secure XML parsers
anywhere but
XMLReaderUtils (TIKA-4939).
diff --git
a/tika-bundles/tika-bundle-standard/src/main/java/org/apache/tika/bundle/internal/BundleActivator.java
b/tika-bundles/tika-bundle-standard/src/main/java/org/apache/tika/bundle/internal/BundleActivator.java
index ffce4fd8a3..c1e029d702 100644
---
a/tika-bundles/tika-bundle-standard/src/main/java/org/apache/tika/bundle/internal/BundleActivator.java
+++
b/tika-bundles/tika-bundle-standard/src/main/java/org/apache/tika/bundle/internal/BundleActivator.java
@@ -21,8 +21,11 @@ import java.util.Hashtable;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
+import org.apache.tika.config.ServiceLoader;
import org.apache.tika.detect.DefaultDetector;
import org.apache.tika.detect.Detector;
+import org.apache.tika.mime.MediaTypeRegistry;
+import org.apache.tika.mime.MimeTypes;
import org.apache.tika.parser.DefaultParser;
import org.apache.tika.parser.Parser;
@@ -37,10 +40,11 @@ public class BundleActivator implements
org.osgi.framework.BundleActivator {
@Override
public void start(BundleContext context) throws Exception {
+ //a registered service must not itself consume dynamic services, or it
finds itself
+ ServiceLoader loader = new
ServiceLoader(BundleActivator.class.getClassLoader(), false);
detectorService = context.registerService(Detector.class.getName(),
- new DefaultDetector(BundleActivator.class.getClassLoader()),
- new Hashtable<>());
- Parser parser = new
DefaultParser(BundleActivator.class.getClassLoader());
+ new DefaultDetector(MimeTypes.getDefaultMimeTypes(), loader),
new Hashtable<>());
+ Parser parser = new
DefaultParser(MediaTypeRegistry.getDefaultRegistry(), loader);
parserService = context.registerService(Parser.class.getName(),
parser, new Hashtable<>());
}
diff --git
a/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
b/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
index 4e23b6635e..5040280aa5 100644
---
a/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
+++
b/tika-bundles/tika-bundle-standard/src/test/java/org/apache/tika/bundle/BundleIT.java
@@ -224,6 +224,54 @@ public class BundleIT {
"Should have lots of parsers, found " + size);
}
+ // parse and detect through the registered services, the way an OSGi
consumer does;
+ // before the fix both recursed into themselves until the stack overflowed
+ @Test
+ public void testParseThroughRegisteredServices() throws Exception {
+ byte[] html = null;
+ try (ZipInputStream zip = new ZipInputStream(
+ BundleIT.class.getResourceAsStream("/test-documents.zip"))) {
+ for (ZipEntry entry = zip.getNextEntry(); entry != null; entry =
zip.getNextEntry()) {
+ if ("testHTML.html".equals(entry.getName())) {
+ html = zip.readAllBytes();
+ }
+ }
+ }
+ assertNotNull(html, "testHTML.html not found");
+
+ Bundle tikaCore = findBundle("org.apache.tika.core");
+ Class<?> metadataClass =
tikaCore.loadClass("org.apache.tika.metadata.Metadata");
+ Class<?> tisClass =
tikaCore.loadClass("org.apache.tika.io.TikaInputStream");
+ Class<?> contextClass =
tikaCore.loadClass("org.apache.tika.parser.ParseContext");
+ Method tisGet = tisClass.getMethod("get", byte[].class);
+ Method metadataSet = metadataClass.getMethod("set", String.class,
String.class);
+
+ Object detector = ctx.getService(ctx.getAllServiceReferences(
+ "org.apache.tika.detect.Detector", null)[0]);
+ Method detect =
tikaCore.loadClass("org.apache.tika.detect.Detector").getMethod("detect",
+ tisClass, metadataClass, contextClass);
+ Object type;
+ try (AutoCloseable tis = (AutoCloseable) tisGet.invoke(null, (Object)
html)) {
+ type = detect.invoke(detector, tis,
metadataClass.getConstructor().newInstance(),
+ contextClass.getConstructor().newInstance());
+ }
+ assertEquals("text/html", type.toString());
+
+ Object parser = ctx.getService(ctx.getAllServiceReferences(
+ "org.apache.tika.parser.Parser", null)[0]);
+ Method parse =
tikaCore.loadClass("org.apache.tika.parser.Parser").getMethod("parse",
+ tisClass, ContentHandler.class, metadataClass, contextClass);
+ Object metadata = metadataClass.getConstructor().newInstance();
+ metadataSet.invoke(metadata, "Content-Type", type.toString());
+ ContentHandler handler = (ContentHandler) tikaCore
+ .loadClass("org.apache.tika.sax.BodyContentHandler")
+ .getConstructor(int.class).newInstance(-1);
+ try (AutoCloseable tis = (AutoCloseable) tisGet.invoke(null, (Object)
html)) {
+ parse.invoke(parser, tis, handler, metadata,
contextClass.getConstructor().newInstance());
+ }
+ assertTrue(handler.toString().contains("Test Indexation Html"),
handler.toString());
+ }
+
@Test
public void testPdfParsing() throws Exception {
byte[] pdf = null;
@@ -251,11 +299,8 @@ public class BundleIT {
.loadClass("org.apache.tika.sax.BodyContentHandler")
.getConstructor(int.class).newInstance(-1);
- // Uses PDFParser directly: parsing through the registered
DefaultParser
- // service recurses, as TikaActivator feeds it back to itself.
- Object parser = findBundle("org.apache.tika.bundle-standard")
- .loadClass("org.apache.tika.parser.pdf.PDFParser")
- .getConstructor().newInstance();
+ Object parser = ctx.getService(ctx.getAllServiceReferences(
+ "org.apache.tika.parser.Parser", null)[0]);
try (AutoCloseable tis = (AutoCloseable) tisClass.getMethod("get",
byte[].class)
.invoke(null, (Object) pdf)) {
parse.invoke(parser, tis, handler, metadata,
contextClass.getConstructor().newInstance());
diff --git
a/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
b/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
index 200c3eec9a..e6cd160446 100644
--- a/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
+++ b/tika-core/src/main/java/org/apache/tika/detect/DefaultDetector.java
@@ -201,6 +201,10 @@ public class DefaultDetector extends CompositeDetector {
public List<Detector> getDetectors() {
if (loader != null && loader.isDynamic()) {
List<Detector> detectors =
loader.loadDynamicServiceProviders(Detector.class);
+ //a dynamic DefaultDetector already merges every dynamic provider;
nesting one
+ //(this detector included) delegates back into itself
+ detectors.removeIf(d -> d instanceof DefaultDetector &&
+ ((DefaultDetector) d).loader != null && ((DefaultDetector)
d).loader.isDynamic());
if (!detectors.isEmpty()) {
detectors.addAll(super.getDetectors());
return detectors;
diff --git a/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
b/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
index c80d2d37e2..a78f954dc6 100644
--- a/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
+++ b/tika-core/src/main/java/org/apache/tika/parser/DefaultParser.java
@@ -165,7 +165,7 @@ public class DefaultParser extends CompositeParser {
public Map<MediaType, Parser> getParsers(ParseContext context) {
List<Parser> all = new ArrayList<>(super.getAllComponentParsers());
if (loader != null) {
- List<Parser> dynamic =
loader.loadDynamicServiceProviders(Parser.class);
+ List<Parser> dynamic = dynamicParsers();
Collections.reverse(dynamic); // best parser last
all.addAll(dynamic);
}
@@ -178,11 +178,23 @@ public class DefaultParser extends CompositeParser {
List<Parser> parsers = super.getAllComponentParsers();
if (loader != null) {
parsers = new ArrayList<>(parsers);
- parsers.addAll(loader.loadDynamicServiceProviders(Parser.class));
+ parsers.addAll(dynamicParsers());
}
return parsers;
}
+ //a dynamic DefaultParser already merges every dynamic provider; nesting
one (this
+ //parser included) delegates back into itself
+ private List<Parser> dynamicParsers() {
+ List<Parser> dynamic =
loader.loadDynamicServiceProviders(Parser.class);
+ dynamic.removeIf(p -> p instanceof DefaultParser &&
isDynamic((DefaultParser) p));
+ return dynamic;
+ }
+
+ private static boolean isDynamic(DefaultParser parser) {
+ return parser.loader != null && parser.loader.isDynamic();
+ }
+
/**
* Returns the classes that were explicitly excluded when constructing
this parser.
* Used for round-trip serialization to preserve exclusion configuration.
diff --git
a/tika-core/src/test/java/org/apache/tika/config/DynamicDefaultProviderTest.java
b/tika-core/src/test/java/org/apache/tika/config/DynamicDefaultProviderTest.java
new file mode 100644
index 0000000000..8165b9745c
--- /dev/null
+++
b/tika-core/src/test/java/org/apache/tika/config/DynamicDefaultProviderTest.java
@@ -0,0 +1,149 @@
+/*
+ * 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.config;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+import org.xml.sax.ContentHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.detect.DefaultDetector;
+import org.apache.tika.detect.Detector;
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.io.TikaInputStream;
+import org.apache.tika.metadata.HttpHeaders;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.mime.MediaType;
+import org.apache.tika.mime.MediaTypeRegistry;
+import org.apache.tika.mime.MimeTypes;
+import org.apache.tika.parser.DefaultParser;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.Parser;
+
+/**
+ * OSGi registers a DefaultParser and DefaultDetector as dynamic services, and
every dynamic
+ * DefaultParser/DefaultDetector merges dynamic services in. Neither may end
up delegating
+ * to itself, or to another dynamic default, and real providers must still be
used.
+ */
+public class DynamicDefaultProviderTest {
+
+ private static final MediaType TYPE =
MediaType.application("x-tika-osgi-test");
+ private final List<Object> refs = new ArrayList<>();
+
+ private static class MarkerParser implements Parser {
+ @Override
+ public Set<MediaType> getSupportedTypes(ParseContext context) {
+ return Collections.singleton(TYPE);
+ }
+
+ @Override
+ public void parse(TikaInputStream stream, ContentHandler handler,
Metadata metadata,
+ ParseContext context) throws IOException,
SAXException, TikaException {
+ metadata.set("marker", "parsed");
+ }
+ }
+
+ private void register(Object service) {
+ Object ref = new Object();
+ refs.add(ref);
+ ServiceLoader.addService(ref, service, 0);
+ }
+
+ @AfterEach
+ public void unregister() {
+ for (Object ref : refs) {
+ ServiceLoader.removeService(ref);
+ }
+ refs.clear();
+ }
+
+ private static Metadata parse(Parser parser) throws Exception {
+ Metadata metadata = new Metadata();
+ metadata.set(HttpHeaders.CONTENT_TYPE, TYPE.toString());
+ try (TikaInputStream tis =
TikaInputStream.get("x".getBytes(StandardCharsets.UTF_8))) {
+ parser.parse(tis, new DefaultHandler(), metadata, new
ParseContext());
+ }
+ return metadata;
+ }
+
+ private static MediaType detect(Detector detector) throws Exception {
+ try (TikaInputStream tis =
TikaInputStream.get("<html><body>hi</body></html>"
+ .getBytes(StandardCharsets.UTF_8))) {
+ return detector.detect(tis, new Metadata(), new ParseContext());
+ }
+ }
+
+ // the shape the OSGi bundle registered before this fix: a dynamic
DefaultParser
+ @Test
+ @Timeout(30)
+ public void testRegisteredDynamicDefaultParserDoesNotRecurse() throws
Exception {
+ register(new MarkerParser());
+ DefaultParser registered = new DefaultParser();
+ register(registered);
+ assertEquals("parsed", parse(registered).get("marker"));
+ assertEquals("parsed", parse(new DefaultParser()).get("marker"));
+ }
+
+ @Test
+ @Timeout(30)
+ public void testTwoDynamicDefaultParsersDoNotRecurse() throws Exception {
+ register(new MarkerParser());
+ register(new DefaultParser());
+ register(new DefaultParser());
+ assertEquals("parsed", parse(new DefaultParser()).get("marker"));
+ }
+
+ // what a bundle should register: its own parsers, no dynamic lookup of
its own
+ @Test
+ @Timeout(30)
+ public void testStaticDefaultParserStillContributes() throws Exception {
+ DefaultParser bundleParser = new
DefaultParser(MediaTypeRegistry.getDefaultRegistry(),
+ new
ServiceLoader(DynamicDefaultProviderTest.class.getClassLoader(), false));
+ register(bundleParser);
+ assertTrue(new
DefaultParser().getAllComponentParsers().contains(bundleParser));
+ }
+
+ @Test
+ @Timeout(30)
+ public void testRegisteredDynamicDefaultDetectorDoesNotRecurse() throws
Exception {
+ DefaultDetector registered = new DefaultDetector();
+ register(registered);
+ assertEquals(MediaType.TEXT_HTML, detect(registered));
+ assertEquals(MediaType.TEXT_HTML, detect(new DefaultDetector()));
+ }
+
+ @Test
+ @Timeout(30)
+ public void testStaticDefaultDetectorStillContributes() throws Exception {
+ DefaultDetector bundleDetector = new
DefaultDetector(MimeTypes.getDefaultMimeTypes(),
+ new
ServiceLoader(DynamicDefaultProviderTest.class.getClassLoader(), false));
+ register(bundleDetector);
+ assertTrue(new
DefaultDetector().getDetectors().contains(bundleDetector));
+ }
+}