Skip to content

Creating a Textual Editor

Stephan Seifermann edited this page Sep 4, 2018 · 22 revisions

The goal of this page is to create a textual editor for a chosen diagram type that you can integrate in the Cooperate Modeling Environment. The required steps include

  1. Definition and implementation of a grammar and a meta model
  2. Implementation of automated issue resolutions
  3. Implementation of state calculators
  4. Implementation of outline
  5. Implementation of formatter
  6. Implementation of templates
  7. Implementation of comparator for CDO

Please note that automated issue resolutions and state calculators are interwined. You often cannot implement them sequentially but have to do tasks from one step while doing the other.

Foundations

  • Xtext
    • Textual editor framework
    • Important topics: Grammar, quickfixes, validation, outline
  • 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

Definition of a meta model

The meta model defines the structure of the information displayed in your textual syntax. When working with Xtext, the structure of your meta model and your grammar are tigthly coupled. Therefore, it might be necessary to adjust the meta model and grammar together if you want to change anything.

First of all, you have to create a new plugin project:

  • Create a plugin project that will include the meta model (it is best practice to name the project ...activity.metamodel if you create a meta model for an activity diagram)
    • Choose Eclilpse as the target platform in the wizard and do not change the default version
    • Create a new folder model
    • Add a dependency to de.cooperateproject.modeling.textual.common.metamodel

Afterwards, you can create the meta model in the model folder:

  • Set the name, ns prefix and ns uri of the root package
  • Load the textual commons resource
    • If the textual commons meta model is installed, use the registered packages and look for http://www.cooperateproject.de/modeling/textual/commons
    • If the textual commons meta model is located in your workspace, use the workspace browser and select the ecore model
  • For every non-artificial parser rule in your grammar, create a concrete EClass
  • For every feature in your grammar, create an according EStructuralFeature
  • Use the base classes of the textual commons meta model whenever possible
    • Every concrete EClass that has a counterpart in the UML meta model has to (transitively) extend the UMLReferencingElement EClass
  • Add interfaces, abstract classes, and relations to improve the structure of your meta model (e.g. to introduce reasonable inheritance structures)

Next, you have to create the generator model with the same name in the model folder:

  • Create a new EMF generator model from the wizard
    • Give it the file name as the ecore model
    • Choose Ecore model (CDO Native)
    • Select your newly created ecore model
    • Select the root package of your ecore model as root package
    • Select all other referenced models in the bottom area of the dialog page
  • Open the generator model
    • Select the root element
      • change the Feature Delegation in the group Model to Dynamic
      • change the Model Directory in the group Model from .../src to .../src-gen
    • Select the first entry under the root element
      • set the Base Package in the group All to the name of your plugin project
  • Open the generator model in a text editor
    • For all entries in the usedGenPackages attribute, replace ../../ with platform:/plugin/

Finally, you should generate the code for model, edit, and editor plugins with the generator model. You have to regenerate the code everytime you change the model.

Definition of a grammar

Before designing your grammar, please have a look at our general syntax guidelines and existing syntaxes. Additionally, you have to choose relevant elements for your diagram and omit elements that are not essential or that are rarely used.

First, you should create a new Xtext project.

  • Select Xtext Project From Existing Ecore Models in the new wizard
  • Select the generator model you created in the step before
  • Choose your entry rule (usually you want to choose the XYDiagram element)
  • Use the project name that you used for the meta model plugin but omit the .metamodel
  • Use a short file extension (usually 3 digits) such as abbreviations suggested in the UML 2.5 standard Annex A
  • Use the same name like the project name but append the diagram abbreviation
  • Keep all settings regarding plugin generation as it is
  • After pressing finish, 5 projects should have been created

In the next step, you can fix the errors in the generated grammar file:

  • Delete all existing rules and start over
  • Replace all import statements that are marked with the platform resource URI
    • old: import "http://www.cooperateproject.de/modeling/textual/cls/Cls"
    • new: import "platform:/resource/de.cooperateproject.modeling.textual.cls.metamodel/model/cls.ecore"

