blob: 3251687b94b13ba8d4d1360af0966913a10c3d4a [file] [log] [blame]
/**
* 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.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
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.XmlSchemaAllMember;
import org.apache.ws.commons.schema.XmlSchemaAny;
import org.apache.ws.commons.schema.XmlSchemaChoice;
import org.apache.ws.commons.schema.XmlSchemaChoiceMember;
import org.apache.ws.commons.schema.XmlSchemaCollection;
import org.apache.ws.commons.schema.XmlSchemaElement;
import org.apache.ws.commons.schema.XmlSchemaGroup;
import org.apache.ws.commons.schema.XmlSchemaGroupParticle;
import org.apache.ws.commons.schema.XmlSchemaGroupRef;
import org.apache.ws.commons.schema.XmlSchemaParticle;
import org.apache.ws.commons.schema.XmlSchemaSequence;
import org.apache.ws.commons.schema.XmlSchemaSequenceMember;
import org.apache.ws.commons.schema.XmlSchemaType;
/**
* Walks an {@link XmlSchema} from a starting {@link XmlSchemaElement},
* notifying attached visitors as it descends.
*/
public final class XmlSchemaWalker {
private Set<QName> userRecognizedTypes;
private final XmlSchemaCollection schemas;
private final List<XmlSchemaVisitor> visitors;
private final Map<QName, List<XmlSchemaElement>> elemsBySubstGroup;
private final SchemasByNamespace schemasByNamespace;
private final Map<QName, XmlSchemaScope> scopeCache;
private final Set<QName> visitedElements;
/**
* Initializes the {@link XmlSchemaWalker} with the
* {@link XmlSchemaCollection} to reference when following an
* {@link XmlSchemaElement}.
*/
public XmlSchemaWalker(XmlSchemaCollection xmlSchemas) {
if (xmlSchemas == null) {
throw new IllegalArgumentException("Input XmlSchemaCollection cannot be null.");
}
schemas = xmlSchemas;
visitors = new ArrayList<XmlSchemaVisitor>(1);
schemasByNamespace = new SchemasByNamespace();
elemsBySubstGroup = new HashMap<QName, List<XmlSchemaElement>>();
for (XmlSchema schema : schemas.getXmlSchemas()) {
schemasByNamespace.addSchema(schema.getTargetNamespace(), schema);
for (XmlSchemaElement elem : schema.getElements().values()) {
if (elem.getSubstitutionGroup() != null) {
List<XmlSchemaElement> elems = elemsBySubstGroup.get(elem.getSubstitutionGroup());
if (elems == null) {
elems = new ArrayList<XmlSchemaElement>();
elemsBySubstGroup.put(elem.getSubstitutionGroup(), elems);
}
elems.add(elem);
}
}
}
scopeCache = new HashMap<QName, XmlSchemaScope>();
visitedElements = new java.util.HashSet<QName>();
userRecognizedTypes = null;
}
/**
* Initializes the <code>XmlSchemaWalker</code> with an
* {@link XmlSchemaVisitor} to notify as the schema is walked.
* <p>
* (Other visitors may continue to be added after this one.)
* </p>
*
* @param xmlSchemas The set of schemas to walk.
* @param visitor The visitor to visit during the walk.
*/
public XmlSchemaWalker(XmlSchemaCollection xmlSchemas, XmlSchemaVisitor visitor) {
this(xmlSchemas);
if (visitor != null) {
visitors.add(visitor);
}
}
/**
* Adds a new visitor to be notified as the XML Schemas are walked.
*
* @param visitor The visitor to be notified.
* @return This <code>XmlSchemaWalker</code> instance for method chaining.
*/
public XmlSchemaWalker addVisitor(XmlSchemaVisitor visitor) {
visitors.add(visitor);
return this;
}
/**
* Removes the visitor to be notified as the XML Schemas are walked.
*
* @param visitor The visitor to remove.
* @return This <code>XmlSchemaWalker</code> instance for method chaining.
*/
public XmlSchemaWalker removeVisitor(XmlSchemaVisitor visitor) {
if (visitor != null) {
visitors.remove(visitor);
}
return this;
}
/**
* Clears the internal state in preparation for another walk through the
* schema.
*/
public void clear() {
scopeCache.clear();
visitedElements.clear();
}
/**
* Defines the set of types the calling code recognizes. If one of the types
* are found during the walk through the XML Schema, it is attached to the
* relevant {@link XmlSchemaTypeInfo} that is passed to the
* {@link XmlSchemaVisitor}s, with lower types in the hierarchy taking
* precedence over higher types.
* <p>
* This information is useful when translating from XML Schema to another
* schema, as this automatically associates the destination type with the
* source XML Schema type.
* </p>
*
* @param userRecognizedTypes The set of types the user recognizes and would
* like recognized when traversed.
*/
public void setUserRecognizedTypes(Set<QName> userRecognizedTypes) {
this.userRecognizedTypes = userRecognizedTypes;
}
/**
* The user-defined types set with the call to
* {@link #setUserRecognizedTypes(Set)}, or <code>null</code> if none.
*/
public Set<QName> getUserRecognizedTypes() {
return userRecognizedTypes;
}
/**
* Initiates a walk through the {@link XmlSchemaCollection} starting with
* the provided root {@link XmlSchemaElement}. Any visitors will be notified
* as the walk progresses.
* <p>
* Once this method completes, call {@link #clear()} before starting another
* walk through the XML Schemas.
* </p>
*
* @param element The root element to start the walk from.
*/
public void walk(XmlSchemaElement element) {
element = getElement(element, false);
final XmlSchemaElement substGroupElem = element;
/*
* If this element is the root of a substitution group, notify the
* visitors.
*/
List<XmlSchemaElement> substitutes = null;
if (elemsBySubstGroup.containsKey(getElementQName(element))) {
substitutes = elemsBySubstGroup.get(element.getQName());
for (XmlSchemaVisitor visitor : visitors) {
visitor.onEnterSubstitutionGroup(substGroupElem);
}
// Force a copy to change the min & max occurs.
element = getElement(element, true);
element.setMinOccurs(XmlSchemaParticle.DEFAULT_MIN_OCCURS);
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);
}
}
if (schemaType != null) {
XmlSchemaScope scope = null;
if ((schemaType.getQName() != null) && scopeCache.containsKey(schemaType.getQName())) {
scope = scopeCache.get(schemaType.getQName());
} else {
scope = new XmlSchemaScope(schemaType, schemasByNamespace, scopeCache, userRecognizedTypes);
if (schemaType.getQName() != null) {
scopeCache.put(schemaType.getQName(), scope);
}
}
// 1. Fetch all attributes as a List<XmlSchemaAttribute>.
final Collection<XmlSchemaAttrInfo> attrs = scope.getAttributesInScope();
final XmlSchemaTypeInfo typeInfo = scope.getTypeInfo();
// 2. for each visitor, call visitor.startElement(element, type);
final boolean previouslyVisited = (!element.isAnonymous() && visitedElements.contains(element
.getQName()));
for (XmlSchemaVisitor visitor : visitors) {
visitor.onEnterElement(element, typeInfo, previouslyVisited);
}
if (!element.isAnonymous() && !previouslyVisited) {
visitedElements.add(element.getQName());
}
// If we already visited this element, skip the attributes and
// child.
if (!previouslyVisited) {
// 3. Walk the attributes in the element, retrieving type
// information.
if (attrs != null) {
for (XmlSchemaAttrInfo attr : attrs) {
XmlSchemaType attrType = attr.getAttribute().getSchemaType();
if (attrType != null) {
XmlSchemaScope attrScope;
if ((attrType.getQName() != null) && scopeCache.containsKey(attrType.getQName())) {
attrScope = scopeCache.get(attrType.getQName());
} else {
attrScope = new XmlSchemaScope(attrType,
schemasByNamespace, scopeCache,
userRecognizedTypes);
if (attrType.getName() != null) {
scopeCache.put(attrType.getQName(), attrScope);
}
}
final XmlSchemaTypeInfo attrTypeInfo = attrScope.getTypeInfo();
attr.setType(attrTypeInfo);
}
for (XmlSchemaVisitor visitor : visitors) {
visitor.onVisitAttribute(element, attr);
}
}
}
// 4. Visit the anyAttribute, if any.
if (scope.getAnyAttribute() != null) {
for (XmlSchemaVisitor visitor : visitors) {
visitor.onVisitAnyAttribute(element, scope.getAnyAttribute());
}
}
/*
* 5. Notify that we visited all of the attributes (even if
* there weren't any).
*/
for (XmlSchemaVisitor visitor : visitors) {
visitor.onEndAttributes(element, typeInfo);
}
// 6. Walk the child groups and elements (if any), depth-first.
final XmlSchemaParticle child = scope.getParticle();
if (child != null) {
walk(child);
}
}
/*
* 7. On the way back up, call visitor.endElement(element, type,
* attributes);
*/
for (XmlSchemaVisitor visitor : visitors) {
visitor.onExitElement(element, typeInfo, previouslyVisited);
}
} else if (!element.isAbstract()) {
throw new IllegalStateException("Element " + element.getQName()
+ " is not abstract and has no type.");
}
// 8. Now handle substitute elements, if any.
if (substitutes != null) {
for (XmlSchemaElement substitute : substitutes) {
walk(substitute);
}
for (XmlSchemaVisitor visitor : visitors) {
visitor.onExitSubstitutionGroup(substGroupElem);
}
}
}
private void walk(XmlSchemaParticle particle) {
if (particle instanceof XmlSchemaGroupRef) {
XmlSchemaGroupRef groupRef = (XmlSchemaGroupRef)particle;
XmlSchemaGroupParticle group = groupRef.getParticle();
if (group == null) {
XmlSchemaGroup g = schemasByNamespace.getGroupByName(groupRef.getRefName());
if (g != null) {
group = g.getParticle();
}
}
walk(group, groupRef.getMinOccurs(), groupRef.getMaxOccurs());
} else if (particle instanceof XmlSchemaGroupParticle) {
walk((XmlSchemaGroupParticle)particle, particle.getMinOccurs(), particle.getMaxOccurs());
} else if (particle instanceof XmlSchemaElement) {
walk((XmlSchemaElement)particle);
} else if (particle instanceof XmlSchemaAny) {
for (XmlSchemaVisitor visitor : visitors) {
visitor.onVisitAny((XmlSchemaAny)particle);
}
} else {
throw new IllegalArgumentException("Unknown particle type " + particle.getClass().getName());
}
}
private void walk(XmlSchemaGroupParticle group, long minOccurs, long maxOccurs) {
// Only make a copy of the particle if the minOccurs or maxOccurs was
// set.
final boolean forceCopy = ((minOccurs != group.getMinOccurs()) || (maxOccurs != group.getMaxOccurs()));
// 1. Determine the group particle type.
XmlSchemaAll all = null;
XmlSchemaChoice choice = null;
XmlSchemaSequence seq = null;
ArrayList<XmlSchemaParticle> children = null;
if (group instanceof XmlSchemaAll) {
all = (XmlSchemaAll)group;
} else if (group instanceof XmlSchemaChoice) {
choice = (XmlSchemaChoice)group;
} else if (group instanceof XmlSchemaSequence) {
seq = (XmlSchemaSequence)group;
} else {
throw new IllegalArgumentException("Unrecognized XmlSchemaGroupParticle of type "
+ group.getClass().getName());
}
// 2. Make a copy if necessary.
if (forceCopy) {
if (all != null) {
XmlSchemaAll copy = new XmlSchemaAll();
copy.setAnnotation(all.getAnnotation());
copy.setId(all.getId());
copy.setLineNumber(all.getLineNumber());
copy.setLinePosition(all.getLinePosition());
copy.setMetaInfoMap(all.getMetaInfoMap());
copy.setMinOccurs(minOccurs);
copy.setMaxOccurs(maxOccurs);
copy.setSourceURI(all.getSourceURI());
copy.setUnhandledAttributes(all.getUnhandledAttributes());
copy.getItems().addAll(all.getItems());
all = copy;
} else if (choice != null) {
XmlSchemaChoice copy = new XmlSchemaChoice();
copy.setAnnotation(choice.getAnnotation());
copy.setId(choice.getId());
copy.setLineNumber(choice.getLineNumber());
copy.setLinePosition(choice.getLinePosition());
copy.setMinOccurs(minOccurs);
copy.setMaxOccurs(maxOccurs);
copy.setMetaInfoMap(choice.getMetaInfoMap());
copy.setSourceURI(choice.getSourceURI());
copy.setUnhandledAttributes(choice.getUnhandledAttributes());
copy.getItems().addAll(choice.getItems());
choice = copy;
} else if (seq != null) {
XmlSchemaSequence copy = new XmlSchemaSequence();
copy.setAnnotation(seq.getAnnotation());
copy.setId(seq.getId());
copy.setLineNumber(seq.getLineNumber());
copy.setLinePosition(seq.getLinePosition());
copy.setMinOccurs(minOccurs);
copy.setMaxOccurs(maxOccurs);
copy.setMetaInfoMap(seq.getMetaInfoMap());
copy.setSourceURI(seq.getSourceURI());
copy.setUnhandledAttributes(seq.getUnhandledAttributes());
seq = copy;
}
}
// 3. Notify the visitors.
for (XmlSchemaVisitor visitor : visitors) {
if (all != null) {
visitor.onEnterAllGroup(all);
} else if (choice != null) {
visitor.onEnterChoiceGroup(choice);
} else if (seq != null) {
visitor.onEnterSequenceGroup(seq);
}
}
// 4. Walk the children.
if (all != null) {
children = new ArrayList<XmlSchemaParticle>(all.getItems().size());
for (XmlSchemaAllMember item : all.getItems()) {
if (item instanceof XmlSchemaGroup) {
children.add(((XmlSchemaGroup)item).getParticle());
} else if (item instanceof XmlSchemaParticle) {
children.add((XmlSchemaParticle)item);
} else {
throw new IllegalArgumentException(
"All child is not an XmlSchemaGroup or XmlSchemaParticle; "
+ "it is a " + item.getClass().getName());
}
}
} else if (choice != null) {
children = new ArrayList<XmlSchemaParticle>(choice.getItems().size());
for (XmlSchemaChoiceMember item : choice.getItems()) {
if (item instanceof XmlSchemaGroup) {
children.add(((XmlSchemaGroup)item).getParticle());
} else if (item instanceof XmlSchemaParticle) {
children.add((XmlSchemaParticle)item);
} else {
throw new IllegalArgumentException(
"Choice child is not an XmlSchemaGroup or XmlSchemaParticle; "
+ "it is a " + item.getClass().getName());
}
}
} else if (seq != null) {
children = new ArrayList<XmlSchemaParticle>(seq.getItems().size());
for (XmlSchemaSequenceMember item : seq.getItems()) {
if (item instanceof XmlSchemaGroup) {
children.add(((XmlSchemaGroup)item).getParticle());
} else if (item instanceof XmlSchemaParticle) {
children.add((XmlSchemaParticle)item);
} else {
throw new IllegalArgumentException(
"Sequence child is not an XmlSchemaGroup or XmlSchemaParticle; "
+ "it is a " + item.getClass().getName());
}
}
}
if (children == null) {
throw new IllegalStateException("Could not process group of type " + group.getClass().getName());
}
for (XmlSchemaParticle child : children) {
walk(child);
}
// 5. Notify the visitors we are exiting the group.
for (XmlSchemaVisitor visitor : visitors) {
if (all != null) {
visitor.onExitAllGroup(all);
} else if (choice != null) {
visitor.onExitChoiceGroup(choice);
} else if (seq != null) {
visitor.onExitSequenceGroup(seq);
}
}
}
/**
* If the provided {@link XmlSchemaElement} is a reference, track down the
* original and add the minimum and maximum occurrence fields. Otherwise,
* just return the provided <code>element</code>.
*
* @param element The element to get the definition of.
* @return The real {@link XmlSchemaElement}.
*/
private XmlSchemaElement getElement(XmlSchemaElement element, boolean isSubstitutionGroup) {
if (!element.isRef() && !isSubstitutionGroup) {
return element;
}
final QName elemQName = getElementQName(element);
XmlSchemaElement globalElem = null;
if (!element.isRef()) {
globalElem = element;
} else if (element.getRef().getTarget() != null) {
globalElem = element.getRef().getTarget();
} else {
globalElem = schemasByNamespace.getElementByName(elemQName);
}
/*
* An XML Schema element reference defines the id, minOccurs, and
* maxOccurs attributes, while the global element definition defines id
* and all other attributes. This combines the two together.
*/
String id = element.getId();
if (id == null) {
id = globalElem.getId();
}
XmlSchema schema = schemasByNamespace.getSchemaDefiningElement(elemQName);
if (schema == null) {
// TODO: is this correct? The previous code used whichever schema was stored in the schemasByNamespace HashMap
// for the element's namespace.
schema = element.getParent();
}
final XmlSchemaElement copy = new XmlSchemaElement(schema, false);
copy.setName(globalElem.getName());
copy.setAbstract(globalElem.isAbstract());
copy.setAnnotation(globalElem.getAnnotation());
copy.setBlock(globalElem.getBlock());
copy.setDefaultValue(globalElem.getDefaultValue());
copy.setFinal(globalElem.getFinal());
copy.setFixedValue(globalElem.getFixedValue());
copy.setForm(globalElem.getForm());
copy.setId(id);
copy.setLineNumber(element.getLineNumber());
copy.setLinePosition(element.getLinePosition());
copy.setMaxOccurs(element.getMaxOccurs());
copy.setMinOccurs(element.getMinOccurs());
copy.setMetaInfoMap(globalElem.getMetaInfoMap());
copy.setNillable(globalElem.isNillable());
copy.setType(globalElem.getSchemaType());
copy.setSchemaTypeName(globalElem.getSchemaTypeName());
copy.setSourceURI(globalElem.getSourceURI());
copy.setSubstitutionGroup(globalElem.getSubstitutionGroup());
copy.setUnhandledAttributes(globalElem.getUnhandledAttributes());
return copy;
}
private static QName getElementQName(XmlSchemaElement element) {
if (element.isRef()) {
return element.getRefBase().getTargetQName();
} else {
return element.getQName();
}
}
}