Test coverage report for ExpressionOperations.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.metrics;

import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;

import com.sdmetrics.math.ExpressionNode;
import com.sdmetrics.math.ExpressionParser;
import com.sdmetrics.math.HashMultiSet;
import com.sdmetrics.model.MetaModelElement;
import com.sdmetrics.model.ModelElement;

/**
 * Holds the expression operations for metric, set, and condition expressions.
 */
class ExpressionOperations {

	private final ScalarOperationCache scalarOperations;
	private final BooleanOperationCache booleanOperations;
	private final SetOperationCache setOperations;

	private final HashSet<String> functionNames;
	private final HashSet<String> highPrecedenceRelationNames;
	private final HashSet<String> lowPrecedenceRelationNames;

	/**
	 * Constructor.
	 */
	ExpressionOperations() {
		functionNames = new HashSet<>();
		highPrecedenceRelationNames = new HashSet<>();
		lowPrecedenceRelationNames = new HashSet<>();

		scalarOperations = new ScalarOperationCache();
		booleanOperations = new BooleanOperationCache();
		setOperations = new SetOperationCache();
	}

	/**
	 * Cache for scalar operations.
	 */
	class ScalarOperationCache extends ProcedureCache<ScalarOperation> {

		/**
		 * Creates a new cache and registers the standard scalar operations.
		 */
		ScalarOperationCache() {
			super("operation");
			addProcedure(this, "size", SizeFunction.class, functionNames);
			addProcedure(this, "flatsize", FlatSizeFunction.class,
					functionNames);
			addProcedure(this, "length", LengthFunction.class, functionNames);
			addProcedure(this, "tolowercase", ToLowerFunction.class,
					functionNames);
			addProcedure(this, "typeof", TypeOfFunction.class, functionNames);
			addProcedure(this, "qualifiedname", QualifiedNameFunction.class,
					functionNames);
			addProcedureClass(".", DotOperator.class);
			addProcedure(this, "upto", UpToOperator.class,
					lowPrecedenceRelationNames);
			addProcedure(this, "topmost", TopMostOperator.class,
					lowPrecedenceRelationNames);
			addProcedureClass("->", InOperator.class);
			addProcedureClass("+", PlusOperator.class);
			addProcedureClass("-", MinusOperator.class);
			addProcedureClass("*", MultiplicationOperator.class);
			addProcedureClass("/", DivisionOperator.class);
			addProcedureClass("^", ExponentiationOperator.class);
			addProcedure(this, "ln", LogarithmFunction.class, functionNames);
			addProcedure(this, "exp", ExponentiationFunction.class,
					functionNames);
			addProcedure(this, "sqrt", SquareRootFunction.class, functionNames);
			addProcedure(this, "abs", AbsoluteFunction.class, functionNames);
			addProcedure(this, "floor", FloorFunction.class, functionNames);
			addProcedure(this, "ceil", CeilingFunction.class, functionNames);
			addProcedure(this, "round", RoundFunction.class, functionNames);
			addProcedure(this, "parsenumber", ParseNumberFunction.class,
					functionNames);
		}

		@Override
		protected Class<? extends ScalarOperation> loadClass(String className)
				throws ClassNotFoundException {
			return Class.forName(className).asSubclass(ScalarOperation.class);
		}
	}

	/**
	 * Adds an operation and registers its name to be recognized by the
	 * expression parser.
	 */
	private <T extends AbstractProcedure> void addProcedure(
			ProcedureCache<T> cache, String name, Class<? extends T> cls,
			HashSet<String> type) {
		cache.addProcedureClass(name, cls);
		type.add(name);
	}

	/**
	 * Cache for Boolean operations.
	 */
	class BooleanOperationCache extends ProcedureCache<BooleanOperation> {