In order to generate code from your grammar file, you have to use the MWE2 workflow. Adjust the workflow in order to generate the code required for integration in the Cooperate Modeling Environment. You can use the template given below. Do not forget to replace every placeholder in angle brackets.

module <module qualifier, just keep it>

import org.eclipse.xtext.xtext.generator.*
import org.eclipse.xtext.xtext.generator.model.project.*
import de.cooperateproject.modeling.textual.xtext.generator.*

var rootPath = ".."
var basename = "<the base package you chose in the generator model when defining the meta model>"

Workflow {

    bean = org.eclipse.emf.mwe.utils.StandaloneSetup {
        platformUri = "${rootPath}"
        scanClassPath = true

        uriMap = {
            from = "platform:/plugin/org.eclipse.emf.codegen.ecore/model/GenModel.genmodel"
            to = "platform:/resource/org.eclipse.emf.codegen.ecore/model/GenModel.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.emf.ecore/model/Ecore.genmodel"
            to = "platform:/resource/org.eclipse.emf.ecore/model/Ecore.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.codegen.ecore/model/GenModel.genmodel"
            to = "platform:/resource/org.eclipse.uml2.codegen.ecore/model/GenModel.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.uml/model/UML.genmodel"
            to = "platform:/resource/org.eclipse.uml2.uml/model/UML.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.emf.codegen.ecore/model/GenModel.ecore"
            to = "platform:/resource/org.eclipse.emf.codegen.ecore/model/GenModel.ecore"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.emf.ecore/model/Ecore.ecore"
            to = "platform:/resource/org.eclipse.emf.ecore/model/Ecore.ecore"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.codegen.ecore/model/GenModel.ecore"
            to = "platform:/resource/org.eclipse.uml2.codegen.ecore/model/GenModel.ecore"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.uml/model/UML.ecore"
            to = "platform:/resource/org.eclipse.uml2.uml/model/UML.ecore"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.types/model/Types.genmodel"
            to = "platform:/resource/org.eclipse.uml2.types/model/Types.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/org.eclipse.uml2.types/model/Types.ecore"
            to = "platform:/resource/org.eclipse.uml2.types/model/Types.ecore"
        }
        uriMap = {
            from = "platform:/plugin/de.cooperateproject.modeling.textual.common.metamodel/model/textualCommons.genmodel"
            to = "platform:/resource/de.cooperateproject.modeling.textual.common.metamodel/model/textualCommons.genmodel"
        }
        uriMap = {
            from = "platform:/plugin/de.cooperateproject.modeling.textual.common.metamodel/model/textualCommons.ecore"
            to = "platform:/resource/de.cooperateproject.modeling.textual.common.metamodel/model/textualCommons.ecore"
        }

        registerGeneratedEPackage = "org.eclipse.uml2.codegen.ecore.genmodel.GenModelPackage"
        registerGeneratedEPackage = "org.eclipse.emf.codegen.ecore.genmodel.GenModelPackage"
    }
    
    component = CooperateXtextGenerator {       
        configuration = {
            project = StandardProjectConfig {
                baseName = "${basename}"
                rootPath = rootPath
                runtimeTest = {
                    enabled = true
                    root = "../../tests/${basename}.tests"
                }
                eclipsePlugin = {
                    enabled = true
                }
                eclipsePluginTest = {
                    enabled = true
                    root = "../../tests/${basename}.ui.tests"
                }
                createEclipseMetaData = true
            }
            code = {
                encoding = "UTF-8"
                fileHeader = "/*\n * generated by Xtext \${version}\n */"
            }
            
        }
        
        language = StandardLanguage {
            name = "${basename}.<file extension starting with an upper case character, keep it>"
            fileExtensions = "<file extension, keep it>"
            
            referencedResource = "platform:/resource/${basename}.metamodel/model/<name of your genmodel file>.genmodel"
            referencedResource = "platform:/resource/${basename}.metamodel/model/<name of your meta model file>.ecore"
            referencedResource = "platform:/plugin/org.eclipse.uml2.uml/model/UML.ecore"
            referencedResource = "platform:/plugin/org.eclipse.uml2.uml/model/UML.genmodel"
            
            renameRefactoring = fragments.CooperateRenameGeneratorFragment2 auto-inject {}
            
            fragment = ecore2xtext.Ecore2XtextValueConverterServiceFragment2 auto-inject {}

            fragment = org.eclipse.xtext.generator.adapter.FragmentAdapter {
                fragment = org.eclipse.xtext.generator.formatting2.Formatter2Fragment {}
            }
            
            fragment = scoping.ImportNamespacesScopingFragment2 auto-inject {}
            fragment = exporting.QualifiedNamesFragment2 auto-inject {}

            quickFixProvider = fragments.CooperateQuickfixProviderFragment2 auto-inject {}

            generator = {
                generateStub = false
            }

            serializer = {
                generateStub = false
            }
            
            validator = fragments.CooperateValidatorFragment2 auto-inject {
                generateXtendStub = false
            }
                
            fragment = junit.Junit4Fragment2 {
                generateStub = false
                generateXtendStub = false
            }
                                
            fragment = fragments.CooperateCDOXtextFragment2 auto-inject {}
        }
    }
}

