Test coverage report for FilterAttributeProcessor.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.Iterator;
import java.util.NoSuchElementException;

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

/**
 * Processes the standard filter attributes for individual model elements or
 * entire sets. The filter attributes are the attributes "target",
 * "targetcondition", "element", "eltype", "condition", and "scope".
 */
public class FilterAttributeProcessor {
	// The names of the filter attributes
	private static final String ATTR_TARGET = "target";
	private static final String ATTR_TARGETCOND = "targetcondition";
	private static final String ATTR_ELEMENT = "element";
	private static final String ATTR_ELTYPE = "eltype";
	private static final String ATTR_CONDEXP = "condition";
	private static final String ATTR_SCOPE = "scope";

	// The admissible values for the 'scope' attribute
	private static final String IDEM = "idem";
	private static final String NOTIDEM = "notidem";
	private static final String CONTAINEDIN = "containedin";
	private static final String NOTCONTAINEDIN = "notcontainedin";
	private static final String SAME = "same";
	private static final String OTHER = "other";
	private static final String HIGHER = "higher";
	private static final String LOWER = "lower";
	private static final String SAMEORHIGHER = "sameorhigher";
	private static final String SAMEORLOWER = "sameorlower";
	private static final String NOTHIGHER = "nothigher";
	private static final String NOTLOWER = "notlower";
	private static final String SAMEBRANCH = "samebranch";
	private static final String NOTSAMEBRANCH = "notsamebranch";

	/** The metrics engine for all calculations. */
	private final MetricsEngine engine;
	/** Expression of the "target" attribute. */
	private final ExpressionNode targetExpr;
	/** Expression of the "targetCondition". */
	private final ExpressionNode targetConditionExpr;
	/** Expression of the "element" attribute. */
	private final ExpressionNode elementExpr;
	/** Expression of the "eltype" attribute. */
	private final ExpressionNode eltypeExprt;
	/** Expression of the "condition" attribute. */
	private final ExpressionNode conditionExpr;
	/** Value of the "scope" attribute. */
	private final String scope;
	/**
	 * The validity of the most recent element to which the filter attributes
	 * were applied.
	 */
	private boolean valid;

	/**
	 * Constructor.
	 * @param engine Metric engine to evaluate the filter expressions
	 * @param attributes metric or set calculation procedure definition with the
	 *        filter attributes to apply.
	 * @throws SDMetricsException The "scope" attribute is set but does not
	 *         contain a string.
	 */
	public FilterAttributeProcessor(MetricsEngine engine,
			ProcedureAttributes attributes) throws SDMetricsException {
		this.engine = engine;
		targetExpr = attributes.getExpression(ATTR_TARGET);
		targetConditionExpr = attributes.getExpression(ATTR_TARGETCOND);
		elementExpr = attributes.getExpression(ATTR_ELEMENT);
		eltypeExprt = attributes.getExpression(ATTR_ELTYPE);
		conditionExpr = attributes.getExpression(ATTR_CONDEXP);
		scope = attributes.getStringValue(ATTR_SCOPE);
	}

	/**
	 * Applies the filter attributes to a candidate element.
	 * <p>
	 * Returns the element produced by the "element" filter attribute, or
	 * <code>null</code> if the "element" filter attribute yields no model
	 * element. If the "element" filter attribute is not set, the candidate
	 * element itself is returned.
	 * <p>
	 * In either case, after the call to this method, method {@link #isValid()}
	 * indicates if the returned element satisfies the conditions of the other
	 * filter attributes.
	 * 
	 * @param principal The model element for which the metric or set is
	 *        calculated.
	 * @param candidate The candidate model element.
	 * @param vars Variables for the evaluation of the condition expressions
	 * @return The result from applying the "element" filter attribute, if
	 *         specified, otherwise the candidate model element.
	 * @throws SDMetricsException An error occurred evaluating one of the filter
	 *         attributes.
	 */
	public ModelElement applyFilters(ModelElement principal,
			ModelElement candidate, Variables vars) throws SDMetricsException {
		// process "target" attribute
		valid = checkType(candidate.getType(), targetExpr);

		// check "targetCondition"
		if (valid && targetConditionExpr != null) {
			valid = engine.evalBooleanExpression(candidate,
					targetConditionExpr, vars);
		}

		ModelElement result = candidate;
		// process "element" and "eltype" attributes
		if (valid && elementExpr != null) {
			result = engine.evalModelElementExpression(candidate, elementExpr,
					vars);
			if (result == null) {
				return null;
			}

			valid = checkType(result.getType(), eltypeExprt);
		}

		if (valid && conditionExpr != null) {
			valid = engine.evalBooleanExpression(result, conditionExpr, vars);
		}

		if (valid && scope != null) {
			valid = checkScope(principal, result);
		}

		return result;
	}

