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

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

commit 2b7f52ec58bb2b98af895113e07057d0a131987d
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Apr 23 14:52:45 2026 +0200

    feat: add public API for hardened JAXP factories
    
    Introduce `XmlFactories` with one factory method per JAXP parser type
    (`DocumentBuilderFactory`, `SAXParserFactory`, `XMLInputFactory`,
    `TransformerFactory`, `XPathFactory`, `SchemaFactory`), the `XmlProvider`
    SPI that downstream implementations plug into via
    `java.util.ServiceLoader`, the internal `ProviderRegistry` that resolves
    a factory class to its responsible provider, and
    `UnsupportedXmlImplementationException` for the case where no provider
    matches.
    
    Each factory method's Javadoc documents which hardening knobs are
    applied (as a small table of settings and states), the known
    limitations for `TransformerFactory` and `SchemaFactory` (stylesheets
    and schemas read by an implementation-picked internal parser), and the
    top-level-URI caveat common to all JAXP input (`StreamSource(systemId)`
    and friends are fetched as-is).
    
    No bundled providers ship in this commit. The registry falls through to
    the `ServiceLoader`, which finds nothing on the classpath, so every
    `XmlFactories` call currently throws
    `UnsupportedXmlImplementationException`. Providers for the stock JDK,
    Apache Xerces, Woodstox and Saxon-HE, plus the `Hardening` helper they
    share, will land in follow-up commits.
    
    Assisted-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 .../UnsupportedXmlImplementationException.java     |  53 +++++
 .../apache/commons/xml/factory/XmlFactories.java   | 215 +++++++++++++++++++++
 .../xml/factory/internal/ProviderRegistry.java     | 108 +++++++++++
 .../commons/xml/factory/spi/XmlProvider.java       | 132 +++++++++++++
 .../factory/UnsupportedXmlImplementationTest.java  |  81 ++++++++
 .../xml/factory/internal/ProviderRegistryTest.java | 100 ++++++++++
 6 files changed, 689 insertions(+)

diff --git 
a/src/main/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationException.java
 