After editing the MWE2 workflow, you can execute it. This should lead to 4 additional projects. The workflow tries to adjust several files including the plugin.xml file every time you run the workflow. If the workflow is not able to merge the changes, there will be a plugin.xml_gen file. If this file exists, you have to merge the changes manually. Otherwise, the changes will not be applied.

After generating the plugins required for the textual editor, you have to adjust some parts of the generated code:

  • <basename>.ui plugin - plugin.xml
    • Add the matching strategy de.cooperateproject.modeling.textual.xtext.runtime.ui.editor.CooperateEditorMatchingStrategy to the editor extension in the extension point org.eclipse.ui.editors
    • Add the category de.cooperateproject.ui.preferences.cooperate to the first page of the page extension in the extension point org.eclipse.ui.preferencePages
    • Add the term Diagram to the name of the first page of the page extension in the extension point org.eclipse.ui.preferencePages

Afer these changes, you can complete your grammar (and meta model). You can always test the editor after executing the MWE2 workflow, starting a new Eclipse instance, and creating an empty file ending with the file extension you specified.

Implementation of automated issue resolutions

Automated issue resolutions combine the Xtext capabilities of validating a model and applying quick fixes on the model. Validation is shown to the user by underlining erroneous parts of the text with colors indicating the severity of the issue. Quick fixes are offered from the ruler bar or a context menu and allow applying fixes automatically. Automated issue resolutions link the error detection and error correction. This allows a fully automated issue fixing.

The major use case of automated issue resolutions is the creation and modification of elements in the UML model shared between graphical and textual diagram. For instance, after creating a new class in the textual UML diagram, the class has to be created in the UML model as well. The automated issue resolution detects that the reference to the UML model is not given yet and marks this as an error. At the same time, it knows how to solve it automatically and offers a fix to apply the solution. The error will be shown as information only because it can be fixed automatically. All these fixes are applied automatically when saving the diagram.

The most common automated issue resolutions that you will have to implement are

  • Creation of UML elements
  • Setting of UML element features to the values provided in the textual editor

You should avoid automatically setting textual features to the value provided by the UML model because this would discard user changes and violate our conventions regarding automated fixes.

The realization of an automated issue resolution requires the following steps:

  1. Implementation of a factory
  2. Implementation of a resolution
  3. Registration of the factory

Implementation of a factory

The factory class should extend the AutomatedIssueResolutionFactoryBase in the issues package of your Xtext project. The purpose of the factory is to provide the detection mechanism (hasIssueInternal), the error information (getIssueDescriptionInternal, getIssueFeatureInternal), the resolution name (getResolutionNameInternal), the actual resolution (createInternal), and means for detecting if the resolution is applicable (isResolvable).

