diff --git a/cli/de.cau.cs.kieler.spviz.cli/src/de/cau/cs/kieler/spviz/cli/SPVizCLI.xtend b/cli/de.cau.cs.kieler.spviz.cli/src/de/cau/cs/kieler/spviz/cli/SPVizCLI.xtend index 2d326ef..a27067d 100644 --- a/cli/de.cau.cs.kieler.spviz.cli/src/de/cau/cs/kieler/spviz/cli/SPVizCLI.xtend +++ b/cli/de.cau.cs.kieler.spviz.cli/src/de/cau/cs/kieler/spviz/cli/SPVizCLI.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2024-2025 by + * Copyright 2024-2026 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -77,6 +77,12 @@ class SPVizCLI implements Runnable { @Option(names = #["-g", "--build-generator"], defaultValue = "false", description = "Automatically build the generator projects with Maven.") protected boolean buildGenerator + + @Option(names = #["--no-model-dsl"], defaultValue = "false", description = "Skip generating the model DSL for your architecture model and skip incorporating them into the build process.") + protected boolean noModelDsl + + @Option(names = #["--no-diff"], defaultValue = "false", description = "Skip generating the difference visualization and its DSL and skip incorporating them into the build process.") + protected boolean noDiff /** * Main entry point for this command line tool. @@ -107,7 +113,7 @@ class SPVizCLI implements Runnable { // Parse the model file. LOGGER.info("Generating sources for {}", spvizModelFile.absolutePath.replace("\\", "/")) val Resource resource = rs.getResource(URI.createURI("file://" + spvizModelFile.absolutePath.replace("\\", "/")), true) - SPVizModelGenerator.generate(resource, output) + SPVizModelGenerator.generate(resource, output, noModelDsl, noDiff) } // Prepare loading .spviz files. @@ -118,7 +124,7 @@ class SPVizCLI implements Runnable { LOGGER.info("Generating sources for {}", spvizFile.absolutePath.replace("\\", "/")) val Resource resource = rs.createResource(URI.createURI("file://" + spvizFile.absolutePath.replace("\\", "/"))) resource.load(rs.getLoadOptions()) - SPVizGenerator.generate(resource, output) + SPVizGenerator.generate(resource, output, noModelDsl, noDiff) // Build the project. val buildProject = output.toAbsolutePath.toString.replace("\\", "/") + "/" + (resource.contents.head as SPViz).package + ".build" diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateActions.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateActions.xtend index 7b55c8e..32b8e11 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateActions.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateActions.xtend @@ -499,8 +499,10 @@ class GenerateActions { import de.cau.cs.kieler.klighd.kgraph.util.KGraphUtil import de.cau.cs.kieler.klighd.Klighd import «data.getBundleNamePrefix».viz.SynthesisProperties + import «data.getBundleNamePrefix».viz.SynthesisUtils import «data.getBundleNamePrefix».model.IVisualizationContext import «data.getBundleNamePrefix».model.«data.visualizationName» + import java.util.List import org.eclipse.core.runtime.Status import org.eclipse.emf.ecore.util.EcoreUtil.Copier @@ -519,12 +521,28 @@ class GenerateActions { abstract class AbstractVisualizationContextChangingAction implements IAction { final override execute(ActionContext context) { - val visualizationContexts = context.viewContext.getProperty(SynthesisProperties.VISUALIZATION_CONTEXTS) - val index = context.viewContext.getProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX).intValue + var List<«data.visualizationName»> visualizationContexts = null + var index = 0 + + // Differentiate source/target model in diff visualization. + var visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS + var otherVisContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS_OTHER + var currentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX + var otherCurrentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX_OTHER + + if (SynthesisUtils.isTargetModel(context.getKNode())) { + visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS_OTHER + otherVisContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS + currentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX_OTHER + otherCurrentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX + } + + visualizationContexts = context.viewContext.getProperty(visContextsProperty) + index = context.viewContext.getProperty(currentVisContextIndexProperty).intValue val currentVisualizationContext = visualizationContexts.get(index) - // Make a deep-copy of the current context and store it for the action to be the next canditate for the undo + // Make a deep-copy of the current context and store it for the action to be the next candidate for the undo // function. // Copy the root context and the currently shown one from the same Copier to guarantee a completely copied @@ -552,10 +570,18 @@ class GenerateActions { try { changeVisualization(modelVisualizationContext, context) + // FIXME: this is currently a hack that changes the other visualization context, but does no copying and index updating yet, so redo/undo currently is not implemented in diff visualizations. + if (context.viewContext.viewModel.getChildren().get(0).getChildren.get(0).getProperty(SynthesisProperties.SOURCE_MODEL) !== null) { + var targetModelVisualizationContext = SynthesisUtils.getDiffContext(modelVisualizationContext, context.getViewContext().getProperty(otherVisContextsProperty).last()) + if (targetModelVisualizationContext !== null) { + changeVisualization(targetModelVisualizationContext, context) + } + } + // Put the old context, that will be updated below at the at index + 1 and remember that new index as the // current index. visualizationContexts.add(index + 1, currentVisualizationContext) - context.viewContext.setProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX, index + 1) + context.viewContext.setProperty(currentVisContextIndexProperty, index + 1) return getActionResult(context) diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateDiffViz.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateDiffViz.xtend new file mode 100644 index 0000000..cf7bbcc --- /dev/null +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateDiffViz.xtend @@ -0,0 +1,216 @@ +/* + * KIELER - Kiel Integrated Environment for Layout Eclipse RichClient + * + * http://rtsys.informatik.uni-kiel.de/kieler + * + * Copyright 2025 by + * + Kiel University + * + Department of Computer Science + * + Real-Time and Embedded Systems Group + * + * This code is provided under the terms of the Eclipse Public License 2.0 (EPL-2.0). + */ +package de.cau.cs.kieler.spviz.spviz.generator + +import de.cau.cs.kieler.spviz.spvizmodel.generator.FileGenerator +import java.io.File + +/** + * Generates classes for the dual difference visualization. + * + * @author nre + */ +class GenerateDiffViz { + def static void generate(File sourceFolder, DataAccess data) { + val File folder = FileGenerator.createDirectory(sourceFolder, data.getBundleNamePrefix.replace('.','/') + "/diffviz") + + FileGenerator.updateFile(folder, data.visualizationName.toFirstUpper + "DiffDiagramSynthesis.xtend", + generateDiffVizDiagramSynthesis(data)) + FileGenerator.updateFile(folder, "KlighdSetup.xtend", generateKlighdSetup(data)) + FileGenerator.updateFile(folder, "DiffSynthesisProperties.xtend", generateDiffSynthesisProperties(data)) + } + + /** + * Generates the content for the diagram synthesis to show two models next to each other and to compare them. + * + * @param data + * a DataAccess to easily get the information from + * @return + * the generated file content as a string + */ + def static String generateDiffVizDiagramSynthesis(DataAccess data) { + return ''' + package «data.bundleNamePrefix».diffviz + + import com.google.inject.Inject + import de.cau.cs.kieler.klighd.kgraph.KNode + import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared + import de.cau.cs.kieler.klighd.krendering.extensions.KEdgeExtensions + import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions + import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions + import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis + import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses + import «data.modelBundleNamePrefix».diff.dsl.«data.spvizModel.name.toFirstLower»DiffDsl.«data.spvizModel.name.toFirstUpper»Diff + import «data.modelBundleNamePrefix».model.«data.projectName» + import «data.bundleNamePrefix».viz.«data.projectName»DiagramSynthesis + import org.eclipse.elk.alg.layered.options.LayeredOptions + import org.eclipse.elk.core.options.CoreOptions + import org.eclipse.elk.core.options.Direction + import org.eclipse.emf.common.util.URI + import org.eclipse.emf.ecore.resource.Resource + import org.eclipse.xtext.resource.XtextResourceSet + + @ViewSynthesisShared + class «data.visualizationName.toFirstUpper»DiffDiagramSynthesis extends AbstractDiagramSynthesis<«data.spvizModel.name.toFirstUpper»Diff> { + @Inject extension KNodeExtensions + @Inject extension KEdgeExtensions + @Inject extension KRenderingExtensions + + @Inject «data.projectName»DiagramSynthesis «data.projectName.toFirstLower»DiagramSynthesis + + override getDisplayedActions() { + return «data.projectName.toFirstLower»DiagramSynthesis.displayedActions + } + + override getDisplayedLayoutOptions() { + return «data.projectName.toFirstLower»DiagramSynthesis.displayedLayoutOptions + } + + override getDisplayedSynthesisOptions() { + return «data.projectName.toFirstLower»DiagramSynthesis.displayedSynthesisOptions + } + + override KNode transform(«data.spvizModel.name.toFirstUpper»Diff model) { + val root = model.createNode().associateWith(model) + + val resource = model.eResource + // Find out where this files is located. The comparison files are interpreted relative to this path. + val fileString = resource.URI.toString + val folderString = fileString.substring(0, fileString.lastIndexOf('/')) + + // In the following, exceptions can occur if the model at the given path is not loadable. + // Load the source model + val XtextResourceSet rs = new XtextResourceSet + + var source«data.projectName» = usedContext.getProperty(DiffSynthesisProperties.SOURCE_«data.spvizModel.name.toUpperCase»_MODEL) + if (source«data.projectName» === null || !source«data.projectName».eResource.URI.toString.equals(folderString + "/" + model.sourceModel)) { + + val Resource source = rs.createResource(URI.createURI(folderString + "/" + model.sourceModel)) + source.load(rs.getLoadOptions()) + source«data.projectName» = source.contents.head as «data.projectName» + + usedContext.setProperty(DiffSynthesisProperties.SOURCE_«data.spvizModel.name.toUpperCase»_MODEL, source«data.projectName») + } + + var target«data.projectName» = usedContext.getProperty(DiffSynthesisProperties.TARGET_«data.spvizModel.name.toUpperCase»_MODEL) + if (target«data.projectName» === null || !target«data.projectName».eResource.URI.toString.equals(folderString + "/" + model.targetModel)) { + + val Resource target = rs.createResource(URI.createURI(folderString + "/" + model.targetModel)) + target.load(rs.getLoadOptions()) + target«data.projectName» = target.contents.head as «data.projectName» + usedContext.setProperty(DiffSynthesisProperties.TARGET_«data.spvizModel.name.toUpperCase»_MODEL, target«data.projectName») + + } + + val source«data.projectName»_ = source«data.projectName» + val target«data.projectName»_ = target«data.projectName» + + root.children += createNode => [ + addRectangle => [ invisible = true ] + associateWith(model) + DiagramSyntheses.setLayoutOption(it, CoreOptions::ALGORITHM, "org.eclipse.elk.layered") + setLayoutOption(LayeredOptions.DIRECTION, Direction.RIGHT) + «data.projectName.toFirstLower»DiagramSynthesis.targetModel = target«data.projectName»_ + «data.projectName.toFirstLower»DiagramSynthesis.sourceModel = source«data.projectName»_ + + // display both «data.projectName»s and tell their synthesis which model is which + «data.projectName.toFirstLower»DiagramSynthesis.other = false + + val overviewSource«data.projectName»Node = «data.projectName.toFirstLower»DiagramSynthesis.transform(source«data.projectName»_, usedContext) + children += overviewSource«data.projectName»Node + + «data.projectName.toFirstLower»DiagramSynthesis.other = true + + val overviewTarget«data.projectName»Node = «data.projectName.toFirstLower»DiagramSynthesis.transform(target«data.projectName»_, usedContext) + children += overviewTarget«data.projectName»Node + + val edge = createEdge(source«data.projectName»_, target«data.projectName»_) => [ + addPolyline => [ invisible = true ] + source = overviewSource«data.projectName»Node + target = overviewTarget«data.projectName»Node + ] + overviewSource«data.projectName»Node.outgoingEdges.add(edge) + + ] + return root + } + } + + ''' + } + + /** + * Generates the content for the KLighD setup. + * + * @param data + * a DataAccess to easily get the information from + * @return + * the generated file content as a string + */ + def static String generateKlighdSetup(DataAccess data) { + return ''' + package «data.getBundleNamePrefix».diffviz + + import de.cau.cs.kieler.klighd.IKlighdStartupHook + import de.cau.cs.kieler.klighd.KlighdDataManager + + /** + * Setup registering all KLighD extensions required to run this bundle. + */ + class KlighdSetup implements IKlighdStartupHook { + override execute() { + KlighdDataManager.instance + .registerDiagramSynthesisClass(«data.visualizationName.toFirstUpper»DiffDiagramSynthesis.name, «data.visualizationName.toFirstUpper»DiffDiagramSynthesis) + } + } + + ''' + } + + /** + * Generates the content for DiffSynthesisProperties class. + * + * @param data + * a DataAccess to easily get the information from + * @return + * the generated file content as a string + */ + def static String generateDiffSynthesisProperties(DataAccess data) { + return ''' + package «data.getBundleNamePrefix».diffviz + + import «data.modelBundleNamePrefix».model.«data.projectName» + import org.eclipse.elk.graph.properties.IProperty + import org.eclipse.elk.graph.properties.Property + + /** + * Class to provide easy access to properties stored for the diff syntheses. + */ + class DiffSynthesisProperties { + /** + * Property that stores the view context of the source model. Used to remember already loaded synthesis. + */ + public static final IProperty<«data.projectName»> SOURCE_«data.spvizModel.name.toUpperCase»_MODEL = new Property<«data.projectName»>("«data.spvizModel.name.toFirstLower»diff.sourceModel",null) + + /** + * Property that stores the view context of the target model. Used to remember already loaded synthesis. + */ + + public static final IProperty<«data.projectName»> TARGET_«data.spvizModel.name.toUpperCase»_MODEL = new Property<«data.projectName»>("«data.spvizModel.name.toFirstLower»diff.targetModel",null) + + } + + ''' + } + +} \ No newline at end of file diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateLanguageServer.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateLanguageServer.xtend index 7fa82a4..90aac9a 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateLanguageServer.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateLanguageServer.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2022-2024 by + * Copyright 2022-2026 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -22,20 +22,20 @@ import java.io.File */ class GenerateLanguageServer { - def static void generate(File sourceFolder, File launchFolder, DataAccess data) { + def static void generate(File sourceFolder, File launchFolder, DataAccess data, boolean noModelDsl, boolean noDiff) { val String bundleNamePrefix = data.getBundleNamePrefix val File folder = FileGenerator.createDirectory(sourceFolder, bundleNamePrefix.replace('.','/') + "/language/server") - var String content = generateLanguageRegistration(data) + var String content = generateLanguageRegistration(data, noModelDsl, noDiff) FileGenerator.updateFile(folder, data.visualizationName + "LanguageRegistration.xtend", content) content = generateLanguageServer(data) FileGenerator.updateFile(folder, data.visualizationName + "LanguageServer.xtend", content) content = generateLsCreator(data) FileGenerator.updateFile(folder, data.visualizationName + "LsCreator.xtend", content) - content = generateRegistrationLsExt(data) + content = generateRegistrationLsExt(data, noModelDsl, noDiff) FileGenerator.updateFile(folder, data.visualizationName + "RegistrationLsExt.xtend", content) - content = generateLaunchConfig(data) + content = generateLaunchConfig(data, noModelDsl, noDiff) FileGenerator.generateFile(launchFolder, data.visualizationName + " Launguage Server.launch", content) } @@ -47,7 +47,7 @@ class GenerateLanguageServer { * @return * the generated file content as a string */ - def static String generateLanguageRegistration(DataAccess data) { + def static String generateLanguageRegistration(DataAccess data, boolean noModelDsl, boolean noDiff) { return ''' package «data.getBundleNamePrefix».language.server @@ -55,6 +55,12 @@ class GenerateLanguageServer { import de.cau.cs.kieler.klighd.lsp.launch.ILanguageRegistration import «data.getBundleNamePrefix».model.«data.visualizationName.toFirstUpper»Package import «data.modelBundleNamePrefix».model.«data.spvizModel.name.toFirstUpper»Package + «IF !noModelDsl» + import «data.modelBundleNamePrefix».model.dsl.«data.spvizModel.name.toFirstUpper»DslStandaloneSetup + «ENDIF» + «IF !noDiff» + import «data.modelBundleNamePrefix».diff.dsl.«data.spvizModel.name.toFirstUpper»DiffDslStandaloneSetup + «ENDIF» import org.eclipse.emf.ecore.resource.Resource import org.eclipse.emf.ecore.xmi.impl.XMIResourceFactoryImpl @@ -78,8 +84,18 @@ class GenerateLanguageServer { modelPackageInstance = modelPackageInstance vizmodelPackageInstance = vizmodelPackageInstance - Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap.put("«data.spvizModel.name.toLowerCase»", new XMIResourceFactoryImpl); - Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap.put("«data.visualizationName.toLowerCase»", new XMIResourceFactoryImpl); + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap.put("«data.spvizModel.name.toLowerCase»", new XMIResourceFactoryImpl) + Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap.put("«data.visualizationName.toLowerCase»", new XMIResourceFactoryImpl) + «IF !noModelDsl» + + // Register model DSL language. + «data.spvizModel.name.toFirstUpper»DslStandaloneSetup.doSetup + «ENDIF» + «IF !noDiff» + + // Register diff DSL language. + «data.spvizModel.name.toFirstUpper»DiffDslStandaloneSetup.doSetup + «ENDIF» } } @@ -167,7 +183,7 @@ class GenerateLanguageServer { * @return * the generated file content as a string */ - def static String generateRegistrationLsExt(DataAccess data) { + def static String generateRegistrationLsExt(DataAccess data, boolean noModelDsl, boolean noDiff) { return ''' package «data.getBundleNamePrefix».language.server @@ -185,6 +201,23 @@ class GenerateLanguageServer { override getLanguageExtensions() { return newArrayList( + «IF !noModelDsl» + new Language("«data.spvizModel.name.toLowerCase»dsl", "«data.spvizModel.name» Model DSL", #[ + "projectName", "external", + «FOR artifact : data.artifacts SEPARATOR ", "» + "«artifact.name.toFirstLower»", "«artifact.name.toFirstLower»s" + «ENDFOR» + «FOR connection : data.connections BEFORE ", " SEPARATOR ", "» + "«connection.name.toFirstLower»" + «ENDFOR» + ]), + «ENDIF» + «IF !noDiff» + new Language("«data.visualizationName.toLowerCase»diffdsl", "«data.visualizationName» Diff DSL", #[ + "compare", + "to" + ]), + «ENDIF» new Language("«data.spvizModel.name.toLowerCase»", "«data.spvizModel.name» Model", #[]), new Language("«data.visualizationName.toLowerCase»", "«data.visualizationName» Model", #[]) ) @@ -195,7 +228,7 @@ class GenerateLanguageServer { ''' } - def static String generateLaunchConfig(DataAccess data) { + def static String generateLaunchConfig(DataAccess data, boolean noModelDsl, boolean noDiff) { return ''' @@ -213,12 +246,26 @@ class GenerateLanguageServer { - + - - + + + «IF !noModelDsl» + + + + + «ENDIF» + «IF !noDiff» + + + + + + + «ENDIF» diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateMavenBuild.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateMavenBuild.xtend index 6425430..4ef1fb8 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateMavenBuild.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateMavenBuild.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2022-2024 by + * Copyright 2022-2026 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -14,6 +14,7 @@ package de.cau.cs.kieler.spviz.spviz.generator import de.cau.cs.kieler.spviz.spvizmodel.generator.FileGenerator import java.io.File +import java.util.List class GenerateMavenBuild { /* @@ -22,7 +23,8 @@ class GenerateMavenBuild { * (/bin, /target, dependencies.txt, ...) */ - static String[] bundleSuffixes = #["viz", "model", "language.server"] + static List vizBundleSuffixes + static List modelBundleSuffixes /** * Generates the entire Maven build for this visualization. @@ -33,10 +35,19 @@ class GenerateMavenBuild { * @param modelIdPrefix The ID prefix of the spvizmodel. * @param version The version the generated project should be generated with. */ - static def generate(String rootPath, String artifactIdPrefix, String vizName, String modelIdPrefix, String version) { + static def generate(String rootPath, String artifactIdPrefix, String vizName, String modelIdPrefix, String version, boolean noModelDsl, boolean noDiff) { + vizBundleSuffixes = newArrayList("viz", "model", "language.server") + modelBundleSuffixes = newArrayList("model") + if (!noDiff) { + vizBundleSuffixes.add("diffviz") + modelBundleSuffixes.add("diff.dsl.parent") + } + if (!noModelDsl) { + modelBundleSuffixes.add("model.dsl.parent") + } val root = new File (rootPath) // A pom for each sub-module - for (bundleSuffix : bundleSuffixes) { + for (bundleSuffix : vizBundleSuffixes) { addProjectPom(root, artifactIdPrefix, bundleSuffix, version) } // The main build folder with the main pom to build this viz, an Eclipse feature, LS CLI build, and Tycho update site config @@ -129,10 +140,12 @@ class GenerateMavenBuild { ../spviz.build ../spviz.build/de.cau.cs.kieler.spviz.targetplatform - «FOR bundleSuffix : bundleSuffixes» + «FOR bundleSuffix : vizBundleSuffixes» ../«vizArtifactIdPrefix».«bundleSuffix» «ENDFOR» - ../«modelIdPrefix».model + «FOR modelBundleSuffix : modelBundleSuffixes» + ../«modelIdPrefix».«modelBundleSuffix» + «ENDFOR» feature «vizArtifactIdPrefix».repository diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSubSyntheses.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSubSyntheses.xtend index 43d9216..e49e4a4 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSubSyntheses.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSubSyntheses.xtend @@ -35,7 +35,7 @@ class GenerateSubSyntheses { val bundleNamePrefix = data.getBundleNamePrefix val folder = FileGenerator.createDirectory(sourceFolder, bundleNamePrefix.replace('.', '/') + "/viz/subsyntheses") for (artifact : data.artifacts) { - var content = generateSimpleSynthesis(bundleNamePrefix, artifact.name) + var content = generateSimpleSynthesis(artifact, data) FileGenerator.updateFile(folder, "Simple" + artifact.name + "Synthesis.xtend", content) content = generateSynthesis(artifact, data) @@ -93,6 +93,7 @@ class GenerateSubSyntheses { «FOR shownElement : view.shownElements» import «data.getBundleNamePrefix».model.«shownElement.shownElement.name»Context «ENDFOR» + import «data.modelBundleNamePrefix».model.«data.projectName» «FOR artifact : categories» import «data.bundleNamePrefix».model.«artifact.name.toFirstUpper»Context import «data.modelBundleNamePrefix».model.«artifact.name.toFirstUpper» @@ -101,9 +102,8 @@ class GenerateSubSyntheses { import «data.bundleNamePrefix».model.«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Container «ENDFOR» - import static «data.getBundleNamePrefix».viz.Options.* - import static extension de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses.* + import static extension «data.getBundleNamePrefix».viz.Options.* import static extension «data.getBundleNamePrefix».viz.SynthesisUtils.* import static extension «data.getBundleNamePrefix».model.util.ContextExtensions.* @@ -124,6 +124,10 @@ class GenerateSubSyntheses { extension KGraphFactory = KGraphFactory.eINSTANCE override transform(«viewName»OverviewContext context) { + transform(context, null) + } + + def transform(«viewName»OverviewContext context, «data.projectName» differentModel) { return #[ createNode => [ associateWith(context) @@ -232,7 +236,7 @@ class GenerateSubSyntheses { «ENDFOR» // Add all simple «viewName.toFirstLower» renderings in a first subgraph (top because of node order) - val collapsedOverviewNode = transformCollapsed«viewName»Overview(context) + val collapsedOverviewNode = if (differentModel === null) transformCollapsed«viewName»Overview(context) else transformCollapsed«viewName»Overview(context, differentModel) // only show the collapsed nodes if there are collapsed nodes or if the hide button would uncover the hidden collapsed ones. «FOR shownElement : view.shownElements BEFORE "if (!collapsedOverviewNode.children.empty || usedContext.getOptionValue(INTERACTIVE_BUTTONS) as Boolean && (" SEPARATOR " || " AFTER ")) {"»««« « »!context.collapsed«shownElement.shownElement.name.toFirstUpper»Contexts.isEmpty««« @@ -241,7 +245,13 @@ class GenerateSubSyntheses { } // Add all detailed «viewName.toFirstLower» renderings and their connections in a second subgraph (bottom because of node order) - val detailedOverviewNode = transformDetailed«viewName»Overview(context, it««« + val detailedOverviewNode = if (differentModel === null) transformDetailed«viewName»Overview(context, it««« +« »«FOR categoryConnection : outerCategoryConnections BEFORE ', ' SEPARATOR ', '»««« +« »connected«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »connected«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections, ««« +« »connecting«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections««« +« »«ENDFOR») else transformDetailed«viewName»Overview(context, it, differentModel««« « »«FOR categoryConnection : outerCategoryConnections BEFORE ', ' SEPARATOR ', '»««« « »connected«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« « »connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« @@ -261,6 +271,15 @@ class GenerateSubSyntheses { * @param «viewName.toFirstLower»OverviewContext The overview context for all «viewName.toFirstLower»s in this subsynthesis. */ private def KNode transformCollapsed«viewName»Overview(«viewName»OverviewContext «viewName.toFirstLower»OverviewContext) { + transformCollapsed«viewName»Overview(«viewName.toFirstLower»OverviewContext, null) + } + + /** + * The top part of the «viewName.toFirstLower» overview rendering containing all collapsed «viewName.toFirstLower» renderings in a box layout. + * + * @param «viewName.toFirstLower»OverviewContext The overview context for all «viewName.toFirstLower»s in this subsynthesis. + */ + private def KNode transformCollapsed«viewName»Overview(«viewName»OverviewContext «viewName.toFirstLower»OverviewContext, «data.projectName» differentModel) { val shown = «viewName.toFirstLower»OverviewContext.showCollapsedElements «FOR shownElement : view.shownElements» val filteredCollapsed«shownElement.shownElement.name»Contexts = if (shown) { @@ -288,7 +307,7 @@ class GenerateSubSyntheses { SynthesisUtils.getId(modelElement.name, usedContext) ].forEach [ collapsed«shownElement.shownElement.name»Context, index | children += simple«shownElement.shownElement.name»Synthesis.transform( - collapsed«shownElement.shownElement.name»Context as «shownElement.shownElement.name»Context, -index) + collapsed«shownElement.shownElement.name»Context as «shownElement.shownElement.name»Context, -index, differentModel) ] «ENDFOR» ] @@ -313,6 +332,36 @@ class GenerateSubSyntheses { « »Iterable<«categoryConnection.connectedCategory.name.toFirstUpper»Context> connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« « »Iterable<«data.bundleNamePrefix».model.Pair<«categoryConnection.connectingArtifact.name.toFirstUpper»Context, «categoryConnection.connectedArtifact.name.toFirstUpper»Context>> connected«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections, ««« « »Iterable<«data.bundleNamePrefix».model.Pair<«categoryConnection.connectingArtifact.name.toFirstUpper»Context, «categoryConnection.connectedArtifact.name.toFirstUpper»Context>> connecting«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections««« +« »«ENDFOR») { + transformDetailed«viewName»Overview(context, parentNode, null««« +« »«FOR categoryConnection : outerCategoryConnections BEFORE ', ' SEPARATOR ', '»««« +« »connected«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »connected«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections, ««« +« »connecting«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections««« +« »«ENDFOR») + } + + /** + * The bottom part of the «viewName.toFirstLower» overview rendering containing all detailed «viewName.toFirstLower» renderings and their + * connections in a layered layout. + * + * @param context The overview context for all «viewName.toFirstLower»s in this sub-synthesis. + * @param parentNode The node that the returned node will be added to later. Important to be able to connect the inner category edge. + * @param differentModel The other project to compare against in a diff visualization. + «FOR categoryConnection : outerCategoryConnections» + * @param connected«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s the category connections going out of this overview («categoryConnection.connectedCategory.name.toFirstUpper») + * @param connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s the category connections going into this overview («categoryConnection.connectedCategory.name.toFirstUpper») + * @param connected«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections the category connections going out of this overview («categoryConnection.connectingArtifact.name.toFirstUpper»->«categoryConnection.connectedArtifact.name.toFirstUpper») + * @param connecting«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections the category connections going into this overview («categoryConnection.connectingArtifact.name.toFirstUpper»->«categoryConnection.connectedArtifact.name.toFirstUpper») + «ENDFOR» + */ + private def KNode transformDetailed«viewName»Overview(«viewName»OverviewContext context, KNode parentNode, «data.projectName» differentModel««« +« »«FOR categoryConnection : outerCategoryConnections BEFORE ', ' SEPARATOR ', '»««« +« »Iterable<«categoryConnection.connectedCategory.name.toFirstUpper»Context> connected«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »Iterable<«categoryConnection.connectedCategory.name.toFirstUpper»Context> connecting«categoryConnection.connectedCategory.name.toFirstUpper»From«categoryConnection.connectingCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»s, ««« +« »Iterable<«data.bundleNamePrefix».model.Pair<«categoryConnection.connectingArtifact.name.toFirstUpper»Context, «categoryConnection.connectedArtifact.name.toFirstUpper»Context>> connected«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections, ««« +« »Iterable<«data.bundleNamePrefix».model.Pair<«categoryConnection.connectingArtifact.name.toFirstUpper»Context, «categoryConnection.connectedArtifact.name.toFirstUpper»Context>> connecting«categoryConnection.connectingArtifact.name.toFirstUpper»And«categoryConnection.connectedArtifact.name.toFirstUpper»In«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»Connections««« « »«ENDFOR») { «FOR shownElement : view.shownElements» val filteredDetailed«shownElement.shownElement.name»Contexts = filteredElementContexts( @@ -332,7 +381,7 @@ class GenerateSubSyntheses { «FOR shownElement : view.shownElements» children += filteredDetailed«shownElement.shownElement.name»Contexts.flatMap [ - return «shownElement.shownElement.name.toFirstLower»Synthesis.transform(it as «shownElement.shownElement.name»Context) + return «shownElement.shownElement.name.toFirstLower»Synthesis.transform(it as «shownElement.shownElement.name»Context, differentModel) ] «ENDFOR» @@ -341,8 +390,10 @@ class GenerateSubSyntheses { context.«shownConnection.shownConnection.connecting.name.toFirstLower»Connects«shownConnection.shownConnection.connected.name»Named«shownConnection.shownConnection.name»Edges.forEach [ ««« // Connects the {@code sourceBundleNode} and the {@code usedByBundleNode} via an arrow in UML style, ««« // so [usedByBundleNode] ----- uses -----> [sourceBundleNode] + // var boolean different = false val connecting = key val connected = value + if (!nodeExists(connecting) || !nodeExists(connected)) { // Only Add edges if the nodes are actually shown. return @@ -357,7 +408,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connecting, connected) => [ - addConnected«shownConnection.shownConnection.connecting.name»Connects«shownConnection.shownConnection.connected.name»Named«shownConnection.shownConnection.name»EdgeRendering(true, false) + addConnected«shownConnection.shownConnection.connecting.name»Connects«shownConnection.shownConnection.connected.name»Named«shownConnection.shownConnection.name»EdgeRendering(true, false, SynthesisUtils.differenceInConnection«shownConnection.shownConnection.name.toFirstUpper»(connecting.modelElement, connected.modelElement, differentModel)) sourcePort = connectingPort targetPort = connectedPort source = connectingNode @@ -395,6 +446,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, false) sourcePort = connectingPort targetPort = connectedPort @@ -436,6 +488,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, thickIndices.contains(thisIndex)) sourcePort = connectingPort targetPort = connectedPort @@ -470,6 +523,7 @@ class GenerateSubSyntheses { val connectedPort = newPort val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, false) sourcePort = connectingPort targetPort = connectedPort @@ -505,6 +559,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(true, thickIndices.contains(thisIndex)) sourcePort = connectingPort targetPort = connectedPort @@ -544,6 +599,7 @@ class GenerateSubSyntheses { val connectedPort = newPort val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, false) sourcePort = connectingPort targetPort = connectedPort @@ -563,6 +619,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, false) sourcePort = connectingPort targetPort = connectedPort @@ -596,6 +653,7 @@ class GenerateSubSyntheses { val connectedPort = newPort val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, false) sourcePort = connectingPort targetPort = connectedPort @@ -618,6 +676,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connectingNode, connectedNode, connectionId) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectingArtifact.name.toFirstUpper»Connects«categoryConnection.connectedArtifact.name.toFirstUpper»Named«categoryConnection.connection.name.toFirstUpper»EdgeRendering(false, true) sourcePort = connectingPort targetPort = connectedPort @@ -650,6 +709,7 @@ class GenerateSubSyntheses { ] val edge = createEdge(connecting, connected) => [ +««« TODO: add difference to category edges as well. addConnected«categoryConnection.connectedCategory.name.toFirstUpper»CategoryConnects«categoryConnection.connectedCategory.name.toFirstUpper»Via«(categoryConnection.connection.connecting).name.toFirstUpper»Dot«categoryConnection.connection.name.toFirstUpper»EdgeRendering sourcePort = connectingPort targetPort = connectedPort @@ -751,6 +811,10 @@ class GenerateSubSyntheses { extension KGraphFactory = KGraphFactory.eINSTANCE override transform(«artifactName»Context context) { + transform(context, null) + } + + def transform(«artifactName»Context context, «data.projectName» differentModel) { val «artifactName.toFirstLower» = context.modelElement return #[ context.createNode() => [ @@ -772,7 +836,7 @@ class GenerateSubSyntheses { // Only show this, if the option for it says so and if the context is available. if (usedContext.getOptionValue(Options.«artifactName.toUpperCase»_SHOW_«containedView.view.name.toUpperCase») === true && context.«containedView.view.name.toFirstLower»OverviewContext !== null) { - val «containedView.view.name.toFirstLower»OverviewNodes = «containedView.view.name.toFirstLower»OverviewSynthesis.transform(context.«containedView.view.name.toFirstLower»OverviewContext) + val «containedView.view.name.toFirstLower»OverviewNodes = «containedView.view.name.toFirstLower»OverviewSynthesis.transform(context.«containedView.view.name.toFirstLower»OverviewContext, differentModel) children += «containedView.view.name.toFirstLower»OverviewNodes «FOR categoryConnection : categories.filter[it.innerView === containedView.view]» @@ -961,7 +1025,7 @@ class GenerateSubSyntheses { «FOR view : views SEPARATOR ' || ' AFTER ','» context.parent instanceof «view.name»OverviewContext «ENDFOR» - hasChildren, usedContext) + hasChildren, usedContext, differentModel) «ENDIF» ] ] @@ -981,7 +1045,9 @@ class GenerateSubSyntheses { * @return * the generated file content as a string */ - def static String generateSimpleSynthesis(String packageName, String artifactName) { + def static String generateSimpleSynthesis(Artifact artifact, DataAccess data) { + val packageName = data.bundleNamePrefix + val artifactName = artifact.name return ''' package «packageName».viz.subsyntheses @@ -994,6 +1060,7 @@ class GenerateSubSyntheses { import «packageName».viz.SynthesisUtils import «packageName».viz.Styles import «packageName».model.«artifactName»Context + import «data.modelBundleNamePrefix».model.«data.projectName» import static extension de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses.* @@ -1011,6 +1078,10 @@ class GenerateSubSyntheses { } def transform(«artifactName»Context context, int priority) { + transform(context, priority, null) + } + + def transform(«artifactName»Context context, int priority, «data.projectName» differentModel) { val «artifactName.toFirstLower» = context.modelElement return #[ context.createNode() => [ @@ -1018,7 +1089,7 @@ class GenerateSubSyntheses { data += createKIdentifier => [ it.id = context.hashCode.toString ] val label = SynthesisUtils.getId(«artifactName.toFirstLower».name, usedContext) ?: "" setLayoutOption(CoreOptions::PRIORITY, priority) - add«artifactName»InOverviewRendering(«artifactName.toFirstLower», label, usedContext) + add«artifactName»InOverviewRendering(«artifactName.toFirstLower», label, usedContext, differentModel) ] ] } diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSyntheses.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSyntheses.xtend index 1629610..1071b2b 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSyntheses.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/GenerateSyntheses.xtend @@ -63,8 +63,10 @@ class GenerateSyntheses { import com.google.inject.Inject import de.cau.cs.kieler.klighd.DisplayedActionData import de.cau.cs.kieler.klighd.kgraph.KGraphFactory + import de.cau.cs.kieler.klighd.kgraph.KNode import de.cau.cs.kieler.klighd.krendering.ViewSynthesisShared import de.cau.cs.kieler.klighd.krendering.extensions.KNodeExtensions + import de.cau.cs.kieler.klighd.krendering.extensions.KRenderingExtensions import de.cau.cs.kieler.klighd.syntheses.AbstractDiagramSynthesis import «data.getBundleNamePrefix».viz.actions.RedoAction import «data.getBundleNamePrefix».viz.actions.ResetViewAction @@ -81,6 +83,7 @@ class GenerateSyntheses { «ENDFOR» import «data.modelBundleNamePrefix».model.«data.projectName» import java.util.LinkedHashSet + import java.util.List import org.eclipse.elk.alg.layered.options.CrossingMinimizationStrategy import org.eclipse.elk.alg.layered.options.LayeredMetaDataProvider import org.eclipse.elk.alg.layered.options.LayeredOptions @@ -96,6 +99,7 @@ class GenerateSyntheses { @ViewSynthesisShared class «data.projectName.toFirstUpper»DiagramSynthesis extends AbstractDiagramSynthesis<«data.projectName»> { @Inject extension KNodeExtensions + @Inject extension KRenderingExtensions @Inject extension Styles «FOR view : data.views» @Inject «view.name»OverviewSynthesis «view.name.toFirstLower»OverviewSynthesis @@ -103,6 +107,10 @@ class GenerateSyntheses { extension KGraphFactory = KGraphFactory.eINSTANCE + public var other = false + public var «data.projectName» sourceModel = null + public var «data.projectName» targetModel = null + override getInputDataType() { «data.projectName» } @@ -161,15 +169,30 @@ class GenerateSyntheses { override transform(«data.projectName» model) { val modelNode = createNode.associateWith(model) + modelNode.addRectangle => [ invisible = true ] if (TOPDOWN_LAYOUT.booleanValue) { SynthesisUtils.configureTopdownLayout(modelNode, true) } + // set which model is synthesized + modelNode.setProperty(SynthesisProperties.IS_TARGET_MODEL, other) + modelNode.setProperty(SynthesisProperties.SOURCE_MODEL, sourceModel) + modelNode.setProperty(SynthesisProperties.TARGET_MODEL, targetModel) + + + // Differentiate source/target model in diff visualization. + var «data.visualizationName» visualizationContext = null + var visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS + var visContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX + if (SynthesisUtils.isTargetModel(modelNode)) { + visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS_OTHER + visContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX_OTHER + } + // Create a view with the currently stored visualization context in mind. If there is no current context, create // a new one for the general model overview and store that for later use. - val visualizationContexts = usedContext.getProperty(SynthesisProperties.VISUALIZATION_CONTEXTS) - var index = usedContext.getProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX) - var «data.visualizationName» visualizationContext = null + val visualizationContexts = usedContext.getProperty(visContextsProperty) + var index = usedContext.getProperty(visContextIndexProperty) if (!visualizationContexts.empty && index !== null) { visualizationContext = visualizationContexts.get(index) @@ -179,7 +202,7 @@ class GenerateSyntheses { if (visualizationContext === null || !visualizationContext.isRootModel(model)) { visualizationContexts.removeIf [ true ] index = 0 - usedContext.setProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX, index) + usedContext.setProperty(visContextIndexProperty, index) visualizationContext = VizModelUtil.create«data.visualizationName»(model) visualizationContexts.add(visualizationContext) } @@ -197,19 +220,18 @@ class GenerateSyntheses { if (TOPDOWN_LAYOUT.booleanValue) { SynthesisUtils.configureTopdownLayout(it, false) } + setLayoutOption(BoxLayouterOptions.BOX_PACKING_MODE, PackingMode.GROUP_MIXED) addProjectRendering(model.projectName, usedContext) - «FOR view : data.views» - - val overview«view.name»Nodes = «view.name.toFirstLower»OverviewSynthesis.transform(visContext.«view.name.toFirstLower»OverviewContext) - children += overview«view.name»Nodes - «ENDFOR» + + // send the respectively different model through + createSubNodes(visContext, other ? sourceModel : targetModel, children) ] return modelNode } else { // Delegate the view model generation to another subsynthesis that can show the requested visualization context. - val children = transformSubModel(visualizationContext.focus) + val children = transformSubModel(visualizationContext.focus, other ? sourceModel : targetModel) modelNode.children += children @@ -217,11 +239,19 @@ class GenerateSyntheses { } } - private def transformSubModel(IVisualizationContext context) { + private def createSubNodes(«data.visualizationName» visContext, «data.projectName» otherModel, List children) { + «FOR view : data.views» + + val overview«view.name»Nodes = «view.name.toFirstLower»OverviewSynthesis.transform(visContext.«view.name.toFirstLower»OverviewContext, otherModel) + children += overview«view.name»Nodes + «ENDFOR» + } + + private def transformSubModel(IVisualizationContext context, «data.projectName» otherModel) { switch (context) { «FOR view : data.views» «view.name»OverviewContext: { - return «view.name.toFirstLower»OverviewSynthesis.transform(context) + return «view.name.toFirstLower»OverviewSynthesis.transform(context, otherModel) } «ENDFOR» default: { @@ -284,11 +314,23 @@ class GenerateSyntheses { } override transform(«data.visualizationName»Impl model) { - val visualizationContexts = usedContext.getProperty(SynthesisProperties.VISUALIZATION_CONTEXTS) - var index = usedContext.getProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX) - val rootVisualization = usedContext.getProperty(SynthesisProperties.MODEL_VISUALIZATION_CONTEXT) var «data.visualizationName» visualizationContext = null + // Differentiate source/target model in diff visualization. + var visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS + var currentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX + var modelVisualizationContextProperty = SynthesisProperties.MODEL_VISUALIZATION_CONTEXT + + if (SynthesisUtils.isTargetModel(usedContext.getViewModel())) { + visContextsProperty = SynthesisProperties.VISUALIZATION_CONTEXTS_OTHER + currentVisContextIndexProperty = SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX_OTHER + modelVisualizationContextProperty = SynthesisProperties.MODEL_VISUALIZATION_CONTEXT_OTHER + } + + val visualizationContexts = usedContext.getProperty(visContextsProperty) + var index = usedContext.getProperty(currentVisContextIndexProperty) + val rootVisualization = usedContext.getProperty(modelVisualizationContextProperty) + if (!visualizationContexts.empty && index !== null) { visualizationContext = visualizationContexts.get(index) } @@ -297,14 +339,14 @@ class GenerateSyntheses { if (visualizationContext === null || rootVisualization !== model) { visualizationContexts.removeIf [ true ] index = 0 - usedContext.setProperty(SynthesisProperties.CURRENT_VISUALIZATION_CONTEXT_INDEX, index) + usedContext.setProperty(currentVisContextIndexProperty, index) // As we use a different model, the root model may differ from what is shown in the visualization // context. So create a new visualization context and initialize and connect it with everything the old model // had as well. visualizationContext = VisualizationReInitializer.reInitialize(model) visualizationContexts.add(visualizationContext) - usedContext.setProperty(SynthesisProperties.MODEL_VISUALIZATION_CONTEXT, model) + usedContext.setProperty(modelVisualizationContextProperty, model) // Set synthesis and layout options according to the stored options. setSynthesisOptions(model.getSynthesisOptions) @@ -480,6 +522,7 @@ class GenerateSyntheses { import de.cau.cs.kieler.klighd.krendering.KContainerRendering import de.cau.cs.kieler.klighd.krendering.KPolyline import de.cau.cs.kieler.klighd.krendering.KRectangle + import de.cau.cs.kieler.klighd.krendering.KRendering import de.cau.cs.kieler.klighd.krendering.KRoundedRectangle import de.cau.cs.kieler.klighd.krendering.KText import de.cau.cs.kieler.klighd.krendering.LineStyle @@ -507,9 +550,11 @@ class GenerateSyntheses { import «data.getBundleNamePrefix».viz.actions.RevealConnecting«connection.connecting.name»Connects«connection.connected.name»Named«connection.name»Action import «data.getBundleNamePrefix».viz.actions.RemoveConnecting«connection.connecting.name»Connects«connection.connected.name»Named«connection.name»Action «ENDFOR» + import «data.getBundleNamePrefix».viz.SynthesisUtils.ArtifactDifference «FOR artifact : data.artifacts» import «data.modelBundleNamePrefix».model.«artifact.name» «ENDFOR» + import «data.modelBundleNamePrefix».model.«data.projectName» ««« import java.util.List import static «data.getBundleNamePrefix».viz.Options.* @@ -530,6 +575,8 @@ class GenerateSyntheses { // The colors used for the visualization. public static final String DEFAULT_BACKGROUND_COLOR = "white" + + // Artifact colors «FOR artifact : data.artifacts» public static final String COLOR_«artifact.name» = "«colors.get(artifact)»" public static final String SECONDARY_COLOR_«artifact.name» = "«secondaryColors.get(artifact)»" @@ -543,21 +590,48 @@ class GenerateSyntheses { public static final String SHADOW_COLOR = "black" - // Edge colors. - public static final String SELECTION_COLOR = "blue" + // Selection color. + public static final String SELECTION_COLOR = "orange" /** The roundness of visualized rounded rectangles. */ static final int ROUNDNESS = 4 + // comparison colors. + public static final String ADDED = "#00cc00" + public static final String REMOVED = "#ff0000" + public static final String MODIFIED = "#2a00ff" + // ------------------------------------- Generic renderings ------------------------------------- /** * Sets the selection style of the node. */ - def setSelectionStyle(KContainerRendering rendering) { + def setSelectionStyle(KContainerRendering rendering, boolean alreadyWide) { rendering => [ - selectionLineWidth = 3 * lineWidthValue; - selectionForeground = SELECTION_COLOR.color; + if (!alreadyWide) { + selectionLineWidth = 3 * lineWidthValue + } + selectionForeground = SELECTION_COLOR.color + ] + } + + /** + * Set the difference style on the rendering. + */ + def setDifferenceStyle(KRendering rendering, SynthesisUtils.ArtifactDifference difference, boolean isTargetModel, boolean isEdge) { + var String color = null + switch (difference) { + case SynthesisUtils.ArtifactDifference.UNCHANGED: + return + case SynthesisUtils.ArtifactDifference.NON_EXISTENT: + color = isTargetModel ? ADDED : REMOVED + case SynthesisUtils.ArtifactDifference.MODIFIED: + color = MODIFIED + } + val color_ = color + rendering => [ + lineWidth = (isEdge ? 1.5f : 3) * lineWidthValue + foreground = color_.color ] } @@ -571,7 +645,7 @@ class GenerateSyntheses { setShadow(SHADOW_COLOR.color, 4, 4) } addSimpleLabel(name, false) - setSelectionStyle + setSelectionStyle(false) ] } @@ -601,7 +675,7 @@ class GenerateSyntheses { if (isConnectable) { columns += 1 } - } + } setGridPlacement(columns) invisible = true addRectangle => [ @@ -632,7 +706,7 @@ class GenerateSyntheses { } background = DEFAULT_BACKGROUND_COLOR.color tooltip = tooltipText - setSelectionStyle + setSelectionStyle(false) ] } else { // Collapsed @@ -661,7 +735,7 @@ class GenerateSyntheses { } background = DEFAULT_BACKGROUND_COLOR.color tooltip = tooltipText - setSelectionStyle + setSelectionStyle(false) ] } } @@ -972,7 +1046,7 @@ class GenerateSyntheses { } background = DEFAULT_BACKGROUND_COLOR.color tooltip = "The overview of all available views for this project." - setSelectionStyle + setSelectionStyle(false) ] } @@ -990,11 +1064,24 @@ class GenerateSyntheses { * {link ReferencedSynthesisExpandAction} to dynamically call the feature synthesis for the given feature. * * @param node The KNode to add this rendering to. - * @param aritfact The «artifact.name.toFirstLower» this rendering should represent. + * @param artifact The «artifact.name.toFirstLower» this rendering should represent. * @param label The representing name of this feature that should be shown. * @param context The used ViewContext. */ def add«artifact.name»InOverviewRendering(KNode node, «artifact.name» artifact, String name, ViewContext context) { + add«artifact.name»InOverviewRendering(node, artifact, name, context, null) + } + + /** + * Adds a simple rendering for a {@link «artifact.name»} to the given node that can be expanded to call the + * {link ReferencedSynthesisExpandAction} to dynamically call the feature synthesis for the given feature. + * + * @param node The KNode to add this rendering to. + * @param artifact The «artifact.name.toFirstLower» this rendering should represent. + * @param label The representing name of this feature that should be shown. + * @param differentModel The other model to compare against for this artifact's coloring. + */ + def add«artifact.name»InOverviewRendering(KNode node, «artifact.name» artifact, String name, ViewContext context, «data.projectName» differentModel) { node.addRoundedRectangle(ROUNDNESS, ROUNDNESS) => [ val interactiveButtons = context.getOptionValue(INTERACTIVE_BUTTONS) as Boolean var columns = 1 @@ -1026,7 +1113,9 @@ class GenerateSyntheses { setShadow(SHADOW_COLOR.color, 4, 4) } tooltip = "«artifact.name» \"" + artifact.getName + "\"" - setSelectionStyle + val diff = SynthesisUtils.differenceInModel(artifact, differentModel) + setDifferenceStyle(diff, SynthesisUtils.isTargetModel(differentModel, context), false) + setSelectionStyle(diff !== ArtifactDifference.UNCHANGED) ] } @@ -1044,6 +1133,24 @@ class GenerateSyntheses { */ def KRoundedRectangle add«artifact.name»Rendering(KNode node, «artifact.name» artifact, boolean inOverview, boolean hasChildren, ViewContext context) { + add«artifact.name»Rendering(node, artifact, inOverview, hasChildren, context, null) + } + + /** + * Adds a rendering for a {@link «artifact.name»} to the given node. + * Contains the name of the «artifact.name.toFirstLower», a button to focus this artifact and text for the ID and description of this artifact. + * + * @param node The KNode this rendering should be attached to. + * @param artifact The «artifact.name.toFirstLower» this rendering represents. + * @param inOverview If this product is shown in a «artifact.name.toFirstLower» overview. + * @param hasChildren If this rendering should leave space for a child area. + * @param context The view context used in the synthesis. + * @param differentModel The other model to compare against for this artifact's coloring. + * + * @return The entire rendering for a «artifact.name.toFirstLower». + */ + def KRoundedRectangle add«artifact.name»Rendering(KNode node, «artifact.name» artifact, boolean inOverview, boolean hasChildren, + ViewContext context, «data.projectName» differentModel) { node.addRoundedRectangle(ROUNDNESS, ROUNDNESS) => [ if (artifact.isExternal) { setBackgroundGradient(EXTERNAL_COLOR_«artifact.name».color, EXTERNAL_SECONDARY_COLOR_«artifact.name».color, 90) @@ -1111,7 +1218,9 @@ class GenerateSyntheses { tooltip = "«artifact.name» \"" + artifact.getName + "\"" addSingleClickAction(SelectRelatedAction::ID, ModifierState.NOT_PRESSED, ModifierState.NOT_PRESSED, ModifierState.NOT_PRESSED) - setSelectionStyle + val diff = SynthesisUtils.differenceInModel(artifact, differentModel) + setDifferenceStyle(diff, SynthesisUtils.isTargetModel(differentModel, context), false) + setSelectionStyle(diff !== ArtifactDifference.UNCHANGED) ] } @@ -1140,17 +1249,28 @@ class GenerateSyntheses { /** * Adds the rendering for an edge showing a «connected.connected.name.toFirstLower» connection. * - * @param head if this edge shold render an arrow head. - * @param thick if this edge shold be rendered thicker. + * @param head if this edge should render an arrow head. + * @param thick if this edge should be rendered thicker. */ def addConnected«connected.connecting.name»Connects«connected.connected.name»Named«connected.name»EdgeRendering(KEdge edge, boolean head, boolean thick) { + addConnected«connected.connecting.name»Connects«connected.connected.name»Named«connected.name»EdgeRendering(edge, head, thick, SynthesisUtils.ArtifactDifference.UNCHANGED) + } + + /** + * Adds the rendering for an edge showing a «connected.connected.name.toFirstLower» connection. + * + * @param head if this edge should render an arrow head. + * @param thick if this edge should be rendered thicker. + * @param different if there is a difference between source and target model. + * @param targetModel true if this edge is missing in the target model, false if it is missing in the source model. + */ + def addConnected«connected.connecting.name»Connects«connected.connected.name»Named«connected.name»EdgeRendering(KEdge edge, boolean head, boolean thick, SynthesisUtils.ArtifactDifference difference) { edge.addPolyline => [ lineWidth = thick ? 4 : 2 if (head) { addHeadArrowDecorator => [ + setDifferenceStyle(difference, SynthesisUtils.isTargetModel(edge), true) lineWidth = thick ? 2 : 1 - background = "black".color - foreground = "black".color selectionLineWidth = thick ? 3 : 1.5f selectionForeground = SELECTION_COLOR.color selectionBackground = SELECTION_COLOR.color @@ -1159,6 +1279,7 @@ class GenerateSyntheses { suppressSelectablility ] } + setDifferenceStyle(difference, SynthesisUtils.isTargetModel(edge), true) lineStyle = LineStyle.DASH selectionLineWidth = thick ? 6 : 3 selectionForeground = SELECTION_COLOR.color @@ -1239,6 +1360,8 @@ class GenerateSyntheses { import de.cau.cs.kieler.klighd.SynthesisOption import de.cau.cs.kieler.klighd.ViewContext + import de.cau.cs.kieler.klighd.kgraph.KEdge + import de.cau.cs.kieler.klighd.kgraph.KGraphElement import de.cau.cs.kieler.klighd.kgraph.KNode import de.cau.cs.kieler.klighd.syntheses.DiagramSyntheses import «data.getBundleNamePrefix».model.IOverviewVisualizationContext @@ -1256,7 +1379,9 @@ class GenerateSyntheses { import org.eclipse.elk.core.options.PortConstraints import org.eclipse.elk.core.options.TopdownSizeApproximator import org.eclipse.elk.core.options.TopdownNodeTypes - + import «data.modelBundleNamePrefix».model.Identifiable + import «data.modelBundleNamePrefix».model.«data.projectName» + import «data.bundleNamePrefix».model.«data.visualizationName» «FOR artifact : data.artifacts» import «data.modelBundleNamePrefix».model.«artifact.name.toFirstUpper» «ENDFOR» @@ -1285,6 +1410,111 @@ class GenerateSyntheses { * Utils class can not be instantiated. */ private new() {} + + /** + * Enum for how two artifacts between models differ with each other. + */ + static enum ArtifactDifference { + /** + * The artifact is unchanged between both models. + */ + UNCHANGED, + /** + * The artifact does not exist in the other model and is therefore either removed/added in the other model. + */ + NON_EXISTENT, + /** + * The artifact is modified in the other model. A modification indicates that an artifact with the same + * ID exists in the other model that has different *outgoing* connections or different *contained* artifacts. + * Incoming connections and different containers are ignored. + */ + MODIFIED + } + + /** + * Method finds corresponding IVisualizationContext from a different model by reversing the way + * up the ContextModel. + * + * @param originContext VizContext to search for + * @param targetViz top of the VizContext model to search through + * + * @return the corresponding VizContext or null if there is none + */ + def static IVisualizationContext getDiffContext(IVisualizationContext originContext, «data.visualizationName» targetViz) { +««« TODO: this seems inefficient. + var currentContext = originContext + var toFindContext = targetViz as IVisualizationContext + var wayUp = >newLinkedList() + + // save all viewContexts that are parents of the origin and the origin itself, + // until there is no parent + while (currentContext.getParent() !== null) { + wayUp.add(currentContext) + currentContext = currentContext.getParent() + } + + // reverse the order of parents to find the corresponding children until we followed the + // way completely + wayUp.reverse() + for (context : wayUp) { + var children = toFindContext.childContexts + for (child : children) { + if (child.class === context.class) { + if (child.modelElement !== null && context.modelElement !== null) { + var element_target = child.modelElement as Identifiable + var element_source = context.modelElement as Identifiable + if (element_target.ecoreId.equals(element_source.ecoreId)) { + toFindContext = child + } + } else { + toFindContext = child + } + + } + } + } + + + return toFindContext.class === originContext.class ? toFindContext : null + } + + /** + * Checks which of the models is the root. Returns true if the root is the source model and false + * if its the target model. + * + * @param current KNode location + * + * @return whether the root is the target model or not + */ + def static boolean isTargetModel(KGraphElement current) { + var KNode currentNode = null + if (current instanceof KNode) { + currentNode = current + } else if (current instanceof KEdge) { + currentNode = current.source + } + currentNode = find«data.projectName»Impl(currentNode) + return currentNode !== null && currentNode.getProperty(SynthesisProperties.IS_TARGET_MODEL) + } + +««« TODO: hacky solution, should be solved better in the long term. + /** + * If this model (given the other model) is the target model + */ + def static boolean isTargetModel(«data.projectName.toFirstUpper» differentModel, ViewContext context) { + return differentModel !== null && differentModel !== context.viewModel.getChildren()?.get(0)?.getChildren?.get(0)?.getProperty(SynthesisProperties.TARGET_MODEL) + } + + /** + * Returns the KNode that represents the «data.projectName»Impl + */ + def static KNode find«data.projectName»Impl(KNode current) { + var currentNode = current + while (currentNode !== null && currentNode.properties.get(SynthesisProperties.IS_TARGET_MODEL) === null) { + currentNode = currentNode.getParent() + } + return currentNode + } /** * If the id should be truncated by the prefix of the {@link Options#SHORTEN_BY} option, this returns a @@ -1374,6 +1604,129 @@ class GenerateSyntheses { } } + /** + * If the two artifacts represent the same thing in two different models + */ + def static isEqualTo(Identifiable a1, Identifiable a2) { + return a1.class == a2.class && a1.ecoreId.equals(a2.ecoreId) + } + + «FOR artifact : data.artifacts» + /** + * Find and return the «artifact.name.toFirstUpper» with the same ID in a different model. + * Returns {@code null} if artifact does not exist in the other model. + */ + def static «artifact.name.toFirstUpper» findEqualArtifactInModel(«artifact.name.toFirstUpper» artifact, «data.projectName.toFirstUpper» otherModel) { + val String identifier = artifact.ecoreId + for («artifact.name.toFirstUpper» other: otherModel.«artifact.name.toFirstLower»s) { + if (identifier.equals(other.ecoreId)) { + return other + } + } + return null + } + + /** + * Calculates the {@link ArtifactDifference} of a «artifact.name.toFirstLower» compared to a different project. + */ + def static ArtifactDifference differenceInModel(«artifact.name.toFirstUpper» artifact, «data.projectName.toFirstUpper» otherModel) { + if (otherModel === null) { + return ArtifactDifference.UNCHANGED + } + + // check for existence in other model + val otherArtifact = findEqualArtifactInModel(artifact, otherModel) + if (otherArtifact === null) { + return ArtifactDifference.NON_EXISTENT + } + + // check for equal outgoing connections and contained artifacts. + if (equalOutgoingConnections(artifact, otherArtifact) && equalContainedArtifacts(artifact, otherArtifact)) { + return ArtifactDifference.UNCHANGED + } + return ArtifactDifference.MODIFIED + } + + /** + * Check if all outgoing connections (those defined within the Artifact's body in the .spvizmodel via 'connects') of this artifact1 (this model) connect to all artifacts with the same IDs in artifact2 (the other model). + */ + def static boolean equalOutgoingConnections(«artifact.name.toFirstUpper» artifact1, «artifact.name.toFirstUpper» artifact2) { + «IF !data.getConnectedArtifacts(artifact).empty» + // check the sizes of outgoing connection lists first. + if ( + «FOR connection : data.getConnectedArtifacts(artifact) SEPARATOR " ||"» + artifact1.connected«connection.name.toFirstUpper»«connection.connected.name.toFirstUpper»s.size !== artifact2.connected«connection.name.toFirstUpper»«connection.connected.name.toFirstUpper»s.size + «ENDFOR» + ) { + return false + } + + // if lists are the same size, search a corresponding target for each connection. + «ENDIF» + «FOR connection : data.getConnectedArtifacts(artifact)» + for (connected : artifact1.connected«connection.name.toFirstUpper»«connection.connected.name.toFirstUpper»s) { + if (artifact2.connected«connection.name.toFirstUpper»«connection.connected.name.toFirstUpper»s.findFirst[isEqualTo(connected)] === null) { + return false + } + } + «ENDFOR» + // found everything! + return true + } + + /** + * Check if all contained artifacts (those defined within the Artifact's body in the .spvizmodel via 'contains') are in equal in both artifacts. + */ + def static boolean equalContainedArtifacts(«artifact.name.toFirstUpper» artifact1, «artifact.name.toFirstUpper» artifact2) { + «IF !artifact.containedArtifacts.empty» + // check the sizes of contained artifact lists first. + if ( + «FOR containment : artifact.containedArtifacts SEPARATOR " ||"» + artifact1.«containment.name.toFirstLower»s.size !== artifact2.«containment.name.toFirstLower»s.size + «ENDFOR» + ) { + return false + } + + // if lists are the same size, search for a corresponding contained element for each containment. + «ENDIF» + «FOR containment : artifact.containedArtifacts» + for (contained : artifact1.«containment.name.toFirstLower»s) { + if (artifact2.«containment.name.toFirstLower»s.findFirst[isEqualTo(contained)] === null) { + return false + } + } + «ENDFOR» + // found everything! + return true + } + «ENDFOR» + + «FOR connection : data.connections» + def static ArtifactDifference differenceInConnection«connection.name.toFirstUpper»(«connection.connecting.name.toFirstUpper» sourceArtifact, «connection.connected.name.toFirstUpper» targetArtifact, «data.projectName.toFirstUpper» otherModel) { + if (otherModel === null) { + return ArtifactDifference.UNCHANGED + } + + // check for existence in other model + val otherSourceArtifact = findEqualArtifactInModel(sourceArtifact, otherModel) + if (otherSourceArtifact === null) { + return ArtifactDifference.NON_EXISTENT + } + val otherTargetArtifact = findEqualArtifactInModel(targetArtifact, otherModel) + if (otherTargetArtifact === null) { + return ArtifactDifference.NON_EXISTENT + } + + // check that connection still exists + if (!otherSourceArtifact.connected«connection.name.toFirstUpper»«connection.connected.name.toFirstUpper»s.contains(otherTargetArtifact)) { + return ArtifactDifference.NON_EXISTENT + } + + return ArtifactDifference.UNCHANGED + } + «ENDFOR» + «FOR categoryConnection : data.getUniqueCategoryConnections» /** * For a category connection between «categoryConnection.connectedCategory.name.toFirstLower» based on their «categoryConnection.connectingArtifact.name.toFirstLower»->«categoryConnection.connectedArtifact.name.toFirstLower» «categoryConnection.connection.name.toFirstLower» in «(categoryConnection.connection.connecting).name.toFirstLower»Dot«categoryConnection.connection.name.toFirstUpper» and given a such «categoryConnection.connectedCategory.name.toFirstLower» category container and a «categoryConnection.connectedCategory.name.toFirstLower», @@ -1616,6 +1969,7 @@ class GenerateSyntheses { import de.cau.cs.kieler.klighd.ViewContext import «data.getBundleNamePrefix».model.«data.visualizationName» + import «data.modelBundleNamePrefix».model.«data.projectName» import java.util.LinkedList import java.util.List import org.eclipse.elk.graph.properties.IProperty @@ -1631,7 +1985,12 @@ class GenerateSyntheses { * Currently does not store a delta between the contexts, but a hard copy of every state used since the beginning * default view. */ - public static final IProperty> VISUALIZATION_CONTEXTS = new Property>("osgimodel.visualizationContexts", new LinkedList<«data.visualizationName»>) + public static final IProperty> VISUALIZATION_CONTEXTS = new Property>("model.visualizationContexts", new LinkedList<«data.visualizationName»>) + + /** + * The other visualization contexts in a difference visualization, see {@code VISUALIZATION_CONTEXTS}. + */ + public static final IProperty> VISUALIZATION_CONTEXTS_OTHER = new Property>("model.visualizationContextsOther", new LinkedList<«data.visualizationName»>) /** * Property pointing towards which index points towards the currently used visualization context in the @@ -1641,6 +2000,11 @@ class GenerateSyntheses { */ public static final IProperty CURRENT_VISUALIZATION_CONTEXT_INDEX = new Property("model.currentVisualizationContextIndex", null) + /** + * The other visualization context index in a difference visualization, see {@code CURRENT_VISUALIZATION_CONTEXT_INDEX}. + */ + public static final IProperty CURRENT_VISUALIZATION_CONTEXT_INDEX_OTHER = new Property("model.currentVisualizationContextIndexOther", null) + /** * The root model visualization context for the VizSynthesis to figure out the change of file against the usual * change of the visualization model. @@ -1648,6 +2012,29 @@ class GenerateSyntheses { */ public static final IProperty<«data.visualizationName»> MODEL_VISUALIZATION_CONTEXT = new Property<«data.visualizationName»>("model.modelVisualizationContext", null) + /** + * The other model visualization context in a difference visualization, see {@code MODEL_VISUALIZATION_CONTEXT}. + */ + public static final IProperty<«data.visualizationName»> MODEL_VISUALIZATION_CONTEXT_OTHER = new Property<«data.visualizationName»>("model.modelVisualizationContextOther", null) + + /** + * Property that indicates the source model in a difference visualization. Set to null (or not set at all) + * if normal visualization for one model is used. + */ + public static final IProperty<«data.projectName»> SOURCE_MODEL = new Property<«data.projectName»>("model.sourceModel", null) + + /** + * Property that indicates the target model in a difference visualization. Set to null (or not set at all) + * if normal visualization for one model is used. + */ + public static final IProperty<«data.projectName»> TARGET_MODEL = new Property<«data.projectName»>("model.targetModel", null) + + /** + * Checkmark for marking the synthesis for each model. + * If true, then current model in context is source model, if false, then it is the target model. + */ + public static final IProperty IS_TARGET_MODEL = new Property("model.isTargetModel", null) + } ''' diff --git a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/SPVizGenerator.xtend b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/SPVizGenerator.xtend index bf7029c..2b97228 100644 --- a/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/SPVizGenerator.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spviz/src/de/cau/cs/kieler/spviz/spviz/generator/SPVizGenerator.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2020-2024 by + * Copyright 2020-2026 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -48,7 +48,7 @@ class SPVizGenerator extends AbstractGenerator { override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { val workspace = ResourcesPlugin.workspace val output = Paths.get(workspace.root.location.toString) - SPVizGenerator.generate(resource, output) + SPVizGenerator.generate(resource, output, false, false) } // TODO: add flags and a class for each 'via' connection (look at 'UsedPackagesOfBundleEdgeConnection' for reference) @@ -61,7 +61,7 @@ class SPVizGenerator extends AbstractGenerator { "AbstractMethodError", "AssertionError", "BootstrapMethodError", "ClassCircularityError", "ClassFormatError", "Error", "ExceptionInInitializerError", "IllegalAccessError", "IncompatibleClassChangeError", "InstantiationError", "InternalError", "LinkageError", "NoClassDefFoundError", "NoSuchFieldError", "NoSuchMethodError", "OutOfMemoryError", "StackOverflowError", "ThreadDeath", "UnknownError", "UnsatisfiedLinkError", "UnsupportedClassVersionError", "VerifyError", "VirtualMachineError" ] - static def void generate(Resource resource, Path rootPath) { + static def void generate(Resource resource, Path rootPath, boolean noModelDsl, boolean noDiff) { val DataAccess data = new DataAccess(resource) val root = rootPath.toAbsolutePath.toString + "/" @@ -118,6 +118,30 @@ class SPVizGenerator extends AbstractGenerator { // Copy icons over into the project copyIcons(FileGenerator.createDirectory(vizProjectDirectory, "icons")) + if (noDiff) { + LOGGER.info("Skip generating the diffviz project.") + } else { + // Generate the -viz.diffviz project + val diffVizProjectName = data.getBundleNamePrefix + ".diffviz" + val diffVizProjectPath = root + diffVizProjectName + if (new File(diffVizProjectPath).isDirectory) { + LOGGER.info("Updating sources of project {}", diffVizProjectPath) + } else { + LOGGER.info("Generating project {}", diffVizProjectPath) + } + FileGenerator.createDirectory(diffVizProjectPath) + new ProjectGenerator(diffVizProjectName, diffVizProjectPath) + .configureMaven(true) + .configureKlighd(true) + .additionalSourceFolder("xtend-gen") + .configureRequiredBundles(requiredDiffVizBundles(data)) + .configureExportedPackages(exportedDiffVizPackages(data)) + .generate() + + val sourceDiffVizFolder = new File(diffVizProjectPath, "src-gen") + GenerateDiffViz.generate(sourceDiffVizFolder, data) + } + // Generate the .language.server Maven project val lsProjectName = data.bundleNamePrefix + ".language.server" @@ -129,7 +153,7 @@ class SPVizGenerator extends AbstractGenerator { } val lsProjectDirectory = FileGenerator.createDirectory(lsProjectPath) new JavaMavenProjectGenerator(data.bundleNamePrefix, lsProjectName, lsProjectPath) - .configureDependencies(requiredLSDependencies(data)) + .configureDependencies(requiredLSDependencies(data, noModelDsl, noDiff)) .configureDependencyManagement(lsDependencyManagement) .configureXtendSources(true) .configureSourceFolderName("src-gen") @@ -140,10 +164,10 @@ class SPVizGenerator extends AbstractGenerator { // Generate further source files for the java project val launchFolder = FileGenerator.createDirectory(lsProjectDirectory, "launch") val lsSourceFolder = FileGenerator.createDirectory(lsProjectDirectory, "src-gen") - GenerateLanguageServer.generate(lsSourceFolder, launchFolder, data) + GenerateLanguageServer.generate(lsSourceFolder, launchFolder, data, noModelDsl, noDiff) // Generate the Maven build framework for this visualization. - GenerateMavenBuild.generate(root, data.bundleNamePrefix, data.visualizationName.toFirstUpper, data.modelBundleNamePrefix, "0.1.0") + GenerateMavenBuild.generate(root, data.bundleNamePrefix, data.visualizationName.toFirstUpper, data.modelBundleNamePrefix, "0.1.0", noModelDsl, noDiff) } /** @@ -252,8 +276,8 @@ class SPVizGenerator extends AbstractGenerator { ] } - protected static def List requiredLSDependencies(DataAccess data) { - return #[ + protected static def List requiredLSDependencies(DataAccess data, boolean noModelDsl, boolean noDiff) { + val deps = newLinkedList( new Dependency("com.google.code.gson", "gson", "${gson-version}"), new Dependency("com.google.inject", "guice", "${guice-version}"), new Dependency("de.cau.cs.kieler.klighd", "de.cau.cs.kieler.kgraph.text", "${klighd-version}"), @@ -274,10 +298,18 @@ class SPVizGenerator extends AbstractGenerator { new Dependency("org.eclipse.xtend", "org.eclipse.xtend.lib", "${xtend-version}"), new Dependency("org.eclipse.xtext", "org.eclipse.xtext.ide", "${xtext-version}"), new Dependency("org.eclipse.xtext", "org.eclipse.xtext.xbase.lib", "${xtext-version}"), - new Dependency(data.bundleNamePrefix, data.bundleNamePrefix + ".viz", "${project.version}"), new Dependency(data.bundleNamePrefix, data.bundleNamePrefix + ".model", "${project.version}"), + new Dependency(data.bundleNamePrefix, data.bundleNamePrefix + ".viz", "${project.version}"), new Dependency(data.modelBundleNamePrefix, data.modelBundleNamePrefix + ".model", "${project.version}") - ] + ) + if (!noModelDsl) { + deps += new Dependency(data.modelBundleNamePrefix + ".model.dsl", data.modelBundleNamePrefix + ".model.dsl", "1.0.0-SNAPSHOT") + } + if (!noDiff) { + deps += new Dependency(data.bundleNamePrefix, data.bundleNamePrefix + ".diffviz", "${project.version}") + deps += new Dependency(data.modelBundleNamePrefix + ".diff.dsl", data.modelBundleNamePrefix + ".diff.dsl", "1.0.0-SNAPSHOT") + } + return deps } protected static def List requiredVizBundles(DataAccess data) { @@ -302,6 +334,29 @@ class SPVizGenerator extends AbstractGenerator { ] } + protected static def List requiredDiffVizBundles(DataAccess data) { + return #[ + "com.google.inject", + "de.cau.cs.kieler.klighd", + "de.cau.cs.kieler.klighd.krendering", + "de.cau.cs.kieler.klighd.krendering.extensions", + "org.eclipse.elk.alg.layered", + "org.eclipse.elk.core", + "org.eclipse.emf", + "org.eclipse.xtext", + "org.eclipse.xtext.xbase.lib", + data.modelBundleNamePrefix + ".model", + data.modelBundleNamePrefix + ".diff.dsl", + data.bundleNamePrefix + ".viz" + ] + } + + protected static def List exportedDiffVizPackages(DataAccess data) { + return #[ + data.bundleNamePrefix + ".diffviz" + ] + } + /** * Generates the content for the Model.xcore file * @@ -334,7 +389,7 @@ class SPVizGenerator extends AbstractGenerator { /////////////////////////////////////////////////////////////////////////////////////// /* - * Interface for visualization contexts of the (OSGi) model synthesis. Each context may contain child contexts, where each + * Interface for visualization contexts of the model synthesis. Each context may contain child contexts, where each * context will give the synthesis additional information in which state parts of the model should be generated in. * * @param The model element class this visualization context is for. diff --git a/plugins/de.cau.cs.kieler.spviz.spvizmodel/META-INF/MANIFEST.MF b/plugins/de.cau.cs.kieler.spviz.spvizmodel/META-INF/MANIFEST.MF index d4ae512..9f8e157 100644 --- a/plugins/de.cau.cs.kieler.spviz.spvizmodel/META-INF/MANIFEST.MF +++ b/plugins/de.cau.cs.kieler.spviz.spvizmodel/META-INF/MANIFEST.MF @@ -21,7 +21,8 @@ Require-Bundle: org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", org.eclipse.xtext.xbase, org.eclipse.xtext.xbase.lib;bundle-version="2.14.0", org.eclipse.xtend.lib;bundle-version="2.14.0", - slf4j.api + slf4j.api, + org.eclipse.xtext.xtext.wizard Bundle-RequiredExecutionEnvironment: JavaSE-17 Export-Package: de.cau.cs.kieler.spviz.spvizmodel, de.cau.cs.kieler.spviz.spvizmodel.generator, diff --git a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/GenerateModelMavenBuild.xtend b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/GenerateModelMavenBuild.xtend index bd47ae0..65372d8 100644 --- a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/GenerateModelMavenBuild.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/GenerateModelMavenBuild.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2022-2024 by + * Copyright 2022-2025 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -225,7 +225,7 @@ class GenerateModelMavenBuild { 7.0.0 3.0.2.v20240507 0.22.0 - 2.7.5 + 3.0.5 2.33.0 2.33.0 @@ -450,7 +450,7 @@ class GenerateModelMavenBuild { org.eclipse.tycho target-platform-configuration - [2.7.5,) + [3.0.5,) target-platform @@ -492,6 +492,8 @@ class GenerateModelMavenBuild { + + ««« @@ -510,6 +512,7 @@ class GenerateModelMavenBuild { + diff --git a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/SPVizModelGenerator.xtend b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/SPVizModelGenerator.xtend index 5e38a8f..c889fb2 100644 --- a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/SPVizModelGenerator.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/generator/SPVizModelGenerator.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2020-2024 by + * Copyright 2020-2026 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -25,6 +25,13 @@ import org.eclipse.emf.ecore.resource.Resource import org.eclipse.xtext.generator.AbstractGenerator import org.eclipse.xtext.generator.IFileSystemAccess2 import org.eclipse.xtext.generator.IGeneratorContext +import org.eclipse.xtext.util.JavaVersion +import org.eclipse.xtext.xtext.wizard.BuildSystem +import org.eclipse.xtext.xtext.wizard.LanguageDescriptor +import org.eclipse.xtext.xtext.wizard.LanguageDescriptor.FileExtensions +import org.eclipse.xtext.xtext.wizard.LineDelimiter +import org.eclipse.xtext.xtext.wizard.WizardConfiguration +import org.eclipse.xtext.xtext.wizard.cli.CliProjectsCreator import org.slf4j.Logger import org.slf4j.LoggerFactory @@ -41,10 +48,10 @@ class SPVizModelGenerator extends AbstractGenerator { override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) { val workspace = ResourcesPlugin.workspace val output = Paths.get(workspace.root.location.toString) - SPVizModelGenerator.generate(resource, output) + SPVizModelGenerator.generate(resource, output, false, false) } - static def void generate(Resource resource, Path rootPath) { + static def void generate(Resource resource, Path rootPath, boolean noModelDsl, boolean noDiff) { val rootDirectory = new File(rootPath.toAbsolutePath.toString) val SPVizModel model = resource.contents.head as SPVizModel @@ -80,6 +87,114 @@ class SPVizModelGenerator extends AbstractGenerator { // Generate a .generate scaffold for the model if not already existent. GenerateGeneratorScaffold.generate(rootDirectory, model, version) + + if (noDiff) { + LOGGER.info("Skip generating difference DSL.") + } else { + LOGGER.info("Generate difference DSL") + // Generate DiffDSL + val CliProjectsCreator creator = new CliProjectsCreator() + val WizardConfiguration config = new WizardConfiguration() => [ + rootLocation = rootPath.toAbsolutePath.toString + baseName = model.package + ".diff.dsl" + language.name = baseName + "." + model.name + "DiffDsl" + language.fileExtensions = FileExtensions.fromString(model.name.toLowerCase + "diff") + preferredBuildSystem = BuildSystem.MAVEN + javaVersion = JavaVersion.JAVA17 + ideProject.enabled = true + // ensures that META-INF/MANIFEST.MF will be generated for all projects + uiProject.enabled = true + // cannot find a way to also auto-generate .project files for Eclipse + ] + creator.lineDelimiter = LineDelimiter.UNIX.value + + creator.createProjects(config) + + // modify xtext grammar + val diffDslFolder = new File(config.rootLocation + "/" + config.baseName) + val diffDslPackageFolder = FileGenerator.createDirectory(diffDslFolder, "src/" + config.baseName.replace('.', '/')) + var content = generateDiffGrammar(model, config.language) + FileGenerator.updateFile(diffDslPackageFolder, model.name + "DiffDsl.xtext", content) + // TODO: only update this file and not do a full regeneration (only the referencedResource is missing) + content = generateDiffDslMwe2(model) + FileGenerator.updateFile(diffDslPackageFolder, "Generate" + model.name + "DiffDsl.mwe2", content) + // TODO: also only update this file, only the emf.ecore.xcore dependency is missing. + val diffDslManifestFolder = FileGenerator.createDirectory(diffDslFolder, "META-INF") + content = diffManifestContent(model) + FileGenerator.updateFile(diffDslManifestFolder, "MANIFEST.MF", content) + // TODO: also only update this file, only the emf.ecore.xcore.sdk.feature.group is missing. + val diffDslTargetSourceFolder = new File(config.rootLocation + "/" + config.baseName + ".target") + content = dslTargetPlatformContent(model, true) + FileGenerator.updateFile(diffDslTargetSourceFolder, config.baseName + ".target.target", content) + // The generated .ide plugin does not export the correct packages required by the also generated ui plugin, re-generate its MANIFEST.MF file + val diffDslIdeFolder = new File(config.rootLocation + "/" + config.baseName + ".ide") + content = dslIdeManifestContent(model, true) + FileGenerator.updateFile(FileGenerator.createDirectory(diffDslIdeFolder, "META-INF"), "MANIFEST.MF", content) + // The generated .ui plugin misses some imports in its Manifest as well + val diffDslUiFolder = new File(config.rootLocation + "/" + config.baseName + ".ui") + content = dslUiManifestContent(model, true) + FileGenerator.updateFile(FileGenerator.createDirectory(diffDslUiFolder, "META-INF"), "MANIFEST.MF", content) + } + + if (noModelDsl) { + LOGGER.info("Skip generating model DSL.") + } else { + LOGGER.info("Generate model DSL") + // Generate model DSL + val CliProjectsCreator creator = new CliProjectsCreator() + val WizardConfiguration config = new WizardConfiguration() => [ + rootLocation = rootPath.toAbsolutePath.toString + baseName = model.package + ".model.dsl" + language.name = baseName + "." + model.name + "Dsl" + language.fileExtensions = FileExtensions.fromString(model.name.toLowerCase + "dsl") + preferredBuildSystem = BuildSystem.MAVEN + javaVersion = JavaVersion.JAVA17 + ideProject.enabled = true + // ensures that META-INF/MANIFEST.MF will be generated for all projects + uiProject.enabled = true + // cannot find a way to also auto-generate .project files for Eclipse + ] + creator.lineDelimiter = LineDelimiter.UNIX.value + + creator.createProjects(config) + + // modify xtext grammar + val dslFolder = new File(config.rootLocation + "/" + config.baseName) + val dslPackageFolder = FileGenerator.createDirectory(dslFolder, "src/" + config.baseName.replace('.', '/')) + var content = generateDslGrammar(model, config.language) + FileGenerator.updateFile(dslPackageFolder, model.name + "Dsl.xtext", content) + // TODO: only update this file and not do a full regeneration (only the referencedResource is missing) + content = generateDslMwe2(model) + FileGenerator.updateFile(dslPackageFolder, "Generate" + model.name + "Dsl.mwe2", content) + // TODO: also only update this file, only the emf.ecore.xcore dependency is missing. + val dslManifestFolder = FileGenerator.createDirectory(dslFolder, "META-INF") + content = dslManifestContent(model) + FileGenerator.updateFile(dslManifestFolder, "MANIFEST.MF", content) + // TODO: also only update this file, only the emf.ecore.xcore.sdk.feature.group is missing. + val dslTargetSourceFolder = new File(config.rootLocation + "/" + config.baseName + ".target") + content = dslTargetPlatformContent(model, false) + FileGenerator.updateFile(dslTargetSourceFolder, config.baseName + ".target.target", content) + // The build properties falsely add the "plugin.xml" to the binary inclusions automatically, this removes that again. + content = generateDslBuildProperties(model) + FileGenerator.updateFile(dslFolder, "build.properties", content) + // The generated .ide plugin does not export the correct packages required by the also generated ui plugin, re-generate its MANIFEST.MF file + val dslIdeFolder = new File(config.rootLocation + "/" + config.baseName + ".ide") + content = dslIdeManifestContent(model, false) + FileGenerator.updateFile(FileGenerator.createDirectory(dslIdeFolder, "META-INF"), "MANIFEST.MF", content) + // The generated .ui plugin misses some imports in its Manifest as well + val dslUiFolder = new File(config.rootLocation + "/" + config.baseName + ".ui") + content = dslUiManifestContent(model, false) + FileGenerator.updateFile(FileGenerator.createDirectory(dslUiFolder, "META-INF"), "MANIFEST.MF", content) + + // Adapt the source files of the model DSL as in thesis so that it creates a correct model readable by the synthesis. + // RuntimeModule + content = generateRuntimeModule(model) + FileGenerator.updateFile(dslPackageFolder, model.name + "DslRuntimeModule.java", content) + // Resource + content = generateResource(model) + FileGenerator.updateFile(dslPackageFolder, model.name + "DslResource.xtend", content) + } + } /** @@ -166,4 +281,483 @@ class SPVizModelGenerator extends AbstractGenerator { «ENDFOR» ''' } -} \ No newline at end of file + + private static def String generateDiffGrammar(SPVizModel model, LanguageDescriptor language) { + return ''' + grammar «model.package».diff.dsl.«model.name»DiffDsl with org.eclipse.xtext.common.Terminals + + generate «model.name.toFirstLower»DiffDsl "«language.nsURI»/«model.name»DiffDsl" + + import "«model.package».model" as «model.name»Model + + «model.name»Diff: + 'compare' sourceModel=STRING + 'to' targetModel=STRING + ; + ''' + + } + + private static def String generateDslGrammar(SPVizModel model, LanguageDescriptor language) { + return ''' + grammar «model.package».model.dsl.«model.name»Dsl with org.eclipse.xtext.common.Terminals + + import "«model.package».model" + import "http://www.eclipse.org/emf/2002/Ecore" as ecore + + «model.name»Project returns «model.name»Project: + ('projectName' projectName=EString)? + ( + «FOR artifact : model.artifacts SEPARATOR " |"» + «artifact.name.toFirstLower»s += «artifact.name.toFirstUpper» + «ENDFOR» + )* + ; + + «FOR artifact : model.artifacts» + «artifact.name.toFirstUpper» returns «artifact.name.toFirstUpper»: + (external?='external')? + '«artifact.name.toFirstLower»' + name=EString + ('{' + «FOR containment : artifact.references.filter(Containment)» + ('«containment.contains.name.toFirstLower»s:' '[' «containment.contains.name.toFirstLower»s += [«containment.contains.name.toFirstUpper»|EString] ( "," «containment.contains.name.toFirstLower»s += [«containment.contains.name.toFirstUpper»|EString])* ']' )? + «ENDFOR» + «FOR connection : artifact.references.filter(Connection)» + ('«connection.name.toFirstLower»' connected«connection.name.toFirstUpper»«connection.connects.name.toFirstUpper»s += [«connection.connects.name.toFirstUpper»|EString] ('«connection.name.toFirstLower»' connected«connection.name.toFirstUpper»«connection.connects.name.toFirstUpper»s += [«connection.connects.name.toFirstUpper»|EString])*)? + «ENDFOR» + '}')? + ; + + «ENDFOR» + EString returns ecore::EString: + STRING | ID + ; + ''' + } + + private static def String generateDiffDslMwe2(SPVizModel model) { + return ''' + module «model.package».diff.dsl.Generate«model.name»DiffDsl + + import org.eclipse.xtext.xtext.generator.* + import org.eclipse.xtext.xtext.generator.model.project.* + + var rootPath = ".." + + Workflow { + + component = XtextGenerator { + configuration = { + project = StandardProjectConfig { + baseName = "«model.package».diff.dsl" + rootPath = rootPath + eclipsePlugin = { + enabled = true + } + createEclipseMetaData = true + } + code = { + encoding = "UTF-8" + lineDelimiter = "\n" + fileHeader = "/*\n * generated by SPViz and Xtext \${version}\n */" + preferXtendStubs = false + } + } + language = StandardLanguage { + name = "«model.package».diff.dsl.«model.name»DiffDsl" +««« This is the important missing line + referencedResource = "platform:/resource/«model.package».model/model/«model.name»Model.xcore" + fileExtensions = "«model.name.toLowerCase»diff" + + serializer = { + generateStub = false + } + validator = { + // composedCheck = "org.eclipse.xtext.validation.NamesAreUniqueValidator" + // Generates checks for @Deprecated grammar annotations, an IssueProvider and a corresponding PropertyPage + generateDeprecationValidation = true + } + generator = { + generateXtendStub = true + } + } + } + } + ''' + } + + private static def String generateDslMwe2(SPVizModel model) { + return ''' + module «model.package».model.dsl.Generate«model.name»Dsl + + import org.eclipse.xtext.xtext.generator.* + import org.eclipse.xtext.xtext.generator.model.project.* + + var rootPath = ".." + + Workflow { + + component = XtextGenerator { + configuration = { + project = StandardProjectConfig { + baseName = "«model.package».model.dsl" + rootPath = rootPath + eclipsePlugin = { + enabled = true + } + createEclipseMetaData = true + } + code = { + encoding = "UTF-8" + lineDelimiter = "\n" + fileHeader = "/*\n * generated by SPViz and Xtext \${version}\n */" + preferXtendStubs = false + } + } + language = StandardLanguage { + name = "«model.package».model.dsl.«model.name»Dsl" + fileExtensions = "«model.name.toLowerCase»dsl" + referencedResource = "platform:/resource/«model.package».model/model/«model.name»Model.xcore" + + fragment = ecore2xtext.Ecore2XtextValueConverterServiceFragment2 auto-inject {} + + serializer = { + generateStub = false + } + validator = { + // composedCheck = "org.eclipse.xtext.validation.NamesAreUniqueValidator" + // Generates checks for @Deprecated grammar annotations, an IssueProvider and a corresponding PropertyPage + generateDeprecationValidation = true + } + generator = { + generateXtendStub = true + } + } + } + } + ''' + } + + private static def String dslTargetPlatformContent(SPVizModel model, boolean diff) { + return ''' + + + + + + + + + + +««« Only this line would be missing otherwise + + + + + + + + + + + + + + + + + + + + + + + + + + ''' + } + + private static def String diffManifestContent(SPVizModel model) { + return ''' + Manifest-Version: 1.0 + Bundle-ManifestVersion: 2 + Bundle-Name: «model.package».diff.dsl + Bundle-Vendor: SPViz + Bundle-Version: 1.0.0.qualifier + Bundle-SymbolicName: «model.package».diff.dsl; singleton:=true + Bundle-ActivationPolicy: lazy + Require-Bundle: «model.package».model, + org.eclipse.xtext, +««« missing line follows: + org.eclipse.emf.ecore.xcore, + org.eclipse.xtext.xbase, + org.eclipse.equinox.common;bundle-version="3.16.0", + org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", + org.eclipse.emf.ecore, + org.eclipse.xtext.xbase.lib;bundle-version="2.14.0", + org.eclipse.xtext.util, + org.eclipse.emf.common + Bundle-RequiredExecutionEnvironment: JavaSE-17 + Automatic-Module-Name: «model.package».diff.dsl + Export-Package: «model.package».diff.dsl, + «model.package».diff.dsl.scoping, + «model.package».diff.dsl.«model.name.toFirstLower»DiffDsl.util, + «model.package».diff.dsl.services, + «model.package».diff.dsl.parser.antlr, + «model.package».diff.dsl.serializer, + «model.package».diff.dsl.validation, + «model.package».diff.dsl.«model.name.toFirstLower»DiffDsl, + «model.package».diff.dsl.generator, + «model.package».diff.dsl.«model.name.toFirstLower»DiffDsl.impl, + «model.package».diff.dsl.parser.antlr.internal + Import-Package: org.apache.log4j + ''' + } + + private static def String dslManifestContent(SPVizModel model) { + return ''' + Manifest-Version: 1.0 + Bundle-ManifestVersion: 2 + Bundle-Name: «model.package».model.dsl + Bundle-Vendor: SPViz + Bundle-Version: 1.0.0.qualifier + Bundle-SymbolicName: «model.package».model.dsl; singleton:=true + Bundle-ActivationPolicy: lazy + Require-Bundle: «model.package».model, + org.eclipse.xtext, +««« missing line follows: + org.eclipse.emf.ecore.xcore, + org.eclipse.xtext.xbase, + org.eclipse.equinox.common;bundle-version="3.16.0", + org.antlr.runtime;bundle-version="[3.2.0,3.2.1)", + org.eclipse.emf.ecore, + org.eclipse.xtext.xbase.lib;bundle-version="2.14.0", + org.eclipse.xtext.util, + org.eclipse.emf.common + Bundle-RequiredExecutionEnvironment: JavaSE-17 + Automatic-Module-Name: «model.package».model.dsl + Export-Package: «model.package».model.dsl, + «model.package».model.dsl.scoping, + «model.package».model.dsl.services, + «model.package».model.dsl.parser.antlr, + «model.package».model.dsl.serializer, + «model.package».model.dsl.validation, + «model.package».model.dsl.generator, + «model.package».model.dsl.parser.antlr.internal + Import-Package: org.apache.log4j + ''' + } + + private static def String dslIdeManifestContent(SPVizModel model, boolean diff) { + val String dslPackageName = model.package + "." + (diff ? "diff" : "model") + ".dsl" + val String idePackageName = dslPackageName + ".ide" + val String uiPackageName = dslPackageName + ".ui" + return ''' + Manifest-Version: 1.0 + Bundle-ManifestVersion: 2 + Bundle-Name: «idePackageName» + Bundle-Vendor: My Company + Bundle-Version: 1.0.0.qualifier + Bundle-SymbolicName: «idePackageName»; singleton:=true + Bundle-ActivationPolicy: lazy + Require-Bundle: «dslPackageName», + org.eclipse.xtext.ide, + org.eclipse.xtext.xbase.ide + Bundle-RequiredExecutionEnvironment: JavaSE-17 + Automatic-Module-Name: «idePackageName» + Export-Package: «idePackageName».contentassist.antlr, + «idePackageName».contentassist.antlr.internal;x-friends:="«uiPackageName»" + ''' + } + + private static def String dslUiManifestContent(SPVizModel model, boolean diff) { + val String dslPackageName = model.package + "." + (diff ? "diff" : "model") + ".dsl" + val String idePackageName = dslPackageName + ".ide" + val String uiPackageName = dslPackageName + ".ui" + return ''' + Manifest-Version: 1.0 + Bundle-ManifestVersion: 2 + Bundle-Name: «uiPackageName» + Bundle-Vendor: SPViz + Bundle-Version: 1.0.0.qualifier + Bundle-SymbolicName: «uiPackageName»; singleton:=true + Bundle-ActivationPolicy: lazy + Require-Bundle: «dslPackageName», + «idePackageName», + org.eclipse.xtext.ui, + org.eclipse.xtext.ui.shared, + org.eclipse.xtext.ui.codetemplates.ui, + org.eclipse.ui.editors;bundle-version="3.14.300", + org.eclipse.ui.ide;bundle-version="3.18.500", + org.eclipse.compare, + org.eclipse.xtext.builder + Import-Package: org.apache.log4j + Bundle-RequiredExecutionEnvironment: JavaSE-17 + Automatic-Module-Name: «uiPackageName» + ''' + } + + private static def String generateDslBuildProperties(SPVizModel model) { + return ''' + source.. = src/,\ + src-gen/,\ + xtend-gen/ + bin.includes = .,\ + META-INF/ + bin.excludes = **/*.mwe2,\ + **/*.xtend + additional.bundles = org.eclipse.xtext.xbase,\ + org.eclipse.xtext.common.types,\ + org.eclipse.xtext.xtext.generator,\ + org.eclipse.emf.codegen.ecore,\ + org.eclipse.emf.mwe.utils,\ + org.eclipse.emf.mwe2.launch,\ + org.eclipse.emf.mwe2.lib,\ + org.objectweb.asm,\ + org.apache.commons.logging,\ + org.apache.log4j + ''' + } + + private static def String generateRuntimeModule(SPVizModel model) { + return ''' + /* + * generated by SPViz + */ + package «model.package».model.dsl; + + import org.eclipse.xtext.resource.XtextResource; + + /** + * Use this class to register components to be used at runtime / without the Equinox extension registry. + */ + public class «model.name»DslRuntimeModule extends Abstract«model.name»DslRuntimeModule { + @Override + public Class bindXtextResource() { + return «model.name»DslResource.class; + } + } + ''' + } + + private static def String generateResource(SPVizModel model) { + return ''' + package «model.package».model.dsl + + import java.util.HashMap + import java.util.Map + import org.eclipse.emf.common.notify.impl.NotifyingListImpl + import org.eclipse.xtext.linking.lazy.LazyLinkingResource + import org.eclipse.xtext.parser.IParseResult + «FOR artifact : model.artifacts» + import «model.package».model.«artifact.name» + «ENDFOR» + + /** + * A customized {@link LazyLinkingResource}. Modifies the parsed model and adds an ID based on the element name. + * + * @author nre & mam + */ + class «model.name»DslResource extends LazyLinkingResource { + + «FOR artifact : model.artifacts» + public static val «artifact.name.toUpperCase»_ID_PREFIX = "«artifact.name»_" + «ENDFOR» + + override void updateInternalState(IParseResult parseResult) { + super.updateInternalState(parseResult) + // Give each element a default ID based on the name if it is not explicitly set. + if (parseResult.rootASTElement !== null) { + parseResult.rootASTElement.eAllContents.forEach[ element | + switch (element) { + «FOR artifact : model.artifacts» + «artifact.name»: { + val «artifact.name.toFirstLower» = element as «artifact.name» + if («artifact.name.toFirstLower».ecoreId === null) { + «artifact.name.toFirstLower».ecoreId = «artifact.name.toUpperCase»_ID_PREFIX + «artifact.name.toFirstLower».name.toAscii + } + // resolve all opposite relations, as Xtext fails to properly set them up here. + «FOR connection : artifact.references.filter(Connection)» + «artifact.name.toFirstLower».connected«connection.name»«connection.connects.name»s.forEach [ connected | + if ( + !connected.connecting«connection.name»«artifact.name»s.contains(«artifact.name.toFirstLower») + && connected.connecting«connection.name»«artifact.name»s instanceof NotifyingListImpl + ) { + (connected.connecting«connection.name»«artifact.name»s as NotifyingListImpl<«artifact.name»>).basicAdd(«artifact.name.toFirstLower», null) + } + ] + «ENDFOR» + «FOR containment : artifact.references.filter(Containment)» + «artifact.name.toFirstLower».«containment.contains.name.toFirstLower»s.forEach [ «containment.contains.name.toFirstLower» | + if ( + !«containment.contains.name.toFirstLower».«artifact.name.toFirstLower»s.contains(«artifact.name.toFirstLower») + && «containment.contains.name.toFirstLower».«artifact.name.toFirstLower»s instanceof NotifyingListImpl + ) { + («containment.contains.name.toFirstLower».«artifact.name.toFirstLower»s as NotifyingListImpl<«artifact.name»>).basicAdd(«artifact.name.toFirstLower», null) + } + ] + «ENDFOR» + } + «ENDFOR» + default: { + // nop + } + } + ] + } + } + + /** + * Converts the given name to an ASCII string save for using in an Ecore ID. + * German umlauts are converted to their long form counterparts (e.g., ä->ae) + * and special characters not in the alphabet are replaced by underscores (_). + * + * @param name The name to convert to an ASCII string + * @return An ASCII-only version of the string. + */ + def private String toAscii(String name) { + if (name === null) return null + + val Map mappings = new HashMap + mappings.put('Ä', "Ae") + mappings.put('ä', "ae") + mappings.put('Ö', "Oe") + mappings.put('ö', "oe") + mappings.put('Ü', "Ue") + mappings.put('ü', "ue") + mappings.put('ẞ', "Ss") + mappings.put('ß', "ss") + + val StringBuilder sb = new StringBuilder() + name.chars().forEachOrdered([character | + // Replace all known mappings to readable allowable ID substrings + + // character literals in xtend are dumb ~ nre mam + val char A = 'A' + val char Z = 'Z' + val char a = 'a' + val char z = 'z' + val char dot = '.' + val char dash = '-' + + if (mappings.containsKey(character as char)) { + sb.append(mappings.get(character as char)) + // Keep all A-Z,a-z and .- the same. + } else if (character >= A && character <= Z || character >= a && character <= z || character === dot || character === dash) { + sb.append(character as char) + // Replace all other characters by _ + } else { + sb.append('_') + } + ]) + + return sb.toString(); + } + + } + ''' + } +} diff --git a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/util/SPVizModelExtension.xtend b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/util/SPVizModelExtension.xtend index f7a066a..5938cf0 100644 --- a/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/util/SPVizModelExtension.xtend +++ b/plugins/de.cau.cs.kieler.spviz.spvizmodel/src/de/cau/cs/kieler/spviz/spvizmodel/util/SPVizModelExtension.xtend @@ -3,7 +3,7 @@ * * http://rtsys.informatik.uni-kiel.de/kieler * - * Copyright 2021-2023 by + * Copyright 2021-2025 by * + Kiel University * + Department of Computer Science * + Real-Time and Embedded Systems Group @@ -18,6 +18,7 @@ package de.cau.cs.kieler.spviz.spvizmodel.util import de.cau.cs.kieler.spviz.spvizmodel.sPVizModel.Artifact import de.cau.cs.kieler.spviz.spvizmodel.sPVizModel.Connection +import de.cau.cs.kieler.spviz.spvizmodel.sPVizModel.Containment /** * Utility extension class for usability of the SPVizmodel classes. @@ -62,4 +63,24 @@ class SPVizModelExtension { && c1.connecting.name.equals(c2.connecting.name) } + /** + * Returns all contained artifacts of this artifact. + * + * @param theArtifact the parent artifact to search in. + * @return all artifacts that are contained within this. + */ + static def getContainedArtifacts(Artifact theArtifact) { + return theArtifact.references.filter(Containment).map[contains] + } + + /** + * Returns all connected artifacts of this artifact. + * + * @param theArtifact the source artifact to search in. + * @return all artifacts that are connected within this. + */ + static def getConnectedArtifacts(Artifact theArtifact) { + return theArtifact.references.filter(Connection).map[connected] + } + } \ No newline at end of file