		/**
		 * Creates a new cache and registers the standard Boolean operations.
		 */
		BooleanOperationCache() {
			super("boolean operation");
			addProcedureClass("&", AndOperator.class);
			addProcedureClass("|", OrOperator.class);
			addProcedureClass("!", NotOperator.class);
			addProcedure(this, "isunique", IsUniqueFunction.class,
					functionNames);
			addProcedure(this, "startswithcapital",
					StartsWithCapitalFunction.class, functionNames);
			addProcedure(this, "startswithlowercase",
					StartsWithLowerCaseFunction.class, functionNames);
			addProcedure(this, "islowercase", IsLowerCaseFunction.class,
					functionNames);
			addProcedure(this, "instanceof", InstanceOfFunction.class,
					functionNames);
			addProcedureClass("->", InOperatorBool.class);
			addProcedure(this, "onlist", OnListOperator.class,
					highPrecedenceRelationNames);
			addProcedureClass("=", EqualsOperator.class);
			addProcedureClass("!=", NotEqualsOperator.class);
			addProcedureClass(">", GreaterThanOperator.class);
			addProcedureClass(">=", GreaterOrEqualOperator.class);
			addProcedureClass("<", LessThanOperator.class);
			addProcedureClass("<=", LessOrEqualOperator.class);
			addProcedure(this, "startswith", StartsWithOperator.class,
					highPrecedenceRelationNames);
			addProcedure(this, "endswith", EndsWithOperator.class,
					highPrecedenceRelationNames);
		}

		@Override
		protected Class<? extends BooleanOperation> loadClass(String className)
				throws ClassNotFoundException {
			return Class.forName(className).asSubclass(BooleanOperation.class);
		}

	}

	/**
	 * Cache for Boolean operations.
	 */
	class SetOperationCache extends ProcedureCache<SetOperation> {

		/**
		 * Creates a new cache and registers the standard Boolean operations.
		 */
		SetOperationCache() {
			super("set operation");
			addProcedureClass(".", DotOperatorSet.class);
			addProcedureClass("+", UnionOperation.class);
			addProcedureClass("-", DifferenceOperation.class);
			addProcedureClass("*", IntersectionOperation.class);
		}

		@Override
		protected Class<? extends SetOperation> loadClass(String className)
				throws ClassNotFoundException {
			return Class.forName(className).asSubclass(SetOperation.class);
		}
	}

	/**
	 * Provides access to the scalar operations.
	 * 
	 * @return The scalar operations.
	 */
	ProcedureCache<ScalarOperation> getScalarOperations() {
		return scalarOperations;
	}

	/**
	 * Provides access to the Boolean operations.
	 * 
	 * @return The Boolean operations.
	 */
	ProcedureCache<BooleanOperation> getBooleanOperations() {
		return booleanOperations;
	}

	/**
	 * Provides access to the Boolean operations.
	 * 
	 * @return The Boolean operations.
	 */
	ProcedureCache<SetOperation> getSetOperations() {
		return setOperations;
	}

	/**
	 * Registers the name of a custom function to be recognized by the
	 * expression parser.
	 * 
	 * @param name The name of the custom function
	 */
	void addCustomFunctionName(String name) {
		functionNames.add(name);
	}

	/**
	 * Creates a new expression parser that recognizes all the operations
	 * registered with this object.
	 * 
	 * @return Expression parser
	 */
	ExpressionParser getExpressionParser() {
		return new ExpressionParser(functionNames, highPrecedenceRelationNames,
				lowPrecedenceRelationNames);
	}

	// Scalar Operations

	/**
	 * Calculates the number of elements in a set.
	 */
	public static class SizeFunction extends ScalarOperation {