Thereby, an issue can be detected but the resolution can be postponed. This is necessary if the resolution depends on other resolutions applied before. For instance, if a class shall be created in the UML model, the containing package has to be created before. Please note that the constructor of the factory must not require any parameter. The issue code required by the constructor of the super class has to be unique regarding all automated issue resolutions. The required IResolvableChecker usually just redirects the call to the isResolvable method.

Please note that it is crucial to check every requirement of the resolution in the isResolvable method. The semantics of the methods are: If the issue is resolvable, the resolution is guaranteed to be successfull. If the resolution fails despite of beein considered resolvable, the whole automated issue resolution processing is likely to fail during the save operation of the editor.

In order to implement the UML model element creation, it is a commonly used pattern to use a single issue resolution factory and a separate IResolvableChecker. This is beneficial because all methods can be implemented in the same way. For an example on how to implement this factory using dynamic description generation, please refer to the example below for use case diagrams.

class UsecaseUMLReferencingElementMissingElementFactory extends UsecaseAutomatedIssueResolutionFactoryBase<UMLReferencingElement<Element>> {
	
	private static val ISSUE_CODE = "referencedUMLElementMissing"
	
	new() {
		super(ISSUE_CODE, new UsecaseUMLReferencingElementMissingElementChecker(), UMLReferencingElement as Class<?> as Class<UMLReferencingElement<Element>>)
	}
	
	override protected hasIssueInternal(UMLReferencingElement<Element> element) {
		return element.referencedElement === null;
	}
	
	override protected createInternal(UMLReferencingElement<Element> element) {
		new UsecaseUMLReferencingElementMissingElementResolution(element, resolvableChecker)
	}
	
	override protected getResolutionNameInternal(UMLReferencingElement<Element> element) {
		"Create UML element";
	}
	
	override protected getIssueDescriptionInternal(UMLReferencingElement<Element> element) {
		"The UML element does not exist yet.";
	}
	
	override getIssueFeatureInternal(UMLReferencingElement<Element> eObject) {
		new IssueLocator(eObject.relevantFeature, eObject)
	}
	
	protected def dispatch relevantFeature(NamedElement element) {
		TextualCommonsPackage.Literals.NAMED_ELEMENT__NAME
	}
	
	protected def dispatch relevantFeature(de.cooperateproject.modeling.textual.common.metamodel.textualCommons.Element element) {
		TextualCommonsPackage.Literals.UML_REFERENCING_ELEMENT__REFERENCED_ELEMENT
	}
	
}

The resolvable checker should extend the DependingElementMissingElementResolvableCheckerBase base class. Using this class is beneficial because it can handle dependencies between elements. For instance, if you want to create a class, you can define a dependency to the parent element. Using this mechanism, the error in the textual editor will be displayed as information instead of an error. The resolution can, however, only be applied if the dependency has a referenced alement and the localResolutionPossible method returns true. This is shown in the example below:

class ClsUMLReferencingElementMissingElementResolvableChecker extends DependingElementMissingElementResolvableCheckerBase {
   
	protected def dispatch localResolutionPossible(Classifier<?> element) {
		true
	}
	
	protected def dispatch getDependencies(Classifier<?> object) {
		#[object.owningPackage]
	}

}

Implementation of the resolution

The issue resolution usually extends the AutomatedIssueResolutionBase base class. You just have to forward the parameters given in the constructor to the super class constructor and implement the resolve method. It is important to note that as soon as the resolve method is called, the issue has to be fixed. You must not implement any conditionals and further checks that prohibit the issue resolution from beeing applied. These checks have to be placed in the isResolvable method of the factory, which is checked before executing the issue resolution.

For the creation of UML elements, the commonly applied pattern is using a single resolution class with dispatch methods for every case. After creating the element in the UML model, you have to set the reference in the textual model accordingly. An example is shown below:

class ClsUMLReferencingElementMissingElementResolution extends AutomatedIssueResolutionBase<UMLReferencingElement<Element>> {

	new(UMLReferencingElement<Element> element, IResolvableChecker<UMLReferencingElement<Element>> resolvableChecker) {
		super(element, resolvableChecker)
	}

	override resolve() {
		getProblematicElement.fixMissingUMLElement
	}

	private def dispatch fixMissingUMLElement(Package element) {
		val umlParent = element.owningPackage.referencedElement
		val umlPackage = umlParent.createPackagedElement(element.name,
			UMLPackage.Literals.PACKAGE) as org.eclipse.uml2.uml.Package
		element.referencedElement = umlPackage
	}

	private def dispatch fixMissingUMLElement(Class element) {
		val result = fixMissingUMLElement(element, UMLPackage.Literals.CLASS)
		if (result instanceof org.eclipse.uml2.uml.Class) {
			result.isAbstract = element.abstract
		}
	}

	private def fixMissingUMLElement(Classifier element, EClass umlType) {
		val umlParent = element.owningPackage.referencedElement
		val umlClassifier = umlParent.createPackagedElement(element.name, umlType) as org.eclipse.uml2.uml.Classifier
		if (element.isSetVisibility) {
		  umlClassifier.visibility = element.visibility  
		}
		if (element.alias !== null) {
			umlClassifier.createNameExpression(element.alias, null)
		}
		element.referencedElement = umlClassifier
		return umlClassifier
	}
}

Registration of the factory

In order to make the automated issue resolution available, you have to register the factory class via the Eclipse extension point mechanism. First, you have to open the extension point configuration via the manifest editor or the editor of the plugin.xml file. Afterwards, you just have to create a new extension for the extension point de.cooperateproject.modeling.textual.xtext.runtime.automatedissueresolutionfactories and set the class to the created factory class.

Implementation of State Calculators

In order to reduce the duplicated information between the textual and the graphical diagram, we aim for storing a piece of information only once. We can achieve this by leveraging the information stored in a central UML model that is shared between both diagram types.

The editor detects the UML model to be used via the file name of the textual diagram. In order to work properly, the default detection mechanism (see class ConventionalUMLUriFinder) requires the name of the textual diagram to match model - textual.ext, where model is the file name of the UML model without extension, textual is an arbitrary string (not containing - characters), and ext is the extension you configured above. A valid combination in case of the class diagram looks like this:

  • someUMLModel.uml
  • someUMLModel - someTextualDiagramName.cls

You can create the UML model by using the Papyrus diagram editor or manually. If you create the model manually, insert an element of type Model with the name RootElement as the root of the whole model. All other elements have to be nested below this root element.

A state calculator handles one concrete element type. If an element has more than one type (because of inheritance in the ecore model), the state calculators for every supertype of this element will be executed as well in arbitrary order. The order can, however, be influenced by the individual processors. They can define types that have to be processed before they can be executed. In a similar fashion, they can define types that shall not be executed anymore because they provide the same/modified functionality.

We will illustrate this with the following artificial example. Let's assume, there is a class Class that inherits from Classifier, which inherits from UMLReferencingElement. Additionally, Class inherits from VisibilityHavingElement. The state calculators of these classes define the following relationships:

  • Class requires UMLReferencingElement to be processed earlier
  • VisibilityHavingElement requires Classifier to be processed earlier
  • Class replaces Classifier

Inheritance hierarchy of model classes including dependencies and replacements

The resulting order of processor execution will be

  1. UMLReferencingElement
  2. Class
  3. VisibilityHavingElement Classifier will not be executed anymore because it has been replaced by Class. The dependency from VisibilityHavingElement to Classifier has been redirected to Class.

Resulting state calculator hierarchy

The order of element processing can be adjusted as well. For instance, you can define that all classifiers are processed before the relations between these classifiers. This is useful if there are dependencies between elements. You can use the generated DerivedStateElementComparator in the derivedstate package of your Xtext project to achieve this. You simply have to define the given compare method. The elements will be processed in the order define by the comparator.

