Test coverage report for MetricsEngine.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.HashMap;
import com.sdmetrics.math.ExpressionNode;
import com.sdmetrics.model.MetaModel;
import com.sdmetrics.model.Model;
import com.sdmetrics.model.ModelElement;
/**
* Calculates metrics and sets.
*
* The metrics engine offers methods to retrieve a metric value (
* {@link #getMetricValue getMetricValue()}) or set ({@link #getSet getSet()})
* for a particular model element.
* <p>
*
* The engine uses a "lazy calculation" strategy: if a set or metric for a model
* element is requested for the first time, the set or metric is calculated,
* stored in a cache, and returned to the caller. On subsequent requests for the
* same set or metric, the value is simply retrieved from the cache.
*/
public class MetricsEngine {
/** The definitions of the metrics and sets. */
private MetricStore metrics;
/** The model with the elements for which to calculate metrics. */
private Model model;
/** Cache to store all measurement values. */
private MetricValuesCache metricCache;
/** Cache to keep the metric calculation procedures for reuse. */
private MetricProcedureCache metricProcedures;
/** Cache to keep the set calculation procedures for reuse. */
private SetProcedureCache setProcedures;
/**
* Initializes a new metrics engine.
*
* @param metrics The definitions of the sets and metrics to calculate.
* @param model The model on which to operate. Must use the same metamodel
* as the metric definitions.
*/
public MetricsEngine(MetricStore metrics, Model model) {
if (metrics.getMetaModel() != model.getMetaModel()) {
throw new IllegalArgumentException(
"Metamodel of metrics and elements do no match.");
}
this.metrics = metrics;
this.model = model;
this.metricProcedures = metrics.getMetricProcedures();
this.setProcedures = metrics.getSetProcedures();
this.metricCache = new MetricValuesCache();
}
/**
* Retrieves the metamodel on which this metrics engine is based.
*
* @return Metamodel of this engine.
*/
public MetaModel getMetaModel() {
return metrics.getMetaModel();
}
/**
* Retrieves the metric definitions used by this metrics engine.
*
* @return Metric definitions of this engine.
*/
public MetricStore getMetricStore() {
return metrics;
}
/**
* Retrieves the model on which this metrics engine operates.
*
* @return Model of this engine.
*/
public Model getModel() {
return model;
}
/**
* Retrieves the value of a metric for a model element.
* <p>
* Returns the cached value if the metric has been calculated before for the
* element. Otherwise, the value is calculated and cached.
*
* @param element Model element to retrieve the metric value for.
* @param metric The metric to retrieve. Must be taken from the metric store
* of this engine.
* @return Value of the metric for the specified element.
* @throws SDMetricsException An error occurred during the metric
* calculation.
*/
public Object getMetricValue(ModelElement element, Metric metric)
throws SDMetricsException {
// Check if the metric value has already been calculated.
Object value = metricCache.getMetricValue(element, metric);
if (value != null) {
return value;
}
// preliminarily set the metric value to 0 in the cache, to preempt
// any infinite recursion.
metricCache.setMetricValue(element, metric, MetricTools.ZERO);
// calculate the metric value.
try {
// Obtain the procedure to calculate the metrics
String procedureName = metric.getProcedureName();
MetricProcedure procedure = metricProcedures
.getProcedure(procedureName);
procedure.setMetricsEngine(this);
// Perform the calculation and return the procedure for reuse
value = procedure.calculate(element, metric);
metricProcedures.returnProcedure(procedure);
} catch (SDMetricsException ex) {
ex.fillInPerpetrators(element, metric);
throw ex;
} catch (RuntimeException ex) {
// wrap exceptions in an SDMetricsException so we know
// what metric/element is to blame
throw new SDMetricsException(element, metric, ex);
}
// Cache and return the final metric value.
metricCache.setMetricValue(element, metric, value);
return value;
}
/**
* Retrieves the contents of a set for a model element.
* <p>
* Returns the cached set if the set has been calculated before for the
* element. Otherwise, the set is calculated and cached.
* <p>
* Sets typically contain either model elements, numbers (mix of instances
* of Integer and Float), or strings.
*
* @param element Model element to retrieve the set for.
* @param set The set to retrieve. Must be taken from the metric store of
* this engine.
* @return Contents of the set for the specified element.
* @throws SDMetricsException An error occurred during the metric
* calculation.
*/
public Collection<?> getSet(ModelElement element, Set set)
throws SDMetricsException {
// Check if the set has already been calculated.
Collection<?> result = metricCache.getSet(element, set);
if (result != null) {
return result;
}
// preliminarily set the contents to be the empty set to preempt
// any infinite recursion
metricCache.setSet(element, set, Collections.EMPTY_SET);
try {
result = computeSet(element, set);
} catch (SDMetricsException ex) {
ex.fillInPerpetrators(element, set);
throw ex;
} catch (RuntimeException ex) {
// wrap exceptions in an SDMetricsException so we know
// what set/element is to blame
throw new SDMetricsException(element, set, ex);
}
metricCache.setSet(element, set, result);
return result;
}
/**
* Calculates a set. Does not use the cache. The set definition need not be
* taken from the metric store of the engine.
*
* @param element Model element to calculate the set for.
* @param set Metric object with the set definition to calculate.
* @return The contents of the resulting set.
* @throws SDMetricsException if an error occurred during calculation of the
* set.
*/
Collection<?> computeSet(ModelElement element, Set set)
throws SDMetricsException {
// Obtain the procedure to calculate the set
String procedureName = set.getProcedureName();
SetProcedure procedure = setProcedures.getProcedure(procedureName);
// Perform the calculation, return the procedure for reuse
procedure.setMetricsEngine(this);
Collection<?> result = procedure.calculate(element, set);
setProcedures.returnProcedure(procedure);
return result;
}
/**
* Evaluates a metric expression. Metric expressions return a scalar value
* (a number, string, or model element).
*
* @param element The model element for which to calculate the metric
* expression.
* @param node Root node of the metric expression operator tree.
* @param vars Variables for the expression evaluation
* @return The result of the metric expression.
* @throws SDMetricsException An error occurred evaluating the expression.
*/
Object evalExpression(ModelElement element, ExpressionNode node,
Variables vars) throws SDMetricsException {
// Check the node type (operation, identifier, or constant), and act
// accordingly.
if (node.isOperation()) {
return evalOperationExpression(element, node, vars);
}
if (node.isNumberConstant()) {
return Float.valueOf(node.getValue());
}
if (node.isStringConstant()) {
return node.getValue();
}
if (node.isIdentifier()) {
return evalIdentifier(element, node.getValue(), vars);
}
throw new SDMetricsException(element, null,
"Unknown expression node type.");
}
/**
* Evaluate a metric expression that returns a model element.
*
* @param element The model element for which to calculate the metric
* expression.
* @param node Root node of the metric expression operator tree.
* @param vars Variables for the expression evaluation
* @return The resulting model element, or <code>null</code> if the
* expression did not produce a model element.
* @throws SDMetricsException An error occurred evaluating the metric
* expression.
*/
ModelElement evalModelElementExpression(ModelElement element,
ExpressionNode node, Variables vars) throws SDMetricsException {
Object obj = evalExpression(element, node, vars);
if (!(obj instanceof ModelElement)) {
return null;
}
return (ModelElement) obj;
}
/**
* Evaluates a condition expression. Condition expressions return a boolean
* value <code>true</code> or <code>false</code>.
*
* @param element The model element for which to calculate the condition
* expression.
* @param node Root node of the condition expression operator tree.
* @param vars Variables for the expression evaluation
* @return The result of the condition expression.
* @throws SDMetricsException An error occurred evaluating the expression.
*/
boolean evalBooleanExpression(ModelElement element, ExpressionNode node,
Variables vars) throws SDMetricsException {
String operator = node.getValue();
ProcedureCache<BooleanOperation> scops = metrics
.getExpressionOperations().getBooleanOperations();
BooleanOperation op = scops.getProcedure(operator);
op.setMetricsEngine(this);
boolean result = op.calculateValue(element, node, vars);
scops.returnProcedure(op);
return result;
}
/**
* Evaluates a scalar operator in a metric expression. Scalar operators
* return numbers, strings, or model elements.
*
* @param element The model element for which to calculate the operation.
* @param node Node with the operator.
* @param vars Variables for the expression evaluation
* @return The result of the operation.
* @throws SDMetricsException An error occurred evaluating the operation.
*/
private Object evalOperationExpression(ModelElement element,
ExpressionNode node, Variables vars) throws SDMetricsException {
String operator = node.getValue();
ProcedureCache<ScalarOperation> scops = metrics
.getExpressionOperations().getScalarOperations();
ScalarOperation op = scops.getProcedure(operator);
op.setMetricsEngine(this);
Object result = op.calculateValue(element, node, vars);
scops.returnProcedure(op);
return result;
}
/**
* Evaluates an identifier in a metric expression.
* <p>
* The identifier may refer to a metric, an attribute, or a variable
* (including the implicit variable _self). The type of identifier is
* checked in that order.
*
* @param element The model element for which to evaluate the identifier.
* @param identifier The identifier.
* @param vars Variables for the expression evaluation
* @return The value of the identifier.
* @throws SDMetricsException The identifier could not be resolved.
*/
private Object evalIdentifier(ModelElement element, String identifier,
Variables vars) throws SDMetricsException {
// check if identifier is a metric
Metric metric = metrics.getMetric(element.getType(), identifier);
if (metric != null) {
return getMetricValue(element, metric);
}
// check if identifier is a single-valued attribute
if (element.getType().hasAttribute(identifier)) {
if (!element.getType().isSetAttribute(identifier)) {
if (element.getType().isRefAttribute(identifier)) {
ModelElement ref = element.getRefAttribute(identifier);
if (ref == null) {
return ""; // no element referenced -> empty string
}
return ref;
}
// plain data attribute, return its value
return element.getPlainAttribute(identifier);
}
}
// _self returns the current model element; also accept the
// deprecated "self"
if ("_self".equals(identifier) || "self".equals(identifier)) {
return element;
}
// check variables in the value map
if (vars != null) {
if (vars.hasVariable(identifier)) {
return vars.getVariable(identifier);
}
}
throw new SDMetricsException(element, null,
"No metric or single-valued attribute '" + identifier
+ "' for elements of type '"
+ element.getType().getName() + "'.");
}
/**
* Evaluates a set expression. Set expressions return sets.
*
* @param element The model element for which to calculate the set
* expression.
* @param node Root node of the set expression operator tree.
* @param vars Variables for the expression evaluation
* @return The contents of the resulting set.
* @throws SDMetricsException An error occurred evaluating the set
* expression.
*/
Collection<?> evalSetExpression(ModelElement element, ExpressionNode node,
Variables vars) throws SDMetricsException {
// The root of a set expression must be an operator or identifier
// accordingly.
if (node.isIdentifier()) {
return evalSetIdentifier(element, node.getValue(), vars);
}
// Make sure we have a binary operator
if (!node.isOperation()) {
throw new SDMetricsException(element, null,
"Illegal set expression.");
}
// Process the operator
String operator = node.getValue();
ProcedureCache<SetOperation> scops = metrics.getExpressionOperations()
.getSetOperations();
SetOperation op = scops.getProcedure(operator);
op.setMetricsEngine(this);
Collection<?> result = op.calculateValue(element, node, vars);
scops.returnProcedure(op);
return result;
}
/**
* Evaluates an identifier in a set expression.
*
* @param element The model element for which to evaluate the set
* identifier.
* @param identifier The set identifier.
* @param vars Variables for the expression evaluation
* @return The the set specified by the identifier.
* @throws SDMetricsException The identifier could not be resolved.
*/
private Collection<?> evalSetIdentifier(ModelElement element,
String identifier, Variables vars) throws SDMetricsException {
// Check if there is a set of that name for the element
Set set = metrics.getSet(element.getType(), identifier);
if (set != null) {
return getSet(element, set);
}
// Check multi-valued attributes of the element
if (element.getType().hasAttribute(identifier)) {
if (element.getType().isSetAttribute(identifier)) {
return element.getSetAttribute(identifier);
}
}
// Check the variables
if (vars != null) {
Object o = vars.getVariable(identifier);
if (o != null) {
if (o instanceof Collection) {
return (Collection<?>) o;
}
throw new SDMetricsException(element, null, "Variable '"
+ identifier + "' is not a set.");
}
}
throw new SDMetricsException(element, null, "Unknown set identifier '"
+ identifier + "' for elements of type '"
+ element.getType().getName() + "'.");
}
/** Cache to store all measurement values. */
class MetricValuesCache {
/** Metric value cache. */
private final HashMap<ModelElement, Object[]> metricValues = new HashMap<>();
/** Set value cache. */
private final HashMap<ModelElement, Collection<?>[]> setValues = new HashMap<>();
/**
* Sets the value for a metric of an element.
*
* @param element the model element
* @param metric the metric
* @param value the value of the metric
*/
void setMetricValue(ModelElement element, Metric metric, Object value) {
Object[] values = metricValues.get(element);
if (values == null) {
values = new Object[metrics.getMetrics(element.getType())
.size()];
metricValues.put(element, values);
}
values[metric.getID()] = value;
}
/**
* Sets the value of a set for a model element.
*
* @param element the model element
* @param set the set
* @param value the value of the metric
*/
void setSet(ModelElement element, Set set, Collection<?> value) {
Collection<?>[] collections = setValues.get(element);
if (collections == null) {
collections = new Collection<?>[metrics.getSets(element.getType())
.size()];
setValues.put(element, collections);
}
collections[set.getID()] = value;
}
/**
* Retrieves the value of a metric for a model element.
*
* @param element the model element
* @param metric the metric
* @return Value of the metric, <code>null</code> if the value has not
* yet been cached
*/
Object getMetricValue(ModelElement element, Metric metric) {
Object[] result = metricValues.get(element);
if (result != null) {
return result[metric.getID()];
}
return null;
}
/**
* Retrieves the value of a set for a model element.
*
* @param element the model element
* @param set the set
* @return Value of the set, <code>null</code> if the value has not yet
* been cached
*/
Collection<?> getSet(ModelElement element, Set set) {
Collection<?>[] result = setValues.get(element);
if (result != null) {
return result[set.getID()];
}
return null;
}
}
}