You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@commons.apache.org by pk...@apache.org on 2026/06/20 20:08:59 UTC
(commons-xml) 04/50: feat: add public API for hardened JAXP factories
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 <pk...@apache.org>
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) <no...@anthropic.com>
---
.../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));
+ }
+}