b/src/main/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationException.java
new file mode 100644
index 0000000..11ed207
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationException.java
@@ -0,0 +1,53 @@
+/*
+ * 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.factory;
+
+/**
+ * Thrown when no {@link org.apache.commons.xml.factory.spi.XmlProvider 
XmlProvider} is able to harden a JAXP factory of the given concrete class.
+ */
+public class UnsupportedXmlImplementationException extends 
IllegalStateException {
+
+    private static final long serialVersionUID = 1L;
+
+    private final Class<?> unsupportedFactoryClass;
+
+    /**
+     * Constructs a new exception naming the unsupported factory class.
+     *
+     * <p>The detail message identifies the class and suggests remediation 
steps.</p>
+     *
+     * @param unsupportedFactoryClass the concrete factory class that no 
provider recognises.
+     */
+    public UnsupportedXmlImplementationException(final Class<?> 
unsupportedFactoryClass) {
+        super(buildMessage(unsupportedFactoryClass));
+        this.unsupportedFactoryClass = unsupportedFactoryClass;
+    }
+
+    private static String buildMessage(final Class<?> factoryClass) {
+        return String.format("No XmlProvider supports JAXP factory class %s. 
Add a supported JAXP implementation (for example Apache Xerces) to the " +
+                "classpath, or register a custom XmlProvider via 
META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider.", 
factoryClass.getName());
+    }
+
+    /**
+     * Gets the concrete factory class that no registered provider could 
configure.
+     *
+     * @return the unsupported factory class.
+     */
+    public Class<?> getUnsupportedFactoryClass() {
+        return unsupportedFactoryClass;
+    }
+}
diff --git a/src/main/java/org/apache/commons/xml/factory/XmlFactories.java 
b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java
new file mode 100644
index 0000000..c6a9902
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java
@@ -0,0 +1,215 @@
+/*
+ * 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.factory;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+
+import org.apache.commons.xml.factory.internal.ProviderRegistry;
+import org.apache.commons.xml.factory.spi.XmlProvider;
+
+/**
+ * Entry point for obtaining hardened JAXP factories.
+ *
+ * <p>Every method on this class returns a <em>fresh, hardened</em> factory 
instance. No caching or pooling is performed; callers on a hot path are 
responsible
+ * for their own caching.</p>
+ *
+ * <p>Hardening blocks the secondary external-resource fetches an 
implementation would otherwise initiate while processing document content:</p>
+ *
+ * <ul>
+ *   <li>DOCTYPE declarations and external DTDs,</li>
+ *   <li>external general and parameter entities,</li>
+ *   <li>{@code xsi:schemaLocation} and {@code xsi:noNamespaceSchemaLocation} 
hints,</li>
+ *   <li>XInclude (where applicable),</li>
+ *   <li>{@code xsl:import}, {@code xsl:include} and {@code document()} in 
XSLT,</li>
+ *   <li>{@code doc()}, {@code collection()} and {@code unparsed-text()} in 
XPath 3.1+,</li>
+ *   <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} in XML 
Schema.</li>
+ * </ul>
+ *
+ * <p>A top-level URI passed directly by the caller is fetched as-is: {@code 
StreamSource(systemId)}, {@code DocumentBuilder.parse(String)}, or a
+ * {@code SAXSource} built from a system id all cause the JAXP implementation 
to open that URI without consulting the hardening layer. Use a
+ * {@link javax.xml.transform.URIResolver} or {@link 
org.xml.sax.EntityResolver} if you need to restrict the top-level fetch.</p>
+ *
+ * <p>The returned factories inherit the thread-safety properties of the 
underlying JAXP implementation, which in practice means they are <strong>not
+ * guaranteed to be thread-safe</strong>. Create a new factory per thread or 
synchronise externally.</p>
+ *
+ * <p>This class itself is thread-safe: all methods are static and 
stateless.</p>
+ */
+public final class XmlFactories {
+
+    /**
+     * Returns a fresh, hardened {@link DocumentBuilderFactory}.
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>DOCTYPE</td><td>forbidden</td></tr>
+     *   <tr><td>XInclude</td><td>disabled</td></tr>
+     *   <tr><td>DTD validation</td><td>disabled</td></tr>
+     * </table>
+     *
+     * <p>Together these block <strong>every</strong> external reference the 
parser would follow while reading a document through this factory.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying JAXP 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static DocumentBuilderFactory newDocumentBuilderFactory() {
+        final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    /**
+     * Returns a fresh, hardened {@link SAXParserFactory}.
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>DOCTYPE</td><td>forbidden</td></tr>
+     *   <tr><td>XInclude</td><td>disabled</td></tr>
+     *   <tr><td>DTD validation</td><td>disabled</td></tr>
+     * </table>
+     *
+     * <p>Together these block <strong>every</strong> external reference the 
parser would follow while reading a document through this factory.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying JAXP 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static SAXParserFactory newSAXParserFactory() {
+        final SAXParserFactory factory = SAXParserFactory.newInstance();
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    /**
+     * Returns a fresh, hardened {@link SchemaFactory} configured for W3C XML 
Schema ({@link XMLConstants#W3C_XML_SCHEMA_NS_URI}).
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>{@link 
XMLConstants#FEATURE_SECURE_PROCESSING}</td><td>enabled</td></tr>
+     *   <tr><td>{@link XMLConstants#ACCESS_EXTERNAL_DTD}</td><td>blocked (set 
to {@code ""})</td></tr>
+     *   <tr><td>{@link XMLConstants#ACCESS_EXTERNAL_SCHEMA}</td><td>blocked 
(set to {@code ""})</td></tr>
+     * </table>
+     *
+     * <p>Together these block <strong>every</strong> external reference 
({@code xs:import}, {@code xs:include}, {@code xs:redefine}, external DTDs) 
declared
+     * inside the schema that the factory compiles.</p>
+     *
+     * <p><strong>Limitation.</strong> The schema passed to {@link 
SchemaFactory#newSchema(javax.xml.transform.Source)} is read by a parser the
+     * implementation selects internally, which is not guaranteed to honour 
DOCTYPE-level hardening. Treat schemas as trusted, or pre-parse the schema 
through
+     * a hardened {@link #newDocumentBuilderFactory()}/{@link 
#newSAXParserFactory()} and pass a {@link javax.xml.transform.dom.DOMSource} or
+     * {@link javax.xml.transform.sax.SAXSource}.</p>
+     *
+     * <p></p>Validators created from the returned {@link 
javax.xml.validation.Schema} inherit the hardening for secondary fetches, but 
the XML input given to
+     * {@link 
javax.xml.validation.Validator#validate(javax.xml.transform.Source) 
Validator.validate(Source)} is subject to the same top-level-URI caveat as
+     * above.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying Schema 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static SchemaFactory newSchemaFactory() {
+        final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    /**
+     * Returns a fresh, hardened {@link TransformerFactory}.
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>{@link 
XMLConstants#FEATURE_SECURE_PROCESSING}</td><td>enabled</td></tr>
+     *   <tr><td>{@link XMLConstants#ACCESS_EXTERNAL_DTD}</td><td>blocked (set 
to {@code ""})</td></tr>
+     *   <tr><td>{@link 
XMLConstants#ACCESS_EXTERNAL_STYLESHEET}</td><td>blocked (set to {@code 
""})</td></tr>
+     * </table>
+     *
+     * <p>Together these block <strong>every</strong> external reference 
({@code xsl:import}, {@code xsl:include}, {@code document()}, external DTDs) 
declared
+     * in the stylesheet that the factory compiles.</p>
+     *
+     * <p><strong>Limitation.</strong> The stylesheet passed to {@link 
TransformerFactory#newTransformer(javax.xml.transform.Source)} and the XML 
input passed
+     * to {@code Transformer.transform(Source, Result)} are read by a parser 
the JAXP implementation selects internally, which is not guaranteed to honour
+     * DOCTYPE-level hardening. Treat stylesheets as trusted, or pre-parse the 
stylesheet and input through a hardened
+     * {@link #newDocumentBuilderFactory()}/{@link #newSAXParserFactory()} and 
pass them as {@link javax.xml.transform.dom.DOMSource} or
+     * {@link javax.xml.transform.sax.SAXSource}.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying TrAX 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static TransformerFactory newTransformerFactory() {
+        final TransformerFactory factory = TransformerFactory.newInstance();
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    /**
+     * Returns a fresh, hardened {@link XMLInputFactory}.
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>DOCTYPE</td><td>processing disabled</td></tr>
+     *   <tr><td>XInclude</td><td>N/A</td></tr>
+     *   <tr><td>DTD validation</td><td>disabled</td></tr>
+     * </table>
+     *
+     * <p>StAX has no XInclude, so nothing further is needed. Together these 
block <strong>every</strong> external reference the parser would follow while
+     * reading a document through this factory.</p>
+     *
+     * <p></p>Unlike DOM and SAX, a DOCTYPE is tolerated rather than rejected: 
the declaration is read, but the DTD body is never processed, so entity
+     * references declared in the DTD fail at the reference site. The settings 
map to {@link XMLInputFactory#SUPPORT_DTD} and
+     * {@link XMLInputFactory#IS_VALIDATING}.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying StAX 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static XMLInputFactory newXMLInputFactory() {
+        final XMLInputFactory factory = XMLInputFactory.newInstance();
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    /**
+     * Returns a fresh, hardened {@link XPathFactory} for the default XPath 
object model.
+     *
+     * <table>
+     *   <caption>Hardening applied</caption>
+     *   <tr><th>Setting</th><th>State</th></tr>
+     *   <tr><td>{@link 
XMLConstants#FEATURE_SECURE_PROCESSING}</td><td>enabled</td></tr>
+     * </table>
+     *
+     * <p>This blocks external-URI access from XPath 3.1+ functions such as 
{@code doc()}, {@code collection()} and {@code unparsed-text()} where the
+     * underlying implementation honours the flag.</p>
+     *
+     * @return a hardened factory.
+     * @throws UnsupportedXmlImplementationException if the underlying XPath 
implementation is not recognised by any registered {@link XmlProvider}.
+     */
+    public static XPathFactory newXPathFactory() {
+        final XPathFactory factory = XPathFactory.newInstance();
+        
ProviderRegistry.getInstance().providerFor(factory.getClass()).configure(factory);
+        return factory;
+    }
+
+    private XmlFactories() {
+        // static only
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java 
b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java
new file mode 100644
index 0000000..0beb077
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java
@@ -0,0 +1,108 @@
+/*
+ * 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.factory.internal;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.ServiceLoader;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+import org.apache.commons.xml.factory.UnsupportedXmlImplementationException;
+import org.apache.commons.xml.factory.spi.XmlProvider;
+
+/**
+ * Internal lookup for the {@link XmlProvider} responsible for a given 
concrete JAXP factory class.
+ *
+ * <h2>Dispatch order</h2>
+ *
+ * <ol>
+ *   <li>Bundled providers in a fixed order.</li>
+ *   <li>Third-party providers discovered via {@link ServiceLoader}, in the 
order {@link ServiceLoader} yields them.</li>
+ *   <li>If nothing matches, {@link 
UnsupportedXmlImplementationException}.</li>
+ * </ol>
+ *
+ * <p>Bundled providers are consulted first so that a third-party provider on 
the classpath cannot intercept hardening of a factory class this library already
+ * supports.</p>
+ *
+ * <p>Lookup results are cached keyed by factory class. The cache is 
thread-safe and only holds {@link XmlProvider} instances, never the factories
+ * themselves.</p>
+ */
+public final class ProviderRegistry {
+
+    private static final ProviderRegistry INSTANCE = new 
ProviderRegistry(bundledProviders());
+
+    private final List<XmlProvider> bundled;
+
+    private final ConcurrentMap<Class<?>, XmlProvider> cache = new 
ConcurrentHashMap<>();
+
+    /**
+     * Constructs a registry with the supplied bundled providers.
+     *
+     * <p>Package-private for test use; production code should call {@link 
#getInstance()}.</p>
+     */
+    ProviderRegistry(final List<XmlProvider> bundled) {
+        this.bundled = Collections.unmodifiableList(new ArrayList<>(bundled));
+    }
+
+    /**
+     * Gets the process-wide registry instance.
+     *
+     * @return the singleton.
+     */
+    public static ProviderRegistry getInstance() {
+        return INSTANCE;
+    }
+
+    /**
+     * Looks up the provider responsible for factories of the supplied 
concrete class.
+     *
+     * @param factoryClass the concrete factory class; never {@code null}.
+     * @return the matching provider.
+     * @throws UnsupportedXmlImplementationException if no provider matches.
+     */
+    public XmlProvider providerFor(final Class<?> factoryClass) {
+        Objects.requireNonNull(factoryClass);
+        return cache.computeIfAbsent(factoryClass, this::lookup);
+    }
+
+    private XmlProvider lookup(final Class<?> factoryClass) {
+        for (final XmlProvider provider : bundled) {
+            if (provider.supports(factoryClass)) {
+                return provider;
+            }
+        }
+        for (final XmlProvider provider : serviceLoaderProviders()) {
+            if (provider.supports(factoryClass)) {
+                return provider;
+            }
+        }
+        throw new UnsupportedXmlImplementationException(factoryClass);
+    }
+
+    private static List<XmlProvider> bundledProviders() {
+        return Collections.emptyList();
+    }
+
+    private static Iterable<XmlProvider> serviceLoaderProviders() {
+        final ClassLoader loader = ProviderRegistry.class.getClassLoader();
+        return ServiceLoader.load(XmlProvider.class, loader);
+    }
+
+}
diff --git a/src/main/java/org/apache/commons/xml/factory/spi/XmlProvider.java 
b/src/main/java/org/apache/commons/xml/factory/spi/XmlProvider.java
new file mode 100644
index 0000000..06d74d3
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/spi/XmlProvider.java
@@ -0,0 +1,132 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.commons.xml.factory.spi;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+
+/**
+ * Service-provider interface implemented by a single JAXP implementation's 
hardening logic.
+ *
+ * <p>Each provider understands one or more concrete factory classes and 
applies the implementation-specific feature and property settings needed to 
turn that
+ * factory into a safe-by-default parser.</p>
+ *
+ * <h2>Dispatch</h2>
+ *
+ * <p>Providers are consulted in a fixed order by {@link 
org.apache.commons.xml.factory.XmlFactories}. The first provider whose {@link 
#supports(Class)} method
+ * returns {@code true} for a vanilla factory's concrete class is asked to 
configure that factory. Bundled providers are consulted before
+ * {@link java.util.ServiceLoader}-discovered providers, so a user-supplied 
provider cannot shadow hardening for a JAXP implementation that this library 
ships
+ * support for.</p>
+ *
+ * <h2>Implementor contract</h2>
+ *
+ * <ul>
+ *   <li>{@link #supports(Class)} must return {@code true} <em>only</em> for 
factory classes for which this provider can implement <em>every</em> relevant
+ *       {@code configure} method.</li>
+ *   <li>Each {@code configure} method must either complete successfully with 
the factory fully hardened, or throw. Returning a partially-configured factory 
is
+ *       a bug; callers rely on the contract that a factory observed after a 
successful {@code configure} call has every relevant feature locked down. Wrap 
any
+ *       checked exception (for example {@link 
javax.xml.parsers.ParserConfigurationException}, {@link 
javax.xml.stream.XMLStreamException},
+ *       {@link javax.xml.transform.TransformerConfigurationException}) in an 
unchecked exception whose message names the feature or property that 
failed.</li>
+ *   <li>Providers need not be thread-safe themselves, but they must not hold 
mutable global state. Each {@code configure} call operates on a caller-supplied
+ *       factory.</li>
+ * </ul>
+ *
+ * <p>The default {@code configure} implementations throw {@link 
UnsupportedOperationException}. A provider should override only the methods 
corresponding to
+ * factory types it actually supports.</p>
+ */
+public interface XmlProvider {
+
+    /**
+     * Indicates whether this provider can harden a factory of the given 
concrete class.
+     *
+     * <p>Implementations typically match on {@link Class#getName()}; they 
must not return {@code true} for a class they cannot fully configure.</p>
+     *
+     * @param factoryClass the concrete factory class to test; never {@code 
null}.
+     * @return {@code true} if this provider is responsible for factories of 
this class.
+     */
+    boolean supports(Class<?> factoryClass);
+
+    /**
+     * Hardens a {@link DocumentBuilderFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match DOM factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final DocumentBuilderFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Hardens a {@link SAXParserFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match SAX factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final SAXParserFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Hardens an {@link XMLInputFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match StAX input factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final XMLInputFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Hardens a {@link TransformerFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match TrAX factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final TransformerFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Hardens an {@link XPathFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match XPath factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final XPathFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Hardens a {@link SchemaFactory}.
+     *
+     * <p>The default implementation throws {@link 
UnsupportedOperationException}; providers that match Schema factories must 
override.</p>
+     *
+     * @param factory the vanilla factory to harden in place; never {@code 
null}.
+     */
+    default void configure(final SchemaFactory factory) {
+        throw new UnsupportedOperationException();
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationTest.java
 
b/src/test/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationTest.java
new file mode 100644
index 0000000..d83ef82
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/UnsupportedXmlImplementationTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.factory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.commons.xml.factory.internal.ProviderRegistry;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies that an unknown factory class surfaces {@link 
UnsupportedXmlImplementationException} with the class attached.
+ */
+class UnsupportedXmlImplementationTest {
+
+    /**
+     * A stand-in factory class whose fully qualified name is not matched by 
any bundled provider or {@code ServiceLoader} service on the test classpath.
+     */
+    public static final class FakeDocumentBuilderFactory extends 
DocumentBuilderFactory {
+
+        @Override
+        public DocumentBuilder newDocumentBuilder() {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void setAttribute(final String name, final Object value) {
+            // no-op
+        }
+
+        @Override
+        public Object getAttribute(final String name) {
+            return null;
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) {
+            // no-op
+        }
+
+        @Override
+        public boolean getFeature(final String name) {
+            return false;
+        }
+    }
+
+    @Test
+    void registryRejectsUnknownFactory() {
+        final UnsupportedXmlImplementationException thrown = assertThrows(
+                UnsupportedXmlImplementationException.class,
+                () -> 
ProviderRegistry.getInstance().providerFor(FakeDocumentBuilderFactory.class));
+        assertSame(FakeDocumentBuilderFactory.class, 
thrown.getUnsupportedFactoryClass());
+        assertNotNull(thrown.getMessage());
+        
assertTrue(thrown.getMessage().contains(FakeDocumentBuilderFactory.class.getName()),
+                "Exception message must name the unsupported class: " + 
thrown.getMessage());
+        assertTrue(thrown.getMessage().contains("ServiceLoader")
+                        || thrown.getMessage().contains("XmlProvider"),
+                "Message should hint at the remediation path: " + 
thrown.getMessage());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/internal/ProviderRegistryTest.java
 
b/src/test/java/org/apache/commons/xml/factory/internal/ProviderRegistryTest.java
new file mode 100644
index 0000000..684ad8f
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/internal/ProviderRegistryTest.java
@@ -0,0 +1,100 @@
+/*
+ * 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.factory.internal;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import org.apache.commons.xml.factory.UnsupportedXmlImplementationException;
+import org.apache.commons.xml.factory.spi.XmlProvider;
+import org.junit.jupiter.api.Test;
+
+/**
+ * Verifies registry dispatch without relying on any bundled provider.
+ *
+ * <p>Stub providers are injected through the package-private {@link 
ProviderRegistry#ProviderRegistry(java.util.List)} constructor so the tests 
exercise the
+ * lookup and cache logic in isolation from the ServiceLoader-discovered 
classpath.</p>
+ */
+class ProviderRegistryTest {
+
+    /** Stub that claims to support every non-null factory class. */
+    private static final class MatchesAll implements XmlProvider {
+        @Override
+        public boolean supports(final Class<?> factoryClass) {
+            return factoryClass != null;
+        }
+    }
+
+    /** Stub that matches nothing. */
+    private static final class MatchesNone implements XmlProvider {
+        @Override
+        public boolean supports(final Class<?> factoryClass) {
+            return false;
+        }
+    }
+
+    @Test
+    void providerForNullThrowsNpe() {
+        final ProviderRegistry registry = new 
ProviderRegistry(Collections.emptyList());
+        assertThrows(NullPointerException.class, () -> 
registry.providerFor(null));
+    }
+
+    @Test
+    void providerForUnknownClassThrowsUnsupportedWithClassAttached() {
+        final ProviderRegistry registry = new 
ProviderRegistry(Collections.emptyList());
+        final UnsupportedXmlImplementationException thrown = assertThrows(
+                UnsupportedXmlImplementationException.class,
+                () -> registry.providerFor(String.class));
+        assertSame(String.class, thrown.getUnsupportedFactoryClass());
+    }
+
+    @Test
+    void providerForCachesResolvedProvider() {
+        final XmlProvider stub = new MatchesAll();
+        final ProviderRegistry registry = new 
ProviderRegistry(Collections.singletonList(stub));
+        final XmlProvider first = registry.providerFor(String.class);
+        final XmlProvider second = registry.providerFor(String.class);
+        assertSame(first, second);
+        assertSame(stub, first);
+    }
+
+    @Test
+    void bundledProvidersAreConsultedInDeclaredOrder() {
+        final XmlProvider winner = new MatchesAll();
+        final XmlProvider shadowed = new MatchesAll();
+        final ProviderRegistry registry = new 
ProviderRegistry(Arrays.asList(winner, shadowed));
+        assertSame(winner, registry.providerFor(String.class));
+    }
+
+    @Test
+    void nonMatchingBundledProviderFallsThroughToNext() {
+        final XmlProvider noMatch = new MatchesNone();
+        final XmlProvider match = new MatchesAll();
+        final ProviderRegistry registry = new 
ProviderRegistry(Arrays.asList(noMatch, match));
+        assertSame(match, registry.providerFor(String.class));
+    }
+
+    @Test
+    void emptyBundledListWithNoServiceLoaderProvidersThrows() {
+        final ProviderRegistry registry = new 
ProviderRegistry(Collections.emptyList());
+        assertThrows(UnsupportedXmlImplementationException.class,
+                () -> registry.providerFor(Integer.class));
+    }
+}


Reply via email to