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)); + } +}