In order to reuse the information of the UML model, we have to perform three steps:

  1. Mark the feature containing the information transient in the meta model
  2. Provide state calculators that allow deriving the value of the feature from the UML model
  3. Tell the textual editor that the feature shall still be shown in the text

In general, you should be able to derive all features from the UML model except the containment references. Have a look at the state calculators in the de.cooperateproject.modeling.textual.common.derivedstate package because they already handle many common tasks such as names.

Meta Model Modification

The meta model modification is straight forward.

  • Open the meta model using the ecore editor
  • Find the attribute you want to be derived from the UML model
  • In the properties view, set the value of Transient to true
  • In the properties view, set the value of Unsettable to true
  • Regenerate the model code after your change

Creating State Calculators

All calculators have to implement the IAtomicDerivedStateProcessor interface. This interface has four methods, which perform the actual state calculation, return the processable type, define the requirements and replacements of the state calculator. It is beneficial to use the base class AtomicDerivedStateProcessorBase because it handles type-related issues automatically.

The sceleton of a state calculator looks like the following example:

@Applicability(applicabilities = DerivedStateProcessorApplicability.CALCULATION)
public class ImplementationCalculator extends AtomicDerivedStateProcessorBase<Implementation> {

    public ImplementationCalculator() {
        super(Implementation.class);
    }

    @Override
    protected void applyTyped(Implementation object) {
        ...
    }

    @Override
    public Collection<java.lang.Class<? extends EObject>> getReplacements() {
        return Arrays.asList(UMLReferencingElement.class);
    }

}

You have to set the type parameter of the base class AtomicDerivedStateProcessorBase to the concrete type you want to process. You have to pass the class of this type to the super constructor to allow automated type checking. The constructor of your state calculator must not contain any parameters. Afterwards, you implement the method applyTyped. You can specify replacements (as shown in the case above) and requirements by overriding the base class methods. You always pass the class object of the required/replaced type instead of the class of the processor. To specify the type of calculator, you have to place the annotation @Applicability before the class declaration. We will learn about the different types later.

Please note that you can use the Guice injection mechanism of Xtext in the calculators. You can use attribute or method injection but no constructor injection. This can be useful to gain access to the global scope or name calculators.

After implementing the calculator, you have to register it via the Eclipse extension point mechanism. Simply add a new extension to the extension point de.cooperateproject.modeling.textual.xtext.runtime.derivedstate.processor via the Manifest or Plugin editor of the containing plugin. Select the newly created calculator as the class of the extension.

In the following sections, you will learn about the concrete state calculator types.

State Initializers

State Initializers are called after opening the diagram. They initialize the values of the features based on the values of the UML model. You annotate them with the INITIALIZATION applicability. You can assume that the reference to the UML model element is set in this state because the diagram has been restored from the persisted version that requires these references to be set.

Please note that you must not initialize the features if it already contains a value. Therefore, you have to ensure that the value of the feature is not set by using eIsSet() or equivalent checks.

State Removers

State Removers are called before persisting and merging the diagram. They remove values of features that can be recalculated based on the UML model. You annotate them with the CLEANING applicability.

Please not that you must not remove the value of a feature if it does not match the value in the UML model. You have to ensure this by comparing the values via equals operations. If the values match, you have to remove the value by calling eUnset.

To handle removals of elements correctly in the textual editor, you have to handle null values in a special way. If the value of a feature is null and the method undergoesAutomatedIssueResolution() of the state calculator base class returns true, you have to set the feature again to null. This removes the unset flag from the feature and expresses that the value should be considered removed.

An example of the application of these rules is shown below for properties of a class diagram.

if (feature.flatMap(f -> referencedElement.map(e -> e.eGet(f, true)))
		.map(umlValue -> umlValue == object.getType()).orElse(false)) {
	object.unsetType();
} else if (object.getType() == null && undergoesAutomatedIssueResolution(object)) {
	object.setType(null);
}

