diff --git a/.gitignore b/.gitignore index 763bc5ded238b70af005ecfed5fca311895d1e0a..8a621c8e4016a7303f39ad048b788fd50d0ba0e5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ /target/ *.rej .DS_Store -._.DS_Store +._.DS_Store diff --git a/.gitmodules b/.gitmodules index 6eabd0adfc781b97a5110fc9b00a4c8783258bda..ac46925eb1987978008571062fda3d7957c1b5c8 100644 --- a/.gitmodules +++ b/.gitmodules @@ -34,6 +34,12 @@ [submodule "org.fortiss.variability/.settings"] path = org.fortiss.variability/.settings url = https://git.fortiss.org/af3/plugin-settings.git +[submodule "org.fortiss.tooling.ext.quality/.settings"] + path = org.fortiss.tooling.ext.quality/.settings + url = https://git.fortiss.org/af3/plugin-settings.git +[submodule "org.fortiss.tooling.ext.quality.ui/.settings"] + path = org.fortiss.tooling.ext.quality.ui/.settings + url = https://git.fortiss.org/af3/plugin-settings.git [submodule "org.fortiss.tooling.ext.reuse/.settings"] path = org.fortiss.tooling.ext.reuse/.settings url = https://git.fortiss.org/af3/plugin-settings.git diff --git a/build.properties b/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..34d2e4d2dad529ceaeb953bfcdb63c51d69ffed2 --- /dev/null +++ b/build.properties @@ -0,0 +1,4 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + . diff --git a/org.fortiss.tooling.ext.quality.ui/.classpath b/org.fortiss.tooling.ext.quality.ui/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..13ff42602618641083d7f5f8bfc6696892e9b0a8 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/.classpath @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="src" path="src"/> + <classpathentry kind="src" path="res"/> + <classpathentry kind="output" path="build"/> +</classpath> diff --git a/org.fortiss.tooling.ext.quality.ui/.project b/org.fortiss.tooling.ext.quality.ui/.project new file mode 100644 index 0000000000000000000000000000000000000000..5ab5ccca9fd03df8e8b290633a0a1c5de0e5cd5b --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/.project @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.fortiss.tooling.ext.quality.ui</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.emfgeneration.git.EcoreBuilderGIT</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.CodeReviewBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.RemoveWarningsBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.GuidelinesCheckBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.systemfocus.tooling.codereview.nature.CodeReviewNature</nature> + </natures> +</projectDescription> diff --git a/org.fortiss.tooling.ext.quality.ui/.settings b/org.fortiss.tooling.ext.quality.ui/.settings new file mode 160000 index 0000000000000000000000000000000000000000..310d1c04f28f6252d5a02dd8fde1b76ae4a4da51 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/.settings @@ -0,0 +1 @@ +Subproject commit 310d1c04f28f6252d5a02dd8fde1b76ae4a4da51 diff --git a/org.fortiss.tooling.ext.quality.ui/META-INF/MANIFEST.MF b/org.fortiss.tooling.ext.quality.ui/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000000000000000000000000000000..b0798135a5797ff8ce43acf2acd5c7c124499887 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/META-INF/MANIFEST.MF @@ -0,0 +1,14 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Model Quality UI +Bundle-SymbolicName: org.fortiss.tooling.ext.quality.ui;singleton:=true +Bundle-Version: 2.23.0.qualifier +Automatic-Module-Name: org.fortiss.tooling.ext.quality.ui +Bundle-RequiredExecutionEnvironment: JavaSE-11 +Require-Bundle: org.fortiss.tooling.base.ui;visibility:=reexport, + org.fortiss.tooling.ext.quality;visibility:=reexport, + org.fortiss.tooling.spiderchart.ui;visibility:=reexport +Export-Package: org.fortiss.tooling.ext.quality.ui.view.fx +Bundle-Activator: org.fortiss.tooling.ext.quality.ui.ModelQualityUIActivator +Bundle-ActivationPolicy: lazy +Bundle-Vendor: fortiss GmbH diff --git a/org.fortiss.tooling.ext.quality.ui/build.properties b/org.fortiss.tooling.ext.quality.ui/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..89e6a0c49c96d230bdef3865573cec7fe09f3624 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/build.properties @@ -0,0 +1,9 @@ +# (c) 2023 fortiss GmbH +bin.includes = .,\ + META-INF/,\ + plugin.xml,\ + icons/ +source.. = src/,\ + res/ +output.. = build/ +src.includes = icons/ diff --git a/org.fortiss.tooling.ext.quality.ui/html/developer/.ratings b/org.fortiss.tooling.ext.quality.ui/html/developer/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..1c6ee9d1b91ce3a08078c3c27854d70ba3135f75 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/html/developer/.ratings @@ -0,0 +1 @@ +documentation.html aaa534ab30ef337c5e7bc75d37490eb6a5a4edd1 GREEN diff --git a/org.fortiss.tooling.ext.quality.ui/html/developer/documentation.html b/org.fortiss.tooling.ext.quality.ui/html/developer/documentation.html new file mode 100644 index 0000000000000000000000000000000000000000..aaa534ab30ef337c5e7bc75d37490eb6a5a4edd1 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/html/developer/documentation.html @@ -0,0 +1,13 @@ +<html><body> +<h1>Developer Documentation for <i>AF3 Model Quality</i> (org.fortiss.tooling.ext.quality.ui)</h1> + +<h2>Plugin description</h2> +<p>This is the user interface part of the tooling.ext.quality plugin. It provides a simple view to display collected metrics</p> + +<h2>Package description</h2> +<ul> +<li><tt>quality.ui</tt>: main package with the UI plugin activator.</li> +<li><tt>quality.ui.view.fx</tt>: implementation of quality library view (FX-based).</li> +</ul> + +</body></html> diff --git a/org.fortiss.tooling.ext.quality.ui/icons/metric.png b/org.fortiss.tooling.ext.quality.ui/icons/metric.png new file mode 100644 index 0000000000000000000000000000000000000000..0822af9ca579b86080348b6520d12b0f93dab96c Binary files /dev/null and b/org.fortiss.tooling.ext.quality.ui/icons/metric.png differ diff --git a/org.fortiss.tooling.ext.quality.ui/plugin.properties b/org.fortiss.tooling.ext.quality.ui/plugin.properties new file mode 100644 index 0000000000000000000000000000000000000000..63ff52c8a1cc1c3322ec47fd7707502614cfc530 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/plugin.properties @@ -0,0 +1,4 @@ +<!-- (c) 2023 fortiss GmbH --> + +pluginName = Model Quality UI +providerName = fortiss GmbH diff --git a/org.fortiss.tooling.ext.quality.ui/plugin.xml b/org.fortiss.tooling.ext.quality.ui/plugin.xml new file mode 100644 index 0000000000000000000000000000000000000000..25a97df238a3d86523d74dabc4fe8c3127c2da83 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/plugin.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.0"?> +<!-- (c) 2021 fortiss GmbH --> + +<plugin> + <extension point="org.fortiss.tooling.kernel.ui.contextMenuContribution"> + <contextMenuContribution + contributor="org.fortiss.tooling.ext.quality.ui.ModelQualityExtractionMenu"> + </contextMenuContribution> + </extension> + <extension point="org.eclipse.ui.views"> + <view + class="org.fortiss.tooling.ext.quality.ui.view.fx.ModelQualityFXViewPart" + icon="icons/metric.png" + id="org.fortiss.tooling.ext.quality.ui.metricsView" + name="Metrics" + restorable="true" + allowMultiple="false"> + </view> + </extension> +</plugin> \ No newline at end of file diff --git a/org.fortiss.tooling.ext.quality.ui/res/org/fortiss/tooling/ext/quality/ui/view/fx/MetricsViewFx.fxml b/org.fortiss.tooling.ext.quality.ui/res/org/fortiss/tooling/ext/quality/ui/view/fx/MetricsViewFx.fxml new file mode 100644 index 0000000000000000000000000000000000000000..ebb44aa74b0b18837f664176ca3b4774c9ecc180 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/res/org/fortiss/tooling/ext/quality/ui/view/fx/MetricsViewFx.fxml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<?import javafx.scene.chart.BarChart?> +<?import javafx.scene.chart.CategoryAxis?> +<?import javafx.scene.chart.NumberAxis?> +<?import javafx.scene.chart.PieChart?> +<?import javafx.scene.chart.ScatterChart?> +<?import javafx.scene.control.ChoiceBox?> +<?import javafx.scene.control.SplitPane?> +<?import javafx.scene.control.Tab?> +<?import javafx.scene.control.TabPane?> +<?import javafx.scene.layout.AnchorPane?> +<?import javafx.scene.layout.BorderPane?> +<?import javafx.scene.layout.HBox?> +<?import javafx.scene.text.Text?> + +<SplitPane fx:id="metricsSplitPane" dividerPositions="0.5" maxHeight="1.7976931348623157E308" maxWidth="1.7976931348623157E308" minHeight="-Infinity" minWidth="-Infinity" orientation="VERTICAL" prefHeight="400.0" prefWidth="1035.0" xmlns="http://javafx.com/javafx/13.0.2" xmlns:fx="http://javafx.com/fxml/1"> + <items> + <TabPane prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE"> + <tabs> + <Tab text="Pie and Spider"> + <content> + <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0"> + <children> + <SplitPane dividerPositions="0.5" layoutX="56.0" layoutY="-19.0" prefHeight="134.0" prefWidth="1033.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <items> + <BorderPane prefHeight="200.0" prefWidth="200.0"> + <center> + <PieChart fx:id="pieChart" prefHeight="184.0" prefWidth="1033.0" BorderPane.alignment="CENTER" /> + </center> + <top> + <ChoiceBox fx:id="childMetricChoiceBox" onAction="#onChildMetricChoiceBoxChange" prefHeight="24.0" prefWidth="270.0" BorderPane.alignment="CENTER" /> + </top> + </BorderPane> + <BorderPane fx:id="borderPane" prefHeight="158.0" prefWidth="81.0" /> + </items> + </SplitPane> + </children> + </AnchorPane> + </content> + </Tab> + <Tab text="Scatter and Bar"> + <content> + <AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="180.0" prefWidth="200.0"> + <children> + <SplitPane dividerPositions="0.5" prefHeight="336.0" prefWidth="1033.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"> + <items> + <BorderPane prefHeight="200.0" prefWidth="200.0"> + <center> + <ScatterChart fx:id="scatterChart" prefHeight="322.0" prefWidth="540.0" BorderPane.alignment="CENTER"> + <xAxis> + <NumberAxis side="BOTTOM" /> + </xAxis> + <yAxis> + <NumberAxis side="LEFT" /> + </yAxis> + </ScatterChart> + </center> + <top> + <HBox BorderPane.alignment="CENTER"> + <children> + <ChoiceBox fx:id="yScatterMetricChoiceBox" onAction="#onScatterMetricChoiceBoxChange" prefHeight="24.0" prefWidth="270.0" /> + <ChoiceBox fx:id="xScatterMetricChoiceBox" onAction="#onScatterMetricChoiceBoxChange" prefHeight="24.0" prefWidth="270.0" /> + </children> + </HBox> + </top> + </BorderPane> + <BarChart fx:id="barChart"> + <xAxis> + <CategoryAxis side="BOTTOM" /> + </xAxis> + <yAxis> + <NumberAxis side="LEFT" /> + </yAxis> + </BarChart> + </items> + </SplitPane> + </children> + </AnchorPane> + </content> + </Tab> + </tabs> + </TabPane> + <Text fx:id="bottomText" strokeType="OUTSIDE" strokeWidth="0.0" text="Save model to refresh" /> + </items> +</SplitPane> diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/.ratings b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..cbf0a8d86d8afb41340358f374a3dd30fb080d1e --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/.ratings @@ -0,0 +1,2 @@ +ModelQualityExtractionMenu.java d9282c7df44aa9461ec662beb557e74589f6b05f GREEN +ModelQualityUIActivator.java 2f47db82bc088072dbfe53f82d3ed417d77fd23c GREEN 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 new file mode 100644 index 0000000000000000000000000000000000000000..d9282c7df44aa9461ec662beb557e74589f6b05f --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityExtractionMenu.java @@ -0,0 +1,91 @@ +/*-------------------------------------------------------------------------+ +| 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.ui; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import static org.fortiss.tooling.kernel.ui.service.IContextMenuService.BOTTOM_MOST_MENU_SECTION_ID; + +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.ActionContributionItem; +import org.eclipse.jface.action.IContributionItem; +import org.eclipse.jface.resource.ImageDescriptor; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.fortiss.tooling.kernel.extension.data.ITopLevelElement; +import org.fortiss.tooling.kernel.service.IPersistencyService; +import org.fortiss.tooling.kernel.ui.extension.IContextMenuContributor; +import org.fortiss.tooling.kernel.ui.extension.data.ContextMenuContextProvider; + +/** + * Creates a context menu entry to extract metrics for the selected model. + * + * @author groh + */ +public class ModelQualityExtractionMenu implements IContextMenuContributor { + + /** String appearing in the context menu. */ + private static final String MENU_NAME = "Extract metrics"; + + /** {@inheritDoc} */ + @Override + public List<IContributionItem> getContributedItems(EObject selection, + ContextMenuContextProvider contextProvider) { + + if(!(selection.eContainer() != null)) { + return emptyList(); + } + + // A null eContainer indicates the model root + return asList(new IContributionItem[] { + new ActionContributionItem(new MetricExtractionAction(selection))}); + } + + /** {@inheritDoc} */ + @Override + public String getMenuSectionID() { + return BOTTOM_MOST_MENU_SECTION_ID; + } + + /** Returns the icon that is visible in the context menu for this entry. */ + protected ImageDescriptor getActionIcon() { + return ModelQualityUIActivator.getImageDescriptor("icons/metric.png"); + } + + /** Action for generating the set of . */ + protected class MetricExtractionAction extends Action { + + /** Model on which the action will be triggered. */ + private final EObject model; + + /** Constructor. */ + public MetricExtractionAction(EObject model) { + super(MENU_NAME, getActionIcon()); + this.model = model; + } + + /** {@inheritDoc} */ + @Override + public void run() { + ITopLevelElement top = + IPersistencyService.getInstance().getTopLevelElementFor(this.model); + IModelQualityService.getInstance().scheduleMetricCollection(top); + } + } +} diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityUIActivator.java b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityUIActivator.java new file mode 100644 index 0000000000000000000000000000000000000000..2f47db82bc088072dbfe53f82d3ed417d77fd23c --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/ModelQualityUIActivator.java @@ -0,0 +1,75 @@ +/*-------------------------------------------------------------------------+ +| 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.ui; + +import static org.eclipse.jface.resource.ResourceLocator.imageDescriptorFromBundle; + +import org.eclipse.core.runtime.Plugin; +import org.eclipse.jface.resource.ImageDescriptor; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.fortiss.tooling.ext.quality.ui.view.fx.ModelQualityFXViewPart; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle. + * + * @author blaschke + */ +public class ModelQualityUIActivator extends Plugin { + + /** The plug-in ID. */ + public static final String PLUGIN_ID = ModelQualityUIActivator.class.getPackage().getName(); // $NON-NLS-1$ + + /** The shared instance. */ + private static ModelQualityUIActivator plugin; + + /** {@inheritDoc} */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + IModelQualityService.getInstance() + .registerMetricUpdateListener(ModelQualityFXViewPart.getMetricsFXController()); + System.out.println("[Plugin] " + PLUGIN_ID + " started."); + } + + /** {@inheritDoc} */ + @Override + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + /** + * Returns the shared instance. + * + * @return The shared instance of {@link ModelQualityUIActivator} + */ + public static ModelQualityUIActivator getDefault() { + return plugin; + } + + /** + * Returns the image descriptor for the given icon file. + * + * @param path + * The path to the icon file of the image descriptor + * @return The image descriptor + */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromBundle(PLUGIN_ID, path).orElse(null); + } +} diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/.ratings b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..988b0e6731a953fd80ba5a76c6065f0e87018e42 --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/.ratings @@ -0,0 +1,3 @@ +IModelQualityViewPart.java 708f8089645df12098ea67190805cce343045d2e GREEN +ModelQualityFXController.java c5577d2762eb916835386de5987109fc7899f1bc GREEN +ModelQualityFXViewPart.java 9cfc7f60a86ea5b915c726b16712f8be7dec2c5f GREEN diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/IModelQualityViewPart.java b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/IModelQualityViewPart.java new file mode 100644 index 0000000000000000000000000000000000000000..708f8089645df12098ea67190805cce343045d2e --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/IModelQualityViewPart.java @@ -0,0 +1,28 @@ +/*-------------------------------------------------------------------------+ +| 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.ui.view.fx; + +/** + * Interface for views displaying the Metrics view. + * + * @author groh + */ +public interface IModelQualityViewPart { + + /** View ID for model quality views. */ + // Keep in sync with plugin.xml + public static String MODEL_QUALITY_VIEW_ID = "org.fortiss.tooling.ext.quality.ui.metricsView"; +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..c5577d2762eb916835386de5987109fc7899f1bc --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXController.java @@ -0,0 +1,458 @@ +/*-------------------------------------------------------------------------+ +| 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.ui.view.fx; + +import static java.util.stream.Collectors.toList; +import static javafx.scene.paint.Color.BLUE; +import static javafx.scene.paint.Color.DARKGRAY; +import static javafx.scene.paint.Color.LIGHTGRAY; +import static org.fortiss.tooling.common.ui.javafx.style.FontStyle.BLACK_VERDANA_10PT; +import static org.fortiss.tooling.common.ui.javafx.style.FontStyle.BLACK_VERDANA_12PT; +import static org.fortiss.tooling.common.ui.javafx.style.FontStyle.BLACK_VERDANA_14PT; +import static org.fortiss.tooling.common.ui.javafx.style.FontStyle.BLACK_VERDANA_8PT; +import static org.fortiss.tooling.common.ui.javafx.style.LineStyle.SOLID_BLACK_1PT; +import static org.fortiss.tooling.kernel.ui.util.SelectionUtils.checkAndPickFirst; +import static org.fortiss.tooling.kernel.utils.EcoreUtils.getFirstParentWithType; + +import java.text.DecimalFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.ui.ISelectionListener; +import org.eclipse.ui.IWorkbenchPart; +import org.fortiss.tooling.common.ui.javafx.layout.CompositeFXControllerBase; +import org.fortiss.tooling.common.ui.javafx.style.FillStyle; +import org.fortiss.tooling.common.ui.javafx.style.FontStyle; +import org.fortiss.tooling.common.ui.javafx.style.LineStyle; +import org.fortiss.tooling.ext.quality.IMetricUpdateListener; +import org.fortiss.tooling.ext.quality.data.MetricData; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.ext.quality.data.MetricTreeNode; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.fortiss.tooling.kernel.model.IProjectRootElement; +import org.fortiss.tooling.spiderchart.control.SpiderChartViewer; +import org.fortiss.tooling.spiderchart.model.DataSeries; +import org.fortiss.tooling.spiderchart.model.DoubleAxis; +import org.fortiss.tooling.spiderchart.model.SpiderChart; +import org.fortiss.tooling.spiderchart.style.AxisStyle; +import org.fortiss.tooling.spiderchart.style.ChartStyle; +import org.fortiss.tooling.spiderchart.style.DataSeriesStyle; +import org.fortiss.tooling.spiderchart.style.LegendStyle; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.chart.BarChart; +import javafx.scene.chart.PieChart; +import javafx.scene.chart.ScatterChart; +import javafx.scene.chart.XYChart; +import javafx.scene.control.ChoiceBox; +import javafx.scene.control.Label; +import javafx.scene.control.SplitPane; +import javafx.scene.layout.BorderPane; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import javafx.util.Pair; + +/** + * FX Controller for the Metrics view. + * + * @author groh + */ + +@SuppressWarnings("unchecked") +public class ModelQualityFXController extends CompositeFXControllerBase<SplitPane, Node> + implements ISelectionListener, IMetricUpdateListener { + + /** + * Selection of colors for the overlays in the spider chart. The length of this array also + * limits the depth of the {@link ModelQualityFXController#lastViewedElements} list. + */ + private static Color[] SPIDER_CHART_OVERLAY_COLORS = {Color.GREEN, Color.BLUE, Color.RED}; + + /** {@link ChoiceBox} for selecting the metric which shall be displayed. */ + @FXML + private ChoiceBox<MetricKey> childMetricChoiceBox; + + /** {@link PieChart} displaying the metrics. */ + @FXML + private PieChart pieChart; + + /** The bottom {@link BorderPane} for the spider chart. */ + @FXML + private BorderPane borderPane; + + /** {@link ChoiceBox} for selecting the metric displayed on the x axis of the scatter chart. */ + @FXML + private ChoiceBox<MetricKey> xScatterMetricChoiceBox; + + /** {@link ChoiceBox} for selecting the metric displayed on the y axis of the scatter chart. */ + @FXML + private ChoiceBox<MetricKey> yScatterMetricChoiceBox; + + /** A {@link ScatterChart} for displaying metrics. */ + @FXML + private ScatterChart<Number, Number> scatterChart; + + /** A {@link BarChart} for displaying metrics. */ + @FXML + private BarChart<String, Number> barChart; + + /** {@link Text} at the bottom of the view. */ + @FXML + private Text bottomText; + + /** Time format when displaying updating time. */ + private final DateTimeFormatter timeFormat = DateTimeFormatter.ofPattern("HH:mm:ss"); + + /** + * The last {@link EObject} which the user has clicked. Might be null when no or an invalid + * object is selected. + */ + private EObject lastSelectedElement; + + /** List of elements for which metrics were displayed. */ + private List<Pair<MetricTreeNode, Set<MetricKey>>> lastViewedElements = new ArrayList<>(); + + /** {@inheritDoc} */ + @Override + public String getFXMLLocation() { + return "MetricsViewFx.fxml"; + } + + /** {@inheritDoc} */ + @Override + public void initialize() { + // TODO (#4335) + childMetricChoiceBox.getItems().addAll(MetricKey.values()); + xScatterMetricChoiceBox.getItems().addAll(MetricKey.values()); + yScatterMetricChoiceBox.getItems().addAll(MetricKey.values()); + } + + /** {@inheritDoc} */ + @Override + public void selectionChanged(IWorkbenchPart part, ISelection selection) { + + // Invalidate last selection, so if no new selection is found the charts are cleared + lastSelectedElement = null; + + EObject selected = checkAndPickFirst(selection, EObject.class); + + if(selected == null) { + updateCharts("Nothing selected."); + return; + } + + IProjectRootElement currentRootElement = + selected instanceof IProjectRootElement ? (IProjectRootElement)selected + : getFirstParentWithType(selected, IProjectRootElement.class); + + if(currentRootElement == null) { + updateCharts("Selection does not have a project root element."); + return; + } + + MetricData metricData = IModelQualityService.getInstance().getMetricData(); + var rootDataElement = metricData.getDataRootElementMap().get(currentRootElement); + + if(rootDataElement == null) { + bottomText.setText( + "Save model or select \"Export Metrics\" in project options to generate metrics."); + updateCharts("No metrics available"); + return; + } + + bottomText.setText("Metrics last updated: " + + timeFormat.format(rootDataElement.getLastRefresh()) + ". Save model to refresh."); + lastSelectedElement = selected; + updateCharts("Selection does not have metric information."); + } + + /** Triggered when the choice box changes. */ + public void onChoiceBoxChange() { + updateCharts("Invalid selection or metrics not available."); + } + + /** Triggered when the child metric choice box changes. */ + public void onChildMetricChoiceBoxChange() { + updateCharts("Invalid selection or metrics not available."); + } + + /** Triggered when one of the scatter choice boxes changes. */ + public void onScatterMetricChoiceBoxChange() { + updateCharts("Invalid selection or metrics not available."); + } + + /** + * Updates the chart in the metric view. + * + * @param message + * to display if metric is not available + */ + private void updateCharts(String message) { + + if(lastSelectedElement != null) { + MetricData metricData = IModelQualityService.getInstance().getMetricData(); + MetricTreeNode node = metricData.getTreeNodeLookupTable().get(lastSelectedElement); + + if(node != null) { + if(!node.getChildren().isEmpty()) { + + updatePieChart(node); + updateScatterChart(node); + updateBarChart(node); + + updateLastViewedElements(node); + SpiderChartViewer viewer = buildSpiderChart(); + borderPane.setCenter(viewer.getViewerPane()); + + // Return, otherwise the data will be cleared + return; + } + message = "Selection does not have contained elements."; + } else { + message = "Selection does not have metric information."; + } + } else { + // Nothing selected, so remove the bottom text + bottomText.setText(""); + } + borderPane.setCenter(new Label(message)); + pieChart.getData().clear(); + } + + /** + * Updates the {@link #pieChart} with data from the provided {@link MetricTreeNode}. + * + * @param node + * which provides the data for this chart + */ + private void updatePieChart(MetricTreeNode node) { + MetricKey selectedMetric = childMetricChoiceBox.getValue(); + if(selectedMetric != null) { + + // Create PieChart and add data + pieChart.getData().clear(); + pieChart.setLegendVisible(false); + pieChart.setTitle(""); + DecimalFormat format = new DecimalFormat("0.#"); + + for(var child : node.getChildren()) { + Double value = child.getValueAsDouble(selectedMetric); + if(value != null) { + PieChart.Data slice = + new PieChart.Data(child.getName() + ": " + format.format(value), value); + pieChart.getData().add(slice); + } + } + } + } + + /** + * Updates the {@link #scatterChart} with data from the provided {@link MetricTreeNode}. + * + * @param node + * which provides the data for this chart + */ + private void updateScatterChart(MetricTreeNode node) { + MetricKey xScatterKey = xScatterMetricChoiceBox.getValue(); + MetricKey yScatterKey = yScatterMetricChoiceBox.getValue(); + if(xScatterKey != null && yScatterKey != null) { + + scatterChart.getXAxis().setLabel(xScatterKey.toString()); + scatterChart.getYAxis().setLabel(yScatterKey.toString()); + scatterChart.getData().clear(); + + for(var child : node.getChildren()) { + Double xValue = child.getValueAsDouble(xScatterKey); + Double yValue = child.getValueAsDouble(yScatterKey); + + if(xValue != null && yValue != null) { + // Add data to the chart + XYChart.Series<Number, Number> dataSeries = new XYChart.Series<>(); + dataSeries.setName(child.getName()); + dataSeries.getData().add(new XYChart.Data<>(xValue, yValue)); + scatterChart.getData().add(dataSeries); + } + } + } + } + + /** + * Updates the {@link #barChart} with data from the provided {@link MetricTreeNode}. + * + * @param node + * which provides the data for this chart + */ + private void updateBarChart(MetricTreeNode node) { + MetricKey yScatterKey = yScatterMetricChoiceBox.getValue(); + if(yScatterKey != null) { + + barChart.getYAxis().setLabel(yScatterKey.toString()); + barChart.getData().clear(); + + for(var child : node.getChildren()) { + Double yValue = child.getValueAsDouble(yScatterKey); + + if(yValue != null) { + // Add data to the chart + XYChart.Series<String, Number> dataSeries = new XYChart.Series<>(); + dataSeries.setName(child.getName()); + dataSeries.getData().add(new XYChart.Data<>(child.getName(), yValue)); + barChart.getData().add(dataSeries); + } + } + } + } + + /** + * Creates a SpiderChart using the elements in + * {@link ModelQualityFXController#lastViewedElements}. A set of keys which are present in all + * elements in the list is created, and then these elements are overlaid. + */ + private SpiderChartViewer buildSpiderChart() { + // Calculate the intersection of the keys, i.e. a set of keys which have a non-null value in + // all elements of the lastViewedElements list + + // lastViewedElements is never empty due to the fact, that updateLastViewedElements() is + // always executed before in updateCharts(), which ensure that there is at least one element + Set<MetricKey> keysIntersection = lastViewedElements.get(0).getValue(); + for(var p : lastViewedElements) { + keysIntersection.retainAll(p.getValue()); + } + + // Create DataSeries for all elements + List<Pair<MetricTreeNode, DataSeries>> dataSeriesList = + lastViewedElements.stream().map(pair -> { + MetricTreeNode key = pair.getKey(); + String name = key.getName(); + return new Pair<>(key, (name == null) ? new DataSeries("No name found") + : new DataSeries(name)); + }).collect(toList()); + + // Create SpiderChart and set basic styling + SpiderChart spiderChart = new SpiderChart(); + spiderChart.setLegendLabel("Legend"); + ChartStyle chartStyle = new ChartStyle(true, true, true); + + chartStyle.setUseIndividualAxisSegments(false); + chartStyle.setTitleStyle(new FontStyle("Verdana", 14, BLUE.brighter())); + + // Iterate over all keys, and add data points + for(MetricKey key : keysIntersection) { + // Calculate maximumValue for the range of the axis + Double maxmiumValue = lastViewedElements.stream() + .mapToDouble(p -> p.getKey().getValueAsDouble(key)).max().getAsDouble(); + + if(maxmiumValue == 0.0) { + // Don't display metrics where all values are zero + continue; + } + + // Create Axis and apply Styling + DoubleAxis doubleAxis = new DoubleAxis(key.toString(), 0.0, maxmiumValue); + AxisStyle aStyle3Segs = new AxisStyle(SOLID_BLACK_1PT, BLACK_VERDANA_14PT, 3, + BLACK_VERDANA_8PT, new DecimalFormat("#.##")); + chartStyle.setAxisStyle(doubleAxis, aStyle3Segs); + + spiderChart.addAxis(doubleAxis); + // Add the data point for this axis to all elements + for(var pair : dataSeriesList) { + pair.getValue().setPoint(doubleAxis, pair.getKey().getValueAsDouble(key)); + } + } + + // Iterate over all dataSeries and add them to the spider chart + int i = 0; + for(var pair : dataSeriesList) { + spiderChart.addData(pair.getValue()); + // Assign different colors to different data series + // updateLastViewedElements() ensures that the are are at most three elements in + // updateLastViewedElements, from which dataSeriesList is derived above. + Color color = SPIDER_CHART_OVERLAY_COLORS[i]; + LineStyle olive1pt = new LineStyle(color); + FillStyle oliveFill = new FillStyle(color, 0.25); + DataSeriesStyle style = new DataSeriesStyle(olive1pt, oliveFill, true, true, + BLACK_VERDANA_10PT, 7, new DecimalFormat("#.#")); + chartStyle.setDataSeriesStyle(pair.getValue(), style); + i++; + } + + // Configure legend + LegendStyle legendStyle = new LegendStyle(false, 5, BLACK_VERDANA_12PT); + chartStyle.setLegendStyle(legendStyle); + chartStyle.setAxisSegments(4); + chartStyle.setBackgroundFillStyle(new FillStyle(LIGHTGRAY)); + chartStyle.setBackgroundLineStyle(new LineStyle(DARKGRAY)); + + SpiderChartViewer viewer = new SpiderChartViewer(spiderChart, chartStyle); + return viewer; + } + + /** {@inheritDoc} */ + @Override + public void metricUpdate(IProjectRootElement updatedElement) { + Platform.runLater(() -> { + if(lastSelectedElement == null) { + // If nothing is selected, there is nothing to update + return; + } + + MetricData metricData = IModelQualityService.getInstance().getMetricData(); + var rootDataElement = metricData.getDataRootElementMap().get(updatedElement); + if(rootDataElement != null) { + bottomText.setText("Metrics last updated: " + + timeFormat.format(rootDataElement.getLastRefresh()) + + ". Save model to refresh."); + } else { + bottomText.setText( + "Save model or select \"Export Metrics\" in project options to generate metrics."); + } + updateCharts("Selection does not have metric information."); + }); + } + + /** + * Updates the {@link ModelQualityFXController#lastViewedElements} variable. It will add the + * provided {@link MetricTreeNode} in the back of the list, and if the list is too long it will + * remove the first element. + * + * @param node + * which was selected + */ + private void updateLastViewedElements(MetricTreeNode node) { + + // Create set of keys which store values in the node + Set<MetricKey> keys = new HashSet<>(); + keys.addAll(node.getDoubleMetrics().keySet()); + keys.addAll(node.getIntegerMetrics().keySet()); + + // Check if the node is already part of the list, and skip adding it if it is + if(lastViewedElements.stream().allMatch(p -> p.getKey() != node)) { + lastViewedElements.add(new Pair<>(node, keys)); + } + + // Limit the amount of elements in the list to the amount of colors + if(lastViewedElements.size() > SPIDER_CHART_OVERLAY_COLORS.length) { + lastViewedElements.remove(0); + } + } +} diff --git a/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXViewPart.java b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXViewPart.java new file mode 100644 index 0000000000000000000000000000000000000000..9cfc7f60a86ea5b915c726b16712f8be7dec2c5f --- /dev/null +++ b/org.fortiss.tooling.ext.quality.ui/src/org/fortiss/tooling/ext/quality/ui/view/fx/ModelQualityFXViewPart.java @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------+ +| 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.ui.view.fx; + +import org.eclipse.ui.part.ViewPart; +import org.fortiss.tooling.common.ui.javafx.AF3FXViewPart; + +import javafx.scene.Scene; + +/** + * {@link ViewPart} for the FX implementation of the metrics view. + * + * @author groh + */ +public class ModelQualityFXViewPart extends AF3FXViewPart implements IModelQualityViewPart { + + /** The FX Controller for this view. */ + private static final ModelQualityFXController VIEW_CONTROLLER = new ModelQualityFXController(); + + /** Constructor. */ + public ModelQualityFXViewPart() throws Exception { + super(VIEW_CONTROLLER, null); + } + + /** + * Returns the initialized {@link ModelQualityFXController} for the quality + * version of the {@link AF3FXViewPart}. + * + * @return The view controller for the metrics view + */ + public static ModelQualityFXController getMetricsFXController() { + return VIEW_CONTROLLER; + } + + /** {@inheritDoc} */ + @Override + protected Scene createFxScene() { + Scene scene = super.createFxScene(); + getSite().getWorkbenchWindow().getSelectionService().addSelectionListener(VIEW_CONTROLLER); + + return scene; + } +} diff --git a/org.fortiss.tooling.ext.quality/.classpath b/org.fortiss.tooling.ext.quality/.classpath new file mode 100644 index 0000000000000000000000000000000000000000..1a2f6f5de91bf0eb28fe3bf922046cd5055e3627 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/.classpath @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<classpath> + <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-11"/> + <classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/> + <classpathentry kind="src" path="src"/> + <classpathentry kind="output" path="build"/> +</classpath> diff --git a/org.fortiss.tooling.ext.quality/.project b/org.fortiss.tooling.ext.quality/.project new file mode 100644 index 0000000000000000000000000000000000000000..fa567514166b5b95143adf3d2c9e60ef00c77caf --- /dev/null +++ b/org.fortiss.tooling.ext.quality/.project @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="UTF-8"?> +<projectDescription> + <name>org.fortiss.tooling.ext.quality</name> + <comment></comment> + <projects> + </projects> + <buildSpec> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.emfgeneration.git.EcoreBuilderGIT</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.jdt.core.javabuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.ManifestBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.pde.SchemaBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.CodeReviewBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.RemoveWarningsBuilder</name> + <arguments> + </arguments> + </buildCommand> + <buildCommand> + <name>org.eclipse.systemfocus.tooling.codereview.builder.GuidelinesCheckBuilder</name> + <arguments> + </arguments> + </buildCommand> + </buildSpec> + <natures> + <nature>org.eclipse.pde.PluginNature</nature> + <nature>org.eclipse.jdt.core.javanature</nature> + <nature>org.eclipse.systemfocus.tooling.codereview.nature.CodeReviewNature</nature> + </natures> +</projectDescription> diff --git a/org.fortiss.tooling.ext.quality/.settings b/org.fortiss.tooling.ext.quality/.settings new file mode 160000 index 0000000000000000000000000000000000000000..310d1c04f28f6252d5a02dd8fde1b76ae4a4da51 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/.settings @@ -0,0 +1 @@ +Subproject commit 310d1c04f28f6252d5a02dd8fde1b76ae4a4da51 diff --git a/org.fortiss.tooling.ext.quality/META-INF/MANIFEST.MF b/org.fortiss.tooling.ext.quality/META-INF/MANIFEST.MF new file mode 100644 index 0000000000000000000000000000000000000000..730b39560b4c2ad54dd97284b1747ed12c47830e --- /dev/null +++ b/org.fortiss.tooling.ext.quality/META-INF/MANIFEST.MF @@ -0,0 +1,18 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Model Quality +Bundle-SymbolicName: org.fortiss.tooling.ext.quality;singleton:=true +Bundle-Version: 2.23.0.qualifier +Automatic-Module-Name: org.fortiss.tooling.ext.quality +Bundle-RequiredExecutionEnvironment: JavaSE-11 +Bundle-Vendor: fortiss GmbH +Bundle-Activator: org.fortiss.tooling.ext.quality.ModelQualityActivator +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.emf.ecore;visibility:=reexport, + org.fortiss.tooling.base;visibility:=reexport, + org.fortiss.tooling.kernel;visibility:=reexport +Export-Package: org.fortiss.tooling.ext.quality, + org.fortiss.tooling.ext.quality.data, + org.fortiss.tooling.ext.quality.service +Bundle-ActivationPolicy: lazy + diff --git a/org.fortiss.tooling.ext.quality/build.properties b/org.fortiss.tooling.ext.quality/build.properties new file mode 100644 index 0000000000000000000000000000000000000000..d45ffd2c580f438c11f0287a88f92b4df1e0cac5 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/build.properties @@ -0,0 +1,8 @@ +# (c) 2023 fortiss GmbH + +bin.includes = .,\ + META-INF/,\ + plugin.properties,\ + plugin.xml +source.. = src/ +output.. = build/ diff --git a/org.fortiss.tooling.ext.quality/html/developer/.ratings b/org.fortiss.tooling.ext.quality/html/developer/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..424f339e671bc7ed76131d24d7a85be5620d3670 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/html/developer/.ratings @@ -0,0 +1 @@ +documentation.html b66c2bfb495047a8e4b680b02231ab2fdb34cec3 GREEN diff --git a/org.fortiss.tooling.ext.quality/html/developer/documentation.html b/org.fortiss.tooling.ext.quality/html/developer/documentation.html new file mode 100644 index 0000000000000000000000000000000000000000..b66c2bfb495047a8e4b680b02231ab2fdb34cec3 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/html/developer/documentation.html @@ -0,0 +1,161 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> +<html> +<head> +</head> + +<body lang="en-US" dir="ltr"> + +<h1>Developer Documentation for <i>Quality Functionality +(</i><i>org.fortiss.tooling.ext.quality</i><i>)</i></h1> + +<h2>Metrics List </h2> +<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> + +<table border="1"> + <thead> + <tr> + <th>Provider</th> + <th>Qualified Name</th> + <th>Metric</th> + <th>Type</th> + <th>Description</th> + </tr> + </thead> + <tbody> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>UNIQUE_ID</td> + <td>Integer</td> + <td>This is an unique identifier for each element as specified in org.fortiss.tooling.kernel.model.IIdLabeled.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_CONNECTORS</td> + <td>Integer</td> + <td>Number of connectors in the respective element.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_CONTAINED_ELEMENTS</td> + <td>Integer</td> + <td>Number of entries in the containedElements list.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_CONNECTIONS</td> + <td>Integer</td> + <td>Number of connections in this element. List of aggregated connection model elements. Usually aggregates all connections of its direct sub-structure.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_CONNECTORS</td> + <td>Integer</td> + <td>Total number of connectors in this element and all contained elements.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_ENTRY_CONNECTORS</td> + <td>Integer</td> + <td>Sum of all connectors which implement org.fortiss.tooling.base.model.base.EntryConnectorBase.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_EXIT_CONNECTORS</td> + <td>Integer</td> + <td>Sum of all connectors which implement org.fortiss.tooling.base.model.base.ExitConnectorBase. Usually, the sum over this and NUMBER_OF_TOTAL_ENTRY_CONNECTORS should equal NUMBER_OF_TOTAL_CONNECTORS.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_ELEMENTS</td> + <td>Integer</td> + <td>Total number of elements contained in this element. Includes the element for which this metric is recorded.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_COMMENTABLE_ELEMENTS</td> + <td>Integer</td> + <td>Total number of elements implementing org.fortiss.tooling.kernel.model.INamedCommentedElement in this element. Includes the element for which this metric is recorded. Never exceeds NUMBER_OF_TOTAL_ELEMENTS.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_COMMENTED_ELEMENTS</td> + <td>Integer</td> + <td>Total number of elements which are commentable and do not have null or an empty string as a comment.</td> + </tr> + <tr> + <td>HierarchicElementMetricProvider</td> + <td>org.fortiss.tooling.ext.quality.HierarchicElementMetricProvider</td> + <td>NUMBER_OF_TOTAL_LEAF_ELEMENTS</td> + <td>Integer</td> + <td>Total number of elements contained in this element which do not contain elements. Includes self.</td> + </tr> + <tr> + <td>GraphMetricsProvider</td> + <td>org.fortiss.tooling.ext.quality.GraphMetricsProvider</td> + <td>BETWEENESS_CENTRALITY</td> + <td>Double</td> + <td>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 neighbors of these elements.</td> + </tr> + <tr> + <td>GraphMetricsProvider</td> + <td>org.fortiss.tooling.ext.quality.GraphMetricsProvider</td> + <td>BETWEENESS_CENTRALITY_RECURSIVELY</td> + <td>Double</td> + <td>Same as above, but all connections are resolved to leaf elements, i.e. elements which do not contain other elements.</td> + </tr> + <tr> + <td>GraphMetricsProvider</td> + <td>org.fortiss.tooling.ext.quality.GraphMetricsProvider</td> + <td>CLUSTERING_COEFFICIENT</td> + <td>Double</td> + <td>Clustering coefficient of this element. Ignores connection direction and resolves all neighbors to leaf elements.</td> + </tr> + <tr> + <td>ModelQualityService</td> + <td>org.fortiss.tooling.ext.quality.service.ModelQualityService</td> + <td>NESTING_LEVEL</td> + <td>Integer</td> + <td>The depth of this element. Defined by the number of elements which have to be traversed to reach this element from the root.</td> + </tr> + <tr> + <td>ModelQualityService</td> + <td>org.fortiss.tooling.ext.quality.service.ModelQualityService</td> + <td>CONSTRAINT_VIOLATIONS_ERROR</td> + <td>Integer</td> + <td>Number of constraint violations of the severity error which have this element as source.</td> + </tr> + <tr> + <td>ModelQualityService</td> + <td>org.fortiss.tooling.ext.quality.service.ModelQualityService</td> + <td>CONSTRAINT_VIOLATIONS_WARNING</td> + <td>Integer</td> + <td>Number of constraint violations of the severity warning which have this element as source.</td> + </tr> + </tbody> +</table> + +<h2></h2> + +The included packages are: +<ul> +<li><i>org.fortiss.tooling.ext.quality</i>: containing the GraphMetrics and HierarchicElement providers. The other providers are therefore located on the +necessary plugin level to support the extraction of meta information from other elements. All metric providers implement the IMetricProvider interface.</li> +<li><i>org.fortiss.tooling.ext.quality.data</i>: contains the classes to handle the data structure and map of metrics. It manages the data during runtime + so the visualizations can receive the necessary data.</li> +<li><i>org.fortiss.tooling.ext.quality.service</i>: The interface and implementation of the registered service.</li> +<li><i>org.fortiss.tooling.ext.quality.storage</i>: The classes here manage the csv extraction to archive the metrics.</li> +</ul> + +</body> +</html> \ No newline at end of file diff --git a/org.fortiss.tooling.ext.quality/plugin.properties b/org.fortiss.tooling.ext.quality/plugin.properties new file mode 100644 index 0000000000000000000000000000000000000000..c18cbbb30532298ac0bbdeb6f0fc43f0d544a09f --- /dev/null +++ b/org.fortiss.tooling.ext.quality/plugin.properties @@ -0,0 +1,4 @@ +<!-- (c) 2023 fortiss GmbH --> + +pluginName = Model Quality +providerName = fortiss GmbH diff --git a/org.fortiss.tooling.ext.quality/plugin.xml b/org.fortiss.tooling.ext.quality/plugin.xml new file mode 100644 index 0000000000000000000000000000000000000000..5535690d3778e11156a22397241826e5d5766009 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/plugin.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8"?> +<?eclipse version="3.4"?> +<plugin> + +</plugin> diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/.ratings b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..44fde8c402ed4300f3e45b7cf81b5dff1f3389ce --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/.ratings @@ -0,0 +1,5 @@ +GraphMetricsProvider.java ce70c838b887df0bdf776f462b23cf594c700822 GREEN +HierarchicElementMetricProvider.java 966b3aeef92475034fc35e68ba5344688988b88a GREEN +IMetricProvider.java 99fc8993b0e65b2f8757978eeb0481d912f5608c GREEN +IMetricUpdateListener.java c24dc7c0f282623bbf1eefac1fbbb6752c97ddf0 GREEN +ModelQualityActivator.java 6d5f0794aa48670cf45fb405bd7641bce6c1fec4 GREEN 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 0000000000000000000000000000000000000000..ce70c838b887df0bdf776f462b23cf594c700822 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/GraphMetricsProvider.java @@ -0,0 +1,371 @@ +/*-------------------------------------------------------------------------+ +| 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 static org.fortiss.tooling.kernel.utils.EcoreUtils.pickInstanceOf; + +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.MetricData; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.ext.quality.data.MetricTreeNode; + +/** + * Supplies methods to compute certain graph metrics. + * + * @author groh + */ + +// TODO (#4332) Restructure GraphMetricsProvider +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) { + // 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 : 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 : 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 : pickInstanceOf(ExitConnectorBase.class, + element.getConnectors())) { + recursivlyFollowOutgoingConnection(exitConnectors, neighbors); + } + for(EntryConnectorBase entryConnectors : pickInstanceOf(EntryConnectorBase.class, + element.getConnectors())) { + recursivlyFollowIncomingConnection(entryConnectors, neighbors); + } + return neighbors; + } + + /** + * Calculates the clustering coefficient based on the neighborhood. + * See, e.g., https://en.wikipedia.org/wiki/Clustering_coefficient + * Clustering coefficient C for a node V is counting the triangles divided by the "number of + * neighbors of "V" times "number of neighbors of V - 1" which results in the potential number + * of maximum links between the nodes + * + * @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). + } + // Iterate over neighbors and get their neighbors and check if they are also our neighbor in + // which case we have a triangle + 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. See, e.g., https://en.wikipedia.org/wiki/Betweenness_centrality + * + * @param scopeElement + * the scope of this calculation + * @param metricData + * location to save the metrics + * @param recursively + * if true, all calculation will be carried out on leaf elements + * if false, it is only computing the centrality with the children + * which means the inner circle. + */ + public static void calculateBetweennessCentrality(IHierarchicElement scopeElement, + MetricData metricData, boolean recursively) { + + // Abort if no elements are found + if(scopeElement.getContainedElements().isEmpty()) { + return; + } + + 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 with standard values + Stack<IHierarchicElement> stack = new Stack<>(); + Map<IHierarchicElement, List<IHierarchicElement>> predecessors = new HashMap<>(); + Map<IHierarchicElement, Integer> distance = new HashMap<>(); + Map<IHierarchicElement, Double> dependency = new HashMap<>(); + Queue<IHierarchicElement> queue = new LinkedList<>(); + + for(IHierarchicElement v : graphNodes) { + predecessors.put(v, new ArrayList<>()); + distance.put(v, -1); + dependency.put(v, 0.0); + } + dependency.put(elementNode, 1.0); + distance.put(elementNode, 0); + queue.add(elementNode); + + processCentrality(queue, stack, predecessors, distance, dependency, graphNodes, + elementNode, recursively); + + // 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 : predecessors.get(w)) { + double c = (dependency.get(v) / dependency.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 + saveMetric(scopeElement, metricData, recursively, betweenness); + } + + /** Saving the metrics for each contained element of an {@link IHierarchicElement}. */ + private static void saveMetric(IHierarchicElement scopeElement, MetricData metricData, + boolean recursively, Map<IHierarchicElement, Double> betweenness) { + MetricKey key = recursively ? MetricKey.BETWEENESS_CENTRALITY_RECURSIVELY + : MetricKey.BETWEENESS_CENTRALITY; + for(IHierarchicElement child : scopeElement.getContainedElements()) { + MetricTreeNode node = metricData.getTreeNodeLookupTable().get(child); + node.getDoubleMetrics().put(key, betweenness.get(child)); + } + } + + /** + * Performs intermediate calculations for the betweenness centrality algorithm. + * + * @param queue + * of nodes for Breadth-First Search. + * @param stack + * of nodes for Breadth-First Search. + * @param predecessor + * maps to lists used to keep track of the predecessors of nodes in the shortest + * paths. + * @param distance + * map used to store the shortest distances from the source node to other nodes. + * @param dependency + * map used to calculate and store the dependency values for nodes in the network. + * These values are part of the calculation for betweenness centrality. + * @param graphNodes + * list of nodes + * @param scopeElement + * the scope of this calculation + * @param recursively + * if true, all calculation will be carried out on leaf elements + */ + private static void processCentrality(Queue<IHierarchicElement> queue, + Stack<IHierarchicElement> stack, + Map<IHierarchicElement, List<IHierarchicElement>> predecessor, + Map<IHierarchicElement, Integer> distance, Map<IHierarchicElement, Double> dependency, + Set<IHierarchicElement> graphNodes, IHierarchicElement scopeElement, + boolean recursively) { + + while(!queue.isEmpty()) { + IHierarchicElement v = queue.remove(); + stack.push(v); + + // Iterate over all connectors going out from the element + for(ExitConnectorBase exitConnectors : 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)) { + var targetDistance = distance.get(targetElement); + var vDistance = distance.get(v); + // first start of distance calculation from v to the target Element + if(targetDistance < 0) { + queue.add(targetElement); + distance.put(targetElement, vDistance + 1); + } else if(targetDistance == vDistance + 1) { + dependency.put(targetElement, + dependency.get(targetElement) + dependency.get(v)); + predecessor.get(targetElement).add(v); + } + } + } + } + } + } + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementMetricProvider.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementMetricProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..966b3aeef92475034fc35e68ba5344688988b88a --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/HierarchicElementMetricProvider.java @@ -0,0 +1,91 @@ +/*-------------------------------------------------------------------------+ +| 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 static org.fortiss.tooling.kernel.utils.EcoreUtils.pickInstanceOf; + +import org.fortiss.tooling.base.model.base.EntryConnectorBase; +import org.fortiss.tooling.base.model.base.ExitConnectorBase; +import org.fortiss.tooling.base.model.element.IHierarchicElement; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.ext.quality.data.MetricTreeNode; +import org.fortiss.tooling.kernel.model.IIdLabeled; +import org.fortiss.tooling.kernel.model.INamedCommentedElement; + +/** + * {@link IMetricProvider} to collect various metrics from an {@link IHierarchicElement}. + * + * @author groh + */ +public class HierarchicElementMetricProvider implements IMetricProvider<IHierarchicElement> { + /** {@inheritDoc} */ + @Override + public void collectMetrics(MetricTreeNode node, IHierarchicElement currentElement) { + + var intMetrics = node.getIntegerMetrics(); + // Only collect these metrics if the name is null, in which case they have not been + // collected before + // This is used in some cases to combine metrics from two different IHierarchicElement + if(node.getName() == null) { + + // Metrics are always set to default value, as these metric are combined later + if(currentElement instanceof INamedCommentedElement) { + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_COMMENTABLE_ELEMENTS, 1); + INamedCommentedElement nce = (INamedCommentedElement)currentElement; + String comment = nce.getComment(); + if(comment != null && comment != "") { + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_COMMENTED_ELEMENTS, 1); + } + node.setName(nce.getName()); + } else { + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_COMMENTABLE_ELEMENTS, 0); + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_COMMENTED_ELEMENTS, 0); + node.setName("No Name existant"); + } + + if(currentElement instanceof IIdLabeled) { + intMetrics.put(MetricKey.UNIQUE_ID, ((IIdLabeled)currentElement).getId()); + } else { + intMetrics.put(MetricKey.UNIQUE_ID, -1); + } + } + + // connector metrics + var connectors = currentElement.getConnectors(); + intMetrics.put(MetricKey.NUMBER_OF_CONNECTORS, connectors.size()); + intMetrics.put(MetricKey.NUMBER_OF_CONTAINED_ELEMENTS, + currentElement.getContainedElements().size()); + intMetrics.put(MetricKey.NUMBER_OF_CONNECTIONS, currentElement.getConnections().size()); + + // depth metrics + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_CONNECTORS, connectors.size()); + + var entry_connectors = pickInstanceOf(EntryConnectorBase.class, connectors); + var exit_connectors = pickInstanceOf(ExitConnectorBase.class, connectors); + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_ENTRY_CONNECTORS, entry_connectors.size()); + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_EXIT_CONNECTORS, exit_connectors.size()); + + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_ELEMENTS, 1); + + if(currentElement.getContainedElements().isEmpty()) { + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_LEAF_ELEMENTS, 0); + } else { + intMetrics.put(MetricKey.NUMBER_OF_TOTAL_LEAF_ELEMENTS, 1); + } + + // graph metrics + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricProvider.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..99fc8993b0e65b2f8757978eeb0481d912f5608c --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricProvider.java @@ -0,0 +1,33 @@ +/*-------------------------------------------------------------------------+ +| 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 org.eclipse.emf.ecore.EObject; +import org.fortiss.tooling.ext.quality.data.MetricTreeNode; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.fortiss.tooling.kernel.service.base.IEObjectAware; + +/** + * Interface for all metric providers. + * They are called by {@link IModelQualityService} to collect metrics on model elements. + * + * @author blaschke + */ +public interface IMetricProvider<C extends EObject> extends IEObjectAware<EObject> { + + /** Applies the IMetricProvider to the given model element. */ + void collectMetrics(MetricTreeNode node, C element); +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricUpdateListener.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricUpdateListener.java new file mode 100644 index 0000000000000000000000000000000000000000..c24dc7c0f282623bbf1eefac1fbbb6752c97ddf0 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/IMetricUpdateListener.java @@ -0,0 +1,33 @@ +/*-------------------------------------------------------------------------+ +| 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 org.fortiss.tooling.kernel.model.IProjectRootElement; + +/** + * Listener getting called when metrics update + * + * @author groh + */ +public interface IMetricUpdateListener { + /** + * Callback for metric update. This method will get called every time the metrics are updated. + * + * @param updatedElement + * the corresponding root element which was updated + */ + public void metricUpdate(IProjectRootElement updatedElement); +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/ModelQualityActivator.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/ModelQualityActivator.java new file mode 100644 index 0000000000000000000000000000000000000000..6d5f0794aa48670cf45fb405bd7641bce6c1fec4 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/ModelQualityActivator.java @@ -0,0 +1,59 @@ +package org.fortiss.tooling.ext.quality; + +/*-------------------------------------------------------------------------+ +| 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. | ++--------------------------------------------------------------------------*/ + +import org.eclipse.core.runtime.Plugin; +import org.fortiss.tooling.base.model.element.IHierarchicElement; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle. + * + * @author blaschke + */ +public class ModelQualityActivator extends Plugin { + /** The plug-in ID. */ + public static final String PLUGIN_ID = ModelQualityActivator.class.getPackage().getName(); // $NON-NLS-1$ + + /** The shared instance. */ + private static ModelQualityActivator plugin; + + /** {@inheritDoc} */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + System.out.println("[Plugin] " + PLUGIN_ID + " started."); + IModelQualityService mqs = IModelQualityService.getInstance(); + mqs.startService(); + mqs.registerMetricProvider(new HierarchicElementMetricProvider(), + IHierarchicElement.class); + } + + /** {@inheritDoc} */ + @Override + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + /** Returns the shared instance. */ + public static ModelQualityActivator getDefault() { + return plugin; + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/.ratings b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..a66b729711906e63b7594c05e1e5bca1c281ef70 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/.ratings @@ -0,0 +1,4 @@ +DataRootElement.java 7b655236c155e3a4473301aa8c3ecfe769cf071c GREEN +MetricData.java b3c1a395cca29ced5845b789d76ee2c965ec66f6 GREEN +MetricKey.java d78026f0c68aeeff0cc758bbd76fbb84da65afd6 GREEN +MetricTreeNode.java 88ddbdfc62fb8efee71aa3ee44257d1a1a582fe6 GREEN diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/DataRootElement.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/DataRootElement.java new file mode 100644 index 0000000000000000000000000000000000000000..7b655236c155e3a4473301aa8c3ecfe769cf071c --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/DataRootElement.java @@ -0,0 +1,77 @@ +/*-------------------------------------------------------------------------+ +| 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.data; + +import java.time.LocalDateTime; + +import org.fortiss.tooling.kernel.extension.data.ITopLevelElement; + +/** + * Storage element corresponding to a {@Link IProjectRootElement}. It stores the last refresh time + * as {@link LocalDateTime}, its respective {@link ITopLevelElement} and the {@link MetricTreeNode} + * storing the data. + * + * @author groh + */ +public class DataRootElement { + /** The last time a metric tree was refreshed. */ + private LocalDateTime lastRefresh; + + /** The last time the metric tree was refreshed. */ + private ITopLevelElement topLevelElement; + + /** Root {@link MetricTreeNode} for the metric tree containing all data. */ + private MetricTreeNode rootNode; + + /** Constructor. */ + public DataRootElement(LocalDateTime lastRefresh, ITopLevelElement topLevelElement, + MetricTreeNode rootNode) { + super(); + this.lastRefresh = lastRefresh; + this.topLevelElement = topLevelElement; + this.rootNode = rootNode; + } + + /** Getter for {@link #lastRefresh}. */ + public LocalDateTime getLastRefresh() { + return lastRefresh; + } + + /** Setter for {@link #lastRefresh}. */ + public void setLastRefresh(LocalDateTime lastRefresh) { + this.lastRefresh = lastRefresh; + } + + /** Getter for {@link #topLevelElement}. */ + public ITopLevelElement getTopLevelElement() { + return topLevelElement; + } + + /** Setter for {@link #topLevelElement}. */ + public void setTopLevelElement(ITopLevelElement topLevelElement) { + this.topLevelElement = topLevelElement; + } + + /** Getter for {@link #rootNode}. */ + public MetricTreeNode getRootNode() { + return rootNode; + } + + /** Setter for {@link #rootNode}. */ + public void setRootNode(MetricTreeNode metricTreeNode) { + this.rootNode = metricTreeNode; + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricData.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricData.java new file mode 100644 index 0000000000000000000000000000000000000000..b3c1a395cca29ced5845b789d76ee2c965ec66f6 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricData.java @@ -0,0 +1,52 @@ +/*-------------------------------------------------------------------------+ +| 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.data; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.emf.ecore.EObject; +import org.fortiss.tooling.kernel.model.IProjectRootElement; + +/** + * Class storing the metrics computed for the all suitable elements. + * + * @author groh + */ +public class MetricData { + + /** A map to lookup the corresponding tree node for any {@link EObject}. */ + private Map<EObject, MetricTreeNode> treeNodeLookupTable; + + /** A map to lookup the corresponding root tree node for a project. */ + private Map<IProjectRootElement, DataRootElement> dataRootElementMap; + + /** Constructor. */ + public MetricData() { + dataRootElementMap = new HashMap<>(); + treeNodeLookupTable = new HashMap<>(); + } + + /** Getter for {@link #treeNodeLookupTable}. */ + public Map<EObject, MetricTreeNode> getTreeNodeLookupTable() { + return treeNodeLookupTable; + } + + /** Getter for {@link #dataRootElementMap}. */ + public Map<IProjectRootElement, DataRootElement> getDataRootElementMap() { + return dataRootElementMap; + } +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..d78026f0c68aeeff0cc758bbd76fbb84da65afd6 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java @@ -0,0 +1,183 @@ +/*-------------------------------------------------------------------------+ +| 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.data; + +import java.util.ArrayList; +import java.util.List; + +/** + * Enum class representing a key which can be used to store a respective metric. + * Metrics provided by the HierarchicElementProvider. + * + * @author groh + */ +public enum MetricKey { + + /** Unique ID of the element */ + UNIQUE_ID(false), + + /** Key for the metric counting ports. */ + NUMBER_OF_CONNECTORS(false), + /** Key for the metric counting elements. */ + NUMBER_OF_CONTAINED_ELEMENTS(false), + /** Key for the metric counting channels. */ + NUMBER_OF_CONNECTIONS(false), + // TODO (#4333): + /** Safety level of the element. */ + SAFETY_LEVEL(false, true), + /** How deeply nested the corresponding element is. */ + NESTING_LEVEL(false), + /** The betweenness centrality of the corresponding element. */ + BETWEENESS_CENTRALITY(false, 0.0), + /** + * The betweenness centrality of the corresponding element considering other elements + * recursively. + */ + BETWEENESS_CENTRALITY_RECURSIVELY(false, 0.0), + /** The clustering coefficient of the element considering its neighbors. */ + CLUSTERING_COEFFICIENT(false, 0.0), + + /** How many constraints this element violates with severity error. */ + CONSTRAINT_VIOLATIONS_ERROR(false), + /** How many constraints this element violates with severity warning. */ + CONSTRAINT_VIOLATIONS_WARNING(false), + // TODO(#4333) Remove knowledge about af3 from the kernel. + /** Number of allocated components to this task. */ + TASK_ALLOCATED_COMPONENTS(false), + // TODO (#4333): Remove knowledge about af3 from the kernel. + /** How many constraints this element violates with severity warning. */ + EXECUTION_UNIT_ALLOCATED_TASKS(false), + + // Metrics which are collected over all children nodes + /** Key for the metric counting the total amount of ports contained. */ + NUMBER_OF_TOTAL_CONNECTORS(true), + /** Key for the metric counting the total amount of input ports contained. */ + NUMBER_OF_TOTAL_ENTRY_CONNECTORS(true), + /** Key for the metric counting the total amount of output ports contained. */ + NUMBER_OF_TOTAL_EXIT_CONNECTORS(true), + /** Key for the metric counting the total amount of elements contained. */ + NUMBER_OF_TOTAL_ELEMENTS(true), + /** Key for the metric counting the total amount of elements which can be commented. */ + NUMBER_OF_TOTAL_COMMENTABLE_ELEMENTS(true), + /** Key for the metric counting the total amount of elements which are commented. */ + NUMBER_OF_TOTAL_COMMENTED_ELEMENTS(true), + /** + * Key for the metric counting the total amount of elements which do not contain other elements. + */ + NUMBER_OF_TOTAL_LEAF_ELEMENTS(true), + /** + * Key for the metric counting of the total amount of constant values in all code + * specifications. + */ + NUMBER_OF_TOTAL_CODE_CONSTANTS(true), + /** Cyclomatic complexity number as sum over all components. */ + NUMBER_OF_TOTAL_CODE_CYCLOMATIC_COMPLEXITY(true), + /** Counts the Lines of Code (LoC) in the component and its sub-components. */ + NUMBER_OF_TOTAL_CODE_LINES(true), + /** Representation of the total amount of comment blocks in all code specifications. */ + NUMBER_OF_TOTAL_CODE_COMMENTS(true); + + /** Metrics which are a combination from the data of multiple nodes */ + private static List<MetricKey> collectorKeys = new ArrayList<>(); + + /** Getter for {@link #collectorKeys}. */ + public static List<MetricKey> getCollectorKeys() { + if(collectorKeys == null) { + collectorKeys = new ArrayList<>(); + for(MetricKey key : MetricKey.values()) { + if(key.isCollectorKey) { + collectorKeys.add(key); + } + } + } + return collectorKeys; + } + + /** Used for populating the {@link MetricKey#getCollectorKeys()} method. */ + private boolean isCollectorKey; + + // TODO (4336): In MetricTreeNode, there are int, double, and String metrics. However, here are + // only doubles and strings. What about marking the type of a MetricKey with a + // Class<? extends Number> field? In the constructor, you can check that only the admissible + // types Integer, Double, String are passed. + /** Determines if the corresponding value to this key is a string or not */ + private boolean isStringValue; + /** Determines if the corresponding value to this key is a integer or not */ + private boolean isDoubleValue; + + /** + * Constructor for regular integer metric keys. + * + * @param isCollectorKey + * decides if this is a key returned by {@link MetricKey#getCollectorKeys()} + */ + private MetricKey(boolean isCollectorKey) { + this(isCollectorKey, false); + } + + /** + * Constructor for metric key storing a double. + * + * @param isCollectorKey + * decides if this is a key returned by {@link MetricKey#getCollectorKeys()} + * @param isDoubleValue + * regardless of argument will make the key to a double key + */ + private MetricKey(boolean isCollectorKey, double isDoubleValue) { + this.isCollectorKey = isCollectorKey; + this.isStringValue = false; + this.isDoubleValue = true; + } + + /** + * Constructor for metric key storing a string. + * + * @param isCollectorKey + * decides if this is a key returned by {@link MetricKey#getCollectorKeys()} + * @param isStringValue + * marks if the value associated with this key is a string + */ + private MetricKey(boolean isCollectorKey, boolean isStringValue) { + this.isCollectorKey = isCollectorKey; + this.isStringValue = isStringValue; + this.isDoubleValue = false; + } + + /** + * If this value is true, it should be expected that the value corresponding to this key should + * be a string and be stored in an appropriate place. + * + * @return if the value associated with this key is a string + */ + public boolean isStringValue() { + return isStringValue; + } + + /** + * If this value is true, it should be expected that the value corresponding to this key should + * be a integer and be stored in an appropriate place. + * + * @return if the value associated with this key is a integer + */ + public boolean isDoubleValue() { + return isDoubleValue; + } + + /** Get all collector keys. */ + static { + collectorKeys = getCollectorKeys(); + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricTreeNode.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricTreeNode.java new file mode 100644 index 0000000000000000000000000000000000000000..88ddbdfc62fb8efee71aa3ee44257d1a1a582fe6 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricTreeNode.java @@ -0,0 +1,119 @@ +/*-------------------------------------------------------------------------+ +| 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.data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +/** + * A general class for storing metric data in a hierarchical tree structure. + * <p> + * Allows for efficient storage and retrieval of data. + * + * @author groh + */ +public class MetricTreeNode { + + /** stores the children of this tree node */ + private List<MetricTreeNode> children; + + /** Map containing all metrics which are a Integer */ + private Map<MetricKey, Integer> integerMetrics; + + /** Map containing all metrics which are a Double */ + private Map<MetricKey, Double> doubleMetrics; + + /** Map containing all metrics which are a String */ + private Map<MetricKey, String> stringMetrics; + + /** name of the element which is associated with this data */ + private String name; + + /** Constructor. */ + public MetricTreeNode() { + children = new ArrayList<>(); + integerMetrics = new HashMap<>(); + doubleMetrics = new HashMap<>(); + stringMetrics = new HashMap<>(); + } + + /** Getter for {@link #children}. */ + public List<MetricTreeNode> getChildren() { + return children; + } + + /** Getter for {@link #name}. */ + public String getName() { + return name; + } + + /** Setter for {@link #name}. */ + public void setName(String name) { + this.name = name; + } + + /** Getter for {@link #integerMetrics}. */ + public Map<MetricKey, Integer> getIntegerMetrics() { + return integerMetrics; + } + + /** Getter for {@link #doubleMetrics}. */ + public Map<MetricKey, Double> getDoubleMetrics() { + return doubleMetrics; + } + + /** Getter for {@link #stringMetrics}. */ + public Map<MetricKey, String> getStringMetrics() { + return stringMetrics; + } + + /** + * Allows to retrieve allows to retrieve the value of a key which may be stored as double or + * integer type. + * In the latter case the value will automatically be converted into a double. + * + * @param key + * for which the value shall be retrieved + * @return the respective integer or double + */ + public Double getValueAsDouble(MetricKey key) { + if(key.isDoubleValue()) { + return doubleMetrics.get(key); + } + Integer val = integerMetrics.get(key); + return val == null ? null : val.doubleValue(); + } + + /** + * Traverses the tree in post-order. + * + * @param consumer + * which is applied to each node + */ + public void traverseTree(Consumer<MetricTreeNode> consumer) { + + // traverse all children + for(var child : getChildren()) { + child.traverseTree(consumer); + } + + // apply consumer to self + consumer.accept(this); + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/.ratings b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..8f58ff0101b51c912f7b21b9e889a52ed5bdb52e --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/.ratings @@ -0,0 +1,2 @@ +IModelQualityService.java db237db72cf1a0905fa82b1dfd799f830d092a10 GREEN +ModelQualityService.java 6337c64d1a948fecd938726c88b9fc7d6d7ae147 GREEN 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 new file mode 100644 index 0000000000000000000000000000000000000000..db237db72cf1a0905fa82b1dfd799f830d092a10 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/IModelQualityService.java @@ -0,0 +1,59 @@ +/*-------------------------------------------------------------------------+ +| 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.service; + +import org.eclipse.emf.ecore.EObject; +import org.fortiss.tooling.ext.quality.IMetricProvider; +import org.fortiss.tooling.ext.quality.IMetricUpdateListener; +import org.fortiss.tooling.ext.quality.data.MetricData; +import org.fortiss.tooling.kernel.extension.data.ITopLevelElement; + +/** + * A service for performing metric extraction and analysis. + * + * @author groh + */ +public interface IModelQualityService { + /** Returns the service instance. */ + static IModelQualityService getInstance() { + return ModelQualityService.getInstance(); + } + + /** Starting the service. */ + void startService(); + + /** Registers the metric provider with the service. */ + <T extends EObject> void registerMetricProvider(IMetricProvider<T> provider, + Class<T> modelElementClass); + + /** Schedules an analysis of the metrics and processes them. */ + void scheduleMetricCollection(ITopLevelElement top); + + /** Returns the {@link MetricData} for the Service. */ + MetricData getMetricData(); + + /** + * Registers the provided {@link IMetricUpdateListener} to be called when the metrics are + * updated. + * + * @param listener + * to be registered + */ + void registerMetricUpdateListener(IMetricUpdateListener listener); + + /** Creating the quality folder to store the metrics in. */ + void createQualityFolder(); +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..6337c64d1a948fecd938726c88b9fc7d6d7ae147 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java @@ -0,0 +1,419 @@ +/*-------------------------------------------------------------------------+ +| 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.service; + +import static java.util.Collections.emptyList; +import static java.util.Collections.sort; +import static java.util.stream.Collectors.groupingBy; +import static org.fortiss.tooling.kernel.utils.KernelModelElementUtils.getRootElements; + +import java.io.IOException; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +import org.eclipse.core.runtime.CoreException; +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.element.IHierarchicElement; +import org.fortiss.tooling.ext.quality.GraphMetricsProvider; +import org.fortiss.tooling.ext.quality.IMetricProvider; +import org.fortiss.tooling.ext.quality.IMetricUpdateListener; +import org.fortiss.tooling.ext.quality.data.DataRootElement; +import org.fortiss.tooling.ext.quality.data.MetricData; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.ext.quality.data.MetricTreeNode; +import org.fortiss.tooling.ext.quality.storage.CSVFileWriter; +import org.fortiss.tooling.ext.quality.storage.ModelQualityStorageManager; +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.IPersistencyService; +import org.fortiss.tooling.kernel.service.base.EObjectAwareServiceBase; +import org.fortiss.tooling.kernel.service.listener.IPersistencyServiceListener; + +/** + * This class implements the {@link IModelQualityService}. + * + * @author blaschke + * @author groh + */ +/* package */ class ModelQualityService + extends EObjectAwareServiceBase<IMetricProvider<? extends EObject>> + implements IIntrospectiveKernelService, IModelQualityService, IPersistencyServiceListener { + + /** The singleton instance. */ + private static final ModelQualityService INSTANCE = new ModelQualityService(); + + /** Returns singleton instance of the service. */ + /* package */ static ModelQualityService getInstance() { + return INSTANCE; + } + + /** Collected metrics. */ + private MetricData metricDataContainer = new MetricData(); + + /** List of {@link IMetricUpdateListener} which are called when the metrics are updated */ + private List<IMetricUpdateListener> metricUpdateListeners = new ArrayList<>(); + + /** Top-level elements queued to be processed by the metric collector. */ + private final Queue<ITopLevelElement> queuedProcessableElements = + new ConcurrentLinkedQueue<ITopLevelElement>(); + + /** Stores the metrics collector job. */ + private final Job metricCollectorJob = new Job("Collecting metrics...") { + + @Override + protected IStatus run(IProgressMonitor monitor) { + // Using a do-while-true loop with internal termination in order to ensure that the + // monitor is checked for cancellation at least once. + do { + if(monitor.isCanceled()) { + return Status.CANCEL_STATUS; + } + ITopLevelElement toBeProcessed = queuedProcessableElements.poll(); + if(toBeProcessed == null) { + return Status.OK_STATUS; + } + try { + performMetricCollection(toBeProcessed); + } catch(NoSuchAlgorithmException e) { + e.printStackTrace(); + } catch(IOException e) { + e.printStackTrace(); + } catch(CoreException e) { + e.printStackTrace(); + } + } while(true); + } + }; + + /** {@inheritDoc} */ + @Override + public void scheduleMetricCollection(ITopLevelElement element) { + queuedProcessableElements.add(element); + // The Eclipse Job ensures that at most one instance of the metrics collection is executed. + // When a new element is added to the processing queue while a job is active, this current + // job will run longer in order to process it. + metricCollectorJob.schedule(); + } + + /** {@inheritDoc} */ + @Override + public void registerMetricUpdateListener(IMetricUpdateListener listener) { + this.metricUpdateListeners.add(listener); + } + + /** {@inheritDoc} */ + @Override + public void startService() { + IKernelIntrospectionSystemService.getInstance().registerService(this); + IPersistencyService.getInstance().addTopLevelElementListener(this); + } + + /** Registers the metric provider with the service. */ + @Override + public <T extends EObject> void registerMetricProvider(IMetricProvider<T> provider, + Class<T> modelElementClass) { + addHandler(modelElementClass, provider); + } + + /** {@inheritDoc} */ + @Override + public String getIntrospectionDescription() { + return getIntrospectionLabel() + + "\n\nThis service allows to track the evolution of model metrics." + + "\n The service determines the current metrics of a model and stores" + + "\n them in a CSV file for later analysis of the modeling history." + + "\n\nThe service extension point is 'not existent'."; + } + + /** + * Collects all metrics for all elements in the provided {@link ITopLevelElement}. + * + * @param topLevelElement + * the {@link ITopLevelElement} on which the analysis should be performed + * @throws IOException + * @throws NoSuchAlgorithmException + * @throws CoreException + */ + private void performMetricCollection(ITopLevelElement topLevelElement) + throws NoSuchAlgorithmException, IOException, CoreException { + + List<IProjectRootElement> rootElements = new ArrayList<>( + getRootElements(topLevelElement.getRootModelElement(), IProjectRootElement.class)); + + // TODO(#XXX): Remove below workaround that makes assumptions on the overall structure of + // the model, and its implementation. + String allocationTableClassName = + "org.fortiss.af3.allocation.model.impl.AllocationTableCollectionImpl"; + sort(rootElements, (r1, r2) -> { + if(allocationTableClassName.equals(r1.getClass().getName())) { + return 1; + } else if(allocationTableClassName.equals(r2.getClass().getName())) { + return -1; + } + return 0; + }); + + var timestamp = LocalDateTime.now(); + Map<IProjectRootElement, DataRootElement> updatedElements = new HashMap<>(); + for(IProjectRootElement rootElement : rootElements) { + MetricTreeNode rootNode = new MetricTreeNode(); + if(rootElement instanceof IHierarchicElement) { + IHierarchicElement firstElement = (IHierarchicElement)rootElement; + recursivlyCollectMetrics(rootNode, firstElement, 0); + } else { + // Search for provider on ProjectRootElement + collectMetrics(rootNode, rootElement); + } + + // Collect all violated constraints and group them by their respective source + List<IConstraintViolation<? extends EObject>> contraintViolations = + ConstraintCheckerService.getInstance() + .performAllConstraintChecksRecursively(rootElement); + + // Add the violated constraints to the corresponding tree node + for(Entry<? extends EObject, List<IConstraintViolation<? extends EObject>>> entry : contraintViolations + .stream().collect(groupingBy(IConstraintViolation::getSource)).entrySet()) { + + MetricTreeNode node = + metricDataContainer.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.getIntegerMetrics(); + 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()); + } + } + + // building the data Root Element + DataRootElement dataRootElement = + new DataRootElement(timestamp, topLevelElement, rootNode); + metricDataContainer.getDataRootElementMap().put(rootElement, dataRootElement); + updatedElements.put(rootElement, dataRootElement); + metricDataContainer.getTreeNodeLookupTable().put(rootElement, rootNode); + metricUpdateListeners.forEach(l -> l.metricUpdate(rootElement)); + } + // Store all currently saved metrics to the CSV file + CSVFileWriter.metricExtractionToCSV(updatedElements); + } + + /** + * Collect metrics for the given {@link IHierarchicElement} and all its contained elements. + * + * @param node + * to which the data is collected + * @param currentElement + * element to collect the data from + * @param recursionLevel + * parameter which indicates the current recursion level, used for metric + */ + private void recursivlyCollectMetrics(MetricTreeNode node, IHierarchicElement currentElement, + int recursionLevel) { + + collectMetrics(node, currentElement); + node.getIntegerMetrics().put(MetricKey.NESTING_LEVEL, recursionLevel); + + // Check if any of the specifications is an IHierarchicElement + // This is for example the case for AF3 StateAutomatons + Optional<IHierarchicElement> hierarchicElementOptional = currentElement.getSpecifications() + .stream().filter(s -> s instanceof IHierarchicElement) + .map(s -> (IHierarchicElement)s).findAny(); + if(hierarchicElementOptional.isPresent()) { + // Do not collect statistics for the current element, but instead collect them for the + // specification + IHierarchicElement specificationElement = hierarchicElementOptional.get(); + EList<IHierarchicElement> containedElements = + specificationElement.getContainedElements(); + if(containedElements.size() == 1) { + // Add reference from the element to this, so the lookups works as expected + metricDataContainer.getTreeNodeLookupTable().put(specificationElement, node); + // There is exactly one contained element. Skip the specification element to get a + // more useful tree structure + recursivlyCollectMetrics(node, containedElements.get(0), recursionLevel); + } else { + recursivlyCollectMetrics(node, specificationElement, recursionLevel); + } + // Add reference from the element to this, so the lookups works as expected + metricDataContainer.getTreeNodeLookupTable().put(currentElement, node); + } else { + // Iterate over all children and merge values + var nodeDoubles = node.getDoubleMetrics(); + var nodeIntegers = node.getIntegerMetrics(); + for(IHierarchicElement containedElement : currentElement.getContainedElements()) { + MetricTreeNode child = new MetricTreeNode(); + node.getChildren().add(child); + recursivlyCollectMetrics(child, containedElement, recursionLevel + 1); + + for(MetricKey key : MetricKey.getCollectorKeys()) { + var childDoubles = child.getDoubleMetrics(); + var childIntegers = child.getIntegerMetrics(); + if(childDoubles.containsKey(key)) { + nodeDoubles.merge(key, childDoubles.get(key), Double::sum); + } + if(childIntegers.containsKey(key)) { + nodeIntegers.merge(key, childIntegers.get(key), Integer::sum); + } + } + } + + metricDataContainer.getTreeNodeLookupTable().put(currentElement, node); + + // TODO (#4332) Restructure GraphMetricsProvider in quality Plugin + GraphMetricsProvider.calculateBetweennessCentrality(currentElement, metricDataContainer, + false); + GraphMetricsProvider.calculateBetweennessCentrality(currentElement, metricDataContainer, + true); + node.getDoubleMetrics().put(MetricKey.CLUSTERING_COEFFICIENT, + GraphMetricsProvider.calculateClusteringCoefficent(currentElement)); + } + } + + /** + * Collect all metrics provided by any {@link IMetricProvider}} for the given object and saves + * them in the {@link MetricTreeNode}. + * + * @param node + * to which the data is collected + * @param object + * object to collect the data from + */ + @SuppressWarnings("unchecked") + private void collectMetrics(MetricTreeNode node, EObject object) { + + List<IMetricProvider<? extends EObject>> providers = getAllMetricProviders(object); + + for(IMetricProvider<? extends EObject> provider : providers) { + // TODO (#4331) + ((IMetricProvider<EObject>)provider).collectMetrics(node, object); + } + } + + /** Get all suitable {@link IMetricProvider} for the given input. */ + private List<IMetricProvider<? extends EObject>> getAllMetricProviders(EObject input) { + List<IMetricProvider<?>> providers = new ArrayList<>(); + for(Entry<Class<?>, List<IMetricProvider<?>>> migEntry : handlerMap.entrySet()) { + if(migEntry.getKey().isAssignableFrom(input.getClass())) { + providers.addAll(migEntry.getValue()); + } + } + return providers; + } + + /** {@inheritDoc} */ + @Override + protected String getExtensionPointName() { + return null; + } + + /** {@inheritDoc} */ + @Override + protected String getConfigurationElementName() { + return null; + } + + /** {@inheritDoc} */ + @Override + protected String getHandlerClassAttribute() { + return null; + } + + /** {@inheritDoc} */ + @Override + public String getIntrospectionLabel() { + return "Metric Service"; + } + + /** {@inheritDoc} */ + @Override + public List<IIntrospectionItem> getIntrospectionItems() { + return emptyList(); + } + + /** {@inheritDoc} */ + @Override + public boolean showInIntrospectionNavigation() { + return true; + } + + /** {@inheritDoc} */ + @Override + public IIntrospectionDetailsItem getDetailsItem() { + return null; + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementLoaded(ITopLevelElement element) { + // Schedule metric collection when loading projects + scheduleMetricCollection(element); + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementAdded(ITopLevelElement element) { + // Schedule the metric collection for a newly added project + scheduleMetricCollection(element); + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementRemoved(ITopLevelElement element) { + // Nothing done so we keep the data + } + + /** {@inheritDoc} */ + @Override + public void topLevelElementContentChanged(ITopLevelElement element) { + scheduleMetricCollection(element); + } + + /** {@inheritDoc} */ + @Override + public MetricData getMetricData() { + return metricDataContainer; + } + + /** {@inheritDoc} */ + @Override + public void createQualityFolder() { + if(!ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR.exists()) { + ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR.mkdirs(); + } + } +} diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/.ratings b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/.ratings new file mode 100644 index 0000000000000000000000000000000000000000..d2dd8e585384f28b05f0694781597259fec8af02 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/.ratings @@ -0,0 +1,2 @@ +CSVFileWriter.java ba986ec60099bf94472f9e80cba10c01c85bc73f GREEN +ModelQualityStorageManager.java c5a6783f51f596663bf836f98abf2b23130fe180 GREEN diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java new file mode 100644 index 0000000000000000000000000000000000000000..ba986ec60099bf94472f9e80cba10c01c85bc73f --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/CSVFileWriter.java @@ -0,0 +1,252 @@ +/*-------------------------------------------------------------------------+ +| 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.storage; + +import static java.util.stream.Collectors.toList; +import static org.apache.commons.io.FilenameUtils.getBaseName; + +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.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.NullProgressMonitor; +import org.fortiss.tooling.ext.quality.data.DataRootElement; +import org.fortiss.tooling.ext.quality.data.MetricKey; +import org.fortiss.tooling.ext.quality.service.IModelQualityService; +import org.fortiss.tooling.kernel.extension.data.ITopLevelElement; +import org.fortiss.tooling.kernel.model.IProjectRootElement; + +/** + * Allows the export of metrics to a CSV file. + * + * @author groh + */ +public class CSVFileWriter { + + /** Suffix of the file in which the data is written. */ + private static final String CSV = ".csv"; + + /** + * Method to write the data into CSV files. For each project, one file is built. If it already + * exists, it uses to the existing keys in the file as dimensions, to help that the log file + * always remains consistent. + * + * TODO (#4334) Newly added {@link MetricKey}s are not added in the next export + * + * @param map + * to save into CSV file + * @throws IOException + * @throws NoSuchAlgorithmException + * + */ + public static void metricExtractionToCSV(Map<IProjectRootElement, DataRootElement> map) + throws NoSuchAlgorithmException, IOException { + + // check if there is any data to export + if(map == null || map.isEmpty()) { + return; + } + + IModelQualityService.getInstance().createQualityFolder(); + + // Map is not empty due to check above + DataRootElement firstValue = map.entrySet().iterator().next().getValue(); + ITopLevelElement topLevelElement = firstValue.getTopLevelElement(); + String projectName = topLevelElement.getSaveableName(); + + var fileBaseName = getBaseName(projectName).toString(); + // TODO (#4333) + Path path = new File(ModelQualityStorageManager.MODEL_QUALITY_PROJECT_DIR, + fileBaseName.concat(CSV)).toPath(); + + // Metric extraction is triggered when model files are saved. Therefore, the topLevelElement + // should never be dirty here. In order to be on the safe side, conditionally save model. + // save before using the saved file to produce the hash + if(topLevelElement.isDirty()) { + try { + topLevelElement.doSave(new NullProgressMonitor()); + // Avoid duplicated metric extraction (the save operation will trigger another one). + return; + } catch(Exception e) { + e.printStackTrace(); + } + return; + } + // TODO (#4333) + Path modelFile = new File(ModelQualityStorageManager.MODEL_PROJECT_DIR, + topLevelElement.getSaveableName()).toPath(); + String projectHash = computeGitObjectHash(modelFile); + + List<String> allKeys = new ArrayList<>(); + boolean createNewIndex = true; + + if(Files.exists(path)) { + // Read first line and try to extract the keys from the already existing file + try(BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) { + String line = reader.readLine(); + if(line != null) { + allKeys = Arrays.asList(line.split(",")); + createNewIndex = false; + } + } catch(IOException e) { + // In case there is an error, writeMetricsToFile() will provide the default keys. + // Therefore this exception can be ignored (and just logged). + e.printStackTrace(); + } + } + writeMetricsToFile(map, allKeys, path, createNewIndex, projectName, projectHash); + } + + /** + * Function to write the specified metrics into a CSV file. + * + * @param map + * of the metrics which are written into the file + * @param allKeys + * a list of keys which will be extracted from the provided map + * @param path + * location of the file + * @param createNewIndex + * if a index should be written before writing any values + * @param projectName + * the name of the base fileProject for this map currently in the focus + * @param projectHash + * the model file hash value saved for version identification + */ + private static void writeMetricsToFile(Map<IProjectRootElement, DataRootElement> map, + List<String> allKeys, Path path, boolean createNewIndex, String projectName, + String projectHash) { + + try(var writer = new BufferedWriter(new FileWriter(path.toFile(), true))) { + if(createNewIndex) { + // Create new index and write it into the first line of the file. + allKeys.add("nv_timestamp"); + allKeys.add("nv_project_name"); + allKeys.add("nv_project_hash"); + allKeys.add("nv_root_element_name"); + allKeys.add("nv_name"); + allKeys.add("nv_children"); + // Collect all MetricKeys, convert them to a string and append them to the allKeys + // list + allKeys.addAll(Arrays.stream(MetricKey.values()).map(MetricKey::toString) + .map(String::toLowerCase).collect(toList())); + + // Write first line + writer.write(String.join(",", allKeys)); + writer.write("\n"); + } + + // Remove first 6 keys as they are not provided by the provider + List<String> valueKeys = allKeys.subList(6, allKeys.size()); + + LocalDateTime dateTime = LocalDateTime.now(); + DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + String formattedDateTime = dateTime.format(formatter); + + map.forEach((rootElement, dataRootElement) -> { + // iterate over all project roots + String rootName = rootElement.getClass().getSimpleName(); + var rootNode = dataRootElement.getRootNode(); + rootNode.traverseTree(node -> { + var integers = node.getIntegerMetrics(); + var doubles = node.getDoubleMetrics(); + var strings = node.getStringMetrics(); + // collect IDs of children + String children_ids = node.getChildren().stream() + .map(n -> n.getIntegerMetrics().get(MetricKey.UNIQUE_ID)) + .map(id -> id.toString()).reduce((a, b) -> a + " " + b).orElse(""); + + // These 6 data entries are just concatenated in front + var startStream = Stream.of(formattedDateTime, projectName, projectHash, + rootName, node.getName(), children_ids); + // Collect all values from provider in the correct order and combine + var values = Stream.concat(startStream, valueKeys.stream() + .map(String::toUpperCase).map(MetricKey::valueOf).map(k -> { + if(k.isStringValue()) { + return strings.get(k); + } else if(k.isDoubleValue()) { + return doubles.get(k); + } + return integers.get(k); + }).map(String::valueOf)).collect(Collectors.joining(",")); + try { + // write values + writer.write(values); + writer.write("\n"); + } catch(IOException e) { + e.printStackTrace(); + } + }); + }); + } catch(IOException e) { + e.printStackTrace(); + } + } + + /** + * Return the Git object hash for the contents of the given file: + * <p> + * {@code SHA-1("blob <size>\0"+<contents>).asHexString()}. + */ + private static String computeGitObjectHash(Path filePath) + throws NoSuchAlgorithmException, IOException { + + try(BufferedReader reader = new BufferedReader(new FileReader(filePath.toString()))) { + // Read file line-wise with UNIX line endings + StringBuilder inputBuilder = new StringBuilder(); + do { + String line = reader.readLine(); + if(line == null) { + break; + } + inputBuilder.append(line); + inputBuilder.append("\n"); + } while(true); + + // Construct pre-image (input to hash function) according to Git specification + String fileContents = inputBuilder.toString(); + // fileContexts.length() does not return the correct length on some platforms (e.g., + // Linux) + int n = fileContents.getBytes().length; + String preImage = "blob " + n + "\0" + fileContents; + + // Compute hash and convert it to a hex string + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + byte[] digest = sha1.digest(preImage.getBytes()); + + StringBuilder resultBuilder = new StringBuilder(); + for(byte b : digest) { + resultBuilder.append(String.format("%02x", b & 0xff)); + } + return resultBuilder.toString(); + } + } +} 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 0000000000000000000000000000000000000000..c5a6783f51f596663bf836f98abf2b23130fe180 --- /dev/null +++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/storage/ModelQualityStorageManager.java @@ -0,0 +1,56 @@ +/*-------------------------------------------------------------------------+ +| 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.storage; + +import static org.eclipse.core.resources.ResourcesPlugin.getWorkspace; + +import java.io.File; + +import org.eclipse.core.resources.IWorkspaceRoot; + +// TODO (#4333) Exclude the constants to the corresponding packages. The suggested +// resolution is to distribute the information to other classes. +/** + * Manages the directory of the CSV files. + * + * @author groh + */ +public class ModelQualityStorageManager { + + // TODO(#4333): Move to org.fortiss.af3.project (e.g., create sister class of AF3Project (e.g., + // AF3Metrics), and configure the IModelQualityService from there with the directory name, etc.) + /** The name of the general quality plugin directory. */ + public static final String AF3_QUALITY_DIRECTORY_NAME = "AF3-Model-Quality-Directory"; + + // TODO(#4333): Remove! The single source of truth for this is + // org.fortiss.af3.project.utils.FileUtils + /** The name of the general project directory. */ + public static final String AF3_PROJECT_DIRECTORY_NAME = "AF3-Project-Directory"; + + // TODO(#4333): Remove (unneeded magic) + /** The current workspace root. */ + public static final IWorkspaceRoot WORKSPACE_ROOT = getWorkspace().getRoot(); + + // TODO(#4333): Remove, see suggested AF3Metrics class above + /** {@link File} representing the directory where all data is saved. */ + public static final File MODEL_QUALITY_PROJECT_DIR = + new File(WORKSPACE_ROOT.getLocation() + File.separator + AF3_QUALITY_DIRECTORY_NAME); + + // TODO(#4333): Remove, see suggested AF3Metrics class above + /** {@link File} representing the directory where all data is saved. */ + public static final File MODEL_PROJECT_DIR = + new File(WORKSPACE_ROOT.getLocation() + File.separator + AF3_PROJECT_DIRECTORY_NAME); +} diff --git a/org.fortiss.tooling.ext.reuse.ui/META-INF/MANIFEST.MF b/org.fortiss.tooling.ext.reuse.ui/META-INF/MANIFEST.MF index 3ac20b12b06029da3be9348c33fd7e907a26c895..4b56e3fb91e617b0800f29f8334b67a34e0800ac 100644 --- a/org.fortiss.tooling.ext.reuse.ui/META-INF/MANIFEST.MF +++ b/org.fortiss.tooling.ext.reuse.ui/META-INF/MANIFEST.MF @@ -1,7 +1,7 @@ Manifest-Version: 1.0 Automatic-Module-Name: org.fortiss.tooling.ext.reuse.ui Bundle-ManifestVersion: 2 -Bundle-Name: AF3 Reuse UI +Bundle-Name: Reuse UI Bundle-SymbolicName: org.fortiss.tooling.ext.reuse.ui;singleton:=true Bundle-Version: 2.23.0.qualifier Bundle-Activator: org.fortiss.tooling.ext.reuse.ui.ToolingReuseUIActivator diff --git a/org.fortiss.tooling.ext.reuse.ui/plugin.properties b/org.fortiss.tooling.ext.reuse.ui/plugin.properties index 819b9e6b4946973e4235133269aadc2023bd697e..765418afb2d5ce5fcc99657511aa380e85dddff2 100644 --- a/org.fortiss.tooling.ext.reuse.ui/plugin.properties +++ b/org.fortiss.tooling.ext.reuse.ui/plugin.properties @@ -1,4 +1,4 @@ # (c) 2021 fortiss GmbH -pluginName = AF3 Reuse UI +pluginName = Reuse UI providerName = fortiss GmbH diff --git a/org.fortiss.tooling.ext.reuse/META-INF/MANIFEST.MF b/org.fortiss.tooling.ext.reuse/META-INF/MANIFEST.MF index 63c7277ad71516a7358c66a25e93bfe90674a069..4a9be6e18fea7f812456e4aeaa9e22e5d2f3ffe8 100644 --- a/org.fortiss.tooling.ext.reuse/META-INF/MANIFEST.MF +++ b/org.fortiss.tooling.ext.reuse/META-INF/MANIFEST.MF @@ -1,11 +1,11 @@ Manifest-Version: 1.0 Automatic-Module-Name: org.fortiss.tooling.ext.reuse Bundle-ManifestVersion: 2 -Bundle-Name: %pluginName +Bundle-Name: Reuse Bundle-SymbolicName: org.fortiss.tooling.ext.reuse;singleton:=true Bundle-Version: 2.23.0.qualifier Bundle-ClassPath: . -Bundle-Vendor: %providerName +Bundle-Vendor: fortiss GmbH Bundle-Localization: plugin Bundle-RequiredExecutionEnvironment: JavaSE-11 Bundle-ActivationPolicy: lazy diff --git a/org.fortiss.tooling.ext.reuse/plugin.properties b/org.fortiss.tooling.ext.reuse/plugin.properties index df6c054c8323107d456037cd5393a9f100b0b845..324d8736b02455e0b7a14e7dc7861cf82f8064b8 100644 --- a/org.fortiss.tooling.ext.reuse/plugin.properties +++ b/org.fortiss.tooling.ext.reuse/plugin.properties @@ -1,4 +1,4 @@ <!-- (c) 2021 fortiss GmbH --> -pluginName = AF3 Reuse +pluginName = Reuse providerName = fortiss GmbH diff --git a/src/org/fortiss/tooling/ext/quality/ui/AF3QualityUIActivator.java b/src/org/fortiss/tooling/ext/quality/ui/AF3QualityUIActivator.java new file mode 100644 index 0000000000000000000000000000000000000000..4f51b3dce8b7c9f051324ad57044b2620cf88f46 --- /dev/null +++ b/src/org/fortiss/tooling/ext/quality/ui/AF3QualityUIActivator.java @@ -0,0 +1,65 @@ +/*-------------------------------------------------------------------------+ +| Copyright 2022 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.ui; + +import static org.eclipse.jface.resource.ResourceLocator.imageDescriptorFromBundle; + +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.plugin.AbstractUIPlugin; +import org.osgi.framework.BundleContext; + +/** + * The activator class controls the plug-in life cycle. + * + * @author Blaschke + */ +public class AF3QualityUIActivator extends AbstractUIPlugin { + + /** The plug-in ID. */ + public static final String PLUGIN_ID = AF3QualityUIActivator.class.getPackage().getName(); // $NON-NLS-1$ + + /** The shared instance. */ + private static AF3QualityUIActivator plugin; + + /** Constructor. */ + public AF3QualityUIActivator() { + // do nothing + } + + /** {@inheritDoc} */ + @Override + public void start(BundleContext context) throws Exception { + super.start(context); + plugin = this; + } + + /** {@inheritDoc} */ + @Override + public void stop(BundleContext context) throws Exception { + plugin = null; + super.stop(context); + } + + /** Returns the shared instance. */ + public static AF3QualityUIActivator getDefault() { + return plugin; + } + + /** Returns the image descriptor for the given icon file. */ + public static ImageDescriptor getImageDescriptor(String path) { + return imageDescriptorFromBundle(PLUGIN_ID, path).orElse(null); + } +} diff --git a/src/org/fortiss/tooling/ext/quality/ui/MetricExportMenu.java b/src/org/fortiss/tooling/ext/quality/ui/MetricExportMenu.java new file mode 100644 index 0000000000000000000000000000000000000000..56264ea19292b6365984a710af5629043245f55a --- /dev/null +++ b/src/org/fortiss/tooling/ext/quality/ui/MetricExportMenu.java @@ -0,0 +1,75 @@ +/*-------------------------------------------------------------------------+ +| Copyright 2022 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.ui; + +import static java.util.Arrays.asList; +import static java.util.Collections.emptyList; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.jface.action.IContributionItem; +import org.fortiss.tooling.ext.quality.AF3QualityActivator; +import org.fortiss.tooling.kernel.ui.extension.IContextMenuMultiSelectionContributor; +import org.fortiss.tooling.kernel.ui.extension.data.ContextMenuContextProvider; + +import com.sun.javafx.iio.common.ImageDescriptor; + +/** + * Context menu entry to automatic layout {@link IHierarchicElement}-based models. + * + * @author blaschke + */ +public class MetricExportMenu implements IContextMenuMultiSelectionContributor { + + /** Returns the icon that is visible in the context menu for this entry. */ + protected ImageDescriptor getActionIcon() { + return AF3QualityUIActivator.getImageDescriptor("icons/MetricIcon.png"); + } + + /** {@inheritDoc} */ + @Override + public void run() { + try(PrintStream ps_final = new PrintStream("test")) { + AF3QualityActivator extractor = new AF3QualityActivator(); + String enterpriseArchitectureFile = extractor.transformFileProjectToSysML("enterpriseArchitect"); + ps_final.println(enterpriseArchitectureFile); + } catch(Exception ex) { + ex.printStackTrace(); + showError("Unable to save the SysML xmi architecture file", ex.getMessage()); + } + } + + /** {@inheritDoc} */ + @Override + public String getMenuSectionID() { + return null; + } + + @Override + public List<IContributionItem> getContributedItems(EObject selection, ContextMenuContextProvider contextProvider) { + // TODO Auto-generated method stub + return null; + } + + @Override + public List<IContributionItem> getContributedItems(List<EObject> selection, + ContextMenuContextProvider contextProvider) { + // TODO Auto-generated method stub + return null; + } +}