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 31036ff2d63d6de69e7374e03d96c9a579c6ae04
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Aug 27 21:40:36 2026 +0200

    Add the JDK 8 JAXP static factory methods to the factory classes
    
    Mirror on each Hardening*Factory the static factory methods its JAXP
    counterpart offers in JDK 8, omitting only the deprecated
    XMLInputFactory.newInstance(String, ClassLoader): the explicit
    factoryClassName/ClassLoader overloads, the SchemaFactory and
    XPathFactory language/object-model variants, and the StAX newFactory
    family. Each is a one-liner through the class's hardening recipe.
    
    Assisted-By: Claude Fable 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3
---
 .../xml/HardeningDocumentBuilderFactory.java       | 29 ++++++---
 .../commons/xml/HardeningSAXParserFactory.java     | 25 ++++++--
 .../apache/commons/xml/HardeningSchemaFactory.java | 37 ++++++++----
 .../commons/xml/HardeningTransformerFactory.java   | 48 +++++++++------
 .../commons/xml/HardeningXMLInputFactory.java      | 32 +++++++++-
 .../apache/commons/xml/HardeningXPathFactory.java  | 70 ++++++++++++++++------
 .../commons/xml/HardeningFactoriesSmokeTest.java   | 65 ++++++++++++++++++++
 7 files changed, 243 insertions(+), 63 deletions(-)

