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

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

commit d06988af9786751a766b3455cc34e7d0f2b8cc0d
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Aug 30 11:19:10 2026 -0400

    Add missing test classes.
    
    Coverage went from 75% instructions, 70% branches to 97% instructions,
    91% branches.
---
 src/test/java/com/saxonica/ProviderMarker.java     |  25 +++
 .../xml/FallbackIgnoreEntityResolver2Test.java     |  60 ++++++
 .../xml/FallbackIgnoreLSResourceResolverTest.java  |  47 +++++
 .../commons/xml/FallbackIgnoreURIResolverTest.java |  44 ++++
 .../commons/xml/FallbackIgnoreXMLResolverTest.java |  44 ++++
 .../org/apache/commons/xml/SaxonProviderTest.java  | 120 +++++++++++
 .../xml/SecureDocumentBuilderFactoryTest.java      |  85 ++++++++
 .../commons/xml/SecureDocumentBuilderTest.java     |  48 +++++
 .../apache/commons/xml/SecureExceptionTest.java    |  42 ++++
 .../commons/xml/SecureSAXParserFactoryTest.java    | 100 +++++++++
 .../apache/commons/xml/SecureSAXParserTest.java    | 126 ++++++++++++
 .../commons/xml/SecureSchemaFactoryTest.java       | 118 +++++++++++
 .../org/apache/commons/xml/SecureSchemaTest.java   |  33 +++
 .../commons/xml/SecureTemplatesHandlerTest.java    | 151 ++++++++++++++
 .../apache/commons/xml/SecureTemplatesTest.java    |  60 ++++++
 .../commons/xml/SecureTransformerFactoryTest.java  | 223 ++++++++++++++++++++
 .../commons/xml/SecureTransformerHandlerTest.java  |  64 ++++++
 .../apache/commons/xml/SecureTransformerTest.java  |  71 +++++++
 .../apache/commons/xml/SecureValidatorTest.java    | 116 +++++++++++
 .../apache/commons/xml/SecureXMLFilterTest.java    | 226 +++++++++++++++++++++
 .../apache/commons/xml/SecureXMLReaderTest.java    |  70 +++++++
 .../commons/xml/SecureXPathExpressionTest.java     |  41 ++++
 .../apache/commons/xml/SecureXPathFactoryTest.java | 117 +++++++++++
 .../org/apache/commons/xml/SecureXPathTest.java    | 141 +++++++++++++
 24 files changed, 2172 insertions(+)