		@Override
		public Integer calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Collection<?> c = evalSetExpression(element, node.getLeftNode(),
					vars);
			return Integer.valueOf(c.size());
		}
	}

	/**
	 * Calculates the number of elements in a set, ignoring the cardinality of
	 * elements for multisets.
	 */
	public static class FlatSizeFunction extends ScalarOperation {

		@Override
		public Integer calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Collection<?> c = evalSetExpression(element, node.getLeftNode(),
					vars);
			if (MetricTools.isMultiSet(c)) {
				return Integer.valueOf(((HashMultiSet<?>) c).flatSetSize());
			}
			return Integer.valueOf(c.size());
		}
	}

	/**
	 * Calculates the length of a string.
	 */
	public static class LengthFunction extends ScalarOperation {

		@Override
		public Integer calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			return Integer.valueOf(leftValue.toString().length());
		}
	}

	/**
	 * Converts all of the characters in a string to lower case. The default
	 * locale applies.
	 */
	public static class ToLowerFunction extends ScalarOperation {

		@Override
		public String calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			return leftValue.toString().toLowerCase();
		}
	}

	/**
	 * Returns the type name of a model element as a string.
	 */
	public static class TypeOfFunction extends ScalarOperation {

		@Override
		public String calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			if (leftValue instanceof ModelElement) {
				ModelElement elem = (ModelElement) leftValue;
				return elem.getType().getName();
			}
			if (MetricTools.isEmptyElement(leftValue)) {
				return "";
			}
			throw new SDMetricsException(element, null, "Operator '"
					+ node.getValue()
					+ "' can only be applied to model elements.");
		}
	}

	/**
	 * Returns the fully qualified name of a model element as a string.
	 */
	public static class QualifiedNameFunction extends ScalarOperation {
		@Override
		public String calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			if (leftValue instanceof ModelElement) {
				ModelElement elem = (ModelElement) leftValue;
				return elem.getFullName();
			}
			if (MetricTools.isEmptyElement(leftValue)) {
				return "";
			}
			throw new SDMetricsException(element, null, "Operator '"
					+ node.getValue()
					+ "' can only be applied to model elements.");
		}
	}

	/**
	 * Processes the dot operator in metric expressions.
	 */
	public static class DotOperator extends ScalarOperation {
		@Override
		public Object calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			// Suppresses any errors and return empty string
			// if something cannot be evaluated as it should.
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			if (!(leftValue instanceof ModelElement)) {
				return "";
			}
			try {
				return evalExpression((ModelElement) leftValue,
						node.getRightNode(), vars);
			} catch (Exception ex) {
				return "";
			}
		}
	}

	/**
	 * Processes the "upto" operator.
	 */
	public static class UpToOperator extends ScalarOperation {
		@Override
		public Object calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			while (leftValue != null) {
				if (leftValue instanceof ModelElement) {
					ModelElement elem = (ModelElement) leftValue;
					if (evalBooleanExpression(elem, 
							node.getRightNode(), vars)) {
						return elem;
					}
					leftValue = evalExpression(elem, node.getLeftNode(), vars);
				} else {
					leftValue = null;
				}
			}
			return "";
		}
	}

	/**
	 * Processes the "topmost" operator.
	 */
	public static class TopMostOperator extends ScalarOperation {
		@Override
		public Object calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			Object result = "";
			while (leftValue != null) {
				if (leftValue instanceof ModelElement) {
					ModelElement elem = (ModelElement) leftValue;
					if (evalBooleanExpression(elem, 
							node.getRightNode(), vars)) {
						result = elem;
					}
					leftValue = evalExpression(elem, node.getLeftNode(), vars);
				} else {
					leftValue = null;
				}
			}
			return result;
		}
	}

	/**
	 * Determines the cardinality of an element in a set.
	 */
	public static class InOperator extends ScalarOperation {
		@Override
		public Object calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			ExpressionNode rightNode = node.getRightNode();
			Collection<?> c = evalSetExpression(element, rightNode, vars);
			return Integer.valueOf(MetricTools.elementCount(c, leftValue));
		}
	}

	/**
	 * Calculates the unary and binary plus operator for metric expressions.
	 * Performs numerical addition or string concatenation, depending on the
	 * types of the elements.
	 */
	public static class PlusOperator extends ScalarOperation {
		@Override
		public Object calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);

			// process unary operator
			if (node.getOperandCount() == 1) {
				return leftValue;
			}

			// String concatenation if left hand side is not a number
			if (!(leftValue instanceof Number)) {
				Object rightValue = evalExpression(element,
						node.getRightNode(), vars);
				if (rightValue instanceof Number) {
					return leftValue.toString()
							+ java.text.NumberFormat.getInstance().format(
									rightValue);
				}
				return leftValue.toString() + rightValue.toString();
			}

			// numerical addition
			float left = getFloatValue(leftValue, element);
			float right = getFloatValue(element, node.getRightNode(), vars);
			return Float.valueOf(left + right);
		}
	}

	/**
	 * Calculates the unary and binary minus operator for metric expressions.
	 */
	public static class MinusOperator extends ScalarOperation {

		@Override
		public Float calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			float left = getFloatValue(element, node.getLeftNode(), vars);
			if (node.getOperandCount() == 1) {
				return Float.valueOf(-left);
			}
			float right = getFloatValue(element, node.getRightNode(), vars);
			return Float.valueOf(left - right);
		}
	}

	/**
	 * Base class for binary metric operators that operate on numerical values
	 * only.
	 */
	abstract static class BinaryNumericalOperator extends ScalarOperation {
		@Override
		public final Float calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			float left = getFloatValue(element, node.getLeftNode(), vars);
			float right = getFloatValue(element, node.getRightNode(), vars);
			return Float.valueOf(calculateValue(left, right));
		}

		/**
		 * Calculates the numerical value of the operation.
		 * 
		 * @param left The value of the left hand side operand.
		 * @param right The value of the right hand side operand.
		 * @return Value of the operation.
		 */
		protected abstract float calculateValue(float left, float right);
	}

	/**
	 * Calculates the product of two numbers.
	 */
	public static class MultiplicationOperator extends BinaryNumericalOperator {
		@Override
		protected float calculateValue(float left, float right) {
			return left * right;
		}
	}

	/**
	 * Calculates the quotient of two numbers.
	 */
	public static class DivisionOperator extends BinaryNumericalOperator {
		@Override
		protected float calculateValue(float left, float right) {
			return left / right;
		}
	}

	/**
	 * Exponentiates two numbers.
	 */
	public static class ExponentiationOperator extends BinaryNumericalOperator {
		@Override
		protected float calculateValue(float left, float right) {
			return (float) java.lang.Math.pow(left, right);
		}
	}

	/**
	 * Base class for unary metric operators that operate on numerical values
	 * only.
	 */
	abstract static class UnaryNumericalOperator extends ScalarOperation {
		@Override
		public final Float calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			float left = getFloatValue(element, node.getLeftNode(), vars);
			return Float.valueOf(calculateValue(left));
		}

		/**
		 * Calculates the numerical value of the operation.
		 * 
		 * @param operand The value of the single operand.
		 * @return Value of the operation.
		 */
		protected abstract float calculateValue(float operand);
	}

	/**
	 * Calculates the natural logarithm (base <code>e</code> of a value.
	 */
	public static class LogarithmFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.log(left);
		}
	}

	/**
	 * Calculates the <code>e</code> raised to the power of a value.
	 */
	public static class ExponentiationFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.exp(left);
		}
	}

	/**
	 * Calculates the square root of a value.
	 */
	public static class SquareRootFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.sqrt(left);
		}
	}

	/**
	 * Calculates the absolute value of value.
	 */
	public static class AbsoluteFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return Math.abs(left);
		}
	}

	/**
	 * Rounds to the next lower integer (towards -infinity).
	 */
	public static class FloorFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.floor(left);
		}
	}

	/**
	 * Rounds to the next higher integer (towards +infinity).
	 */
	public static class CeilingFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.ceil(left);
		}
	}

	/**
	 * Rounds to the nearest integer ("half up").
	 */
	public static class RoundFunction extends UnaryNumericalOperator {
		@Override
		protected float calculateValue(float left) {
			return (float) Math.floor(left + 0.5);
		}
	}

	/**
	 * Returns the numerical value of a string containing a number. Assumes
	 * Java-style formatting for decimal numbers.
	 * 
	 * @since 2.31
	 */
	public static class ParseNumberFunction extends ScalarOperation {

		@Override
		public Number calculateValue(ModelElement element, ExpressionNode node,
				Variables vars) throws SDMetricsException {
			String str = evalExpression(element, node.getOperand(0), vars)
					.toString();
			try {
				float value = Float.parseFloat(str);
				return MetricTools.getNumber(value);
			} catch (NumberFormatException ex) {
				throw new SDMetricsException(element, null,
						"Could not parse as number: '" + str + "'");
			}
		}
	}

	// Boolean operations

	/**
	 * Calculates the "and" operator. Short-circuits if possible.
	 */
	public static class AndOperator extends BooleanOperation {
		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			if (evalBooleanExpression(element, node.getLeftNode(), vars)) {
				return evalBooleanExpression(element, node.getRightNode(), vars);
			}
			return false;
		}
	}

	/**
	 * Calculates the "or" operator. Short-circuits if possible.
	 */
	public static class OrOperator extends BooleanOperation {
		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			if (!evalBooleanExpression(element, node.getLeftNode(), vars)) {
				return evalBooleanExpression(element, node.getRightNode(), vars);
			}
			return true;
		}
	}

	/**
	 * Calculates the "not" operator.
	 */
	public static class NotOperator extends BooleanOperation {
		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			return !evalBooleanExpression(element, node.getLeftNode(), vars);
		}
	}

	/**
	 * Checks if all elements in a multiset have cardinality 1.
	 */
	public static class IsUniqueFunction extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Collection<?> c = evalSetExpression(element, node.getLeftNode(),
					vars);
			if (!(MetricTools.isMultiSet(c))) {
				return true;
			}

			return c.size() == ((HashMultiSet<?>) c).flatSetSize();
		}
	}

	/**
	 * Checks if a string starts with a capital (upper case) letter.
	 */
	public static class StartsWithCapitalFunction extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			String s = leftValue.toString();
			if (s.length() < 1) {
				return true;
			}
			return s.charAt(0) == Character.toUpperCase(s.charAt(0));
		}
	}

	/**
	 * Checks if a string starts with a lower case letter.
	 */
	public static class StartsWithLowerCaseFunction extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			String s = leftValue.toString();
			if (s.length() < 1) {
				return true;
			}
			return s.charAt(0) == Character.toLowerCase(s.charAt(0));
		}
	}

	/**
	 * Checks that a string does not contain any capital letters.
	 */
	public static class IsLowerCaseFunction extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			String s = leftValue.toString();
			return s.equals(s.toLowerCase());
		}
	}

	/**
	 * Checks that a model element has a particular type or one of its
	 * sub-types.
	 */
	public static class InstanceOfFunction extends BooleanOperation {
		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {

			// Candidate element is either "self" (if there is only one argument
			// to the function), or the first argument of the function
			ModelElement candidate = element;
			if (node.getOperandCount() == 2) {
				Object o = evalExpression(element, node.getOperand(0), vars);
				if (MetricTools.isEmptyElement(o)) {
					return false;
				}
				if (!(o instanceof ModelElement)) {
					throw new SDMetricsException(element, null,
							"First argument of binary function "
									+ node.getValue()
									+ " must yield a model element");
				}
				candidate = (ModelElement) o;
			}

			// Type to check is the last argument of the function
			Object base = evalExpression(element,
					node.getOperand(node.getOperandCount() - 1), vars);
			MetaModelElement baseType = null;
			if (base instanceof ModelElement) {
				baseType = ((ModelElement) base).getType();
			} else {
				baseType = getMetaModel().getType(base.toString());
			}

			if (baseType == null) {
				throw new SDMetricsException(element, null,
						"Unknown base type '" + base.toString() + "'");
			}

			// Check if model element is an instance of the base type
			return candidate.getType().specializes(baseType);
		}
	}

	/**
	 * Checks if a set contains a particular model element or value.
	 */
	public static class InOperatorBool extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			Collection<?> c = evalSetExpression(element, node.getRightNode(),
					vars);
			return c.contains(leftValue);
		}
	}

	/**
	 * Checks if a word list contains a particular word.
	 */
	public static class OnListOperator extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			ExpressionNode rhsNode = node.getRightNode();
			String wordListName;
			if (rhsNode.isIdentifier() || rhsNode.isStringConstant()) {
				wordListName = rhsNode.getValue();
			} else {
				wordListName = evalExpression(element, rhsNode, vars)
						.toString();
			}
			return getMetricsEngine().getMetricStore().isWordOnList(
					leftValue.toString(), wordListName);
		}
	}

	/**
	 * Base class for binary comparison operators. The operators perform
	 * numerical comparisons when both arguments are numbers, and string or
	 * object comparisons otherwise.
	 */
	abstract static class BooleanComparator extends BooleanOperation {
		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			Object rightValue = evalExpression(element, node.getRightNode(),
					vars);

			if (leftValue instanceof Number) {
				if (rightValue instanceof Number) {
					float lf = ((Number) leftValue).floatValue();
					float rf = ((Number) rightValue).floatValue();

					return compare(lf, rf);
				}
			}

			return compare(leftValue, rightValue);
		}

		/**
		 * Performs a numerical comparison.
		 * 
		 * @param left left hand side operand to compare
		 * @param right right hand side operand to compare
		 * @return result of the comparison
		 */
		protected abstract boolean compare(float left, float right);

		/**
		 * Performs an object or string comparison.
		 * 
		 * @param left left hand side operand to compare
		 * @param right right hand side operand to compare
		 * @return result of the comparison
		 */
		protected abstract boolean compare(Object left, Object right);

	}

	/**
	 * Compares for equality. Object identity is determined by the equals()
	 * method.
	 */
	public static class EqualsOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left == right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			return left.equals(right);
		}
	}

	/**
	 * Compares for non-equality. Object identity is determined by the equals()
	 * method.
	 */
	public static class NotEqualsOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left != right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			return !left.equals(right);
		}
	}

	/** Performs "greater than" comparisons for numerical or string values. */
	public static class GreaterThanOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left > right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			int comp = left.toString().compareTo(right.toString());
			return comp > 0;
		}
	}

	/** Performs "greater or equal" comparisons for numerical or string values. */
	public static class GreaterOrEqualOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left >= right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			int comp = left.toString().compareTo(right.toString());
			return comp >= 0;
		}
	}

	/** Performs "less than" comparisons for numerical or string values. */
	public static class LessThanOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left < right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			int comp = left.toString().compareTo(right.toString());
			return comp < 0;
		}
	}

	/** Performs "greater or equal" comparisons for numerical or string values. */
	public static class LessOrEqualOperator extends BooleanComparator {
		@Override
		protected boolean compare(float left, float right) {
			return left <= right;
		}

		@Override
		protected boolean compare(Object left, Object right) {
			int comp = left.toString().compareTo(right.toString());
			return comp <= 0;
		}
	}

	/**
	 * Checks if a string value starts with a specified prefix.
	 */
	public static class StartsWithOperator extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			Object rightValue = evalExpression(element, node.getRightNode(),
					vars);
			return leftValue.toString().startsWith(rightValue.toString());
		}
	}

	/**
	 * Checks if a string value ends with a specified prefix.
	 */
	public static class EndsWithOperator extends BooleanOperation {

		@Override
		public boolean calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			Object leftValue = evalExpression(element, node.getLeftNode(), vars);
			Object rightValue = evalExpression(element, node.getRightNode(),
					vars);
			return leftValue.toString().endsWith(rightValue.toString());
		}
	}

	// Set operations

	/**
	 * Calculates the dot operator for set expressions.
	 */
	public static class DotOperatorSet extends SetOperation {
		@Override
		public Collection<?> calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {
			// Suppresses any errors and returns empty set
			// if something cannot be evaluated as it should.
			Object lhsObj = evalExpression(element, node.getLeftNode(), vars);
			if (!(lhsObj instanceof ModelElement)) {
				return Collections.EMPTY_SET;
			}
			try {
				return evalSetExpression((ModelElement) lhsObj,
						node.getRightNode(), vars);
			} catch (Exception ex) {
				return Collections.EMPTY_SET;
			}
		}
	}

	/**
	 * Base class for binary set operations where both operands are sets.
	 */
	abstract static class BinarySetOperation extends SetOperation {
		@Override
		public Collection<?> calculateValue(ModelElement element,
				ExpressionNode node, Variables vars) throws SDMetricsException {

			// evaluate the left and right hand side operands
			Collection<?> leftValue = evalSetExpression(element,
					node.getLeftNode(), vars);
			Collection<?> rightValue = evalSetExpression(element,
					node.getRightNode(), vars);

			return calculateValue(leftValue, rightValue);
		}

		/**
		 * Calculates the numerical value of the set operation.
		 * 
		 * @param left The value of the left hand side operand.
		 * @param right The value of the right hand side operand.
		 * @return Result set for the operation.
		 */
		public abstract Collection<?> calculateValue(Collection<?> left,
				Collection<?> right);
	}

	/**
	 * Calculates the union of two sets. Result is a multiset if any of the
	 * operands is a multiset.
	 */
	public static class UnionOperation extends BinarySetOperation {
		@SuppressWarnings("unchecked")
		@Override
		public Collection<?> calculateValue(Collection<?> left,
				Collection<?> right) {
			boolean isMultiSet = MetricTools.isMultiSet(right)
					|| MetricTools.isMultiSet(left);
			@SuppressWarnings("rawtypes")
			Collection result = MetricTools.createHashSet(isMultiSet, left);
			result.addAll(right);
			return result;
		}
	}

	/**
	 * Calculates the difference of two sets. The result is of the same type as
	 * the base (left hand side) set.
	 */
	public static class DifferenceOperation extends BinarySetOperation {
		@Override
		public Collection<?> calculateValue(Collection<?> left,
				Collection<?> right) {
			Collection<?> result = MetricTools.createHashSet(
					MetricTools.isMultiSet(left), left);
			result.removeAll(right);
			return result;
		}
	}

	/**
	 * Calculates the intersection of two sets. Result is a multiset if both
	 * sets are multisets. Otherwise, the result is a regular set.
	 */
	public static class IntersectionOperation extends BinarySetOperation {
		@Override
		public Collection<?> calculateValue(Collection<?> left,
				Collection<?> right) {
			boolean isMultiSet = MetricTools.isMultiSet(right)
					&& MetricTools.isMultiSet(left);
			Collection<?> result = MetricTools.createHashSet(isMultiSet, left);
			result.retainAll(right);
			return result;
		}
	}
}