	/**
	 * Tests if the resulting model element from the most recent application of
	 * filter attributes fulfills all filter conditions.
	 * <p>
	 * Filters are applied by calling method {@link #applyFilters} or methods
	 * hasNext() and next() on the iterators produced by methods
	 * {@link #fullIteration} or {@link #validIteration(Collection, Variables)}.
	 * 
	 * @return <code>true</code> if the model element returned by the most
	 *         recent filter application fulfills all filter attribute
	 *         conditions.
	 */
	public boolean isValid() {
		return valid;
	}

	/**
	 * Evaluates element filter attributes "target" and "eltype".
	 * <p>
	 * These filter attributes contain an expression of the form
	 * [+]type_1[|[+]type_2|...|[+]type_n] that lists the name of all admissible
	 * element types. For element types prefixed by a "+", the type itself or
	 * any of its subtypes are admissible.
	 * 
	 * @param type metamodel element type to check
	 * @param typeTree Operator tree of the "target" or "eltype" expression.
	 * @return <code>true</code> if the typeTree is <code>null</code>
	 *         ("empty tree", admits all model element types), or the tree
	 *         contains the specified element type. <code>false</code> if the
	 *         typeTree is not empty and does not contain the specified type.
	 * @throws SDMetricsException typeTree contains an unknown model element
	 *         type
	 */
	private boolean checkType(MetaModelElement type, ExpressionNode typeTree)
			throws SDMetricsException {
		if (typeTree == null) {
			return true;
		}

		boolean isPlusOperator = typeTree.isOperation()
				&& "+".equals(typeTree.getValue())
				&& typeTree.getOperandCount() == 1;

		if (typeTree.isOperation() && !isPlusOperator) {
			if (checkType(type, typeTree.getLeftNode())) {
				return true;
			}
			if (typeTree.getRightNode() != null) {
				return checkType(type, typeTree.getRightNode());
			}
			return false;
		}

		ExpressionNode typeNameNode = typeTree;
		if (isPlusOperator) {
			typeNameNode = typeTree.getLeftNode();
		}

		MetaModelElement candidate = engine.getMetaModel().getType(
				typeNameNode.getValue());
		if (candidate == null) {
			throw new SDMetricsException(null, null,
					"Unknown model element type '" + typeNameNode.getValue()
							+ "'.");
		}

		if (isPlusOperator) {
			return type.specializes(candidate);
		}
		return type == candidate;
	}

	/**
	 * Evaluates filter attribute "scope".
	 * 
	 * @param principal The model element for which the set or metric is
	 *        calculated.
	 * @param candidate The model element whose scope to test.
	 * @return <code>true</code> if the scope condition is satisfied
	 * @throws SDMetricsException scope attribute value is invalid
	 */
	private boolean checkScope(ModelElement principal, ModelElement canidate)
			throws SDMetricsException {
		if (scope.equals(IDEM)) {
			return (principal == canidate);
		}
		if (scope.equals(NOTIDEM)) {
			return !(principal == canidate);
		}
		if (scope.equals(CONTAINEDIN)) {
			return contains(principal, canidate);
		}
		if (scope.equals(NOTCONTAINEDIN)) {
			return !contains(principal, canidate);
		}

		ModelElement parent1 = principal.getOwner();
		ModelElement parent2 = canidate.getOwner();

		boolean same = (parent1 == parent2);
		if (scope.equals(SAME)) {
			return same;
		}
		if (scope.equals(OTHER)) {
			return !same;
		}

		boolean lower = contains(parent1, parent2);
		if (scope.equals(LOWER)) {
			return lower;
		}
		if (scope.equals(NOTLOWER)) {
			return !lower;
		}
		if (scope.equals(SAMEORLOWER)) {
			return (same || lower);
		}

		boolean higher = contains(parent2, parent1);
		if (scope.equals(HIGHER)) {
			return higher;
		}
		if (scope.equals(NOTHIGHER)) {
			return !higher;
		}
		if (scope.equals(SAMEORHIGHER)) {
			return (same || higher);
		}

		boolean sameBranch = (same || lower || higher);
		if (scope.equals(SAMEBRANCH)) {
			return sameBranch;
		}
		if (scope.equals(NOTSAMEBRANCH)) {
			return !sameBranch;
		}

		throw new SDMetricsException(null, null, "Illegal scope criterion '"
				+ scope + "'.");
	}

