Coverage Report - com.sdmetrics.model.XMITransformations - www.sdmetrics.com
 
Classes in this File Line Coverage Branch Coverage Complexity
XMITransformations
100%
49/49
100%
34/34
5,769
XMITransformations$XMITranformationsParser
99%
111/112
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 static final 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  5099
         private final HashMap<String, ArrayList<XMITransformation>> transformations = 
 54  159
                         new HashMap<String, ArrayList<XMITransformation>>();
 55  
         /** Metamodel on which the XMI transformations are based. */
 56  1665
         private final MetaModel metaModel;
 57  
 
 58  
         /**
 59  
          * Constructor.
 60  
          * @param metaModel Metamodel on which the XMI transformations are based.
 61  
          */
 62  159
         public XMITransformations(MetaModel metaModel) {
 63  159
                 this.metaModel = metaModel;
 64  159
         }
 65  
 
 66  
         /**
 67  
          * Gets a SAX handler to parse an XMI transformation file and store the
 68  
          * transformations with this object.
 69  
          * 
 70  
          * @return SAX handler to parse the XMI transformation file
 71  
          */
 72  
         public DefaultHandler getSAXParserHandler() {
 73  158
                 return new XMITranformationsParser();
 74  
         }
 75  
 
 76  
         /**
 77  
          * Returns the XMI transformation for a particular XMI element. If
 78  
          * conditional XMI transformations exist for the element, and the element
 79  
          * matches at least one of them, an arbitrary matching conditional condition
 80  
          * is returned. If there are no matching conditional XMI transformations, an
 81  
          * unconditional XMI transformation is returned, if one exists.
 82  
          * 
 83  
          * @param xmlElement The name of the XMI element.
 84  
          * @param attrs The XML attributes of the element.
 85  
          * @return The XMI transformation for the XMI element, or <code>null</code>
 86  
          *         if no matching transformation was found.
 87  
          * @throws SAXException An error occurred evaluating the condition of a
 88  
          *         conditional transformation.
 89  
          */
 90  
         XMITransformation getTransformation(String xmlElement, Attributes attrs)
 91  
                         throws SAXException {
 92  57866
                 ArrayList<XMITransformation> candidates = transformations
 93  28933
                                 .get(xmlElement);
 94  28933
                 if (candidates == null) {
 95  12575
                         return null;
 96  
                 }
 97  
 
 98  
                 // Find an XMI transformation with matching condition
 99  32723
                 for (XMITransformation trans : candidates) {
 100  
                         try {
 101  16364
                                 if (evalBooleanExpr(trans.getConditionExpression(), attrs)) {
 102  16356
                                         return trans;
 103  
                                 }
 104  1
                         } catch (Exception e) {
 105  2
                                 throw new SAXException(
 106  2
                                                 "Error evaluating condition for XMI transformation \""
 107  1
                                                                 + trans.getXMIPattern() + "\" in line "
 108  1
                                                                 + trans.getLineNumber()
 109  1
                                                                 + " of the XMI transformation file: "
 110  2
                                                                 + e.getMessage(), e);
 111  
                         }
 112  
                 }
 113  1
                 return null;
 114  
         }
 115  
 
 116  
         /**
 117  
          * Evaluates a condition expression for a conditional XMI transformation.
 118  
          * 
 119  
          * @param node Operator tree of the condition expression.
 120  
          * @param attr Attributes of the candidate XML element.
 121  
          * @return <code>true</code> if the attribute values of the XML element
 122  
          *         fulfill the condition, else <code>false</code>.
 123  
          * @throws IllegalArgumentException The condition expression could not be
 124  
          *         evaluated.
 125  
          */
 126  
         private boolean evalBooleanExpr(ExpressionNode node, Attributes attr) {
 127  16387
                 if (node == null) {
 128  16351
                         return true; // no expression is a match
 129  
                 }
 130  36
                 String operator = node.getValue();
 131  36
                 if ("!".equals(operator)) {
 132  4
                         return !evalBooleanExpr(node.getLeftNode(), attr);
 133  
                 }
 134  32
                 if ("&".equals(operator)) { // handle logical "and"
 135  5
                         if (evalBooleanExpr(node.getLeftNode(), attr)) {
 136  3
                                 return evalBooleanExpr(node.getRightNode(), attr);
 137  
                         }
 138  2
                         return false;
 139  
                 }
 140  27
                 if ("|".equals(operator)) { // handle logical "or"
 141  7
                         if (!evalBooleanExpr(node.getLeftNode(), attr)) {
 142  4
                                 return evalBooleanExpr(node.getRightNode(), attr);
 143  
                         }
 144  3
                         return true;
 145  
                 }
 146  
 
 147  
                 // comparisons =, !=
 148  20
                 if ("=".equals(operator) || "!=".equals(operator)) {
 149  19
                         String lhs = evalExpr(node.getLeftNode(), attr);
 150  19
                         String rhs = evalExpr(node.getRightNode(), attr);
 151  19
                         if ("=".equals(operator)) {
 152  17
                                 return lhs.equals(rhs);
 153  
                         }
 154  2
                         return !lhs.equals(rhs);
 155  
                 }
 156  
 
 157  2
                 throw new IllegalArgumentException("Illegal boolean operation: "
 158  1
                                 + operator);
 159  
         }
 160  
 
 161  
         /**
 162  
          * Evaluates identifiers and constants of an XMI transformation condition.
 163  
          * 
 164  
          * @param node Operator tree node containing the identifier/constant.
 165  
          * @param attr Attributes of the candidate XML element.
 166  
          * @return Value of the identifier or constant.
 167  
          */
 168  
         private String evalExpr(ExpressionNode node, Attributes attr) {
 169  
 
 170  38
                 if (node.isNumberConstant() || node.isStringConstant()) {
 171  20
                         return node.getValue();
 172  
                 }
 173  
 
 174  
                 // treat node as identifier, get value of the XML attribute of that name
 175  18
                 int attrIndex = attr.getIndex(node.getValue());
 176  18
                 if (attrIndex >= 0) {
 177  14
                         return attr.getValue(attrIndex);
 178  
                 }
 179  
 
 180  
                 // Attribute not set - return an empty string. This is designed to
 181  
                 // ensure that comparisons attr='' or attr!='' can be used to test if an
 182  
                 // attribute is set or not
 183  4
                 return "";
 184  
         }
 185  
 
 186  
         /**
 187  
          * SAX handler to parse an XMI transformation file.
 188  
          */
 189  158
         class XMITranformationsParser extends SAXHandler {
 190  
 
 191  
                 private static final String ELEM_TRANSFORMATION = "xmitransformation";
 192  
                 private static final String ELEM_TRIGGER = "trigger";
 193  
 
 194  
                 /**
 195  
                  * Expression parser for condition expressions of conditional
 196  
                  * transformations.
 197  
                  */
 198  158
                 private final ExpressionParser exprParser = new ExpressionParser();
 199  
 
 200  
                 /** Value of the "requirexmiid" attribute of the top level element. */
 201  158
                 private boolean globalRequireID = true;
 202  
 
 203  
                 /** The XMI transformation currently being read. */
 204  
                 private XMITransformation currentTrans;
 205  
 
 206  
                 /**
 207  
                  * Processes a new XMI transformation or trigger.
 208  
                  * 
 209  
                  * @throws SAXException Illegal XML elements or attributes were
 210  
                  *         specified.
 211  
                  */
 212  
                 @Override
 213  
                 public void startElement(String uri, String local, String raw,
 214  
                                 Attributes attrs) throws SAXException {
 215  5115
                         if (TL_ELEMENT.equals(raw)) {
 216  158
                                 checkVersion(attrs, null);
 217  
                                 // if not explicitly set to false, XMI IDs are required by
 218  
                                 // default
 219  471
                                 globalRequireID = !("false".equals(attrs
 220  314
                                                 .getValue("requirexmiid")));
 221  157
                         } else if (ELEM_TRANSFORMATION.equals(raw)) {
 222  1661
                                 processTransformation(attrs);
 223  1657
                         } else if (ELEM_TRIGGER.equals(raw)) {
 224  3295
                                 processTrigger(attrs);
 225  3289
                         } else {
 226  1
                                 reportError("Unexpected XML element <" + raw + ">.");
 227  
                         }
 228  5103
                 }
 229  
 
 230  
                 private void processTransformation(Attributes attrs)
 231  
                                 throws SAXException {
 232  
 
 233  1661
                         if (currentTrans != null) {
 234  1
                                 reportError("XMI transformations must not be nested.");
 235  
                         }
 236  
 
 237  
                         // check model element attribute
 238  1660
                         String typeName = attrs.getValue("modelelement");
 239  1660
                         if (typeName == null) {
 240  1
                                 reportError("XMI transformation is missing the \"modelelement\" attribute.");
 241  
                         }
 242  
 
 243  1659
                         MetaModelElement type = metaModel.getType(typeName);
 244  1659
                         if (type == null) {
 245  2
                                 reportError("Unknown metamodel element type \"" + typeName
 246  1
                                                 + "\".");
 247  
                         }
 248  
 
 249  
                         // check recurse attribute
 250  1658
                         boolean recurse = "true".equals(attrs.getValue("recurse"));
 251  
 
 252  
                         // check requirexmiid attribute
 253  
                         boolean requireID;
 254  1658
                         String reqIDValue = attrs.getValue("requirexmiid");
 255  1658
                         if (globalRequireID) {
 256  1640
                                 requireID = !"false".equals(reqIDValue);
 257  1640
                         } else {
 258  
                                 // use default value "false"
 259  18
                                 requireID = "true".equals(reqIDValue);
 260  
                         }
 261  
 
 262  
                         // check allowxmiidref attribute
 263  1658
                         boolean allowIDRef = "true".equals(attrs.getValue("allowxmiidref"));
 264  
                         
 265  
                         // parse condition expression, if any.
 266  1658
                         ExpressionNode expr = null;
 267  1658
                         String condition = attrs.getValue("condition");
 268  1658
                         if (condition != null) {
 269  17
                                 expr = exprParser.parseExpression(condition);
 270  17
                                 if (expr == null) {
 271  2
                                         reportError("Invalid condition expression \"" + condition
 272  1
                                                         + "\": " + exprParser.getErrorInfo());
 273  
                                 }
 274  
                         }
 275  
 
 276  3314
                         currentTrans = new XMITransformation(type,
 277  1657
                                         attrs.getValue("xmipattern"), recurse, requireID, allowIDRef, expr);
 278  1657
                         currentTrans.setLineNumber(locator.getLineNumber());
 279  1657
                 }
 280  
 
 281  
                 private void processTrigger(Attributes attrs) throws SAXException {
 282  3295
                         if (currentTrans == null) {
 283  1
                                 reportError("Trigger definition outside of an XMI transformation definition.");
 284  
                         }
 285  
 
 286  
                         // check trigger name
 287  3294
                         String name = attrs.getValue("name");
 288  3294
                         if (name == null) {
 289  1
                                 reportError("Trigger is missing the \"name\" attribute.");
 290  
                         }
 291  
 
 292  
                         // check trigger type
 293  3293
                         String typeName = attrs.getValue("type");
 294  3293
                         if (typeName == null) {
 295  1
                                 reportError("Trigger is missing the \"type\" attribute.");
 296  
                         }
 297  
 
 298  3292
                         if (XMITrigger.TriggerType.REFLIST.toString().equals(typeName)) {
 299  6
                                 if (metaModel.getType(name) == null) {
 300  2
                                         reportError("Unknown metamodel element type \"" + name
 301  1
                                                         + "\" for reflist attribute \"attr\".");
 302  
                                 }
 303  0
                         } else {
 304  3286
                                 MetaModelElement type = currentTrans.getType();
 305  3286
                                 if (!type.hasAttribute(name)) {
 306  2
                                         reportError("Attribute \"attr\": Unknown metamodel attribute \""
 307  1
                                                         + name
 308  1
                                                         + "\" for elements of type \""
 309  1
                                                         + type.getName() + "\".");
 310  
                                 }
 311  
                         }
 312  
 
 313  
                         // add the new trigger to the current XMITransformation
 314  
                         try {
 315  6580
                                 XMITrigger trigger = new XMITrigger(name, typeName,
 316  3290
                                                 attrs.getValue("src"), attrs.getValue("attr"),
 317  3290
                                                 attrs.getValue("linkbackattr"));
 318  3289
                                 currentTrans.addTrigger(trigger);
 319  3289
                         } catch (IllegalArgumentException ex) {
 320  1
                                 reportError(ex.getMessage());
 321  
                         }
 322  3289
                 }
 323  
 
 324  
                 /**
 325  
                  * Adds each new transformation after it has completed parsing.
 326  
                  */
 327  
                 @Override
 328  
                 public void endElement(String uri, String local, String raw) {
 329  5085
                         if (ELEM_TRANSFORMATION.equals(raw)) {
 330  
                                 // get list of XMI transformations for the current XMI pattern
 331  3300
                                 ArrayList<XMITransformation> transList = transformations
 332  1650
                                                 .get(currentTrans.getXMIPattern());
 333  
                                 // new XMI pattern, create its lists first
 334  1650
                                 if (transList == null) {
 335  1641
                                         transList = new ArrayList<XMITransformation>();
 336  1641
                                         transformations
 337  1641
                                                         .put(currentTrans.getXMIPattern(), transList);
 338  
                                 }
 339  
 
 340  
                                 // add conditional transformation at the beginning of the list,
 341  
                                 // unconditional transformations at the end (to be checked
 342  
                                 // last).
 343  1650
                                 if (currentTrans.getConditionExpression() == null) {
 344  1634
                                         transList.add(currentTrans);
 345  1634
                                 } else {
 346  16
                                         transList.add(0, currentTrans);
 347  
                                 }
 348  1650
                                 currentTrans = null;
 349  
                         }
 350  5085
                 }
 351  
 
 352  
                 /**
 353  
                  * Calculates trigger inheritance after all transformations have been
 354  
                  * read.
 355  
                  */
 356  
                 @Override
 357  
                 public void endDocument() {
 358  146
                         Set<XMITransformation> processedTransformations = new HashSet<XMITransformation>();
 359  146
                         Iterator<XMITransformation> it = getXMITransIterator();
 360  1941
                         while (it.hasNext()) {
 361  3298
                                 insertInheritedTransformations(it.next(),
 362  1649
                                                 processedTransformations);
 363  
                         }
 364  146
                 }
 365  
 
 366  
                 /**
 367  
                  * Recursively adds the triggers for inherited metamodel element
 368  
                  * attributes to an XMI transformation.
 369  
                  * 
 370  
                  * @param trans The XMI transformation to process.
 371  
                  * @param processedTransformations The set of transformations already
 372  
                  *        processed
 373  
                  */
 374  
                 private void insertInheritedTransformations(XMITransformation trans,
 375  
                                 Set<XMITransformation> processedTransformations) {
 376  
                         // make sure each XMI transformation is processed only once
 377  3148
                         if (processedTransformations.contains(trans)) {
 378  1499
                                 return;
 379  
                         }
 380  1649
                         processedTransformations.add(trans);
 381  
 
 382  
                         // Find the XMI transformation for the parent metamodel element type
 383  1649
                         MetaModelElement parentType = trans.getType().getParent();
 384  1649
                         if (parentType == null) {
 385  146
                                 return;
 386  
                         }
 387  1503
                         XMITransformation parentTrans = findXMITransformation(parentType);
 388  1503
                         if (parentTrans == null) {
 389  4
                                 return;
 390  
                         }
 391  
 
 392  
                         // recursively calculate inherited triggers for parent first
 393  2998
                         insertInheritedTransformations(parentTrans,
 394  1499
                                         processedTransformations);
 395  
 
 396  
                         // Find all of the parent's triggers the child does not yet have
 397  1499
                         List<XMITrigger> missingTriggers = new ArrayList<XMITrigger>();
 398  11943
                         for (XMITrigger trigger : parentTrans.getTriggerList()) {
 399  8945
                                 if (!trans.hasTrigger(trigger.name)) {
 400  8180
                                         missingTriggers.add(trigger);
 401  
                                 }
 402  
                         }
 403  
 
 404  
                         // Add the missing triggers to the child XMITransformation
 405  11178
                         for (XMITrigger trg : missingTriggers) {
 406  8180
                                 trans.addTrigger(trg);
 407  
                         }
 408  1499
                 }
 409  
 
 410  
                 /**
 411  
                  * Finds an XMI transformation for a metamodel element type. If there
 412  
                  * are multiple XMI transformations for the type, the method arbitrarily
 413  
                  * chooses one to return.
 414  
                  * 
 415  
                  * @param type Element type of interest
 416  
                  * @return An XMI transformation for the given type, or
 417  
                  *         <code>null</code> if none was found.
 418  
                  */
 419  
                 private XMITransformation findXMITransformation(MetaModelElement type) {
 420  1503
                         Iterator<XMITransformation> it = getXMITransIterator();
 421  17458
                         while (it.hasNext()) {
 422  15951
                                 XMITransformation trans = it.next();
 423  15951
                                 if (type == trans.getType()) {
 424  1499
                                         return trans;
 425  
                                 }
 426  
                         }
 427  4
                         return null;
 428  
                 }
 429  
 
 430  
                 /**
 431  
                  * Returns an iterator that yields all XMITransformation objects read so
 432  
                  * far.
 433  
                  * 
 434  
                  * @return Iterator over all XMITransformation objects
 435  
                  */
 436  
                 private Iterator<XMITransformation> getXMITransIterator() {
 437  3298
                         return new MappedCollectionsIterator<XMITransformation>(
 438  1649
                                         transformations);
 439  
                 }
 440  
         }
 441  
 }