This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-secure-xml.git


The following commit(s) were added to refs/heads/main by this push:
     new 6d01f27  COMMONSXML-20, COMMONSXML-23: Find the JDK's XML 
implementations in OSGi (#114)
6d01f27 is described below

commit 6d01f27372ee6f1d8f171bd3277153c417e5bb08
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sun Nov 8 13:04:14 2026 +0100

    COMMONSXML-20, COMMONSXML-23: Find the JDK's XML implementations in OSGi 
(#114)
    
    * Load the Java 8 default XMLInputFactory from the bootstrap loader
    
    On Java 8, SecureXMLInputFactory.newDefaultFactory() instantiates the
    JDK's built-in XMLInputFactory by class name. Loading it through the
    library's own class loader fails when that loader cannot see com.sun.*,
    as with an OSGi bundle class loader. Load it from the bootstrap class
    loader instead.
    
    Fixes COMMONSXML-20.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Run the unit tests inside Felix and Equinox
    
    OsgiIT installs the built jar as a bundle in an OSGi framework, attaches
    the test classes as a fragment and runs the unit tests through the JUnit
    Platform launcher. maven-bundle-plugin writes the fragment manifest into
    the test output directory, so no test jar is built.
    
    Failsafe runs it once with Apache Felix and once with Eclipse Equinox.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Exclude OsgiIT from the Android test compile
    
    OsgiIT needs the JUnit Platform launcher and an OSGi framework, which
    are not on the Android test class path.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Find the JDK's XML implementations when the context class loader cannot
    
    The Java 8 newDefaultInstance() fallbacks now load the JDK's built-in
    implementation from the system class loader instead of the context
    class loader. The LSResourceResolver floor retries its
    DOMImplementationRegistry lookup with the system class loader as
    context class loader when the first lookup fails.
    
    OsgiIT checks the default factories with the library bundle's class
    loader as context class loader.
    
    Fixes COMMONSXML-23.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Spell out the suppressed bnd host warning
    
    Match the whole message instead of a wildcard pattern, and explain why
    the warning is harmless.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Check that the OSGi framework stops cleanly
    
    Assert that waitForStop() reports STOPPED instead of ignoring a timeout
    or an error. Also wrap three lines that exceeded 160 columns.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Scan the test fragment recursively for test classes
    
    Test classes added in a subpackage of org.apache.commons.xml.secure are
    now also run inside the OSGi framework.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Scan the whole test fragment for test classes
    
    With a recursive scan, starting at the library's package is no longer
    needed: start at the bundle root and drop the TEST_PACKAGE constant.
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
    
    * Sort the OSGi test classes with Comparator.comparing
    
    Assisted-By: Claude Opus 5.5 <[email protected]>
---
 android-tests/build.gradle.kts                     |   2 +-
 pom.xml                                            | 131 ++++++++++-
 src/changes/changes.xml                            |   2 +
 .../secure/FallbackIgnoreLSResourceResolver.java   |  17 +-
 .../xml/secure/SecureDocumentBuilderFactory.java   |   3 +-
 .../commons/xml/secure/SecureSAXParserFactory.java |   3 +-
 .../commons/xml/secure/SecureSchemaFactory.java    |   3 +-
 .../xml/secure/SecureTransformerFactory.java       |   3 +-
 .../commons/xml/secure/SecureXMLInputFactory.java  |   3 +-
 .../commons/xml/secure/SecureXPathFactory.java     |   3 +-
 .../java/org/apache/commons/xml/osgi/OsgiIT.java   | 246 +++++++++++++++++++++
 11 files changed, 406 insertions(+), 10 deletions(-)

diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts
index dfda1f8..3939221 100644
--- a/android-tests/build.gradle.kts
+++ b/android-tests/build.gradle.kts
@@ -105,7 +105,7 @@ android {
 
 // These are JVM-only test classes; exclude them from the Android test compile.
 tasks.withType<JavaCompile>().configureEach {
-    exclude("**/ShadingFootprintTest.java", "**/JaxbTest.java")
+    exclude("**/ShadingFootprintTest.java", "**/JaxbTest.java", 
"**/OsgiIT.java")
 }
 
 // Skip JAXP groups whose factories Android does not ship
diff --git a/pom.xml b/pom.xml
index c5f948d..32cea02 100644
--- a/pom.xml
+++ b/pom.xml
@@ -53,7 +53,8 @@ limitations under the License.
     <!-- OSGi bundle metadata: override commons-parent's org.apache.commons.* 
defaults. -->
     
<commons.osgi.symbolicName>org.apache.commons.xml.secure</commons.osgi.symbolicName>
     
<commons.osgi.export>org.apache.commons.xml.secure.*;version=${project.version};-noimport:=true</commons.osgi.export>
-    <!-- The negation drops the JDK-internal package bnd infers from the 
reflective Java 8 fallback in SecureXMLInputFactory.newDefaultFactory(). -->
+    <!-- The negation drops the JDK-internal package bnd infers from the Java 
8 fallback in SecureXMLInputFactory.newDefaultFactory(),
+         which loads it from the bootstrap loader. -->
     <commons.osgi.import>
       !com.sun.xml.internal.stream,
       net.sf.saxon.*;resolution:=optional,
@@ -71,6 +72,9 @@ limitations under the License.
     <commons.woodstox.version>7.1.1</commons.woodstox.version>
     <commons.xalan.version>2.7.3</commons.xalan.version>
     <commons.xerces.version>2.12.2</commons.xerces.version>
+    <!-- Test-only: OSGi frameworks for OsgiIT; the last Felix line and the 
latest Equinox both still run on Java 8. -->
+    <commons.felix.framework.version>7.0.5</commons.felix.framework.version>
+    <commons.equinox.version>3.24.300</commons.equinox.version>
     <!-- Test-only: computes each secure class's shade closure, mirroring 
maven-shade minimizeJar, for ShadingFootprintTest. -->
     <commons.jdependency.version>2.16</commons.jdependency.version>
     <!-- Test-only JAXB dependencies. -->
@@ -119,6 +123,24 @@ limitations under the License.
       <version>${commons.jdependency.version}</version>
       <scope>test</scope>
     </dependency>
+    <!-- OsgiIT: runs the unit tests through the JUnit Platform launcher 
inside each OSGi framework. -->
+    <dependency>
+      <groupId>org.junit.platform</groupId>
+      <artifactId>junit-platform-launcher</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.felix</groupId>
+      <artifactId>org.apache.felix.framework</artifactId>
+      <version>${commons.felix.framework.version}</version>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.eclipse.platform</groupId>
+      <artifactId>org.eclipse.osgi</artifactId>
+      <version>${commons.equinox.version}</version>
+      <scope>test</scope>
+    </dependency>
     <!-- JAXB examples. -->
     <dependency>
       <groupId>javax.xml.bind</groupId>
@@ -391,6 +413,110 @@ limitations under the License.
           </execution>
         </executions>
       </plugin>
+      <!--
+        Writes a fragment manifest into the test output directory,
+        so OsgiIT can install it as an exploded bundle attached to the library.
+        The imports are optional:
+        the test harness (JUnit, Mockito, JAXB) is boot-delegated to the class 
path by OsgiIT.
+      -->
+      <plugin>
+        <groupId>org.apache.felix</groupId>
+        <artifactId>maven-bundle-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>test-bundle-manifest</id>
+            <phase>process-test-classes</phase>
+            <goals>
+              <goal>manifest</goal>
+            </goals>
+            <configuration>
+              
<outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
+              
<manifestLocation>${project.build.testOutputDirectory}/META-INF</manifestLocation>
+              <instructions>
+                
<Bundle-SymbolicName>${commons.osgi.symbolicName}.tests</Bundle-SymbolicName>
+                <Fragment-Host>${commons.osgi.symbolicName}</Fragment-Host>
+                <Export-Package>!*</Export-Package>
+                <Private-Package>org.apache.commons.xml.*</Private-Package>
+                <Import-Package>*;resolution:=optional</Import-Package>
+                <!-- The java11 test sources would otherwise raise the osgi.ee 
requirement to that of the build JDK. -->
+                <_noee>true</_noee>
+                <!--
+                  bnd looks the host up on the class path to drop its packages 
from Import-Package, but the host is this module's own jar, not built yet.
+                  Harmless: the tests are in the host's packages, so the 
fragment imports none of them anyway.
+                -->
+                <_fixupmessages>"Host ${commons.osgi.symbolicName}= for this 
fragment/require bundle cannot be found on the 
classpath";is:=ignore</_fixupmessages>
+              </instructions>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <!--
+        OsgiIT runs the unit tests inside an OSGi framework against the 
packaged bundle, once per framework.
+        Each framework jar embeds its own copy of the OSGi API,
+        so each execution keeps exactly one of them on the class path.
+        Saxon is dropped as in test-stockjdk,
+        so provider lookups reach the JDK's built-in implementations, as they 
do for a bundle.
+      -->
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <!-- The same permissive JDK defaults as the surefire executions. -->
+        <configuration>
+          <systemPropertyVariables>
+            <javax.xml.accessExternalDTD>all</javax.xml.accessExternalDTD>
+            
<javax.xml.accessExternalSchema>all</javax.xml.accessExternalSchema>
+            
<javax.xml.accessExternalStylesheet>all</javax.xml.accessExternalStylesheet>
+          </systemPropertyVariables>
+        </configuration>
+        <executions>
+          <execution>
+            <id>test-osgi-felix</id>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+            <configuration>
+              <includes>
+                <include>**/OsgiIT.java</include>
+              </includes>
+              
<reportsDirectory>${project.build.directory}/failsafe-reports/felix</reportsDirectory>
+              
<summaryFile>${project.build.directory}/failsafe-reports/felix/failsafe-summary.xml</summaryFile>
+              <classpathDependencyExcludes>
+                <exclude>net.sf.saxon:Saxon-HE</exclude>
+                <exclude>org.eclipse.platform:org.eclipse.osgi</exclude>
+              </classpathDependencyExcludes>
+              <systemPropertyVariables>
+                
<buildJar>${project.build.directory}/${project.build.finalName}.jar</buildJar>
+                
<testClassesDirectory>${project.build.testOutputDirectory}</testClassesDirectory>
+                
<osgiWorkDirectory>${project.build.directory}/osgi/felix</osgiWorkDirectory>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+          <execution>
+            <id>test-osgi-equinox</id>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
+            <configuration>
+              <includes>
+                <include>**/OsgiIT.java</include>
+              </includes>
+              
<reportsDirectory>${project.build.directory}/failsafe-reports/equinox</reportsDirectory>
+              
<summaryFile>${project.build.directory}/failsafe-reports/equinox/failsafe-summary.xml</summaryFile>
+              <classpathDependencyExcludes>
+                <exclude>net.sf.saxon:Saxon-HE</exclude>
+                <exclude>org.apache.felix:org.apache.felix.framework</exclude>
+              </classpathDependencyExcludes>
+              <systemPropertyVariables>
+                
<buildJar>${project.build.directory}/${project.build.finalName}.jar</buildJar>
+                
<testClassesDirectory>${project.build.testOutputDirectory}</testClassesDirectory>
+                
<osgiWorkDirectory>${project.build.directory}/osgi/equinox</osgiWorkDirectory>
+              </systemPropertyVariables>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
       <!-- Do not publish a test-jar: the test classes are not part of the 
consumer contract. -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -591,6 +717,9 @@ limitations under the License.
                   <goal>verify</goal>
                 </goals>
                 <configuration>
+                  <includes>
+                    <include>**/DescriptorIT.java</include>
+                  </includes>
                   <systemPropertyVariables>
                     
<buildJar>${project.build.directory}/${project.build.finalName}.jar</buildJar>
                   </systemPropertyVariables>
diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index ac14bbb..35f03e8 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -33,6 +33,8 @@ The <action> type attribute can be add, update, fix, or 
remove.
   <body>
     <release version="1.1.1" date="YYYY-MM-DD" description="This is a 
maintenance release. Java 8 or later is required.">
       <!-- FIX -->
+      <action type="fix" dev="pkarwasz" due-to="Konrad Windszus, Piotr P. 
Karwasz, Gary Gregory" issue="COMMONSXML-20">Load the JDK's built-in 
XMLInputFactory on Java 8 from the bootstrap class loader, so 
SecureXMLInputFactory.newDefaultFactory() works in OSGi.</action>
+      <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory" issue="COMMONSXML-23">Load the JDK's built-in implementations from the 
system class loader in the Java 8 newDefaultInstance() fallbacks, and retry 
with it in the LSResourceResolver floor, so they work when the context class 
loader cannot see them.</action>
       <!-- ADD -->
       <!-- UPDATE -->
       <action dev="ggregory" type="update" due-to="Gary Gregory">Bump 
org.apache.commons:commons-parent from 105 to 107.</action>
diff --git 
a/src/main/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolver.java
 
b/src/main/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolver.java
index 0130069..5ed43d4 100644
--- 
a/src/main/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolver.java
+++ 
b/src/main/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolver.java
@@ -42,11 +42,24 @@ final class FallbackIgnoreLSResourceResolver implements 
LSResourceResolver {
     private static final DOMImplementationLS DOM_LS;
 
     static {
+        DOMImplementationLS domLs;
         try {
-            DOM_LS = (DOMImplementationLS) 
DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
+            domLs = (DOMImplementationLS) 
DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
         } catch (final ReflectiveOperationException e) {
-            throw new ExceptionInInitializerError(e);
+            // The registry loads through the context class loader, which may 
not see the JDK's implementation (for example an OSGi bundle class loader).
+            final Thread thread = Thread.currentThread();
+            final ClassLoader contextClassLoader = 
thread.getContextClassLoader();
+            thread.setContextClassLoader(ClassLoader.getSystemClassLoader());
+            try {
+                domLs = (DOMImplementationLS) 
DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
+            } catch (final ReflectiveOperationException retry) {
+                retry.addSuppressed(e);
+                throw new ExceptionInInitializerError(retry);
+            } finally {
+                thread.setContextClassLoader(contextClassLoader);
+            }
         }
+        DOM_LS = domLs;
     }
 
     private LSResourceResolver delegate;
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureDocumentBuilderFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureDocumentBuilderFactory.java
index 9b709f6..e434537 100644
--- 
a/src/main/java/org/apache/commons/xml/secure/SecureDocumentBuilderFactory.java
+++ 
b/src/main/java/org/apache/commons/xml/secure/SecureDocumentBuilderFactory.java
@@ -220,7 +220,8 @@ public static DocumentBuilderFactory newDefaultInstance() {
         }
         try {
             // Java 8: the method does not exist; instantiate the JDK's 
built-in default by its class name instead.
-            return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null);
+            // Load it from the system class loader: the context class loader 
may not see it (for example an OSGi bundle class loader).
+            return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, 
ClassLoader.getSystemClassLoader());
         } catch (final FactoryConfigurationError e) {
             // Neither exists (for example, Android): degrade to the regular 
lookup, which such platforms pin to their built-in parser.
             return newInstance();
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureSAXParserFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureSAXParserFactory.java
index 31a98f0..c8a66b0 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureSAXParserFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureSAXParserFactory.java
@@ -218,7 +218,8 @@ public static SAXParserFactory newDefaultInstance() {
         }
         try {
             // Java 8: the method does not exist; instantiate the JDK's 
built-in default by its class name instead.
-            return newInstance(JDK_SAX_PARSER_FACTORY, null);
+            // Load it from the system class loader: the context class loader 
may not see it (for example an OSGi bundle class loader).
+            return newInstance(JDK_SAX_PARSER_FACTORY, 
ClassLoader.getSystemClassLoader());
         } catch (final FactoryConfigurationError e) {
             // Neither exists (for example, Android): degrade to the regular 
lookup, which such platforms pin to their built-in parser.
             return newInstance();
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureSchemaFactory.java
index 0eb37ce..2d293e5 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureSchemaFactory.java
@@ -237,7 +237,8 @@ public static SchemaFactory newDefaultInstance() {
         }
         try {
             // Java 8: the method does not exist; instantiate the JDK's 
built-in default by its class name instead.
-            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, 
JDK_SCHEMA_FACTORY, null);
+            // Load it from the system class loader: the context class loader 
may not see it (for example an OSGi bundle class loader).
+            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, 
JDK_SCHEMA_FACTORY, ClassLoader.getSystemClassLoader());
         } catch (final IllegalArgumentException e) {
             // Neither exists (for example Android): degrade to the regular 
lookup, whose Android fallback is exactly the Xerces implementation.
             return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
index 3d9130a..a9b0bc9 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformerFactory.java
@@ -481,7 +481,8 @@ public static TransformerFactory newDefaultInstance() {
         }
         // Java 8: the method does not exist; instantiate the JDK's built-in 
default by its class name instead. Where that class does not exist either (for
         // example Android), the lookup miss surfaces as 
TransformerFactoryConfigurationError, like any newInstance miss.
-        return newInstance(JDK_TRANSFORMER_FACTORY, null);
+        // Load it from the system class loader: the context class loader may 
not see it (for example an OSGi bundle class loader).
+        return newInstance(JDK_TRANSFORMER_FACTORY, 
ClassLoader.getSystemClassLoader());
     }
 
     /**
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureXMLInputFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureXMLInputFactory.java
index 7cb5525..f64a8cf 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXMLInputFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXMLInputFactory.java
@@ -285,7 +285,8 @@ public static XMLInputFactory newDefaultFactory() {
         }
         try {
             // Java 8: the method does not exist, and XMLInputFactory has no 
class-name-taking lookup; instantiate the JDK's built-in default directly.
-            return secure((XMLInputFactory) 
Class.forName(JDK_XML_INPUT_FACTORY).getConstructor().newInstance());
+            // Load it from the bootstrap loader: our own loader may not see 
com.sun.* (for example, an OSGi bundle class loader).
+            return secure((XMLInputFactory) 
Class.forName(JDK_XML_INPUT_FACTORY, true, 
null).getConstructor().newInstance());
         } catch (final ReflectiveOperationException e) {
             // Where the class does not exist either (for example Android), 
report the miss like any StAX factory lookup: with FactoryConfigurationError.
             throw new FactoryConfigurationError(e, "Neither 
XMLInputFactory.newDefaultFactory() nor " + JDK_XML_INPUT_FACTORY + " is 
available");
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java 
b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
index 3881c57..b40c12c 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXPathFactory.java
@@ -194,7 +194,8 @@ public static XPathFactory newDefaultInstance() {
         }
         try {
             // Java 8: the method does not exist; instantiate the JDK's 
built-in default by its class name instead.
-            return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, 
JDK_XPATH_FACTORY, null);
+            // Load it from the system class loader: the context class loader 
may not see it (for example an OSGi bundle class loader).
+            return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, 
JDK_XPATH_FACTORY, ClassLoader.getSystemClassLoader());
         } catch (final XPathFactoryConfigurationException e) {
             // newDefaultInstance declares no checked exception; mirror 
XPathFactory.newInstance(), which reports a default-model miss as a 
RuntimeException.
             throw new RuntimeException("Neither 
XPathFactory.newDefaultInstance() nor " + JDK_XPATH_FACTORY + " is available", 
e);
diff --git a/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java 
b/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java
new file mode 100644
index 0000000..83f20ae
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java
@@ -0,0 +1,246 @@
+/*
+ * 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
+ *
+ *      https://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.commons.xml.osgi;
+
+import static org.junit.jupiter.api.Assertions.assertAll;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.InvocationTargetException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DynamicTest;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestFactory;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+import org.junit.platform.launcher.TagFilter;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
+import org.junit.platform.launcher.listeners.TestExecutionSummary;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.FrameworkEvent;
+import org.osgi.framework.launch.Framework;
+import org.osgi.framework.launch.FrameworkFactory;
+import org.osgi.framework.wiring.BundleWiring;
+
+/**
+ * Runs the unit test suite inside an OSGi framework, against the built bundle.
+ *
+ * <p>
+ * The tests catch class-loading assumptions that only hold on a flat 
classpath: an OSGi bundle class loader sees its own classes, the packages it 
imports and
+ * {@code java.*}, but not JDK-internal packages such as {@code com.sun.*}. 
The built jar is installed with its generated manifest, so the bundle also 
proves
+ * that its {@code Import-Package} header resolves.
+ * </p>
+ * <p>
+ * The test output directory, which the build gives a fragment manifest, is 
attached to that bundle, so the tests share its class loader and keep their
+ * package-private access. The test harness (JUnit, Mockito, JAXB) is not 
under test and is boot-delegated to the class path. Only the JDK's built-in JAXP
+ * implementations are used, as in the {@code test-stockjdk} execution: 
provider lookups cannot see other bundles without a mediator such as SPI Fly.
+ * </p>
+ * <p>
+ * The framework is whichever single {@link FrameworkFactory} the class path 
provides; the build runs this class once with Apache Felix and once with Eclipse
+ * Equinox.
+ * </p>
+ */
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class OsgiIT {
+
+    /**
+     * Factory methods that reach the JDK's built-in implementations without a 
provider lookup: {@code SecureSchemaFactory} also initializes the
+     * {@code LSResourceResolver} floor.
+     */
+    private static final String[][] DEFAULT_FACTORIES = 
{{"SecureDocumentBuilderFactory", "newDefaultInstance"},
+        {"SecureSAXParserFactory", "newDefaultInstance"}, 
{"SecureSchemaFactory", "newDefaultInstance"}, {"SecureTransformerFactory", 
"newDefaultInstance"},
+        {"SecureXPathFactory", "newDefaultInstance"}, 
{"SecureXMLInputFactory", "newDefaultFactory"}};
+
+    /**
+     * Package roots the framework delegates to the class path: the test 
harness only, never the library or JDK internals.
+     */
+    private static final String[] HARNESS_PACKAGES = {"org.junit", 
"org.opentest4j", "org.apiguardian", "org.mockito", "net.bytebuddy", 
"org.objenesis",
+        "javax.xml.bind", "com.sun.xml.bind", "org.glassfish.jaxb", 
"com.sun.istack", "javax.activation", "jakarta.activation"};
+
+    /**
+     * Symbolic name of the library bundle.
+     */
+    private static final String HOST_SYMBOLIC_NAME = 
"org.apache.commons.xml.secure";
+
+    /**
+     * Test classes that cannot run inside a framework: {@code 
ShadingFootprintTest} reads {@code target/classes} directly.
+     */
+    private static final List<String> NOT_IN_OSGI = 
Collections.singletonList("org.apache.commons.xml.secure.ShadingFootprintTest");
+
+    /**
+     * Test methods that cannot run inside a framework.
+     *
+     * <p>
+     * They name a test class as the JAXP factory, which JAXP loads through 
the context class loader: that is the class path, which holds a second copy of 
the
+     * test classes.
+     * </p>
+     */
+    private static final List<String> METHODS_NOT_IN_OSGI = 
Arrays.asList("SecureDocumentBuilderFactoryTest#newDocumentBuilderWrapsDeclaredExceptions",
+            "SecureSAXParserFactoryTest#newXmlReaderWrapsDeclaredExceptions");
+
+    private static Framework framework;
+
+    private static Bundle host;
+
+    private static Path requiredPath(final String property) {
+        final String value = System.getProperty(property);
+        assertNotNull(value, "System property '" + property + "' must be set 
by the build");
+        return Paths.get(value).toAbsolutePath();
+    }
+
+    @BeforeAll
+    static void startFramework() throws Exception {
+        final List<FrameworkFactory> factories = new ArrayList<>();
+        ServiceLoader.load(FrameworkFactory.class).forEach(factories::add);
+        assertEquals(1, factories.size(), () -> "Expected exactly one OSGi 
framework on the class path, found " + factories);
+        final Path buildJar = requiredPath("buildJar");
+        final Path work = requiredPath("osgiWorkDirectory");
+        Files.createDirectories(work);
+        final Map<String, String> config = new HashMap<>();
+        config.put(Constants.FRAMEWORK_STORAGE, 
work.resolve("storage").toString());
+        config.put(Constants.FRAMEWORK_STORAGE_CLEAN, 
Constants.FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT);
+        config.put(Constants.FRAMEWORK_BUNDLE_PARENT, 
Constants.FRAMEWORK_BUNDLE_PARENT_FRAMEWORK);
+        // A trailing ".*" only matches subpackages, so each root is listed 
twice.
+        // sun.reflect, jdk.internal.reflect: the JDK defines generated 
reflection accessors under the bundle loader.
+        // leaked: XSLTC rejects the host of Felix bundle:// URLs (for example 
"5.0"), so the XML fixtures resolve as file: URLs instead.
+        config.put(Constants.FRAMEWORK_BOOTDELEGATION, 
Stream.concat(Stream.of(HARNESS_PACKAGES).flatMap(p -> Stream.of(p, p + ".*")),
+                Stream.of("sun.reflect", "jdk.internal.reflect", 
"leaked")).collect(Collectors.joining(",")));
+        // Both frameworks can fall back to the parent loader on their own; 
disable it so only the explicit list above is delegated.
+        config.put("felix.bootdelegation.implicit", "false");
+        config.put("osgi.compatibility.bootdelegation", "false");
+        config.put("osgi.context.bootdelegation", "false");
+        framework = factories.get(0).newFramework(config);
+        framework.start();
+        final BundleContext context = framework.getBundleContext();
+        host = context.installBundle(buildJar.toUri().toString());
+        // The test output directory carries the fragment manifest written by 
maven-bundle-plugin; install it in place.
+        context.installBundle("reference:" + 
requiredPath("testClassesDirectory").toUri());
+        host.start();
+        assertEquals(Bundle.ACTIVE, host.getState(), "library bundle must 
start");
+    }
+
+    @AfterAll
+    static void stopFramework() throws Exception {
+        if (framework != null) {
+            framework.stop();
+            final FrameworkEvent event = framework.waitForStop(10_000);
+            assertEquals(FrameworkEvent.STOPPED, event.getType(),
+                    () -> "framework did not stop cleanly: event type " + 
event.getType() + ", " + event.getThrowable());
+        }
+    }
+
+    /**
+     * Runs one test class, loaded through the library bundle, and fails with 
the inner failures if any.
+     */
+    private static void runTestClass(final Class<?> testClass) {
+        final SummaryGeneratingListener listener = new 
SummaryGeneratingListener();
+        final PostDiscoveryFilter notInOsgi = descriptor -> 
descriptor.getSource()
+                .filter(MethodSource.class::isInstance)
+                .map(MethodSource.class::cast)
+                .filter(source -> 
METHODS_NOT_IN_OSGI.contains(source.getJavaClass().getSimpleName() + "#" + 
source.getMethodName()))
+                .map(source -> FilterResult.excluded("cannot run inside an 
OSGi framework"))
+                .orElseGet(() -> FilterResult.included(null));
+        // The context class loader stays the class path's, as in a plain 
framework launcher.
+        
LauncherFactory.create().execute(LauncherDiscoveryRequestBuilder.request()
+                .selectors(DiscoverySelectors.selectClass(testClass))
+                // Same exclusion as the test-stockjdk execution: xpath3 needs 
Saxon.
+                .filters(TagFilter.excludeTags("xpath3"), notInOsgi)
+                .build(), listener);
+        final TestExecutionSummary summary = listener.getSummary();
+        if (summary.getTotalFailureCount() > 0) {
+            final StringWriter failures = new StringWriter();
+            summary.printFailuresTo(new PrintWriter(failures), 25);
+            throw new AssertionError(summary.getTotalFailureCount() + " 
failure(s) in " + testClass.getName() + " under " + framework.getSymbolicName()
+                    + System.lineSeparator() + failures);
+        }
+    }
+
+    /**
+     * Checks that the default factories do not depend on the context class 
loader, with the library bundle's class loader as one.
+     *
+     * <p>
+     * Runs first: a class initializer, such as that of the {@code 
LSResourceResolver} floor, runs once per framework.
+     * </p>
+     */
+    @Test
+    @Order(1)
+    void defaultFactoriesIgnoreContextClassLoader() throws Exception {
+        final Thread thread = Thread.currentThread();
+        final ClassLoader contextClassLoader = thread.getContextClassLoader();
+        
thread.setContextClassLoader(host.adapt(BundleWiring.class).getClassLoader());
+        try {
+            assertAll(Stream.of(DEFAULT_FACTORIES).map(factory -> () -> {
+                try {
+                    assertNotNull(host.loadClass(HOST_SYMBOLIC_NAME + "." + 
factory[0]).getMethod(factory[1]).invoke(null), factory[0] + "." + factory[1]);
+                } catch (final InvocationTargetException e) {
+                    throw e.getCause();
+                }
+            }));
+        } finally {
+            thread.setContextClassLoader(contextClassLoader);
+        }
+    }
+
+    @TestFactory
+    @Order(2)
+    Stream<DynamicTest> unitTestsPassInsideFramework() throws 
ClassNotFoundException {
+        final List<DynamicTest> tests = new ArrayList<>();
+        final Enumeration<URL> entries = host.findEntries("/", "*Test.class", 
true);
+        assertNotNull(entries, "test fragment must be attached to the library 
bundle");
+        while (entries.hasMoreElements()) {
+            final String path = entries.nextElement().getPath();
+            final String className = path.substring(path.startsWith("/") ? 1 : 
0, path.length() - ".class".length()).replace('/', '.');
+            if (!NOT_IN_OSGI.contains(className)) {
+                final Class<?> testClass = host.loadClass(className);
+                tests.add(DynamicTest.dynamicTest(testClass.getSimpleName(), 
() -> runTestClass(testClass)));
+            }
+        }
+        assertFalse(tests.isEmpty(), "no test classes found in the test 
fragment");
+        tests.sort(Comparator.comparing(DynamicTest::getDisplayName));
+        return tests.stream();
+    }
+}

Reply via email to