Test coverage report for XMITransformations.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 java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

import com.sdmetrics.math.ExpressionNode;
import com.sdmetrics.math.ExpressionParser;
import com.sdmetrics.math.MappedCollectionsIterator;
import com.sdmetrics.util.SAXHandler;

/**
 * Container and XML parser for XMI transformations.
 */
public class XMITransformations {

	/** The name of the top level XML element in the XMI transformation file. */
	public static final String TL_ELEMENT = "xmitransformations";

	/**
	 * HashMap to store the XMI transformations. Maps the XMI element that
	 * triggers the transformation to a list of candidate XMI transformations
	 * for that XMI element.
	 */
	private final HashMap<String, ArrayList<XMITransformation>> transformations = new HashMap<>();
	/** Metamodel on which the XMI transformations are based. */
	private final MetaModel metaModel;

	/**
	 * Constructor.
	 * @param metaModel Metamodel on which the XMI transformations are based.
	 */
	public XMITransformations(MetaModel metaModel) {
		this.metaModel = metaModel;
	}

	/**
	 * Gets a SAX handler to parse an XMI transformation file and store the
	 * transformations with this object.
	 * 
	 * @return SAX handler to parse the XMI transformation file
	 */
	public DefaultHandler getSAXParserHandler() {
		return new XMITranformationsParser();
	}

	/**
	 * Returns the XMI transformation for a particular XMI element. If
	 * conditional XMI transformations exist for the element, and the element
	 * matches at least one of them, an arbitrary matching conditional condition
	 * is returned. If there are no matching conditional XMI transformations, an
	 * unconditional XMI transformation is returned, if one exists.
	 * 
	 * @param xmlElement The name of the XMI element.
	 * @param attrs The XML attributes of the element.
	 * @return The XMI transformation for the XMI element, or <code>null</code>
	 *         if no matching transformation was found.
	 * @throws SAXException An error occurred evaluating the condition of a
	 *         conditional transformation.
	 */
	XMITransformation getTransformation(String xmlElement, Attributes attrs)
			throws SAXException {
		ArrayList<XMITransformation> candidates = transformations
				.get(xmlElement);
		if (candidates == null) {
			return null;
		}

		// Find an XMI transformation with matching condition
		for (XMITransformation trans : candidates) {
			try {
				if (evalBooleanExpr(trans.getConditionExpression(), attrs)) {
					return trans;
				}
			} catch (Exception e) {
				throw new SAXException(
						"Error evaluating condition for XMI transformation \""
								+ trans.getXMIPattern() + "\" in line "
								+ trans.getLineNumber()
								+ " of the XMI transformation file: "
								+ e.getMessage(), e);
			}
		}
		return null;
	}

	/**
	 * Evaluates a condition expression for a conditional XMI transformation.
	 * 
	 * @param node Operator tree of the condition expression.
	 * @param attr Attributes of the candidate XML element.
	 * @return <code>true</code> if the attribute values of the XML element
	 *         fulfill the condition, else <code>false</code>.
	 * @throws IllegalArgumentException The condition expression could not be
	 *         evaluated.
	 */
	private boolean evalBooleanExpr(ExpressionNode node, Attributes attr) {
		if (node == null) {
			return true; // no expression is a match
		}
		String operator = node.getValue();
		if ("!".equals(operator)) {
			return !evalBooleanExpr(node.getLeftNode(), attr);
		}
		if ("&".equals(operator)) { // handle logical "and"
			if (evalBooleanExpr(node.getLeftNode(), attr)) {
				return evalBooleanExpr(node.getRightNode(), attr);
			}
			return false;
		}
		if ("|".equals(operator)) { // handle logical "or"
			if (!evalBooleanExpr(node.getLeftNode(), attr)) {
				return evalBooleanExpr(node.getRightNode(), attr);
			}
			return true;
		}

		// comparisons =, !=
		if ("=".equals(operator) || "!=".equals(operator)) {
			String lhs = evalExpr(node.getLeftNode(), attr);
			String rhs = evalExpr(node.getRightNode(), attr);
			if ("=".equals(operator)) {
				return lhs.equals(rhs);
			}
			return !lhs.equals(rhs);
		}

		throw new IllegalArgumentException("Illegal boolean operation: "
				+ operator);
	}

