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

ppkarwasz pushed a commit to branch feature/reduce-shade-footprint
in repository https://gitbox.apache.org/repos/asf/commons-xml.git

commit 66be10519977c6a89e9334eaa30b91f23d71f610
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Jul 8 10:52:46 2026 +0200

    Split the resolver floors into top-level classes
    
    Promote the five nested Resolvers.Fallback* floors to top-level,
    package-private classes and delete the Resolvers container. The floors
    were bound together only by the shared forbiddenMessage, now on
    HardeningException (Phase 1), and by being nested in one class, which
    maven-shade minimizeJar keeps together. Splitting them means shading one
    hardener pulls only the floor(s) it uses:
    
    - DocumentBuilderHardener: 10 -> 5 classes, 19265 -> 12161 bytes
    - SAXParserHardener:       13 -> 8 classes, 25721 -> 18242 bytes
    - StaxHardener:            10 -> 6 classes, 20547 -> 13433 bytes
    
    The EntityResolver2 floor is named FallbackDenyEntityResolver2; it also
    carries the shared "floor" overview the container used to hold. Update
    ShadingFootprintTest's expected sets and exclude that JVM-only test
    (jdependency) from the Android test compile.
    
    Assisted-By: Claude Opus 4.8 <[email protected]>
---
 android-tests/build.gradle.kts                     |   5 +
 .../commons/xml/FallbackDenyEntityResolver2.java   | 133 +++++++++
 .../xml/FallbackDenyLSResourceResolver.java        |  55 ++++
 .../commons/xml/FallbackDenyURIResolver.java       |  56 ++++
 .../commons/xml/FallbackDenyXMLResolver.java       |  84 ++++++
 .../commons/xml/FallbackIgnoreXMLResolver.java     |  47 ++++
 .../commons/xml/HardeningDocumentBuilder.java      |   4 +-
 .../apache/commons/xml/HardeningSchemaFactory.java |   4 +-
 .../apache/commons/xml/HardeningTransformer.java   |   4 +-
 .../commons/xml/HardeningTransformerFactory.java   |   2 +-
 .../org/apache/commons/xml/HardeningValidator.java |   2 +-
 .../commons/xml/HardeningValidatorHandler.java     |   4 +-
 .../commons/xml/HardeningXMLInputFactory.java      |  18 +-
 .../org/apache/commons/xml/HardeningXMLReader.java |  12 +-
 .../java/org/apache/commons/xml/Resolvers.java     | 308 ---------------------
 .../org/apache/commons/xml/SAXParserHardener.java  |   6 +-
 .../java/org/apache/commons/xml/StaxHardener.java  |  12 +-
 .../apache/commons/xml/TransformerHardener.java    |   2 +-
 .../commons/xml/EntityResolverFloorTest.java       |   4 +-
 .../apache/commons/xml/ShadingFootprintTest.java   |  11 +-
 20 files changed, 421 insertions(+), 352 deletions(-)

diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts
index a8938b9..7a42244 100644
--- a/android-tests/build.gradle.kts
+++ b/android-tests/build.gradle.kts
@@ -64,6 +64,11 @@ android {
     }
 }
 
