This is an automated email from the ASF dual-hosted git repository.
coheigea pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
The following commit(s) were added to refs/heads/master by this push:
new df755e57 Fix the path finder refusing or failing on common valid
documents (#172)
df755e57 is described below
commit df755e57d4e378d6d6b3ce46e236e3fb98670389
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 24 12:08:17 2026 +0100
Fix the path finder refusing or failing on common valid documents (#172)
---
.../schema/docpath/XmlSchemaNamespaceContext.java | 13 +-
.../schema/docpath/XmlSchemaPathFinder.java | 119 +++++++++++---
.../docpath/TestPathFinderValidDocuments.java | 177 +++++++++++++++++++++
.../docpath/TestXmlSchemaNamespaceContext.java | 13 ++
4 files changed, 296 insertions(+), 26 deletions(-)
diff --git
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaNamespaceContext.java
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaNamespaceContext.java
index 012dc587..3edf9a91 100644
---
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaNamespaceContext.java
+++
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaNamespaceContext.java
@@ -137,18 +137,21 @@ public final class XmlSchemaNamespaceContext implements
NamespacePrefixList {
/**
* Adds a new prefix mapping to the context. The prefix may be an empty
* string to represent the default namespace, but it cannot be null. The
- * namespace URI can never be empty or null.
+ * namespace URI cannot be null, and can only be empty for the default
+ * namespace, where it undeclares the default namespace for the scope of
+ * the mapping as <code>xmlns=""</code> does.
*
* @param prefix The prefix to represent the namespace URI.
* @param namespaceUri the namespace URI represented by the prefix.
* @throws IllegalArgumentException if the prefix is null, or if the
- * namespace URI is null or empty.
+ * namespace URI is null, or empty for a non-empty prefix.
*/
public void addNamespace(String prefix, String namespaceUri) {
- if ((prefix == null) || (namespaceUri == null) ||
(namespaceUri.length() == 0)) {
+ if ((prefix == null) || (namespaceUri == null)
+ || ((namespaceUri.length() == 0) && (prefix.length() > 0))) {
- throw new IllegalArgumentException("The prefix may not be null,
and the namespace URI "
- + "may neither be null nor
empty.");
+ throw new IllegalArgumentException("The prefix may not be null,
and the namespace URI may"
+ + " not be null, nor empty
unless the prefix is.");
} else if (isRecognizedPrefix(prefix)) {
// These are already mapped.
diff --git
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java
index 0fa67992..a0cc239f 100644
---
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java
+++
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/docpath/XmlSchemaPathFinder.java
@@ -20,6 +20,7 @@
package org.apache.ws.commons.schema.docpath;
import java.util.ArrayList;
+import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
@@ -93,6 +94,22 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
private ArrayList<QName> elementStack;
private ArrayList<QName> anyStack;
+ /*
+ * The document node of the element most recently ended, until the next
+ * element starts. The current position can stay on an element after it
+ * ends, and a recursive type puts an element of the same name above it,
+ * so the name alone cannot say whether the current element is the one
+ * being ended.
+ */
+ private XmlSchemaDocumentNode<U> endedElement;
+
+ /*
+ * Whether a state machine node's content can match no elements at all,
+ * cached as the question is asked repeatedly for the same nodes.
+ */
+ private final Map<XmlSchemaStateMachineNode, Boolean> emptyContentCache =
+ new IdentityHashMap<XmlSchemaStateMachineNode, Boolean>();
+
private XmlSchemaPathManager<U, V> pathMgr;
/*
@@ -513,6 +530,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
@Override
public void startDocument() throws SAXException {
currentPath = null;
+ endedElement = null;
traversedElements.clear();
elementStack.clear();
@@ -1008,8 +1026,8 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
return rootPathNode;
}
- private static <U, V> Fulfillment isPositionFulfilled(XmlSchemaPathNode<U,
V> currentPath,
- List<Integer>
possiblePaths) {
+ private Fulfillment isPositionFulfilled(XmlSchemaPathNode<U, V>
currentPath,
+ List<Integer> possiblePaths) {
boolean completelyFulfilled = true;
boolean partiallyFulfilled = true;
@@ -1018,7 +1036,9 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
if (currentPath.getDocumentNode() == null) {
// This is the root node. It is not fulfilled.
partiallyFulfilled = false;
- } else if (currentPath.getDocIteration() >= state.getMinOccurs()) {
+ } else if ((currentPath.getDocIteration() >= state.getMinOccurs())
+ || canMatchEmptyContent(state)) {
+ // The remaining occurrences, if any, can each match nothing.
partiallyFulfilled = true;
} else {
partiallyFulfilled = false;
@@ -1066,7 +1086,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
if ((children != null) && children.containsKey(stateIndex)) {
final XmlSchemaDocumentNode<U> child =
children.get(stateIndex);
final int iteration = child.getIteration();
- if (iteration >= nextState.getMinOccurs()) {
+ if ((iteration >= nextState.getMinOccurs()) ||
canMatchEmptyContent(nextState)) {
groupPartiallyFulfilled = true;
if (possiblePaths != null) {
possiblePaths.clear();
@@ -1081,7 +1101,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
possiblePaths.add(stateIndex);
}
} else {
- if (nextState.getMinOccurs() == 0) {
+ if ((nextState.getMinOccurs() == 0) ||
canMatchEmptyContent(nextState)) {
groupPartiallyFulfilled = true;
}
if (nextState.getMaxOccurs() == 0) {
@@ -1139,7 +1159,8 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
if ((children != null) && children.containsKey(stateIndex)) {
final XmlSchemaDocumentNode<U> child =
children.get(stateIndex);
- if (child.getIteration() < nextState.getMinOccurs()) {
+ if ((child.getIteration() < nextState.getMinOccurs())
+ && !canMatchEmptyContent(nextState)) {
partiallyFulfilled = false;
}
if (child.getIteration() < nextState.getMaxOccurs()) {
@@ -1149,7 +1170,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
}
}
} else {
- if (nextState.getMinOccurs() > 0) {
+ if ((nextState.getMinOccurs() > 0) &&
!canMatchEmptyContent(nextState)) {
partiallyFulfilled = false;
}
if (nextState.getMaxOccurs() > 0) {
@@ -1375,14 +1396,10 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
return null;
}
- // If this is a group, confirm it has children.
- if (!state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
- && !state.getNodeType().equals(XmlSchemaStateMachineNode.Type.ANY)
- && ((state.getPossibleNextStates() == null) ||
state.getPossibleNextStates().isEmpty())) {
-
- throw new IllegalStateException("Group " + state.getNodeType()
- + " has no children. Found when
processing " + elemQName);
- }
+ /*
+ * A group with no children, such as an empty xs:sequence, is legal:
+ * it matches no elements, which the loops below find for themselves.
+ */
List<PathSegment<U, V>> choices = null;
@@ -1433,7 +1450,12 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
+
startNode.getDocSequencePosition());
}
- final boolean reachedMinOccurs = (nextPath.getDocIteration()
>= nextPath.getMinOccurs());
+ /*
+ * Content that can match nothing, such as a group of optional
+ * elements, satisfies its remaining occurrences without any.
+ */
+ final boolean reachedMinOccurs = (nextPath.getDocIteration()
>= nextPath.getMinOccurs())
+ ||
canMatchEmptyContent(nextPath.getStateMachineNode());
final List<PathSegment<U, V>> seqPaths = find(nextPath,
elemQName, currDepth + 1);
@@ -1521,10 +1543,9 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
* If the XmlSchemaAny namespace and processing rules apply, this
* element matches. False otherwise.
*/
- if (traversedElements.size() < 2) {
+ if (traversedElements.isEmpty()) {
throw new IllegalStateException("Reached a wildcard element
while searching for " + elemQName
- + ", but we've only seen " +
traversedElements.size()
- + " element(s)!");
+ + ", but that is the root
element!");
}
final XmlSchemaAny any = state.getAny();
@@ -1557,7 +1578,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
if ("##targetNamespace".equals(namespace)) {
needTargetNamespace = true;
- } else if ("##local".equals(namespace) &&
(elemQName.getNamespaceURI() == null)) {
+ } else if ("##local".equals(namespace) &&
(elemQName.getNamespaceURI().length() == 0)) {
matches = true;
@@ -1654,8 +1675,10 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
XmlSchemaDocumentNode<U> iter = currentPath.getDocumentNode();
if
(iter.getStateMachineNode().getNodeType().equals(XmlSchemaStateMachineNode.Type.ELEMENT)
- &&
iter.getStateMachineNode().getElement().getQName().equals(element)) {
+ &&
iter.getStateMachineNode().getElement().getQName().equals(element)
+ && (iter != endedElement)) {
// We are already at the element!
+ endedElement = iter;
return;
}
@@ -1678,6 +1701,7 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
+ currentPath.getStateMachineNode()
+ ", which does not represent
element " + element);
}
+ endedElement = currentPath.getDocumentNode();
}
private void followPath(PathSegment<U, V> path) {
@@ -1699,6 +1723,59 @@ public final class XmlSchemaPathFinder<U, V> extends
DefaultHandler {
pathMgr.followPath(startNode);
currentPath = path.getEnd();
+
+ // An element has started, so none is the one just ended.
+ endedElement = null;
+ }
+
+ /*
+ * Whether a node's content can match no elements at all: an xs:sequence
+ * or xs:all whose particles can each occur zero times, or an xs:choice
+ * or substitution group with one such option. An element or wildcard
+ * always matches an element. Group nesting ends at elements, which this
+ * does not look inside, so the recursion is bounded by the walker's own
+ * limit on nested groups.
+ */
+ private boolean canMatchEmptyContent(XmlSchemaStateMachineNode state) {
+ final Boolean cached = emptyContentCache.get(state);
+ if (cached != null) {
+ return cached.booleanValue();
+ }
+
+ boolean result;
+ final List<XmlSchemaStateMachineNode> children =
state.getPossibleNextStates();
+
+ switch (state.getNodeType()) {
+ case SEQUENCE:
+ case ALL:
+ result = true;
+ if (children != null) {
+ for (XmlSchemaStateMachineNode child : children) {
+ if ((child.getMinOccurs() > 0) &&
!canMatchEmptyContent(child)) {
+ result = false;
+ break;
+ }
+ }
+ }
+ break;
+ case CHOICE:
+ case SUBSTITUTION_GROUP:
+ result = false;
+ if (children != null) {
+ for (XmlSchemaStateMachineNode child : children) {
+ if ((child.getMinOccurs() == 0) ||
canMatchEmptyContent(child)) {
+ result = true;
+ break;
+ }
+ }
+ }
+ break;
+ default:
+ result = false;
+ }
+
+ emptyContentCache.put(state, Boolean.valueOf(result));
+ return result;
}
/*
diff --git
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderValidDocuments.java
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderValidDocuments.java
new file mode 100644
index 00000000..e318d3e3
--- /dev/null
+++
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestPathFinderValidDocuments.java
@@ -0,0 +1,177 @@
+/**
+ * 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
+ *
+ * http://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.ws.commons.schema.docpath;
+
+import java.io.StringReader;
+
+import javax.xml.namespace.QName;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.walker.XmlSchemaWalker;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.xml.sax.InputSource;
+
+/**
+ * Documents that are valid against their schema - each checked against the
JDK's own validator -
+ * must be walked without error, and ones that are not must still be refused.
+ */
+public class TestPathFinderValidDocuments extends Assert {
+
+ private DocumentBuilder docBuilder;
+
+ @Before
+ public void setUp() throws Exception {
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setNamespaceAware(true);
+ docBuilder = factory.newDocumentBuilder();
+ }
+
+ private void walk(String schemaBody, String xml) throws Exception {
+ XmlSchemaCollection collection = new XmlSchemaCollection();
+ collection.read(new StringReader(
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"
xmlns:t=\"urn:t\""
+ + " targetNamespace=\"urn:t\" elementFormDefault=\"qualified\">"
+ + schemaBody + "</xs:schema>"));
+
+ XmlSchemaStateMachineGenerator generator = new
XmlSchemaStateMachineGenerator();
+ new XmlSchemaWalker(collection, generator)
+ .walk(collection.getElementByQName(new QName("urn:t", "root")));
+
+ XmlSchemaPathFinder<Object, Object> pathFinder =
+ new XmlSchemaPathFinder<Object, Object>(generator.getStartNode());
+ new SaxWalkerOverDom(pathFinder)
+ .walk(docBuilder.parse(new InputSource(new StringReader(xml))));
+ }
+
+ private void assertRefused(String schemaBody, String xml) throws Exception
{
+ try {
+ walk(schemaBody, xml);
+ } catch (RuntimeException expected) {
+ return;
+ }
+ fail("expected the document to be refused: " + xml);
+ }
+
+ private static final String LOCAL_WILDCARD =
+ "<xs:element name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:element name=\"a\" type=\"xs:string\"/>"
+ + "<xs:any namespace=\"##local\" processContents=\"skip\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>";
+
+ /**
+ * xmlns="" undeclares the default namespace, which the namespace context
used to refuse.
+ */
+ @Test
+ public void testUndeclaredDefaultNamespace() throws Exception {
+ walk(LOCAL_WILDCARD, "<root xmlns=\"urn:t\"><a>x</a><x
xmlns=\"\"/></root>");
+ }
+
+ @Test
+ public void testLocalWildcardMatchesAnElementInNoNamespace() throws
Exception {
+ walk(LOCAL_WILDCARD, "<t:root
xmlns:t=\"urn:t\"><t:a>x</t:a><x/></t:root>");
+ }
+
+ @Test
+ public void testLocalWildcardRefusesAnElementInANamespace() throws
Exception {
+ assertRefused(LOCAL_WILDCARD, "<root xmlns=\"urn:t\"><a>x</a><x
xmlns=\"urn:other\"/></root>");
+ }
+
+ @Test
+ public void testWildcardAsTheFirstChildOfTheRoot() throws Exception {
+ walk("<xs:element name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:any processContents=\"skip\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>",
+ "<root xmlns=\"urn:t\"><x/></root>");
+ }
+
+ private static final String EMPTY_BASE_EXTENDED =
+ "<xs:complexType name=\"base\"><xs:sequence/>"
+ + "<xs:attribute name=\"id\" type=\"xs:string\"/></xs:complexType>"
+ + "<xs:complexType name=\"derived\"><xs:complexContent><xs:extension
base=\"t:base\">"
+ + "<xs:sequence><xs:element name=\"a\" type=\"xs:int\"/></xs:sequence>"
+ + "</xs:extension></xs:complexContent></xs:complexType>"
+ + "<xs:element name=\"root\" type=\"t:derived\"/>";
+
+ @Test
+ public void testEmptyBaseSequenceExtendedWithContent() throws Exception {
+ walk(EMPTY_BASE_EXTENDED, "<root xmlns=\"urn:t\"
id=\"1\"><a>1</a></root>");
+ }
+
+ @Test
+ public void testEmptyBaseSequenceExtendedWithContentStillChecksIt() throws
Exception {
+ assertRefused(EMPTY_BASE_EXTENDED, "<root xmlns=\"urn:t\"
id=\"1\"><a>notint</a></root>");
+ }
+
+ /**
+ * A required group whose content can match nothing - here a base type
whose elements are all
+ * optional - must not stop the document moving on to what follows it.
+ */
+ @Test
+ public void testBaseOfOptionalElementsExtendedWithContent() throws
Exception {
+ walk("<xs:complexType name=\"base\"><xs:sequence>"
+ + "<xs:element name=\"a\" type=\"xs:string\" minOccurs=\"0\"/>"
+ + "</xs:sequence></xs:complexType>"
+ + "<xs:complexType
name=\"derived\"><xs:complexContent><xs:extension base=\"t:base\">"
+ + "<xs:sequence><xs:element name=\"b\"
type=\"xs:string\"/></xs:sequence>"
+ + "</xs:extension></xs:complexContent></xs:complexType>"
+ + "<xs:element name=\"root\" type=\"t:derived\"/>",
+ "<root xmlns=\"urn:t\"><b>x</b></root>");
+ }
+
+ @Test
+ public void testChoiceWithAnEmptyOption() throws Exception {
+ String schema = "<xs:element
name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:choice><xs:sequence/><xs:element name=\"a\"
type=\"xs:string\"/></xs:choice>"
+ + "<xs:element name=\"b\" type=\"xs:string\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>";
+ walk(schema, "<root xmlns=\"urn:t\"><b>x</b></root>");
+ walk(schema, "<root xmlns=\"urn:t\"><a>x</a><b>x</b></root>");
+ assertRefused(schema, "<root xmlns=\"urn:t\"><c>x</c></root>");
+ }
+
+ @Test
+ public void testChoiceWithAnOptionalOption() throws Exception {
+ walk("<xs:element name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:choice><xs:element name=\"a\" type=\"xs:string\"
minOccurs=\"0\"/>"
+ + "<xs:element name=\"c\" type=\"xs:string\"/></xs:choice>"
+ + "<xs:element name=\"b\" type=\"xs:string\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>",
+ "<root xmlns=\"urn:t\"><b>x</b></root>");
+ }
+
+ /**
+ * Ending an element used to stop at the nearest element of the same name,
which in a
+ * recursive type is the one just ended rather than its parent.
+ */
+ @Test
+ public void testSelfRecursiveType() throws Exception {
+ String schema = "<xs:complexType name=\"T0\"><xs:sequence>"
+ + "<xs:element name=\"c\" type=\"t:T0\"
minOccurs=\"0\" maxOccurs=\"3\"/>"
+ + "</xs:sequence></xs:complexType>"
+ + "<xs:element name=\"root\" type=\"t:T0\"/>";
+ walk(schema, "<root xmlns=\"urn:t\"><c><c/></c></root>");
+ walk(schema, "<root
xmlns=\"urn:t\"><c><c><c/></c></c><c><c/></c><c/></root>");
+ assertRefused(schema, "<root xmlns=\"urn:t\"><c/><c/><c/><c/></root>");
+ }
+}
diff --git
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestXmlSchemaNamespaceContext.java
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestXmlSchemaNamespaceContext.java
index 8f7b5222..a87ca7a2 100644
---
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestXmlSchemaNamespaceContext.java
+++
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/docpath/TestXmlSchemaNamespaceContext.java
@@ -188,6 +188,19 @@ public class TestXmlSchemaNamespaceContext {
nsContext.addNamespace("avro", "");
}
+ /**
+ * xmlns="" undeclares the default namespace for the scope of the mapping.
+ */
+ @Test
+ public void testUndeclareDefaultNamespace() {
+ XmlSchemaNamespaceContext nsContext = new XmlSchemaNamespaceContext();
+ nsContext.addNamespace("", "urn:outer");
+ nsContext.addNamespace("", "");
+ assertEquals("", nsContext.getNamespaceURI(""));
+ nsContext.removeNamespace("");
+ assertEquals("urn:outer", nsContext.getNamespaceURI(""));
+ }
+
@Test(expected = IllegalArgumentException.class)
public void testGetPrefixesWithNullNamespace() {
XmlSchemaNamespaceContext nsContext = new XmlSchemaNamespaceContext();