State Calculators

State Calculators are called after every edit operation in the editor. They recalculate values of the features and adjust them if required by the changes.

In almost all cases, you want to specify a (transitive) dependency to UMLReferencingElement for state calculators because it calculates the reference to the corresponding UML element. Without this reference, you cannot reuse the information from the UML model element. You, however, have to replace UMLReferencingElement if your element cannot be uniquely identified by name. For instance, generalizations between classes usually do not have a name. Therefore, you have to implement your own reference calculation logic in that case.

The example below illustrates how the referenced element can be calculated without the UMLReferencingElement calculator, which is replaced by the Generalization calculator shown below.

@Override
protected void applyTyped(Generalization object) {
	if (object.getLeft() != null && object.getLeft().getReferencedElement() != null && object.getRight() != null
			&& object.getRight().getReferencedElement() != null) {
		org.eclipse.uml2.uml.Generalization umlGeneralization = object.getLeft().getReferencedElement()
				.getGeneralization(object.getRight().getReferencedElement());
		object.setReferencedElement(umlGeneralization);
		return;
	}
}

Transient Value Service

By default, Xtext does not consider transient values when resolving feature values. This leads to errors after our meta model changes have been applied. To prevent this, we have to tell Xtext that it shall still consider our transient values.

There is a generated class that implements the ITransientStatusProvider interface in the services package of you Xtext project. You have to add the feature to the list NON_TRANSIENT_FEATURES. Use the package class of your meta model to find the matching EStructuralFeature. For instance, to refer to the abstract attribute of a class, you can use ClsPackage.Literals.CLASS__ABSTRACT.

Scoping

Xtext links text to model elements via names in so-called scopes. Scopes contain a set of named elements. Name providers generate names from elements. If you use the workflow template described above, the CommonQualifiedNameProvider will be used as default name provider. It derives the name by concatenating the names of all parents with the element's name.

The default implementation of UMLReferencingElementCalculator derives the reference to the UML element by comparing the name of the element in the textual model with the name of the UML element. This works as long as elements have mandatory names and the parent hierarchy between UML and the textual model is equal. Otherwise, the names will not match and the reference will be set to null. You can solve this by either implementing your own calculator and overriding the UMLReferencingElementCalculator or by implementing a custom version of the name provider.

The scope providers allow to limit or widen the available elements at a certain location. Scopes have a pretty obvious effect on the code completion. All elements shown there are part of the scope for the potential element. If you do not want to display certain elements, you can just filter these elements in the scope. Scopes are calculated for a given context and a given reference. To adjust a scope, you have to override the getScope method in the scope provider located in the same project as the workflow definition. It is beneficial to implement dispatch methods for different contextes.

Implementation of Outline

The stub implementation of the outline has already been generated and is located in the ui plugin. It is beneficial to change the supertype of this call to CooperateOutlineTreeProvider to benefit from default and helper methods. There are two types of methods. Both of these methods are dispatch methods.

createChildren(parentNode, element) defines how children of element are generated. The body of the method has to contain calls to the createEObjectNode method for the children to be created. If the element is commentable, there has to be a call _createChildren(parentNode, element as Commentable<?>).

createNode(parentNode, element) defines how a node shall be created. This is often just a delegation to the createEObjectNode method surrounded by conditions.

It is possible to create artificial nodes, which represent elements of a specific feature for instance. Usually, we create them via a call to the createFeatureNode method. To add the number of elements to a label, you can use the getStyledString method and use the output as a label.

The labels and images of the elements shown in the outline, are given in the label provider. A stub of the label provider has already been generated in the ui plugin. You should change the super class of the label provider to CooperateOutlineLabelProvider in order to benefit from helper methods and default implementations.

For every element, you have to define a text and image method that takes the correctly typed element. The base class performs a polymorphic dispatch. The implementation that matches the given element most closely is chosen during runtime. For generating images, we ask you to have a look at UMLImage for an example.

