/* * Initial version copyright 2008 Lockheed Martin Corporation, except * as stated in the file entitled Licensing-Information. * * All modifications copyright 2009 Data Access Technologies, Inc. * * All modifications copyright 2009 Data Access Technologies, Inc. Licensed under the Academic Free License version 3.0 * (http://www.opensource.org/licenses/afl-3.0.php), except as stated * in the file entitled Licensing-Information. * * Contributors: * MDS - initial API and implementation * */ package org.modeldriven.fuml.xmi.validation; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import javax.xml.namespace.QName; import javax.xml.stream.events.Attribute; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.modeldriven.fuml.config.FumlConfiguration; import org.modeldriven.fuml.config.ImportAdapter; import org.modeldriven.fuml.config.NamespaceDomain; import org.modeldriven.fuml.config.ReferenceMappingType; import org.modeldriven.fuml.config.ValidationExemption; import org.modeldriven.fuml.config.ValidationExemptionType; import org.modeldriven.fuml.environment.Environment; import org.modeldriven.fuml.library.Library; import org.modeldriven.fuml.repository.Class_; import org.modeldriven.fuml.repository.Classifier; import org.modeldriven.fuml.repository.Property; import org.modeldriven.fuml.xmi.AbstractXmiNodeVisitor; import org.modeldriven.fuml.xmi.ModelSupport; import org.modeldriven.fuml.xmi.XmiExternalReferenceElement; import org.modeldriven.fuml.xmi.XmiInternalReferenceElement; import org.modeldriven.fuml.xmi.XmiNode; import org.modeldriven.fuml.xmi.XmiNodeVisitor; import org.modeldriven.fuml.xmi.XmiNodeVisitorStatus; import org.modeldriven.fuml.xmi.XmiReference; import org.modeldriven.fuml.xmi.XmiReferenceAttribute; import org.modeldriven.fuml.xmi.stream.StreamNode; /** * A visitor pattern implementation class that walks a given XmiNode hierarchy checking for * various error conditions, creating and gathering ValidationError instances into a collection * helpful for user feedback. This class can be initialized with an initial list of errors gathered * from another context, and errors may be added to it's collection ad-hoc. This class heavily * leverages the fuml.model package. For more information on runtime metadata, see the fuml.model * package documentation. * * @author Scott Cinnamond */ public class ValidationErrorCollector extends AbstractXmiNodeVisitor implements XmiNodeVisitor { private static Log log = LogFactory.getLog(ValidationErrorCollector.class); private List<ValidationError> errors = new ArrayList<ValidationError>(); private boolean validateExternalReferences = true; private List<ValidationEventListener> eventListeners; @SuppressWarnings("unused") private ValidationErrorCollector() { } public ValidationErrorCollector(XmiNode root) { this.xmiRoot = root; modelSupport = new ModelSupport(); } /** * Traverses the entire given graph ala. the visitor pattern, gathering any validation * errors, other than reference errors. References are added to * a collection to be validated subsequently, outside of the * visitor. */ public void validate() { if (eventListeners != null) for (ValidationEventListener listener : eventListeners) listener.validationStarted( new ValidationEvent(this)); this.xmiRoot.accept(this); this.validateReferences(); if (eventListeners != null) for (ValidationEventListener listener : eventListeners) listener.validationCompleted( new ValidationEvent(this)); } public void addEventListener(ValidationEventListener eventListener) { if (eventListeners == null) eventListeners = new ArrayList<ValidationEventListener>(); this.eventListeners.add(eventListener); } public void removeEventListener(ValidationEventListener eventListener) { if (eventListeners == null) return; this.eventListeners.remove(eventListener); } public void visit(XmiNode target, XmiNode sourceXmiNode, String sourceKey, XmiNodeVisitorStatus status, int level) { // The XMI root is just packaging, not something we want to assemble or validate. if (sourceXmiNode == null && "XMI".equals(target.getLocalName())) { if (log.isDebugEnabled()) log.debug("ignoring root XMI node"); return; } XmiNode source = sourceXmiNode; if (source != null && "XMI".equals(source.getLocalName())) { source = null; if (log.isDebugEnabled()) log.debug("ignoring source XMI node"); } if (log.isDebugEnabled()) if (source != null) log.debug("visit: " + target.getLocalName() + " \t\tsource: " + source.getLocalName()); else log.debug("visit: " + target.getLocalName()); StreamNode eventNode = (StreamNode)target; if (target.getXmiId() != null) { if (this.nodeMap.get(target.getXmiId()) == null) { this.nodeMap.put(target.getXmiId(), target); } else addError(ErrorCode.DUPLICATE_REFERENCE, ErrorSeverity.FATAL, eventNode); } Classifier classifier = findClassifier(target, source); if (classifier == null) { // See if this element or it's immediate parent has an import adapter // which means adapted elements can currently have only 2 levels otherwise // the results are undefined. Bad. ImportAdapter importAdapter = FumlConfiguration.getInstance().findImportAdapter( target.getLocalName()); if (importAdapter == null) importAdapter = FumlConfiguration.getInstance().findImportAdapter( target.getXmiType()); if (source != null) { if (importAdapter == null) importAdapter = FumlConfiguration.getInstance().findImportAdapter( source.getLocalName()); if (importAdapter == null) importAdapter = FumlConfiguration.getInstance().findImportAdapter( source.getXmiType()); } // if exists, assume the adapter will transform the // undefined element and any descendants if (importAdapter == null) addError(ErrorCode.UNDEFINED_CLASS, ErrorSeverity.FATAL, eventNode); return; // We're done w/this node. No way to examine further without a classifier. } if (log.isDebugEnabled()) log.debug("identified element '" + target.getLocalName() + "' as classifier, " + classifier.getName()); classifierMap.put(target, classifier); boolean hasAttributes = eventNode.hasAttributes(); if (isNotReferenceElement(eventNode, classifier, hasAttributes)) return; if (isInternalReferenceElement(eventNode, classifier, hasAttributes)) { references.add(new XmiInternalReferenceElement(eventNode, classifier)); return; } if (isExternalReferenceElement(eventNode, classifier, hasAttributes)) { references.add(new XmiExternalReferenceElement(eventNode, classifier)); return; } if (isAbstract(classifier)) { addError(ErrorCode.ABSTRACT_CLASS_INSTANTIATION, ErrorSeverity.FATAL, eventNode); return; } // validate target node as attribute of source if (source != null) { Class_ sourceClassifier = (Class_)classifierMap.get(source); if (sourceClassifier != null) { Property property = sourceClassifier.findProperty(target.getLocalName()); if (property == null) { NamespaceDomain domain = FumlConfiguration.getInstance().findNamespaceDomain(target.getNamespaceURI()); ValidationExemption exemption = null; if (domain != null) exemption = FumlConfiguration.getInstance().findValidationExemptionByProperty(ValidationExemptionType.UNDEFINED_PROPERTY, sourceClassifier, target.getLocalName(), target.getNamespaceURI(), domain); if (exemption == null) { addError(ErrorCode.UNDEFINED_PROPERTY, ErrorSeverity.FATAL, (StreamNode)source, target.getLocalName()); } else { if (log.isDebugEnabled()) log.debug("undefined property exemption found within domain '" + exemption.getDomain().toString() + "' for property '" + sourceClassifier.getName() + "." +target.getLocalName() + "' - ignoring error"); } } } // else source is an undefined class, and we handled that previously } if (classifier instanceof Class_) { if (eventNode.hasAttributes()) validateAttributes(eventNode, source, (Class_)classifier, eventNode.getAttributes()); validateAttributesAgainstModel(eventNode, source, (Class_)classifier); } } private void validateAttributes(StreamNode target, XmiNode source, Class_ classifier, Iterator<Attribute> attributes) { NamespaceDomain domain = null; // only lookup as needed // look at XML attributes while (attributes.hasNext()) { Attribute xmlAttrib = attributes.next(); QName name = xmlAttrib.getName(); String prefix = name.getPrefix(); if (prefix != null && prefix.length() > 0) continue; // not applicable to current element/association-end. if ("href".equals(name.getLocalPart())) continue; // FIXME: why is this "special" ? Property property = classifier.findProperty(name.getLocalPart()); if (property == null) { if (domain == null) domain = FumlConfiguration.getInstance().findNamespaceDomain(target.getNamespaceURI()); ValidationExemption exemption = FumlConfiguration.getInstance().findValidationExemptionByProperty(ValidationExemptionType.UNDEFINED_PROPERTY, classifier, name.getLocalPart(), target.getNamespaceURI(), domain); if (exemption == null) { addError(ErrorCode.UNDEFINED_PROPERTY, ErrorSeverity.FATAL, target, name.getLocalPart()); } else { if (log.isDebugEnabled()) log.debug("undefined property exemption found within domain '" + exemption.getDomain().toString() + "' for property '" + classifier.getName() + "." +name.getLocalPart() + "' - ignoring error"); } continue; } if (isReferenceAttribute(property)) { XmiReferenceAttribute reference = new XmiReferenceAttribute(target, xmlAttrib, classifier); this.references.add(reference); } // TODO: when this error is commented out, an erroneous 'invalid internal reference' validation // error was seen to be thrown during assembly. This seems to be a bug if (property.isDerived()) if (checkDerivedPropertyInstantiationError(target, source, classifier, property)) { addError(ErrorCode.DERIVED_PROPERTY_INSTANTIATION, ErrorSeverity.FATAL, target, property.getName()); } } } private boolean checkDerivedPropertyInstantiationError(StreamNode target, XmiNode source, Classifier classifier, Property property) { boolean error = false; String value = target.getAttributeValue(property.getName()); if (value != null && value.trim().length() > 0) { error = true; } else if (target.findChildByName(property.getName()) != null) error = true; return error; } private void validateAttributesAgainstModel(StreamNode target, XmiNode source, Class_ classifier) { NamespaceDomain domain = null; // only lookup as needed List<Property> properties = classifier.getNamedProperties(); for (Property prop : properties) { if (prop.isRequired()) { if (checkRequiredPropertyError(target, source, classifier, prop)) { if (domain == null) domain = FumlConfiguration.getInstance().findNamespaceDomain(target.getNamespaceURI()); ValidationExemption exemption = FumlConfiguration.getInstance().findValidationExemptionByProperty(ValidationExemptionType.REQUIRED_PROPERTY, classifier, prop.getName(), target.getNamespaceURI(), domain); if (exemption == null) { addError(ErrorCode.PROPERTY_REQUIRED, ErrorSeverity.FATAL, target, prop.getName()); } else { if (log.isDebugEnabled()) log.debug("required property exemption found within domain '" + exemption.getDomain().toString() + "' for property '" + classifier.getName() + "." + prop.getName() + "' - ignoring error"); } } } // other checks?? } } private boolean checkRequiredPropertyError(StreamNode target, XmiNode source, Classifier classifier, Property property) { boolean error = true; if (property.isDerived()) { error = false; return error; } String value = target.getAttributeValue(property.getName()); if (value != null && value.trim().length() > 0) { error = false; } else // or we have a default { String defaultValue = property.findPropertyDefault(); if (defaultValue != null) error = false; } if (target.findChildByName(property.getName()) != null) error = false; // check configured mappings if (error && FumlConfiguration.getInstance().hasReferenceMapping( classifier, property)) { ReferenceMappingType mappingType = FumlConfiguration.getInstance().getReferenceMappingType( classifier, property); if (mappingType == ReferenceMappingType.PARENT) { // All right if the value of this reference property can be derived // from it's "parent", does the parent have an XMI id to // supply the value?? if (source.getXmiId() != null && source.getXmiId().length() > 0) error = false; else log.warn("no parent XMI id found for, " + classifier.getName() + "." + property.getName()); } else log.warn("unrecognized mapping type, " + mappingType.value() + " ignoring mapping for, " + classifier.getName() + "." + property.getName()); } return error; } private void validateReferences() { NamespaceDomain domain = null; // only lookup as needed // validate references post traversal Iterator<XmiReference> iter = references.iterator(); while (iter.hasNext()) { XmiReference reference = iter.next(); Iterator<String> refIter = reference.getXmiIds(); while (refIter.hasNext()) { String id = refIter.next(); if (reference instanceof XmiExternalReferenceElement) { if (validateExternalReferences) { if (Library.getInstance().lookup(id) != null) { // happy } else if (id != null && id.startsWith("pathmap:")) { // FIXME: resolve these references inside/outside of lib(s) } else { StreamNode streamNode = (StreamNode)reference.getXmiNode(); if (domain == null) domain = FumlConfiguration.getInstance().findNamespaceDomain(streamNode.getNamespaceURI()); ValidationExemption exemption = FumlConfiguration.getInstance().findValidationExemptionByReference(ValidationExemptionType.EXTERNAL_REFERENCE, reference.getClassifier(), id, streamNode.getNamespaceURI(), domain); if (exemption == null) { addError(ErrorCode.INVALID_EXTERNAL_REFERENCE, ErrorSeverity.FATAL, reference, id); } } } } else // internal reference { if (nodeMap.get(id) != null) continue; // it's an internal reference relative to this validation frame if (Environment.getInstance().findElementById(id) != null) continue; // it's an already loaded internal reference addError(ErrorCode.INVALID_REFERENCE, ErrorSeverity.FATAL, reference, id); } } } } private void addError(ErrorCode code, ErrorSeverity severity, StreamNode eventNode) { if (log.isDebugEnabled()) log.debug("adding " + code.toString() + " error for element '" + eventNode.getLocalName() + "'"); ValidationError error = new ValidationError(eventNode, code, severity); errors.add(error); if (eventListeners != null) for (ValidationEventListener listener : eventListeners) listener.validationError( new ValidationErrorEvent(error, this)); } private void addError(ErrorCode code, ErrorSeverity severity, StreamNode eventNode, String name) { if (log.isDebugEnabled()) log.debug("adding " + code.toString() + " error for element '" + eventNode.getLocalName() + "'"); ValidationError error = new ValidationError(eventNode, name, code, severity); errors.add(error); if (eventListeners != null) for (ValidationEventListener listener : eventListeners) listener.validationError( new ValidationErrorEvent(error, this)); } private void addError(ErrorCode code, ErrorSeverity severity, XmiReference reference, String id) { if (log.isDebugEnabled()) log.debug("adding " + code.toString() + " error for element '" + id + "'"); ValidationError error = new ValidationError(reference, id, code, severity); errors.add(error); if (eventListeners != null) for (ValidationEventListener listener : eventListeners) listener.validationError( new ValidationErrorEvent(error, this)); } public List<ValidationError> getErrors() { return errors; } public int getErrorCount() { return errors.size(); } public List<ValidationError> getErrors(ErrorCode code) { List<ValidationError> results = new ArrayList<ValidationError>(); Iterator<ValidationError> iter = errors.iterator(); while (iter.hasNext()) { ValidationError error = iter.next(); if (error.getCode().ordinal() == code.ordinal()) results.add(error); } return results; } public int getErrorCount(ErrorCode code) { return getErrors(code).size(); } public List<ValidationError> getErrors(ErrorCategory category) { List<ValidationError> results = new ArrayList<ValidationError>(); Iterator<ValidationError> iter = errors.iterator(); while (iter.hasNext()) { ValidationError error = iter.next(); if (error.getCategory().ordinal() == category.ordinal()) results.add(error); } return results; } public int getErrorCount(ErrorCategory category) { return getErrors(category).size(); } public List<ValidationError> getErrors(ErrorSeverity severity) { List<ValidationError> results = new ArrayList<ValidationError>(); Iterator<ValidationError> iter = errors.iterator(); while (iter.hasNext()) { ValidationError error = iter.next(); if (error.getSeverity().ordinal() == severity.ordinal()) results.add(error); } return results; } public int getErrorCount(ErrorSeverity severity) { return getErrors(severity).size(); } public boolean isValidateExternalReferences() { return validateExternalReferences; } public void setValidateExternalReferences(boolean validateExternalReferences) { this.validateExternalReferences = validateExternalReferences; } }