-
Notifications
You must be signed in to change notification settings - Fork 3
Create Transformations between Textual and Graphical Presentations
The goal of this page is creating transformations between textual and graphical representations of a UML diagram. This requires the following steps:
- QVT-O
- Model to model transformation language
- Important topics: General information about QVT-O language including incremental execution, language specifications only on demand
- General introduction [1] [2]
- Language specifications QVT 1.3 OCL
- Incremental execution informal description
-
EMF
- Modeling framework for Eclipse
- Important topics: Creating a meta model, generating code
-
UML 2.5
- Official UML standard
- Important topics depend on the chosen diagram type, you should read this on demand
We need one project hosting the transformations as well as helper code, and another one hosting the test cases.
-
Create a project called
de.cooperateproject.modeling.transformation.XYZand replace XYZ by the name you used in your textual editor plugin name.- Add dependencies to
org.eclipse.m2m.qvt.omlde.cooperateproject.modeling.transformation.common- your meta model plugin
de.cooperateproject.modeling.transformation.registry
- Create a folder
transformsin the project - Add the transforms folder to the binary includes in the build properties
- Create an activator that exposes the plugin id of the project (see the ClsActivator as an example)
- Add an extension for the extension point 'de.cooperateproject.modeling.common.types' with the following characteristics
- 'name' with an intuitive name for the diagram type (for the class diagram this is
Class) - 'papyrusDiagramKind' with the
diagramKindIdfrom thePapyrusDiagramStyleelement in the notation model (for the class diagram this isorg.eclipse.papyrus.uml.diagram.class) - 'papyrusDiagramType' with the
typeattribute of theDiagramelement in the notation model (for the class diagram this isPapyrusUMLClassDiagram) - 'textualFileExtension' with the file extension of your textual diagram files (for the class diagram this is
cls)
- 'name' with an intuitive name for the diagram type (for the class diagram this is
- Add an extension for the extension point
de.cooperateproject.modeling.transformation.registry.transformationfactorieswith the following characteristics -
transformationfolderwithfoldernametransforms -
defaulttransformationwith-
diagramNamewith the diagram name you have chosen above -
graphicalFileExtensionwithnotation
-
- postprocessor
-
directionwithtextual to graphical -
classwith a newly created classT2GPostProcessorthat looks like the post processor for class diagrams
-
- Add dependencies to
-
Create a fragment bundle project called
de.cooperateproject.modeling.transformation.XYZ.testsand replace XYZ by the name you used in your textual editor plugin name.- Set the host plugin to your transformation plugin
- Add dependencies to
de.cooperateproject.modeling.transformation.tests.commonorg.junitorg.eclipse.emf.comparecom.google.guava
- Create an abstract class
XYZDirectionalTransformationTestBaseaccording to the example given for the class diagram - Create an abstract class
XYZDirectionalTraceTransformationTestBaseaccording to the example given for the class diagram - Create a test class called
GraphicalToTextualXYZTestaccording to the example given for the class diagram. You can omit the methods marked with@Test. - Create a test class called
GraphicalToTextualXYZTraceTestaccording to the example given for the class diagram. You can omit the methods marked with@Test. - Create a test class called
TextualToGraphicalXYZTestaccording to the example given for the class diagram. You can omit the methods marked with@Test. - Create a test class called
TextualToGraphicalXYZTraceTestaccording to the example given for the class diagram. You can omit the methods marked with@Test.
Our objective is to create a set of transformations that transform textual and graphical diagrams in a bidirectional and incremental way. Bidirectional means that the transformation can be executed in both directions. Incremental means that only a small set of changes is applied to an existing model that describes the required changes. The opposite would be a transformation that completely regenerates the target diagram. This, however, would remove all information that is not part of the transformation description, e.g. because there is no matching element that represents this information in the other diagram. We achieve this by coupling two unidirectional transformations and by transforming the trace record of the executed transformation into the trace record of the opposite transformation. For a detailed explanation of the approach, please refer to our publication.
In preparation for creating the transformations, please create the following two library files in the transforms folder
-
TransformationUtils_XYZ.qvto- We will place string constants in this library later. For an example, please refer to the example for the class diagram.
- Add the following lines
library TransformationUtils_XYZ;
import TransformationUtils;
-
TraceUtils_XYZ.qvto- This might contain queries and helpers required for the trace transformation later
- Add the following lines
import TraceUtils;
import TransformationUtils_XYZ;
library TraceUtils_XYZ;
We cannot cover all ways of mapping diagrams because these mappings are often very specific for the covered diagram type. Instead, we give some general hints on creating mappings and finding necessary information.
You have to reverse engineer the created graphical diagram model in order to define the transformation. Create a graphical diagram using the Papyrus editor and have a look at the produced output. You should do this step-wise for single elements. Please note that element types in the graphical diagram may change if they are nested. Therefore, you usually have to treat nested elements different than non nested elements.
We suggest working element-wise and test your transformation afterwards. This means that you start with mapping the root element, write the transformation for it, create a test case, and verify your transformation. Afterwards, you should choose a model element that is directly contained by an element that you already mapped. Even if we describe creating transformations and testing transformations separately, you should mix these two taks.
You must not use transient features in your transformation. You created transient features during the definition of the textual diagram and created state calculators to provide values for them during runtime. Our framework does, however, not guarantee that the state calculators have been executed before the transformation is executed. Therefore, you cannot rely on the values of these features. On the other side, you do not have to assign values to transient features because they will not be persisted anyway.
Avoid incremental executing breaking statements to ensure proper operation during runtime. Breaking operations are conditional blocks that are no inline conditionals, and element creation with the object operation. Incrementally breaks as well if you use unordered data structures (e.g. sets) in your statements. You can almost always avoid this by using an order preserving version. If your model yields a set as value of a feature, please consider setting the ordered attribute of the feature in the meta model to true. Additionally, do not define mappings with the same name because the incremental execution mode cannot distinguish them. Use the reset assignment operator := instead of the addition operator += if possible to avoid duplicated entries. For a complete list, please refer to our publication in Section 3.1.
Use library features whenever possible. You already include these libraries if you use the templates we provide in this tutorial. The contents of these libraries can be found by following the references in the editor or using git. We provide common queries and mappings as well as string constants.
Create trace transformations late or at least after you are pretty sure that your regular transformations do not change too much anymore. Otherwise, you have to adjust the trace transformations with every change in the regular transformation, most probably.
Every element in the graphical diagram has a type id that clearly identifies the type of shape or edge. An overview on available type ids can be found in the Eclipse Wiki. The ids are, however, visible in the XMI representation of the graphical diagram as well. You can use the IDs to avoid instance of tests via OCL. We usually store the IDs as constants in the library file.
The transformation maps the textual diagram to its graphical counterpart. You have to create a transformation named Textual_to_Graphical_for_XYZ.qvto in the transforms folder. XYZ has to match the diagram name you defined in the extension point in the previous section.
The transformation starts with the following header:
import TransformationUtils_XYZ;
transformation Textual_to_Graphical_for_XYZ(in textual : TXT, inout graphical : NOTATION, in umlmodel : UML, in umlprimitives : UML);
modeltype NOTATION uses notation('http://www.eclipse.org/gmf/runtime/1.0.2/notation');
modeltype UML uses uml('http://www.eclipse.org/uml2/5.0.0/UML');
modeltype TXT uses xyz('http://www.cooperateproject.de/modeling/textual/xyz/XYZ');
modeltype ECORE uses ecore('http://www.eclipse.org/emf/2002/Ecore');
modeltype STYLE uses style('http://www.eclipse.org/papyrus/infra/gmfdiag/style');
modeltype TXTCMN uses textualCommons('http://www.cooperateproject.de/modeling/textual/commons');
main() {
var diagrams := textual.rootObjects()[xyz::XYZDiagram];
assert fatal (diagrams->size() = 1) with log ("Expected one diagram but got other number.", diagrams->size());
diagrams->map diagramToDiagram();
}
The mapping of the root node usually follows the following scheme, where you set default attributes in the graphical diagram and map edges and children (which are shapes). Have a look at the graphical diagram in a text editor for finding correct values. You can have a look at a GIT commit for finding the diagram type (class diagram: PapyrusUMLClassDiagram). Diagram kind IDs can be found in a mailing list message (class diagram: org.eclipse.papyrus.uml.diagram.class).
mapping xyz::XYZDiagram::diagramToDiagram() : notation::Diagram {
type := "...";
name := self.title;
measurementUnit := notation::MeasurementUnit::Pixel;
var umlRootElement := self.rootPackage.referencedElement.oclAsType(uml::Package);
element := umlRootElement.toEObject();
children += ...
edges += ...
styles := self.map stringValueStyle();
styles += self.map diagramStyle();
styles += self.map papyrusViewStyle(umlRootElement);
}
mapping xyz::XYZDiagram::papyrusViewStyle(rootElement : uml::Package) : style::PapyrusDiagramStyle
{
owner := rootElement.toEObject();
diagramKindId := "...";
}
The remaining mappings highly depend on the diagram type. You can have a look at existing transformations such as the one for class diagrams to get an idea of how a transformation should look like.
The transformation maps the graphical diagram to its textual counterpart. You have to create a transformation named Graphical_to_Textual_for_XYZ.qvto in the transforms folder. XYZ has to match the diagram name you defined in the extension point in the previous section.
The transformation starts with the following header:
import TransformationUtils_Class;
transformation Grahpical_to_Textual_for_XYZ(in graphical : NOTATION, inout textual : TXT, in umlmodel : UML, in umlprimitives : UML);
modeltype NOTATION uses notation('http://www.eclipse.org/gmf/runtime/1.0.2/notation');
modeltype UML uses uml('http://www.eclipse.org/uml2/5.0.0/UML');
modeltype TXT uses xyz('http://www.cooperateproject.de/modeling/textual/xyz/XYZ');
modeltype ECORE uses ecore('http://www.eclipse.org/emf/2002/Ecore');
modeltype TXTCMN uses textualCommons('http://www.cooperateproject.de/modeling/textual/commons');
main() {
var diagrams := graphical.rootObjects()[notation::Diagram]->select(type="...");
assert fatal (diagrams->size() = 1);
diagrams->map diagramToDiagram();
}
The mapping of the root node usually follows the following scheme.
mapping notation::Diagram::diagramToDiagram() : xyz::XYZDiagram {
title := self.name;
rootPackage := self.map diagramToRootPackage();
}
mapping notation::Diagram::diagramToRootPackage() : xyz::Package {
referencedElement := self.element.oclAsType(uml::Package);
...
}
The remaining mappings highly depend on the diagram type. You can have a look at existing transformations such as the one for class diagrams to get an idea of how a transformation should look like.
In previous steps, you already created the test infrastructure. Now, you can simply add test cases for your transformations. We suggest to start with simple, minimal test cases and extend them as soon as you support more elements in your transformation. You should, however, keep the minimal test cases to ease identification of errors.
In order to create a test case, you have to create the test models in the first place. The best approach is to create the graphical model using Papyrus and create the textual model by hand afterwards. You have to create the textual model as an XMI file and not using the concrete textual syntax because we want to test the transformations independently, i.e. without dependency to the parser framework. When naming the models, choose the same name for all models, so only the file extension differs. Put all models in a folder called models in the test fragment.
After you created the models, you can simply add the following methods to the test class.
@Test
public void test1() throws Exception {
testRegular("fileNameWithoutExtension");
}
@Test
public void test1Incremental() throws Exception {
testIncremental("fileNameWithoutExtension");
}
The test framework will take care of all necessary steps for testing the transformation. Finally, you receive a message indicating if there were any differences. If this is the case, you can activate a debug serialization of the expected and the actual transformation result by adding the statement setDebugSerializationDir(new File("directory")); before the call to the test method.
If the first test fails, you have some errors in your mapping between the diagram types. Another option is that you have an issue in your model files such as the order of elements does not match. If the latter holds true, just reorder the elements in the model files util they match. This will not be an issue later because the incremental execution mode will ensure that matching elements will be updated.
If the second test fails, your transformation is not executable in an incremental way. This can happen if you use language constructs that are not supported in incremental execution mode as stated above. The second test can fail or succeed independently of the first test.
We require trace transformations to allow incremental execution of our two previosly created unidirectional transformations. A transformation trace contains information about executed mappings including their inputs and output. If we feed a trace into a transformation, the transformation can be executed in an incremental mode. This means that for every mapping that has a matching (name and inputs match) trace record, the output element is set to the output element stored in the trace record. Thereby, elements are not recreated but updated by the mapping. A trace record is only valid for the transformation that run during its creation. This means that a trace record for the text to graphic transformation cannot be used for the graphic to text transformation.
To overcome this issue, we create trace transformations. In a simple case, this transformation switches the context element (the element for which the mapping operation has been executed) and the result element (that has been created by the mapping operation) and adjusts the name. For instance, if a mapping operation Package2Shape has been executed for a package p in a textual diagram to a shape s in a graphical diagram, a text to graphic transformation would yield a trace record for the mapping operation Shape2Package with shape s as input and package p as output. There are more complex cases where one trace record in one direction (e.g. text to graphic) yiels many trace records in the other direction (e.g. graphic to text). Names do not have to apply to this scheme and might be different.
It is highly recommended to test your trace transformations as early as possible. In a later section, we describe the testing approach in detail. We, however, first describe how trace transformations are written in general.
Trace transformations are unidirectional as well. Therefore, a single trace transformation might transform a trace of a graphic to text (G2T) transformations to a trace of a text to graphic (T2G) transformation but not vice versa. You have to create two trace transformations:
- Textual_to_Graphical_for_XYZ_Trace.qvto
- executed after Textual_to_Graphical_for_XYZ.qvto
- input: trace of T2G transformation execution
- output: trace of a G2T transformation execution
- Graphical_to_Textual_for_XYZ_Trace.qvto
- executed after Graphical_to_Textual_for_XYZ.qvto
- input: trace of G2T transformation execution
- output: trace of a T2G transformation execution
An example of the header for a T2G trace transformation is shown below:
import TraceUtils_Class;
transformation Textual_to_Graphical_for_Class_Trace(in textualTrace : TRACE, out graphicalTrace : TRACE, in graphicalModel : GRAPHICAL, in g2tTransformation : QVTO);
modeltype QVTO uses qvtoperational('http://www.eclipse.org/QVT/1.0.0/Operational');
modeltype TRACE uses trace('http:///www.eclipse.org/m2m/qvt/operational/trace.ecore');
modeltype ECORE uses ecore('http://www.eclipse.org/emf/2002/Ecore');
modeltype TEXTUAL uses cls('http://www.cooperateproject.de/modeling/textual/xyz/XYZ');
modeltype GRAPHICAL uses notation('http://www.eclipse.org/gmf/runtime/1.0.2/notation');
modeltype UML uses uml('http://www.eclipse.org/uml2/5.0.0/UML');
modeltype TXTCMN uses textualCommons('http://www.cooperateproject.de/modeling/textual/commons');
main() {
var traces := textualTrace.rootObjects()[trace::Trace];
assert fatal (traces->size() = 1);
traces->initializeProperties();
traces->map trace();
}
helper trace::Trace::initializeProperties() {
TARGET_TRANSFORMATION_NAME := "PapyrusClass2Text";
TARGET_TRANSFORMATION := g2tTransformation.rootObjects()[qvtoperational::expressions::OperationalTransformation]->any(true);
}
It is crucial to fill in the properties correctly to make use of the library features we provide. Without the library features, your trace transformations will become pretty verbose. The target transformation name is of the form Grahpical_to_Textual_for_XYZ for a T2G trace transformation. For a G2T trace transformation it is Textual_to_Graphical_for_XYZ.
If you use this header for a G2T trace transformation, you have to switch positions of the input parameters and switch the input model from the graphical to the textual model.
AFter adding the header, we usually use the following base mappings. Please note that we do not execute the trace transformation incrementally. Therefore, the restrictions regarding mutable objects are not in charge anymore.
mapping trace::Trace::trace() : trace::Trace
{
traceRecords += self.traceRecords->map traceRecord()->flatten();
}
mapping trace::TraceRecord::traceRecord() : List(trace::TraceRecord)
{
result += self.map traceRecordElement();
result += self.map traceRecordElement2();
...
}
You perform the actual trace record mappings in the traceRecordElement mappings. The code above simply calls all mappings (after you added them) and tries to create a trace record. The single mappings are guarded with when statements that decide if the currently processed trace record can be handled. If so, they create trace records.
An example for such a mapping is given below. The excerpt handles the mapping operation diagramToDiagram in a trace record of the T2G trace transformation. The incoming trace record recorded the mapping from a textual class diagram element to a graphical class diagram element. To access the graphical diagram element, we use the query getResult on the currently processed trace record and access the modelElement property. To access the textual diagram element, we use the query getSelf on the trace record and access the modelElement property. The yielded elements are always typed as EObject. To access further properties of them, we have to cast them to the appropriate type.
Because the graphical diagram element is mapped to two textual elements, the mapping has to yield two trace records. Remember: The result of the whole T2G trace transformation will be a trace to be used with the G2T transformation. We create two TraceRecord elements. The first statement covers the diagramToDiagram mapping operation. The name of the mapping operation is the first argument. The second and third argument is the name of the context model element and the actual context model element. The fourth and fifth arguments are the name of the result objects and the actual result object. The constructor only takes model elements explicitly typed as EObject. You can use the query eobject to cast any model element to EObject. The second statement covers the mapping operation diagramToRootPackage in a similar way.
mapping trace::TraceRecord::traceRecordDiagram() : List(trace::TraceRecord)
when {self.mappingOperation.name = "diagramToDiagram"}
{
var notationDiagram := self.getResult().modelElement;
var classDiagram := self.getSelf().modelElement.oclAsType(cls::ClassDiagram);
result += new TraceRecord("diagramToDiagram", "Diagram", notationDiagram, "ClassDiagram", classDiagram.eobject());
result += new TraceRecord("diagramToRootPackage", "Diagram", notationDiagram, "Package", classDiagram.rootPackage.eobject());
}
In order to create such mappings, the best approach is to have a look at the model transformations for both directions (T2G and G2T) at the same time. Take a mapping operation from the source transformation (for a T2G trace transformation, the source transformation would be the T2G transformation and the G2T transformation would be the target transformation) and look for mappings in the target transformation that yield the context type. In the mapping operation for this mapping operation's trace record, you have to create trace records for all mappings that yield this type as well as dependent mappings for which no better trace record exists.
Because creating trace transformations is quite challenging and highly depends on the concrete transformations, it is hard to give more detailled instructions. It is always beneficial to have a look at exsting trace transformations such as the ones for the class diagram.
You already created the infrastructure for the trace transformation tests before. You already created test models for the model transformation tests as well. The only thing you have to do is to add a test to the appropriate test class. A test method can look like this:
@Test
public void testWithoutPackages() throws Exception {
testTraceTransformation("fileNameWithoutExtension");
}
Again, if the test fails, you can simply use the setDebugSerializationDir(new File(pathToDirectory)); statement to activate debug serialization of the expected and the actual transformation result. The traces will already be sorted by name and context. Thereby, you can simply use a side-by-side text comparison tool to find differences. This especially helps to identify missing mappings.
Modeling Environment
Tools