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 13d201bb inherit the substitution group head's type for untyped
elements (#162)
13d201bb is described below
commit 13d201bbcfb8f0af64a112631cbe7bf5a70872db
Author: Colm O hEigeartaigh <[email protected]>
AuthorDate: Thu Sep 24 06:25:41 2026 +0100
inherit the substitution group head's type for untyped elements (#162)
---
.../ws/commons/schema/walker/XmlSchemaWalker.java | 71 +++++++++-
.../schema/walker/SubstitutionGroupTypeTest.java | 156 +++++++++++++++++++++
2 files changed, 220 insertions(+), 7 deletions(-)
diff --git
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
index b040bbd1..34601c98 100644
---
a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
+++
b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaWalker.java
@@ -62,6 +62,8 @@ public final class XmlSchemaWalker {
private final SchemasByNamespace schemasByNamespace;
private final Map<QName, XmlSchemaScope> scopeCache;
private final IdentityHashMap<XmlSchemaType, XmlSchemaType> visitedTypes;
+ private final IdentityHashMap<XmlSchemaType, XmlSchemaScope>
anonymousScopeCache;
+ private final Map<QName, XmlSchemaType> typesBySubstGroupHead;
private Set<QName> substGroupsInProgress = new HashSet<QName>();
private Set<QName> groupsInProgress = new HashSet<QName>();
@@ -98,6 +100,8 @@ public final class XmlSchemaWalker {
scopeCache = new HashMap<QName, XmlSchemaScope>();
visitedTypes = new IdentityHashMap<XmlSchemaType, XmlSchemaType>();
+ anonymousScopeCache = new IdentityHashMap<XmlSchemaType,
XmlSchemaScope>();
+ typesBySubstGroupHead = new HashMap<QName, XmlSchemaType>();
userRecognizedTypes = null;
}
@@ -150,6 +154,8 @@ public final class XmlSchemaWalker {
public void clear() {
scopeCache.clear();
visitedTypes.clear();
+ anonymousScopeCache.clear();
+ typesBySubstGroupHead.clear();
substGroupsInProgress.clear();
groupsInProgress.clear();
}
@@ -215,22 +221,25 @@ public final class XmlSchemaWalker {
element.setMaxOccurs(XmlSchemaParticle.DEFAULT_MAX_OCCURS);
}
- XmlSchemaType schemaType = element.getSchemaType();
- if (schemaType == null) {
- final QName typeQName = element.getSchemaTypeName();
- if (typeQName != null) {
- schemaType = schemasByNamespace.getTypeByName(typeQName);
- }
- }
+ XmlSchemaType schemaType = getSchemaTypeOfElement(element);
if (schemaType != null) {
XmlSchemaScope scope = null;
if ((schemaType.getQName() != null) &&
scopeCache.containsKey(schemaType.getQName())) {
scope = scopeCache.get(schemaType.getQName());
+ } else if ((schemaType.getQName() == null) &&
anonymousScopeCache.containsKey(schemaType)) {
+ /*
+ * An anonymous type is shared when a substitution group
+ * member inherits it from its head. Reusing the scope keeps
+ * the type info identical, as it is for named types.
+ */
+ scope = anonymousScopeCache.get(schemaType);
} else {
scope = new XmlSchemaScope(schemaType, schemasByNamespace,
scopeCache, userRecognizedTypes);
if (schemaType.getQName() != null) {
scopeCache.put(schemaType.getQName(), scope);
+ } else {
+ anonymousScopeCache.put(schemaType, scope);
}
}
@@ -619,6 +628,54 @@ public final class XmlSchemaWalker {
return copy;
}
+ /**
+ * Returns the type of the element. An element declared without a type
+ * takes the type of its substitution group head, if it has one.
+ */
+ private XmlSchemaType getSchemaTypeOfElement(XmlSchemaElement element) {
+ /*
+ * The heads followed are remembered, both to detect a cycle and to
+ * cache their resolved type: without the cache, walking every member
+ * of a long chain of untyped elements would take quadratic time.
+ */
+ final Set<QName> visited = new HashSet<QName>();
+ XmlSchemaElement current = element;
+ XmlSchemaType schemaType;
+ while (true) {
+ schemaType = current.getSchemaType();
+ if (schemaType != null) {
+ break;
+ }
+ final QName typeQName = current.getSchemaTypeName();
+ if (typeQName != null) {
+ schemaType = schemasByNamespace.getTypeByName(typeQName);
+ break;
+ }
+ final QName headQName = current.getSubstitutionGroup();
+ if (headQName == null) {
+ break;
+ }
+ if (typesBySubstGroupHead.containsKey(headQName)) {
+ schemaType = typesBySubstGroupHead.get(headQName);
+ break;
+ }
+ if (!visited.add(headQName)) {
+ throw new XmlSchemaException("Cyclic substitution group
detected involving "
+ + headQName + '.');
+ }
+ current = schemasByNamespace.getElementByName(headQName);
+ if (current == null) {
+ throw new XmlSchemaException("The substitution group " +
headQName + " of element "
+ + element.getQName()
+ + " does not resolve to an
element in this collection.");
+ }
+ }
+ for (QName headQName : visited) {
+ typesBySubstGroupHead.put(headQName, schemaType);
+ }
+ return schemaType;
+ }
+
private static QName getElementQName(XmlSchemaElement element) {
if (element.isRef()) {
return element.getRefBase().getTargetQName();
diff --git
a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
new file mode 100644
index 00000000..26c29247
--- /dev/null
+++
b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/SubstitutionGroupTypeTest.java
@@ -0,0 +1,156 @@
+/**
+ * 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.walker;
+
+import java.io.StringReader;
+
+import javax.xml.namespace.QName;
+
+import org.apache.ws.commons.schema.XmlSchemaCollection;
+import org.apache.ws.commons.schema.XmlSchemaElement;
+import org.apache.ws.commons.schema.XmlSchemaException;
+import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineGenerator;
+import org.apache.ws.commons.schema.docpath.XmlSchemaStateMachineNode;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * An element declared without a type, but with a substitution group, takes
+ * the type of its substitution group head.
+ */
+public class SubstitutionGroupTypeTest extends Assert {
+
+ private static final String NS = "urn:substtype";
+
+ private static XmlSchemaStateMachineGenerator walkRoot(String body) {
+ XmlSchemaCollection collection = new XmlSchemaCollection();
+ collection.read(new StringReader(
+ "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\"
xmlns:tns=\"" + NS + "\""
+ + " targetNamespace=\"" + NS + "\"
elementFormDefault=\"qualified\">"
+ + body
+ + "</xs:schema>"));
+ XmlSchemaElement root = collection.getElementByQName(new QName(NS,
"root"));
+ assertNotNull("the schema under test must declare a 'root' element",
root);
+
+ XmlSchemaStateMachineGenerator generator = new
XmlSchemaStateMachineGenerator();
+ XmlSchemaWalker walker = new XmlSchemaWalker(collection, generator);
+ walker.walk(root);
+ return generator;
+ }
+
+ private static void assertRejected(String body, String expectedFragment) {
+ try {
+ walkRoot(body);
+ fail("expected the substitution group to be rejected");
+ } catch (XmlSchemaException expected) {
+ assertTrue(expected.getMessage(),
expected.getMessage().contains(expectedFragment));
+ }
+ }
+
+ @Test
+ public void testUntypedMemberInheritsSimpleTypeOfHead() {
+ XmlSchemaStateMachineGenerator generator =
+ walkRoot("<xs:element name=\"head\" type=\"xs:boolean\"
abstract=\"true\"/>"
+ + "<xs:element name=\"title\"
substitutionGroup=\"tns:head\"/>"
+ + "<xs:element
name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:element ref=\"tns:head\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>");
+
+ XmlSchemaStateMachineNode title =
+ generator.getStateMachineNodesByQName().get(new QName(NS,
"title"));
+ assertNotNull("the substitute element must be walked", title);
+ assertEquals(XmlSchemaTypeInfo.Type.ATOMIC,
title.getElementType().getType());
+ assertEquals(XmlSchemaBaseSimpleType.BOOLEAN,
title.getElementType().getBaseType());
+ }
+
+ @Test
+ public void testUntypedMemberInheritsComplexTypeThroughChain() {
+ XmlSchemaStateMachineGenerator generator =
+ walkRoot("<xs:complexType name=\"HeadType\"><xs:sequence>"
+ + "<xs:element name=\"leaf\" type=\"xs:string\"/>"
+ + "</xs:sequence><xs:attribute name=\"id\"
type=\"xs:string\"/></xs:complexType>"
+ + "<xs:element name=\"head\" type=\"tns:HeadType\"
abstract=\"true\"/>"
+ + "<xs:element name=\"middle\"
substitutionGroup=\"tns:head\" abstract=\"true\"/>"
+ + "<xs:element name=\"title\"
substitutionGroup=\"tns:middle\"/>"
+ + "<xs:element
name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:element ref=\"tns:head\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>");
+
+ XmlSchemaStateMachineNode title =
+ generator.getStateMachineNodesByQName().get(new QName(NS,
"title"));
+ assertNotNull("the substitute element must be walked", title);
+ assertEquals(XmlSchemaTypeInfo.Type.COMPLEX,
title.getElementType().getType());
+ assertEquals(1, title.getAttributes().size());
+ assertNotNull("the head's content model must be walked for the
substitute",
+ generator.getStateMachineNodesByQName().get(new
QName(NS, "leaf")));
+ }
+
+ @Test
+ public void testUntypedMemberInheritsAnonymousTypeOfHead() {
+ XmlSchemaStateMachineGenerator generator =
+ walkRoot("<xs:element name=\"head\"><xs:complexType><xs:sequence>"
+ + "<xs:element name=\"leaf\" type=\"xs:string\"/>"
+ + "<xs:element ref=\"tns:head\" minOccurs=\"0\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>"
+ + "<xs:element name=\"title\"
substitutionGroup=\"tns:head\"/>"
+ + "<xs:element
name=\"root\"><xs:complexType><xs:sequence>"
+ + "<xs:element ref=\"tns:head\"/>"
+ + "</xs:sequence></xs:complexType></xs:element>");
+
+ XmlSchemaStateMachineNode head =
+ generator.getStateMachineNodesByQName().get(new QName(NS, "head"));
+ XmlSchemaStateMachineNode title =
+ generator.getStateMachineNodesByQName().get(new QName(NS,
"title"));
+ assertNotNull("the head element must be walked", head);
+ assertNotNull("the substitute element must be walked", title);
+ assertSame(head.getElementType(), title.getElementType());
+ }
+
+ @Test
+ public void testLongUntypedChainResolvesEachHeadOnce() {
+ StringBuilder body = new StringBuilder("<xs:element name=\"root\"
type=\"xs:string\"/>");
+ String previous = "root";
+ for (int i = 0; i < 2000; i++) {
+ body.append("<xs:element name=\"e").append(i).append("\"
substitutionGroup=\"tns:")
+ .append(previous).append("\"/>");
+ previous = "e" + i;
+ }
+ XmlSchemaStateMachineGenerator generator = walkRoot(body.toString());
+
+ XmlSchemaStateMachineNode last =
+ generator.getStateMachineNodesByQName().get(new QName(NS,
previous));
+ assertNotNull("the last element of the chain must be walked", last);
+ assertEquals(XmlSchemaBaseSimpleType.STRING,
last.getElementType().getBaseType());
+ }
+
+ @Test
+ public void testMissingSubstitutionGroupHeadIsRejected() {
+ assertRejected("<xs:element name=\"root\"
substitutionGroup=\"tns:missing\"/>",
+ "does not resolve to an element");
+ }
+
+ @Test
+ public void testCyclicUntypedSubstitutionGroupIsRejected() {
+ assertRejected("<xs:element name=\"a\" substitutionGroup=\"tns:b\"/>"
+ + "<xs:element name=\"root\"
substitutionGroup=\"tns:a\"/>"
+ + "<xs:element name=\"b\"
substitutionGroup=\"tns:root\"/>",
+ "Cyclic substitution group");
+ }
+}