	/**
	 * Checks if a model element directly or indirectly owns another model
	 * element.
	 * 
	 * @param containing The containing model element.
	 * @param contained The candidate contained model element.
	 * @return <code>true</code> if "containing" element directly or indirectly
	 *         owns the "contained" element.
	 */
	private boolean contains(ModelElement containing, ModelElement contained) {
		if (contained == null) {
			return false;
		}
		ModelElement context = contained.getOwner();
		while (context != null) {
			if (context == containing) {
				return true;
			}
			context = context.getOwner();
		}
		return false;
	}

	/**
	 * Applies element filters and filter attributes to an element set and
	 * returns an iteration over the resulting elements.
	 * <ul>
	 * <li>Elements in the input set that should be ignored according to element
	 * filter settings are immediately dismissed.
	 * <li>To each remaining element, the filter attributes are applied (see
	 * {@link #applyFilters}.
	 * <li>The resulting elements are returned as values of the iteration.
	 * <li>Method {@link #isValid()} indicates if the most recently returned
	 * element of the iteration fulfills the filter attribute conditions.
	 * </ul>
	 * 
	 * @param set Element set, typically the result of a "relation" or "relset"
	 *        attribute in a projection-like metric.
	 * @param vars Variables for the evaluation of expressions
	 * @return Iteration over the resulting elements
	 * @throws SDMetricsException Error evaluating the filter attributes
	 */
	public Iterable<ModelElement> fullIteration(
			final Collection<ModelElement> set, final Variables vars)
			throws SDMetricsException {

		return new Iterable<ModelElement>() {
			@Override
			public Iterator<ModelElement> iterator() {
				FilteringIterator result = new FilteringIterator(set, vars);
				result.returnValidsOnly = false;
				return result;
			}
		};
	}

	/**
	 * Applies element filters and filter attributes to an element set and
	 * returns an iteration over the valid elements.
	 * <ul>
	 * <li>Elements in the input set that should be ignored according to element
	 * filter settings are immediately dismissed.
	 * <li>To each remaining element, the filter attributes are applied (see
	 * {@link #applyFilters}.
	 * <li>If the resulting element also fulfills the filter attribute
	 * conditions, it is returned as values of the iteration. Otherwise, the
	 * element is dismissed.
	 * </ul>
	 * 
	 * @param set Element set, typically the result of a "relation" or "relset"
	 *        attribute in a projection-like metric.
	 * @param vars Variables for the evaluation of expressions
	 * @return Iteration over the resulting elements
	 * @throws SDMetricsException Error evaluating the filter attributes
	 */
	public Iterable<ModelElement> validIteration(
			final Collection<ModelElement> set, final Variables vars)
			throws SDMetricsException {

		return new Iterable<ModelElement>() {
			@Override
			public Iterator<ModelElement> iterator() {
				FilteringIterator result = new FilteringIterator(set, vars);
				result.returnValidsOnly = true;
				return result;
			}
		};
	}

	private class FilteringIterator implements Iterator<ModelElement> {
		private final Iterator<ModelElement> it;
		private final Variables variables;
		private final ModelElement principal;
		
		private boolean hasNext = false;
		private ModelElement next = null;
		private boolean nextKnown = false;
		boolean returnValidsOnly = false;

		/**
		 * Creates a new filtering iterator.
		 * 
		 * @param set Element set to iterate.
		 * @param vars Variables for the evaluation of filter expressions
		 */
		FilteringIterator(Collection<ModelElement> set, Variables vars) {
			this.variables = vars;
			principal = vars.getPrincipal();
			it = set.iterator();
		}

		/**
		 * Tests if the iteration has more elements.
		 * 
		 * @return <tt>true</tt> if the iterator has still more elements.
		 */
		@Override
		public boolean hasNext() {
			if (!nextKnown) {
				findNext();
			}
			return hasNext;
		}

		/**
		 * Returns the next element in the iteration.
		 * 
		 * @return Next element in the iteration.
		 */
		@Override
		public ModelElement next() {
			if (!nextKnown) {
				findNext();
			}

			if (!hasNext) {
				throw new NoSuchElementException();
			}

			nextKnown = false;
			return next;
		}

		private void findNext() throws SDMetricsException {
			nextKnown = true;
			while (it.hasNext()) {
				ModelElement candidate = it.next();
				// Dismiss the element if links to the element should be ignored
				// as per the element filter settings
				if (candidate.getLinksIgnored()) {
					continue;
				}

				// Apply the filter attributes and dismiss elements that do not
				// return a result
				candidate = applyFilters(principal, candidate, variables);
				if (candidate == null) {
					continue;
				}

				// Optionally dismiss elements that do not fulfill the filter
				// attribute conditions
				if (returnValidsOnly && !isValid()) {
					continue;
				}

				hasNext = true;
				next = candidate;
				return;
			}

			// end of the iteration has been reached
			hasNext = false;
			next = null;
		}

		@Override
		public void remove() {
			it.remove();
		}
	}
}