diff --git 
a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
index 1e88134..e6d979f 100644
--- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
@@ -31,6 +31,13 @@
 /**
  * Creates new, hardened {@link DocumentBuilderFactory} instances.
  * <p>
+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, 
XInclude resolution is denied by default. When
+ * {@link DocumentBuilderFactory#setXIncludeAware(boolean) 
setXIncludeAware(true)} is called on the returned factory, the parser will 
process
+ * {@code xi:include} elements but every external resource lookup is rejected. 
To permit specific trusted resources, install an
+ * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link 
DocumentBuilder} that allow-lists them; any href the resolver does not 
explicitly allow
+ * stays blocked.
+ * </p>
+ * <p>
  * Not a {@link DocumentBuilderFactory} itself, so none of the JAXP static 
factory methods is inherited: a caller cannot reach a non-hardened factory 
through this class
  * by calling an inherited method such as {@code newDefaultInstance()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -76,13 +83,6 @@ static DocumentBuilderFactory harden(final 
DocumentBuilderFactory factory) {
 
     /**
      * Returns a new, hardened {@link DocumentBuilderFactory}.
-     * <p>
-     * Beyond the three universal guarantees on {@link 
org.apache.commons.xml}, XInclude resolution is denied by default. When
-     * {@link DocumentBuilderFactory#setXIncludeAware(boolean) 
setXIncludeAware(true)} is called on the returned factory, the parser will 
process
-     * {@code xi:include} elements but every external resource lookup is 
rejected. To permit specific trusted resources, install an
-     * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link 
DocumentBuilder} that allow-lists them; any href the resolver does not 
explicitly allow
-     * stays blocked.
-     * </p>
      *
      * @return A hardened factory.
      * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
@@ -95,6 +95,21 @@ public static DocumentBuilderFactory newInstance() {
         return harden(DocumentBuilderFactory.newInstance());
     }
 
+    /**
+     * Returns a new, hardened {@link DocumentBuilderFactory} of the given 
implementation class.
+     *
+     * @param factoryClassName The fully qualified class name of the {@link 
DocumentBuilderFactory} implementation.
+     * @param classLoader      The class loader used to load the factory 
class; {@code null} means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws IllegalStateException     Thrown if a (non-Andoid) factory 
cannot support the secure processing feature
+     *                                   {@link 
XMLConstants#FEATURE_SECURE_PROCESSING}.
+     * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is 
{@code null} or the factory class cannot be loaded or instantiated.
+     */
+    public static DocumentBuilderFactory newInstance(final String 
factoryClassName, final ClassLoader classLoader) {
+        return harden(DocumentBuilderFactory.newInstance(factoryClassName, 
classLoader));
+    }
+
     /**
      * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
      *
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
index 27d91db..8dda4f2 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
@@ -40,6 +40,12 @@
 /**
  * Creates new, hardened {@link SAXParserFactory} instances.
  * <p>
+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, 
XInclude resolution is denied by default. When
+ * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} 
is called on the returned factory, the parser will process {@code xi:include}
+ * elements but every external resource lookup is rejected. To permit specific 
trusted resources, install an {@link org.xml.sax.EntityResolver
+ * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists them; 
any href the resolver does not explicitly allow stays blocked.
+ * </p>
+ * <p>
  * Not a {@link SAXParserFactory} itself, so none of the JAXP static factory 
methods is inherited: a caller cannot reach a non-hardened factory through this 
class
  * by calling an inherited method such as {@code newDefaultInstance()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -152,12 +158,6 @@ static XMLReader newHardenedReader() throws 
TransformerConfigurationException {
 
     /**
      * Returns a new, hardened {@link SAXParserFactory}.
-     * <p>
-     * Beyond the three universal guarantees on {@link 
org.apache.commons.xml}, XInclude resolution is denied by default. When
-     * {@link SAXParserFactory#setXIncludeAware(boolean) 
setXIncludeAware(true)} is called on the returned factory, the parser will 
process {@code xi:include}
-     * elements but every external resource lookup is rejected. To permit 
specific trusted resources, install an {@link org.xml.sax.EntityResolver
-     * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists 
them; any href the resolver does not explicitly allow stays blocked.
-     * </p>
      *
      * @return A hardened factory.
      * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
@@ -168,6 +168,19 @@ public static SAXParserFactory newInstance() {
         return harden(SAXParserFactory.newInstance());
     }
 
+    /**
+     * Returns a new, hardened {@link SAXParserFactory} of the given 
implementation class.
+     *
+     * @param factoryClassName The fully qualified class name of the {@link 
SAXParserFactory} implementation.
+     * @param classLoader      The class loader used to load the factory 
class; {@code null} means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is 
{@code null} or the factory class cannot be loaded or instantiated.
+     */
+    public static SAXParserFactory newInstance(final String factoryClassName, 
final ClassLoader classLoader) {
+        return harden(SAXParserFactory.newInstance(factoryClassName, 
classLoader));
+    }
+
     private static void setFeature(final SAXParserFactory factory, final 
String feature, final boolean value) {
         try {
             factory.setFeature(feature, value);
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
index a123b4a..5a71d45 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
@@ -36,6 +36,17 @@
 /**
  * Creates new, hardened {@link SchemaFactory} instances.
  * <p>
+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}:
+ * </p>
+ * <ul>
+ * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} 
schemaLocation URIs are not resolved during schema compilation, and</li>
+ * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} 
hints in instance documents are not resolved during validation.</li>
+ * </ul>
+ * <p>
+ * The same guarantees apply to {@link javax.xml.validation.Validator} and 
{@link javax.xml.validation.ValidatorHandler} instances produced from the
+ * resulting {@link javax.xml.validation.Schema}.
+ * </p>
+ * <p>
  * Not a {@link SchemaFactory} itself, so none of the JAXP static factory 
methods is inherited: a caller cannot reach a non-hardened factory through this 
class
  * by calling an inherited method such as {@code newDefaultInstance()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -61,17 +72,6 @@ static SchemaFactory harden(final SchemaFactory factory) {
 
     /**
      * Returns a new, hardened {@link SchemaFactory} for the given schema 
language.
-     * <p>
-     * Beyond the three universal guarantees on {@link org.apache.commons.xml}:
-     * </p>
-     * <ul>
-     * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} 
schemaLocation URIs are not resolved during schema compilation, and</li>
-     * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} 
hints in instance documents are not resolved during validation.</li>
-     * </ul>
-     * <p>
-     * The same guarantees apply to {@link javax.xml.validation.Validator} and 
{@link javax.xml.validation.ValidatorHandler} instances produced from the
-     * resulting {@link javax.xml.validation.Schema}.
-     * </p>
      *
      * @param schemaLanguage The schema language, as accepted by {@link 
SchemaFactory#newInstance(String)}.
      * @return A hardened factory.
@@ -83,6 +83,21 @@ public static SchemaFactory newInstance(final String 
schemaLanguage) {
         return harden(SchemaFactory.newInstance(schemaLanguage));
     }
 
+    /**
+     * Returns a new, hardened {@link SchemaFactory} of the given 
implementation class.
+     *
+     * @param schemaLanguage   The schema language, as accepted by {@link 
SchemaFactory#newInstance(String)}.
+     * @param factoryClassName The fully qualified class name of the {@link 
SchemaFactory} implementation.
+     * @param classLoader      The class loader used to load the factory 
class; {@code null} means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalArgumentException Thrown if {@code factoryClassName} is 
{@code null}, or if the factory class cannot be loaded or instantiated, or does
+     *                                  not support {@code schemaLanguage}.
+     * @throws NullPointerException     Thrown if {@code schemaLanguage} is 
{@code null}.
+     */
+    public static SchemaFactory newInstance(final String schemaLanguage, final 
String factoryClassName, final ClassLoader classLoader) {
+        return harden(SchemaFactory.newInstance(schemaLanguage, 
factoryClassName, classLoader));
+    }
+
     private HardeningSchemaFactory() {
         // static only
     }
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
index 172c988..d80db4e 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
@@ -31,6 +31,7 @@
 import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.TransformerFactoryConfigurationError;
 import javax.xml.transform.URIResolver;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.sax.SAXSource;
@@ -48,6 +49,23 @@
 /**
  * Creates new, hardened {@link TransformerFactory} instances.
  * <p>
+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}: 
{@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not 
resolved.
+ * </p>
+ * <p>
+ * The guarantees govern what the transform reads, not what it writes: an 
output instruction like {@code xsl:result-document} still writes wherever the
+ * stylesheet directs, so an untrusted stylesheet's output destinations must 
be restricted outside the library.
+ * </p>
+ * <p>
+ * The guarantees apply to every parser the factory creates internally for the 
standard {@link TransformerFactory} entry points: stylesheet compilation
+ * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) 
newTemplates(Source)},
+ * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) 
newTransformer(Source)}) and source-document reading at
+ * {@code Transformer.transform(Source, Result)} time.
+ * </p>
+ * <p>
+ * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods 
({@code newTransformerHandler(..)}, {@code newTemplatesHandler()},
+ * {@code newXMLFilter(..)}), if reachable by casting the returned factory, 
produce objects carrying the same guarantees.
+ * </p>
+ * <p>
  * Not a {@link TransformerFactory} itself, so none of the JAXP static factory 
methods is inherited: a caller cannot reach a non-hardened factory through this 
class
  * by calling an inherited method such as {@code newDefaultInstance()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -95,23 +113,6 @@ static TransformerFactory harden(final TransformerFactory 
factory) {
 
     /**
      * Returns a new, hardened {@link TransformerFactory}.
-     * <p>
-     * Beyond the three universal guarantees on {@link 
org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code 
document()} URIs are not resolved.
-     * </p>
-     * <p>
-     * The guarantees govern what the transform reads, not what it writes: an 
output instruction like {@code xsl:result-document} still writes wherever the
-     * stylesheet directs, so an untrusted stylesheet's output destinations 
must be restricted outside the library.
-     * </p>
-     * <p>
-     * The guarantees apply to every parser the factory creates internally for 
the standard {@link TransformerFactory} entry points: stylesheet compilation
-     * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) 
newTemplates(Source)},
-     * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) 
newTransformer(Source)}) and source-document reading at
-     * {@code Transformer.transform(Source, Result)} time.
-     * </p>
-     * <p>
-     * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension 
methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()},
-     * {@code newXMLFilter(..)}), if reachable by casting the returned 
factory, produce objects carrying the same guarantees.
-     * </p>
      *
      * @return A hardened factory.
      * @throws IllegalStateException if a required hardening setting cannot be 
applied to the underlying implementation.
@@ -120,6 +121,19 @@ public static TransformerFactory newInstance() {
         return harden(TransformerFactory.newInstance());
     }
 
+    /**
+     * Returns a new, hardened {@link TransformerFactory} of the given 
implementation class.
+     *
+     * @param factoryClassName The fully qualified class name of the {@link 
TransformerFactory} implementation.
+     * @param classLoader      The class loader used to load the factory 
class; {@code null} means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalStateException                Thrown if a required 
hardening setting cannot be applied to the underlying implementation.
+     * @throws TransformerFactoryConfigurationError Thrown if {@code 
factoryClassName} is {@code null} or the factory class cannot be loaded or 
instantiated.
+     */
+    public static TransformerFactory newInstance(final String 
factoryClassName, final ClassLoader classLoader) {
+        return harden(TransformerFactory.newInstance(factoryClassName, 
classLoader));
+    }
+
     private static void setFeature(final TransformerFactory factory, final 
String feature, final boolean value) {
         try {
             factory.setFeature(feature, value);
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
index 37ed393..907f94f 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
@@ -36,6 +36,9 @@
 /**
  * Creates new, hardened {@link XMLInputFactory} instances.
  * <p>
+ * The three universal guarantees on {@link org.apache.commons.xml} apply; 
StAX exposes no additional vectors beyond them.
+ * </p>
+ * <p>
  * Not a {@link XMLInputFactory} itself, so none of the JAXP static factory 
methods is inherited: a caller cannot reach a non-hardened factory through this 
class
  * by calling an inherited method such as {@code newDefaultFactory()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -66,11 +69,34 @@ static XMLInputFactory harden(final XMLInputFactory 
factory) {
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link XMLInputFactory}, as by {@link 
XMLInputFactory#newFactory()}.
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown if an instance of this factory 
cannot be loaded.
+     */
+    public static XMLInputFactory newFactory() {
+        // XMLInputFactory.newInstance, not newFactory: the same specified 
lookup, but Android's StAX API predates newFactory.
+        return harden(XMLInputFactory.newInstance());
+    }
+
+    /**
+     * Returns a new, hardened {@link XMLInputFactory} resolved from the given 
factory id.
+     *
+     * @param factoryId   The name of the factory to find; a system property 
or service id to look up, not the class name of the implementation.
+     * @param classLoader The class loader used in the lookup; {@code null} 
means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
+     * @throws FactoryConfigurationError Thrown in case of a service 
configuration error or if the implementation is not available or cannot be 
instantiated.
+     * @throws NullPointerException      Thrown if {@code factoryId} is {@code 
null}.
+     */
+    public static XMLInputFactory newFactory(final String factoryId, final 
ClassLoader classLoader) {
+        return harden(XMLInputFactory.newFactory(factoryId, classLoader));
+    }
+
     /**
      * Returns a new, hardened {@link XMLInputFactory}.
-     * <p>
-     * The three universal guarantees on {@link org.apache.commons.xml} apply; 
StAX exposes no additional vectors beyond them.
-     * </p>
      *
      * @return A hardened factory.
      * @throws IllegalStateException     Thrown if a required hardening 
setting cannot be applied to the underlying implementation.
diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
index eb33faf..8b6eed8 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
@@ -29,6 +29,14 @@
 /**
  * Creates new, hardened {@link XPathFactory} instances.
  * <p>
+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, 
URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()},
+ * {@code unparsed-text()}) are not resolved.
+ * </p>
+ * <p>
+ * The guarantees also cover the document parse behind {@code 
XPath.evaluate(String, InputSource)} and {@code 
XPathExpression.evaluate(InputSource)}: the
+ * input document is built through a hardened, namespace-aware {@link 
javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser.
+ * </p>
+ * <p>
  * Not a {@link XPathFactory} itself, so none of the JAXP static factory 
methods is inherited: a caller cannot reach a non-hardened factory through this 
class
  * by calling an inherited method such as {@code newDefaultInstance()}. The 
hardened factories are instances of a nested, non-public wrapper class.
  * </p>
@@ -37,25 +45,6 @@
  */
 public final class HardeningXPathFactory {
 
-    /**
-     * Returns a new, hardened {@link XPathFactory} for the default XPath 
object model.
-     * <p>
-     * Beyond the three universal guarantees on {@link 
org.apache.commons.xml}, URI-fetching XPath 3.1+ functions ({@code doc()}, 
{@code collection()},
-     * {@code unparsed-text()}) are not resolved.
-     * </p>
-     * <p>
-     * The guarantees also cover the document parse behind {@code 
XPath.evaluate(String, InputSource)} and {@code 
XPathExpression.evaluate(InputSource)}: the
-     * input document is built through a hardened, namespace-aware {@link 
javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser.
-     * </p>
-     *
-     * @return A hardened factory.
-     * @throws IllegalStateException Thrown if a required hardening setting 
cannot be applied to the underlying implementation.
-     * @throws RuntimeException      Thrown if there is a failure in creating 
an {@link XPathFactory} for the default object model.
-     */
-    public static XPathFactory newInstance() {
-        return harden(XPathFactory.newInstance());
-    }
-
     /**
      * Capability-driven hardening for any {@link XPathFactory} on the 
classpath.
      *
@@ -94,6 +83,49 @@ static XPathFactory harden(final XPathFactory factory) {
         return new Wrapper(factory);
     }
 
+    /**
+     * Returns a new, hardened {@link XPathFactory} for the default XPath 
object model.
+     *
+     * @return A hardened factory.
+     * @throws IllegalStateException Thrown if a required hardening setting 
cannot be applied to the underlying implementation.
+     * @throws RuntimeException      Thrown if there is a failure in creating 
an {@link XPathFactory} for the default object model.
+     */
+    public static XPathFactory newInstance() {
+        return harden(XPathFactory.newInstance());
+    }
+
+    /**
+     * Returns a new, hardened {@link XPathFactory} for the given object model.
+     *
+     * @param uri The underlying object model identifier, as accepted by 
{@link XPathFactory#newInstance(String)}.
+     * @return A hardened factory.
+     * @throws IllegalStateException              Thrown if a required 
hardening setting cannot be applied to the underlying implementation.
+     * @throws XPathFactoryConfigurationException Thrown if no implementation 
of the object model is available.
+     * @throws NullPointerException               Thrown if {@code uri} is 
{@code null}.
+     * @throws IllegalArgumentException           Thrown if {@code uri} is 
empty.
+     */
+    public static XPathFactory newInstance(final String uri) throws 
XPathFactoryConfigurationException {
+        return harden(XPathFactory.newInstance(uri));
+    }
+
+    /**
+     * Returns a new, hardened {@link XPathFactory} of the given 
implementation class.
+     *
+     * @param uri              The underlying object model identifier, as 
accepted by {@link XPathFactory#newInstance(String)}.
+     * @param factoryClassName The fully qualified class name of the {@link 
XPathFactory} implementation.
+     * @param classLoader      The class loader used to load the factory 
class; {@code null} means the current thread's context class loader.
+     * @return A hardened factory.
+     * @throws IllegalStateException              Thrown if a required 
hardening setting cannot be applied to the underlying implementation.
+     * @throws XPathFactoryConfigurationException Thrown if {@code 
factoryClassName} is {@code null}, or if the factory class cannot be loaded or
+     *                                            instantiated, or does not 
support {@code uri}.
+     * @throws NullPointerException               Thrown if {@code uri} is 
{@code null}.
+     * @throws IllegalArgumentException           Thrown if {@code uri} is 
empty.
+     */
+    public static XPathFactory newInstance(final String uri, final String 
factoryClassName, final ClassLoader classLoader)
+            throws XPathFactoryConfigurationException {
+        return harden(XPathFactory.newInstance(uri, factoryClassName, 
classLoader));
+    }
+
     /**
      * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
      *
diff --git 
a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java 
b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
index eea5a92..0847aa3 100644
--- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
+++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java
@@ -21,12 +21,14 @@
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.StringReader;
 
 import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.FactoryConfigurationError;
 import javax.xml.parsers.SAXParserFactory;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.transform.TransformerFactory;
@@ -131,4 +133,67 @@ void newXPathFactoryReturnsFreshInstance() throws 
Exception {
         assertNotSame(a, b);
         assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
     }
+
+    // The explicit-class-name tests discover the runtime default 
implementation through the raw JAXP factory,
+    // so they stay portable across the JAXP implementations of the surefire 
matrix.
+    @Test
+    void explicitClassNameDocumentBuilderFactoryIsHardened() throws Exception {
+        final Class<?> impl = DocumentBuilderFactory.newInstance().getClass();
+        final DocumentBuilderFactory factory = 
HardeningDocumentBuilderFactory.newInstance(impl.getName(), 
impl.getClassLoader());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void explicitClassNameSAXParserFactoryIsHardened() throws Exception {
+        final Class<?> impl = SAXParserFactory.newInstance().getClass();
+        final SAXParserFactory factory = 
HardeningSAXParserFactory.newInstance(impl.getName(), impl.getClassLoader());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void explicitClassNameSchemaFactoryIsHardened() throws Exception {
+        final Class<?> impl = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).getClass();
+        final SchemaFactory factory = 
HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, 
impl.getName(), impl.getClassLoader());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void explicitClassNameTransformerFactoryIsHardened() {
+        final Class<?> impl = TransformerFactory.newInstance().getClass();
+        final TransformerFactory factory = 
HardeningTransformerFactory.newInstance(impl.getName(), impl.getClassLoader());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void explicitClassNameXPathFactoryIsHardened() throws Exception {
+        final Class<?> impl = XPathFactory.newInstance().getClass();
+        final XPathFactory factory = 
HardeningXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, 
impl.getName(), impl.getClassLoader());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void newFactoryReturnsFreshInstance() {
+        final XMLInputFactory a = HardeningXMLInputFactory.newFactory();
+        final XMLInputFactory b = HardeningXMLInputFactory.newFactory();
+        assertNotSame(a, b);
+        assertEquals(Boolean.TRUE, a.getProperty(XMLInputFactory.SUPPORT_DTD));
+    }
+
+    @Test
+    void factoryIdXMLInputFactoryIsHardened() {
+        final String factoryId = "org.apache.commons.xml.test.staxFactory";
+        // XMLInputFactory.newInstance, not newFactory: Android's StAX API 
predates newFactory, and this file also compiles against android.jar.
+        System.setProperty(factoryId, 
XMLInputFactory.newInstance().getClass().getName());
+        try {
+            final XMLInputFactory factory = 
HardeningXMLInputFactory.newFactory(factoryId, getClass().getClassLoader());
+            assertEquals(Boolean.TRUE, 
factory.getProperty(XMLInputFactory.SUPPORT_DTD));
+        } finally {
+            System.clearProperty(factoryId);
+        }
+    }
+
+    @Test
+    void unknownFactoryClassNameThrows() {
+        assertThrows(FactoryConfigurationError.class, () -> 
HardeningDocumentBuilderFactory.newInstance("no.such.FactoryClass", null));
+    }
 }

Reply via email to