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

Reply via email to