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

ppkarwasz pushed a commit to branch feature/jaxp-factory-methods
in repository https://gitbox.apache.org/repos/asf/commons-xml.git

commit 776e2689fcc0ef72d512570eba0ad3e8287b8e8c
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Aug 27 22:40:12 2026 +0200

    Add the Java 9 newDefaultInstance factory methods, resolved at runtime
    
    Each factory class gains newDefaultInstance (newDefaultFactory for StAX)
    without raising the compile baseline: the Java 9 JAXP method is resolved
    through MethodHandles.publicLookup() and invoked when present; on Java 8
    the JDK's built-in implementation is instantiated by class name instead.
    Where the platform provides neither, for example Android, the lookup
    miss surfaces as the factory's own configuration error, like any
    newInstance miss. java.lang.invoke raises the supported Android baseline
    to API level 26, and the bnd instructions drop the JDK-internal package
    inferred from the reflective StAX fallback.
    
    Assisted-By: Claude Fable 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3
---
 android-tests/build.gradle.kts                     |  3 +-
 pom.xml                                            |  2 +
 .../xml/HardeningDocumentBuilderFactory.java       | 46 +++++++++++++++++++
 .../commons/xml/HardeningSAXParserFactory.java     | 46 +++++++++++++++++++
 .../apache/commons/xml/HardeningSchemaFactory.java | 49 ++++++++++++++++++++
 .../commons/xml/HardeningTransformerFactory.java   | 48 ++++++++++++++++++++
 .../commons/xml/HardeningXMLInputFactory.java      | 53 +++++++++++++++++++++-
 .../apache/commons/xml/HardeningXPathFactory.java  | 53 ++++++++++++++++++++++
 .../java/org/apache/commons/xml/package-info.java  |  2 +-
 src/site/markdown/index.md                         |  2 +-
 .../commons/xml/HardeningFactoriesSmokeTest.java   | 53 ++++++++++++++++++++++
 11 files changed, 353 insertions(+), 4 deletions(-)

diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts
index 9a47c92..a5b7562 100644
--- a/android-tests/build.gradle.kts
+++ b/android-tests/build.gradle.kts
@@ -31,7 +31,8 @@ android {
     compileSdk = 34
 
     defaultConfig {
-        minSdk = 19
+        // java.lang.invoke, used by the newDefault* and newNS* lookups, 
exists from API level 26.
+        minSdk = 26
         // androidx.test runner; Mannodermaus's android-junit5 plugin slots a 
JUnit 5 RunnerBuilder under it so AndroidJUnitRunner picks up Jupiter tests.
         testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
     }
diff --git a/pom.xml b/pom.xml
index 2999a19..4cdc6bb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,7 +54,9 @@ limitations under the License.
     <!-- OSGi bundle metadata: override commons-parent's org.apache.commons.* 
defaults. -->
     
<commons.osgi.symbolicName>org.apache.commons.xml</commons.osgi.symbolicName>
     
<commons.osgi.export>org.apache.commons.xml.*;version=${project.version};-noimport:=true</commons.osgi.export>
+    <!-- The negation drops the JDK-internal package bnd infers from the 
reflective Java 8 fallback in HardeningXMLInputFactory.newDefaultFactory(). -->
     <commons.osgi.import>
+      !com.sun.xml.internal.stream,
       net.sf.saxon.*;resolution:=optional,
       org.apache.xerces.*;resolution:=optional,
       *
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
index e6d979f..5909133 100644
--- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
@@ -17,6 +17,9 @@
 
 package org.apache.commons.xml;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 
 import javax.xml.XMLConstants;
@@ -48,6 +51,19 @@ public final class HardeningDocumentBuilderFactory {
 
     /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, 
which exposes no hardening surface. */
     private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = 
"org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl";
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultInstance()}. */
+    private static final String JDK_DOCUMENT_BUILDER_FACTORY = 
"com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl";
+
+    private static final MethodHandle NEW_DEFAULT_INSTANCE = 
findStatic("newDefaultInstance", 
MethodType.methodType(DocumentBuilderFactory.class));
+
+    private static MethodHandle findStatic(final String name, final MethodType 
type) {
+        try {
+            return 
MethodHandles.publicLookup().findStatic(DocumentBuilderFactory.class, name, 
type);
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
 
     /**
      * Capability-driven hardening for any {@link DocumentBuilderFactory} on 
the classpath.
@@ -81,6 +97,36 @@ static DocumentBuilderFactory harden(final 
DocumentBuilderFactory factory) {
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link DocumentBuilderFactory} of the 
system-default implementation.
+     * <p>
+     * Obtained as by {@code DocumentBuilderFactory.newDefaultInstance()} 
where the platform provides it (Java 9 or later), and
+     * by instantiating the JDK's built-in implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown if the running platform 
provides neither {@code newDefaultInstance()} nor the JDK's built-in 
implementation
+     *                                   (for example Android).
+     */
+    public static DocumentBuilderFactory newDefaultInstance() {
+        if (NEW_DEFAULT_INSTANCE != null) {
+            final DocumentBuilderFactory factory;
+            try {
+                factory = (DocumentBuilderFactory) 
NEW_DEFAULT_INSTANCE.invokeExact();
+            } catch (final FactoryConfigurationError e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        // 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 the factory's own 
FactoryConfigurationError, like any newInstance miss.
+        return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null);
+    }
+
     /**
      * Returns a new, hardened {@link DocumentBuilderFactory}.
      *
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
index 8dda4f2..eb16def 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
@@ -17,6 +17,9 @@
 
 package org.apache.commons.xml;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 
 import javax.xml.XMLConstants;
@@ -58,6 +61,19 @@ public final class HardeningSAXParserFactory {
     private static final String ANDROID_EXPAT_READER = 
"org.apache.harmony.xml.ExpatReader";
     /** Class name of Android's Harmony-based {@link SAXParserFactory}, backed 
by the native Expat parser. */
     private static final String ANDROID_SAX_PARSER_FACTORY = 
"org.apache.harmony.xml.parsers.SAXParserFactoryImpl";
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultInstance()}. */
+    private static final String JDK_SAX_PARSER_FACTORY = 
"com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl";
+
+    private static final MethodHandle NEW_DEFAULT_INSTANCE = 
findStatic("newDefaultInstance", MethodType.methodType(SAXParserFactory.class));
+
+    private static MethodHandle findStatic(final String name, final MethodType 
type) {
+        try {
+            return 
MethodHandles.publicLookup().findStatic(SAXParserFactory.class, name, type);
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
 
     /**
      * Capability-driven hardening for any {@link SAXParserFactory} on the 
classpath.
@@ -138,6 +154,36 @@ static XMLReader harden(final XMLReader reader) {
         return new HardeningXMLReader(reader);
     }
 
+    /**
+     * Returns a new, hardened {@link SAXParserFactory} of the system-default 
implementation.
+     * <p>
+     * Obtained as by {@code SAXParserFactory.newDefaultInstance()} where the 
platform provides it (Java 9 or later), and by
+     * instantiating the JDK's built-in implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown if the running platform 
provides neither {@code newDefaultInstance()} nor the JDK's built-in 
implementation
+     *                                   (for example Android).
+     */
+    public static SAXParserFactory newDefaultInstance() {
+        if (NEW_DEFAULT_INSTANCE != null) {
+            final SAXParserFactory factory;
+            try {
+                factory = (SAXParserFactory) 
NEW_DEFAULT_INSTANCE.invokeExact();
+            } catch (final FactoryConfigurationError e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        // 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 the factory's own 
FactoryConfigurationError, like any newInstance miss.
+        return newInstance(JDK_SAX_PARSER_FACTORY, null);
+    }
+
     /**
      * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX 
wrappers to parse sources with.
      *
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
index 5a71d45..539b8cf 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
@@ -17,8 +17,12 @@
 
 package org.apache.commons.xml;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 
+import javax.xml.XMLConstants;
 import javax.xml.parsers.FactoryConfigurationError;
 import javax.xml.transform.Source;
 import javax.xml.transform.TransformerConfigurationException;
@@ -55,6 +59,21 @@
  */
 public final class HardeningSchemaFactory {
 
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultInstance()}. */
+    private static final String JDK_SCHEMA_FACTORY = 
"com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory";
+
+    private static final MethodHandle NEW_DEFAULT_INSTANCE = 
findNewDefaultInstance();
+
+    private static MethodHandle findNewDefaultInstance() {
+        try {
+            return 
MethodHandles.publicLookup().findStatic(SchemaFactory.class, 
"newDefaultInstance",
+                    MethodType.methodType(SchemaFactory.class));
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
+
     /**
      * Hardening for any {@link SchemaFactory} on the classpath.
      *
@@ -70,6 +89,36 @@ static SchemaFactory harden(final SchemaFactory factory) {
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link SchemaFactory} of the system-default 
implementation, supporting W3C XML Schema 1.0.
+     * <p>
+     * Obtained as by {@code SchemaFactory.newDefaultInstance()} where the 
platform provides it (Java 9 or later), and by instantiating the JDK's built-in
+     * implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException    Thrown if a required hardening setting 
cannot be applied to the underlying implementation.
+     * @throws IllegalArgumentException Thrown if the running platform 
provides neither {@code newDefaultInstance()} nor the JDK's built-in 
implementation
+     *                                 (for example Android).
+     */
+    public static SchemaFactory newDefaultInstance() {
+        if (NEW_DEFAULT_INSTANCE != null) {
+            final SchemaFactory factory;
+            try {
+                factory = (SchemaFactory) NEW_DEFAULT_INSTANCE.invokeExact();
+            } catch (final SchemaFactoryConfigurationError e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        // 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 
IllegalArgumentException, the error SchemaFactory.newInstance(String, String, 
ClassLoader) defines.
+        return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, 
JDK_SCHEMA_FACTORY, null);
+    }
+
     /**
      * Returns a new, hardened {@link SchemaFactory} for the given schema 
language.
      *
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
index d80db4e..a0ff62f 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
@@ -18,6 +18,9 @@
 package org.apache.commons.xml;
 
 import java.io.IOException;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 import java.util.function.Supplier;
 
@@ -74,6 +77,21 @@
  */
 public final class HardeningTransformerFactory {
 
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultInstance()}. */
+    private static final String JDK_TRANSFORMER_FACTORY = 
"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl";
+
+    private static final MethodHandle NEW_DEFAULT_INSTANCE = 
findNewDefaultInstance();
+
+    private static MethodHandle findNewDefaultInstance() {
+        try {
+            return 
MethodHandles.publicLookup().findStatic(TransformerFactory.class, 
"newDefaultInstance",
+                    MethodType.methodType(TransformerFactory.class));
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
+
     /**
      * Capability-driven hardening for any {@link TransformerFactory} on the 
classpath.
      *
@@ -111,6 +129,36 @@ static TransformerFactory harden(final TransformerFactory 
factory) {
         return new Wrapper((SAXTransformerFactory) factory);
     }
 
+    /**
+     * Returns a new, hardened {@link TransformerFactory} of the 
system-default implementation.
+     * <p>
+     * Obtained as by {@code TransformerFactory.newDefaultInstance()} where 
the platform provides it (Java 9 or later), and by instantiating the JDK's 
built-in
+     * implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException                Thrown if a required 
hardening setting cannot be applied to the underlying implementation.
+     * @throws TransformerFactoryConfigurationError Thrown if the running 
platform provides neither {@code newDefaultInstance()} nor the JDK's built-in
+     *                                                implementation (for 
example Android).
+     */
+    public static TransformerFactory newDefaultInstance() {
+        if (NEW_DEFAULT_INSTANCE != null) {
+            final TransformerFactory factory;
+            try {
+                factory = (TransformerFactory) 
NEW_DEFAULT_INSTANCE.invokeExact();
+            } catch (final TransformerFactoryConfigurationError e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        // 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);
+    }
+
     /**
      * Returns a new, hardened {@link TransformerFactory}.
      *
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
index 907f94f..6f15542 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
@@ -19,10 +19,13 @@
 
 import java.io.InputStream;
 import java.io.Reader;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 
-import javax.xml.parsers.FactoryConfigurationError;
 import javax.xml.stream.EventFilter;
+import javax.xml.stream.FactoryConfigurationError;
 import javax.xml.stream.StreamFilter;
 import javax.xml.stream.XMLEventReader;
 import javax.xml.stream.XMLInputFactory;
@@ -53,6 +56,20 @@ public final class HardeningXMLInputFactory {
     static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
     /** Woodstox property: resolver consulted for undeclared entity 
references. */
     static final String WSTX_UNDECLARED_ENTITY_RESOLVER = 
"com.ctc.wstx.undeclaredEntityResolver";
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultFactory()}. */
+    private static final String JDK_XML_INPUT_FACTORY = 
"com.sun.xml.internal.stream.XMLInputFactoryImpl";
+
+    private static final MethodHandle NEW_DEFAULT_FACTORY = 
findNewDefaultFactory();
+
+    private static MethodHandle findNewDefaultFactory() {
+        try {
+            return 
MethodHandles.publicLookup().findStatic(XMLInputFactory.class, 
"newDefaultFactory",
+                    MethodType.methodType(XMLInputFactory.class));
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
 
     /**
      * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on 
the classpath.
@@ -69,6 +86,40 @@ static XMLInputFactory harden(final XMLInputFactory factory) 
{
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link XMLInputFactory} of the system-default 
implementation.
+     * <p>
+     * Obtained as by {@code XMLInputFactory.newDefaultFactory()} where the 
platform provides it (Java 9 or later), and by instantiating the JDK's built-in
+     * implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown if the running platform 
provides neither {@code newDefaultFactory()} nor the JDK's built-in 
implementation
+     *                                   (for example Android).
+     */
+    public static XMLInputFactory newDefaultFactory() {
+        if (NEW_DEFAULT_FACTORY != null) {
+            final XMLInputFactory factory;
+            try {
+                factory = (XMLInputFactory) NEW_DEFAULT_FACTORY.invokeExact();
+            } catch (final FactoryConfigurationError e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        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 harden((XMLInputFactory) 
Class.forName(JDK_XML_INPUT_FACTORY).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");
+        }
+    }
+
     /**
      * Returns a new, hardened {@link XMLInputFactory}, as by {@link 
XMLInputFactory#newFactory()}.
      *
diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
index 8b6eed8..cde63ab 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
@@ -17,6 +17,9 @@
 
 package org.apache.commons.xml;
 
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
 import java.util.Objects;
 
 import javax.xml.XMLConstants;
@@ -45,6 +48,21 @@
  */
 public final class HardeningXPathFactory {
 
+    /** Class name of the JDK's built-in default implementation, the Java 8 
fallback for {@link #newDefaultInstance()}. */
+    private static final String JDK_XPATH_FACTORY = 
"com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl";
+
+    private static final MethodHandle NEW_DEFAULT_INSTANCE = 
findNewDefaultInstance();
+
+    private static MethodHandle findNewDefaultInstance() {
+        try {
+            return MethodHandles.publicLookup().findStatic(XPathFactory.class, 
"newDefaultInstance",
+                    MethodType.methodType(XPathFactory.class));
+        } catch (final ReflectiveOperationException e) {
+            // The method is absent: the running platform predates it.
+            return null;
+        }
+    }
+
     /**
      * Capability-driven hardening for any {@link XPathFactory} on the 
classpath.
      *
@@ -83,6 +101,41 @@ static XPathFactory harden(final XPathFactory factory) {
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link XPathFactory} of the system-default 
implementation, supporting the default XPath object model.
+     * <p>
+     * Obtained as by {@code XPathFactory.newDefaultInstance()} where the 
platform provides it (Java 9 or later), and by instantiating the JDK's built-in
+     * implementation directly on Java 8.
+     * </p>
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException Thrown if a required hardening setting 
cannot be applied to the underlying implementation.
+     * @throws RuntimeException      Thrown if the running platform provides 
neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for
+     *                               example Android).
+     */
+    public static XPathFactory newDefaultInstance() {
+        if (NEW_DEFAULT_INSTANCE != null) {
+            final XPathFactory factory;
+            try {
+                factory = (XPathFactory) NEW_DEFAULT_INSTANCE.invokeExact();
+            } catch (final RuntimeException e) {
+                throw e;
+            } catch (final Throwable e) {
+                // Unreachable: the looked-up method declares no other 
exceptions.
+                throw new IllegalStateException(e);
+            }
+            return harden(factory);
+        }
+        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);
+        } 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);
+        }
+    }
+
     /**
      * Returns a new, hardened {@link XPathFactory} for the default XPath 
object model.
      *
diff --git a/src/main/java/org/apache/commons/xml/package-info.java 
b/src/main/java/org/apache/commons/xml/package-info.java
index c9e3f7a..03764f4 100644
--- a/src/main/java/org/apache/commons/xml/package-info.java
+++ b/src/main/java/org/apache/commons/xml/package-info.java
@@ -33,7 +33,7 @@
  * </ul>
  * <p>
  * These guarantees are defined on OpenJDK 8 or later (and JDK distributions 
built from it). No version of Android supports
- * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android 
(API level 19 or later) the hardening is applied as best-effort without a 
guarantee,
+ * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android 
(API level 26 or later) the hardening is applied as best-effort without a 
guarantee,
  * tested as complete starting with API level 33; see the threat model's 
"Assumptions about the environment".
  * </p>
  * <p>
diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md
index 67b95b0..ad8057f 100644
--- a/src/site/markdown/index.md
+++ b/src/site/markdown/index.md
@@ -66,7 +66,7 @@ so the parse continues without it
 
 ### Supported runtimes
 
-The library requires OpenJDK 8 or later (or a JDK distribution built from it), 
or Android API level 19 or later.
+The library requires OpenJDK 8 or later (or a JDK distribution built from it), 
or Android API level 26 or later.
 
 The security guarantees are defined only on the OpenJDK family
 (see the [Threat Model](threat_model.html)).
diff --git 
a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java 
b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
index 0847aa3..69acdd8 100644
--- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
+++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
@@ -35,9 +35,11 @@
 import javax.xml.validation.SchemaFactory;
 import javax.xml.xpath.XPathFactory;
 
+import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 import org.w3c.dom.Document;
 import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
 
 /**
  * Public-API smoke tests for {@link org.apache.commons.xml}.
@@ -196,4 +198,55 @@ void factoryIdXMLInputFactoryIsHardened() {
     void unknownFactoryClassNameThrows() {
         assertThrows(FactoryConfigurationError.class, () -> 
HardeningDocumentBuilderFactory.newInstance("no.such.FactoryClass", null));
     }
+
+    // The newDefault* methods resolve the Java 9 JAXP method at runtime and 
fall back to the JDK's built-in implementation on Java 8. The dom and sax
+    // variants also run on Android, whose JAXP predates newDefaultInstance 
and carries no JDK-internal fallback: the lookup miss surfaces there as the
+    // factory's own FactoryConfigurationError, like any newInstance miss.
+    @Test
+    @Tag("dom")
+    void newDefaultInstanceDocumentBuilderFactoryIsUsable() throws Exception {
+        if (AttackTestSupport.IS_ANDROID) {
+            assertThrows(FactoryConfigurationError.class, 
HardeningDocumentBuilderFactory::newDefaultInstance);
+            return;
+        }
+        final DocumentBuilderFactory factory = 
HardeningDocumentBuilderFactory.newDefaultInstance();
+        assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new 
StringReader(BENIGN_XML))).getDocumentElement());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    @Tag("sax")
+    void newDefaultInstanceSAXParserFactoryIsUsable() throws Exception {
+        if (AttackTestSupport.IS_ANDROID) {
+            assertThrows(FactoryConfigurationError.class, 
HardeningSAXParserFactory::newDefaultInstance);
+            return;
+        }
+        final SAXParserFactory factory = 
HardeningSAXParserFactory.newDefaultInstance();
+        factory.newSAXParser().parse(new InputSource(new 
StringReader(BENIGN_XML)), new DefaultHandler());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void newDefaultInstanceSchemaFactoryIsHardened() throws Exception {
+        final SchemaFactory factory = 
HardeningSchemaFactory.newDefaultInstance();
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void newDefaultInstanceTransformerFactoryIsHardened() {
+        final TransformerFactory factory = 
HardeningTransformerFactory.newDefaultInstance();
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void newDefaultFactoryXMLInputFactoryIsHardened() {
+        final XMLInputFactory factory = 
HardeningXMLInputFactory.newDefaultFactory();
+        assertEquals(Boolean.TRUE, 
factory.getProperty(XMLInputFactory.SUPPORT_DTD));
+    }
+
+    @Test
+    void newDefaultInstanceXPathFactoryIsHardened() throws Exception {
+        final XPathFactory factory = 
HardeningXPathFactory.newDefaultInstance();
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
 }

Reply via email to