Test coverage report for XMIReader.java - www.sdmetrics.com
/*
* SDMetrics Open Core for UML design measurement
* Copyright (c) Juergen Wuest
* To contact the author, see <http://www.sdmetrics.com/Contact.html>.
*
* This file is part of the SDMetrics Open Core.
*
* SDMetrics Open Core is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
* SDMetrics Open Core is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with SDMetrics Open Core. If not, see <http://www.gnu.org/licenses/>.
*
*/
package com.sdmetrics.model;
import static com.sdmetrics.model.XMITrigger.TriggerType.ATTRVAL;
import static com.sdmetrics.model.XMITrigger.TriggerType.CATTRVAL;
import static com.sdmetrics.model.XMITrigger.TriggerType.CONSTANT;
import static com.sdmetrics.model.XMITrigger.TriggerType.CTEXT;
import static com.sdmetrics.model.XMITrigger.TriggerType.GCATTRVAL;
import static com.sdmetrics.model.XMITrigger.TriggerType.REFLIST;
import static com.sdmetrics.model.XMITrigger.TriggerType.XMI2ASSOC;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.StringTokenizer;
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import com.sdmetrics.util.SAXHandler;
/**
* Reads an XMI source file, processing it as specified by an XMI transformation
* file. The UML model elements retrieved from the file are stored in a
* {@link Model}.
* <p>
*
* In the XMI, design information for one model element is spread across a
* number of XML elements in the XML tree. Therefore, a DOM XML parser would be
* a reasonable choice to use for processing an XMI file. DOM parsers read the
* entire tree and store it in memory. However, XMI files can become large, and
* we usually only need a fraction of the information contained in an XMI file,
* so this is not economical.
* <p>
* Therefore, a deliberate choice was made to use a SAX XML parser. The SAX
* parser reads one XML element at the time, and we need to keep track of the
* context in which an XML element is embedded. This is done using a stack
* holding the model elements to process, and the state at which processing is
* for each model element on the stack.
*/
public class XMIReader extends SAXHandler {
/**
* Message handler for progress messages of the XMI parser. Reading a large
* XMI file can take some time (several tens of seconds). Therefore, the XMI
* reader continuously emits progress messages. You can use these messages
* to convey to the user that the application is still alive.
*/
public interface ProgressMessageHandler {
/**
* Report a progress message.
*
* @param msg Message to display.
*/
void reportXMIProgress(String msg);
}
/** The model to store the extracted model elements. */
private final Model model;
/** Object that knows the available XMI transformations. */
private final XMITransformations transformations;
/** Current nesting level in the XML tree. */
private int nestingLevel;
/** Stack of model element being processed. */
private XMIContextStack stack;
/** Running id for artificial XMI IDs that the reader creates. */
private int artificialIDCounter;
/**
* Temporarily stores links via the "linkbackattr" attribute. These links
* can be registered with the model elements only after the entire XMI file
* has been read and all model elements are known.
*/
private List<DeferredRelation> deferredRelationsList;
/** An entry on the deferred relations list. */
private static class DeferredRelation {
ModelElement sourceElement;
String targetXMIID;
String linkBackAttr;
}
/** Collects text between XML element tags for "ctext" trigger. */
private StringBuilder ctextBuffer = new StringBuilder();
/**
* Hash map holding all model elements. Key is the XMI ID, value the model
* element. Used to collect the model elements while parsing the XMI file,
* and cross-referencing them by their XMI ID.
*/
private HashMap<String, ModelElement> xmiIDMap;
/**
* Cache of string values used for attributes values of model elements. The
* attribute value strings in a UML model contain a fair amount of
* redundancy, XML parsers however will return distinct string objects for
* equal string. Therefore, we cache those string values during parsing and
* replace duplicates with the cached version.
*/
private HashMap<String, String> stringCache;
/** Count of the number of model elements extracted from the XMI file. */
private int elementsExtracted;
/** Object to report progress messages to during parsing. */
private ProgressMessageHandler messageHandler;
/**
* Constructor.
* @param trans The XMI transformations to use.
* @param model The model that will contain the extracted model elements.
*/
public XMIReader(XMITransformations trans, Model model) {
this.model = model;
this.transformations = trans;
}
/**
* Registers a message handler for the XMI reader to report progress to.
*
* @param handler The message handler.
*/
public void setProgressMessageHandler(ProgressMessageHandler handler) {
messageHandler = handler;
}
/**
* Gets the number of elements that have been extracted.
*
* @return number of elements extracted from the XMI file
*/
public int getNumberOfElements() {
return elementsExtracted;
}
// SAX Parser callbacks
/** Prepare parser for new XMI file. */
@Override
public void startDocument() {
stack = new XMIContextStack();
deferredRelationsList = new LinkedList<>();
xmiIDMap = new HashMap<>(2048);
stringCache = new HashMap<>(2048);
elementsExtracted = 0;
nestingLevel = 0;
artificialIDCounter = 0;
}
/**
* Processes the opening tag of an XML element in the XMI source file.
* Depending on the parser state, this either creates a new model element,
* or adds information from a child or grandchild node of the XMI tree to
* the current model element.
*
* @throws SAXException An error occurred during the evaluation of a
* conditional XMI transformation.
*/
@Override
public void startElement(String uri, String local, String raw,
Attributes attrs) throws SAXException {
nestingLevel++;
if (stack.isAcceptingNewElements()) {
// Check if this is the beginning of a new model element
if (checkAndProcessNewElement(raw, attrs)) {
return;
}
}
if (stack.isEmpty()) {
return;
} else if (nestingLevel == stack.getNestingLevel() + 1) {
processXMLFirstLevelChild(raw, attrs);
} else if (nestingLevel == stack.getNestingLevel() + 2) {
processXMLSecondLevelChild(attrs);
}
}
/**
* Processes text between XML element tags.
*/
@Override
public void characters(char[] ch, int start, int length) {
if (stack.activeTriggerTypeEquals(CTEXT)) {
ctextBuffer.append(ch, start, length);
}
}
/**
* Processes the closing tag of an XML element. If the end of the definition
* of the model element on the top of the stack has been reached, add this
* element to the {@link Model}.
*/
@Override
public void endElement(String uri, String local, String raw) {
// Check for end of XML tag relevant to "ctext" triggers
if (nestingLevel == stack.getNestingLevel() + 1) {
if (stack.activeTriggerTypeEquals(CTEXT)) {
// add the character data to the current model element
ModelElement element = stack.getModelElement();
setModelAttributeValue(element, stack.getActiveTrigger().name,
ctextBuffer.toString());
ctextBuffer.setLength(0);
}
// any currently active trigger is expired by now.
stack.setActiveTrigger(null);
}
// Check if a model element has completed and add that
// element to the model
if (nestingLevel == stack.getNestingLevel()) {
ModelElement element = stack.pop();
// if element is its own "owner" (Visio XMI exporter), delete its
// context info
String contextID = element
.getPlainAttribute(MetaModelElement.CONTEXT);
if (contextID.equals(element.getXMIID())) {
contextID = "";
}
// if element has no context info, insert id of owner (element below
// on the stack)
if (contextID.length() == 0) {
ModelElement parent = stack.getModelElement();
if (parent != null) {
contextID = parent.getXMIID();
}
setModelAttributeValue(element, MetaModelElement.CONTEXT,
contextID);
}
// now we can add the element to the model
addModelElement(element.getXMIID(), element);
}
nestingLevel--;
}
/**
* Performs the post-processing after the entire XMI file has been read.
* Process the elements on the deferredRelations list, and cross-references
* all model element links.
*/
@Override
public void endDocument() {
// Now that all model elements are known, we can process deferred
// relations
for (DeferredRelation defRel : deferredRelationsList) {
ModelElement target = xmiIDMap.get(defRel.targetXMIID);
if (target == null) {
continue; // referenced element does not exist
}
// set attribute of target element to point back to the source
if (target.getType().hasAttribute(defRel.linkBackAttr)) {
setModelAttributeValue(target, defRel.linkBackAttr,
defRel.sourceElement.getXMIID());
}
}
// The string cache and deferred relations list have served their
// purpose and can be gc'ed
stringCache = null;
deferredRelationsList = null;
// Process extension attributes
processProfileExtensions();
// Now we can do the cross-referencing of model elements
doCrossreferencing();
}
/**
* Checks if an XML element in the XMI file defines a new model element, and
* process it accordingly.
*
* @param xmlElement Name of the XML element.
* @param attrs The attributes of the XML element.
* @return <code>true</code> if a new model element was created, else
* <code>false</code>.
* @throws SAXException An error occurred evaluating the condition
* expression of a conditional XMI transformation.
*/
private boolean checkAndProcessNewElement(String xmlElement,
Attributes attrs) throws SAXException {
XMITransformation trans = transformations.getTransformation(xmlElement,
attrs);
if (trans == null) {
// XML element defines xmi:type or xsi:type attribute, e.g.:
// <someRelation xmi:id='12' xmi:type="uml:Property" ... />
// Use its value to find an XMI transformation for the element
int xmiTypeIndex = findAttributeIndex(attrs, "xmi:type", "xsi:type");
if (xmiTypeIndex >= 0) {
String xmiPattern = attrs.getValue(xmiTypeIndex);
trans = transformations.getTransformation(xmiPattern, attrs);
}
}
if (trans == null) {
// Check if current element has a "xmi2assoc" trigger where
// xmlElement==trigger.attr, for example:
// XMI Source: <ownedAttribute xmi:id='xmi.43' name='attr1'
// type='xmi.2001'/>
// Candidate Trigger: <trigger name="properties" type="xmi2assoc"
// attr="ownedAttribute" src="uml:Property"/>
XMITransformation currentTrans = stack.getXMITransformation();
if (currentTrans != null) {
for (XMITrigger trigger : currentTrans.getTriggerList()) {
if (trigger.type == XMI2ASSOC && trigger.src != null
&& trigger.attr.equals(xmlElement)) {
trans = transformations.getTransformation(trigger.src,
attrs);
break;
}
}
}
}
if (trans == null) {
return false;
}
if (trans.requiresXMIID()) {
// Test if XML element has an XMI id specified.
// If not, return false to ignore the element.
if (findAttributeIndex(attrs, "xmi.id", "xmi:id") < 0) {
return false;
}
} else {
// Ignore the XML element if it has an XMI idref specified and is not
// explicitly allowed to.
if (!trans.allowsXMIIDRef()
&& findAttributeIndex(attrs, "xmi.idref", "xmi:idref") >= 0) {
return false;
}
}
processNewElement(trans, xmlElement, attrs);
return true;
}
/**
* Finds the index of an XML attribute that can be specified using one of
* two alternative names.
*
* @param attrs SAX attributes of an XML element
* @param attr1 First name used for the attribute.
* @param attr2 Alternative name used for the attribute.
* @return Index of the attribute, or -1 if the attribute is not contained
* under either name
*/
private int findAttributeIndex(Attributes attrs, String attr1, String attr2) {
int idindex = attrs.getIndex(attr1);
return (idindex < 0) ? attrs.getIndex(attr2) : idindex;
}
/**
* Processes a new model element in the XMI file. Creates a new
* {@link ModelElement}, processes the XML elements, and pushes the new
* model element on the context stack.
*
* @param trans XMI transformations to apply for the model element.
* @param xmlElement The name of the XML element in the XMI file.
* @param attrs The XML attributes of the XML element.
*/
private void processNewElement(XMITransformation trans, String xmlElement,
Attributes attrs) {
ModelElement element = new ModelElement(trans.getType());
processAttrTriggers(element, trans, attrs);
// if element has no XMI ID yet, add one
String xmiID = element.getPlainAttribute(MetaModelElement.ID);
if (xmiID.length() == 0) {
xmiID = "SDMetricsID." + (artificialIDCounter++);
setModelAttributeValue(element, MetaModelElement.ID, xmiID);
}
processRelationTriggers(xmlElement, xmiID);
stack.push(element, trans, nestingLevel);
}
/**
* Processes triggers "attrval", "xmi2assoc" and "constant" for a new model
* element. These triggers can be determined from the attributes of the XML
* element that defines the model element.
*
* @param element The new model element.
* @param trans XMI transformation to use for the new model element.
* @param attrs XML attributes defining the model element.
*/
private void processAttrTriggers(ModelElement element,
XMITransformation trans, Attributes attrs) {
MetaModelElement type = trans.getType();
for (XMITrigger trigger : trans.getTriggerList()) {
if (trigger.type == ATTRVAL || trigger.type == XMI2ASSOC) {
// Retrieve model element attribute from an XML attribute in the
// XMI file. For example:
// XMI Source: <ownedMember xmi:id='xmi.42' visibility='public'
// classifier='xmi.43 xmi.44'/>
// Triggers: <trigger name="classifiers" type="xmi2assoc"
// attr="classifier" />
// <trigger name="visible" type="attrval" attr="visibility" />
String xmlAttrValue = attrs.getValue(trigger.attr);
if (xmlAttrValue != null) {
if (type.isSetAttribute(trigger.name)) {
// tokenize the XML attribute values and add each one
StringTokenizer t =
new StringTokenizer(xmlAttrValue);
while (t.hasMoreTokens()) {
String nextID = t.nextToken();
setModelElementAttribute(element, nextID, trigger);
}
} else {
setModelElementAttribute(element, xmlAttrValue, trigger);
}
}
} else if (trigger.type == CONSTANT) {
// Insert the constant defined in the XMI trigger as attribute
// value
setModelAttributeValue(element, trigger.name, trigger.attr);
}
}
}
/**
* Sets a cross-reference attribute value and registers the "linkbackattr"
* on the "deferred relations" list, if defined.
*
* @param sourceElement The source element of the cross-reference.
* @param targetXMIID XMI ID of the target element.
* @param trigger The trigger that produced the target element XMIID.
*/
private void setModelElementAttribute(ModelElement sourceElement,
String targetXMIID, XMITrigger trigger) {
setModelAttributeValue(sourceElement, trigger.name, targetXMIID);
if (trigger.linkback != null) {
DeferredRelation defRel = new DeferredRelation();
defRel.sourceElement = sourceElement;
defRel.targetXMIID = targetXMIID;
defRel.linkBackAttr = trigger.linkback;
deferredRelationsList.add(defRel);
}
}
/**
* Processes "cattrval", "gcattrval", and "xmi2assoc" triggers of the owner
* of a new model element.
*
* @param xmlElement Name of the XML element defining the new model element.
* @param xmiID XMI ID of the new model element.
*/
private void processRelationTriggers(String xmlElement, String xmiID) {
// Process any matching "cattrval" or "xmi2assoc" triggers of the
// current model element
if ((nestingLevel == stack.getNestingLevel() + 1) && !stack.isEmpty()) {
for (XMITrigger trigger : stack.getXMITransformation()
.getTriggerList()) {
if ((trigger.type == CATTRVAL && trigger.src.equals(xmlElement))
|| (trigger.type == XMI2ASSOC && trigger.attr
.equals(xmlElement))) {
// Add relation to the new element from the owner
// model element on the stack
setModelElementAttribute(stack.getModelElement(), xmiID,
trigger);
}
}
}
// process active "gcattr" trigger, if any, as long as they are not for
// the context attribute
if ((nestingLevel == stack.getNestingLevel() + 2)
&& stack.activeTriggerTypeEquals(GCATTRVAL)
&& !MetaModelElement.CONTEXT
.equals(stack.getActiveTrigger().name)) {
processGCATTRElement(xmiID);
}
}
/**
* Checks an XML child element of a model element for pertinent data.
*
* @param xmlElement The name of the XML child element to process.
* @param attrs The XML attributes of the XML child element.
*/
private void processXMLFirstLevelChild(String xmlElement, Attributes attrs) {
// Go through list of triggers for current model element, checking if
// one is defined for the current XMI Element.
for (XMITrigger trigger : stack.getXMITransformation().getTriggerList()) {
if ((trigger.type == CATTRVAL && trigger.src.equals(xmlElement))
|| (trigger.type == XMI2ASSOC && trigger.attr
.equals(xmlElement))) {
// Process cattr or xmi2assoc trigger, for example:
// XMI Source: <containedNode xmi:idref='xmi35'/>
// Trigger: <trigger name="nodes" type="cattrval"
// src="containedNode" attr="xmi:idref" />
// or: <trigger name="nodes" type="xmi2assoc"
// attr="containedNode" />
int attrIndex;
if (trigger.type == XMI2ASSOC) {
attrIndex = findAttributeIndex(attrs, "xmi:idref",
"xmi.idref");
} else {
attrIndex = attrs.getIndex(trigger.attr);
}
if (attrIndex >= 0) {
String attrValue = attrs.getValue(attrIndex);
setModelElementAttribute(stack.getModelElement(),
attrValue, trigger);
}
} else if ((trigger.type == GCATTRVAL || trigger.type == CTEXT
|| trigger.type == REFLIST) && trigger.src.equals(xmlElement)) {
// Set current trigger for processing subsequent character text
// or grandchild XML elements
stack.setActiveTrigger(trigger);
}
}
}
/**
* Checks an XML grandchild element of a model element for pertinent data.
*
* @param attrs The XML attributes of the XML grandchild element.
*/
private void processXMLSecondLevelChild(Attributes attrs) {
if (stack.activeTriggerTypeEquals(GCATTRVAL)) {
String attrValue = attrs.getValue(stack.getActiveTrigger().attr);
processGCATTRElement(attrValue);
} else if (stack.activeTriggerTypeEquals(REFLIST)) {
processReflistElement(attrs);
}
}
/**
* Completes the processing of the active "gcattrval" trigger.
*
* @param attrValue Attribute value extracted by the trigger.
*/
private void processGCATTRElement(String attrValue) {
// Example XMI Source:
// <UML:Partition' name='mySwimlane' xmi.id='xmi12'>
// <UML:Partition.contents>
// <UML:ModelElement xmi.idref='xmi35'/>
// Trigger: <trigger name="contents" type="gcattrval"
// src="UML:Partition.contents" attr="xmi.idref"/>
XMITrigger trigger = stack.getActiveTrigger();
MetaModelElement type = stack.getModelElement().getType();
// Insert attribute value of interest.
if (type.hasAttribute(trigger.name) && attrValue != null) {
setModelElementAttribute(stack.getModelElement(), attrValue,
trigger);
}
// Done processing. If attribute is single-valued, expire trigger.
if (!type.isSetAttribute(trigger.name)) {
stack.setActiveTrigger(null);
}
}
/**
* Processes an XML element that is a member of a "reflist". Creates a new
* model element that is owned by the current model element on the top of
* the stack.
* <p>
* Note: the reflist trigger is deprecated and only supported for backwards
* compatibility. New XMITransformation files should use the
* "[gc][c]relation" triggers.
*
* @param attrs Attributes of the reflist element.
* @return <code>true</code> if the XML element was processed successfully,
* <code>false</code> if the XML element was not suitable.
*/
private boolean processReflistElement(Attributes attrs) {
XMITrigger trigger = stack.getActiveTrigger();
String attrval = attrs.getValue(trigger.attr);
if (attrval == null) {
return false;
}
// Get parent of new model element from top of the stack.
ModelElement parent = stack.getModelElement();
String parid = parent.getPlainAttribute(MetaModelElement.ID);
// Create the new model element.
MetaModelElement nsetype = model.getMetaModel().getType(trigger.name);
ModelElement nse = new ModelElement(nsetype);
// Set the owner of the new model element.
setModelAttributeValue(nse, MetaModelElement.CONTEXT, parid);
// Set the cross-reference attribute to the referenced model element.
setModelAttributeValue(nse, trigger.name, attrval);
// New model element has no name.
setModelAttributeValue(nse, MetaModelElement.NAME, "");
// The XMI ID of the new model element is a concatenation of the
// referenced model element and the parent element.
setModelAttributeValue(nse, MetaModelElement.ID, parid + attrval);
// Add new element to the model and return with success.
addModelElement(nse.getPlainAttribute(MetaModelElement.ID), nse);
return true;
}
/**
* Sets the attribute value of a model element.
* <p>
* Replaces the string value with a cached copy if the value has been used
* before.
*
* @param element The model element
* @param attributeName Name of the attribute to set
* @param value Value for the attribute
*/
private void setModelAttributeValue(ModelElement element,
String attributeName, String value) {
String cachedValue = stringCache.get(value);
if (cachedValue == null && value != null) {
cachedValue = value;
stringCache.put(value, value);
}
element.setAttribute(attributeName, cachedValue);
}
/**
* Adds a new model element to the model.
*
* @param xmiID XMI ID of the model element.
* @param element The model element to add.
*/
private void addModelElement(String xmiID, ModelElement element) {
element.setRunningID(elementsExtracted);
xmiIDMap.put(xmiID, element);
model.addElement(element);
elementsExtracted++;
// issue a progress message every 1000 elements
if (messageHandler != null) {
if ((elementsExtracted % 1000) == 0) {
messageHandler
.reportXMIProgress("Reading UML model. Elements processed: "
+ elementsExtracted);
}
}
}
/**
* Processes the light and full extension references.
*/
private void processProfileExtensions() {
// Iterate over all element types with an extension reference
int extendingElementCount = 0;
List<MetaModelElement> extendingTypes = new LinkedList<>();
for (MetaModelElement type : model.getMetaModel()) {
String extRef = type.getExtensionReference();
if (extRef != null) {
extendingTypes.add(type);
extendingElementCount += model.getElements(type).size();
}
}
if (extendingElementCount == 0) {
return;
}
if (messageHandler != null) {
messageHandler.reportXMIProgress("Processing "
+ extendingElementCount + " model extensions");
}
// Iterate over all element types with an extension reference
for (MetaModelElement type : extendingTypes) {
// merge each extending element with its extended element,
// or assume the name and owner of the extended element
String extRef = type.getExtensionReference();
for (ModelElement extending : model.getElements(type)) {
String extendedID = extending.getPlainAttribute(extRef);
ModelElement extended = xmiIDMap.get(extendedID);
if (extended == null) {
continue;
}
if (type.specializes(extended.getType())) {
extending.merge(extended);
model.removeElement(extended);
xmiIDMap.put(extendedID, extending);
} else {
extending.setAttribute(MetaModelElement.NAME,
extended.getName());
extending.setAttribute(MetaModelElement.CONTEXT, extended
.getPlainAttribute(MetaModelElement.CONTEXT));
}
}
}
}
/**
* Replaces string-valued XMI IDs of cross-reference attribute values with
* references to the respective model element instances. Called after all
* model elements have been added.
*/
private void doCrossreferencing() {
if (messageHandler != null) {
messageHandler.reportXMIProgress("Crossreferencing "
+ elementsExtracted + " model elements");
}
for (MetaModelElement type : model.getMetaModel()) {
for (String attrName : type.getAttributeNames()) {
if (type.isRefAttribute(attrName)) {
crossReferenceAttribute(type, attrName);
}
}
}
// don't need the XMI id mapping anymore
xmiIDMap = null;
}
/**
* Performs the cross-referencing for a single attribute.
* @param type Type of the element with the cross-reference attribute.
* @param attrName Name of the cross-reference attribute.
*/
private void crossReferenceAttribute(MetaModelElement type, String attrName) {
boolean isMultivalued = type.isSetAttribute(attrName);
for (ModelElement element : model.getElements(type)) {
if (isMultivalued) {
Collection<?> oldStringReferences = element
.getSetAttribute(attrName);
if (oldStringReferences.isEmpty()) {
continue;
}
HashSet<ModelElement> newElementReferences = new HashSet<>(
oldStringReferences.size());
for (Object xmiID : oldStringReferences) {
ModelElement referencedElement = xmiIDMap.get(xmiID);
if (referencedElement != null) {
referencedElement.addRelation(attrName, element);
newElementReferences.add(referencedElement);
}
}
element.setSetAttribute(attrName, newElementReferences);
} else {
ModelElement referencedElement = xmiIDMap
.get(element.getPlainAttribute(attrName));
if (referencedElement != null) {
referencedElement.addRelation(attrName, element);
}
element.setRefAttribute(attrName, referencedElement);
}
}
}
}