This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/XmlSchemaWalker in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit e323fcc85a666e2613b51639dde7d219a30f9014 Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 24 06:23:28 2026 +0100 inherit the substitution group head's type for untyped elements --- .../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"); + } +}