diff --git a/src/test/java/com/saxonica/ProviderMarker.java 
b/src/test/java/com/saxonica/ProviderMarker.java
new file mode 100644
index 0000000..328ebcc
--- /dev/null
+++ b/src/test/java/com/saxonica/ProviderMarker.java
@@ -0,0 +1,25 @@
+/*
+ * 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 com.saxonica;
+
+/** Test-only marker for SaxonProvider's commercial-package recognition path. 
*/
+public final class ProviderMarker {
+
+    private ProviderMarker() {
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java
new file mode 100644
index 0000000..a86d03d
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2Test.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.InputSource;
+import org.xml.sax.ext.DefaultHandler2;
+import org.xml.sax.ext.EntityResolver2;
+
+class FallbackIgnoreEntityResolver2Test {
+
+    @Test
+    void resolvesDelegatesAndAllFallbackPaths() throws Exception {
+        final FallbackIgnoreEntityResolver2 floor = new 
FallbackIgnoreEntityResolver2(null);
+        assertEquals("https://example.test/base/entity.dtd";, 
floor.resolveEntity("name", "public", "https://example.test/base/";, 
"entity.dtd").getSystemId());
+        assertEquals("entity.dtd", floor.resolveEntity("name", "public", "not 
a URI", "entity.dtd").getSystemId());
+        assertNotNull(floor.resolveEntity("public", null));
+        final InputSource expected = new org.xml.sax.InputSource();
+        final EntityResolver plain = (publicId, systemId) -> expected;
+        floor.setDelegate(plain);
+        assertSame(expected, floor.resolveEntity("name", "public", 
"https://example.test/base/";, "entity.dtd"));
+        final EntityResolver2 extended = new DefaultHandler2() {
+
+            @Override
+            public org.xml.sax.InputSource resolveEntity(final String name, 
final String publicId, final String base, final String system) {
+                return expected;
+            }
+        };
+        floor.setDelegate(extended);
+        assertSame(expected, floor.resolveEntity("name", "public", "base", 
"system"));
+        floor.setDelegate(null);
+        System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+        try {
+            assertThrows(org.xml.sax.SAXException.class, () -> 
floor.resolveEntity("p", "s"));
+        } finally {
+            System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java
 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java
new file mode 100644
index 0000000..9c76314
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolverTest.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
+class FallbackIgnoreLSResourceResolverTest {
+
+    @Test
+    void coversDelegateFallbackAndDenyBranches() {
+        final FallbackIgnoreLSResourceResolver resolver = new 
FallbackIgnoreLSResourceResolver(null);
+        final org.w3c.dom.ls.LSInput fallback = resolver.resolveResource("t", 
"n", "p", "s", "b");
+        assertEquals("p", fallback.getPublicId());
+        assertEquals("s", fallback.getSystemId());
+        assertEquals("b", fallback.getBaseURI());
+        final org.w3c.dom.ls.LSInput expected = fallback;
+        final org.w3c.dom.ls.LSResourceResolver delegate = (type, namespace, 
publicId, systemId, base) -> expected;
+        resolver.setDelegate(delegate);
+        assertSame(delegate, resolver.getDelegate());
+        assertSame(expected, resolver.resolveResource("t", "n", "p", "s", 
"b"));
+        resolver.setDelegate(null);
+        System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+        try {
+            assertThrows(org.w3c.dom.ls.LSException.class, () -> 
resolver.resolveResource("t", "n", "p", "s", "b"));
+        } finally {
+            System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java
new file mode 100644
index 0000000..2d156d8
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreURIResolverTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
+class FallbackIgnoreURIResolverTest {
+
+    @Test
+    void resolvesDelegatedAndFallbackSources() throws Exception {
+        final javax.xml.transform.dom.DOMSource empty = new 
javax.xml.transform.dom.DOMSource();
+        final FallbackIgnoreURIResolver resolver = new 
FallbackIgnoreURIResolver(null, () -> empty, () -> false);
+        assertSame(empty, resolver.resolve("href", "base"));
+        final javax.xml.transform.dom.DOMSource delegated = new 
javax.xml.transform.dom.DOMSource();
+        final javax.xml.transform.URIResolver delegate = (href, base) -> 
delegated;
+        resolver.setDelegate(delegate);
+        assertSame(delegate, resolver.getDelegate());
+        assertSame(delegated, resolver.resolve("href", "base"));
+        resolver.setDelegate(null);
+        System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+        try {
+            assertThrows(javax.xml.transform.TransformerException.class, () -> 
resolver.resolve("href", "base"));
+        } finally {
+            System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java 
b/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java
new file mode 100644
index 0000000..4c0504d
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/FallbackIgnoreXMLResolverTest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import org.junit.jupiter.api.Test;
+
+class FallbackIgnoreXMLResolverTest {
+
+    @Test
+    void coversDelegateFallbackAndDenyBranches() throws Exception {
+        final FallbackIgnoreXMLResolver resolver = new 
FallbackIgnoreXMLResolver(null);
+        assertNotNull(resolver.resolveEntity("p", "s", "b", "n"));
+        final Object expected = new Object();
+        final javax.xml.stream.XMLResolver delegate = (publicId, systemId, 
base, namespace) -> expected;
+        resolver.setDelegate(delegate);
+        assertSame(delegate, resolver.getDelegate());
+        assertSame(expected, resolver.resolveEntity("p", "s", "b", "n"));
+        resolver.setDelegate(null);
+        System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+        try {
+            assertThrows(javax.xml.stream.XMLStreamException.class, () -> 
resolver.resolveEntity("p", "s", "b", "n"));
+        } finally {
+            System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        }
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SaxonProviderTest.java 
b/src/test/java/org/apache/commons/xml/SaxonProviderTest.java
new file mode 100644
index 0000000..7c1abf8
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SaxonProviderTest.java
@@ -0,0 +1,120 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.xpath.XPathFactory;
+import org.junit.jupiter.api.Test;
+
+class SaxonProviderTest {
+
+    /** Reader used to force SecureConfiguration.makeParser through its 
SecureException translation path. */
+    public static final class FailingXMLReader extends 
org.xml.sax.helpers.XMLFilterImpl {
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
org.xml.sax.SAXNotSupportedException {
+            throw new org.xml.sax.SAXNotSupportedException(name);
+        }
+    }
+
+    private static Class<?> loadSaxon(final String className) {
+        try {
+            return Class.forName(className);
+        } catch (final ClassNotFoundException e) {
+            throw new AssertionError(e);
+        }
+    }
+
+    private static Object newSaxon(final String className) throws 
ReflectiveOperationException {
+        return loadSaxon(className).getConstructor().newInstance();
+    }
+
+    @Test
+    @org.junit.jupiter.api.Tag("xpath3")
+    void configuresSaxonFactoriesAndSuppliesAnEmptySource() throws 
ReflectiveOperationException {
+        final TransformerFactory transformerFactory = 
TransformerFactory.class.cast(newSaxon("net.sf.saxon.TransformerFactoryImpl"));
+        final XPathFactory xpathFactory = 
XPathFactory.class.cast(newSaxon("net.sf.saxon.xpath.XPathFactoryImpl"));
+        assertSame(transformerFactory, 
SaxonProvider.configure(transformerFactory));
+        assertSame(xpathFactory, SaxonProvider.configure(xpathFactory));
+        assertEquals("net.sf.saxon.lib.EmptySource", 
SaxonProvider.emptySourceSupplier().get().getClass().getName());
+    }
+
+    @Test
+    void recognizesNonSaxonClass() {
+        assertFalse(SaxonProvider.isSaxon(getClass()));
+    }
+
+    @Test
+    @org.junit.jupiter.api.Tag("xpath3")
+    void recognizesOpenSourceAndCommercialSaxonClasses() {
+        
assertTrue(SaxonProvider.isSaxon(loadSaxon("net.sf.saxon.TransformerFactoryImpl")));
+        assertTrue(SaxonProvider.isSaxon(com.saxonica.ProviderMarker.class));
+    }
+
+    @Test
+    @org.junit.jupiter.api.Tag("xpath3")
+    void rejectsFactoriesThatDoNotImplementSaxonApis() {
+        org.junit.jupiter.api.Assertions.assertThrows(SecureException.class,
+                () -> 
SaxonProvider.configure(TransformerFactory.newInstance("com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl",
 null)));
+        org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, 
() -> SaxonProvider
+                
.configure(XPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, 
"com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl", null)));
+    }
+
+    @Test
+    @org.junit.jupiter.api.Tag("xpath3")
+    void rejectsSaxonCollectionResolutionWhenConfiguredToThrow() throws 
Exception {
+        final XPathFactory factory = 
XPathFactory.class.cast(newSaxon("net.sf.saxon.xpath.XPathFactoryImpl"));
+        SaxonProvider.configure(factory);
+        final Object configuration = 
factory.getClass().getMethod("getConfiguration").invoke(factory);
+        final Object finder = 
configuration.getClass().getMethod("getCollectionFinder").invoke(configuration);
+        final java.lang.reflect.Method findCollection = 
finder.getClass().getMethod("findCollection", 
loadSaxon("net.sf.saxon.expr.XPathContext"),
+                String.class);
+        final String previous = 
System.getProperty(SecureException.THROW_ON_UNRESOLVED);
+        try {
+            System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+            final java.lang.reflect.InvocationTargetException exception = 
org.junit.jupiter.api.Assertions
+                    
.assertThrows(java.lang.reflect.InvocationTargetException.class, () -> 
findCollection.invoke(finder, null, "urn:collection"));
+            assertEquals("net.sf.saxon.trans.XPathException", 
exception.getCause().getClass().getName());
+        } finally {
+            if (previous == null) {
+                System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+            } else {
+                System.setProperty(SecureException.THROW_ON_UNRESOLVED, 
previous);
+            }
+        }
+    }
+
+    @Test
+    @org.junit.jupiter.api.Tag("xpath3")
+    void translatesSecureParserFailuresToSaxonConfigurationErrors() throws 
Exception {
+        final TransformerFactory factory = 
TransformerFactory.class.cast(newSaxon("net.sf.saxon.TransformerFactoryImpl"));
+        SaxonProvider.configure(factory);
+        final Object configuration = 
factory.getClass().getMethod("getConfiguration").invoke(factory);
+        final java.lang.reflect.Method makeParser = 
configuration.getClass().getMethod("makeParser", String.class);
+        final java.lang.reflect.InvocationTargetException exception = 
org.junit.jupiter.api.Assertions
+                
.assertThrows(java.lang.reflect.InvocationTargetException.class, () -> 
makeParser.invoke(configuration, FailingXMLReader.class.getName()));
+        final javax.xml.transform.TransformerFactoryConfigurationError error = 
org.junit.jupiter.api.Assertions
+                
.assertInstanceOf(javax.xml.transform.TransformerFactoryConfigurationError.class,
 exception.getCause());
+        
org.junit.jupiter.api.Assertions.assertInstanceOf(SecureException.class, 
error.getException());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java
new file mode 100644
index 0000000..88dace2
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderFactoryTest.java
@@ -0,0 +1,85 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.junit.jupiter.api.Test;
+
+class SecureDocumentBuilderFactoryTest {
+
+    @Test
+    void createsSecureBuildersFromEveryStaticEntryPoint() throws Exception {
+        assertInstanceOf(SecureDocumentBuilder.class, 
SecureDocumentBuilderFactory.newInstance().newDocumentBuilder());
+        assertInstanceOf(SecureDocumentBuilder.class, 
SecureDocumentBuilderFactory.newDefaultInstance().newDocumentBuilder());
+        assertInstanceOf(SecureDocumentBuilder.class, 
SecureDocumentBuilderFactory.newNSInstance().newDocumentBuilder());
+        assertInstanceOf(SecureDocumentBuilder.class, 
SecureDocumentBuilderFactory.newDefaultNSInstance().newDocumentBuilder());
+    }
+
+    @Test
+    void forwardsEverySupportedFactoryConfiguration() throws Exception {
+        final DocumentBuilderFactory factory = 
SecureDocumentBuilderFactory.newInstance();
+        factory.setCoalescing(true);
+        factory.setExpandEntityReferences(false);
+        factory.setIgnoringComments(true);
+        factory.setIgnoringElementContentWhitespace(true);
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        factory.setXIncludeAware(false);
+        factory.setSchema(null);
+        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        assertTrue(factory.isCoalescing());
+        assertFalse(factory.isExpandEntityReferences());
+        assertTrue(factory.isIgnoringComments());
+        assertTrue(factory.isIgnoringElementContentWhitespace());
+        assertTrue(factory.isNamespaceAware());
+        assertFalse(factory.isValidating());
+        assertFalse(factory.isXIncludeAware());
+        assertNull(factory.getSchema());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+        assertNotNull(factory.getAttribute(XMLConstants.ACCESS_EXTERNAL_DTD));
+        assertNotNull(factory.newDocumentBuilder());
+    }
+
+    @Test
+    void honorsExplicitFactoryClassAndDefaultParserOverrides() throws 
Exception {
+        final String className = 
DocumentBuilderFactory.newInstance().getClass().getName();
+        assertTrue(SecureDocumentBuilderFactory.newNSInstance(className, 
null).isNamespaceAware());
+        
assertTrue(SecureDocumentBuilderFactory.newNSInstance(true).isNamespaceAware());
+        final String property = "javax.xml.parsers.DocumentBuilderFactory";
+        final String previous = System.getProperty(property);
+        try {
+            System.setProperty(property, className);
+            
assertTrue(SecureDocumentBuilderFactory.newNSInstance(false).isNamespaceAware());
+        } finally {
+            if (previous == null) {
+                System.clearProperty(property);
+            } else {
+                System.setProperty(property, previous);
+            }
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java 
b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java
new file mode 100644
index 0000000..1dd599b
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureDocumentBuilderTest.java
@@ -0,0 +1,48 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.junit.jupiter.api.Test;
+
+class SecureDocumentBuilderTest {
+
+    @Test
+    void createsDocument() throws Exception {
+        assertNotNull(new 
SecureDocumentBuilder(DocumentBuilderFactory.newInstance().newDocumentBuilder()).newDocument());
+    }
+
+    @Test
+    void forwardsDocumentBuilderStateAndDomImplementation() throws Exception {
+        final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        factory.setXIncludeAware(false);
+        factory.setSchema(null);
+        final SecureDocumentBuilder builder = new 
SecureDocumentBuilder(factory.newDocumentBuilder());
+        assertTrue(builder.isNamespaceAware());
+        assertFalse(builder.isValidating());
+        assertFalse(builder.isXIncludeAware());
+        assertNull(builder.getSchema());
+        assertNotNull(builder.getDOMImplementation());
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureExceptionTest.java 
b/src/test/java/org/apache/commons/xml/SecureExceptionTest.java
new file mode 100644
index 0000000..56dd97e
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureExceptionTest.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+class SecureExceptionTest {
+
+    @Test
+    void formatsFailuresAndReadsUnresolvedProperty() {
+        final RuntimeException cause = new RuntimeException("cause");
+        assertSame(cause, SecureException.featureFailed("feature", this, 
cause).getCause());
+        assertTrue(SecureException.forbidden("type", "namespace", "public", 
"system", "base").contains("system"));
+        assertSame(cause, SecureException.readerFailed(cause).getCause());
+        System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        assertFalse(SecureException.throwOnUnresolved());
+        System.setProperty(SecureException.THROW_ON_UNRESOLVED, "true");
+        try {
+            assertTrue(SecureException.throwOnUnresolved());
+        } finally {
+            System.clearProperty(SecureException.THROW_ON_UNRESOLVED);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.java
new file mode 100644
index 0000000..7080e6e
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureSAXParserFactoryTest.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;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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.assertTrue;
+
+import java.io.StringReader;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+
+class SecureSAXParserFactoryTest {
+
+    @Test
+    void createsSecureParsersFromEveryStaticEntryPoint() throws Exception {
+        assertNotNull(SecureSAXParserFactory.newInstance().newSAXParser());
+        
assertNotNull(SecureSAXParserFactory.newDefaultInstance().newSAXParser());
+        assertNotNull(SecureSAXParserFactory.newNSInstance().newSAXParser());
+        
assertNotNull(SecureSAXParserFactory.newDefaultNSInstance().newSAXParser());
+    }
+
+    @Test
+    void forwardsFactoryConfigurationAndCreatesNamespaceAwareParsers() throws 
Exception {
+        final SAXParserFactory factory = SecureSAXParserFactory.newInstance();
+        factory.setNamespaceAware(true);
+        factory.setValidating(false);
+        factory.setXIncludeAware(false);
+        factory.setSchema(null);
+        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        assertTrue(factory.isNamespaceAware());
+        assertFalse(factory.isValidating());
+        assertFalse(factory.isXIncludeAware());
+        assertNull(factory.getSchema());
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+        assertInstanceOf(SecureSAXParser.class, factory.newSAXParser());
+    }
+
+    @Test
+    void respectsDefaultParserSelectionAndLeavesReadersSecureOnlyOnce() throws 
Exception {
+        final String factoryId = "javax.xml.parsers.SAXParserFactory";
+        final String previous = System.getProperty(factoryId);
+        try {
+            System.setProperty(factoryId, 
SAXParserFactory.newInstance().getClass().getName());
+            
assertTrue(SecureSAXParserFactory.newNSInstance(false).isNamespaceAware());
+        } finally {
+            if (previous == null) {
+                System.clearProperty(factoryId);
+            } else {
+                System.setProperty(factoryId, previous);
+            }
+        }
+        
assertTrue(SecureSAXParserFactory.newNSInstance(true).isNamespaceAware());
+        final XMLReader reader = SecureSAXParserFactory.newXMLReader(false);
+        assertSame(reader, SecureSAXParserFactory.secure(reader));
+    }
+
+    @Test
+    void securesOnlySourcesThatNeedAReader() throws Exception {
+        final StreamSource stream = new StreamSource(new 
StringReader("<root/>"));
+        final Source securedStream = SecureSAXParserFactory.secure(stream, 
false);
+        assertInstanceOf(SAXSource.class, securedStream);
+        assertInstanceOf(SecureXMLReader.class, ((SAXSource) 
securedStream).getXMLReader());
+        final SAXSource readerless = new SAXSource(new InputSource(new 
StringReader("<root/>")));
+        assertInstanceOf(SAXSource.class, 
SecureSAXParserFactory.secure(readerless, true));
+        final SAXSource empty = new SAXSource();
+        assertSame(empty, SecureSAXParserFactory.secure(empty, false));
+        final DOMSource dom = new DOMSource();
+        assertSame(dom, SecureSAXParserFactory.secure(dom, false));
+        final SAXSource suppliedReader = new 
SAXSource(SecureSAXParserFactory.newXMLReader(false), new InputSource());
+        assertSame(suppliedReader, 
SecureSAXParserFactory.secure(suppliedReader, false));
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java 
b/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java
new file mode 100644
index 0000000..22926a8
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureSAXParserTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import javax.xml.parsers.SAXParserFactory;
+import org.junit.jupiter.api.Test;
+
+class SecureSAXParserTest {
+
+    private static final class ParserSecureReader extends SecureXMLReader 
implements org.xml.sax.Parser {
+
+        ParserSecureReader(final org.xml.sax.XMLReader reader) {
+            super(reader);
+        }
+
+        @Override
+        public void setDocumentHandler(final org.xml.sax.DocumentHandler 
handler) {
+        }
+
+        @Override
+        public void setLocale(final java.util.Locale locale) {
+        }
+    }
+
+    private static final class ReaderSAXParser extends 
javax.xml.parsers.SAXParser {
+
+        private final org.xml.sax.XMLReader reader;
+
+        ReaderSAXParser(final org.xml.sax.XMLReader reader) {
+            this.reader = reader;
+        }
+
+        @Override
+        public org.xml.sax.Parser getParser() {
+            return (org.xml.sax.Parser) reader;
+        }
+
+        @Override
+        public Object getProperty(final String name) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            return reader.getProperty(name);
+        }
+
+        @Override
+        public javax.xml.validation.Schema getSchema() {
+            return null;
+        }
+
+        @Override
+        public org.xml.sax.XMLReader getXMLReader() {
+            return reader;
+        }
+
+        @Override
+        public boolean isNamespaceAware() {
+            return false;
+        }
+
+        @Override
+        public boolean isValidating() {
+            return false;
+        }
+
+        @Override
+        public boolean isXIncludeAware() {
+            return false;
+        }
+
+        @Override
+        public void reset() {
+        }
+
+        @Override
+        public void setProperty(final String name, final Object value) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            reader.setProperty(name, value);
+        }
+    }
+
+    @Test
+    void cachesSecureViewsThenRecreatesThemAfterReset() throws Exception {
+        final SecureSAXParser parser = new 
SecureSAXParser(SAXParserFactory.newInstance().newSAXParser());
+        final org.xml.sax.XMLReader firstReader = parser.getXMLReader();
+        final org.xml.sax.Parser firstParser = parser.getParser();
+        org.junit.jupiter.api.Assertions.assertSame(firstReader, 
parser.getXMLReader());
+        org.junit.jupiter.api.Assertions.assertSame(firstParser, 
parser.getParser());
+        parser.setProperty("http://xml.org/sax/properties/lexical-handler";, 
null);
+        
org.junit.jupiter.api.Assertions.assertNull(parser.getProperty("http://xml.org/sax/properties/lexical-handler";));
+        parser.reset();
+        org.junit.jupiter.api.Assertions.assertNotSame(firstReader, 
parser.getXMLReader());
+        org.junit.jupiter.api.Assertions.assertNotSame(firstParser, 
parser.getParser());
+    }
+
+    @Test
+    void exposesSecureParserViewsAndState() throws Exception {
+        final SecureSAXParser parser = new 
SecureSAXParser(SAXParserFactory.newInstance().newSAXParser());
+        assertNotNull(parser.getXMLReader());
+        assertNotNull(parser.getParser());
+        parser.getSchema();
+        parser.isNamespaceAware();
+        parser.isValidating();
+        parser.isXIncludeAware();
+        parser.reset();
+    }
+
+    @Test
+    void reusesAReaderThatAlreadyImplementsSax1Parser() throws Exception {
+        final ParserSecureReader reader = new 
ParserSecureReader(SAXParserFactory.newInstance().newSAXParser().getXMLReader());
+        final SecureSAXParser parser = new SecureSAXParser(new 
ReaderSAXParser(reader));
+        org.junit.jupiter.api.Assertions.assertSame(reader, 
parser.getParser());
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java
new file mode 100644
index 0000000..63cc9c8
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureSchemaFactoryTest.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+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.XMLConstants;
+import javax.xml.validation.SchemaFactory;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+class SecureSchemaFactoryTest {
+
+    private static final class PropertySchemaFactory extends SchemaFactory {
+
+        private final SchemaFactory delegate = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+
+        @Override
+        public org.xml.sax.ErrorHandler getErrorHandler() {
+            return delegate.getErrorHandler();
+        }
+
+        @Override
+        public boolean getFeature(final String name) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            return delegate.getFeature(name);
+        }
+
+        @Override
+        public Object getProperty(final String name) {
+            return "value";
+        }
+
+        @Override
+        public org.w3c.dom.ls.LSResourceResolver getResourceResolver() {
+            return delegate.getResourceResolver();
+        }
+
+        @Override
+        public boolean isSchemaLanguageSupported(final String language) {
+            return delegate.isSchemaLanguageSupported(language);
+        }
+
+        @Override
+        public javax.xml.validation.Schema newSchema() throws 
org.xml.sax.SAXException {
+            return delegate.newSchema();
+        }
+
+        @Override
+        public javax.xml.validation.Schema newSchema(final 
javax.xml.transform.Source[] sources) throws org.xml.sax.SAXException {
+            return delegate.newSchema(sources);
+        }
+
+        @Override
+        public void setErrorHandler(final org.xml.sax.ErrorHandler handler) {
+            delegate.setErrorHandler(handler);
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            delegate.setFeature(name, value);
+        }
+
+        @Override
+        public void setProperty(final String name, final Object value) {
+        }
+
+        @Override
+        public void setResourceResolver(final 
org.w3c.dom.ls.LSResourceResolver resolver) {
+            delegate.setResourceResolver(resolver);
+        }
+    }
+
+    @Test
+    void createsSecureSchemas() throws Exception {
+        assertInstanceOf(SecureSchema.class, 
SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema());
+        assertInstanceOf(SecureSchema.class, 
SecureSchemaFactory.newDefaultInstance().newSchema());
+    }
+
+    @Test
+    void forwardsSchemaFactoryConfigurationAndAccessors() throws Exception {
+        final SchemaFactory factory = 
SecureSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+        final DefaultHandler errorHandler = new DefaultHandler();
+        final org.w3c.dom.ls.LSResourceResolver resolver = (type, namespace, 
publicId, systemId, base) -> null;
+        factory.setErrorHandler(errorHandler);
+        factory.setResourceResolver(resolver);
+        factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        factory.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+        assertSame(errorHandler, factory.getErrorHandler());
+        assertSame(resolver, factory.getResourceResolver());
+        
assertTrue(factory.isSchemaLanguageSupported(XMLConstants.W3C_XML_SCHEMA_NS_URI));
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+        assertThrows(org.xml.sax.SAXNotRecognizedException.class, () -> 
factory.getProperty("foo"));
+    }
+
+    @Test
+    void returnsPropertiesFromTheDelegate() throws Exception {
+        final SchemaFactory factory = SecureSchemaFactory.secure(new 
PropertySchemaFactory());
+        assertEquals("value", factory.getProperty("test"));
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureSchemaTest.java 
b/src/test/java/org/apache/commons/xml/SecureSchemaTest.java
new file mode 100644
index 0000000..69ea9b4
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureSchemaTest.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import javax.xml.XMLConstants;
+import javax.xml.validation.SchemaFactory;
+import org.junit.jupiter.api.Test;
+
+class SecureSchemaTest {
+
+    @Test
+    void wrapsAllSchemaProducts() throws Exception {
+        final SecureSchema schema = new 
SecureSchema(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(),
 false);
+        assertInstanceOf(SecureValidator.class, schema.newValidator());
+        assertInstanceOf(SecureValidatorHandler.class, 
schema.newValidatorHandler());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java 
b/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java
new file mode 100644
index 0000000..9497eb3
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureTemplatesHandlerTest.java
@@ -0,0 +1,151 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.xml.transform.Templates;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.TemplatesHandler;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.Attributes;
+import org.xml.sax.Locator;
+import org.xml.sax.helpers.DefaultHandler;
+
+class SecureTemplatesHandlerTest {
+
+    private static final class RecordingHandler extends DefaultHandler 
implements TemplatesHandler {
+
+        final List<String> calls = new ArrayList<>();
+
+        Templates templates;
+
+        String systemId = "initial";
+
+        @Override
+        public void characters(final char[] ch, final int start, final int 
length) {
+            calls.add("characters:" + start + ':' + length);
+        }
+
+        @Override
+        public void endDocument() {
+            calls.add("endDocument");
+        }
+
+        @Override
+        public void endElement(final String uri, final String localName, final 
String qName) {
+            calls.add("endElement:" + uri + ':' + localName + ':' + qName);
+        }
+
+        @Override
+        public void endPrefixMapping(final String prefix) {
+            calls.add("endPrefixMapping:" + prefix);
+        }
+
+        @Override
+        public String getSystemId() {
+            return systemId;
+        }
+
+        @Override
+        public Templates getTemplates() {
+            return templates;
+        }
+
+        @Override
+        public void ignorableWhitespace(final char[] ch, final int start, 
final int length) {
+            calls.add("ignorableWhitespace:" + start + ':' + length);
+        }
+
+        @Override
+        public void processingInstruction(final String target, final String 
data) {
+            calls.add("processingInstruction:" + target + ':' + data);
+        }
+
+        @Override
+        public void setDocumentLocator(final Locator locator) {
+            calls.add("setDocumentLocator");
+        }
+
+        @Override
+        public void setSystemId(final String value) {
+            systemId = value;
+            calls.add("setSystemId:" + value);
+        }
+
+        @Override
+        public void skippedEntity(final String name) {
+            calls.add("skippedEntity:" + name);
+        }
+
+        @Override
+        public void startDocument() {
+            calls.add("startDocument");
+        }
+
+        @Override
+        public void startElement(final String uri, final String localName, 
final String qName, final Attributes atts) {
+            calls.add("startElement:" + uri + ':' + localName + ':' + qName);
+        }
+
+        @Override
+        public void startPrefixMapping(final String prefix, final String uri) {
+            calls.add("startPrefixMapping:" + prefix + ':' + uri);
+        }
+    }
+
+    @Test
+    void forwardsEveryTemplatesHandlerMethodAndWrapsTemplates() throws 
Exception {
+        final RecordingHandler delegate = new RecordingHandler();
+        delegate.templates = TransformerFactory.newInstance()
+                .newTemplates(new StreamSource(new 
StringReader("<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'/>")));
+        final SecureTemplatesHandler handler = new 
SecureTemplatesHandler(delegate, null, null, false);
+        final char[] chars = { 'x', 'y' };
+        handler.characters(chars, 1, 1);
+        handler.endDocument();
+        handler.endElement("u", "l", "q");
+        handler.endPrefixMapping("p");
+        handler.ignorableWhitespace(chars, 0, 2);
+        handler.processingInstruction("target", "data");
+        handler.setDocumentLocator(null);
+        handler.setSystemId("system");
+        handler.skippedEntity("entity");
+        handler.startDocument();
+        handler.startElement("u", "l", "q", null);
+        handler.startPrefixMapping("p", "u");
+        assertEquals("system", handler.getSystemId());
+        assertInstanceOf(SecureTemplates.class, handler.getTemplates());
+        assertEquals(12, delegate.calls.size());
+    }
+
+    @Test
+    void preservesNullTemplates() {
+        final RecordingHandler delegate = new RecordingHandler();
+        assertNull(new SecureTemplatesHandler(delegate, null, null, 
false).getTemplates());
+        assertSame(null, delegate.templates);
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java 
b/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java
new file mode 100644
index 0000000..8c94ffb
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureTemplatesTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.StringReader;
+
+import javax.xml.transform.Templates;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Test;
+
+class SecureTemplatesTest {
+
+    @Test
+    void delegatesPropertiesAndWrapsProducedTransformer() throws Exception {
+        final Templates delegate = 
TransformerFactory.newInstance().newTemplates(new StreamSource(
+                new StringReader("<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template 
match='/'/></xsl:stylesheet>")));
+        final SecureTemplates templates = new SecureTemplates(delegate, (href, 
base) -> null, null, false);
+        assertNotNull(templates.getOutputProperties());
+        assertInstanceOf(SecureTransformer.class, templates.newTransformer());
+        assertNotNull(templates.getDelegate());
+    }
+
+    @Test
+    void preservesANullTransformerFromTheDelegate() throws Exception {
+        final Templates delegate = new Templates() {
+
+            @Override
+            public java.util.Properties getOutputProperties() {
+                return new java.util.Properties();
+            }
+
+            @Override
+            public javax.xml.transform.Transformer newTransformer() {
+                return null;
+            }
+        };
+        assertNull(new SecureTemplates(delegate, null, null, 
false).newTransformer());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java
new file mode 100644
index 0000000..9663061
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureTransformerFactoryTest.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+
+import java.io.StringReader;
+
+import javax.xml.transform.ErrorListener;
+import javax.xml.transform.Source;
+import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TemplatesHandler;
+import javax.xml.transform.sax.TransformerHandler;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.XMLFilter;
+
+class SecureTransformerFactoryTest {
+
+    private static class NullProductsFactory extends SAXTransformerFactory {
+
+        private final SAXTransformerFactory delegate = (SAXTransformerFactory) 
TransformerFactory.newInstance();
+
+        private final java.util.Map<String, Object> attributes = new 
java.util.HashMap<>();
+
+        @Override
+        public Source getAssociatedStylesheet(final Source source, final 
String media, final String title, final String charset)
+                throws TransformerConfigurationException {
+            return delegate.getAssociatedStylesheet(source, media, title, 
charset);
+        }
+
+        @Override
+        public Object getAttribute(final String name) {
+            return attributes.get(name);
+        }
+
+        @Override
+        public ErrorListener getErrorListener() {
+            return delegate.getErrorListener();
+        }
+
+        @Override
+        public boolean getFeature(final String name) {
+            return delegate.getFeature(name);
+        }
+
+        @Override
+        public URIResolver getURIResolver() {
+            return delegate.getURIResolver();
+        }
+
+        @Override
+        public Templates newTemplates(final Source source) {
+            return null;
+        }
+
+        @Override
+        public TemplatesHandler newTemplatesHandler() {
+            return null;
+        }
+
+        @Override
+        public Transformer newTransformer() {
+            return null;
+        }
+
+        @Override
+        public Transformer newTransformer(final Source source) {
+            return null;
+        }
+
+        @Override
+        public TransformerHandler newTransformerHandler() {
+            return null;
+        }
+
+        @Override
+        public TransformerHandler newTransformerHandler(final Source source) {
+            return null;
+        }
+
+        @Override
+        public TransformerHandler newTransformerHandler(final Templates 
templates) {
+            return null;
+        }
+
+        @Override
+        public XMLFilter newXMLFilter(final Source source) {
+            return null;
+        }
+
+        @Override
+        public XMLFilter newXMLFilter(final Templates templates) {
+            return null;
+        }
+
+        @Override
+        public void setAttribute(final String name, final Object value) {
+            attributes.put(name, value);
+        }
+
+        @Override
+        public void setErrorListener(final ErrorListener listener) {
+            delegate.setErrorListener(listener);
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
TransformerConfigurationException {
+            delegate.setFeature(name, value);
+        }
+
+        @Override
+        public void setURIResolver(final URIResolver resolver) {
+            delegate.setURIResolver(resolver);
+        }
+    }
+
+    private static final class RejectingFeatureFactory extends 
NullProductsFactory {
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
TransformerConfigurationException {
+            throw new TransformerConfigurationException(name);
+        }
+    }
+
+    private static void associatedStylesheet(final SAXTransformerFactory 
factory, final Source source) throws Exception {
+        try {
+            factory.getAssociatedStylesheet(source, null, null, null);
+        } catch (final TransformerConfigurationException | 
NullPointerException expected) {
+            // Saxon signals no matching PI with 
TransformerConfigurationException; Xalan rejects a source without an 
InputSource.
+        }
+    }
+
+    private static StreamSource stylesheet() {
+        return new StreamSource(new StringReader(
+                "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>" + "<xsl:template 
match='/'/></xsl:stylesheet>"));
+    }
+
+    @Test
+    void preservesNullResultsFromEveryWrappableProduct() throws Exception {
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
SecureTransformerFactory.secure(new NullProductsFactory());
+        final Templates templates = 
TransformerFactory.newInstance().newTemplates(stylesheet());
+        assertNull(factory.newTemplates(stylesheet()));
+        assertNull(factory.newTemplatesHandler());
+        assertNull(factory.newTransformer());
+        assertNull(factory.newTransformer(stylesheet()));
+        assertNull(factory.newTransformerHandler());
+        assertNull(factory.newTransformerHandler(stylesheet()));
+        assertNull(factory.newTransformerHandler(templates));
+        assertNull(factory.newXMLFilter(stylesheet()));
+        factory.setAttribute("test", "value");
+        org.junit.jupiter.api.Assertions.assertEquals("value", 
factory.getAttribute("test"));
+        factory.setFeature(javax.xml.XMLConstants.FEATURE_SECURE_PROCESSING, 
true);
+    }
+
+    @Test
+    void rejectsDelegatesThatCannotEnableSecureProcessing() {
+        org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, 
() -> SecureTransformerFactory.secure(new RejectingFeatureFactory()));
+    }
+
+    @Test
+    @Tag("trax")
+    void securesAssociatedStylesheetSourcesOfEverySupportedShape() throws 
Exception {
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
SecureTransformerFactory.newInstance();
+        associatedStylesheet(factory, new StreamSource(new 
StringReader("<root/>")));
+        associatedStylesheet(factory, new StreamSource(new 
StringReader("<root>")));
+        associatedStylesheet(factory, new 
javax.xml.transform.sax.SAXSource(new org.xml.sax.InputSource(new 
StringReader("<root/>"))));
+        associatedStylesheet(factory, new javax.xml.transform.sax.SAXSource());
+        associatedStylesheet(factory,
+                new 
javax.xml.transform.sax.SAXSource(SecureSAXParserFactory.newXMLReader(false), 
new org.xml.sax.InputSource(new StringReader("<root/>"))));
+        associatedStylesheet(factory,
+                new 
javax.xml.transform.dom.DOMSource(javax.xml.parsers.DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()));
+    }
+
+    @Test
+    void wrapsEveryStandardAndSaxFactoryProduct() throws Exception {
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
SecureTransformerFactory.newInstance();
+        final URIResolver resolver = (href, base) -> null;
+        factory.setURIResolver(resolver);
+        assertSame(resolver, factory.getURIResolver());
+        factory.setErrorListener(factory.getErrorListener());
+        factory.setAttribute("indent-number", 2);
+        
org.junit.jupiter.api.Assertions.assertThrows(IllegalArgumentException.class, 
() -> factory.getAttribute("indent-number"));
+        final Templates templates = factory.newTemplates(stylesheet());
+        assertInstanceOf(SecureTemplates.class, templates);
+        assertInstanceOf(SecureTransformer.class, factory.newTransformer());
+        assertInstanceOf(SecureTransformer.class, 
factory.newTransformer(stylesheet()));
+        assertInstanceOf(SecureTemplatesHandler.class, 
factory.newTemplatesHandler());
+        assertInstanceOf(SecureTransformerHandler.class, 
factory.newTransformerHandler());
+        assertInstanceOf(SecureTransformerHandler.class, 
factory.newTransformerHandler(stylesheet()));
+        assertInstanceOf(SecureTransformerHandler.class, 
factory.newTransformerHandler(templates));
+        assertInstanceOf(SecureXMLFilter.class, 
factory.newXMLFilter(stylesheet()));
+        assertInstanceOf(SecureXMLFilter.class, 
factory.newXMLFilter(templates));
+        final Templates rawTemplates = 
TransformerFactory.newInstance().newTemplates(stylesheet());
+        assertInstanceOf(SecureXMLFilter.class, 
factory.newXMLFilter(rawTemplates));
+        associatedStylesheet(factory, new StreamSource(new 
StringReader("<root/>")));
+        assertInstanceOf(SecureTransformer.class, 
SecureTransformerFactory.newInstance().newTransformer());
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java 
b/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java
new file mode 100644
index 0000000..217de4f
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureTransformerHandlerTest.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+
+import java.io.StringWriter;
+
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.stream.StreamResult;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.AttributesImpl;
+
+class SecureTransformerHandlerTest {
+
+    @Test
+    void forwardsEveryTransformerHandlerMethod() throws Exception {
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
TransformerFactory.newInstance();
+        final SecureTransformerHandler handler = new 
SecureTransformerHandler(factory.newTransformerHandler(), null, null, false);
+        final char[] chars = { 'x' };
+        handler.setResult(new StreamResult(new StringWriter()));
+        handler.setDocumentLocator(new org.xml.sax.helpers.LocatorImpl());
+        handler.setSystemId("system");
+        handler.startDocument();
+        handler.startDTD("root", null, null);
+        handler.endDTD();
+        handler.startPrefixMapping("p", "urn:test");
+        handler.startElement("", "root", "root", new AttributesImpl());
+        handler.startCDATA();
+        handler.characters(chars, 0, 1);
+        handler.ignorableWhitespace(chars, 0, 1);
+        handler.comment(chars, 0, 1);
+        handler.endCDATA();
+        handler.processingInstruction("t", "d");
+        handler.notationDecl("n", "p", "s");
+        handler.unparsedEntityDecl("e", "p", "s", "n");
+        handler.startEntity("e");
+        handler.endEntity("e");
+        handler.skippedEntity("e");
+        handler.endElement("", "root", "root");
+        handler.endPrefixMapping("p");
+        handler.endDocument();
+        assertEquals("system", handler.getSystemId());
+        assertInstanceOf(SecureTransformer.class, handler.getTransformer());
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureTransformerTest.java 
b/src/test/java/org/apache/commons/xml/SecureTransformerTest.java
new file mode 100644
index 0000000..9f635fd
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureTransformerTest.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Properties;
+
+import javax.xml.transform.OutputKeys;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.dom.DOMSource;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.junit.jupiter.api.Test;
+
+class SecureTransformerTest {
+
+    @Test
+    void forwardsEveryTransformerMethod() throws Exception {
+        final TransformerFactory factory = TransformerFactory.newInstance();
+        final SecureTransformer transformer = new SecureTransformer(factory
+                .newTemplates(new StreamSource(new StringReader(
+                        "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template 
match='/'/></xsl:stylesheet>")))
+                .newTransformer(), null, null, false);
+        transformer.clearParameters();
+        transformer.setParameter("p", "v");
+        assertNotNull(transformer.getParameter("p"));
+        transformer.setOutputProperty(OutputKeys.METHOD, "xml");
+        assertNotNull(transformer.getOutputProperty(OutputKeys.METHOD));
+        transformer.setOutputProperties(new Properties());
+        assertNotNull(transformer.getOutputProperties());
+        transformer.setErrorListener(new javax.xml.transform.ErrorListener() {
+
+            @Override
+            public void error(final javax.xml.transform.TransformerException 
e) {
+            }
+
+            @Override
+            public void fatalError(final 
javax.xml.transform.TransformerException e) {
+            }
+
+            @Override
+            public void warning(final javax.xml.transform.TransformerException 
e) {
+            }
+        });
+        assertNotNull(transformer.getErrorListener());
+        transformer.setURIResolver((href, base) -> null);
+        assertNotNull(transformer.getURIResolver());
+        transformer.transform(new 
DOMSource(DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()),
 new StreamResult(new StringWriter()));
+        transformer.reset();
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureValidatorTest.java 
b/src/test/java/org/apache/commons/xml/SecureValidatorTest.java
new file mode 100644
index 0000000..a143686
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureValidatorTest.java
@@ -0,0 +1,116 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.XMLConstants;
+import javax.xml.validation.SchemaFactory;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.helpers.DefaultHandler;
+
+class SecureValidatorTest {
+
+    private static final class PropertyValidator extends 
javax.xml.validation.Validator {
+
+        private final javax.xml.validation.Validator delegate;
+
+        PropertyValidator() throws org.xml.sax.SAXException {
+            delegate = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator();
+        }
+
+        @Override
+        public org.xml.sax.ErrorHandler getErrorHandler() {
+            return delegate.getErrorHandler();
+        }
+
+        @Override
+        public boolean getFeature(final String name) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            return delegate.getFeature(name);
+        }
+
+        @Override
+        public Object getProperty(final String name) {
+            return "value";
+        }
+
+        @Override
+        public org.w3c.dom.ls.LSResourceResolver getResourceResolver() {
+            return delegate.getResourceResolver();
+        }
+
+        @Override
+        public void reset() {
+            delegate.reset();
+        }
+
+        @Override
+        public void setErrorHandler(final org.xml.sax.ErrorHandler 
errorHandler) {
+            delegate.setErrorHandler(errorHandler);
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) throws 
org.xml.sax.SAXNotRecognizedException, org.xml.sax.SAXNotSupportedException {
+            delegate.setFeature(name, value);
+        }
+
+        @Override
+        public void setProperty(final String name, final Object object) {
+            // This test double only needs to supply a property value.
+        }
+
+        @Override
+        public void setResourceResolver(final 
org.w3c.dom.ls.LSResourceResolver resourceResolver) {
+            delegate.setResourceResolver(resourceResolver);
+        }
+
+        @Override
+        public void validate(final javax.xml.transform.Source source, final 
javax.xml.transform.Result result)
+                throws org.xml.sax.SAXException, java.io.IOException {
+            delegate.validate(source, result);
+        }
+    }
+
+    @Test
+    void getsPropertiesFromTheDelegate() throws Exception {
+        assertEquals("value", new SecureValidator(new PropertyValidator(), 
false).getProperty("property"));
+    }
+
+    @Test
+    void preservesNonRemovableResolverFloorAndForwardsConfiguration() throws 
Exception {
+        final SecureValidator validator = new 
SecureValidator(SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator(),
 false);
+        final DefaultHandler errorHandler = new DefaultHandler();
+        final org.w3c.dom.ls.LSResourceResolver resolver = (type, namespace, 
publicId, systemId, base) -> null;
+        validator.setErrorHandler(errorHandler);
+        assertSame(errorHandler, validator.getErrorHandler());
+        assertNull(validator.getResourceResolver());
+        validator.setResourceResolver(resolver);
+        validator.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        validator.setProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+        assertSame(resolver, validator.getResourceResolver());
+        
assertTrue(validator.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+        assertThrows(org.xml.sax.SAXNotRecognizedException.class, () -> 
validator.getProperty(XMLConstants.ACCESS_EXTERNAL_SCHEMA));
+        validator.reset();
+        assertNull(validator.getResourceResolver());
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java 
b/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java
new file mode 100644
index 0000000..15ed8fe
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureXMLFilterTest.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.URIResolver;
+import javax.xml.transform.TransformerException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.stream.StreamSource;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
+import org.xml.sax.helpers.DefaultHandler;
+
+class SecureXMLFilterTest {
+
+    private static SecureXMLFilter filter() throws Exception {
+        final Templates templates = 
TransformerFactory.newInstance().newTemplates(new StreamSource(new StringReader(
+                "<xsl:stylesheet version='1.0' 
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'><xsl:template 
match='@*|node()'><xsl:copy><xsl:apply-templates 
select='@*|node()'/></xsl:copy></xsl:template></xsl:stylesheet>")));
+        return new SecureXMLFilter(new SecureTemplates(templates, null, null, 
false));
+    }
+
+    @Test
+    void propagatesSaxFailuresFromTheTransformationHandler() throws Exception {
+        final SecureXMLFilter filter = filter();
+        filter.setContentHandler(new DefaultHandler() {
+
+            @Override
+            public void startElement(final String uri, final String localName, 
final String qName, final org.xml.sax.Attributes attributes)
+                    throws SAXException {
+                throw new SAXException("handler");
+            }
+        });
+        final SAXException exception = assertThrows(SAXException.class, () -> 
filter.parse(new InputSource(new StringReader("<root/>"))));
+        org.junit.jupiter.api.Assertions.assertEquals("handler", 
exception.getMessage());
+    }
+
+    @Test
+    void reportsWarningErrorAndFatalErrorUsingSaxShape() throws Exception {
+        final SecureXMLFilter filter = filter();
+        final AtomicInteger reports = new AtomicInteger();
+        filter.setErrorHandler(new DefaultHandler() {
+
+            @Override
+            public void error(final SAXParseException e) {
+                reports.incrementAndGet();
+            }
+
+            @Override
+            public void fatalError(final SAXParseException e) {
+                reports.incrementAndGet();
+            }
+
+            @Override
+            public void warning(final SAXParseException e) {
+                reports.incrementAndGet();
+            }
+        });
+        filter.warning(new TransformerException("warning"));
+        filter.error(new TransformerException("error", new 
SAXParseException("cause", null)));
+        final TransformerException fatal = new TransformerException("fatal");
+        assertSame(fatal, assertThrows(TransformerException.class, () -> 
filter.fatalError(fatal)));
+        org.junit.jupiter.api.Assertions.assertEquals(3, reports.get());
+    }
+
+    @Test
+    void requiresContentHandlerAndTransformsWhenOneIsSet() throws Exception {
+        final SecureXMLFilter filter = filter();
+        assertThrows(SAXException.class, () -> filter.parse(new 
InputSource(new StringReader("<root/>"))));
+        filter.setContentHandler(new DefaultHandler());
+        filter.parse(new InputSource(new StringReader("<root/>")));
+    }
+
+    @Test
+    void rethrowsAnIoExceptionFromTheTransformer() throws Exception {
+        final Transformer delegate = 
TransformerFactory.newInstance().newTransformer();
+        final Templates templates = new Templates() {
+
+            @Override
+            public java.util.Properties getOutputProperties() {
+                return delegate.getOutputProperties();
+            }
+
+            @Override
+            public Transformer newTransformer() {
+                return new Transformer() {
+
+                    @Override
+                    public void clearParameters() {
+                        delegate.clearParameters();
+                    }
+
+                    @Override
+                    public javax.xml.transform.ErrorListener 
getErrorListener() {
+                        return delegate.getErrorListener();
+                    }
+
+                    @Override
+                    public java.util.Properties getOutputProperties() {
+                        return delegate.getOutputProperties();
+                    }
+
+                    @Override
+                    public String getOutputProperty(final String name) {
+                        return delegate.getOutputProperty(name);
+                    }
+
+                    @Override
+                    public Object getParameter(final String name) {
+                        return delegate.getParameter(name);
+                    }
+
+                    @Override
+                    public URIResolver getURIResolver() {
+                        return delegate.getURIResolver();
+                    }
+
+                    @Override
+                    public void reset() {
+                        delegate.reset();
+                    }
+
+                    @Override
+                    public void setErrorListener(final 
javax.xml.transform.ErrorListener listener) {
+                        delegate.setErrorListener(listener);
+                    }
+
+                    @Override
+                    public void setOutputProperties(final java.util.Properties 
properties) {
+                        delegate.setOutputProperties(properties);
+                    }
+
+                    @Override
+                    public void setOutputProperty(final String name, final 
String value) {
+                        delegate.setOutputProperty(name, value);
+                    }
+
+                    @Override
+                    public void setParameter(final String name, final Object 
value) {
+                        delegate.setParameter(name, value);
+                    }
+
+                    @Override
+                    public void setURIResolver(final URIResolver resolver) {
+                        delegate.setURIResolver(resolver);
+                    }
+
+                    @Override
+                    public void transform(final javax.xml.transform.Source 
source, final Result result) throws TransformerException {
+                        throw new TransformerException(new 
IOException("transform"));
+                    }
+                };
+            }
+        };
+        final SecureXMLFilter filter = new SecureXMLFilter(new 
SecureTemplates(templates, null, null, false));
+        filter.setContentHandler(new DefaultHandler());
+        final IOException exception = assertThrows(IOException.class, () -> 
filter.parse(new InputSource(new StringReader("<root/>"))));
+        org.junit.jupiter.api.Assertions.assertEquals("transform", 
exception.getMessage());
+    }
+
+    @Test
+    void sendsLexicalEventsToALexicalContentHandler() throws Exception {
+        final SecureXMLFilter filter = filter();
+        filter.setContentHandler(new org.xml.sax.ext.DefaultHandler2());
+        filter.parse(new InputSource(new 
StringReader("<root><!--comment--><![CDATA[text]]></root>")));
+    }
+
+    @Test
+    void wrapsErrorHandlerFailuresAndUsesTheConfiguredParent() throws 
Exception {
+        final SecureXMLFilter filter = filter();
+        final SAXException handlerFailure = new SAXException("error handler");
+        filter.setErrorHandler(new DefaultHandler() {
+
+            @Override
+            public void warning(final SAXParseException e) throws SAXException 
{
+                throw handlerFailure;
+            }
+        });
+        final TransformerException warning = 
assertThrows(TransformerException.class, () -> filter.warning(new 
TransformerException("warning")));
+        assertSame(handlerFailure, warning.getCause());
+        filter.setContentHandler(new DefaultHandler());
+        filter.setParent(SecureSAXParserFactory.newXMLReader(false));
+        filter.parse(new InputSource(new StringReader("<root/>")));
+    }
+
+    @Test
+    void wrapsIoFailuresFromTheParentReader() throws Exception {
+        final SecureXMLFilter filter = filter();
+        filter.setContentHandler(new DefaultHandler());
+        filter.setParent(new org.xml.sax.helpers.XMLFilterImpl() {
+
+            @Override
+            public void parse(final InputSource input) throws IOException {
+                throw new IOException("parent");
+            }
+        });
+        final SAXException exception = assertThrows(SAXException.class, () -> 
filter.parse(new InputSource(new StringReader("<root/>"))));
+        org.junit.jupiter.api.Assertions.assertNotNull(exception.getCause());
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java 
b/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java
new file mode 100644
index 0000000..6c5554c
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureXMLReaderTest.java
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import java.io.StringReader;
+import javax.xml.parsers.SAXParserFactory;
+import org.junit.jupiter.api.Test;
+import org.xml.sax.InputSource;
+import org.xml.sax.helpers.DefaultHandler;
+
+class SecureXMLReaderTest {
+
+    private static final class RecordingReader extends 
org.xml.sax.helpers.XMLFilterImpl {
+
+        boolean inputSourceParsed;
+
+        boolean systemIdParsed;
+
+        @Override
+        public void parse(final InputSource input) {
+            inputSourceParsed = true;
+        }
+
+        @Override
+        public void parse(final String systemId) {
+            systemIdParsed = true;
+        }
+    }
+
+    @Test
+    void forwardsBothParseOverloads() throws Exception {
+        final RecordingReader delegate = new RecordingReader();
+        final SecureXMLReader reader = new SecureXMLReader(delegate);
+        reader.parse(new InputSource());
+        reader.parse("system");
+        
org.junit.jupiter.api.Assertions.assertTrue(delegate.inputSourceParsed);
+        org.junit.jupiter.api.Assertions.assertTrue(delegate.systemIdParsed);
+    }
+
+    @Test
+    void forwardsReaderConfigurationAndParse() throws Exception {
+        final SecureXMLReader reader = new 
SecureXMLReader(SAXParserFactory.newInstance().newSAXParser().getXMLReader());
+        final DefaultHandler handler = new DefaultHandler();
+        reader.setContentHandler(handler);
+        reader.setDTDHandler(handler);
+        reader.setErrorHandler(handler);
+        reader.setEntityResolver((publicId, systemId) -> null);
+        reader.getContentHandler();
+        reader.getDTDHandler();
+        reader.getErrorHandler();
+        reader.getEntityResolver();
+        reader.parse(new InputSource(new StringReader("<root/>")));
+        
org.junit.jupiter.api.Assertions.assertThrows(java.io.IOException.class, () -> 
reader.parse("file:/definitely-not-present-commons-xml-test.xml"));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java 
b/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java
new file mode 100644
index 0000000..390dae4
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureXPathExpressionTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.io.StringReader;
+
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.InputSource;
+
+class SecureXPathExpressionTest {
+
+    @Test
+    void evaluatesEveryXPathExpressionOverload() throws Exception {
+        final SecureXPathExpression expression = new 
SecureXPathExpression(XPathFactory.newInstance().newXPath().compile("/root/text()"),
 false);
+        final InputSource source = new InputSource(new 
StringReader("<root>value</root>"));
+        assertEquals("value", expression.evaluate(source));
+        assertEquals("value", expression.evaluate(new InputSource(new 
StringReader("<root>value</root>")), javax.xml.xpath.XPathConstants.STRING));
+        assertEquals("value", 
expression.evaluate(org.apache.commons.xml.SecureXPath.parse(new 
InputSource(new StringReader("<root>value</root>")), false)));
+        assertEquals("value",
+                expression.evaluate(SecureXPath.parse(new InputSource(new 
StringReader("<root>value</root>")), false), 
javax.xml.xpath.XPathConstants.STRING));
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java 
b/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java
new file mode 100644
index 0000000..d474c2e
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureXPathFactoryTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Test;
+
+class SecureXPathFactoryTest {
+
+    @Test
+    void createsAndConfiguresAFactoryForTheDefaultObjectModel() throws 
Exception {
+        final XPathFactory factory = 
SecureXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI);
+        final javax.xml.xpath.XPathFunctionResolver resolver = (name, arity) 
-> null;
+        factory.setXPathFunctionResolver(resolver);
+        final javax.xml.xpath.XPathVariableResolver variableResolver = name -> 
null;
+        factory.setXPathVariableResolver(variableResolver);
+        
assertTrue(factory.isObjectModelSupported(XPathFactory.DEFAULT_OBJECT_MODEL_URI));
+        final SecureXPath xpath = (SecureXPath) factory.newXPath();
+        assertSame(resolver, xpath.getXPathFunctionResolver());
+        assertSame(variableResolver, xpath.getXPathVariableResolver());
+    }
+
+    @Test
+    void createsSecureXPathFromStaticEntryPoints() {
+        assertInstanceOf(SecureXPath.class, 
SecureXPathFactory.newInstance().newXPath());
+        assertInstanceOf(SecureXPath.class, 
SecureXPathFactory.newDefaultInstance().newXPath());
+    }
+
+    @Test
+    void preservesANullXPathFromTheDelegate() {
+        final XPathFactory delegate = new XPathFactory() {
+
+            @Override
+            public boolean getFeature(final String name) {
+                return false;
+            }
+
+            @Override
+            public boolean isObjectModelSupported(final String objectModel) {
+                return true;
+            }
+
+            @Override
+            public javax.xml.xpath.XPath newXPath() {
+                return null;
+            }
+
+            @Override
+            public void setFeature(final String name, final boolean value) {
+            }
+
+            @Override
+            public void setXPathFunctionResolver(final 
javax.xml.xpath.XPathFunctionResolver resolver) {
+            }
+
+            @Override
+            public void setXPathVariableResolver(final 
javax.xml.xpath.XPathVariableResolver resolver) {
+            }
+        };
+        assertNull(SecureXPathFactory.secure(delegate).newXPath());
+    }
+
+    @Test
+    void wrapsARejectedRequiredFeatureInSecureException() {
+        final XPathFactory rejectingFactory = new XPathFactory() {
+
+            @Override
+            public boolean getFeature(final String name) {
+                return false;
+            }
+
+            @Override
+            public boolean isObjectModelSupported(final String objectModel) {
+                return true;
+            }
+
+            @Override
+            public javax.xml.xpath.XPath newXPath() {
+                return null;
+            }
+
+            @Override
+            public void setFeature(final String name, final boolean value) 
throws javax.xml.xpath.XPathFactoryConfigurationException {
+                throw new 
javax.xml.xpath.XPathFactoryConfigurationException(name);
+            }
+
+            @Override
+            public void setXPathFunctionResolver(final 
javax.xml.xpath.XPathFunctionResolver resolver) {
+            }
+
+            @Override
+            public void setXPathVariableResolver(final 
javax.xml.xpath.XPathVariableResolver resolver) {
+            }
+        };
+        org.junit.jupiter.api.Assertions.assertThrows(SecureException.class, 
() -> SecureXPathFactory.secure(rejectingFactory));
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/SecureXPathTest.java 
b/src/test/java/org/apache/commons/xml/SecureXPathTest.java
new file mode 100644
index 0000000..b64dae0
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/SecureXPathTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.commons.xml;
+
+import 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.assertThrows;
+
+import java.io.StringReader;
+import java.util.Collections;
+
+import javax.xml.XMLConstants;
+import javax.xml.namespace.NamespaceContext;
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Test;
+import org.xml.sax.InputSource;
+
+class SecureXPathTest {
+
+    @Test
+    void delegatesEveryXPathMethod() throws Exception {
+        final SecureXPath xpath = new 
SecureXPath(XPathFactory.newInstance().newXPath(), false);
+        final NamespaceContext context = new NamespaceContext() {
+
+            @Override
+            public String getNamespaceURI(final String prefix) {
+                return XMLConstants.NULL_NS_URI;
+            }
+
+            @Override
+            public String getPrefix(final String namespaceUri) {
+                return null;
+            }
+
+            @Override
+            public java.util.Iterator<String> getPrefixes(final String 
namespaceUri) {
+                return Collections.<String>emptyList().iterator();
+            }
+        };
+        xpath.setNamespaceContext(context);
+        xpath.setXPathFunctionResolver((name, arity) -> null);
+        xpath.setXPathVariableResolver(name -> null);
+        assertNotNull(xpath.getNamespaceContext());
+        assertNotNull(xpath.getXPathFunctionResolver());
+        assertNotNull(xpath.getXPathVariableResolver());
+        assertNotNull(xpath.compile("/root"));
+        assertEquals("value", xpath.evaluate("/root/text()", new 
InputSource(new StringReader("<root>value</root>"))));
+        assertEquals("value", xpath.evaluate("/root/text()", new 
InputSource(new StringReader("<root>value</root>")), 
javax.xml.xpath.XPathConstants.STRING));
+        assertEquals("value", xpath.evaluate("/root/text()", 
SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), 
false)));
+        assertEquals("value", xpath.evaluate("/root/text()", 
SecureXPath.parse(new InputSource(new StringReader("<root>value</root>")), 
false),
+                javax.xml.xpath.XPathConstants.STRING));
+        xpath.reset();
+    }
+
+    @Test
+    void preservesANullCompiledExpressionFromTheDelegate() throws Exception {
+        final javax.xml.xpath.XPath delegate = new javax.xml.xpath.XPath() {
+
+            @Override
+            public javax.xml.xpath.XPathExpression compile(final String 
expression) {
+                return null;
+            }
+
+            @Override
+            public String evaluate(final String expression, final InputSource 
source) {
+                return null;
+            }
+
+            @Override
+            public Object evaluate(final String expression, final InputSource 
source, final javax.xml.namespace.QName returnType) {
+                return null;
+            }
+
+            @Override
+            public String evaluate(final String expression, final Object item) 
{
+                return null;
+            }
+
+            @Override
+            public Object evaluate(final String expression, final Object item, 
final javax.xml.namespace.QName returnType) {
+                return null;
+            }
+
+            @Override
+            public NamespaceContext getNamespaceContext() {
+                return null;
+            }
+
+            @Override
+            public javax.xml.xpath.XPathFunctionResolver 
getXPathFunctionResolver() {
+                return null;
+            }
+
+            @Override
+            public javax.xml.xpath.XPathVariableResolver 
getXPathVariableResolver() {
+                return null;
+            }
+
+            @Override
+            public void reset() {
+            }
+
+            @Override
+            public void setNamespaceContext(final NamespaceContext context) {
+            }
+
+            @Override
+            public void setXPathFunctionResolver(final 
javax.xml.xpath.XPathFunctionResolver resolver) {
+            }
+
+            @Override
+            public void setXPathVariableResolver(final 
javax.xml.xpath.XPathVariableResolver resolver) {
+            }
+        };
+        assertNull(new SecureXPath(delegate, false).compile("/root"));
+    }
+
+    @Test
+    void wrapsParseFailuresAsXPathExpressionExceptions() {
+        final javax.xml.xpath.XPathExpressionException exception = 
assertThrows(javax.xml.xpath.XPathExpressionException.class,
+                () -> SecureXPath.parse(new InputSource(new 
StringReader("<root>")), false));
+        assertNotNull(exception.getCause());
+    }
+}

Reply via email to