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 adf3772  Strengthen OSGi factory selection and resolver initialization 
tests (#118)
adf3772 is described below

commit adf377268328f4e492bbbc8e8d60d2b2e7547e43
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Nov 8 07:40:58 2026 -0500

    Strengthen OSGi factory selection and resolver initialization tests (#118)
    
    Verify that all six secure default factory entry points select their
    expected JDK implementations when the thread context class loader is
    the library's OSGi bundle loader. Set each factory's provider property
    to a nonexistent provider during its check so an accidental fallback
    to ordinary provider discovery cannot silently pass. Restore each
    property afterward.
    
    Add isolated class-loading tests for FallbackIgnoreLSResourceResolver
    so its static initializer executes independently for each scenario.
    Verify that:
    - A failed lookup through the context class loader succeeds on retry
      through the system class loader.
    - A failed retry retains the original lookup exception as suppressed.
    - Both successful and failed retries restore the caller's context
      class loader.
    Skip these initialization tests in native images and on Android,
    where their class-loading assumptions do not apply.
    
    Clarify the OSGi test Javadoc: only the initial default-factory checks
    use the bundle loader as the thread context class loader. The reused
    unit tests retain the launcher's context loader because XSLTC-generated
    translets require visibility of JDK internals. Document the suite's
    explicit class, method, and tag exclusions.
    
    Assisted-By: OpenAI Codex
---
 android-tests/build.gradle.kts                     |   7 +-
 .../java/org/apache/commons/xml/osgi/OsgiIT.java   |  41 ++++++-
 ...IgnoreLSResourceResolverInitializationTest.java | 132 +++++++++++++++++++++
 3 files changed, 174 insertions(+), 6 deletions(-)

diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts
index 3939221..f16bde4 100644
--- a/android-tests/build.gradle.kts
+++ b/android-tests/build.gradle.kts
@@ -105,7 +105,12 @@ android {
 
 // These are JVM-only test classes; exclude them from the Android test compile.
 tasks.withType<JavaCompile>().configureEach {
-    exclude("**/ShadingFootprintTest.java", "**/JaxbTest.java", 
"**/OsgiIT.java")
+    exclude(
+        "**/ShadingFootprintTest.java",
+        "**/JaxbTest.java",
+        "**/OsgiIT.java",
+        "**/FallbackIgnoreLSResourceResolverInitializationTest.java"
+    )
 }
 
 // Skip JAXP groups whose factories Android does not ship
diff --git a/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java 
b/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java
index 83f20ae..6b0647f 100644
--- a/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java
+++ b/src/test/java/org/apache/commons/xml/osgi/OsgiIT.java
@@ -24,6 +24,7 @@
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
+import java.lang.reflect.Field;
 import java.lang.reflect.InvocationTargetException;
 import java.net.URL;
 import java.nio.file.Files;
@@ -67,7 +68,7 @@
 import org.osgi.framework.wiring.BundleWiring;
 
 /**
- * Runs the unit test suite inside an OSGi framework, against the built bundle.
+ * Runs the applicable unit tests 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
@@ -80,6 +81,11 @@
  * 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>
+ * Only the initial default-factory checks use the bundle class loader as the 
thread context class loader (TCCL). The reused tests retain the launcher's
+ * TCCL: XSLTC-generated translets need a parent loader that can see JDK 
internals. They therefore do not prove that all operations work with a bundle 
TCCL.
+ * The reused suite excludes {@code xpath3}, {@link #NOT_IN_OSGI} and {@link 
#METHODS_NOT_IN_OSGI}.
+ * </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>
@@ -91,9 +97,18 @@ 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"}};
+    private static final String[][] DEFAULT_FACTORIES = {
+        {"SecureDocumentBuilderFactory", "newDefaultInstance", 
"javax.xml.parsers.DocumentBuilderFactory",
+            
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"},
+        {"SecureSAXParserFactory", "newDefaultInstance", 
"javax.xml.parsers.SAXParserFactory",
+            "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"},
+        {"SecureSchemaFactory", "newDefaultInstance", 
"javax.xml.validation.SchemaFactory:http://www.w3.org/2001/XMLSchema";,
+            
"com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"},
+        {"SecureTransformerFactory", "newDefaultInstance", 
"javax.xml.transform.TransformerFactory",
+            
"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"},
+        {"SecureXPathFactory", "newDefaultInstance", 
"javax.xml.xpath.XPathFactory:http://java.sun.com/jaxp/xpath/dom";,
+            "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl"},
+        {"SecureXMLInputFactory", "newDefaultFactory", 
"javax.xml.stream.XMLInputFactory", 
"com.sun.xml.internal.stream.XMLInputFactoryImpl"}};
 
     /**
      * Package roots the framework delegates to the class path: the test 
harness only, never the library or JDK internals.
@@ -214,10 +229,26 @@ void defaultFactoriesIgnoreContextClassLoader() throws 
Exception {
         
thread.setContextClassLoader(host.adapt(BundleWiring.class).getClassLoader());
         try {
             assertAll(Stream.of(DEFAULT_FACTORIES).map(factory -> () -> {
+                final String className = factory[0];
+                final String methodName = factory[1];
+                final String property = factory[2];
+                final String provider = factory[3];
+                // A broken provider selection makes an accidental fallback to 
ordinary discovery fail, even on a stock-JDK class path.
+                final String previous = System.setProperty(property, 
"missing.Provider");
                 try {
-                    assertNotNull(host.loadClass(HOST_SYMBOLIC_NAME + "." + 
factory[0]).getMethod(factory[1]).invoke(null), factory[0] + "." + factory[1]);
+                    final Object secured = host.loadClass(HOST_SYMBOLIC_NAME + 
"." + className).getMethod(methodName).invoke(null);
+                    assertNotNull(secured, className + "." + methodName);
+                    final Field delegate = 
secured.getClass().getDeclaredField("delegate");
+                    delegate.setAccessible(true);
+                    assertEquals(provider, 
delegate.get(secured).getClass().getName(), className);
                 } catch (final InvocationTargetException e) {
                     throw e.getCause();
+                } finally {
+                    if (previous == null) {
+                        System.clearProperty(property);
+                    } else {
+                        System.setProperty(property, previous);
+                    }
                 }
             }));
         } finally {
diff --git 
a/src/test/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolverInitializationTest.java
 
b/src/test/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolverInitializationTest.java
new file mode 100644
index 0000000..506c3eb
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/secure/FallbackIgnoreLSResourceResolverInitializationTest.java
@@ -0,0 +1,132 @@
+/*
+ * 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.secure;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+import org.w3c.dom.bootstrap.DOMImplementationRegistry;
+
+/**
+ * Tests resolver initialization using fresh JVM class definitions and the JDK 
DOM implementation.
+ *
+ * <p>
+ * Excluded from Android compilation because Android does not support this JVM 
class-loading mechanism.
+ * </p>
+ */
+@DisabledInNativeImage
+class FallbackIgnoreLSResourceResolverInitializationTest {
+
+    private static void checkInitialization(final boolean failRetry) throws 
Exception {
+        final Thread thread = Thread.currentThread();
+        final ClassLoader original = thread.getContextClassLoader();
+        final String source = failRetry ? "missing.DOMImplementationSource" : 
"com.sun.org.apache.xerces.internal.dom.DOMXSImplementationSourceImpl";
+        final ClassNotFoundException firstFailure = new 
ClassNotFoundException("Rejected by test TCCL");
+        final ClassLoader restricted = new ClassLoader(original) {
+
+            @Override
+            protected Class<?> loadClass(final String name, final boolean 
resolve) throws ClassNotFoundException {
+                if (source.equals(name)) {
+                    throw firstFailure;
+                }
+                return super.loadClass(name, resolve);
+            }
+        };
+        final String previous = 
System.setProperty(DOMImplementationRegistry.PROPERTY, source);
+        thread.setContextClassLoader(restricted);
+        try {
+            if (failRetry) {
+                final ExceptionInInitializerError error = 
assertThrows(ExceptionInInitializerError.class,
+                        
FallbackIgnoreLSResourceResolverInitializationTest::initializeResolver);
+                final Throwable retry = error.getCause();
+                assertInstanceOf(ClassNotFoundException.class, retry);
+                assertEquals(1, retry.getSuppressed().length);
+                assertSame(firstFailure, retry.getSuppressed()[0]);
+            } else {
+                final Field domLs = 
initializeResolver().getDeclaredField("DOM_LS");
+                domLs.setAccessible(true);
+                assertNotNull(domLs.get(null));
+            }
+            assertSame(restricted, thread.getContextClassLoader(), 
"Initialization must restore the caller's TCCL");
+        } finally {
+            thread.setContextClassLoader(original);
+            if (previous == null) {
+                System.clearProperty(DOMImplementationRegistry.PROPERTY);
+            } else {
+                System.setProperty(DOMImplementationRegistry.PROPERTY, 
previous);
+            }
+        }
+    }
+
+    /**
+     * Defines only the resolver in a fresh loader, so each test executes its 
static initializer independently.
+     */
+    private static Class<?> initializeResolver() throws Exception {
+        final String name = FallbackIgnoreLSResourceResolver.class.getName();
+        final byte[] bytes;
+        try (InputStream input = 
FallbackIgnoreLSResourceResolver.class.getResourceAsStream("FallbackIgnoreLSResourceResolver.class");
+                ByteArrayOutputStream output = new ByteArrayOutputStream()) {
+            assertNotNull(input);
+            final byte[] buffer = new byte[4096];
+            int count;
+            while ((count = input.read(buffer)) != -1) {
+                output.write(buffer, 0, count);
+            }
+            bytes = output.toByteArray();
+        }
+        final ClassLoader isolated = new 
ClassLoader(FallbackIgnoreLSResourceResolver.class.getClassLoader()) {
+
+            @Override
+            protected Class<?> loadClass(final String className, final boolean 
resolve) throws ClassNotFoundException {
+                if (!name.equals(className)) {
+                    return super.loadClass(className, resolve);
+                }
+                synchronized (getClassLoadingLock(className)) {
+                    Class<?> result = findLoadedClass(className);
+                    if (result == null) {
+                        result = defineClass(className, bytes, 0, 
bytes.length);
+                    }
+                    if (resolve) {
+                        resolveClass(result);
+                    }
+                    return result;
+                }
+            }
+        };
+        return Class.forName(name, true, isolated);
+    }
+
+    @Test
+    void initializationRestoresContextLoaderAfterRetry() throws Exception {
+        checkInitialization(false);
+    }
+
+    @Test
+    void initializationRetainsBothFailuresAndRestoresContextLoader() throws 
Exception {
+        checkInitialization(true);
+    }
+}

Reply via email to