	/**
	 * Evaluates identifiers and constants of an XMI transformation condition.
	 * 
	 * @param node Operator tree node containing the identifier/constant.
	 * @param attr Attributes of the candidate XML element.
	 * @return Value of the identifier or constant.
	 */
	private String evalExpr(ExpressionNode node, Attributes attr) {

		if (node.isNumberConstant() || node.isStringConstant()) {
			return node.getValue();
		}

		// treat node as identifier, get value of the XML attribute of that name
		int attrIndex = attr.getIndex(node.getValue());
		if (attrIndex >= 0) {
			return attr.getValue(attrIndex);
		}

		// Attribute not set - return an empty string. This is designed to
		// ensure that comparisons attr='' or attr!='' can be used to test if an
		// attribute is set or not
		return "";
	}

	/**
	 * SAX handler to parse an XMI transformation file.
	 */
	class XMITranformationsParser extends SAXHandler {

		private static final String ELEM_TRANSFORMATION = "xmitransformation";
		private static final String ELEM_TRIGGER = "trigger";

		/**
		 * Expression parser for condition expressions of conditional
		 * transformations.
		 */
		private final ExpressionParser exprParser = new ExpressionParser();

		/** Value of the "requirexmiid" attribute of the top level element. */
		private boolean globalRequireID = true;

		/** The XMI transformation currently being read. */
		private XMITransformation currentTrans;

		/**
		 * Processes a new XMI transformation or trigger.
		 * 
		 * @throws SAXException Illegal XML elements or attributes were
		 *         specified.
		 */
		@Override
		public void startElement(String uri, String local, String raw,
				Attributes attrs) throws SAXException {
			if (TL_ELEMENT.equals(raw)) {
				checkVersion(attrs, null);
				// if not explicitly set to false, XMI IDs are required by
				// default
				globalRequireID = !("false".equals(attrs
						.getValue("requirexmiid")));
			} else if (ELEM_TRANSFORMATION.equals(raw)) {
				processTransformation(attrs);
			} else if (ELEM_TRIGGER.equals(raw)) {
				processTrigger(attrs);
			} else {
				throw buildSAXException("Unexpected XML element <" + raw + ">.");
			}
		}

		private void processTransformation(Attributes attrs)
				throws SAXException {

			if (currentTrans != null) {
				throw buildSAXException("XMI transformations must not be nested.");
			}

			// check model element attribute
			String typeName = attrs.getValue("modelelement");
			if (typeName == null) {
				throw buildSAXException("XMI transformation is missing the \"modelelement\" attribute.");
			}

			MetaModelElement type = metaModel.getType(typeName);
			if (type == null) {
				throw buildSAXException("Unknown metamodel element type \"" + typeName
						+ "\".");
			}

			// check recurse attribute
			boolean recurse = "true".equals(attrs.getValue("recurse"));

			// check requirexmiid attribute
			boolean requireID;
			String reqIDValue = attrs.getValue("requirexmiid");
			if (globalRequireID) {
				requireID = !"false".equals(reqIDValue);
			} else {
				// use default value "false"
				requireID = "true".equals(reqIDValue);
			}

			// check allowxmiidref attribute
			boolean allowIDRef = "true".equals(attrs.getValue("allowxmiidref"));
			
			// parse condition expression, if any.
			ExpressionNode expr = null;
			String condition = attrs.getValue("condition");
			if (condition != null) {
				expr = exprParser.parseExpression(condition);
				if (expr == null) {
					throw buildSAXException("Invalid condition expression \"" + condition
							+ "\": " + exprParser.getErrorInfo());
				}
			}

			currentTrans = new XMITransformation(type,
					attrs.getValue("xmipattern"), recurse, requireID, allowIDRef, expr);
			currentTrans.setLineNumber(locator.getLineNumber());
		}