+// ShadingFootprintTest is a JVM-only build check (it uses jdependency to read 
target/classes); exclude it from the Android test compile.
+tasks.withType<JavaCompile>().configureEach {
+    exclude("**/ShadingFootprintTest.java")
+}
+
 // Skip JAXP groups whose factories Android does not ship
 junitPlatform {
     filters {
diff --git 
a/src/main/java/org/apache/commons/xml/FallbackDenyEntityResolver2.java 
b/src/main/java/org/apache/commons/xml/FallbackDenyEntityResolver2.java
new file mode 100644
index 0000000..92ccf4f
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/FallbackDenyEntityResolver2.java
@@ -0,0 +1,133 @@
+/*
+ * 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 java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.ext.DefaultHandler2;
+import org.xml.sax.ext.EntityResolver2;
+
+/**
+ * Entity resolver that consults an optional caller-supplied resolver and 
denies (throws) whatever the caller does not resolve.
+ *
+ * <p>The canonical hardening floor, and the entity-resolution counterpart of 
the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties. Every floor
+ * ({@link FallbackDenyLSResourceResolver}, {@link FallbackDenyURIResolver}, 
{@link FallbackDenyXMLResolver} and its {@link FallbackIgnoreXMLResolver} 
variant)
+ * shares two defining properties:</p>
+ * <ol>
+ * <li><strong>Non-removable, and it wraps the resolver the caller 
sets.</strong> The hardened wrappers install one and route a caller-set 
resolver through
+ * {@code setDelegate} rather than letting it replace the floor, so the 
caller's resolver is consulted first but cannot remove the floor underneath 
it.</li>
+ * <li><strong>It supplies the default action for a lookup the caller's 
resolver does not resolve</strong> (a {@code null} return, or no caller 
resolver at
+ * all). This is where a floor departs from stock JAXP: normally an unresolved 
lookup falls back to the processor's built-in resolution and the resource is
+ * <em>fetched</em>; a floor instead <em>denies</em> it (throws).</li>
+ * </ol>
+ *
+ * <p>The hardened DOM and SAX wrappers install one of these and, when the 
caller sets their own {@link EntityResolver}, route it through {@link 
#setDelegate}
+ * rather than letting it replace the floor. A caller therefore opts a 
specific resource in by returning a non-{@code null} {@link InputSource} from 
their
+ * resolver; anything they leave unresolved (a {@code null} return, or no 
caller resolver at all) goes to {@link #onUnresolved}, which denies by 
default.</p>
+ *
+ * <p>It extends {@link DefaultHandler2} so it is also usable as a {@link 
org.xml.sax.ext.LexicalHandler} (see {@code SAXParserHardener}'s Android 
subclass,
+ * which needs {@code startDTD}/{@code endDTD}); {@link #getExternalSubset} 
therefore inherits the {@code DefaultHandler2} "no synthetic subset" default. 
Only
+ * {@link #resolveEntity(String, String, String, String) resolveEntity} (the 
actual external fetch) reaches the deny fallback.</p>
+ */
+class FallbackDenyEntityResolver2 extends DefaultHandler2 {
+
+    /**
+     * Caller-supplied resolver consulted first, or {@code null} for a pure 
deny-all floor.
+     */
+    private EntityResolver delegate;
+
+    /**
+     * Resolves {@code systemId} against {@code baseURI}.
+     *
+     * @param baseURI  The absolute base URI to resolve against, or {@code 
null} if none is available.
+     * @param systemId The system identifier, possibly relative to {@code 
baseURI}.
+     * @return The absolutized system identifier, or {@code systemId} 
unchanged when it cannot or need not be resolved.
+     */
+    private static String absolutize(final String baseURI, final String 
systemId) {
+        if (systemId == null || baseURI == null) {
+            return systemId;
+        }
+        try {
+            final URI system = new URI(systemId);
+            return system.isAbsolute() ? systemId : new 
URI(baseURI).resolve(system).toString();
+        } catch (final URISyntaxException e) {
+            return systemId;
+        }
+    }
+
+    FallbackDenyEntityResolver2(final EntityResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    /**
+     * Replaces the caller resolver consulted ahead of the floor; lets a 
single floor instance back successive {@code setEntityResolver} calls.
+     *
+     * @param delegate The caller-supplied resolver, or {@code null} for a 
pure deny-all floor.
+     */
+    final void setDelegate(final EntityResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    final EntityResolver getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public final InputSource resolveEntity(final String publicId, final String 
systemId) throws SAXException, IOException {
+        return resolveEntity(null, publicId, null, systemId);
+    }
+
+    @Override
+    public final InputSource resolveEntity(final String name, final String 
publicId, final String baseURI, final String systemId)
+            throws SAXException, IOException {
+        final InputSource resolved = resolveWithDelegate(name, publicId, 
baseURI, systemId);
+        return resolved != null ? resolved : onUnresolved(name, publicId, 
baseURI, systemId);
+    }
+
+    /**
+     * Outcome when neither the caller delegate nor this resolver provides the 
entity. Denies by default; a subclass may permit specific lookups (e.g. the
+     * external DTD subset) by returning {@code null} or an {@link 
InputSource} instead of calling {@code super}.
+     *
+     * @param name     The entity name, or {@code null} on the 2-arg 
resolution path.
+     * @param publicId The public identifier, or {@code null} if none.
+     * @param baseURI  The base URI for relative resolution, or {@code null}.
+     * @param systemId The system identifier of the unresolved entity.
+     * @return An {@link InputSource} to permit the lookup, or {@code null} to 
skip it silently; the default implementation never returns normally.
+     * @throws SAXException to deny the lookup (the default behavior).
+     * @throws IOException  if a subclass opens a stream that fails.
+     */
+    protected InputSource onUnresolved(final String name, final String 
publicId, final String baseURI, final String systemId)
+            throws SAXException, IOException {
+        throw new SAXException(HardeningException.forbidden(name, null, 
publicId, systemId, baseURI));
+    }
+
+    private InputSource resolveWithDelegate(final String name, final String 
publicId, final String baseURI,
+            final String systemId) throws SAXException, IOException {
+        if (delegate != null) {
+            return delegate instanceof EntityResolver2 ? ((EntityResolver2) 
delegate).resolveEntity(name, publicId, baseURI, systemId) :
+                    // We need to resolve the systemId against baseURI, 
because a plain EntityResolver expects an absolute URI.
+                    delegate.resolveEntity(publicId, absolutize(baseURI, 
systemId));
+        }
+        return null;
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/FallbackDenyLSResourceResolver.java 
b/src/main/java/org/apache/commons/xml/FallbackDenyLSResourceResolver.java
new file mode 100644
index 0000000..60ab7b5
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/FallbackDenyLSResourceResolver.java
@@ -0,0 +1,55 @@
+/*
+ * 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 org.w3c.dom.ls.LSInput;
+import org.w3c.dom.ls.LSResourceResolver;
+
+/**
+ * {@link LSResourceResolver} floor: consults an optional caller-supplied 
resolver and denies (throws) whatever the caller does not resolve.
+ *
+ * <p>The schema-compile counterpart of {@link FallbackDenyEntityResolver2}. 
The hardened {@link javax.xml.validation.SchemaFactory}, {@link
+ * javax.xml.validation.Validator} and {@link 
javax.xml.validation.ValidatorHandler} wrappers install one of these and route 
a caller-set resolver through
+ * {@link #setDelegate} rather than letting it replace the floor. A caller 
opts a specific resource in by returning a non-{@code null} {@link LSInput};
+ * anything left unresolved is denied.</p>
+ */
+final class FallbackDenyLSResourceResolver implements LSResourceResolver {
+
+    private LSResourceResolver delegate;
+
+    FallbackDenyLSResourceResolver(final LSResourceResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    void setDelegate(final LSResourceResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    LSResourceResolver getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public LSInput resolveResource(final String type, final String 
namespaceURI, final String publicId, final String systemId, final String 
baseURI) {
+        final LSInput resolved = delegate != null ? 
delegate.resolveResource(type, namespaceURI, publicId, systemId, baseURI) : 
null;
+        if (resolved != null) {
+            return resolved;
+        }
+        throw new SecurityException(HardeningException.forbidden(type, 
namespaceURI, publicId, systemId, baseURI));
+    }
+}
diff --git a/src/main/java/org/apache/commons/xml/FallbackDenyURIResolver.java 
b/src/main/java/org/apache/commons/xml/FallbackDenyURIResolver.java
new file mode 100644
index 0000000..8f5acfa
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/FallbackDenyURIResolver.java
@@ -0,0 +1,56 @@
+/*
+ * 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.transform.Source;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.URIResolver;
+
+/**
+ * {@link URIResolver} floor: consults an optional caller-supplied resolver 
and denies (throws) whatever the caller does not resolve.
+ *
+ * <p>The XSLT counterpart of {@link FallbackDenyEntityResolver2}, guarding 
{@code xsl:import}/{@code xsl:include} at compile time and {@code document()} at
+ * transform time. The hardened {@link javax.xml.transform.TransformerFactory} 
and {@link javax.xml.transform.Transformer} wrappers install one of these and
+ * route a caller-set resolver through {@link #setDelegate} rather than 
letting it replace the floor. A caller opts a specific URI in by returning a
+ * non-{@code null} {@link Source}; anything left unresolved is denied.</p>
+ */
+final class FallbackDenyURIResolver implements URIResolver {
+
+    private URIResolver delegate;
+
+    FallbackDenyURIResolver(final URIResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    void setDelegate(final URIResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    URIResolver getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public Source resolve(final String href, final String base) throws 
TransformerException {
+        final Source resolved = delegate != null ? delegate.resolve(href, 
base) : null;
+        if (resolved != null) {
+            return resolved;
+        }
+        throw new TransformerException(HardeningException.forbidden("uri", 
null, null, href, base));
+    }
+}
diff --git a/src/main/java/org/apache/commons/xml/FallbackDenyXMLResolver.java 
b/src/main/java/org/apache/commons/xml/FallbackDenyXMLResolver.java
new file mode 100644
index 0000000..b5e1812
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/FallbackDenyXMLResolver.java
@@ -0,0 +1,84 @@
+/*
+ * 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.XMLResolver;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.Source;
+
+/**
+ * {@link XMLResolver} floor: consults an optional caller-supplied resolver 
and denies (throws) whatever the caller does not resolve.
+ *
+ * <p>The StAX counterpart of {@link FallbackDenyEntityResolver2}, installed 
on each entity-resolution hook. The hardened {@link 
javax.xml.stream.XMLInputFactory}
+ * wrapper routes a caller-set resolver through {@link #setDelegate} rather 
than letting it replace the floor. A caller opts a specific entity in by 
returning
+ * a non-{@code null} result; anything left unresolved goes to {@link 
#onUnresolved}, which denies by default. Subclasses override {@code 
onUnresolved} to give
+ * a hook a different unresolved policy (e.g. return an empty input for the 
external DTD subset, or for undeclared entities) while keeping the 
caller-delegate
+ * behavior.</p>
+ */
+class FallbackDenyXMLResolver implements XMLResolver {
+
+    private XMLResolver delegate;
+
+    FallbackDenyXMLResolver(final XMLResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    final void setDelegate(final XMLResolver delegate) {
+        this.delegate = delegate;
+    }
+
+    final XMLResolver getDelegate() {
+        return delegate;
+    }
+
+    @Override
+    public final Object resolveEntity(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
+        final Object resolved = delegate != null ? 
delegate.resolveEntity(publicID, systemID, baseURI, namespace) : null;
+        return resolved != null ? resolved : onUnresolved(publicID, systemID, 
baseURI, namespace);
+    }
+
+    /**
+     * Outcome when the caller delegate does not resolve the entity. Denies by 
default; a subclass may return an {@link java.io.InputStream}, {@link Source} or
+     * other {@link XMLResolver}-supported value (for example an empty input) 
instead of calling {@code super}, or {@code throw}
+     * {@link #denied(String, String, String, String)} to deny only some 
lookups.
+     *
+     * @param publicID The public identifier, or {@code null} if none.
+     * @param systemID The system identifier of the unresolved entity.
+     * @param baseURI  The base URI for relative resolution, or {@code null}.
+     * @param namespace The namespace (or, for Woodstox, the entity name), or 
{@code null}.
+     * @return The replacement input, or a value the caller's parser accepts; 
the default implementation never returns normally.
+     * @throws XMLStreamException to deny the lookup (the default behavior).
+     */
+    protected Object onUnresolved(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
+        throw denied(publicID, systemID, baseURI, namespace);
+    }
+
+    /**
+     * Builds the standard "forbidden by hardening" exception for a denied 
lookup, so a subclass with a mixed policy can reuse the deny outcome for the
+     * lookups it refuses.
+     *
+     * @param publicID The public identifier, or {@code null} if none.
+     * @param systemID The system identifier of the unresolved entity.
+     * @param baseURI  The base URI for relative resolution, or {@code null}.
+     * @param namespace The namespace (or, for Woodstox, the entity name), or 
{@code null}.
+     * @return The exception to throw.
+     */
+    protected final XMLStreamException denied(final String publicID, final 
String systemID, final String baseURI, final String namespace) {
+        return new XMLStreamException(HardeningException.forbidden(null, 
namespace, publicID, systemID, baseURI));
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/FallbackIgnoreXMLResolver.java 
b/src/main/java/org/apache/commons/xml/FallbackIgnoreXMLResolver.java
new file mode 100644
index 0000000..38f1a0c
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/FallbackIgnoreXMLResolver.java
@@ -0,0 +1,47 @@
+/*
+ * 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 java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLStreamException;
+
+/**
+ * {@link FallbackDenyXMLResolver} variant whose unresolved policy returns an 
empty input instead of throwing, so the parse continues with no replacement
+ * content. Used on Woodstox's DTD-subset and undeclared-entity hooks (where a 
missing resource must be skipped, not denied), while still consulting an
+ * optional caller-supplied resolver first.
+ */
+class FallbackIgnoreXMLResolver extends FallbackDenyXMLResolver {
+
+    /**
+     * Empty {@link ByteArrayInputStream} shared across every call. {@code 
read()} on a zero-length array always returns {@code -1}, so reusing the 
instance
+     * is safe even though the type is technically stateful.
+     */
+    private static final InputStream EMPTY = new ByteArrayInputStream(new 
byte[0]);
+
+    FallbackIgnoreXMLResolver(final XMLResolver delegate) {
+        super(delegate);
+    }
+
+    @Override
+    protected Object onUnresolved(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
+        return EMPTY;
+    }
+}
diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java 
b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java
index bb0b6a8..44a3ef3 100644
--- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java
+++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilder.java
@@ -32,7 +32,7 @@
 /**
  * {@link DocumentBuilder} wrapper that keeps a deny-all {@link 
EntityResolver} as a non-overridable floor.
  *
- * <p>A caller-set resolver is sandwiched inside a {@link 
Resolvers.FallbackDenyResolver} instead of replacing the deny-all one, so an 
external lookup the
+ * <p>A caller-set resolver is sandwiched inside a {@link 
FallbackDenyEntityResolver2} instead of replacing the deny-all one, so an 
external lookup the
  * caller's resolver does not satisfy is denied rather than fetched. {@link 
#reset()} re-establishes the bare deny-all floor, matching the just-constructed
  * state.</p>
  */
@@ -40,7 +40,7 @@ final class HardeningDocumentBuilder extends DocumentBuilder {
 
     private final DocumentBuilder delegate;
 
-    private final Resolvers.FallbackDenyResolver floor = new 
Resolvers.FallbackDenyResolver(null);
+    private final FallbackDenyEntityResolver2 floor = new 
FallbackDenyEntityResolver2(null);
 
     HardeningDocumentBuilder(final DocumentBuilder delegate) {
         this.delegate = delegate;
diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
index 537f40e..dcdbb54 100644
--- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java
@@ -36,7 +36,7 @@
  *
  * <p>Three layers cooperate:</p>
  * <ol>
- *   <li>{@link HardeningSchemaFactory} installs a deny-all {@link 
Resolvers.FallbackDenyLSResourceResolver} floor on the factory (blocking
+ *   <li>{@link HardeningSchemaFactory} installs a deny-all {@link 
FallbackDenyLSResourceResolver} 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 XmlFactories#harden(Source)}.</li>
  *   <li>{@link HardeningSchema} wraps every Validator/ValidatorHandler the 
inner Schema produces and re-installs the floor on each (blocking
@@ -55,7 +55,7 @@ final class HardeningSchemaFactory extends SchemaFactory {
 
     private final SchemaFactory delegate;
 
-    private final Resolvers.FallbackDenyLSResourceResolver floor = new 
Resolvers.FallbackDenyLSResourceResolver(null);
+    private final FallbackDenyLSResourceResolver floor = new 
FallbackDenyLSResourceResolver(null);
 
     HardeningSchemaFactory(final SchemaFactory delegate) {
         this.delegate = delegate;
diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformer.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
index 577051b..de18485 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformer.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformer.java
@@ -39,11 +39,11 @@ final class HardeningTransformer extends Transformer {
 
     private final Transformer delegate;
 
-    private final Resolvers.FallbackDenyURIResolver floor;
+    private final FallbackDenyURIResolver floor;
 
     HardeningTransformer(final Transformer delegate, final URIResolver 
uriResolver) {
         this.delegate = delegate;
-        this.floor = new Resolvers.FallbackDenyURIResolver(uriResolver);
+        this.floor = new FallbackDenyURIResolver(uriResolver);
         delegate.setURIResolver(floor);
     }
 
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
index 8d61bc6..b12a1a4 100644
--- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java
@@ -60,7 +60,7 @@ final class HardeningTransformerFactory extends 
SAXTransformerFactory {
 
     private final SAXTransformerFactory delegate;
 
-    private final Resolvers.FallbackDenyURIResolver floor = new 
Resolvers.FallbackDenyURIResolver(null);
+    private final FallbackDenyURIResolver floor = new 
FallbackDenyURIResolver(null);
 
     HardeningTransformerFactory(final SAXTransformerFactory delegate) {
         this.delegate = delegate;
diff --git a/src/main/java/org/apache/commons/xml/HardeningValidator.java 
b/src/main/java/org/apache/commons/xml/HardeningValidator.java
index 8eaa4b2..b3790c0 100644
--- a/src/main/java/org/apache/commons/xml/HardeningValidator.java
+++ b/src/main/java/org/apache/commons/xml/HardeningValidator.java
@@ -39,7 +39,7 @@ final class HardeningValidator extends Validator {
 
     private final Validator delegate;
 
-    private final Resolvers.FallbackDenyLSResourceResolver floor = new 
Resolvers.FallbackDenyLSResourceResolver(null);
+    private final FallbackDenyLSResourceResolver floor = new 
FallbackDenyLSResourceResolver(null);
 
     HardeningValidator(final Validator delegate) {
         this.delegate = delegate;
diff --git 
a/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java 
b/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java
index aa56dd8..f06c418 100644
--- a/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java
+++ b/src/main/java/org/apache/commons/xml/HardeningValidatorHandler.java
@@ -33,13 +33,13 @@
  * {@link ValidatorHandler} wrapper that keeps a deny-all {@link 
LSResourceResolver} floor a caller cannot remove.
  *
  * <p>Blocks {@code xsi:schemaLocation} resolution during SAX-driven 
validation. A caller-set resolver is routed through a {@link
- * Resolvers.FallbackDenyLSResourceResolver} rather than replacing the floor, 
so a schema the caller does not resolve is denied instead of fetched.</p>
+ * FallbackDenyLSResourceResolver} rather than replacing the floor, so a 
schema the caller does not resolve is denied instead of fetched.</p>
  */
 final class HardeningValidatorHandler extends ValidatorHandler {
 
     private final ValidatorHandler delegate;
 
-    private final Resolvers.FallbackDenyLSResourceResolver floor = new 
Resolvers.FallbackDenyLSResourceResolver(null);
+    private final FallbackDenyLSResourceResolver floor = new 
FallbackDenyLSResourceResolver(null);
 
     HardeningValidatorHandler(final ValidatorHandler delegate) {
         this.delegate = delegate;
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
index fe63e35..66eee4d 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java
@@ -32,13 +32,13 @@
 import javax.xml.transform.Source;
 
 /**
- * {@link XMLInputFactory} wrapper that keeps the {@link 
Resolvers.FallbackDenyXMLResolver} floors {@link StaxHardener} installs on the 
entity-resolution hooks
+ * {@link XMLInputFactory} wrapper that keeps the {@link 
FallbackDenyXMLResolver} floors {@link StaxHardener} installs on the 
entity-resolution hooks
  * non-removable by the caller.
  *
  * <p>Every resolver-valued entry point ({@link #setXMLResolver(XMLResolver)}, 
{@code setProperty(XMLInputFactory.RESOLVER, ...)} and the Woodstox
- * {@code com.ctc.wstx.*Resolver} keys) is routed uniformly: a caller who 
supplies their own {@link Resolvers.FallbackDenyXMLResolver} takes control and 
it is
+ * {@code com.ctc.wstx.*Resolver} keys) is routed uniformly: a caller who 
supplies their own {@link FallbackDenyXMLResolver} takes control and it is
  * passed straight to the delegate; otherwise the current resolver on that 
hook is read, and if it is one of our floors the caller's resolver is set as its
- * {@link Resolvers.FallbackDenyXMLResolver#setDelegate delegate} (an opt-in 
the floor cannot be removed by), or, if the hook is empty, the caller's 
resolver is
+ * {@link FallbackDenyXMLResolver#setDelegate delegate} (an opt-in the floor 
cannot be removed by), or, if the hook is empty, the caller's resolver is
  * wrapped in a fresh floor. This matters because Woodstox does not chain 
resolvers: when a resolver returns {@code null}, {@code DefaultInputResolver} 
falls
  * through to fetching the systemId URL itself, so a caller-set resolver that 
returns {@code null} must still land behind the floor. {@link 
#getXMLResolver()} and
  * {@code getProperty} report the caller's resolver unwrapped.</p>
@@ -83,18 +83,18 @@ public Object getProperty(final String name) {
      * Routes a caller-set resolver for the property {@code name} behind the 
floor currently installed on that hook.
      *
      * @param name     The resolver-valued property being set.
-     * @param resolver The caller's resolver, or their own {@link 
Resolvers.FallbackDenyXMLResolver} to take control.
+     * @param resolver The caller's resolver, or their own {@link 
FallbackDenyXMLResolver} to take control.
      */
     private void setResolverProperty(final String name, final XMLResolver 
resolver) {
-        if (resolver instanceof Resolvers.FallbackDenyXMLResolver) {
+        if (resolver instanceof FallbackDenyXMLResolver) {
             // The caller supplies their own floor: hand it to the delegate 
as-is.
             delegate.setProperty(name, resolver);
         } else {
             final Object current = delegate.getProperty(name);
-            if (current instanceof Resolvers.FallbackDenyXMLResolver) {
-                ((Resolvers.FallbackDenyXMLResolver) 
current).setDelegate(resolver);
+            if (current instanceof FallbackDenyXMLResolver) {
+                ((FallbackDenyXMLResolver) current).setDelegate(resolver);
             } else {
-                delegate.setProperty(name, new 
Resolvers.FallbackDenyXMLResolver(resolver));
+                delegate.setProperty(name, new 
FallbackDenyXMLResolver(resolver));
             }
         }
     }
@@ -107,7 +107,7 @@ private static boolean isResolverProperty(final String 
name) {
     }
 
     private static XMLResolver unwrap(final XMLResolver resolver) {
-        return resolver instanceof Resolvers.FallbackDenyXMLResolver ? 
((Resolvers.FallbackDenyXMLResolver) resolver).getDelegate() : resolver;
+        return resolver instanceof FallbackDenyXMLResolver ? 
((FallbackDenyXMLResolver) resolver).getDelegate() : resolver;
     }
 
     // <editor-fold defaultstate="collapsed" desc="Trivial delegation">
diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLReader.java 
b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java
index 763f05a..666ccf2 100644
--- a/src/main/java/org/apache/commons/xml/HardeningXMLReader.java
+++ b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java
@@ -30,14 +30,14 @@
 import org.xml.sax.XMLReader;
 
 /**
- * {@link XMLReader} wrapper that keeps a {@link 
Resolvers.FallbackDenyResolver} floor as the reader's entity resolver, 
non-overridable by the caller.
+ * {@link XMLReader} wrapper that keeps a {@link FallbackDenyEntityResolver2} 
floor as the reader's entity resolver, non-overridable by the caller.
  *
  * <p>The floor is installed once and stays the reader's entity resolver for 
the wrapper's lifetime; {@link #setEntityResolver(EntityResolver)} routes the
- * caller's resolver through {@link 
Resolvers.FallbackDenyResolver#setDelegate} instead of replacing it. This 
includes the {@code DefaultHandler} that
+ * caller's resolver through {@link FallbackDenyEntityResolver2#setDelegate} 
instead of replacing it. This includes the {@code DefaultHandler} that
  * {@link javax.xml.parsers.SAXParser#parse(org.xml.sax.InputSource, 
org.xml.sax.helpers.DefaultHandler) SAXParser.parse(source, handler)} installs 
as the
  * reader's entity resolver, which would otherwise silently replace the floor. 
{@link #getEntityResolver()} reports the caller's resolver unwrapped.</p>
  *
- * <p>A path that needs a non-deny floor (e.g. one that also permits the 
external DTD subset) passes a {@link Resolvers.FallbackDenyResolver} subclass 
to the
+ * <p>A path that needs a non-deny floor (e.g. one that also permits the 
external DTD subset) passes a {@link FallbackDenyEntityResolver2} subclass to 
the
  * two-argument constructor; a single stable floor instance also lets that 
subclass double as a {@link org.xml.sax.ext.LexicalHandler}. Every other method
  * forwards to the wrapped delegate; subclasses (e.g. {@code 
HardeningExpatXMLReader}) add per-implementation fixups on top of the floor.</p>
  */
@@ -45,13 +45,13 @@ class HardeningXMLReader implements XMLReader {
 
     private final XMLReader delegate;
 
-    private final Resolvers.FallbackDenyResolver floor;
+    private final FallbackDenyEntityResolver2 floor;
 
     HardeningXMLReader(final XMLReader delegate) {
-        this(delegate, new Resolvers.FallbackDenyResolver(null));
+        this(delegate, new FallbackDenyEntityResolver2(null));
     }
 
-    HardeningXMLReader(final XMLReader delegate, final 
Resolvers.FallbackDenyResolver floor) {
+    HardeningXMLReader(final XMLReader delegate, final 
FallbackDenyEntityResolver2 floor) {
         this.delegate = delegate;
         this.floor = floor;
         delegate.setEntityResolver(floor);
diff --git a/src/main/java/org/apache/commons/xml/Resolvers.java 
b/src/main/java/org/apache/commons/xml/Resolvers.java
deleted file mode 100644
index 3a7d92e..0000000
--- a/src/main/java/org/apache/commons/xml/Resolvers.java
+++ /dev/null
@@ -1,308 +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 java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.URI;
-import java.net.URISyntaxException;
-
-import javax.xml.stream.XMLResolver;
-import javax.xml.stream.XMLStreamException;
-import javax.xml.transform.Source;
-import javax.xml.transform.TransformerException;
-import javax.xml.transform.URIResolver;
-
-import org.w3c.dom.ls.LSInput;
-import org.w3c.dom.ls.LSResourceResolver;
-import org.xml.sax.EntityResolver;
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-import org.xml.sax.ext.DefaultHandler2;
-import org.xml.sax.ext.EntityResolver2;
-
-/**
- * Policy resolvers that fix the outcome of every external lookup. Each member 
is a floor with two defining properties:
- *
- * <ol>
- * <li><strong>Non-removable, and it wraps the resolver the caller 
sets.</strong> The hardened wrappers install one and route a caller-set 
resolver through
- * {@code setDelegate} rather than letting it replace the floor, so the 
caller's resolver is consulted first but cannot remove the floor underneath 
it.</li>
- * <li><strong>It supplies the default action for a lookup the caller's 
resolver does not resolve</strong> (a {@code null} return, or no caller 
resolver at
- * all). This is where a floor departs from stock JAXP: normally an unresolved 
lookup falls back to the processor's built-in resolution and the resource is
- * <em>fetched</em>; a floor instead <em>denies</em> it (throws).</li>
- * </ol>
- *
- * <p>The deny members are {@link FallbackDenyResolver} (for {@code 
EntityResolver}), {@link FallbackDenyLSResourceResolver} (for {@link 
LSResourceResolver}),
- * {@link FallbackDenyURIResolver} (for {@link URIResolver}) and {@link 
FallbackDenyXMLResolver} (for {@link XMLResolver}). {@link 
FallbackIgnoreXMLResolver} is a
- * variant whose default action returns an empty input instead of throwing, 
for the Woodstox DTD-subset and undeclared-entity hooks where a missing 
resource must
- * be skipped rather than denied.</p>
- */
-final class Resolvers {
-
-    /**
-     * Entity resolver that consults an optional caller-supplied resolver and 
denies (throws) whatever the caller does not resolve.
-     *
-     * <p>This is the entity-resolution counterpart of the JAXP 1.5 {@code 
ACCESS_EXTERNAL_*} properties: a non-overridable floor. The hardened DOM and SAX
-     * wrappers install one of these and, when the caller sets their own 
{@link EntityResolver}, route it through {@link #setDelegate} rather than 
letting it
-     * replace the floor. A caller therefore opts a specific resource in by 
returning a non-{@code null} {@link InputSource} from their resolver; anything 
they
-     * leave unresolved (a {@code null} return, or no caller resolver at all) 
goes to {@link #onUnresolved}, which denies by default.</p>
-     *
-     * <p>It extends {@link DefaultHandler2} so it is also usable as a {@link 
org.xml.sax.ext.LexicalHandler} (see {@code SAXParserHardener}'s Android 
subclass,
-     * which needs {@code startDTD}/{@code endDTD}); {@link 
#getExternalSubset} therefore inherits the {@code DefaultHandler2} "no 
synthetic subset" default. Only
-     * {@link #resolveEntity(String, String, String, String) resolveEntity} 
(the actual external fetch) reaches the deny fallback.</p>
-     */
-    static class FallbackDenyResolver extends DefaultHandler2 {
-
-        /**
-         * Caller-supplied resolver consulted first, or {@code null} for a 
pure deny-all floor.
-         */
-        private EntityResolver delegate;
-
-        /**
-         * Resolves {@code systemId} against {@code baseURI}.
-         *
-         * @param baseURI  The absolute base URI to resolve against, or {@code 
null} if none is available.
-         * @param systemId The system identifier, possibly relative to {@code 
baseURI}.
-         * @return The absolutized system identifier, or {@code systemId} 
unchanged when it cannot or need not be resolved.
-         */
-        private static String absolutize(final String baseURI, final String 
systemId) {
-            if (systemId == null || baseURI == null) {
-                return systemId;
-            }
-            try {
-                final URI system = new URI(systemId);
-                return system.isAbsolute() ? systemId : new 
URI(baseURI).resolve(system).toString();
-            } catch (final URISyntaxException e) {
-                return systemId;
-            }
-        }
-
-        FallbackDenyResolver(final EntityResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        /**
-         * Replaces the caller resolver consulted ahead of the floor; lets a 
single floor instance back successive {@code setEntityResolver} calls.
-         *
-         * @param delegate The caller-supplied resolver, or {@code null} for a 
pure deny-all floor.
-         */
-        final void setDelegate(final EntityResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        final EntityResolver getDelegate() {
-            return delegate;
-        }
-
-        @Override
-        public final InputSource resolveEntity(final String publicId, final 
String systemId) throws SAXException, IOException {
-            return resolveEntity(null, publicId, null, systemId);
-        }
-
-        @Override
-        public final InputSource resolveEntity(final String name, final String 
publicId, final String baseURI, final String systemId)
-                throws SAXException, IOException {
-            final InputSource resolved = resolveWithDelegate(name, publicId, 
baseURI, systemId);
-            return resolved != null ? resolved : onUnresolved(name, publicId, 
baseURI, systemId);
-        }
-
-        /**
-         * Outcome when neither the caller delegate nor this resolver provides 
the entity. Denies by default; a subclass may permit specific lookups (e.g. the
-         * external DTD subset) by returning {@code null} or an {@link 
InputSource} instead of calling {@code super}.
-         *
-         * @param name     The entity name, or {@code null} on the 2-arg 
resolution path.
-         * @param publicId The public identifier, or {@code null} if none.
-         * @param baseURI  The base URI for relative resolution, or {@code 
null}.
-         * @param systemId The system identifier of the unresolved entity.
-         * @return An {@link InputSource} to permit the lookup, or {@code 
null} to skip it silently; the default implementation never returns normally.
-         * @throws SAXException to deny the lookup (the default behavior).
-         * @throws IOException  if a subclass opens a stream that fails.
-         */
-        protected InputSource onUnresolved(final String name, final String 
publicId, final String baseURI, final String systemId)
-                throws SAXException, IOException {
-            throw new SAXException(HardeningException.forbidden(name, null, 
publicId, systemId, baseURI));
-        }
-
-        private InputSource resolveWithDelegate(final String name, final 
String publicId, final String baseURI,
-                final String systemId) throws SAXException, IOException {
-            if (delegate != null) {
-                return delegate instanceof EntityResolver2 ? 
((EntityResolver2) delegate).resolveEntity(name, publicId, baseURI, systemId) :
-                        // We need to resolve the systemId against baseURI, 
because a plain EntityResolver expects an absolute URI.
-                        delegate.resolveEntity(publicId, absolutize(baseURI, 
systemId));
-            }
-            return null;
-        }
-    }
-
-    /**
-     * {@link LSResourceResolver} floor: consults an optional caller-supplied 
resolver and denies (throws) whatever the caller does not resolve.
-     *
-     * <p>The schema-compile counterpart of {@link FallbackDenyResolver}. The 
hardened {@link javax.xml.validation.SchemaFactory}, {@link
-     * javax.xml.validation.Validator} and {@link 
javax.xml.validation.ValidatorHandler} wrappers install one of these and route 
a caller-set resolver through
-     * {@link #setDelegate} rather than letting it replace the floor. A caller 
opts a specific resource in by returning a non-{@code null} {@link LSInput};
-     * anything left unresolved is denied.</p>
-     */
-    static final class FallbackDenyLSResourceResolver implements 
LSResourceResolver {
-
-        private LSResourceResolver delegate;
-
-        FallbackDenyLSResourceResolver(final LSResourceResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        void setDelegate(final LSResourceResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        LSResourceResolver getDelegate() {
-            return delegate;
-        }
-
-        @Override
-        public LSInput resolveResource(final String type, final String 
namespaceURI, final String publicId, final String systemId, final String 
baseURI) {
-            final LSInput resolved = delegate != null ? 
delegate.resolveResource(type, namespaceURI, publicId, systemId, baseURI) : 
null;
-            if (resolved != null) {
-                return resolved;
-            }
-            throw new SecurityException(HardeningException.forbidden(type, 
namespaceURI, publicId, systemId, baseURI));
-        }
-    }
-
-    /**
-     * {@link URIResolver} floor: consults an optional caller-supplied 
resolver and denies (throws) whatever the caller does not resolve.
-     *
-     * <p>The XSLT counterpart of {@link FallbackDenyResolver}, guarding 
{@code xsl:import}/{@code xsl:include} at compile time and {@code document()} at
-     * transform time. The hardened {@link 
javax.xml.transform.TransformerFactory} and {@link 
javax.xml.transform.Transformer} wrappers install one of these and
-     * route a caller-set resolver through {@link #setDelegate} rather than 
letting it replace the floor. A caller opts a specific URI in by returning a
-     * non-{@code null} {@link Source}; anything left unresolved is denied.</p>
-     */
-    static final class FallbackDenyURIResolver implements URIResolver {
-
-        private URIResolver delegate;
-
-        FallbackDenyURIResolver(final URIResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        void setDelegate(final URIResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        URIResolver getDelegate() {
-            return delegate;
-        }
-
-        @Override
-        public Source resolve(final String href, final String base) throws 
TransformerException {
-            final Source resolved = delegate != null ? delegate.resolve(href, 
base) : null;
-            if (resolved != null) {
-                return resolved;
-            }
-            throw new TransformerException(HardeningException.forbidden("uri", 
null, null, href, base));
-        }
-    }
-
-    /**
-     * {@link XMLResolver} floor: consults an optional caller-supplied 
resolver and denies (throws) whatever the caller does not resolve.
-     *
-     * <p>The StAX counterpart of {@link FallbackDenyResolver}, installed on 
each entity-resolution hook. The hardened {@link 
javax.xml.stream.XMLInputFactory}
-     * wrapper routes a caller-set resolver through {@link #setDelegate} 
rather than letting it replace the floor. A caller opts a specific entity in by 
returning
-     * a non-{@code null} result; anything left unresolved goes to {@link 
#onUnresolved}, which denies by default. Subclasses override {@code 
onUnresolved} to give
-     * a hook a different unresolved policy (e.g. return an empty input for 
the external DTD subset, or for undeclared entities) while keeping the 
caller-delegate
-     * behavior.</p>
-     */
-    static class FallbackDenyXMLResolver implements XMLResolver {
-
-        private XMLResolver delegate;
-
-        FallbackDenyXMLResolver(final XMLResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        final void setDelegate(final XMLResolver delegate) {
-            this.delegate = delegate;
-        }
-
-        final XMLResolver getDelegate() {
-            return delegate;
-        }
-
-        @Override
-        public final Object resolveEntity(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
-            final Object resolved = delegate != null ? 
delegate.resolveEntity(publicID, systemID, baseURI, namespace) : null;
-            return resolved != null ? resolved : onUnresolved(publicID, 
systemID, baseURI, namespace);
-        }
-
-        /**
-         * Outcome when the caller delegate does not resolve the entity. 
Denies by default; a subclass may return an {@link java.io.InputStream}, {@link 
Source} or
-         * other {@link XMLResolver}-supported value (for example an empty 
input) instead of calling {@code super}, or {@code throw}
-         * {@link #denied(String, String, String, String)} to deny only some 
lookups.
-         *
-         * @param publicID The public identifier, or {@code null} if none.
-         * @param systemID The system identifier of the unresolved entity.
-         * @param baseURI  The base URI for relative resolution, or {@code 
null}.
-         * @param namespace The namespace (or, for Woodstox, the entity name), 
or {@code null}.
-         * @return The replacement input, or a value the caller's parser 
accepts; the default implementation never returns normally.
-         * @throws XMLStreamException to deny the lookup (the default 
behavior).
-         */
-        protected Object onUnresolved(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
-            throw denied(publicID, systemID, baseURI, namespace);
-        }
-
-        /**
-         * Builds the standard "forbidden by hardening" exception for a denied 
lookup, so a subclass with a mixed policy can reuse the deny outcome for the
-         * lookups it refuses.
-         *
-         * @param publicID The public identifier, or {@code null} if none.
-         * @param systemID The system identifier of the unresolved entity.
-         * @param baseURI  The base URI for relative resolution, or {@code 
null}.
-         * @param namespace The namespace (or, for Woodstox, the entity name), 
or {@code null}.
-         * @return The exception to throw.
-         */
-        protected final XMLStreamException denied(final String publicID, final 
String systemID, final String baseURI, final String namespace) {
-            return new XMLStreamException(HardeningException.forbidden(null, 
namespace, publicID, systemID, baseURI));
-        }
-    }
-
-    /**
-     * {@link FallbackDenyXMLResolver} variant whose unresolved policy returns 
an empty input instead of throwing, so the parse continues with no replacement
-     * content. Used on Woodstox's DTD-subset and undeclared-entity hooks 
(where a missing resource must be skipped, not denied), while still consulting 
an
-     * optional caller-supplied resolver first.
-     */
-    static class FallbackIgnoreXMLResolver extends FallbackDenyXMLResolver {
-
-        /**
-         * Empty {@link ByteArrayInputStream} shared across every call. {@code 
read()} on a zero-length array always returns {@code -1}, so reusing the 
instance
-         * is safe even though the type is technically stateful.
-         */
-        private static final InputStream EMPTY = new ByteArrayInputStream(new 
byte[0]);
-
-        FallbackIgnoreXMLResolver(final XMLResolver delegate) {
-            super(delegate);
-        }
-
-        @Override
-        protected Object onUnresolved(final String publicID, final String 
systemID, final String baseURI, final String namespace) throws 
XMLStreamException {
-            return EMPTY;
-        }
-    }
-
-    private Resolvers() {
-    }
-}
diff --git a/src/main/java/org/apache/commons/xml/SAXParserHardener.java 
b/src/main/java/org/apache/commons/xml/SAXParserHardener.java
index 5e0d67c..cbc4b5d 100644
--- a/src/main/java/org/apache/commons/xml/SAXParserHardener.java
+++ b/src/main/java/org/apache/commons/xml/SAXParserHardener.java
@@ -61,11 +61,11 @@ final class SAXParserHardener {
      *
      * <p>Android's Expat routes every external fetch (subset, DOCTYPE {@code 
SYSTEM}, general/parameter entity) through the 2-arg
      * {@link EntityResolver#resolveEntity(String, String)}; a deny-all 
resolver there would also reject a DOCTYPE that merely <em>names</em> an unused 
external
-     * subset. As a {@link Resolvers.FallbackDenyResolver} it consults the 
caller's resolver first; as a {@link LexicalHandler} (via {@code 
DefaultHandler2}) it
+     * subset. As a {@link FallbackDenyEntityResolver2} it consults the 
caller's resolver first; as a {@link LexicalHandler} (via {@code 
DefaultHandler2}) it
      * tracks the declared subset's identifiers so {@link #onUnresolved} can 
tell the subset apart from a forbidden external general or parameter entity. It 
is
      * stateful, so a fresh instance is installed per reader.</p>
      */
-    private static final class DtdAwareDenyResolver extends 
Resolvers.FallbackDenyResolver {
+    private static final class DtdAwareDenyResolver extends 
FallbackDenyEntityResolver2 {
 
         private String dtdPublicId;
         private String dtdSystemId;
@@ -110,7 +110,7 @@ static final class HardeningExpatXMLReader extends 
HardeningXMLReader {
 
         private static final String NAMESPACE_PREFIXES_FEATURE = 
"http://xml.org/sax/features/namespace-prefixes";;
 
-        HardeningExpatXMLReader(final XMLReader delegate, final 
Resolvers.FallbackDenyResolver floor) {
+        HardeningExpatXMLReader(final XMLReader delegate, final 
FallbackDenyEntityResolver2 floor) {
             super(delegate, floor);
         }
 
diff --git a/src/main/java/org/apache/commons/xml/StaxHardener.java 
b/src/main/java/org/apache/commons/xml/StaxHardener.java
index 128b44a..57739d6 100644
--- a/src/main/java/org/apache/commons/xml/StaxHardener.java
+++ b/src/main/java/org/apache/commons/xml/StaxHardener.java
@@ -28,7 +28,7 @@
  * <ul>
  *     <li><strong>External DTD subset</strong>: skipped via Zephyr's {@value 
#ZEPHYR_IGNORE_EXTERNAL_DTD} (best-effort), so a DOCTYPE-only document parses
  *         without a fetch attempt instead of tripping the deny-all resolver 
below. Woodstox skips it through {@value #WSTX_DTD_RESOLVER} instead.</li>
- *     <li><strong>External entities</strong>: denied through a non-removable 
{@link Resolvers.FallbackDenyXMLResolver} floor on the entity-resolution hook,
+ *     <li><strong>External entities</strong>: denied through a non-removable 
{@link FallbackDenyXMLResolver} floor on the entity-resolution hook,
  *         leaving the standard {@code SUPPORT_DTD} / {@code 
IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched. Woodstox exposes 
fine-grained hooks, so when all
  *         three apply the factory is Woodstox: {@value #WSTX_DTD_RESOLVER} 
(empty external subset, but a thrown error on external parameter entities, 
which share
  *         that hook), {@value #WSTX_ENTITY_RESOLVER} (the floor, denying 
declared external general entities) and {@value 
#WSTX_UNDECLARED_ENTITY_RESOLVER}
@@ -52,14 +52,14 @@ final class StaxHardener {
     private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = 
"http://java.sun.com/xml/stream/properties/ignore-external-dtd";;
 
     /**
-     * Woodstox DTD-subset floor: a {@link 
Resolvers.FallbackIgnoreXMLResolver} that returns the empty input for the 
external DTD subset (its inherited policy)
+     * Woodstox DTD-subset floor: a {@link FallbackIgnoreXMLResolver} that 
returns the empty input for the external DTD subset (its inherited policy)
      * but throws on external parameter entities.
      *
      * <p>Woodstox calls this hook with {@code entityName == null} for the 
subset and {@code entityName != null} for parameter-entity expansion (that
      * discriminator is the 4th {@code resolveEntity} argument; the JDK Zephyr 
always passes {@code null} there). Applied best-effort, ignored by 
implementations
      * that do not recognize the property.</p>
      */
-    private static final class DtdSubsetFloor extends 
Resolvers.FallbackIgnoreXMLResolver {
+    private static final class DtdSubsetFloor extends 
FallbackIgnoreXMLResolver {
 
         DtdSubsetFloor() {
             super(null);
@@ -84,10 +84,10 @@ static XMLInputFactory harden(final XMLInputFactory 
factory) {
         // HardeningXMLInputFactory, which routes a caller-set resolver into 
the floor rather than replacing it). The DTD-subset and undeclared-entity hooks 
skip
         // (empty input) rather than deny on an unresolved lookup, so a 
DOCTYPE-only document still parses.
         if (!(trySetProperty(factory, WSTX_DTD_RESOLVER, new DtdSubsetFloor())
-                && trySetProperty(factory, WSTX_ENTITY_RESOLVER, new 
Resolvers.FallbackDenyXMLResolver(null))
-                && trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, 
new Resolvers.FallbackIgnoreXMLResolver(null)))) {
+                && trySetProperty(factory, WSTX_ENTITY_RESOLVER, new 
FallbackDenyXMLResolver(null))
+                && trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, 
new FallbackIgnoreXMLResolver(null)))) {
             // Fallback (JDK Zephyr or unrecognized): the single resolver 
carries the deny-all floor.
-            factory.setXMLResolver(new 
Resolvers.FallbackDenyXMLResolver(null));
+            factory.setXMLResolver(new FallbackDenyXMLResolver(null));
         }
         return new HardeningXMLInputFactory(factory);
     }
diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java 
b/src/main/java/org/apache/commons/xml/TransformerHardener.java
index b0d1b30..493e5bb 100644
--- a/src/main/java/org/apache/commons/xml/TransformerHardener.java
+++ b/src/main/java/org/apache/commons/xml/TransformerHardener.java
@@ -41,7 +41,7 @@
  *         hardening surface is reachable only through a vendor API.</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 Resolvers.FallbackDenyURIResolver} floor</strong>: 
required. A deny-all {@link URIResolver} floor, installed by
+ *     <li><strong>{@link FallbackDenyURIResolver} floor</strong>: required. A 
deny-all {@link URIResolver} floor, installed by
  *         {@link HardeningTransformerFactory} and carried onto every produced 
{@link Transformer}, blocks {@code xsl:import}/{@code xsl:include} at compile 
time
  *         and {@code document()} at runtime, 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 drop the block.</li>
diff --git a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java 
b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java
index 42a473f..2762c5c 100644
--- a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java
+++ b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java
@@ -60,7 +60,7 @@
  *
  * <p>The observable contract on every hardened factory is the same: a 
resource the caller resolves (returns a non-null value) is allowed, but 
anything the
  * caller does not resolve is denied instead of fetched, so a resolver that 
resolves nothing leaves the block in place. Most factories enforce this with a
- * {@link Resolvers.FallbackDenyResolver}-style floor that consults the caller 
and denies on a {@code null} return; Saxon enforces the equivalent through its
+ * {@link FallbackDenyEntityResolver2}-style floor that consults the caller 
and denies on a {@code null} return; Saxon enforces the equivalent through its
  * {@code ALLOWED_PROTOCOLS} restrictor. Every resolver channel is exercised: 
the SAX/DOM {@link EntityResolver}, the StAX {@link XMLResolver}, the schema
  * {@link LSResourceResolver} and the XSLT {@link URIResolver}.</p>
  */
@@ -364,7 +364,7 @@ void transformerDeniesUnlisted() {
 
     /**
      * A hardened {@link TransformerFactory} with a re-throwing error 
listener. XSLTC and Xalan enforce the deny through the
-     * {@link Resolvers.FallbackDenyURIResolver} floor; Saxon enforces it 
through its {@code ALLOWED_PROTOCOLS} restrictor. Either way a caller-set 
resolver that
+     * {@link FallbackDenyURIResolver} floor; Saxon enforces it through its 
{@code ALLOWED_PROTOCOLS} restrictor. Either way a caller-set resolver that
      * returns {@code null} cannot re-open the fetch. The strict listener is 
required because interpretive Xalan routes a blocked {@code xsl:import} through 
the
      * error listener and would otherwise recover and compile instead of 
throwing (XSLTC and Saxon throw regardless).
      */
diff --git a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java 
b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
index 237d93e..80dbbc9 100644
--- a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
+++ b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java
@@ -53,22 +53,19 @@ class ShadingFootprintTest {
 
     private static final Set<String> DOCUMENT_BUILDER_HARDENER = set(
             "DocumentBuilderHardener", "HardeningDocumentBuilder", 
"HardeningDocumentBuilderFactory", CORE,
-            "Resolvers", "Resolvers$FallbackDenyResolver", 
"Resolvers$FallbackDenyLSResourceResolver", "Resolvers$FallbackDenyURIResolver",
-            "Resolvers$FallbackDenyXMLResolver", 
"Resolvers$FallbackIgnoreXMLResolver");
+            "FallbackDenyEntityResolver2");
 
     private static final Set<String> SAX_PARSER_HARDENER = set(
             "SAXParserHardener", "SAXParserHardener$DtdAwareDenyResolver", 
"SAXParserHardener$HardeningExpatXMLReader",
             "HardeningSAXParser", "HardeningSAXParserFactory", 
"HardeningXMLReader", CORE,
-            "Resolvers", "Resolvers$FallbackDenyResolver", 
"Resolvers$FallbackDenyLSResourceResolver", "Resolvers$FallbackDenyURIResolver",
-            "Resolvers$FallbackDenyXMLResolver", 
"Resolvers$FallbackIgnoreXMLResolver");
+            "FallbackDenyEntityResolver2");
 
     private static final Set<String> STAX_HARDENER = set(
             "StaxHardener", "StaxHardener$DtdSubsetFloor", 
"HardeningXMLInputFactory", CORE,
-            "Resolvers", "Resolvers$FallbackDenyResolver", 
"Resolvers$FallbackDenyLSResourceResolver", "Resolvers$FallbackDenyURIResolver",
-            "Resolvers$FallbackDenyXMLResolver", 
"Resolvers$FallbackIgnoreXMLResolver");
+            "FallbackDenyXMLResolver", "FallbackIgnoreXMLResolver");
 
     /** The TrAX/XPath/schema entry points all pull the whole library through 
{@link XmlFactories}; this is its class count (Phase 4 territory to reduce). */
-    private static final int WHOLE_LIBRARY_SIZE = 33;
+    private static final int WHOLE_LIBRARY_SIZE = 32;
 
     /** Entry points reported by the {@link #reportFootprint()} diagnostic, 
most-focused first, ending with the whole library. */
     private static final String[] REPORTED = {

Reply via email to