Coverage Report - com.sdmetrics.metrics.MetricsEngine - www.sdmetrics.com
 
Classes in this File Line Coverage Branch Coverage Complexity
MetricsEngine
98%
126/128
98%
49/50
4,444
MetricsEngine$MetricValuesCache
100%
27/27
100%
8/8
4,444
 
 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.metrics;
 23  
 
 24  
 import java.util.Collection;
 25  
 import java.util.Collections;
 26  
 import java.util.HashMap;
 27  
 
 28  
 import com.sdmetrics.math.ExpressionNode;
 29  
 import com.sdmetrics.model.MetaModel;
 30  
 import com.sdmetrics.model.Model;
 31  
 import com.sdmetrics.model.ModelElement;
 32  
 
 33  
 /**
 34  
  * Calculates metrics and sets.
 35  
  * 
 36  
  * The metrics engine offers methods to retrieve a metric value (
 37  
  * {@link #getMetricValue getMetricValue()}) or set ({@link #getSet getSet()})
 38  
  * for a particular model element.
 39  
  * <p>
 40  
  * 
 41  
  * The engine uses a "lazy calculation" strategy: if a set or metric for a model
 42  
  * element is requested for the first time, the set or metric is calculated,
 43  
  * stored in a cache, and returned to the caller. On subsequent requests for the
 44  
  * same set or metric, the value is simply retrieved from the cache.
 45  
  */
 46  
 
 47  
 public class MetricsEngine {
 48  
         /** The definitions of the metrics and sets. */
 49  318
         private MetricStore metrics;
 50  
         /** The model with the elements for which to calculate metrics. */
 51  
         private Model model;
 52  
         /** Cache to store all measurement values. */
 53  
         private MetricValuesCache metricCache;
 54  
         /** Cache to keep the metric calculation procedures for reuse. */
 55  
         private MetricProcedureCache metricProcedures;
 56  
         /** Cache to keep the set calculation procedures for reuse. */
 57  
         private SetProcedureCache setProcedures;
 58  
 
 59  
         /**
 60  
          * Initializes a new metrics engine.
 61  
          * 
 62  
          * @param metrics The definitions of the sets and metrics to calculate.
 63  
          * @param model The model on which to operate. Must use the same metamodel
 64  
          *        as the metric definitions.
 65  
          */
 66  119
         public MetricsEngine(MetricStore metrics, Model model) {
 67  119
                 if (metrics.getMetaModel() != model.getMetaModel()) {
 68  2
                         throw new IllegalArgumentException(
 69  1
                                         "Metamodel of metrics and elements do no match.");
 70  
                 }
 71  
 
 72  118
                 this.metrics = metrics;
 73  118
                 this.model = model;
 74  118
                 this.metricProcedures = metrics.getMetricProcedures();
 75  118
                 this.setProcedures = metrics.getSetProcedures();
 76  118
                 this.metricCache = new MetricValuesCache();
 77  118
         }
 78  
 
 79  
         /**
 80  
          * Retrieves the metamodel on which this metrics engine is based.
 81  
          * 
 82  
          * @return Metamodel of this engine.
 83  
          */
 84  
         public MetaModel getMetaModel() {
 85  1336
                 return metrics.getMetaModel();
 86  
         }
 87  
 
 88  
         /**
 89  
          * Retrieves the metric definitions used by this metrics engine.
 90  
          * 
 91  
          * @return Metric definitions of this engine.
 92  
          */
 93  
         public MetricStore getMetricStore() {
 94  136
                 return metrics;
 95  
         }
 96  
 
 97  
         /**
 98  
          * Retrieves the model on which this metrics engine operates.
 99  
          * 
 100  
          * @return Model of this engine.
 101  
          */
 102  
         public Model getModel() {
 103  9
                 return model;
 104  
         }
 105  
 
 106  
         /**
 107  
          * Retrieves the value of a metric for a model element.
 108  
          * <p>
 109  
          * Returns the cached value if the metric has been calculated before for the
 110  
          * element. Otherwise, the value is calculated and cached.
 111  
          * 
 112  
          * @param element Model element to retrieve the metric value for.
 113  
          * @param metric The metric to retrieve. Must be taken from the metric store
 114  
          *        of this engine.
 115  
          * @return Value of the metric for the specified element.
 116  
          * @throws SDMetricsException An error occurred during the metric
 117  
          *         calculation.
 118  
          */
 119  
         public Object getMetricValue(ModelElement element, Metric metric)
 120  
                         throws SDMetricsException {
 121  
                 // Check if the metric value has already been calculated.
 122  192
                 Object value = metricCache.getMetricValue(element, metric);
 123  192
                 if (value != null) {
 124  22
                         return value;
 125  
                 }
 126  
 
 127  
                 // preliminarily set the metric value to 0 in the cache, to preempt
 128  
                 // any infinite recursion.
 129  170
                 metricCache.setMetricValue(element, metric, MetricTools.ZERO);
 130  
 
 131  
                 // calculate the metric value.
 132  
                 try {
 133  
                         // Obtain the procedure to calculate the metrics
 134  170
                         String procedureName = metric.getProcedureName();
 135  340
                         MetricProcedure procedure = metricProcedures
 136  170
                                         .getProcedure(procedureName);
 137  170
                         procedure.setMetricsEngine(this);
 138  
 
 139  
                         // Perform the calculation and return the procedure for reuse
 140  170
                         value = procedure.calculate(element, metric);
 141  166
                         metricProcedures.returnProcedure(procedure);
 142  166
                 } catch (SDMetricsException ex) {
 143  3
                         ex.fillInPerpetrators(element, metric);
 144  3
                         throw ex;
 145  1
                 } catch (RuntimeException ex) {
 146  
                         // wrap exceptions in an SDMetricsException so we know
 147  
                         // what metric/element is to blame
 148  1
                         throw new SDMetricsException(element, metric, ex);
 149  
                 }
 150  
 
 151  
                 // Cache and return the final metric value.
 152  166
                 metricCache.setMetricValue(element, metric, value);
 153  166
                 return value;
 154  
         }
 155  
 
 156  
         /**
 157  
          * Retrieves the contents of a set for a model element.
 158  
          * <p>
 159  
          * Returns the cached set if the set has been calculated before for the
 160  
          * element. Otherwise, the set is calculated and cached.
 161  
          * <p>
 162  
          * Sets typically contain either model elements, numbers (mix of instances
 163  
          * of Integer and Float), or strings.
 164  
          * 
 165  
          * @param element Model element to retrieve the set for.
 166  
          * @param set The set to retrieve. Must be taken from the metric store of
 167  
          *        this engine.
 168  
          * @return Contents of the set for the specified element.
 169  
          * @throws SDMetricsException An error occurred during the metric
 170  
          *         calculation.
 171  
          */
 172  
         public Collection<?> getSet(ModelElement element, Set set)
 173  
                         throws SDMetricsException {
 174  
                 // Check if the set has already been calculated.
 175  200
                 Collection<?> result = metricCache.getSet(element, set);
 176  200
                 if (result != null) {
 177  7
                         return result;
 178  
                 }
 179  
 
 180  
                 // preliminarily set the contents to be the empty set to preempt
 181  
                 // any infinite recursion
 182  193
                 metricCache.setSet(element, set, Collections.EMPTY_SET);
 183  
 
 184  
                 try {
 185  193
                         result = computeSet(element, set);
 186  191
                 } catch (SDMetricsException ex) {
 187  1
                         ex.fillInPerpetrators(element, set);
 188  1
                         throw ex;
 189  1
                 } catch (RuntimeException ex) {
 190  
                         // wrap exceptions in an SDMetricsException so we know
 191  
                         // what set/element is to blame
 192  1
                         throw new SDMetricsException(element, set, ex);
 193  
                 }
 194  191
                 metricCache.setSet(element, set, result);
 195  191
                 return result;
 196  
         }
 197  
 
 198  
         /**
 199  
          * Calculates a set. Does not use the cache. The set definition need not be
 200  
          * taken from the metric store of the engine.
 201  
          * 
 202  
          * @param element Model element to calculate the set for.
 203  
          * @param set Metric object with the set definition to calculate.
 204  
          * @return The contents of the resulting set.
 205  
          * @throws SDMetricsException if an error occurred during calculation of the
 206  
          *         set.
 207  
          */
 208  
         Collection<?> computeSet(ModelElement element, Set set)
 209  
                         throws SDMetricsException {
 210  
                 // Obtain the procedure to calculate the set
 211  228
                 String procedureName = set.getProcedureName();
 212  228
                 SetProcedure procedure = setProcedures.getProcedure(procedureName);
 213  
                 // Perform the calculation, return the procedure for reuse
 214  228
                 procedure.setMetricsEngine(this);
 215  228
                 Collection<?> result = procedure.calculate(element, set);
 216  226
                 setProcedures.returnProcedure(procedure);
 217  226
                 return result;
 218  
         }
 219  
 
 220  
         /**
 221  
          * Evaluates a metric expression. Metric expressions return a scalar value
 222  
          * (a number, string, or model element).
 223  
          * 
 224  
          * @param element The model element for which to calculate the metric
 225  
          *        expression.
 226  
          * @param node Root node of the metric expression operator tree.
 227  
          * @param vars Variables for the expression evaluation
 228  
          * @return The result of the metric expression.
 229  
          * @throws SDMetricsException An error occurred evaluating the expression.
 230  
          */
 231  
         Object evalExpression(ModelElement element, ExpressionNode node,
 232  
                         Variables vars) throws SDMetricsException {
 233  
                 // Check the node type (operation, identifier, or constant), and act
 234  
                 // accordingly.
 235  3323
                 if (node.isOperation()) {
 236  1212
                         return evalOperationExpression(element, node, vars);
 237  
                 }
 238  
 
 239  2111
                 if (node.isNumberConstant()) {
 240  334
                         return Float.valueOf(node.getValue());
 241  
                 }
 242  
 
 243  1777
                 if (node.isStringConstant()) {
 244  139
                         return node.getValue();
 245  
                 }
 246  
 
 247  1638
                 if (node.isIdentifier()) {
 248  1638
                         return evalIdentifier(element, node.getValue(), vars);
 249  
                 }
 250  
 
 251  0
                 throw new SDMetricsException(element, null,
 252  0
                                 "Unknown expression node type.");
 253  
         }
 254  
 
 255  
         /**
 256  
          * Evaluate a metric expression that returns a model element.
 257  
          * 
 258  
          * @param element The model element for which to calculate the metric
 259  
          *        expression.
 260  
          * @param node Root node of the metric expression operator tree.
 261  
          * @param vars Variables for the expression evaluation
 262  
          * @return The resulting model element, or <code>null</code> if the
 263  
          *         expression did not produce a model element.
 264  
          * @throws SDMetricsException An error occurred evaluating the metric
 265  
          *         expression.
 266  
          */
 267  
         ModelElement evalModelElementExpression(ModelElement element,
 268  
                         ExpressionNode node, Variables vars) throws SDMetricsException {
 269  148
                 Object obj = evalExpression(element, node, vars);
 270  148
                 if (!(obj instanceof ModelElement)) {
 271  11
                         return null;
 272  
                 }
 273  137
                 return (ModelElement) obj;
 274  
         }
 275  
 
 276  
         /**
 277  
          * Evaluates a condition expression. Condition expressions return a boolean
 278  
          * value <code>true</code> or <code>false</code>.
 279  
          * 
 280  
          * @param element The model element for which to calculate the condition
 281  
          *        expression.
 282  
          * @param node Root node of the condition expression operator tree.
 283  
          * @param vars Variables for the expression evaluation
 284  
          * @return The result of the condition expression.
 285  
          * @throws SDMetricsException An error occurred evaluating the expression.
 286  
          */
 287  
         boolean evalBooleanExpression(ModelElement element, ExpressionNode node,
 288  
                         Variables vars) throws SDMetricsException {
 289  480
                 String operator = node.getValue();
 290  960
                 ProcedureCache<BooleanOperation> scops = metrics
 291  480
                                 .getExpressionOperations().getBooleanOperations();
 292  
 
 293  480
                 BooleanOperation op = scops.getProcedure(operator);
 294  479
                 op.setMetricsEngine(this);
 295  479
                 boolean result = op.calculateValue(element, node, vars);
 296  476
                 scops.returnProcedure(op);
 297  476
                 return result;
 298  
         }
 299  
 
 300  
         /**
 301  
          * Evaluates a scalar operator in a metric expression. Scalar operators
 302  
          * return numbers, strings, or model elements.
 303  
          * 
 304  
          * @param element The model element for which to calculate the operation.
 305  
          * @param node Node with the operator.
 306  
          * @param vars Variables for the expression evaluation
 307  
          * @return The result of the operation.
 308  
          * @throws SDMetricsException An error occurred evaluating the operation.
 309  
          */
 310  
         private Object evalOperationExpression(ModelElement element,
 311  
                         ExpressionNode node, Variables vars) throws SDMetricsException {
 312  
 
 313  1212
                 String operator = node.getValue();
 314  2424
                 ProcedureCache<ScalarOperation> scops = metrics
 315  1212
                                 .getExpressionOperations().getScalarOperations();
 316  
 
 317  1212
                 ScalarOperation op = scops.getProcedure(operator);
 318  1210
                 op.setMetricsEngine(this);
 319  1210
                 Object result = op.calculateValue(element, node, vars);
 320  1204
                 scops.returnProcedure(op);
 321  1204
                 return result;
 322  
         }
 323  
 
 324  
         /**
 325  
          * Evaluates an identifier in a metric expression.
 326  
          * <p>
 327  
          * The identifier may refer to a metric, an attribute, or a variable
 328  
          * (including the implicit variable _self). The type of identifier is
 329  
          * checked in that order.
 330  
          * 
 331  
          * @param element The model element for which to evaluate the identifier.
 332  
          * @param identifier The identifier.
 333  
          * @param vars Variables for the expression evaluation
 334  
          * @return The value of the identifier.
 335  
          * @throws SDMetricsException The identifier could not be resolved.
 336  
          */
 337  
         private Object evalIdentifier(ModelElement element, String identifier,
 338  
                         Variables vars) throws SDMetricsException {
 339  
                 // check if identifier is a metric
 340  1638
                 Metric metric = metrics.getMetric(element.getType(), identifier);
 341  1638
                 if (metric != null) {
 342  116
                         return getMetricValue(element, metric);
 343  
                 }
 344  
 
 345  
                 // check if identifier is a single-valued attribute
 346  1522
                 if (element.getType().hasAttribute(identifier)) {
 347  791
                         if (!element.getType().isSetAttribute(identifier)) {
 348  790
                                 if (element.getType().isRefAttribute(identifier)) {
 349  163
                                         ModelElement ref = element.getRefAttribute(identifier);
 350  163
                                         if (ref == null) {
 351  14
                                                 return ""; // no element referenced -> empty string
 352  
                                         }
 353  149
                                         return ref;
 354  
                                 }
 355  
                                 // plain data attribute, return its value
 356  627
                                 return element.getPlainAttribute(identifier);
 357  
                         }
 358  
                 }
 359  
 
 360  
                 // _self returns the current model element; also accept the
 361  
                 // deprecated "self"
 362  732
                 if ("_self".equals(identifier) || "self".equals(identifier)) {
 363  94
                         return element;
 364  
                 }
 365  
 
 366  
                 // check variables in the value map
 367  638
                 if (vars != null) {
 368  634
                         if (vars.hasVariable(identifier)) {
 369  633
                                 return vars.getVariable(identifier);
 370  
                         }
 371  
                 }
 372  
 
 373  10
                 throw new SDMetricsException(element, null,
 374  10
                                 "No metric or single-valued attribute '" + identifier
 375  5
                                                 + "' for elements of type '"
 376  5
                                                 + element.getType().getName() + "'.");
 377  
         }
 378  
 
 379  
         /**
 380  
          * Evaluates a set expression. Set expressions return sets.
 381  
          * 
 382  
          * @param element The model element for which to calculate the set
 383  
          *        expression.
 384  
          * @param node Root node of the set expression operator tree.
 385  
          * @param vars Variables for the expression evaluation
 386  
          * @return The contents of the resulting set.
 387  
          * @throws SDMetricsException An error occurred evaluating the set
 388  
          *         expression.
 389  
          */
 390  
         Collection<?> evalSetExpression(ModelElement element, ExpressionNode node,
 391  
                         Variables vars) throws SDMetricsException {
 392  
 
 393  
                 // The root of a set expression must be an operator or identifier
 394  
                 // accordingly.
 395  204
                 if (node.isIdentifier()) {
 396  186
                         return evalSetIdentifier(element, node.getValue(), vars);
 397  
                 }
 398  
 
 399  
                 // Make sure we have a binary operator
 400  18
                 if (!node.isOperation()) {
 401  2
                         throw new SDMetricsException(element, null,
 402  1
                                         "Illegal set expression.");
 403  
                 }
 404  
 
 405  
                 // Process the operator
 406  17
                 String operator = node.getValue();
 407  34
                 ProcedureCache<SetOperation> scops = metrics.getExpressionOperations()
 408  17
                                 .getSetOperations();
 409  17
                 SetOperation op = scops.getProcedure(operator);
 410  15
                 op.setMetricsEngine(this);
 411  15
                 Collection<?> result = op.calculateValue(element, node, vars);
 412  15
                 scops.returnProcedure(op);
 413  15
                 return result;
 414  
         }
 415  
 
 416  
         /**
 417  
          * Evaluates an identifier in a set expression.
 418  
          * 
 419  
          * @param element The model element for which to evaluate the set
 420  
          *        identifier.
 421  
          * @param identifier The set identifier.
 422  
          * @param vars Variables for the expression evaluation
 423  
          * @return The the set specified by the identifier.
 424  
          * @throws SDMetricsException The identifier could not be resolved.
 425  
          */
 426  
         private Collection<?> evalSetIdentifier(ModelElement element,
 427  
                         String identifier, Variables vars) throws SDMetricsException {
 428  
                 // Check if there is a set of that name for the element
 429  186
                 Set set = metrics.getSet(element.getType(), identifier);
 430  186
                 if (set != null) {
 431  143
                         return getSet(element, set);
 432  
                 }
 433  
 
 434  
                 // Check multi-valued attributes of the element
 435  43
                 if (element.getType().hasAttribute(identifier)) {
 436  2
                         if (element.getType().isSetAttribute(identifier)) {
 437  1
                                 return element.getSetAttribute(identifier);
 438  
                         }
 439  
                 }
 440  
 
 441  
                 // Check the variables
 442  42
                 if (vars != null) {
 443  40
                         Object o = vars.getVariable(identifier);
 444  40
                         if (o != null) {
 445  39
                                 if (o instanceof Collection) {
 446  38
                                         return (Collection<?>) o;
 447  
                                 }
 448  
 
 449  2
                                 throw new SDMetricsException(element, null, "Variable '"
 450  1
                                                 + identifier + "' is not a set.");
 451  
                         }
 452  
                 }
 453  
 
 454  6
                 throw new SDMetricsException(element, null, "Unknown set identifier '"
 455  3
                                 + identifier + "' for elements of type '"
 456  3
                                 + element.getType().getName() + "'.");
 457  
         }
 458  
 
 459  
         /** Cache to store all measurement values. */
 460  118
         class MetricValuesCache {
 461  
                 /** Metric value cache. */
 462  118
                 private final HashMap<ModelElement, Object[]> metricValues = 
 463  118
                                 new HashMap<ModelElement, Object[]>();
 464  
                 /** Set value cache. */
 465  118
                 private final HashMap<ModelElement, Collection<?>[]> setValues = 
 466  118
                                 new HashMap<ModelElement, Collection<?>[]>();
 467  
 
 468  
                 /**
 469  
                  * Sets the value for a metric of an element.
 470  
                  * 
 471  
                  * @param element the model element
 472  
                  * @param metric the metric
 473  
                  * @param value the value of the metric
 474  
                  */
 475  
                 void setMetricValue(ModelElement element, Metric metric, Object value) {
 476  336
                         Object[] values = metricValues.get(element);
 477  336
                         if (values == null) {
 478  262
                                 values = new Object[metrics.getMetrics(element.getType())
 479  131
                                                 .size()];
 480  131
                                 metricValues.put(element, values);
 481  
                         }
 482  336
                         values[metric.getID()] = value;
 483  336
                 }
 484  
 
 485  
                 /**
 486  
                  * Sets the value of a set for a model element.
 487  
                  * 
 488  
                  * @param element the model element
 489  
                  * @param set the set
 490  
                  * @param value the value of the metric
 491  
                  */
 492  
                 void setSet(ModelElement element, Set set, Collection<?> value) {
 493  384
                         Collection<?>[] collections = setValues.get(element);
 494  384
                         if (collections == null) {
 495  374
                                 collections = new Collection<?>[metrics.getSets(element.getType())
 496  187
                                                 .size()];
 497  187
                                 setValues.put(element, collections);
 498  
                         }
 499  384
                         collections[set.getID()] = value;
 500  384
                 }
 501  
 
 502  
                 /**
 503  
                  * Retrieves the value of a metric for a model element.
 504  
                  * 
 505  
                  * @param element the model element
 506  
                  * @param metric the metric
 507  
                  * @return Value of the metric, <code>null</code> if the value has not
 508  
                  *         yet been cached
 509  
                  */
 510  
                 Object getMetricValue(ModelElement element, Metric metric) {
 511  192
                         Object[] result = metricValues.get(element);
 512  192
                         if (result != null) {
 513  61
                                 return result[metric.getID()];
 514  
                         }
 515  131
                         return null;
 516  
                 }
 517  
 
 518  
                 /**
 519  
                  * Retrieves the value of a set for a model element.
 520  
                  * 
 521  
                  * @param element the model element
 522  
                  * @param set the set
 523  
                  * @return Value of the set, <code>null</code> if the value has not yet
 524  
                  *         been cached
 525  
                  */
 526  
                 Collection<?> getSet(ModelElement element, Set set) {
 527  200
                         Collection<?>[] result = setValues.get(element);
 528  200
                         if (result != null) {
 529  13
                                 return result[set.getID()];
 530  
                         }
 531  187
                         return null;
 532  
                 }
 533  
         }
 534  
 }