		private void processTrigger(Attributes attrs) throws SAXException {
			if (currentTrans == null) {
				throw buildSAXException("Trigger definition outside of an XMI transformation definition.");
			}

			// check trigger name
			String name = attrs.getValue("name");
			if (name == null) {
				throw buildSAXException("Trigger is missing the \"name\" attribute.");
			}

			// check trigger type
			String typeName = attrs.getValue("type");
			if (typeName == null) {
				throw buildSAXException("Trigger is missing the \"type\" attribute.");
			}

			if (XMITrigger.TriggerType.REFLIST.toString().equals(typeName)) {
				if (metaModel.getType(name) == null) {
					throw buildSAXException("Unknown metamodel element type \"" + name
							+ "\" for reflist attribute \"attr\".");
				}
			} else {
				MetaModelElement type = currentTrans.getType();
				if (!type.hasAttribute(name)) {
					throw buildSAXException("Attribute \"attr\": Unknown metamodel attribute \""
							+ name
							+ "\" for elements of type \""
							+ type.getName() + "\".");
				}
			}

			// add the new trigger to the current XMITransformation
			try {
				XMITrigger trigger = new XMITrigger(name, typeName,
						attrs.getValue("src"), attrs.getValue("attr"),
						attrs.getValue("linkbackattr"));
				currentTrans.addTrigger(trigger);
			} catch (IllegalArgumentException ex) {
				throw buildSAXException(ex.getMessage());
			}
		}

		/**
		 * Adds each new transformation after it has completed parsing.
		 */
		@Override
		public void endElement(String uri, String local, String raw) {
			if (ELEM_TRANSFORMATION.equals(raw)) {
				// get list of XMI transformations for the current XMI pattern
				ArrayList<XMITransformation> transList = transformations
						.get(currentTrans.getXMIPattern());
				// new XMI pattern, create its lists first
				if (transList == null) {
					transList = new ArrayList<>();
					transformations
							.put(currentTrans.getXMIPattern(), transList);
				}

				// add conditional transformation at the beginning of the list,
				// unconditional transformations at the end (to be checked
				// last).
				if (currentTrans.getConditionExpression() == null) {
					transList.add(currentTrans);
				} else {
					transList.add(0, currentTrans);
				}
				currentTrans = null;
			}
		}

		/**
		 * Calculates trigger inheritance after all transformations have been
		 * read.
		 */
		@Override
		public void endDocument() {
			Set<XMITransformation> processedTransformations = new HashSet<>();
			Iterator<XMITransformation> it = getXMITransIterator();
			while (it.hasNext()) {
				insertInheritedTransformations(it.next(),
						processedTransformations);
			}
		}

		/**
		 * Recursively adds the triggers for inherited metamodel element
		 * attributes to an XMI transformation.
		 * 
		 * @param trans The XMI transformation to process.
		 * @param processedTransformations The set of transformations already
		 *        processed
		 */
		private void insertInheritedTransformations(XMITransformation trans,
				Set<XMITransformation> processedTransformations) {
			// make sure each XMI transformation is processed only once
			if (processedTransformations.contains(trans)) {
				return;
			}
			processedTransformations.add(trans);

			// Find the XMI transformation for the parent metamodel element type
			MetaModelElement parentType = trans.getType().getParent();
			if (parentType == null) {
				return;
			}
			XMITransformation parentTrans = findXMITransformation(parentType);
			if (parentTrans == null) {
				return;
			}

			// recursively calculate inherited triggers for parent first
			insertInheritedTransformations(parentTrans,
					processedTransformations);

			// Find all of the parent's triggers the child does not yet have
			List<XMITrigger> missingTriggers = new ArrayList<>();
			for (XMITrigger trigger : parentTrans.getTriggerList()) {
				if (!trans.hasTrigger(trigger.name)) {
					missingTriggers.add(trigger);
				}
			}

			// Add the missing triggers to the child XMITransformation
			for (XMITrigger trg : missingTriggers) {
				trans.addTrigger(trg);
			}
		}

		/**
		 * Finds an XMI transformation for a metamodel element type. If there
		 * are multiple XMI transformations for the type, the method arbitrarily
		 * chooses one to return.
		 * 
		 * @param type Element type of interest
		 * @return An XMI transformation for the given type, or
		 *         <code>null</code> if none was found.
		 */
		private XMITransformation findXMITransformation(MetaModelElement type) {
			Iterator<XMITransformation> it = getXMITransIterator();
			while (it.hasNext()) {
				XMITransformation trans = it.next();
				if (type == trans.getType()) {
					return trans;
				}
			}
			return null;
		}

		/**
		 * Returns an iterator that yields all XMITransformation objects read so
		 * far.
		 * 
		 * @return Iterator over all XMITransformation objects
		 */
		private Iterator<XMITransformation> getXMITransIterator() {
			return new MappedCollectionsIterator<>(
					transformations);
		}
	}
}