Coverage Report - com.sdmetrics.model.XMITransformations - www.sdmetrics.com
 
Classes in this File Line Coverage Branch Coverage Complexity
XMITransformations
100%
48/48
100%
34/34
5,769
XMITransformations$XMITranformationsParser
99%
108/109
100%
58/58
5,769
 
 1  
 /*
 2  
  * SDMetrics Open Core for UML design measurement
 3  
  * Copyright (c) Juergen Wuest
 4  
  * To contact the author, see <http://www.sdmetrics.com/Contact.html>.
 5  
  * 
 6  
  * This file is part of the SDMetrics Open Core.
 7  
  * 
 8  
  * SDMetrics Open Core is free software: you can redistribute it and/or modify
 9  
  * it under the terms of the GNU Affero General Public License as
 10  
  * published by the Free Software Foundation, either version 3 of the
 11  
  * License, or (at your option) any later version.
 12  
     
 13  
  * SDMetrics Open Core is distributed in the hope that it will be useful,
 14  
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
 15  
  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16  
  * GNU Affero General Public License for more details.
 17  
  *
 18  
  * You should have received a copy of the GNU Affero General Public License
 19  
  * along with SDMetrics Open Core.  If not, see <http://www.gnu.org/licenses/>.
 20  
  *
 21  
  */
 22  
 package com.sdmetrics.model;
 23  
 
 24  
 import java.util.ArrayList;
 25  
 import java.util.HashMap;
 26  
 import java.util.HashSet;
 27  
 import java.util.Iterator;
 28  
 import java.util.List;
 29  
 import java.util.Set;
 30  
 
 31  
 import org.xml.sax.Attributes;
 32  
 import org.xml.sax.SAXException;
 33  
 import org.xml.sax.helpers.DefaultHandler;
 34  
 
 35  
 import com.sdmetrics.math.ExpressionNode;
 36  
 import com.sdmetrics.math.ExpressionParser;
 37  
 import com.sdmetrics.math.MappedCollectionsIterator;
 38  
 import com.sdmetrics.util.SAXHandler;
 39  
 
 40  
 /**
 41  
  * Container and XML parser for XMI transformations.
 42  
  */
 43  
 public class XMITransformations {
 44  
 
 45  
         /** The name of the top level XML element in the XMI transformation file. */
 46  
         public final static String TL_ELEMENT = "xmitransformations";
 47  
 
 48  
         /**
 49  
          * HashMap to store the XMI transformations. Maps the XMI element that
 50  
          * triggers the transformation to a list of candidate XMI transformations
 51  
          * for that XMI element.
 52  
          */
 53  5035
         private final HashMap<String, ArrayList<XMITransformation>> transformations = new HashMap<String, ArrayList<XMITransformation>>();
 54  
         /** Metamodel on which the XMI transformations are based. */
 55  1645
         private final MetaModel metaModel;
 56  
 
 57  
         /**
 58  
          * @param metaModel Metamodel on which the XMI transformations are based.
 59  
          */
 60  155
         public XMITransformations(MetaModel metaModel) {
 61  155
                 this.metaModel = metaModel;
 62  155
         }
 63  
 
 64  
         /**
 65  
          * Gets a SAX handler to parse an XMI transformation file and store the
 66  
          * transformations with this object.
 67  
          * 
 68  
          * @return SAX handler to parse the XMI transformation file
 69  
          */
 70  
         public DefaultHandler getSAXParserHandler() {
 71  155
                 return new XMITranformationsParser();
 72  
         }
 73  
 
 74  
         /**
 75  
          * Returns the XMI transformation for a particular XMI element. If
 76  
          * conditional XMI transformations exist for the element, and the element
 77  
          * matches at least one of them, an arbitrary matching conditional condition
 78  
          * is returned. If there are no matching conditional XMI transformations, an
 79  
          * unconditional XMI transformation is returned, if one exists.
 80  
          * 
 81  
          * @param xmlElement The name of the XMI element.
 82  
          * @param attrs The XML attributes of the element.
 83  
          * @return The XMI transformation for the XMI element, or <code>null</code>
 84  
          *         if no matching transformation was found.
 85  
          * @throws SAXException An error occurred evaluating the condition of a
 86  
          *         conditional transformation.
 87  
          */
 88  
         XMITransformation getTransformation(String xmlElement, Attributes attrs)
 89  
                         throws SAXException {
 90  57718
                 ArrayList<XMITransformation> candidates = transformations
 91  28859
                                 .get(xmlElement);
 92  28859
                 if (candidates == null)
 93  12529
                         return null;
 94  
 
 95  
                 // Find an XMI transformation with matching condition
 96  32667
                 for (XMITransformation trans : candidates) {
 97  
                         try {
 98  16336
                                 if (evalBooleanExpr(trans.getConditionExpression(), attrs))
 99  16328
                                         return trans;
 100  1
                         } catch (Exception e) {
 101  2
                                 throw new SAXException(
 102  2
                                                 "Error evaluating condition for XMI transformation \""
 103  1
                                                                 + trans.getXMIPattern() + "\" in line "
 104  1
                                                                 + trans.getLineNumber()
 105  1
                                                                 + " of the XMI transformation file: "
 106  2
                                                                 + e.getMessage(), e);
 107  
                         }
 108  
                 }
 109  1
                 return null;
 110  
         }
 111  
 
 112  
         /**
 113  
          * Evaluates a condition expression for a conditional XMI transformation.
 114  
          * 
 115  
          * @param node Operator tree of the condition expression.
 116  
          * @param attr Attributes of the candidate XML element.
 117  
          * @return <code>true</code> if the attribute values of the XML element
 118  
          *         fulfill the condition, else <code>false</code>.
 119  
          * @throws IllegalArgumentException The condition expression could not be
 120  
          *         evaluated.
 121  
          */
 122  
         private boolean evalBooleanExpr(ExpressionNode node, Attributes attr) {
 123  16359
                 if (node == null)
 124  16325
                         return true; // no expression is a match
 125  34
                 String operator = node.getValue();
 126  34
                 if ("!".equals(operator)) // handle negation
 127  4
                         return !evalBooleanExpr(node.getLeftNode(), attr);
 128  30
                 if ("&".equals(operator)) { // handle logical "and"
 129  5
                         if (evalBooleanExpr(node.getLeftNode(), attr))
 130  3
                                 return evalBooleanExpr(node.getRightNode(), attr);
 131  2
                         return false;
 132  
                 }
 133  25
                 if ("|".equals(operator)) { // handle logical "or"
 134  7
                         if (!evalBooleanExpr(node.getLeftNode(), attr))
 135  4
                                 return evalBooleanExpr(node.getRightNode(), attr);
 136  3
                         return true;
 137  
                 }
 138  
 
 139  
                 // comparisons =, !=
 140  18
                 if ("=".equals(operator) || "!=".equals(operator)) {
 141  17
                         String lhs = evalExpr(node.getLeftNode(), attr);
 142  17
                         String rhs = evalExpr(node.getRightNode(), attr);
 143  17
                         if ("=".equals(operator))
 144  15
                                 return lhs.equals(rhs);
 145  2
                         return !lhs.equals(rhs);
 146  
                 }
 147  
 
 148  2
                 throw new IllegalArgumentException("Illegal boolean operation: "
 149  1
                                 + operator);
 150  
         }
 151  
 
 152  
         /**
 153  
          * Evaluates identifiers and constants of an XMI transformation condition.
 154  
          * 
 155  
          * @param node Operator tree node containing the identifier/constant.
 156  
          * @param attr Attributes of the candidate XML element.
 157  
          * @return Value of the identifier or constant.
 158  
          */
 159  
         private String evalExpr(ExpressionNode node, Attributes attr) {
 160  
 
 161  34
                 if (node.isNumberConstant() || node.isStringConstant())
 162  18
                         return node.getValue();
 163  
 
 164  
                 // treat node as identifier, get value of the XML attribute of that name
 165  16
                 int attrIndex = attr.getIndex(node.getValue());
 166  16
                 if (attrIndex >= 0)
 167  14
                         return attr.getValue(attrIndex);
 168  
 
 169  
                 // Attribute not set - return an empty string. This is designed to
 170  
                 // ensure that comparisons attr='' or attr!='' can be used to test if an
 171  
                 // attribute is set or not
 172  2
                 return "";
 173  
         }
 174  
 
 175  
         /**
 176  
          * SAX handler to parse an XMI transformation file.
 177  
          */
 178  155
         class XMITranformationsParser extends SAXHandler {
 179  
 
 180  
                 private final static String ELEM_TRANSFORMATION = "xmitransformation";
 181  
                 private final static String ELEM_TRIGGER = "trigger";
 182  
 
 183  
                 /**
 184  
                  * Expression parser for condition expressions of conditional
 185  
                  * transformations.
 186  
                  */
 187  155
                 private final ExpressionParser exprParser = new ExpressionParser();
 188  
 
 189  
                 /** Value of the "requirexmiid" attribute of the top level element. */
 190  155
                 private boolean globalRequireID = true;
 191  
 
 192  
                 /** The XMI transformation currently being read. */
 193  
                 private XMITransformation currentTrans;
 194  
 
 195  
                 /**
 196  
                  * Processes a new XMI transformation or trigger.
 197  
                  * 
 198  
                  * @throws SAXException Illegal XML elements or attributes were
 199  
                  *         specified.
 200  
                  */
 201  
                 @Override
 202  
                 public void startElement(String uri, String local, String raw,
 203  
                                 Attributes attrs) throws SAXException {
 204  5046
                         if (TL_ELEMENT.equals(raw)) {
 205  155
                                 checkVersion(attrs, null);
 206  
                                 // if not explicitly set to false, XMI IDs are required by
 207  
                                 // default
 208  462
                                 globalRequireID = !("false".equals(attrs
 209  308
                                                 .getValue("requirexmiid")));
 210  154
                         } else if (ELEM_TRANSFORMATION.equals(raw)) {
 211  1641
                                 processTransformation(attrs);
 212  1637
                         } else if (ELEM_TRIGGER.equals(raw)) {
 213  3249
                                 processTrigger(attrs);
 214  3243
                         } else {
 215  1
                                 reportError("Unexpected XML element <" + raw + ">.");
 216  
                         }
 217  5034
                 }
 218  
 
 219  
                 private void processTransformation(Attributes attrs)
 220  
                                 throws SAXException {
 221  
 
 222  1641
                         if (currentTrans != null)
 223  1
                                 reportError("XMI transformations must not be nested.");
 224  
 
 225  
                         // check model element attribute
 226  1640
                         String typeName = attrs.getValue("modelelement");
 227  1640
                         if (typeName == null)
 228  1
                                 reportError("XMI transformation is missing the \"modelelement\" attribute.");
 229  
 
 230  1639
                         MetaModelElement type = metaModel.getType(typeName);
 231  1639
                         if (type == null)
 232  2
                                 reportError("Unknown metamodel element type \"" + typeName
 233  1
                                                 + "\".");
 234  
 
 235  
                         // check recurse attribute
 236  1638
                         boolean recurse = "true".equals(attrs.getValue("recurse"));
 237  
 
 238  
                         // check requirexmiid attribute
 239  
                         boolean requireID;
 240  1638
                         String reqIDValue = attrs.getValue("requirexmiid");
 241  1638
                         if (globalRequireID) // use default value "true"
 242  1620
                                 requireID = !"false".equals(reqIDValue);
 243  
                         else
 244  
                                 // use default value "false"
 245  18
                                 requireID = "true".equals(reqIDValue);
 246  
 
 247  
                         // parse condition expression, if any.
 248  1638
                         ExpressionNode expr = null;
 249  1638
                         String condition = attrs.getValue("condition");
 250  1638
                         if (condition != null) {
 251  15
                                 expr = exprParser.parseExpression(condition);
 252  15
                                 if (expr == null)
 253  2
                                         reportError("Invalid condition expression \"" + condition
 254  1
                                                         + "\": " + exprParser.getErrorInfo());
 255  
                         }
 256  
 
 257  3274
                         currentTrans = new XMITransformation(type,
 258  1637
                                         attrs.getValue("xmipattern"), recurse, requireID, expr);
 259  1637
                         currentTrans.setLineNumber(locator.getLineNumber());
 260  1637
                 }
 261  
 
 262  
                 private void processTrigger(Attributes attrs) throws SAXException {
 263  3249
                         if (currentTrans == null)
 264  1
                                 reportError("Trigger definition outside of an XMI transformation definition.");
 265  
 
 266  
                         // check trigger name
 267  3248
                         String name = attrs.getValue("name");
 268  3248
                         if (name == null)
 269  1
                                 reportError("Trigger is missing the \"name\" attribute.");
 270  
 
 271  
                         // check trigger type
 272  3247
                         String typeName = attrs.getValue("type");
 273  3247
                         if (typeName == null)
 274  1
                                 reportError("Trigger is missing the \"type\" attribute.");
 275  
 
 276  3246
                         if (XMITrigger.TriggerType.REFLIST.toString().equals(typeName)) {
 277  6
                                 if (metaModel.getType(name) == null)
 278  2
                                         reportError("Unknown metamodel element type \"" + name
 279  1
                                                         + "\" for reflist attribute \"attr\".");
 280  0
                         } else {
 281  3240
                                 MetaModelElement type = currentTrans.getType();
 282  3240
                                 if (!type.hasAttribute(name))
 283  2
                                         reportError("Attribute \"attr\": Unknown metamodel attribute \""
 284  1
                                                         + name
 285  1
                                                         + "\" for elements of type \""
 286  1
                                                         + type.getName() + "\".");
 287  
                         }
 288  
 
 289  
                         // add the new trigger to the current XMITransformation
 290  
                         try {
 291  6488
                                 XMITrigger trigger = new XMITrigger(name, typeName,
 292  3244
                                                 attrs.getValue("src"), attrs.getValue("attr"),
 293  3244
                                                 attrs.getValue("linkbackattr"));
 294  3243
                                 currentTrans.addTrigger(trigger);
 295  3243
                         } catch (IllegalArgumentException ex) {
 296  1
                                 reportError(ex.getMessage());
 297  
                         }
 298  3243
                 }
 299  
 
 300  
                 /**
 301  
                  * Adds each new transformation after it has completed parsing.
 302  
                  */
 303  
                 @Override
 304  
                 public void endElement(String uri, String local, String raw) {
 305  5016
                         if (ELEM_TRANSFORMATION.equals(raw)) {
 306  
                                 // get list of XMI transformations for the current XMI pattern
 307  3260
                                 ArrayList<XMITransformation> transList = transformations
 308  1630
                                                 .get(currentTrans.getXMIPattern());
 309  
                                 // new XMI pattern, create its lists first
 310  1630
                                 if (transList == null) {
 311  1621
                                         transList = new ArrayList<XMITransformation>();
 312  1621
                                         transformations
 313  1621
                                                         .put(currentTrans.getXMIPattern(), transList);
 314  
                                 }
 315  
 
 316  
                                 // add conditional transformation at the beginning of the list,
 317  
                                 // unconditional transformations at the end (to be checked
 318  
                                 // last).
 319  1630
                                 if (currentTrans.getConditionExpression() == null)
 320  1616
                                         transList.add(currentTrans);
 321  
                                 else
 322  14
                                         transList.add(0, currentTrans);
 323  1630
                                 currentTrans = null;
 324  
                         }
 325  5016
                 }
 326  
 
 327  
                 /**
 328  
                  * Calculates trigger inheritance after all transformations have been
 329  
                  * read.
 330  
                  */
 331  
                 @Override
 332  
                 public void endDocument() {
 333  143
                         Set<XMITransformation> processedTransformations = new HashSet<XMITransformation>();
 334  143
                         Iterator<XMITransformation> it = getXMITransIterator();
 335  1915
                         while (it.hasNext())
 336  3258
                                 insertInheritedTransformations(it.next(),
 337  1629
                                                 processedTransformations);
 338  143
                 }
 339  
 
 340  
                 /**
 341  
                  * Recursively adds the triggers for inherited metamodel element
 342  
                  * attributes to an XMI transformation.
 343  
                  * 
 344  
                  * @param trans The XMI transformation to process.
 345  
                  * @param processedTransformations The set of transformations already
 346  
                  *        processed
 347  
                  */
 348  
                 private void insertInheritedTransformations(XMITransformation trans,
 349  
                                 Set<XMITransformation> processedTransformations) {
 350  
                         // make sure each XMI transformation is processed only once
 351  3111
                         if (processedTransformations.contains(trans))
 352  1482
                                 return;
 353  1629
                         processedTransformations.add(trans);
 354  
 
 355  
                         // Find the XMI transformation for the parent metamodel element type
 356  1629
                         MetaModelElement parentType = trans.getType().getParent();
 357  1629
                         if (parentType == null)
 358  143
                                 return;
 359  1486
                         XMITransformation parentTrans = findXMITransformation(parentType);
 360  1486
                         if (parentTrans == null)
 361  4
                                 return;
 362  
 
 363  
                         // recursively calculate inherited triggers for parent first
 364  2964
                         insertInheritedTransformations(parentTrans,
 365  1482
                                         processedTransformations);
 366  
 
 367  
                         // Find all of the parent's triggers the child does not yet have
 368  1482
                         List<XMITrigger> missingTriggers = new ArrayList<XMITrigger>();
 369  11825
                         for (XMITrigger trigger : parentTrans.getTriggerList())
 370  8861
                                 if (!trans.hasTrigger(trigger.name))
 371  8109
                                         missingTriggers.add(trigger);
 372  
 
 373  
                         // Add the missing triggers to the child XMITransformation
 374  11073
                         for (XMITrigger trg : missingTriggers)
 375  8109
                                 trans.addTrigger(trg);
 376  1482
                 }
 377  
 
 378  
                 /**
 379  
                  * Finds an XMI transformation for a metamodel element type. If there
 380  
                  * are multiple XMI transformations for the type, the method arbitrarily
 381  
                  * chooses one to return.
 382  
                  * 
 383  
                  * @param type Element type of interest
 384  
                  * @return An XMI transformation for the given type, or
 385  
                  *         <code>null</code> if none was found.
 386  
                  */
 387  
                 private XMITransformation findXMITransformation(MetaModelElement type) {
 388  1486
                         Iterator<XMITransformation> it = getXMITransIterator();
 389  17307
                         while (it.hasNext()) {
 390  15817
                                 XMITransformation trans = it.next();
 391  15817
                                 if (type == trans.getType())
 392  1482
                                         return trans;
 393  
                         }
 394  4
                         return null;
 395  
                 }
 396  
 
 397  
                 /**
 398  
                  * Returns an iterator that yields all XMITransformation objects read so
 399  
                  * far.
 400  
                  * 
 401  
                  * @return Iterator over all XMITransformation objects
 402  
                  */
 403  
                 private Iterator<XMITransformation> getXMITransIterator() {
 404  3258
                         return new MappedCollectionsIterator<XMITransformation>(
 405  1629
                                         transformations);
 406  
                 }
 407  
         }
 408  
 }