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

ppkarwasz pushed a commit to branch feature/merge-hardeners-into-factories
in repository https://gitbox.apache.org/repos/asf/commons-xml.git

commit d401123fcbb419106203e1cc2a5f979da1084276
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Aug 27 21:25:10 2026 +0200

    Merge the static hardening recipes into the public factory classes
    
    Each public Hardening*Factory absorbs its package-private hardener
    (DocumentBuilderHardener and siblings): the harden methods, helpers and
    constants move onto the entry point, the hardener files are deleted, and
    the wrap methods are removed since each wrapper is now constructed from
    its own class only. On HardeningSAXParserFactory the reader and source
    variants become harden(XMLReader) and harden(Source) overloads.
    
    Assisted-By: Claude Fable 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3
---
 src/conf/spotbugs-exclude-filter.xml               |   6 -
 .../commons/xml/DocumentBuilderHardener.java       |  86 ----------
 .../commons/xml/FallbackIgnoreURIResolver.java     |   2 +-
 .../xml/HardeningDocumentBuilderFactory.java       |  54 ++++++-
 .../org/apache/commons/xml/HardeningSAXParser.java |   2 +-
 .../commons/xml/HardeningSAXParserFactory.java     | 160 ++++++++++++++++--
 .../org/apache/commons/xml/HardeningSchema.java    |   2 +-
 .../apache/commons/xml/HardeningSchemaFactory.java |  35 ++--
 .../apache/commons/xml/HardeningTransformer.java   |   4 +-
 .../commons/xml/HardeningTransformerFactory.java   |  82 ++++++----
 .../org/apache/commons/xml/HardeningValidator.java |   4 +-
 .../org/apache/commons/xml/HardeningXMLFilter.java |   2 +-
 .../commons/xml/HardeningXMLInputFactory.java      |  42 +++--
 .../org/apache/commons/xml/HardeningXPath.java     |   2 +-
 .../apache/commons/xml/HardeningXPathFactory.java  |  78 ++++++++-
 .../org/apache/commons/xml/SAXParserHardener.java  | 180 ---------------------
 .../java/org/apache/commons/xml/SaxonProvider.java |   6 +-
 .../org/apache/commons/xml/SchemaHardener.java     |  53 ------
 .../java/org/apache/commons/xml/StaxHardener.java  |  47 ------
 .../apache/commons/xml/TransformerHardener.java    |  73 ---------
 .../java/org/apache/commons/xml/XPathHardener.java | 106 ------------
 .../org/apache/commons/xml/AttackTestSupport.java  |  12 +-
 .../commons/xml/SaxonAlternateFactoryTest.java     |   2 +-
 .../apache/commons/xml/ShadingFootprintTest.java   |  89 +++++-----
 .../xml/UnsupportedXmlImplementationTest.java      |   4 +-
 .../java/org/apache/commons/xml/XIncludeTest.java  |   6 +-
 26 files changed, 433 insertions(+), 706 deletions(-)

diff --git a/src/conf/spotbugs-exclude-filter.xml 
b/src/conf/spotbugs-exclude-filter.xml
index 24038fd..5ee3d92 100644
--- a/src/conf/spotbugs-exclude-filter.xml
+++ b/src/conf/spotbugs-exclude-filter.xml
@@ -19,10 +19,4 @@
     xmlns="https://github.com/spotbugs/filter/3.0.0";
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
     xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 
https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd";>
-  <!-- Looks like an NPE: the DTD_SUBSET_ONLY lambda forwards a known-null 
entityName to the ignore-all resolver. -->
-  <Match>
-    <Class name="org.apache.commons.xml.StaxHardener" />
-    <Method name="lambda$static$0" />
-    <Bug pattern="NP_LOAD_OF_KNOWN_NULL_VALUE" />
-  </Match>
 </FindBugsFilter>
diff --git a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java 
b/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java
deleted file mode 100644
index 6e2563e..0000000
--- a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * 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;
-
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilder;
-import javax.xml.parsers.DocumentBuilderFactory;
-import javax.xml.parsers.ParserConfigurationException;
-
-import org.xml.sax.EntityResolver;
-
-/**
- * Capability-driven hardening for any {@link DocumentBuilderFactory} on the 
classpath.
- *
- * <p>Rather than branching on the implementation class, {@link 
#harden(DocumentBuilderFactory)} probes what the factory supports and 
adapts:</p>
- * <ul>
- *     <li><strong>Android</strong> (Harmony / KXmlParser): recognized by 
class name and left untouched. It exposes no {@link 
XMLConstants#FEATURE_SECURE_PROCESSING
- *         FSP}, no JAXP 1.5 {@code ACCESS_EXTERNAL_*} and no attribute API at 
all, while KXmlParser silently drops user-defined entities, so there is nothing 
to
- *         apply.</li>
- *     <li><strong>FSP</strong>: required. It switches on the implementation's 
built-in security manager, which is what carries the processing limits.</li>
- *     <li><strong>Ignore-all resolver floor</strong>: every produced {@link 
DocumentBuilder} is wrapped by a {@link HardeningDocumentBuilderFactory} that 
keeps an
- *         ignore-all {@link EntityResolver} floor. That floor blocks external 
DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's
- *         XInclude processor ignores {@code ACCESS_EXTERNAL_*} and consults 
the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} attributes 
are
- *         needed here. A caller can chain its own resolver onto the floor to 
allow-list resources, but cannot remove it.</li>
- * </ul>
- */
-final class DocumentBuilderHardener {
-
-    /** 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";
-
-    /**
-     * Hardens the given factory, returning a hardened wrapper if necessary.
-     *
-     * @param factory The factory to harden.
-     * @return A new hardened factory or the original factory, as-is, if it is 
a known Android factory.
-     * @throws HardeningException Thrown if a (non-Andoid) factory cannot 
support the secure processing feature {@link 
XMLConstants#FEATURE_SECURE_PROCESSING}.
-     */
-    static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) 
{
-        // Android exposes no FSP, ACCESS_EXTERNAL_* or attribute API, and 
KXmlParser drops user-defined entities; nothing to apply.
-        if 
(ANDROID_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
-            return factory;
-        }
-        // Required: enables the implementation's security manager, which 
carries the limits.
-        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        // Required: HardeningDocumentBuilderFactory installs an ignore-all 
EntityResolver floor on every DocumentBuilder.
-        // That floor blocks external DTD, entity, schema and xi:include 
fetches in one place: no ACCESS_EXTERNAL_* attributes are needed here.
-        // Callers can chain their resolvers, but not override the floor.
-        return HardeningDocumentBuilderFactory.wrap(factory);
-    }
-
-    /**
-     * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
-     *
-     * @param factory The factory to harden.
-     * @param feature The feature to set.
-     * @param value   The value to set.
-     * @throws HardeningException   Thrown if this factory or the {@code 
XPath}s it creates cannot support this feature.
-     * @throws NullPointerException Thrown if the {@code feature} parameter is 
null.
-     */
-    private static void setFeature(final DocumentBuilderFactory factory, final 
String feature, final boolean value) {
-        try {
-            factory.setFeature(feature, value);
-        } catch (final ParserConfigurationException e) {
-            throw HardeningException.settingFailed("feature", feature, 
factory, e);
-        }
-    }
-
-    private DocumentBuilderHardener() {
-    }
-}
diff --git 
a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java 
b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java
index 30f9eec..cf3e920 100644
--- a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java
+++ b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java
@@ -116,7 +116,7 @@ public Source resolve(final String href, final String base) 
throws TransformerEx
         final Source resolved = delegate != null ? delegate.resolve(href, 
base) : null;
         if (resolved != null) {
             // The implementation parses the opted-in handle with an internal 
reader at its own defaults; the rewrite hands it a hardened reader instead.
-            return SAXParserHardener.hardenSource(resolved);
+            return HardeningSAXParserFactory.harden(resolved);
         }
         if (HardeningException.throwOnUnresolved()) {
             throw new TransformerException(HardeningException.forbidden("uri", 
null, null, href, base));
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
index 1888f3c..1e88134 100644
--- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java
@@ -39,6 +39,41 @@
  */
 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";
+
+    /**
+     * Capability-driven hardening for any {@link DocumentBuilderFactory} on 
the classpath.
+     *
+     * <p>Rather than branching on the implementation class, this method 
probes what the factory supports and adapts:</p>
+     * <ul>
+     *     <li><strong>Android</strong> (Harmony / KXmlParser): recognized by 
class name and left untouched. It exposes no {@link 
XMLConstants#FEATURE_SECURE_PROCESSING
+     *         FSP}, no JAXP 1.5 {@code ACCESS_EXTERNAL_*} and no attribute 
API at all, while KXmlParser silently drops user-defined entities, so there is 
nothing to
+     *         apply.</li>
+     *     <li><strong>FSP</strong>: required. It switches on the 
implementation's built-in security manager, which is what carries the 
processing limits.</li>
+     *     <li><strong>Ignore-all resolver floor</strong>: every produced 
{@link DocumentBuilder} is wrapped by the nested wrapper, which keeps an
+     *         ignore-all {@link EntityResolver} floor. That floor blocks 
external DTD, entity, schema and {@code xi:include} fetches in one place: the 
stock JDK's
+     *         XInclude processor ignores {@code ACCESS_EXTERNAL_*} and 
consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} 
attributes are
+     *         needed here. A caller can chain its own resolver onto the floor 
to allow-list resources, but cannot remove it.</li>
+     * </ul>
+     *
+     * @param factory The factory to harden.
+     * @return A new hardened factory or the original factory, as-is, if it is 
a known Android factory.
+     * @throws HardeningException Thrown if a (non-Andoid) factory cannot 
support the secure processing feature {@link 
XMLConstants#FEATURE_SECURE_PROCESSING}.
+     */
+    static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) 
{
+        // Android exposes no FSP, ACCESS_EXTERNAL_* or attribute API, and 
KXmlParser drops user-defined entities; nothing to apply.
+        if 
(ANDROID_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
+            return factory;
+        }
+        // Required: enables the implementation's security manager, which 
carries the limits.
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Required: the wrapper installs an ignore-all EntityResolver floor 
on every DocumentBuilder.
+        // That floor blocks external DTD, entity, schema and xi:include 
fetches in one place: no ACCESS_EXTERNAL_* attributes are needed here.
+        // Callers can chain their resolvers, but not override the floor.
+        return new Wrapper(factory);
+    }
+
     /**
      * Returns a new, hardened {@link DocumentBuilderFactory}.
      * <p>
@@ -57,17 +92,24 @@ public final class HardeningDocumentBuilderFactory {
      *                                   implementation is not available or 
cannot be instantiated.
      */
     public static DocumentBuilderFactory newInstance() {
-        return 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+        return harden(DocumentBuilderFactory.newInstance());
     }
 
     /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
+     * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
      *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
+     * @param factory The factory to harden.
+     * @param feature The feature to set.
+     * @param value   The value to set.
+     * @throws HardeningException   Thrown if this factory or the {@code 
XPath}s it creates cannot support this feature.
+     * @throws NullPointerException Thrown if the {@code feature} parameter is 
null.
      */
