From ffe63d038649657ab2245a6dbb87a78ea18f6e79 Mon Sep 17 00:00:00 2001 From: Eddie Groh <groh@fortiss.org> Date: Thu, 10 Aug 2023 18:15:42 +0200 Subject: [PATCH] Added automatic trigger for metric collection and executed large refactor and cleanup as well as added developer documentation Issue-Ref: 4310 Issue-Url: https://git.fortiss.org/af3/af3/-/issues/4310 Signed-off-by: Eddie Groh <groh@fortiss.org> --- .../ui/ModelQualityExtractionMenu.java | 4 +- .../ui/view/fx/ModelQualityFXController.java | 13 +- .../html/developer/documentation.html | 124 +++++- .../ext/quality/GraphMetricsProvider.java | 321 ++++++++++++++ .../quality/HierarchicElementProvider.java | 2 +- .../tooling/ext/quality/data/MetricKey.java | 2 +- .../quality/service/IModelQualityService.java | 4 +- .../quality/service/ModelQualityService.java | 395 +++++------------- .../{io => storage}/CSVFileWriter.java | 13 +- .../storage/ModelQualityStorageManager.java | 43 ++ 10 files changed, 613 insertions(+), 308 deletions(-) create mode 100644 org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/GraphMetricsProvider.java rename org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/{io => storage}/CSVFileWriter.java (93%) create mode 100644 org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/ModelQualityStorageManager.java diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityExtractionMenu.java b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityExtractionMenu.java index 31ae65d7e..e260d8d38 100644 --- a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityExtractionMenu.java +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityExtractionMenu.java @@ -36,7 +36,7 @@ import org.fortiss.tooling.kernel.ui.extension.data.ContextMenuContextProvider; /** * Crates a context menu entry to generate the for the selected * - * @author munaro + * @author groh */ public class ModelQualityExtractionMenu implements IContextMenuContributor { @@ -81,7 +81,7 @@ public class ModelQualityExtractionMenu implements IContextMenuContributor { @Override public void run() { ITopLevelElement top = IPersistencyService.getInstance().getTopLevelElementFor(this.fp); - IModelQualityService.getInstance().performMetricAnalysis(top); + IModelQualityService.getInstance().scheduleMetricCollection(top); } } } diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXController.java b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXController.java index 4ffec83f3..a477b26f6 100644 --- a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXController.java +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXController.java @@ -177,6 +177,11 @@ public class ModelQualityFXController extends CompositeFXControllerBase<SplitPan Set<MetricKey> validKeys = new HashSet<>(); validKeys.addAll(node.getStoredDoubles().keySet()); validKeys.addAll(node.getStoredIntegers().keySet()); + for(MetricTreeNode child : node.getChildren()) { + validKeys.addAll(child.getStoredDoubles().keySet()); + validKeys.addAll(child.getStoredIntegers().keySet()); + } + ObservableList<MetricKey> choices = choiceBox.getItems(); choices.retainAll(validKeys); for(MetricKey key : validKeys) { @@ -256,7 +261,7 @@ public class ModelQualityFXController extends CompositeFXControllerBase<SplitPan } double maxval = children.stream().map(p -> p.getValueAsDouble(key)).filter(p -> p != null) - .mapToDouble(p -> p).max().getAsDouble(); + .mapToDouble(p -> p).max().orElse(0.0); chartStyle.setUseIndividualAxisSegments(false); chartStyle.setTitleStyle(new FontStyle("Verdana", 14, BLUE.brighter())); @@ -264,10 +269,14 @@ public class ModelQualityFXController extends CompositeFXControllerBase<SplitPan DataSeries elementData = new DataSeries("Data"); for(var child : node.getChildren()) { Double value = child.getValueAsDouble(key); + String name = child.getName(); if(value == null) { value = 0.0; } - DoubleAxis testing = new DoubleAxis(child.getName(), 0.0, maxval); + if(name == null) { + name = "Impossible null"; + } + DoubleAxis testing = new DoubleAxis(name, 0.0, maxval); AxisStyle aStyle3Segs = new AxisStyle(SOLID_BLACK_1PT, BLACK_VERDANA_14PT, 3, BLACK_VERDANA_8PT, new DecimalFormat("#.##")); diff --git a/org.fortiss.tooling.ext.quality/html/developer/documentation.html b/org.fortiss.tooling.ext.quality/html/developer/documentation.html index d8e07a13f..d97b0a20c 100644 --- a/org.fortiss.tooling.ext.quality/html/developer/documentation.html +++ b/org.fortiss.tooling.ext.quality/html/developer/documentation.html @@ -6,7 +6,129 @@ <body lang="en-US" dir="ltr"> <h1>Developer Documentation for <i>Quality Functionality (</i><i>org.fortiss.tooling.ext.quality</i><i>)</i></h1> -<p>Lorem Ipsum</p> + +<p>Below is a list of all collected metrics. They are represented by the enum <code> org.fortiss.tooling.ext.quality.data.MetricKey</code></p> +<p>They are categorized by the respective provider collecting the metric</p> + +<h2>HierarchicElementProvider</h2> +<code>org.fortiss.tooling.ext.quality.HierarchicElementProvider</code> + +<h4>UNQIUE_ID</h4> +<p><code>Integer</code>: This is an unique identifier for each element as specified in <code>org.fortiss.tooling.kernel.model.IIdLabeled</code>. </p> + +<h4>NUMBER_OF_CONNECTORS</h4> +<p><code>Integer</code>: Number of connectors in the respective element</p> + +<h4>NUMBER_OF_CONTAINED_ELEMENTS</h4> +<p><code>Integer</code>: Number of entries in the containedElements list.</p> + +<h4>NUMBER_OF_CONNECTIONS</h4> +<p><code>Integer</code>: Number of connections in this element. List of aggregated connection model elements. Usually a aggregates all connections of its direct sub-structure.</p> + +<h4>NUMBER_OF_TOTAL_CONNECTORS</h4> +<p><code>Integer</code>: Total number of connectors in this element and all contained elements</p> + +<h4>NUMBER_OF_TOTAL_ENTRY_CONNECTORS</h4> +<p><code>Integer</code>: Sum of all connectors which implement <code>org.fortiss.tooling.base.model.base.EntryConnectorBase</code></p> + +<h4>NUMBER_OF_TOTAL_EXIT_CONNECTORS</h4> +<p><code>Integer</code>: Sum of all connectors which implement <code>org.fortiss.tooling.base.model.base.ExitConnectorBase</code>. Usually the sum over this and <code>NUMBER_OF_TOTAL_ENTRY_CONNECTORS</code> should equal <code>NUMBER_OF_TOTAL_CONNECTORS</code></p> + +<h4>NUMBER_OF_TOTAL_ELEMENTS</h4> +<p><code>Integer</code>: Total number of elements contained in this element. Includes the element for which this metric is recorded.</p> + +<h4>NUMBER_OF_TOTAL_COMMENTABLE_ELEMENTS</h4> +<p><code>Integer</code>: Total number of elements implementing <code>org.fortiss.tooling.kernel.model.INamedCommentedElement</code> in this element. Includes the element for which this metric is recorded. Never exceeds <code>NUMBER_OF_TOTAL_ELEMENTS</code>.</p> + +<h4>NUMBER_OF_TOTAL_COMMENTED_ELEMENTS</h4> +<p><code>Integer</code>: Total number of elements which are commentable and do not have <code>null</code> or an empty string as comment</p> + +<h4>NUMBER_OF_TOTAL_LEAF_ELEMENTS</h4> +<p><code>Integer</code>: Total number of elements contained in this element which do not contain elements. Includes self</p> + + +<h2>GraphMetricsProvider</h2> +<code>org.fortiss.tooling.ext.quality.GraphMetricsProvider</code> + + +<h4>BETWEENESS_CENTRALITY</h4> +<p><code>Double</code>: Value of the betweenness centrality of this element embedded in a graph of consisting of all elements which are contained in the element which contains this element, as well as all neighbours of these elements</p> +<p>The direction of the connections are respected, and it is assumed that all connections connect to a ExitConnectorBase or EntryConnectorBase</p> + + +<h4>BETWEENESS_CENTRALITY_RECURSIVELY</h4> +<p><code>Double</code>: Same as above, but all connections are resolved to leaf elements, i.e. elements which do not contain other elements</p> + + +<h4>CLUSTERING_COEFFICIENT</h4> +<p><code>Double</code>: Clustering coefficient of this element. Ignores connection direction and resolves all neighbours to leaf elements.</p> + + + + +<h2>ComponentMetricProvider</h2> +<code>org.fortiss.af3.component.quality.ComponentMetricProvider</code> + +<h4>NUMBER_OF_TOTAL_CODE_CONSTANTS</h4> +<p><code>Integer</code>: Total number of constants in code specifications in this element and all contained elements</p> + +<h4>NUMBER_OF_TOTAL_CODE_CYCLOMATIC_COMPLEXITY</h4> +<p><code>Integer</code>: The total cyclomatic complexity of all code specifications in this element and all contained elements</p> + +<h4>NUMBER_OF_TOTAL_CODE_LINES</h4> +<p><code>Integer</code>: Total number of lines of code in all code specifications in this element and all contained elements</p> + +<h4>NUMBER_OF_TOTAL_CODE_COMMENTS</h4> +<p><code>Integer</code>: Total number of comment blocks in all code specifications in this element and all contained elements</p> + + +<h2>ComponentMetricProvider</h2> +<code>org.fortiss.af3.component.quality.ComponentMetricProvider</code> + + +<h2>DataDictonaryProvider</h2> +<code>org.fortiss.af3.component.quality.DataDictonaryProvider</code> +<p> Provides Metrics also collected by the HierarchicElementProvider, but for the DataDictionary.</p> + + +<h2>SafetyMetricProvider</h2> +<code>org.fortiss.af3.safety.quality.SafetyMetricProvider</code> + +<h4>SAFETY_LEVEL</h4> +<p><code>String</code>: String representation of the assigned safety level of this element</p> + + +<h2>AllocationTableProvider</h2> +<code>org.fortiss.af3.task.quality.AllocationTableProvider</code> + +<h4>TASK_ALLOCATED_COMPONENTS</h4> +<p><code>Integer</code>: Number of allocated Components on this Task. This metric can only be non-null for <code>org.fortiss.af3.task.model.Task</code> Elements</p> + +<h4>HARDWARE_ALLOCATED_TASKS</h4> +<p><code>Integer</code>: Number of allocated Tasks on this ExecutionUnit. This metric can only be non-null for <code>org.fortiss.af3.platform.model.ExecutionUnit</code> Elements</p> + + + +<h2>ModelQualityService</h2> +<code>org.fortiss.tooling.ext.quality.service.ModelQualityService</code> + +<h4>NESTING_LEVEL</h4> +<p><code>Integer</code>: The depth of this element. Defined by the number of elements which have to be traversed to reach this element from the root</p> + + +<h4>CONSTRAINT_VIOLATIONS_ERROR</h4> +<p><code>Integer</code>: Number of constraint violations of the severity error which have this element as source</p> + + +<h4>CONSTRAINT_VIOLATIONS_WARNING</h4> +<p><code>Integer</code>: Number of constraint violations of the severity warning which have this element as source</p> + + + + + + + </body> </html> \ No newline at end of file diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/GraphMetricsProvider.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/GraphMetricsProvider.java new file mode 100644 index 000000000..bb73def50 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/GraphMetricsProvider.java @@ -0,0 +1,321 @@ +/*-------------------------------------------------------------------------+ +| Copyright 2023 fortiss GmbH | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); | +| you may not use this file except in compliance with the License. | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, | +| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | +| See the License for the specific language governing permissions and | +| limitations under the License. | ++--------------------------------------------------------------------------*/ +package org.fortiss.tooling.ext.quality; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Set; +import java.util.Stack; + +import org.eclipse.emf.common.util.EList; +import org.fortiss.tooling.base.model.base.EntryConnectorBase; +import org.fortiss.tooling.base.model.base.ExitConnectorBase; +import org.fortiss.tooling.base.model.element.IConnection; +import org.fortiss.tooling.base.model.element.IConnector; +import org.fortiss.tooling.base.model.element.IHierarchicElement; +import org.fortiss.tooling.ext.quality.data.MetricDataManager; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.kernel.utils.EcoreUtils; + +/** + * + * @author groh + */ +public class GraphMetricsProvider { + + /** + * Creates a list of nodes contained in the graph centered around the provided element + * It will contain all elements contained within the provided element as well as all elements + * connected to the provided element + * + * @param scopeElement + * element defining the scope of the graph + * @param recursively + * if true, everything is resolved to leaf elements + * @return node list + */ + private static Set<IHierarchicElement> getLocalGraphView(IHierarchicElement scopeElement, + boolean recursively) { + // Create a view consisting of all nodes inside this element, as well as all elements + // connected to this element + + // This ensures that elements inside this element communicating with elements outside + // this element are properly recognized + Set<IHierarchicElement> graphNodes = new HashSet<>(); + if(recursively) { + recursivelyGetLeafElements(scopeElement, graphNodes); + } else { + graphNodes.addAll(scopeElement.getContainedElements()); + } + // Collect all elements to which currentElement has a outgoing connection + for(ExitConnectorBase exitConnector : EcoreUtils.pickInstanceOf(ExitConnectorBase.class, + scopeElement.getConnectors())) { + for(IConnection exitConnection : exitConnector.getOutgoing()) { + graphNodes.add(exitConnection.getTarget().getOwner()); + } + } + // Collect all elements to which currentElement has a incoming connection + for(EntryConnectorBase entryConnector : EcoreUtils.pickInstanceOf(EntryConnectorBase.class, + scopeElement.getConnectors())) { + for(IConnection entryConnection : entryConnector.getIncoming()) { + graphNodes.add(entryConnection.getSource().getOwner()); + } + } + return graphNodes; + } + + /** + * Get all leafs inside the provided element + * + * @param element + * the element containing leafs + * @param allElements + * list where the leafs are saved + */ + private static void recursivelyGetLeafElements(IHierarchicElement element, + Set<IHierarchicElement> allElements) { + allElements.add(element); + for(IHierarchicElement containedElement : element.getContainedElements()) { + recursivelyGetLeafElements(containedElement, allElements); + } + } + + /** + * Follows outgoing connections until the connector has no further outgoing connections and then + * saves the owner of this connector to the provided list + * + * @param connector + * starting point for search + * @param allElements + * list where the owners are collected + */ + private static void recursivlyFollowOutgoingConnection(IConnector connector, + Set<IHierarchicElement> allElements) { + + EList<IConnection> outgoingConnections = connector.getOutgoing(); + + if(outgoingConnections.isEmpty()) { + allElements.add(connector.getOwner()); + return; + } + + for(IConnection outgoingConnection : outgoingConnections) { + recursivlyFollowOutgoingConnection(outgoingConnection.getTarget(), allElements); + } + } + + /** + * Follows incoming connections until the connector has no further incoming connections and + * then + * saves the owner of this connector to the provided list + * + * @param connector + * starting point for search + * @param allElements + * list where the owners are collected + */ + private static void recursivlyFollowIncomingConnection(IConnector connector, + Set<IHierarchicElement> allElements) { + + EList<IConnection> incomingConnections = connector.getIncoming(); + + if(incomingConnections.isEmpty()) { + allElements.add(connector.getOwner()); + return; + } + + for(IConnection incomingConnection : incomingConnections) { + recursivlyFollowIncomingConnection(incomingConnection.getSource(), allElements); + } + } + + /** + * Collects all neighbors regardless of the direction of connection + * + * @param element + * of which the neighbors should be collected + * @return set of the neighbors + */ + private static Set<IHierarchicElement> getUndirectedNeighbors(IHierarchicElement element) { + Set<IHierarchicElement> neighbors = new HashSet<>(); + for(ExitConnectorBase exitConnectors : EcoreUtils.pickInstanceOf(ExitConnectorBase.class, + element.getConnectors())) { + recursivlyFollowOutgoingConnection(exitConnectors, neighbors); + } + for(EntryConnectorBase entryConnectors : EcoreUtils.pickInstanceOf(EntryConnectorBase.class, + element.getConnectors())) { + recursivlyFollowIncomingConnection(entryConnectors, neighbors); + } + return neighbors; + } + + /** + * Calculates the clustering coefficient based on the neighborhood + * + * @param element + * of which the coefficient shall be calculated + * @return value of the coefficient + */ + public static double calculateClusteringCoefficent(IHierarchicElement element) { + + Set<IHierarchicElement> neighbors = getUndirectedNeighbors(element); + + int totalEdges = neighbors.size(); + if(totalEdges < 2) { + return 0.0; // If there are fewer than 2 neighbors, the coefficient is undefined (or 0). + } + + int triangles = 0; + for(IHierarchicElement neighbor : neighbors) { + + Set<IHierarchicElement> neighborNeighbors = getUndirectedNeighbors(neighbor); + + for(IHierarchicElement secondNeighbor : neighbors) { + if(secondNeighbor != neighbor && neighborNeighbors.contains(secondNeighbor)) { + triangles++; + } + } + } + double clusteringCoefficient = (double)triangles / (totalEdges * (totalEdges - 1)); + return clusteringCoefficient; + } + + /** + * Calculates and saves the betweenness centrality of all elements contained inside the provided + * scopeElement + * + * @param scopeElement + * the scope of this calculation + * @param manager + * location to save the metrics + * @param recursively + * if true, all calculation will be carried out on leaf elements + */ + public static void calculateBetweennessCentrality(IHierarchicElement scopeElement, + MetricDataManager manager, boolean recursively) { + + // Abort if no elements are found + if(!scopeElement.getContainedElements().isEmpty()) { + + Set<IHierarchicElement> graphNodes = getLocalGraphView(scopeElement, recursively); + // Initialize the betweenness values for all nodes to zero + Map<IHierarchicElement, Double> betweenness = new HashMap<>(); + for(IHierarchicElement elementNode : graphNodes) { + betweenness.put(elementNode, 0.0); + } + + // Iterate over all collected nodes + for(IHierarchicElement elementNode : graphNodes) { + + // Initialize all variables + Stack<IHierarchicElement> stack = new Stack<>(); + Map<IHierarchicElement, List<IHierarchicElement>> pred = new HashMap<>(); + Map<IHierarchicElement, Integer> dist = new HashMap<>(); + Map<IHierarchicElement, Double> sigma = new HashMap<>(); + Queue<IHierarchicElement> queue = new LinkedList<>(); + + for(IHierarchicElement v : graphNodes) { + pred.put(v, new ArrayList<>()); + dist.put(v, -1); + sigma.put(v, 0.0); + } + sigma.put(elementNode, 1.0); + dist.put(elementNode, 0); + queue.add(elementNode); + + while(!queue.isEmpty()) { + IHierarchicElement v = queue.remove(); + stack.push(v); + + // Iterate over all connectors going out from the element + for(ExitConnectorBase exitConnectors : EcoreUtils + .pickInstanceOf(ExitConnectorBase.class, v.getConnectors())) { + // Iterate over all connections + for(IConnection outgoingConnection : exitConnectors.getOutgoing()) { + + Set<IHierarchicElement> targetElements = new HashSet<>(); + if(recursively) { + recursivlyFollowOutgoingConnection(outgoingConnection.getTarget(), + targetElements); + } else { + IHierarchicElement primaryTargetElement = + outgoingConnection.getTarget().getOwner(); + // When we have a connection to the scopeElement, we have a + // connection leaving the local scope + if(scopeElement == primaryTargetElement) { + // Continue searching for outgoing connections on the target + // Connector + IConnector targetConnector = outgoingConnection.getTarget(); + for(IConnection secondaryOutgoingConnection : targetConnector + .getOutgoing()) { + targetElements.add( + secondaryOutgoingConnection.getTarget().getOwner()); + } + } else { + targetElements.add(primaryTargetElement); + } + } + for(IHierarchicElement targetElement : targetElements) { + + // Check if element is in node list, to avoid creating data for + // nodes outside of our graph + if(graphNodes.contains(targetElement)) { + if(dist.get(targetElement) < 0) { + queue.add(targetElement); + dist.put(targetElement, dist.get(v) + 1); + } + if(dist.get(targetElement) == dist.get(v) + 1) { + sigma.put(targetElement, + sigma.get(targetElement) + sigma.get(v)); + pred.get(targetElement).add(v); + } + } + } + } + } + } + // Initialize delta + Map<IHierarchicElement, Double> delta = new HashMap<>(); + for(IHierarchicElement v : graphNodes) { + delta.put(v, 0.0); + } + + while(!stack.isEmpty()) { + IHierarchicElement w = stack.pop(); + for(IHierarchicElement v : pred.get(w)) { + double c = (sigma.get(v) / sigma.get(w)) * (1.0 + delta.get(w)); + delta.put(v, delta.get(v) + c); + } + if(!w.equals(elementNode)) { + betweenness.put(w, betweenness.get(w) + delta.get(w)); + } + } + } + // Save metric + MetricKey key = recursively ? MetricKey.BETWEENESS_CENTRALITY_RECURSIVELY + : MetricKey.BETWEENESS_CENTRALITY; + for(IHierarchicElement child : scopeElement.getContainedElements()) { + manager.getTreeNodeLookupTable().get(child).getStoredDoubles().put(key, + betweenness.get(child)); + } + } + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementProvider.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementProvider.java index 894d4fdc7..f98165974 100644 --- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementProvider.java +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementProvider.java @@ -78,7 +78,7 @@ public class HierarchicElementProvider implements IMetricProvider<IHierarchicEle integers.put(MetricKey.NUMBER_OF_CONTAINED_ELEMENTS, currentElement.getContainedElements().size()); - integers.put(MetricKey.NUMBER_OF_CHANNELS, currentElement.getConnections().size()); + integers.put(MetricKey.NUMBER_OF_CONNECTIONS, currentElement.getConnections().size()); // depth metrics integers.put(MetricKey.NUMBER_OF_TOTAL_CONNECTORS, connectors.size()); var entry_connectors = EcoreUtils.pickInstanceOf(EntryConnectorBase.class, connectors); diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java index 827082829..2b24baa69 100644 --- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java @@ -35,7 +35,7 @@ public enum MetricKey { /** Key for the metric counting elements */ NUMBER_OF_CONTAINED_ELEMENTS(false), /** Key for the metric counting channels */ - NUMBER_OF_CHANNELS(false), + NUMBER_OF_CONNECTIONS(false), /** Safety level of the element */ SAFETY_LEVEL(false, true), /** How deeply nested the corresponding element is */ diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/IModelQualityService.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/IModelQualityService.java index 025bc2e75..f355df420 100644 --- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/IModelQualityService.java +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/IModelQualityService.java @@ -34,6 +34,6 @@ public interface IModelQualityService { <T extends EObject> void registerMetricProvider(IMetricProvider<T> provider, Class<T> modelElementClass); - /** analyses the metrics and processes them */ - void performMetricAnalysis(ITopLevelElement top); + /** Schedules an analysis of the metrics and processes them */ + void scheduleMetricCollection(ITopLevelElement top); } diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java index 9ecfd8179..65d4ba8af 100644 --- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java @@ -19,46 +19,49 @@ import static java.util.Collections.emptyList; import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedList; import java.util.List; -import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Queue; -import java.util.Set; -import java.util.Stack; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.stream.Collectors; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; import org.eclipse.emf.common.util.EList; import org.eclipse.emf.ecore.EObject; -import org.fortiss.tooling.base.model.base.EntryConnectorBase; -import org.fortiss.tooling.base.model.base.ExitConnectorBase; -import org.fortiss.tooling.base.model.element.IConnection; -import org.fortiss.tooling.base.model.element.IConnector; import org.fortiss.tooling.base.model.element.IHierarchicElement; +import org.fortiss.tooling.ext.quality.GraphMetricsProvider; import org.fortiss.tooling.ext.quality.IMetricProvider; import org.fortiss.tooling.ext.quality.data.MetricDataManager; import org.fortiss.tooling.ext.quality.data.MetricKey; import org.fortiss.tooling.ext.quality.data.MetricTreeNode; -import org.fortiss.tooling.ext.quality.io.CSVFileWriter; +import org.fortiss.tooling.ext.quality.storage.CSVFileWriter; +import org.fortiss.tooling.kernel.extension.data.IConstraintViolation; +import org.fortiss.tooling.kernel.extension.data.IConstraintViolation.ESeverity; import org.fortiss.tooling.kernel.extension.data.ITopLevelElement; +import org.fortiss.tooling.kernel.internal.ConstraintCheckerService; import org.fortiss.tooling.kernel.introspection.IIntrospectionDetailsItem; import org.fortiss.tooling.kernel.introspection.IIntrospectionItem; import org.fortiss.tooling.kernel.introspection.IIntrospectiveKernelService; import org.fortiss.tooling.kernel.model.IProjectRootElement; import org.fortiss.tooling.kernel.service.IKernelIntrospectionSystemService; import org.fortiss.tooling.kernel.service.IMigrationService; +import org.fortiss.tooling.kernel.service.IPersistencyService; import org.fortiss.tooling.kernel.service.base.EObjectAwareServiceBase; +import org.fortiss.tooling.kernel.service.listener.IPersistencyServiceListener; import org.fortiss.tooling.kernel.utils.EcoreUtils; /** * This class implements the {@link IMigrationService} interface. * * @author blaschke + * @author groh */ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider<?>> - implements IIntrospectiveKernelService, IModelQualityService { + implements IIntrospectiveKernelService, IModelQualityService, IPersistencyServiceListener { /** The singleton instance. */ private static final ModelQualityService INSTANCE = new ModelQualityService(); @@ -81,10 +84,43 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider /** Static instance of the MetricDataManager */ public static MetricDataManager metricDataManagerInstance = new MetricDataManager(); + /** Top-level elements queued to be processed by the metric collector. */ + private final Queue<ITopLevelElement> queuedProcessableElements = + new ConcurrentLinkedQueue<ITopLevelElement>(); + + /** Stores the constraint checking job. */ + private final Job metricCollectorJob = new Job("Collecting metrics ...") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + + do { + if(monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + + ITopLevelElement toBeProcessed = queuedProcessableElements.poll(); + if(toBeProcessed == null) { + return Status.OK_STATUS; + } + + performMetricCollection(toBeProcessed); + } while(true); + } + }; + + /** {@inheritDoc} */ + @Override + public void scheduleMetricCollection(ITopLevelElement element) { + queuedProcessableElements.add(element); + metricCollectorJob.schedule(); + } + /** {@inheritDoc} */ @Override public void startService() { IKernelIntrospectionSystemService.getInstance().registerService(this); + IPersistencyService.getInstance().addTopLevelElementListener(this); } /** Registers the migration provider with the service. */ @@ -108,9 +144,7 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider * @param topLvl * the file project on which the analysis should be performed */ - @Override - public void performMetricAnalysis(ITopLevelElement topLvl) { - + private void performMetricCollection(ITopLevelElement topLvl) { // Sort root elements, as some metrics are saved into trees from other root elements // To avoid these problems, the elements have to be processed in a certain order @@ -145,6 +179,29 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider collectMetrics(rootNode, rootElement); } + // Collect all violated constraints and group them by their respective source + List<IConstraintViolation<? extends EObject>> contraintViolations = + ConstraintCheckerService.getInstance() + .performAllConstraintChecksRecursively(rootElement); + + for(Entry<? extends EObject, List<IConstraintViolation<? extends EObject>>> entry : contraintViolations + .stream().collect(Collectors.groupingBy(IConstraintViolation::getSource)) + .entrySet()) { + + MetricTreeNode node = + metricDataManagerInstance.getTreeNodeLookupTable().get(entry.getKey()); + + if(node != null) { + // Collect violations and store them in the error and warning metric + List<IConstraintViolation<? extends EObject>> violations = entry.getValue(); + var integers = node.getStoredIntegers(); + integers.put(MetricKey.CONSTRAINT_VIOLATIONS_ERROR, (int)violations.stream() + .filter(v -> v.getSeverity() == ESeverity.ERROR).count()); + integers.put(MetricKey.CONSTRAINT_VIOLATIONS_WARNING, (int)violations.stream() + .filter(v -> v.getSeverity() == ESeverity.WARNING).count()); + } + } + root_nodes.put(rootElement, rootNode); metricDataManagerInstance.getTreeNodeLookupTable().put(rootElement, rootNode); } @@ -213,288 +270,10 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider manager.getTreeNodeLookupTable().put(currentElement, node); - calculateBetweennessCentrality(currentElement, manager, false); - calculateBetweennessCentrality(currentElement, manager, true); + GraphMetricsProvider.calculateBetweennessCentrality(currentElement, manager, false); + GraphMetricsProvider.calculateBetweennessCentrality(currentElement, manager, true); node.getStoredDoubles().put(MetricKey.CLUSTERING_COEFFICIENT, - calculateClusteringCoefficent(currentElement)); - } - } - - /** - * Creates a list of nodes contained in the graph centered around the provided element - * It will contain all elements contained within the provided element as well as all elements - * connected to the provided element - * - * @param scopeElement - * element defining the scope of the graph - * @param recursively - * if true, everything is resolved to leaf elements - * @return node list - */ - private static Set<IHierarchicElement> getLocalGraphView(IHierarchicElement scopeElement, - boolean recursively) { - // Create a view consisting of all nodes inside this element, as well as all elements - // connected to this element - - // This ensures that elements inside this element communicating with elements outside - // this element are properly recognized - Set<IHierarchicElement> graphNodes = new HashSet<>(); - if(recursively) { - recursivelyGetLeafElements(scopeElement, graphNodes); - } else { - graphNodes.addAll(scopeElement.getContainedElements()); - } - // Collect all elements to which currentElement has a outgoing connection - for(ExitConnectorBase exitConnector : EcoreUtils.pickInstanceOf(ExitConnectorBase.class, - scopeElement.getConnectors())) { - for(IConnection exitConnection : exitConnector.getOutgoing()) { - graphNodes.add(exitConnection.getTarget().getOwner()); - } - } - // Collect all elements to which currentElement has a incoming connection - for(EntryConnectorBase entryConnector : EcoreUtils.pickInstanceOf(EntryConnectorBase.class, - scopeElement.getConnectors())) { - for(IConnection entryConnection : entryConnector.getIncoming()) { - graphNodes.add(entryConnection.getSource().getOwner()); - } - } - return graphNodes; - } - - /** - * Get all leafs inside the provided element - * - * @param element - * the element containing leafs - * @param allElements - * list where the leafs are saved - */ - private static void recursivelyGetLeafElements(IHierarchicElement element, - Set<IHierarchicElement> allElements) { - allElements.add(element); - for(IHierarchicElement containedElement : element.getContainedElements()) { - recursivelyGetLeafElements(containedElement, allElements); - } - } - - /** - * Follows outgoing connections until the connector has no further outgoing connections and then - * saves the owner of this connector to the provided list - * - * @param connector - * starting point for search - * @param allElements - * list where the owners are collected - */ - private static void recursivlyFollowOutgoingConnection(IConnector connector, - Set<IHierarchicElement> allElements) { - - EList<IConnection> outgoingConnections = connector.getOutgoing(); - - if(outgoingConnections.isEmpty()) { - allElements.add(connector.getOwner()); - return; - } - - for(IConnection outgoingConnection : outgoingConnections) { - recursivlyFollowOutgoingConnection(outgoingConnection.getTarget(), allElements); - } - } - - /** - * Follows incoming connections until the connector has no further incoming connections and - * then - * saves the owner of this connector to the provided list - * - * @param connector - * starting point for search - * @param allElements - * list where the owners are collected - */ - private static void recursivlyFollowIncomingConnection(IConnector connector, - Set<IHierarchicElement> allElements) { - - EList<IConnection> incomingConnections = connector.getIncoming(); - - if(incomingConnections.isEmpty()) { - allElements.add(connector.getOwner()); - return; - } - - for(IConnection incomingConnection : incomingConnections) { - recursivlyFollowIncomingConnection(incomingConnection.getSource(), allElements); - } - } - - /** - * Collects all neighbors regardless of the direction of connection - * - * @param element - * of which the neighbors should be collected - * @return set of the neighbors - */ - private static Set<IHierarchicElement> getUndirectedNeighbors(IHierarchicElement element) { - Set<IHierarchicElement> neighbors = new HashSet<>(); - for(ExitConnectorBase exitConnectors : EcoreUtils.pickInstanceOf(ExitConnectorBase.class, - element.getConnectors())) { - recursivlyFollowOutgoingConnection(exitConnectors, neighbors); - } - for(EntryConnectorBase entryConnectors : EcoreUtils.pickInstanceOf(EntryConnectorBase.class, - element.getConnectors())) { - recursivlyFollowIncomingConnection(entryConnectors, neighbors); - } - return neighbors; - } - - /** - * Calculates the clustering coefficient based on the neighborhood - * - * @param element - * of which the coefficient shall be calculated - * @return value of the coefficient - */ - private static double calculateClusteringCoefficent(IHierarchicElement element) { - - Set<IHierarchicElement> neighbors = getUndirectedNeighbors(element); - - int totalEdges = neighbors.size(); - if(totalEdges < 2) { - return 0.0; // If there are fewer than 2 neighbors, the coefficient is undefined (or 0). - } - - int triangles = 0; - for(IHierarchicElement neighbor : neighbors) { - - Set<IHierarchicElement> neighborNeighbors = getUndirectedNeighbors(neighbor); - - for(IHierarchicElement secondNeighbor : neighbors) { - if(secondNeighbor != neighbor && neighborNeighbors.contains(secondNeighbor)) { - triangles++; - } - } - } - double clusteringCoefficient = (double)triangles / (totalEdges * (totalEdges - 1)); - return clusteringCoefficient; - } - - /** - * Calculates and saves the betweenness centrality of all elements contained inside the provided - * scopeElement - * - * @param scopeElement - * the scope of this calculation - * @param manager - * location to save the metrics - * @param recursively - * if true, all calculation will be carried out on leaf elements - */ - private static void calculateBetweennessCentrality(IHierarchicElement scopeElement, - MetricDataManager manager, boolean recursively) { - - // Abort if no elements are found - if(!scopeElement.getContainedElements().isEmpty()) { - - Set<IHierarchicElement> graphNodes = getLocalGraphView(scopeElement, recursively); - // Initialize the betweenness values for all nodes to zero - Map<IHierarchicElement, Double> betweenness = new HashMap<>(); - for(IHierarchicElement elementNode : graphNodes) { - betweenness.put(elementNode, 0.0); - } - - // Iterate over all collected nodes - for(IHierarchicElement elementNode : graphNodes) { - - // Initialize all variables - Stack<IHierarchicElement> stack = new Stack<>(); - Map<IHierarchicElement, List<IHierarchicElement>> pred = new HashMap<>(); - Map<IHierarchicElement, Integer> dist = new HashMap<>(); - Map<IHierarchicElement, Double> sigma = new HashMap<>(); - Queue<IHierarchicElement> queue = new LinkedList<>(); - - for(IHierarchicElement v : graphNodes) { - pred.put(v, new ArrayList<>()); - dist.put(v, -1); - sigma.put(v, 0.0); - } - sigma.put(elementNode, 1.0); - dist.put(elementNode, 0); - queue.add(elementNode); - - while(!queue.isEmpty()) { - IHierarchicElement v = queue.remove(); - stack.push(v); - - // Iterate over all connectors going out from the element - for(ExitConnectorBase exitConnectors : EcoreUtils - .pickInstanceOf(ExitConnectorBase.class, v.getConnectors())) { - // Iterate over all connections - for(IConnection outgoingConnection : exitConnectors.getOutgoing()) { - - Set<IHierarchicElement> targetElements = new HashSet<>(); - if(recursively) { - recursivlyFollowOutgoingConnection(outgoingConnection.getTarget(), - targetElements); - } else { - IHierarchicElement primaryTargetElement = - outgoingConnection.getTarget().getOwner(); - // When we have a connection to the scopeElement, we have a - // connection leaving the local scope - if(scopeElement == primaryTargetElement) { - // Continue searching for outgoing connections on the target - // Connector - IConnector targetConnector = outgoingConnection.getTarget(); - for(IConnection secondaryOutgoingConnection : targetConnector - .getOutgoing()) { - targetElements.add( - secondaryOutgoingConnection.getTarget().getOwner()); - } - } else { - targetElements.add(primaryTargetElement); - } - } - for(IHierarchicElement targetElement : targetElements) { - - // Check if element is in node list, to avoid creating data for - // nodes outside of our graph - if(graphNodes.contains(targetElement)) { - if(dist.get(targetElement) < 0) { - queue.add(targetElement); - dist.put(targetElement, dist.get(v) + 1); - } - if(dist.get(targetElement) == dist.get(v) + 1) { - sigma.put(targetElement, - sigma.get(targetElement) + sigma.get(v)); - pred.get(targetElement).add(v); - } - } - } - } - } - } - // Initialize delta - Map<IHierarchicElement, Double> delta = new HashMap<>(); - for(IHierarchicElement v : graphNodes) { - delta.put(v, 0.0); - } - - while(!stack.isEmpty()) { - IHierarchicElement w = stack.pop(); - for(IHierarchicElement v : pred.get(w)) { - double c = (sigma.get(v) / sigma.get(w)) * (1.0 + delta.get(w)); - delta.put(v, delta.get(v) + c); - } - if(!w.equals(elementNode)) { - betweenness.put(w, betweenness.get(w) + delta.get(w)); - } - } - } - // Save metric - MetricKey key = recursively ? MetricKey.BETWEENESS_CENTRALITY_RECURSIVELY - : MetricKey.BETWEENESS_CENTRALITY; - for(IHierarchicElement child : scopeElement.getContainedElements()) { - manager.getTreeNodeLookupTable().get(child).getStoredDoubles().put(key, - betweenness.get(child)); - } + GraphMetricsProvider.calculateClusteringCoefficent(currentElement)); } } @@ -571,4 +350,28 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider public IIntrospectionDetailsItem getDetailsItem() { return null; } + + /** {@inheritDoc} */ + @Override + public void topLevelElementLoaded(ITopLevelElement element) { + // Nothing done + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementAdded(ITopLevelElement element) { + // Nothing done + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementRemoved(ITopLevelElement element) { + // Nothing done + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementContentChanged(ITopLevelElement element) { + scheduleMetricCollection(element); + } } diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/io/CSVFileWriter.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java similarity index 93% rename from org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/io/CSVFileWriter.java rename to org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java index 2598767c1..c7ef4a391 100644 --- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/io/CSVFileWriter.java +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java @@ -13,16 +13,16 @@ | See the License for the specific language governing permissions and | | limitations under the License. | +--------------------------------------------------------------------------*/ -package org.fortiss.tooling.ext.quality.io; +package org.fortiss.tooling.ext.quality.storage; import java.io.BufferedReader; import java.io.BufferedWriter; +import java.io.File; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; @@ -56,8 +56,15 @@ public class CSVFileWriter { return; } + if(!ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR.exists()) { + + ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR.mkdirs(); + } + // path to csv file - Path path = Paths.get("data_mega_cool.csv"); + Path path = + new File(ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR, "metric_logfile.csv") + .toPath(); List<String> allKeys = new ArrayList<>(); boolean createNewIndex = true; diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/ModelQualityStorageManager.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/ModelQualityStorageManager.java new file mode 100644 index 000000000..df5eddf73 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/ModelQualityStorageManager.java @@ -0,0 +1,43 @@ +/*-------------------------------------------------------------------------+ +| Copyright 2021 fortiss GmbH | +| | +| Licensed under the Apache License, Version 2.0 (the "License"); | +| you may not use this file except in compliance with the License. | +| You may obtain a copy of the License at | +| | +| http://www.apache.org/licenses/LICENSE-2.0 | +| | +| Unless required by applicable law or agreed to in writing, software | +| distributed under the License is distributed on an "AS IS" BASIS, | +| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | +| See the License for the specific language governing permissions and | +| limitations under the License. | ++--------------------------------------------------------------------------*/ +package org.fortiss.tooling.ext.quality.storage; + +import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; + +import java.io.File; + +import org.eclipse.core.resources.IWorkspaceRoot; + +/** + * Manages the local log of the model quality service. + * + * @author groh + */ +public class ModelQualityStorageManager { + + /** The name of the general reuse (library) directory. */ + public static final String AF3_QUALITY_DIRECTORY_NAME = "AF3-Model-Quality-Directory"; + + /** AF3 reuse library files extension */ + public static final String AF3_MODEL_QUALITY_FILES_EXTENSION = "af3_mq"; + + /** The current workspace root. */ + public static final IWorkspaceRoot WORKSPACE_ROOT = getWorkspace().getRoot(); + + /** The File handler of the reuse project. */ + public static final File MODEL_QUALITY_PROJECT_DIR = + new File(WORKSPACE_ROOT.getLocation() + File.separator + AF3_QUALITY_DIRECTORY_NAME); +} -- GitLab