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));
+    }
+}

Reply via email to