Implementation of Formatter

The formatter enforces a common style in the textual representation. When loading a textual diagram from the CDO repository, the formatter completely defines the style of the text. Therefore, it is mandatory to implement the formatter. A stub has already been created in the project containing the workflow.

For every element that shall be formatted, you have to add a dispatch method format(T element, extension IFormattableDocument document). Inside the method, you have to call format on all direct child elements that shall be formatted. For a description of the API, please consult the Xtext documentation.

In order to avoid formatting region conflicts, we suggest to always either prepend or append spaces and newlines.

Implementation of Templates

It is possible to define custom templates in a templates.xml file, placed at the .ui project of the diagram type. Besides a name, an id and a short description, the context of the template and the pattern itself have to be provided. Autoinsert and enabled should be set true, deleted false. Patterns are defined as follows:

  • all elements of the pattern that should appear as plain text just like in the pattern, e.g. keywords: can be just typed.
  • elements which specifiy e.g. names: ${name}
    • "name" will appear as a suggestion, but the user will be given the opportunity to change it.
  • selections between elements of a specific type: $(variableName:CrossReference(ElementInMetamodel)}
    • the variableName can be used to use the same chosen value at all occurences of variables with the same name
    • the ElementInMetamodel specifies the element that should be placed here and so the type of elements the user can choose between.
    • Example:
      - port ${portName} realizes ${type:CrossReference(Port.realizedClassifier)}
      
      inserts a new, private port. "- port" and "realizes" will just appear as given in the template. "portName" allows the user to either accept this name or change it without giving suggestions. For the type, there will appear all elements that are suitable for the realizedClassifier property of Port in the metamodel.
  • Complete Example:
<template autoinsert="true" context="de.cooperateproject.modeling.textual.component.Component.Port" deleted="false" 
description="inserts a new port" enabled="true" id="cmpport" name="Insert port">
- port ${portName} realizes ${type:CrossReference(Port.realizedClassifier)}</template>

Implementation of Comparator for CDO

Matching of model elements is important to realize incremental model updates after editing models in the textual editor. The matcher has to provide rules for matching elements originating from different models.

The code generator already created an initial version of this matcher in the UI project of your textual notation. It is located in the cdoxtext package and called MatchEngineFactory.xtend. You have to replace the getMatchEngine method with the following snippet:

override getMatchEngine() {
	val idComputation = new Function<EObject, String>() {
		override apply(EObject input) {
			if (input === null) {
				return null;
			}
			val cdoObject = CDOUtil.getCDOObject(input)
			cdoObject.cdoID.toString
		}
	}
	
	val idFunction = new Function<EObject, String>() {
		override apply(EObject input) {				
			switch input {
				UMLReferencingElement<?>: input.class.simpleName + "_UMLReferencingElement" + idComputation.apply(input.referencedElement)
				StringExpression: "StringExp" + input.name
				default: null
			}
		}
	}
	
	val proxMatcher = new ProximityEObjectMatcher(dfProvider.get)
	val matcher = new IdentifierEObjectMatcher(proxMatcher, idFunction)
	return new DefaultMatchEngine(matcher, new DefaultComparisonFactory(new DefaultEqualityHelperFactory()))
}

The snipped calculates an ID for every model element. The matcher uses the IDs of the elements to match elements. For most elements, the snippet is sufficient to calculate an ID. It is, however, not sufficient if the model element does not inherit from UMLReferencingElement or refers to the same UMLReferencingElement as another element. For both cases, you have to add a case in the switch that defines an alternative ID computation.

The excerpt below describes the ID computation for a cardinality element. Please note that the resulting ID has to be unique for the model. For the class diagram, this holds true if we combine the name of the cardinality class (Cardinality), the containing feature name (upper / lower) and the ID of the referenced UML element.

Cardinality: Cardinality.simpleName + input.eContainingFeature.name + idComputation.apply(input.referencedElement)
Clone this wiki locally