-    static DocumentBuilderFactory wrap(final DocumentBuilderFactory delegate) {
-        return new Wrapper(delegate);
+    private static void setFeature(final DocumentBuilderFactory factory, final 
String feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final ParserConfigurationException e) {
+            throw HardeningException.settingFailed("feature", feature, 
factory, e);
+        }
     }
 
     private HardeningDocumentBuilderFactory() {
diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java 
b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
index 8abdc8a..162332c 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java
@@ -81,7 +81,7 @@ public Schema getSchema() {
     @Override
     public XMLReader getXMLReader() throws SAXException {
         if (hardenedReader == null) {
-            hardenedReader = 
SAXParserHardener.hardenReader(delegate.getXMLReader());
+            hardenedReader = 
HardeningSAXParserFactory.harden(delegate.getXMLReader());
         }
         return hardenedReader;
     }
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
index d45581f..27d91db 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java
@@ -19,12 +19,19 @@
 
 import java.util.Objects;
 
+import javax.xml.XMLConstants;
 import javax.xml.parsers.FactoryConfigurationError;
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.stream.StreamSource;
 import javax.xml.validation.Schema;
 
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
 import org.xml.sax.SAXNotRecognizedException;
 import org.xml.sax.SAXNotSupportedException;
@@ -41,6 +48,108 @@
  */
 public final class HardeningSAXParserFactory {
 
+    /** Class name of Android's Expat-backed {@link XMLReader}. */
+    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";
+
+    /**
+     * Capability-driven hardening for any {@link SAXParserFactory} on the 
classpath.
+     *
+     * <p>Rather than branching on the implementation class, this method 
probes what the parser supports and adapts. Because
+     * {@link SAXParserFactory} exposes only a feature API and no property 
API, the per-parse configuration runs on each {@link XMLReader} the factory 
produces,
+     * funnelled through the nested wrapper into {@link 
#harden(XMLReader)}:</p>
+     * <ul>
+     *     <li><strong>Android</strong> (Harmony / Expat): {@link 
XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code 
ACCESS_EXTERNAL_*} properties
+     *         are not recognized, and libexpat enforces its own Billion 
Laughs check, so neither is applied. Two fixups are still needed: an ignore-all 
resolver
+     *         (Expat ignores external fetches silently when no resolver is 
set; the floor keeps that behavior non-bypassable, resolving anything 
unresolved to
+     *         empty), and a {@link HardeningExpatXMLReader} so the 
unsupported {@code namespace-prefixes} feature is rejected at
+     *         configuration time rather than mid-parse.</li>
+     *     <li><strong>FSP</strong>: required on every other reader. It 
switches on the implementation's built-in security manager, which is what 
carries the
+     *         processing limits.</li>
+     *     <li><strong>Ignore-all resolver floor</strong>: every reader is 
wrapped in a {@link HardeningXMLReader} that keeps an ignore-all {@link 
EntityResolver} floor.
+     *         That floor blocks external DTD, entity, schema and {@code 
xi:include} fetches in one place: the stock JDK's XInclude processor ignores
+     *         {@code ACCESS_EXTERNAL_*} and consults the {@link 
EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} properties are needed 
here. A caller can
+     *         chain its own resolver onto the floor to allow-list resources, 
but cannot remove it.</li>
+     * </ul>
+     *
+     * @param factory the factory to harden; never {@code null}.
+     * @return a hardened factory.
+     */
+    static SAXParserFactory harden(final SAXParserFactory factory) {
+        // Required: enables the implementation's security manager, which 
carries the limits. Android's Expat rejects FSP, so it is skipped there.
+        if (!ANDROID_SAX_PARSER_FACTORY.equals(factory.getClass().getName())) {
+            setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        }
+        // The per-parse hardening (limits, entity blocking, Android fixups) 
lives in harden(XMLReader) because SAXParserFactory has no property API.
+        return new Wrapper(factory);
+    }
+
+    /**
+     * Rewrites a {@link Source} so that any SAX parsing it triggers runs 
through a hardened {@link XMLReader}.
+     * <p>
+     * Only a {@link StreamSource} or a {@link SAXSource} without a reader is 
enriched with a hardened, namespace-aware reader; other source kinds are 
returned
+     * as-is. Used by the TrAX and schema wrappers to route every source they 
parse through the SAX hardening path.
+     * </p>
+     *
+     * @param source the source to harden; never {@code null}.
+     * @return a hardened source.
+     * @throws TransformerConfigurationException if a hardened reader cannot 
be obtained.
+     * @throws FactoryConfigurationError         Thrown from a factory in case 
of a {@link java.util.ServiceConfigurationError service
+     *                                           configuration error} or if 
the implementation is not available or cannot be instantiated.
+     */
+    static Source harden(final Source source) throws 
TransformerConfigurationException {
+        if (source instanceof StreamSource || source instanceof SAXSource && 
((SAXSource) source).getXMLReader() == null) {
+            final InputSource inputSource = 
SAXSource.sourceToInputSource(source);
+            return inputSource == null ? source : new 
SAXSource(newHardenedReader(), inputSource);
+        }
+        return source;
+    }
+
+    /**
+     * Hardens an existing {@link XMLReader}.
+     *
+     * @param reader The reader to harden; never {@code null}.
+     * @return A hardened reader.
+     * @throws IllegalStateException if a required hardening setting cannot be 
applied to the underlying implementation.
+     */
+    static XMLReader harden(final XMLReader reader) {
+        if (reader instanceof HardeningXMLReader) {
+            // Already hardened (for example, a reader from a hardened factory 
passed back through harden(XMLReader)); the floor is already in place.
+            return reader;
+        }
+        if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) {
+            // Expat ignores external fetches when no resolver is set; the 
ignore-all floor keeps that behavior non-bypassable (routing a caller-set 
resolver,
+            // including SAXParser.parse's handler, through it and resolving 
anything unresolved to empty) and, via HardeningExpatXMLReader, rejects the
+            // unsupported namespace-prefixes feature eagerly rather than 
mid-parse.
+            return new HardeningExpatXMLReader(reader);
+        }
+        // Required: enables the JDK XMLSecurityManager / Xerces 
SecurityManager limits.
+        setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Required: HardeningXMLReader installs an ignore-all EntityResolver 
floor on the reader.
+        // That floor blocks external DTD, entity, schema and xi:include 
fetches in one place: no ACCESS_EXTERNAL_* properties are needed here.
+        // Callers can chain their resolvers, but not override the floor.
+        return new HardeningXMLReader(reader);
+    }
+
+    /**
+     * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX 
wrappers to parse sources with.
+     *
+     * @return a hardened reader.
+     * @throws TransformerConfigurationException if a hardened reader cannot 
be obtained.
+     * @throws FactoryConfigurationError Thrown from a factory in case of a 
{@link java.util.ServiceConfigurationError service
+     *                                   configuration error} or if the 
implementation is not available or cannot be instantiated.
+     */
+    static XMLReader newHardenedReader() throws 
TransformerConfigurationException {
+        try {
+            final SAXParserFactory factory = 
harden(SAXParserFactory.newInstance());
+            factory.setNamespaceAware(true);
+            return factory.newSAXParser().getXMLReader();
+        } catch (final ParserConfigurationException | SAXException e) {
+            throw new TransformerConfigurationException("Failed to obtain a 
hardened XMLReader for source parsing", e);
+        }
+    }
+
     /**
      * Returns a new, hardened {@link SAXParserFactory}.
      * <p>
@@ -56,17 +165,23 @@ public final class HardeningSAXParserFactory {
      *                                   error} or if the implementation is 
not available or cannot be instantiated.
      */
     public static SAXParserFactory newInstance() {
-        return SAXParserHardener.harden(SAXParserFactory.newInstance());
+        return harden(SAXParserFactory.newInstance());
     }
 
-    /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
-     *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
-     */
-    static SAXParserFactory wrap(final SAXParserFactory delegate) {
-        return new Wrapper(delegate);
+    private static void setFeature(final SAXParserFactory factory, final 
String feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final Exception e) {
+            throw HardeningException.settingFailed("feature", feature, 
factory, e);
+        }
+    }
+
+    private static void setFeature(final XMLReader reader, final String 
feature, final boolean value) {
+        try {
+            reader.setFeature(feature, value);
+        } catch (final Exception e) {
+            throw HardeningException.settingFailed("feature", feature, reader, 
e);
+        }
     }
 
     private HardeningSAXParserFactory() {
@@ -74,7 +189,32 @@ private HardeningSAXParserFactory() {
     }
 
     /**
-     * Universal SAX factory wrapper that funnels every produced parser 
through {@link SAXParserHardener#hardenReader(XMLReader)}.
+     * {@link HardeningXMLReader} for Android's {@code 
org.apache.harmony.xml.ExpatReader} that additionally surfaces its {@code 
namespace-prefixes} limitation at
+     * configuration time.
+     *
+     * <p>ExpatReader does not actually support the {@code namespace-prefixes} 
feature: enabling it is accepted by {@code setFeature} but fails later, during
+     * {@code parse}, with a {@link SAXNotSupportedException}. Reporting the 
rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that 
probe
+     * the feature, such as Xalan's identity transformer, catch the exception 
and fall back instead of failing the whole parse.</p>
+     */
+    static final class HardeningExpatXMLReader extends HardeningXMLReader {
+
+        private static final String NAMESPACE_PREFIXES_FEATURE = 
"http://xml.org/sax/features/namespace-prefixes";;
+
+        HardeningExpatXMLReader(final XMLReader delegate) {
+            super(delegate);
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+            if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) {
+                throw new SAXNotSupportedException("ExpatReader does not 
support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature");
+            }
+            super.setFeature(name, value);
+        }
+    }
+
+    /**
+     * Universal SAX factory wrapper that funnels every produced parser 
through {@link HardeningSAXParserFactory#harden(XMLReader)}.
      * <p>
      * {@link SAXParserFactory} exposes only a feature API and no property 
API, so the per-parse hardening (limits, entity blocking, 
implementation-specific fixups)
      * has to run on each {@link XMLReader} the factory produces. This wrapper 
returns a {@link HardeningSAXParser}, which applies that hardening lazily to 
both the
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchema.java 
b/src/main/java/org/apache/commons/xml/HardeningSchema.java
index fe2166d..e92374c 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchema.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchema.java
@@ -25,7 +25,7 @@
 
 /**
  * {@link Schema} wrapper that hardens every {@link Validator} and {@link 
ValidatorHandler} the inner Schema produces: each {@link Validator} is wrapped 
in
- * {@link HardeningValidator} (which rewrites the Source through {@link 
SAXParserHardener#hardenSource(javax.xml.transform.Source)} and installs the 
resolver
+ * {@link HardeningValidator} (which rewrites the Source through {@link 
HardeningSAXParserFactory#harden(javax.xml.transform.Source)} and installs the 
resolver
  * floor), and each {@link ValidatorHandler} is wrapped in a {@link 
HardeningValidatorHandler} that keeps the same ignore-all resolver floor so
  * {@code xsi:schemaLocation} is not resolved during SAX-driven validation.
  */
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
index 2f2f9dc..a123b4a 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
@@ -44,6 +44,21 @@
  */
 public final class HardeningSchemaFactory {
 
+    /**
+     * Hardening for any {@link SchemaFactory} on the classpath.
+     *
+     * <p>Unlike the other factory types there is no per-implementation 
branching and no feature or limit configuration on the factory itself: schema 
compilation
+     * and validation reach external resources only through the resolver hook, 
so wrapping the factory with a non-removable ignore-all resolver floor is 
enough on
+     * every implementation. The reader used to parse schema and instance 
documents is hardened separately, through
+     * {@link 
HardeningSAXParserFactory#harden(javax.xml.transform.Source)}.</p>
+     *
+     * @param factory the factory to harden; never {@code null}.
+     * @return a hardened factory.
+     */
+    static SchemaFactory harden(final SchemaFactory factory) {
+        return new Wrapper(factory);
+    }
+
     /**
      * Returns a new, hardened {@link SchemaFactory} for the given schema 
language.
      * <p>
@@ -65,17 +80,7 @@ public final class HardeningSchemaFactory {
      * @throws SchemaFactoryConfigurationError Thrown if a configuration error 
is encountered.
      */
     public static SchemaFactory newInstance(final String schemaLanguage) {
-        return new Wrapper(SchemaFactory.newInstance(schemaLanguage));
-    }
-
-    /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
-     *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
-     */
-    static SchemaFactory wrap(final SchemaFactory delegate) {
-        return new Wrapper(delegate);
+        return harden(SchemaFactory.newInstance(schemaLanguage));
     }
 
     private HardeningSchemaFactory() {
@@ -91,14 +96,14 @@ private HardeningSchemaFactory() {
      * <ol>
      *   <li>{@link HardeningSchemaFactory} installs an ignore-all {@link 
FallbackIgnoreLSResourceResolver} floor on the factory (blocking
      *       {@code xs:import}/{@code xs:include}/{@code xs:redefine} at 
compile time) and rewrites the Source on every {@code newSchema(Source[])} 
entry point
-     *       through {@link SAXParserHardener#hardenSource(Source)}.</li>
+     *       through {@link HardeningSAXParserFactory#harden(Source)}.</li>
      *   <li>{@link HardeningSchema} wraps every Validator/ValidatorHandler 
the inner Schema produces and re-installs the floor on each (blocking
      *       {@code xsi:schemaLocation} at validation time), since neither the 
JDK nor Xerces reliably propagates it through {@code Schema}.</li>
      *   <li>{@link HardeningValidator} rewrites the Source on every {@link 
Validator#validate(Source)} call.</li>
      * </ol>
      *
      * <p>
-     * The hardened reader supplied by {@link 
SAXParserHardener#hardenSource(Source)} already carries {@code 
FEATURE_SECURE_PROCESSING} and the processing limits, so a
+     * The hardened reader supplied by {@link 
HardeningSAXParserFactory#harden(Source)} already carries {@code 
FEATURE_SECURE_PROCESSING} and the processing limits, so a
      * DOCTYPE, external entity or Billion Laughs payload in the schema or 
instance document is bounded there rather than on this factory. The JAXP 1.5
      * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the 
resolver floor already blocks the same fetches on every implementation, and the 
JDK 8
      * {@code SchemaFactory} has a bug whereby those properties keep blocking 
even when a caller's own resolver would grant the access. The floor is a 
non-removable
@@ -111,7 +116,7 @@ private HardeningSchemaFactory() {
     private static final class Wrapper extends SchemaFactory {
 
         /**
-         * Hardens every schema source through {@link 
SAXParserHardener#hardenSource(Source)}.
+         * Hardens every schema source through {@link 
HardeningSAXParserFactory#harden(Source)}.
          *
          * @param schemas the schema sources to harden; must not be {@code 
null}.
          * @return a new array of hardened sources.
@@ -123,7 +128,7 @@ private static Source[] harden(final Source[] schemas) 
throws SAXException {
             final Source[] hardened = new Source[schemas.length];
             try {
                 for (int i = 0; i < schemas.length; i++) {
-                    hardened[i] = SAXParserHardener.hardenSource(schemas[i]);
+                    hardened[i] = HardeningSAXParserFactory.harden(schemas[i]);
                 }
             } catch (final TransformerConfigurationException e) {
                 throw new SAXException("Failed to harden schema source", e);
diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformer.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
index c0ab998..39669d6 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformer.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
@@ -32,7 +32,7 @@
 
 /**
  * {@link Transformer} wrapper that rewrites the Source on every {@link 
Transformer#transform(Source, Result)} call through
- * {@link SAXParserHardener#hardenSource(Source)} before delegating, and keeps 
an ignore-all {@link URIResolver} floor so runtime {@code document()} calls a
+ * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and 
keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} 
calls a
  * caller does not resolve return empty rather than being fetched.
  * <p>
  * The floor is installed on the delegate transformer at construction, seeded 
with the factory's compile-time resolver; {@link #setURIResolver(URIResolver)}
@@ -137,7 +137,7 @@ public void setURIResolver(final URIResolver resolver) {
     @Override
     public void transform(final Source xmlSource, final Result outputTarget) 
throws TransformerException {
         try {
-            delegate.transform(SAXParserHardener.hardenSource(xmlSource), 
outputTarget);
+            delegate.transform(HardeningSAXParserFactory.harden(xmlSource), 
outputTarget);
         } catch (final TransformerConfigurationException e) {
             throw new TransformerException(e);
         }
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
index 1a73f58..172c988 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
@@ -56,6 +56,43 @@
  */
 public final class HardeningTransformerFactory {
 
+    /**
+     * Capability-driven hardening for any {@link TransformerFactory} on the 
classpath.
+     *
+     * <p>Rather than branching on the implementation class, this method 
probes what the factory supports and adapts:</p>
+     * <ul>
+     *     <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by 
package prefix and handed to {@link 
SaxonProvider#configure(TransformerFactory)} for the
+     *         channels the standard JAXP knobs cannot close (reflection-based 
extension functions, the collection finder, the internal SAX parser). It is then
+     *         wrapped like every other implementation to install the {@link 
FallbackIgnoreURIResolver} floor; the only
+     *         difference is the empty-{@link Source} shape the floor returns, 
{@code EmptySource} for Saxon rather than the default empty DOM document.</li>
+     *     <li><strong>FSP</strong> ({@link 
XMLConstants#FEATURE_SECURE_PROCESSING}): required. On XSLTC it enables the 
runtime evaluator limits; on Xalan it disables
+     *         reflection-based extension functions.</li>
+     *     <li><strong>{@link FallbackIgnoreURIResolver} floor</strong>: 
required. An ignore-all {@link URIResolver} floor, installed by
+     *         the nested wrapper and carried onto every produced {@link 
Transformer}, resolves {@code xsl:import}/{@code xsl:include} at compile
+     *         time and {@code document()} at runtime to an empty document, 
the one channel both XSLTC and Xalan route through. A caller-set {@link 
URIResolver} is
+     *         routed through the floor rather than replacing it, so a caller 
can opt a specific URI in but cannot reopen the fetch.</li>
+     *     <li><strong>The nested wrapper</strong>: required. Both 
implementations fall back to {@code SAXParserFactory.newInstance()} to parse a
+     *         stylesheet or source document that does not carry its own 
reader, and only set FSP on it; wrapping the factory rewrites every {@link 
Source} through an
+     *         {@link org.apache.commons.xml}-hardened reader instead.</li>
+     * </ul>
+     *
+     * @param factory the factory to harden; never {@code null}.
+     * @return a hardened factory.
+     */
+    static TransformerFactory harden(final TransformerFactory factory) {
+        // Required: enables secure processing (XSLTC runtime limits; Xalan's 
extension-function block).
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        if (SaxonProvider.isSaxon(factory.getClass())) {
+            // Saxon keeps its vendor Configuration for the channels JAXP 
cannot close,
+            // then goes through the same wrapper as every other 
implementation for the URIResolver floor;
+            // EmptySource is the empty-source shape Saxon's consumers expect.
+            return new Wrapper((SAXTransformerFactory) 
SaxonProvider.configure(factory), SaxonProvider.emptySourceSupplier());
+        }
+        // Required: source/stylesheet parsing provisions its own SAX reader 
otherwise; the wrapper routes every Source through a hardened one and installs 
the
+        // ignore-all URIResolver floor (blocking xsl:import/include at 
compile time and document() at runtime) that a caller-set resolver cannot 
remove.
+        return new Wrapper((SAXTransformerFactory) factory);
+    }
+
     /**
      * Returns a new, hardened {@link TransformerFactory}.
      * <p>
@@ -80,28 +117,15 @@ public final class HardeningTransformerFactory {
      * @throws IllegalStateException if a required hardening setting cannot be 
applied to the underlying implementation.
      */
     public static TransformerFactory newInstance() {
-        return TransformerHardener.harden(TransformerFactory.newInstance());
+        return harden(TransformerFactory.newInstance());
     }
 
-    /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
-     *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
-     */
-    static TransformerFactory wrap(final SAXTransformerFactory delegate) {
-        return new Wrapper(delegate);
-    }
-
-    /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
-     *
-     * @param delegate    the delegate to wrap; must not be {@code null}.
-     * @param emptySource supplies the empty document a denied fetch resolves 
to; may be {@code null} for the default empty DOM document.
-     * @return The hardened factory.
-     */
-    static TransformerFactory wrap(final SAXTransformerFactory delegate, final 
Supplier<Source> emptySource) {
-        return new Wrapper(delegate, emptySource);
+    private static void setFeature(final TransformerFactory factory, final 
String feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final Exception e) {
+            throw HardeningException.settingFailed("feature", feature, 
factory, e);
+        }
     }
 
     private HardeningTransformerFactory() {
@@ -109,7 +133,7 @@ private HardeningTransformerFactory() {
     }
 
     /**
-     * {@link javax.xml.transform.TransformerFactory} wrapper that rewrites 
every Source-taking entry point through {@link 
SAXParserHardener#hardenSource(Source)} before
+     * {@link TransformerFactory} wrapper that rewrites every Source-taking 
entry point through {@link HardeningSAXParserFactory#harden(Source)} before
      * delegating.
      *
      * <p>Used by providers whose underlying TrAX implementation pulls a new 
{@code SAXParserFactory.newInstance()} for any Source that is not already a
@@ -144,10 +168,10 @@ private static final class Wrapper extends 
SAXTransformerFactory {
         /**
          * Parses a reader-less source into a DOM through a hardened, 
namespace-aware {@link javax.xml.parsers.DocumentBuilder} and returns a {@link 
DOMSource}
          * carrying its system id, so the consumer walks the tree instead of 
provisioning its own reader. Any other source is left to
-         * {@link SAXParserHardener#hardenSource(Source)}.
+         * {@link HardeningSAXParserFactory#harden(Source)}.
          *
          * @param source The source to scan for an associated stylesheet.
-         * @return A {@link DOMSource} for a reader-less source, otherwise the 
result of {@link SAXParserHardener#hardenSource(Source)}.
+         * @return A {@link DOMSource} for a reader-less source, otherwise the 
result of {@link HardeningSAXParserFactory#harden(Source)}.
          * @throws TransformerConfigurationException if the source cannot be 
parsed.
          * @throws FactoryConfigurationError Thrown from a factory in case of 
a {@link java.util.ServiceConfigurationError service
          *                                   configuration error} or if the 
implementation is not available or cannot be instantiated.
@@ -158,7 +182,7 @@ private static Source hardenSourceToDom(final Source 
source) throws TransformerC
                 final InputSource inputSource = 
SAXSource.sourceToInputSource(source);
                 if (inputSource != null) {
                     try {
-                        final DocumentBuilderFactory factory = 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+                        final DocumentBuilderFactory factory = 
HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance());
                         factory.setNamespaceAware(true);
                         final Document document = 
factory.newDocumentBuilder().parse(inputSource);
                         return new DOMSource(document, 
inputSource.getSystemId());
@@ -167,7 +191,7 @@ private static Source hardenSourceToDom(final Source 
source) throws TransformerC
                     }
                 }
             }
-            return SAXParserHardener.hardenSource(source);
+            return HardeningSAXParserFactory.harden(source);
         }
 
         /**
@@ -229,7 +253,7 @@ private Wrapper(final SAXTransformerFactory delegate, final 
Supplier<Source> emp
         public Source getAssociatedStylesheet(final Source source, final 
String media, final String title, final String charset)
                 throws TransformerConfigurationException {
             // Xalan's getAssociatedStylesheet drops a SAXSource's reader and 
self-provisions its own to scan for xml-stylesheet PIs (XALANJ-2849).
-            final Source hardened = isXalan(delegate) ? 
hardenSourceToDom(source) : SAXParserHardener.hardenSource(source);
+            final Source hardened = isXalan(delegate) ? 
hardenSourceToDom(source) : HardeningSAXParserFactory.harden(source);
             return delegate.getAssociatedStylesheet(hardened, media, title, 
charset);
         }
 
@@ -265,7 +289,7 @@ private TransformerHandler hardenHandler(final 
TransformerHandler handler) {
          */
         @Override
         public Templates newTemplates(final Source source) throws 
TransformerConfigurationException {
-            final Templates templates = 
delegate.newTemplates(SAXParserHardener.hardenSource(source));
+            final Templates templates = 
delegate.newTemplates(HardeningSAXParserFactory.harden(source));
             return templates == null ? null : new 
HardeningTemplates(templates, getURIResolver(), emptySource);
         }
 
@@ -290,7 +314,7 @@ public Transformer newTransformer() throws 
TransformerConfigurationException {
          */
         @Override
         public Transformer newTransformer(final Source source) throws 
TransformerConfigurationException {
-            final Transformer transformer = 
delegate.newTransformer(SAXParserHardener.hardenSource(source));
+            final Transformer transformer = 
delegate.newTransformer(HardeningSAXParserFactory.harden(source));
             return transformer == null ? null : new 
HardeningTransformer(transformer, getURIResolver(), emptySource);
         }
 
@@ -307,7 +331,7 @@ public TransformerHandler newTransformerHandler() throws 
TransformerConfiguratio
          */
         @Override
         public TransformerHandler newTransformerHandler(final Source source) 
throws TransformerConfigurationException {
-            return 
hardenHandler(delegate.newTransformerHandler(SAXParserHardener.hardenSource(source)));
+            return 
hardenHandler(delegate.newTransformerHandler(HardeningSAXParserFactory.harden(source)));
         }
 
         @Override
diff --git a/src/main/java/org/apache/commons/xml/HardeningValidator.java 
b/src/main/java/org/apache/commons/xml/HardeningValidator.java
index 6c61c7b..92d06fc 100644
--- a/src/main/java/org/apache/commons/xml/HardeningValidator.java
+++ b/src/main/java/org/apache/commons/xml/HardeningValidator.java
@@ -34,7 +34,7 @@
 
 /**
  * {@link Validator} wrapper that rewrites the Source on every {@link 
Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call 
through
- * {@link SAXParserHardener#hardenSource(Source)} before delegating, and keeps 
an ignore-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is 
not resolved at
+ * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and 
keeps an ignore-all {@link LSResourceResolver} floor so {@code 
xsi:schemaLocation} is not resolved at
  * validation time. {@link #reset()} re-establishes the bare ignore-all floor, 
matching the just-constructed state.
  */
 final class HardeningValidator extends Validator {
@@ -113,7 +113,7 @@ public void setResourceResolver(final LSResourceResolver 
resourceResolver) {
     @Override
     public void validate(final Source source, final Result result) throws 
SAXException, IOException {
         try {
-            delegate.validate(SAXParserHardener.hardenSource(source), result);
+            delegate.validate(HardeningSAXParserFactory.harden(source), 
result);
         } catch (final TransformerConfigurationException e) {
             throw new SAXException("Failed to harden source for validation", 
e);
         }
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java
index a8ad74b..1a8533e 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java
@@ -70,7 +70,7 @@ public void parse(final InputSource input) throws 
SAXException, IOException {
         }
         if (getParent() == null) {
             try {
-                setParent(SAXParserHardener.newHardenedReader());
+                setParent(HardeningSAXParserFactory.newHardenedReader());
             } catch (final TransformerException e) {
                 throw new SAXException(e);
             }
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
index 5e9bc0e..37ed393 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
@@ -44,6 +44,28 @@
  */
 public final class HardeningXMLInputFactory {
 
+    /** Woodstox property: resolver consulted for the external DTD subset. */
+    static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
+    /** Woodstox property: resolver consulted for declared external general 
entities. */
+    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";
+
+    /**
+     * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on 
the classpath.
+     *
+     * <p>One recipe covers both the JDK Zephyr and Woodstox: the wrapper 
installs a non-removable {@link FallbackIgnoreXMLResolver} floor on
+     * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} 
/ {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see the wrapper's
+     * Javadoc for the per-implementation hook routing.</p>
+     *
+     * @param factory the factory to harden; never {@code null}.
+     * @return a hardened factory.
+     */
+    static XMLInputFactory harden(final XMLInputFactory factory) {
+        // The wrapper installs the non-removable ignore-all resolver floor 
that resolves every external DTD and entity to empty content.
+        return new Wrapper(factory);
+    }
+
     /**
      * Returns a new, hardened {@link XMLInputFactory}.
      * <p>
@@ -55,17 +77,7 @@ public final class HardeningXMLInputFactory {
      * @throws FactoryConfigurationError Thrown if an instance of this factory 
cannot be loaded.
      */
     public static XMLInputFactory newInstance() {
-        return StaxHardener.harden(XMLInputFactory.newInstance());
-    }
-
-    /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
-     *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
-     */
-    static XMLInputFactory wrap(final XMLInputFactory delegate) {
-        return new Wrapper(delegate);
+        return harden(XMLInputFactory.newInstance());
     }
 
     private HardeningXMLInputFactory() {
@@ -77,7 +89,7 @@ private HardeningXMLInputFactory() {
      * non-removable by the caller.
      *
      * <p>The constructor installs the floor through {@code setXMLResolver}, 
which every implementation routes external resolution through (Woodstox fans it 
out to
-     * both its DTD-subset and entity resolvers). Woodstox keeps one hook 
outside that fan-out, {@value StaxHardener#WSTX_UNDECLARED_ENTITY_RESOLVER}, 
which is
+     * both its DTD-subset and entity resolvers). Woodstox keeps one hook 
outside that fan-out, {@value 
HardeningXMLInputFactory#WSTX_UNDECLARED_ENTITY_RESOLVER}, which is
      * deliberately left empty: emptying the external subset leaves any entity 
it declared undeclared, and Woodstox then rejects the reference. The rejection 
is
      * implementation-prescribed and keeps the resource just as unfetched as 
the empty resolution the other implementations produce; a caller who wants those
      * references resolved can still set the property, and their resolver 
lands behind a floor like on every other resolver hook.</p>
@@ -96,9 +108,9 @@ private static final class Wrapper extends XMLInputFactory {
 
         private static boolean isResolverProperty(final String name) {
             return XMLInputFactory.RESOLVER.equals(name)
-                    || StaxHardener.WSTX_DTD_RESOLVER.equals(name)
-                    || StaxHardener.WSTX_ENTITY_RESOLVER.equals(name)
-                    || 
StaxHardener.WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name);
+                    || WSTX_DTD_RESOLVER.equals(name)
+                    || WSTX_ENTITY_RESOLVER.equals(name)
+                    || WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name);
         }
 
 
diff --git a/src/main/java/org/apache/commons/xml/HardeningXPath.java 
b/src/main/java/org/apache/commons/xml/HardeningXPath.java
index 4825629..abac0b7 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXPath.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXPath.java
@@ -66,7 +66,7 @@ final class HardeningXPath implements XPath {
     static Document parse(final InputSource source) throws 
XPathExpressionException {
         Objects.requireNonNull(source, "source");
         try {
-            final DocumentBuilderFactory factory = 
DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance());
+            final DocumentBuilderFactory factory = 
HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance());
             factory.setNamespaceAware(true);
             return factory.newDocumentBuilder().parse(source);
         } catch (final ParserConfigurationException | SAXException | 
IOException e) {
diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
index 460efad..eb33faf 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java
@@ -19,6 +19,7 @@
 
 import java.util.Objects;
 
+import javax.xml.XMLConstants;
 import javax.xml.xpath.XPath;
 import javax.xml.xpath.XPathFactory;
 import javax.xml.xpath.XPathFactoryConfigurationException;
@@ -52,19 +53,84 @@ public final class HardeningXPathFactory {
      * @throws RuntimeException      Thrown if there is a failure in creating 
an {@link XPathFactory} for the default object model.
      */
     public static XPathFactory newInstance() {
-        return XPathHardener.harden(XPathFactory.newInstance());
+        return harden(XPathFactory.newInstance());
     }
 
     /**
-     * Wraps a prepared delegate in the hardening wrapper; called by the 
hardener once the required settings are applied.
+     * Capability-driven hardening for any {@link XPathFactory} on the 
classpath.
      *
-     * @param delegate the delegate to wrap; must not be {@code null}.
-     * @return The hardened factory.
+     * <p>The XPath object model mirrors TrAX: the stock JDK and Apache Xalan 
ship an XPath 1.0 engine with no URI-fetching functions, while Saxon adds the 
XPath 3.1
+     * {@code fn:doc}, {@code fn:collection} and {@code fn:unparsed-text} 
functions that can reach external resources. Rather than branching on the 
implementation
+     * class, this method probes what the factory supports and adapts:</p>
+     * <ul>
+     *     <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by 
package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so 
any public
+     *         subclass routes to the same recipe as the registered factory. 
Its URI-fetching
+     *         functions and reflection-based extension calls are reachable 
only through a locked-down Saxon {@code Configuration}, not the standard JAXP 
knobs; this
+     *         is the XPath counterpart of the Saxon exception in {@link 
HardeningTransformerFactory#harden(javax.xml.transform.TransformerFactory)}, 
kept as a
+     *         documented package-prefix exception because the required 
hardening surface is reachable only through a vendor API.</li>
+     *     <li><strong>FODP</strong> ({@code jdk.xml.overrideDefaultParser}, 
set to {@code false}): best-effort. On the stock JDK it pins the internal 
parser lookup to
+     *         the bundled SAX parser, blocking a system property swap to a 
third-party parser (defense-in-depth); Xalan rejects the feature and is left 
unchanged.</li>
+     *     <li><strong>FSP</strong> ({@link 
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only 
knob both the stock JDK and Xalan XPath
+     *         engines expose, and switches on their secure-processing limits. 
{@link XPathFactory} has no attribute API for finer control.</li>
+     *     <li><strong>The nested wrapper</strong>: required. FSP governs only 
the engine, not the parser it provisions internally for the
+     *         {@link org.xml.sax.InputSource}-taking {@code evaluate} entry 
points; the wrapper performs that document build with a hardened parser 
instead, so
+     *         the engine never parses.</li>
+     * </ul>
+     *
+     * @param factory The factory to harden.
+     * @return A new hardened factory or the original factory, hardened, if it 
is a known Saxon factory.
+     * @throws HardeningException Thrown if this {@link XPathFactory} or the 
{@code XPath}s it creates cannot support this feature.
      */
-    static XPathFactory wrap(final XPathFactory delegate) {
-        return new Wrapper(delegate);
+    static XPathFactory harden(final XPathFactory factory) {
+        if (SaxonProvider.isSaxon(factory.getClass())) {
+            // Saxon: only a locked-down Configuration can close its 
URI-fetching functions and extension-function surface.
+            return SaxonProvider.configure(factory);
+        }
+        // Best-effort: the stock JDK pins its bundled SAX parser 
(defense-in-depth); Xalan rejects the feature.
+        setOptionalFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false);
+        // Required: enables the engine's secure-processing limits; 
XPathFactory has no attribute API for finer control.
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Required: FSP does not reach the parser the engine provisions for 
InputSource-taking evaluate calls; the wrapper parses those itself.
+        return new Wrapper(factory);
     }
 
+    /**
+     * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
+     *
+     * @param factory The factory to harden.
+     * @param feature The feature to set.
+     * @param value   The value to set.
+     * @throws HardeningException Thrown if this {@link XPathFactory} or the 
{@code XPath}s it creates cannot support this feature or if {@code feature} is
+     *                            {@code null}.
+     */
+    private static void setFeature(final XPathFactory factory, final String 
feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final XPathFactoryConfigurationException e) {
+            throw HardeningException.settingFailed("feature", feature, 
factory, e);
+        }
+    }
+
+    /**
+     * Sets an optional feature on the given factory, ignoring it if the 
implementation does not recognize it.
+     *
+     * @param factory The factory to harden.
+     * @param feature The feature to set.
+     * @param value   The value to set.
+     */
+    private static void setOptionalFeature(final XPathFactory factory, final 
String feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final XPathFactoryConfigurationException e) {
+            // Ignored: the implementation does not recognize this optional 
feature.
+        }
+    }
+
+    /**
+     * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX 
parser; defense-in-depth against a system property swap to a third-party parser.
+     */
+    private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = 
"jdk.xml.overrideDefaultParser";
+
     private HardeningXPathFactory() {
         // static only
     }
diff --git a/src/main/java/org/apache/commons/xml/SAXParserHardener.java 
b/src/main/java/org/apache/commons/xml/SAXParserHardener.java
deleted file mode 100644
index f1c7760..0000000
--- a/src/main/java/org/apache/commons/xml/SAXParserHardener.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * 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;
-
-import javax.xml.XMLConstants;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParserFactory;
-import javax.xml.transform.Source;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.sax.SAXSource;
-import javax.xml.transform.stream.StreamSource;
-
-import org.xml.sax.EntityResolver;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-import org.xml.sax.SAXNotRecognizedException;
-import org.xml.sax.SAXNotSupportedException;
-import org.xml.sax.XMLReader;
-
-/**
- * Capability-driven hardening for any {@link SAXParserFactory} on the 
classpath.
- *
- * <p>Rather than branching on the implementation class, {@link 
#harden(SAXParserFactory)} probes what the parser supports and adapts. Because
- * {@link SAXParserFactory} exposes only a feature API and no property API, 
the per-parse configuration runs on each {@link XMLReader} the factory produces,
- * funnelled through {@link HardeningSAXParserFactory} into {@link 
#hardenReader(XMLReader)}:</p>
- * <ul>
- *     <li><strong>Android</strong> (Harmony / Expat): {@link 
XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code 
ACCESS_EXTERNAL_*} properties
- *         are not recognized, and libexpat enforces its own Billion Laughs 
check, so neither is applied. Two fixups are still needed: an ignore-all 
resolver
- *         (Expat ignores external fetches silently when no resolver is set; 
the floor keeps that behavior non-bypassable, resolving anything unresolved to
- *         empty), and a {@link HardeningExpatXMLReader} so the unsupported 
{@code namespace-prefixes} feature is rejected at
- *         configuration time rather than mid-parse.</li>
- *     <li><strong>FSP</strong>: required on every other reader. It switches 
on the implementation's built-in security manager, which is what carries the
- *         processing limits.</li>
- *     <li><strong>Ignore-all resolver floor</strong>: every reader is wrapped 
in a {@link HardeningXMLReader} that keeps an ignore-all {@link EntityResolver} 
floor.
- *         That floor blocks external DTD, entity, schema and {@code 
xi:include} fetches in one place: the stock JDK's XInclude processor ignores
- *         {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} 
instead, so no {@code ACCESS_EXTERNAL_*} properties are needed here. A caller 
can
- *         chain its own resolver onto the floor to allow-list resources, but 
cannot remove it.</li>
- * </ul>
- */
-final class SAXParserHardener {
-
-    /**
-     * {@link HardeningXMLReader} for Android's {@code 
org.apache.harmony.xml.ExpatReader} that additionally surfaces its {@code 
namespace-prefixes} limitation at
-     * configuration time.
-     *
-     * <p>ExpatReader does not actually support the {@code namespace-prefixes} 
feature: enabling it is accepted by {@code setFeature} but fails later, during
-     * {@code parse}, with a {@link SAXNotSupportedException}. Reporting the 
rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that 
probe
-     * the feature, such as Xalan's identity transformer, catch the exception 
and fall back instead of failing the whole parse.</p>
-     */
-    static final class HardeningExpatXMLReader extends HardeningXMLReader {
-
-        private static final String NAMESPACE_PREFIXES_FEATURE = 
"http://xml.org/sax/features/namespace-prefixes";;
-
-        HardeningExpatXMLReader(final XMLReader delegate) {
-            super(delegate);
-        }
-
-        @Override
-        public void setFeature(final String name, final boolean value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
-            if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) {
-                throw new SAXNotSupportedException("ExpatReader does not 
support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature");
-            }
-            super.setFeature(name, value);
-        }
-    }
-
-    /** 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 Android's Expat-backed {@link XMLReader}. */
-    private static final String ANDROID_EXPAT_READER = 
"org.apache.harmony.xml.ExpatReader";
-
-    static SAXParserFactory harden(final SAXParserFactory factory) {
-        // Required: enables the implementation's security manager, which 
carries the limits. Android's Expat rejects FSP, so it is skipped there.
-        if (!ANDROID_SAX_PARSER_FACTORY.equals(factory.getClass().getName())) {
-            setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        }
-        // The per-parse hardening (limits, entity blocking, Android fixups) 
lives in hardenReader() because SAXParserFactory has no property API.
-        return HardeningSAXParserFactory.wrap(factory);
-    }
-
-    /**
-     * Hardens an existing {@link XMLReader}.
-     *
-     * @param reader The reader to harden; never {@code null}.
-     * @return A hardened reader.
-     * @throws IllegalStateException if a required hardening setting cannot be 
applied to the underlying implementation.
-     */
-    static XMLReader hardenReader(final XMLReader reader) {
-        if (reader instanceof HardeningXMLReader) {
-            // Already hardened (for example, a reader from a hardened factory 
passed back through hardenReader); the floor is already in place.
-            return reader;
-        }
-        if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) {
-            // Expat ignores external fetches when no resolver is set; the 
ignore-all floor keeps that behavior non-bypassable (routing a caller-set 
resolver,
-            // including SAXParser.parse's handler, through it and resolving 
anything unresolved to empty) and, via HardeningExpatXMLReader, rejects the
-            // unsupported namespace-prefixes feature eagerly rather than 
mid-parse.
-            return new HardeningExpatXMLReader(reader);
-        }
-        // Required: enables the JDK XMLSecurityManager / Xerces 
SecurityManager limits.
-        setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        // Required: HardeningXMLReader installs an ignore-all EntityResolver 
floor on the reader.
-        // That floor blocks external DTD, entity, schema and xi:include 
fetches in one place: no ACCESS_EXTERNAL_* properties are needed here.
-        // Callers can chain their resolvers, but not override the floor.
-        return new HardeningXMLReader(reader);
-    }
-
-    /**
-     * Rewrites a {@link Source} so that any SAX parsing it triggers runs 
through a hardened {@link XMLReader}.
-     * <p>
-     * Only a {@link StreamSource} or a {@link SAXSource} without a reader is 
enriched with a hardened, namespace-aware reader; other source kinds are 
returned
-     * as-is. Used by the TrAX and schema wrappers to route every source they 
parse through the SAX hardening path.
-     * </p>
-     *
-     * @param source the source to harden; never {@code null}.
-     * @return a hardened source.
-     * @throws TransformerConfigurationException if a hardened reader cannot 
be obtained.
-     * @throws FactoryConfigurationError         Thrown from a factory in case 
of a {@link java.util.ServiceConfigurationError service
-     *                                           configuration error} or if 
the implementation is not available or cannot be instantiated.
-     */
-    static Source hardenSource(final Source source) throws 
TransformerConfigurationException {
-        if (source instanceof StreamSource || source instanceof SAXSource && 
((SAXSource) source).getXMLReader() == null) {
-            final InputSource inputSource = 
SAXSource.sourceToInputSource(source);
-            return inputSource == null ? source : new 
SAXSource(newHardenedReader(), inputSource);
-        }
-        return source;
-    }
-
-    /**
-     * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX 
wrappers to parse sources with.
-     *
-     * @return a hardened reader.
-     * @throws TransformerConfigurationException if a hardened reader cannot 
be obtained.
-     * @throws FactoryConfigurationError Thrown from a factory in case of a 
{@link java.util.ServiceConfigurationError service
-     *                                   configuration error} or if the 
implementation is not available or cannot be instantiated.
-     */
-    static XMLReader newHardenedReader() throws 
TransformerConfigurationException {
-        try {
-            final SAXParserFactory factory = 
harden(SAXParserFactory.newInstance());
-            factory.setNamespaceAware(true);
-            return factory.newSAXParser().getXMLReader();
-        } catch (final ParserConfigurationException | SAXException e) {
-            throw new TransformerConfigurationException("Failed to obtain a 
hardened XMLReader for source parsing", e);
-        }
-    }
-
-    private static void setFeature(final SAXParserFactory factory, final 
String feature, final boolean value) {
-        try {
-            factory.setFeature(feature, value);
-        } catch (final Exception e) {
-            throw HardeningException.settingFailed("feature", feature, 
factory, e);
-        }
-    }
-
-    private static void setFeature(final XMLReader reader, final String 
feature, final boolean value) {
-        try {
-            reader.setFeature(feature, value);
-        } catch (final Exception e) {
-            throw HardeningException.settingFailed("feature", feature, reader, 
e);
-        }
-    }
-
-    private SAXParserHardener() {
-    }
-}
diff --git a/src/main/java/org/apache/commons/xml/SaxonProvider.java 
b/src/main/java/org/apache/commons/xml/SaxonProvider.java
index 9453796..935b422 100644
--- a/src/main/java/org/apache/commons/xml/SaxonProvider.java
+++ b/src/main/java/org/apache/commons/xml/SaxonProvider.java
@@ -52,7 +52,7 @@ final class SaxonProvider {
      *
      * <ol>
      *   <li><b>SAX layer.</b> {@link #makeParser} hands every {@link 
XMLReader} Saxon would otherwise use through
-     *   {@link SAXParserHardener#hardenReader(XMLReader)}, which routes it to 
the matching bundled hardening recipe. External DTDs, entities and XInclude
+     *   {@link HardeningSAXParserFactory#harden(XMLReader)}, which routes it 
to the matching bundled hardening recipe. External DTDs, entities and XInclude
      *   resolve to empty content at parse time.</li>
      *   <li><b>Collection layer.</b> {@code fn:collection} bypasses the 
resource resolver and fetches directly, so an empty {@link CollectionFinder} 
supplies its
      *   ignore outcome instead.</li>
@@ -87,7 +87,7 @@ private HardenedConfiguration() {
         @Override
         public XMLReader makeParser(final String className) throws 
TransformerFactoryConfigurationError {
             try {
-                return 
SAXParserHardener.hardenReader(super.makeParser(className));
+                return 
HardeningSAXParserFactory.harden(super.makeParser(className));
             } catch (final HardeningException e) {
                 throw new TransformerFactoryConfigurationError(e);
             }
@@ -103,7 +103,7 @@ public XMLReader makeParser(final String className) throws 
TransformerFactoryCon
     private static final class SaxonProviderConfigurer {
 
         private static TransformerFactory configure(final TransformerFactory 
factory) {
-            // The URIResolver floor is installed by the 
HardeningTransformerFactory wrapper that TransformerHardener puts around this 
factory.
+            // The URIResolver floor is installed by the 
HardeningTransformerFactory wrapper that HardeningTransformerFactory.harden 
puts around this factory.
             ((SaxonTransformerFactory) factory).setConfiguration(new 
HardenedConfiguration());
             return factory;
         }
diff --git a/src/main/java/org/apache/commons/xml/SchemaHardener.java 
b/src/main/java/org/apache/commons/xml/SchemaHardener.java
deleted file mode 100644
index ac355a8..0000000
--- a/src/main/java/org/apache/commons/xml/SchemaHardener.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * 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;
-
-import javax.xml.validation.SchemaFactory;
-
-/**
- * Hardening for any {@link SchemaFactory} on the classpath.
- *
- * <p>Unlike the other hardeners there is no per-implementation branching and 
no feature or limit configuration on the factory itself: schema compilation and
- * validation reach external resources only through the resolver hook, so 
wrapping the factory with a non-removable ignore-all resolver floor is enough 
on every
- * implementation. The reader used to parse schema and instance documents is 
hardened separately, through
- * {@link SAXParserHardener#hardenSource(javax.xml.transform.Source)}.</p>
- */
-final class SchemaHardener {
-
-    /**
-     * Hardens an existing {@link SchemaFactory}.
-     *
-     * <p>Beyond the three universal guarantees (no external DTD fetch, no 
external entity resolution, bounded internal entity expansion):</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 factory the factory to harden; never {@code null}.
-     * @return a hardened factory.
-     */
-    static SchemaFactory harden(final SchemaFactory factory) {
-        return HardeningSchemaFactory.wrap(factory);
-    }
-
-    private SchemaHardener() {
-    }
-}
diff --git a/src/main/java/org/apache/commons/xml/StaxHardener.java 
b/src/main/java/org/apache/commons/xml/StaxHardener.java
deleted file mode 100644
index db4a7d0..0000000
--- a/src/main/java/org/apache/commons/xml/StaxHardener.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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;
-
-import javax.xml.stream.XMLInputFactory;
-
-/**
- * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the 
classpath.
- *
- * <p>One recipe covers both the JDK Zephyr and Woodstox: {@link 
HardeningXMLInputFactory} installs a non-removable {@link 
FallbackIgnoreXMLResolver} floor on
- * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} / 
{@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see that wrapper's
- * Javadoc for the per-implementation hook routing.</p>
- */
-final class StaxHardener {
-
-    /** Woodstox property: resolver consulted for the external DTD subset. */
-    static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
-
-    /** Woodstox property: resolver consulted for declared external general 
entities. */
-    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";
-
-    static XMLInputFactory harden(final XMLInputFactory factory) {
-        // HardeningXMLInputFactory installs the non-removable ignore-all 
resolver floor that resolves every external DTD and entity to empty content.
-        return HardeningXMLInputFactory.wrap(factory);
-    }
-
-    private StaxHardener() {
-    }
-}
diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java 
b/src/main/java/org/apache/commons/xml/TransformerHardener.java
deleted file mode 100644
index e7d6c0e..0000000
--- a/src/main/java/org/apache/commons/xml/TransformerHardener.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * 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;
-
-import javax.xml.XMLConstants;
-import javax.xml.transform.Source;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.URIResolver;
-import javax.xml.transform.sax.SAXTransformerFactory;
-
-/**
- * Capability-driven hardening for any {@link TransformerFactory} on the 
classpath.
- *
- * <p>Rather than branching on the implementation class, {@link 
#harden(TransformerFactory)} probes what the factory supports and adapts:</p>
- * <ul>
- *     <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by 
package prefix and handed to {@link 
SaxonProvider#configure(TransformerFactory)} for the
- *         channels the standard JAXP knobs cannot close (reflection-based 
extension functions, the collection finder, the internal SAX parser). It is then
- *         wrapped in {@link HardeningTransformerFactory} like every other 
implementation to install the {@link FallbackIgnoreURIResolver} floor; the only
- *         difference is the empty-{@link Source} shape the floor returns, 
{@code EmptySource} for Saxon rather than the default empty DOM document.</li>
- *     <li><strong>FSP</strong> ({@link 
XMLConstants#FEATURE_SECURE_PROCESSING}): required. On XSLTC it enables the 
runtime evaluator limits; on Xalan it disables
- *         reflection-based extension functions.</li>
- *     <li><strong>{@link FallbackIgnoreURIResolver} floor</strong>: required. 
An ignore-all {@link URIResolver} floor, installed by
- *         {@link HardeningTransformerFactory} and carried onto every produced 
{@link Transformer}, resolves {@code xsl:import}/{@code xsl:include} at compile
- *         time and {@code document()} at runtime to an empty document, the 
one channel both XSLTC and Xalan route through. A caller-set {@link 
URIResolver} is
- *         routed through the floor rather than replacing it, so a caller can 
opt a specific URI in but cannot reopen the fetch.</li>
- *     <li><strong>{@link HardeningTransformerFactory}</strong>: required. 
Both implementations fall back to {@code SAXParserFactory.newInstance()} to 
parse a
- *         stylesheet or source document that does not carry its own reader, 
and only set FSP on it; wrapping the factory rewrites every {@link Source} 
through an
- *         {@link org.apache.commons.xml}-hardened reader instead.</li>
- * </ul>
- */
-final class TransformerHardener {
-
-    static TransformerFactory harden(final TransformerFactory factory) {
-        // Required: enables secure processing (XSLTC runtime limits; Xalan's 
extension-function block).
-        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        if (SaxonProvider.isSaxon(factory.getClass())) {
-            // Saxon keeps its vendor Configuration for the channels JAXP 
cannot close,
-            // then goes through the same wrapper as every other 
implementation for the URIResolver floor;
-            // EmptySource is the empty-source shape Saxon's consumers expect.
-            return HardeningTransformerFactory.wrap((SAXTransformerFactory) 
SaxonProvider.configure(factory), SaxonProvider.emptySourceSupplier());
-        }
-        // Required: source/stylesheet parsing provisions its own SAX reader 
otherwise; the wrapper routes every Source through a hardened one and installs 
the
-        // ignore-all URIResolver floor (blocking xsl:import/include at 
compile time and document() at runtime) that a caller-set resolver cannot 
remove.
-        return HardeningTransformerFactory.wrap((SAXTransformerFactory) 
factory);
-    }
-
-    private static void setFeature(final TransformerFactory factory, final 
String feature, final boolean value) {
-        try {
-            factory.setFeature(feature, value);
-        } catch (final Exception e) {
-            throw HardeningException.settingFailed("feature", feature, 
factory, e);
-        }
-    }
-
-    private TransformerHardener() {
-    }
-}
diff --git a/src/main/java/org/apache/commons/xml/XPathHardener.java 
b/src/main/java/org/apache/commons/xml/XPathHardener.java
deleted file mode 100644
index fccbfd5..0000000
--- a/src/main/java/org/apache/commons/xml/XPathHardener.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/*
- * 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;
-
-import javax.xml.XMLConstants;
-import javax.xml.xpath.XPathFactory;
-import javax.xml.xpath.XPathFactoryConfigurationException;
-
-/**
- * Capability-driven hardening for any {@link XPathFactory} on the classpath.
- *
- * <p>The XPath object model mirrors TrAX: the stock JDK and Apache Xalan ship 
an XPath 1.0 engine with no URI-fetching functions, while Saxon adds the XPath 
3.1
- * {@code fn:doc}, {@code fn:collection} and {@code fn:unparsed-text} 
functions that can reach external resources. Rather than branching on the 
implementation
- * class, {@link #harden(XPathFactory)} probes what the factory supports and 
adapts:</p>
- * <ul>
- *     <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by 
package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so 
any public
- *         subclass routes to the same recipe as the registered factory. Its 
URI-fetching
- *         functions and reflection-based extension calls are reachable only 
through a locked-down Saxon {@code Configuration}, not the standard JAXP knobs; 
this
- *         is the XPath counterpart of the Saxon exception in {@link 
TransformerHardener}, kept as a documented package-prefix exception because the 
required
- *         hardening surface is reachable only through a vendor API.</li>
- *     <li><strong>FODP</strong> ({@code jdk.xml.overrideDefaultParser}, set 
to {@code false}): best-effort. On the stock JDK it pins the internal parser 
lookup to
- *         the bundled SAX parser, blocking a system property swap to a 
third-party parser (defense-in-depth); Xalan rejects the feature and is left 
unchanged.</li>
- *     <li><strong>FSP</strong> ({@link 
XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only knob both 
the stock JDK and Xalan XPath engines expose,
- *         and switches on their secure-processing limits. {@link 
XPathFactory} has no attribute API for finer control.</li>
- *     <li><strong>{@link HardeningXPathFactory}</strong>: required. FSP 
governs only the engine, not the parser it provisions internally for the
- *         {@link org.xml.sax.InputSource}-taking {@code evaluate} entry 
points; the wrapper performs that document build with a hardened parser 
instead, so
- *         the engine never parses.</li>
- * </ul>
- */
-final class XPathHardener {
-
-    /**
-     * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX 
parser; defense-in-depth against a system property swap to a third-party parser.
-     */
-    private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = 
"jdk.xml.overrideDefaultParser";
-
-    /**
-     * Hardens the given factory, returning a hardened wrapper if necessary.
-     *
-     * @param factory The factory to harden.
-     * @return A new hardened factory or the original factory, hardened, if it 
is a known Saxon factory.
-     * @throws HardeningException Thrown if this {@link XPathFactory} or the 
{@code XPath}s it creates cannot support this feature.
-     */
-    static XPathFactory harden(final XPathFactory factory) {
-        if (SaxonProvider.isSaxon(factory.getClass())) {
-            // Saxon: only a locked-down Configuration can close its 
URI-fetching functions and extension-function surface.
-            return SaxonProvider.configure(factory);
-        }
-        // Best-effort: the stock JDK pins its bundled SAX parser 
(defense-in-depth); Xalan rejects the feature.
-        setOptionalFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false);
-        // Required: enables the engine's secure-processing limits; 
XPathFactory has no attribute API for finer control.
-        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
-        // Required: FSP does not reach the parser the engine provisions for 
InputSource-taking evaluate calls; the wrapper parses those itself.
-        return HardeningXPathFactory.wrap(factory);
-    }
-
-    /**
-     * Sets a feature on the given factory, throwing a {@link 
HardeningException} if the implementation does not recognize it.
-     *
-     * @param factory The factory to harden.
-     * @param feature The feature to set.
-     * @param value   The value to set.
-     * @throws HardeningException Thrown if this {@link XPathFactory} or the 
{@code XPath}s it creates cannot support this feature or if {@code feature} is
-     *                            {@code null}.
-     */
-    private static void setFeature(final XPathFactory factory, final String 
feature, final boolean value) {
-        try {
-            factory.setFeature(feature, value);
-        } catch (final XPathFactoryConfigurationException e) {
-            throw HardeningException.settingFailed("feature", feature, 
factory, e);
-        }
-    }
-
-    /**
-     * Sets an optional feature on the given factory, ignoring it if the 
implementation does not recognize it.
-     *
-     * @param factory The factory to harden.
-     * @param feature The feature to set.
-     * @param value   The value to set.
-     */
-    private static void setOptionalFeature(final XPathFactory factory, final 
String feature, final boolean value) {
-        try {
-            factory.setFeature(feature, value);
-        } catch (final XPathFactoryConfigurationException e) {
-            // Ignored: the implementation does not recognize this optional 
feature.
-        }
-    }
-
-    private XPathHardener() {
-    }
-}
diff --git a/src/test/java/org/apache/commons/xml/AttackTestSupport.java 
b/src/test/java/org/apache/commons/xml/AttackTestSupport.java
index 0e3ea63..0edeae0 100644
--- a/src/test/java/org/apache/commons/xml/AttackTestSupport.java
+++ b/src/test/java/org/apache/commons/xml/AttackTestSupport.java
@@ -91,7 +91,7 @@
 final class AttackTestSupport {
 
     /**
-     * Test-only permissive counterpart of {@code 
SAXParserHardener.HardeningExpatXMLReader}: a pass-through Expat wrapper that 
rejects the
+     * Test-only permissive counterpart of {@code 
HardeningSAXParserFactory.HardeningExpatXMLReader}: a pass-through Expat 
wrapper that rejects the
      * {@code namespace-prefixes} feature eagerly (so a probing TrAX identity 
transformer falls back instead of failing the whole parse) but installs no 
ignore-all
      * resolver floor, so the unconfigured/positive controls stay permissive.
      */
@@ -691,7 +691,7 @@ static void assertValidatorValidates(final String xml) {
     /**
      * Asserts a hardened-in-place XMLReader parse of the payload throws.
      *
-     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link SAXParserHardener#hardenReader(XMLReader)}; only a thrown exception 
passes.</p>
+     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link HardeningSAXParserFactory#harden(XMLReader)}; only a thrown exception 
passes.</p>
      */
     static void assertXmlReaderBlocks(final String payload) {
         assertParseFails(() -> consumeXmlReader(rawHardenedReader(), payload), 
"XMLReader", SAXException.class);
@@ -707,7 +707,7 @@ static void assertXmlReaderBlocksOrDoesNotLeak(final String 
payload) {
     /**
      * Asserts a hardened-in-place XMLReader parse completes without throwing 
and without leaked content.
      *
-     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link SAXParserHardener#hardenReader(XMLReader)}; use this when the hardening 
contract
+     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link HardeningSAXParserFactory#harden(XMLReader)}; use this when the 
hardening contract
      * guarantees the parse succeeds but never resolves the external 
resource.</p>
      */
     static void assertXmlReaderDoesNotLeak(final String payload) {
@@ -717,7 +717,7 @@ static void assertXmlReaderDoesNotLeak(final String 
payload) {
     /**
      * Asserts a hardened-in-place XMLReader parse succeeds.
      *
-     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link SAXParserHardener#hardenReader(XMLReader)}; positive control for 
DOCTYPE-only
+     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link HardeningSAXParserFactory#harden(XMLReader)}; positive control for 
DOCTYPE-only
      * payloads.</p>
      */
     static void assertXmlReaderParses(final String payload) {
@@ -926,13 +926,13 @@ private static boolean probeDomResolvesInternalEntities() 
{
         }
     }
 
-    /** Builds a raw {@link XMLReader} from a deliberately permissive {@link 
SAXParserFactory} and hardens it via {@link 
SAXParserHardener#hardenReader(XMLReader)}. */
+    /** Builds a raw {@link XMLReader} from a deliberately permissive {@link 
SAXParserFactory} and hardens it via {@link 
HardeningSAXParserFactory#harden(XMLReader)}. */
     private static XMLReader rawHardenedReader() throws Exception {
         final SAXParserFactory factory = SAXParserFactory.newInstance();
         if (!IS_ANDROID) {
             factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
         }
-        return 
SAXParserHardener.hardenReader(factory.newSAXParser().getXMLReader());
+        return 
HardeningSAXParserFactory.harden(factory.newSAXParser().getXMLReader());
     }
 
     /** Opens the named test resource as a {@link StreamSource} preserving its 
system id, so relative includes/imports/redefines resolve normally. */
diff --git 
a/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java
index ea21886..475cfd0 100644
--- a/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java
+++ b/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java
@@ -87,7 +87,7 @@ private static String transform(final TransformerFactory 
factory) throws Transfo
     void hardenedBasicFactoryDoesNotLeakCollection() {
         assumeSaxonPresent();
         try {
-            final String result = 
transform(TransformerHardener.harden(basicSaxonFactory()));
+            final String result = 
transform(HardeningTransformerFactory.harden(basicSaxonFactory()));
             assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), 
"collection() leaked through the alternate Saxon factory:\n" + result);
         } catch (final TransformerException blocked) {
             // Throwing is an acceptable outcome since it also prevents 
leaking the marker.
diff --git a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java 
b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
index f1554ba..57ea76f 100644
--- a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
+++ b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
@@ -39,12 +39,12 @@
 import org.vafer.jdependency.Clazzpath;
 
 /**
- * Guards the shade footprint: the set of classes a consumer pulls in when 
they shade a single hardener entry point.
+ * Guards the shade footprint: the set of classes a consumer pulls in when 
they shade a single factory entry point.
  *
  * <p>Using {@code jdependency}, the same library {@code maven-shade-plugin}'s 
{@code minimizeJar} uses, this test computes each entry point's transitive class
- * closure over the compiled {@code target/classes} and pins it to an expected 
set. It keeps each hardener from silently regaining a dependency on classes it
- * should not need (for example a sibling resolver floor or another hardener), 
so schema builds only on the shared SAX path, TrAX and XPath additionally on the
- * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse 
through, while only the public {@link org.apache.commons.xml} entry pulls the 
whole
+ * closure over the compiled {@code target/classes} and pins it to an expected 
set. It keeps each entry point from silently regaining a dependency on classes 
it
+ * should not need (for example a sibling resolver floor or another factory 
class), so schema builds only on the shared SAX path, TrAX and XPath 
additionally on the
+ * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse 
through, while the six public entry points together pull the whole
  * library. Update the expected sets deliberately: a change here is a change 
to what a downstream shade includes.</p>
  *
  * <p>The test reads the compiled {@code .class} files from the code-source 
location, which only exists on a regular JVM: a native image carries no 
bytecode (and
@@ -56,8 +56,7 @@ class ShadingFootprintTest {
     private static final String PKG = "org.apache.commons.xml.";
 
     // @formatter:off
-    private static final Set<String> DOCUMENT_BUILDER_HARDENER = set(
-            "DocumentBuilderHardener",
+    private static final Set<String> DOCUMENT_BUILDER_FACTORY = set(
             "FallbackIgnoreEntityResolver2",
             "HardeningDocumentBuilder",
             "HardeningDocumentBuilderFactory",
@@ -67,36 +66,33 @@ class ShadingFootprintTest {
     // @formatter:on
 
     // @formatter:off
-    private static final Set<String> SAX_PARSER_HARDENER = set(
+    private static final Set<String> SAX_PARSER_FACTORY = set(
             "FallbackIgnoreEntityResolver2",
             "HardeningException",
             "HardeningSAXParser",
             "HardeningSAXParserFactory",
             "HardeningSAXParserFactory$1",
+            "HardeningSAXParserFactory$HardeningExpatXMLReader",
             "HardeningSAXParserFactory$Wrapper",
-            "HardeningXMLReader",
-            "SAXParserHardener",
-            "SAXParserHardener$HardeningExpatXMLReader");
+            "HardeningXMLReader");
     // @formatter:on
 
     // @formatter:off
-    private static final Set<String> STAX_HARDENER = set(
+    private static final Set<String> XML_INPUT_FACTORY = set(
             "FallbackIgnoreXMLResolver",
             "HardeningException",
             "HardeningXMLInputFactory",
             "HardeningXMLInputFactory$1",
-            "HardeningXMLInputFactory$Wrapper",
-            "StaxHardener");
+            "HardeningXMLInputFactory$Wrapper");
     // @formatter:on
 
     /**
-     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
SAXParserHardener#hardenSource(Source)}, so each builds on the full SAX closure 
below;
-     * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} 
source and XPath its InputSource-taking evaluate calls through the DOM 
hardener, so
+     * TrAX, XPath and schema re-harden their sub-parsers through {@link 
HardeningSAXParserFactory#harden(Source)}, so each builds on the full SAX 
closure below;
+     * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} 
source and XPath its InputSource-taking evaluate calls through the DOM entry 
point, so
      * their closures carry that set too.
      */
     // @formatter:off
-    private static final Set<String> TRANSFORMER_HARDENER = 
saxParsersHardenerPlus(
-            "DocumentBuilderHardener",
+    private static final Set<String> TRANSFORMER_FACTORY = 
saxParserFactoryPlus(
             "FallbackIgnoreEntityResolver2",
             "FallbackIgnoreURIResolver",
             "HardeningDocumentBuilder",
@@ -107,6 +103,7 @@ class ShadingFootprintTest {
             "HardeningSAXParser",
             "HardeningSAXParserFactory",
             "HardeningSAXParserFactory$1",
+            "HardeningSAXParserFactory$HardeningExpatXMLReader",
             "HardeningSAXParserFactory$Wrapper",
             "HardeningTemplates",
             "HardeningTemplatesHandler",
@@ -117,18 +114,14 @@ class ShadingFootprintTest {
             "HardeningTransformerHandler",
             "HardeningXMLFilter",
             "HardeningXMLReader",
-            "SAXParserHardener",
-            "SAXParserHardener$HardeningExpatXMLReader",
             "SaxonProvider",
             "SaxonProvider$1",
             "SaxonProvider$HardenedConfiguration",
-            "SaxonProvider$SaxonProviderConfigurer",
-            "TransformerHardener");
+            "SaxonProvider$SaxonProviderConfigurer");
     // @formatter:on
 
     // @formatter:off
-    private static final Set<String> XPATH_HARDENER = saxParsersHardenerPlus(
-            "DocumentBuilderHardener",
+    private static final Set<String> XPATH_FACTORY = saxParserFactoryPlus(
             "FallbackIgnoreEntityResolver2",
             "FallbackIgnoreURIResolver",
             "HardeningDocumentBuilder",
@@ -139,6 +132,7 @@ class ShadingFootprintTest {
             "HardeningSAXParser",
             "HardeningSAXParserFactory",
             "HardeningSAXParserFactory$1",
+            "HardeningSAXParserFactory$HardeningExpatXMLReader",
             "HardeningSAXParserFactory$Wrapper",
             "HardeningXMLReader",
             "HardeningXPath",
@@ -146,23 +140,21 @@ class ShadingFootprintTest {
             "HardeningXPathFactory",
             "HardeningXPathFactory$1",
             "HardeningXPathFactory$Wrapper",
-            "SAXParserHardener",
-            "SAXParserHardener$HardeningExpatXMLReader",
             "SaxonProvider",
             "SaxonProvider$1",
             "SaxonProvider$HardenedConfiguration",
-            "SaxonProvider$SaxonProviderConfigurer",
-            "XPathHardener");
+            "SaxonProvider$SaxonProviderConfigurer");
     // @formatter:on
 
     // @formatter:off
-    private static final Set<String> SCHEMA_HARDENER = saxParsersHardenerPlus(
+    private static final Set<String> SCHEMA_FACTORY = saxParserFactoryPlus(
             "FallbackIgnoreEntityResolver2",
             "FallbackIgnoreLSResourceResolver",
             "HardeningException",
             "HardeningSAXParser",
             "HardeningSAXParserFactory",
             "HardeningSAXParserFactory$1",
+            "HardeningSAXParserFactory$HardeningExpatXMLReader",
             "HardeningSAXParserFactory$Wrapper",
             "HardeningSchema",
             "HardeningSchemaFactory",
@@ -170,22 +162,19 @@ class ShadingFootprintTest {
             "HardeningSchemaFactory$Wrapper",
             "HardeningValidator",
             "HardeningValidatorHandler",
-            "HardeningXMLReader",
-            "SAXParserHardener",
-            "SAXParserHardener$HardeningExpatXMLReader",
-            "SchemaHardener");
+            "HardeningXMLReader");
     // @formatter:on
 
     /**
      * Class count of the {@link #rootClosure()} DOM entry point, the baseline 
the {@link #reportFootprint()} percentages are computed against.
      */
-    private static final int LIBRARY_CLASS_COUNT = 7;
+    private static final int LIBRARY_CLASS_COUNT = 6;
 
     /**
      * Entry points reported by the {@link #reportFootprint()} diagnostic, 
most-focused first, ending with the whole library.
      */
-    private static final String[] REPORTED = {"DocumentBuilderHardener", 
"SAXParserHardener", "StaxHardener", "TransformerHardener", "XPathHardener",
-            "SchemaHardener"};
+    private static final String[] REPORTED = 
{"HardeningDocumentBuilderFactory", "HardeningSAXParserFactory", 
"HardeningXMLInputFactory",
+            "HardeningTransformerFactory", "HardeningXPathFactory", 
"HardeningSchemaFactory"};
 
     private static Clazzpath clazzpath;
     private static Path classesDir;
@@ -253,10 +242,10 @@ private static Set<String> rootClosure() {
     }
 
     /**
-     * {@link #SAX_PARSER_HARDENER} plus the extra names; used where an entry 
point's closure is the SAX path plus its own classes.
+     * {@link #SAX_PARSER_FACTORY} plus the extra names; used where an entry 
point's closure is the SAX path plus its own classes.
      */
-    private static Set<String> saxParsersHardenerPlus(final String... more) {
-        final Set<String> union = new TreeSet<>(SAX_PARSER_HARDENER);
+    private static Set<String> saxParserFactoryPlus(final String... more) {
+        final Set<String> union = new TreeSet<>(SAX_PARSER_FACTORY);
         union.addAll(Arrays.asList(more));
         return union;
     }
@@ -270,8 +259,8 @@ private static String strip(final String qualifiedName) {
     }
 
     @Test
-    void documentBuilderHardenerFootprint() {
-        assertEquals(DOCUMENT_BUILDER_HARDENER, 
closureOf("DocumentBuilderHardener"));
+    void documentBuilderFactoryFootprint() {
+        assertEquals(DOCUMENT_BUILDER_FACTORY, 
closureOf("HardeningDocumentBuilderFactory"));
     }
 
     @Test
@@ -280,27 +269,27 @@ void rootClosureMatchesDocumentBuilderFootprint() {
     }
 
     @Test
-    void saxParserHardenerFootprint() {
-        assertEquals(SAX_PARSER_HARDENER, closureOf("SAXParserHardener"));
+    void saxParserFactoryFootprint() {
+        assertEquals(SAX_PARSER_FACTORY, 
closureOf("HardeningSAXParserFactory"));
     }
 
     @Test
-    void schemaHardenerFootprint() {
-        assertEquals(SCHEMA_HARDENER, closureOf("SchemaHardener"));
+    void schemaFactoryFootprint() {
+        assertEquals(SCHEMA_FACTORY, closureOf("HardeningSchemaFactory"));
     }
 
     @Test
-    void staxHardenerFootprint() {
-        assertEquals(STAX_HARDENER, closureOf("StaxHardener"));
+    void xmlInputFactoryFootprint() {
+        assertEquals(XML_INPUT_FACTORY, closureOf("HardeningXMLInputFactory"));
     }
 
     @Test
-    void transformerHardenerFootprint() {
-        assertEquals(TRANSFORMER_HARDENER, closureOf("TransformerHardener"));
+    void transformerFactoryFootprint() {
+        assertEquals(TRANSFORMER_FACTORY, 
closureOf("HardeningTransformerFactory"));
     }
 
     @Test
-    void xPathHardenerFootprint() {
-        assertEquals(XPATH_HARDENER, closureOf("XPathHardener"));
+    void xPathFactoryFootprint() {
+        assertEquals(XPATH_FACTORY, closureOf("HardeningXPathFactory"));
     }
 }
diff --git 
a/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java 
b/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java
index 7d9d01e..23fcfdd 100644
--- a/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java
+++ b/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java
@@ -93,7 +93,7 @@ public void setFeature(final String name, final boolean 
value) throws SAXNotReco
     void hardenRejectsUnsecurableFactory() {
         final IllegalStateException thrown = assertThrows(
                 IllegalStateException.class,
-                () -> DocumentBuilderHardener.harden(new 
FakeDocumentBuilderFactory()));
+                () -> HardeningDocumentBuilderFactory.harden(new 
FakeDocumentBuilderFactory()));
         assertNotNull(thrown.getMessage());
         
assertTrue(thrown.getMessage().contains(FakeDocumentBuilderFactory.class.getName()),
                 "Exception message must name the unsupported class: " + 
thrown.getMessage());
@@ -103,7 +103,7 @@ void hardenRejectsUnsecurableFactory() {
     void hardenRejectsUnsecurableSaxFactory() {
         final IllegalStateException thrown = assertThrows(
                 IllegalStateException.class,
-                () -> SAXParserHardener.harden(new FakeSAXParserFactory()));
+                () -> HardeningSAXParserFactory.harden(new 
FakeSAXParserFactory()));
         assertNotNull(thrown.getMessage());
         
assertTrue(thrown.getMessage().contains(FakeSAXParserFactory.class.getName()),
                 "Exception message must name the unsupported class: " + 
thrown.getMessage());
diff --git a/src/test/java/org/apache/commons/xml/XIncludeTest.java 
b/src/test/java/org/apache/commons/xml/XIncludeTest.java
index 3dbc4ce..64cda94 100644
--- a/src/test/java/org/apache/commons/xml/XIncludeTest.java
+++ b/src/test/java/org/apache/commons/xml/XIncludeTest.java
@@ -341,7 +341,7 @@ void hardenReaderAllowListResolvesParseXml() throws 
Exception {
         final SAXParserFactory unhardenedFactory = 
SAXParserFactory.newInstance();
         unhardenedFactory.setNamespaceAware(true);
         assumeXIncludeAware(unhardenedFactory);
-        final XMLReader reader = 
SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader());
+        final XMLReader reader = 
HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader());
         reader.setEntityResolver(new AllowListResolver());
         final String captured = captureCharacters(reader, input);
         assertEquals(RESOLVED_MARKER, captured.trim(),
@@ -356,7 +356,7 @@ void hardenReaderBlocksParseText() throws Exception {
         final SAXParserFactory unhardenedFactory = 
SAXParserFactory.newInstance();
         unhardenedFactory.setNamespaceAware(true);
         assumeXIncludeAware(unhardenedFactory);
-        final XMLReader reader = 
SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader());
+        final XMLReader reader = 
HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader());
         final String captured = captureCharacters(reader, input);
         assertFalse(captured.contains(LEAKED_MARKER),
                 "hardenReader parse=text must resolve the include to empty, 
not leak; got: " + captured);
@@ -371,7 +371,7 @@ void hardenReaderBlocksParseXml() throws Exception {
         final SAXParserFactory unhardenedFactory = 
SAXParserFactory.newInstance();
         unhardenedFactory.setNamespaceAware(true);
         assumeXIncludeAware(unhardenedFactory);
-        final XMLReader reader = 
SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader());
+        final XMLReader reader = 
HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader());
         assertThrows(SAXException.class, () -> reader.parse(input),
                 "hardenReader should block XInclude parse=xml on reader with 
XInclude already enabled");
     }

Reply via email to