This is an automated email from the ASF dual-hosted git repository. coheigea pushed a commit to branch coheigea/emptty in repository https://gitbox.apache.org/repos/asf/ws-xmlschema.git
commit 284c0fa017771222740fb83f1cb9696163de9e1c Author: Colm O hEigeartaigh <[email protected]> AuthorDate: Thu Sep 24 11:47:42 2026 +0100 Allow an xs:all alongside an empty particle in a complex content extension --- .../ws/commons/schema/walker/XmlSchemaScope.java | 40 +++++++- .../walker/ComplexContentExtensionWalkerTest.java | 101 +++++++++++++++++++-- 2 files changed, 127 insertions(+), 14 deletions(-) diff --git a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java index 08be6e43..fb835948 100644 --- a/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java +++ b/xmlschema-walker/src/main/java/org/apache/ws/commons/schema/walker/XmlSchemaScope.java @@ -32,12 +32,14 @@ import java.util.Set; import javax.xml.namespace.QName; import org.apache.ws.commons.schema.XmlSchema; +import org.apache.ws.commons.schema.XmlSchemaAll; import org.apache.ws.commons.schema.XmlSchemaAnyAttribute; import org.apache.ws.commons.schema.XmlSchemaAttribute; import org.apache.ws.commons.schema.XmlSchemaAttributeGroup; import org.apache.ws.commons.schema.XmlSchemaAttributeGroupMember; import org.apache.ws.commons.schema.XmlSchemaAttributeGroupRef; import org.apache.ws.commons.schema.XmlSchemaAttributeOrGroupRef; +import org.apache.ws.commons.schema.XmlSchemaChoice; import org.apache.ws.commons.schema.XmlSchemaComplexContent; import org.apache.ws.commons.schema.XmlSchemaComplexContentExtension; import org.apache.ws.commons.schema.XmlSchemaComplexContentRestriction; @@ -407,10 +409,18 @@ final class XmlSchemaScope { || !(ext.getParticle() instanceof XmlSchemaSequenceMember)) { // Only an xs:all is not a sequence member, and XML Schema 1.0 // does not allow one to be combined with any other particle. - throw new XmlSchemaException("An extension of " + ext.getBaseTypeName() - + " adds content to it, but one of the two content" - + " models is an xs:all group, which cannot be" - + " combined with other particles."); + // An empty particle adds no content, though, so an xs:all + // alongside one is the whole content model. + if (isEmptyParticle(ext.getParticle())) { + child = baseParticle; + } else if (isEmptyParticle(baseParticle)) { + child = ext.getParticle(); + } else { + throw new XmlSchemaException("An extension of " + ext.getBaseTypeName() + + " adds content to it, but one of the two content" + + " models is an xs:all group, which cannot be" + + " combined with other particles."); + } } else { XmlSchemaSequence seq = new XmlSchemaSequence(); seq.getItems().add((XmlSchemaSequenceMember)baseParticle); @@ -838,6 +848,28 @@ final class XmlSchemaScope { return (parent == null) ? null : parent.getUserRecognizedType(); } + /** + * Whether a particle can match nothing at all: one that may not occur, an + * xs:all or xs:sequence with no particles, or an optional xs:choice with + * none. These are the particles XML Schema treats as empty content when it + * builds the content model of an extension. + */ + private static boolean isEmptyParticle(XmlSchemaParticle particle) { + if (particle.getMaxOccurs() == 0) { + return true; + } + if (particle instanceof XmlSchemaAll) { + return ((XmlSchemaAll)particle).getItems().isEmpty(); + } + if (particle instanceof XmlSchemaSequence) { + return ((XmlSchemaSequence)particle).getItems().isEmpty(); + } + if (particle instanceof XmlSchemaChoice) { + return ((XmlSchemaChoice)particle).getItems().isEmpty() && particle.getMinOccurs() == 0; + } + return false; + } + private static boolean isAnyNamespace(String namespace) { return (namespace == null) || "##any".equals(namespace.trim()); } diff --git a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/ComplexContentExtensionWalkerTest.java b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/ComplexContentExtensionWalkerTest.java index 2d9faecf..03242850 100644 --- a/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/ComplexContentExtensionWalkerTest.java +++ b/xmlschema-walker/src/test/java/org/apache/ws/commons/schema/walker/ComplexContentExtensionWalkerTest.java @@ -19,6 +19,7 @@ package org.apache.ws.commons.schema.walker; import java.io.StringReader; +import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; @@ -41,6 +42,31 @@ import org.junit.Test; public class ComplexContentExtensionWalkerTest extends Assert { private static List<String> walkRoot(String body) { + // Records the namespace of every attribute wildcard visited. + final List<String> anyAttributeNamespaces = new ArrayList<String>(); + walk(body, (proxy, method, args) -> { + if ("onVisitAnyAttribute".equals(method.getName())) { + anyAttributeNamespaces.add(((XmlSchemaAnyAttribute)args[1]).getNamespace()); + } + return null; + }); + return anyAttributeNamespaces; + } + + /** + * Walks the root element and returns the name of every visitor callback, + * in order. + */ + private static List<String> walkEvents(String body) { + final List<String> events = new ArrayList<String>(); + walk(body, (proxy, method, args) -> { + events.add(method.getName()); + return null; + }); + return events; + } + + private static void walk(String body, InvocationHandler handler) { XmlSchemaCollection collection = new XmlSchemaCollection(); collection.read(new StringReader( "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\" xmlns:tns=\"urn:ext\"" @@ -50,18 +76,9 @@ public class ComplexContentExtensionWalkerTest extends Assert { XmlSchemaElement root = collection.getElementByQName(new QName("urn:ext", "root")); assertNotNull("the schema under test must declare a 'root' element", root); - // Records the namespace of every attribute wildcard visited. - final List<String> anyAttributeNamespaces = new ArrayList<String>(); XmlSchemaVisitor visitor = (XmlSchemaVisitor)Proxy.newProxyInstance( - XmlSchemaVisitor.class.getClassLoader(), new Class<?>[] {XmlSchemaVisitor.class}, - (proxy, method, args) -> { - if ("onVisitAnyAttribute".equals(method.getName())) { - anyAttributeNamespaces.add(((XmlSchemaAnyAttribute)args[1]).getNamespace()); - } - return null; - }); + XmlSchemaVisitor.class.getClassLoader(), new Class<?>[] {XmlSchemaVisitor.class}, handler); new XmlSchemaWalker(collection, visitor).walk(root); - return anyAttributeNamespaces; } @Test @@ -81,6 +98,70 @@ public class ComplexContentExtensionWalkerTest extends Assert { } } + /** + * An empty particle adds no content, so XML Schema allows it alongside an + * xs:all: the xs:all is then the whole content model. + */ + @Test + public void testExtendingAnAllGroupWithAnEmptySequenceWalks() { + assertAllGroupWalked("<xs:complexType name=\"base\"><xs:all>" + + "<xs:element name=\"a\" type=\"xs:string\"/>" + + "<xs:element name=\"b\" type=\"xs:string\"/>" + + "</xs:all></xs:complexType>" + + "<xs:complexType name=\"derived\"><xs:complexContent>" + + "<xs:extension base=\"tns:base\"><xs:sequence/>" + + "<xs:attribute name=\"id\" type=\"xs:string\"/>" + + "</xs:extension></xs:complexContent></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:derived\"/>"); + } + + @Test + public void testExtendingAnAllGroupWithAChoiceThatCannotOccurWalks() { + assertAllGroupWalked("<xs:complexType name=\"base\"><xs:all>" + + "<xs:element name=\"a\" type=\"xs:string\"/>" + + "</xs:all></xs:complexType>" + + "<xs:complexType name=\"derived\"><xs:complexContent>" + + "<xs:extension base=\"tns:base\">" + + "<xs:choice minOccurs=\"0\" maxOccurs=\"0\"/>" + + "</xs:extension></xs:complexContent></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:derived\"/>"); + } + + @Test + public void testExtendingAnEmptySequenceWithAnAllGroupWalks() { + assertAllGroupWalked("<xs:complexType name=\"base\"><xs:sequence/>" + + "<xs:attribute name=\"id\" type=\"xs:string\"/>" + + "</xs:complexType>" + + "<xs:complexType name=\"derived\"><xs:complexContent>" + + "<xs:extension base=\"tns:base\"><xs:all>" + + "<xs:element name=\"a\" type=\"xs:string\"/>" + + "</xs:all></xs:extension></xs:complexContent></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:derived\"/>"); + } + + @Test + public void testExtendingANonEmptySequenceWithAnAllGroupIsRejected() { + try { + walkRoot("<xs:complexType name=\"base\"><xs:sequence>" + + "<xs:element name=\"a\" type=\"xs:string\"/>" + + "</xs:sequence></xs:complexType>" + + "<xs:complexType name=\"derived\"><xs:complexContent>" + + "<xs:extension base=\"tns:base\"><xs:all>" + + "<xs:element name=\"b\" type=\"xs:string\"/>" + + "</xs:all></xs:extension></xs:complexContent></xs:complexType>" + + "<xs:element name=\"root\" type=\"tns:derived\"/>"); + fail("expected an xs:all combined with other content to be rejected"); + } catch (XmlSchemaException expected) { + assertTrue(expected.getMessage(), expected.getMessage().contains("xs:all")); + } + } + + private static void assertAllGroupWalked(String body) { + List<String> events = walkEvents(body); + assertTrue(events.toString(), events.contains("onEnterAllGroup")); + assertFalse(events.toString(), events.contains("onEnterSequenceGroup")); + } + @Test public void testExtendingASequenceWithASequenceStillWalks() { walkRoot("<xs:complexType name=\"base\"><xs:sequence>"
