-
-
- Tips and Tricks
Software Engineering for Smart Data Analytics & Smart Data Analytics for Software Engineering
After this lecture you will have created a DSL with xtext for an achievement domain. The information and working steps were taken from the official Xtext documentation.
If you have already installed Xtext, you can skip this step.
Open the Eclipse Marketplace (“Help→Eclipse Marketplace…”). In this dialog search for “xtext” in all markets and press the “Install” button on the right of the Xtext feature.
As an alternative, drag this icon onto your Eclipse menu bar:
As of December 2013 Xtext 2.3.1 is the current version of the marketplace, the latest version can be obtained via the xtext website.
Read and work through the 5 Minutes Tutorial of the Xtext website. The full documentation of Xtext (Version 2.3) can be found at http://www.eclipse.org/Xtext/documentation/2.3.0/Documentation.pdf.
In the end of the practical lecture, you will have created a DSL for defining achievements. In addition, Xtext provides you an editor with content assist, Java code generation and more.
The DSL that we want to create should look like the below code fragment. We want to define an achievement with a name, define how much experience is rewarded for gaining the achievement and a text that should describe the achievement as well as be able to show what needs to be done.
achievement Reader text "You read a document, congratulations!" experience 1 end
To learn more about achievements and what they are, take a look at this link: http://gamification.org/wiki/Game_Mechanics/Achievements
In order to get started we first need to create some Eclipse projects. Use the Eclipse wizard to do so: “File → New → Project… → Xtext → Xtext project”. Choose the following values:
Project name | edu.bonn.atsc.xtext |
---|---|
Language name | edu.bonn.atsc.xtext.Achievement |
DSL-File extension | game |
Click on Finish to create the projects. After you have successfully finished the wizard, you will find four new projects in your workspace.
edu.bonn.atsc.xtext | Contains the grammar definition and all runtime components (parser, lexer, linker, validation, etc.) |
edu.bonn.atsc.xtext.tests | Unit tests go here. |
edu.bonn.atsc.xtext.ui | The Eclipse editor and all the other workbench related functionality. |
edu.bonn.atsc.xtext.sdk | Defines a feature containing all three above projects. |
The wizard will automatically open the grammar file Achievement.xtext in the editor. As you can see that it already contains the simple Hello World grammar:
grammar edu.bonn.atsc.xtext.Achievement with org.eclipse.xtext.common.Terminals generate achievement "http://www.bonn.edu/atsc/xtext/Achievement" Model: greetings+=Greeting*; Greeting: 'Hello' name=ID '!';
Let's start to replace the Hello World grammar with our own, so we delete the two rules on the bottom. The first rule in a grammar is always used as the entry or start rule. As we want to define achievements, we define our model to contain an arbitrary number[*] of achievements which will be added [+=] to a feature called elements.
Model: elements+=Achievement*;
After defining this rule, we now need to define what the rule Achievement is. Taking a look at the above achievement snippet, we define it the following way:
Achievement: 'achievement' name=ID elements+=(BodyElement)+ 'end' ;
The Achievement rule starts with a keyword achievement, followed by an identifier which is parsed by a rule called ID. The rule ID is defined in the super grammar org.eclipse.xtext.common.Terminals and parses a single word, a.k.a identifier. You can navigate to the declaration by using F3 on the rule call. The value returned by the call to ID is assigned[=] to the feature name. Afterwards, we expect at least one[+] BodyElement, similar to the Model rule before. Body elements are either the definition of the text or experience. The rule ends with the keyword end.
The BodyElement rule delegates either to the Experience or Text rule
BodyElement: Experience | Text ;
Task: Define now both the Experience and Text rules. You can expect experience to be an integer, thus you can use INT and assign it to an identifier named experience. Similar for the text an identifier text is assigned to be a STRING.
Now that we have the grammar in place and defined we need to execute the code generator that will derive the various language components. To do so, locate the file GenerateAchievement.mwe2 file next to the grammar file in the package explorer view. From its context menu, choose
Run As → MWE2 Workflow.
This will trigger the Xtext language generator. It generates the parser and serializer and some additional infrastructure code. You will see its logging messages in the Console View.
We are now able to test the IDE integration. If you select Run → Run Configurations… from the Eclipse menu, you can choose Eclipse Application → Launch Runtime Eclipse. This preconfigured launch shortcut already has appropriate memory settings and parameters set. Now you can hit Run to start a new Eclipse.
This will start a new Eclipse workbench with your newly developed plug-ins installed. In the new workbench, create a new project of your choice, e.g. File → New → Project… → Java Project and therein a new file with the file extension you chose in the beginning (*.game). This will open the generated entity editor. Try it and discover the default functionality for code completion, syntax highlighting, syntactic validation, linking errors, the outline view, find references etc.
Before we just defined simple achievements, that have a text description and an experience reward. Sometimes you have an achievement that requires other achievements to be completed before. To be able to model this fact, we want to extend our existing solution. The following snippet illustrates our desired DSL.
achievement Reader experience 1 text "You read a document, congratulations!" end achievement Coder experience 101 text "101 coding skills proven." end achievement Tutorial text "Hurrah you have finished the Tutorial achievement by reading and coding!" experience 42 requires Reader Coder end
Task: Extend the existing grammar to be able to parse the requires field and accept references. To achieve this, add the Requires rule as a new alternative to the BodyElement. You can reference other elements with an ID by writing them in square brackets, e.g. test=[Achievement]. This would mean, that a reference to an achievement is required and is assigned to the identifier test. We expect one or more [+] references in the Requires rule.
After you have implemented your grammar, re-generate the language artifacts and run the generated code. Play around with your editor and see what works and what can still be improved.
The references to achievements that we have added before also work between different files. Try it and see how it looks and works with the help of the content assist. Now if you define two achievements with the same name and you try to reference it, it will give you an error that the name is ambiguous. We want to improve this by validating the uniqueness of the name. In Xtext >2.2 this can be easily done by uncommenting a single line in the GenerateAchievement.mwe2 file. Open this file and uncomment the composedCheck with the NamesAreUniqueValidator (see below). This checks automatically that all IDs should be unique.
// java-based API for validation fragment = validation.JavaValidatorFragment { // composedCheck = "org.eclipse.xtext.validation.ImportUriValidator" composedCheck = "org.eclipse.xtext.validation.NamesAreUniqueValidator" }
Still, there are other things that we need to validate! Try to locate the class AchievementValidator.xtend in the package edu.bonn.atsc.xtext.validation. It can be found in the language plug-in. We want to specify, that the text description should not be empty.
Any name for the method will do. The important thing is the @Check annotation that advises the framework to use the method as a validation rule. If the text is empty, a warning will be attached to the text description.
package edu.bonn.atsc.xtext.validation import org.eclipse.xtext.validation.Check import edu.bonn.atsc.xtext.achievement.Text import edu.bonn.atsc.xtext.achievement.AchievementPackage class AchievementValidator extends AbstractAchievementValidator { @Check def checkTextIsNotEmpty(Text text) { if(text.getText().trim().isEmpty()) warning("Text description should not be empty", AchievementPackage.Literals.TEXT__TEXT); } }
Another important issue that we still have, is that we can add an arbitrary number of text descriptions or experience definitions. In the end, we want to have exactly one of each. We can either change our grammar to have a fixed order and explicitly state the order. An alternative would be an unordered group. The elements of an unordered group can occur in any order but each element must appear once. Unordered groups are separated by &. In unordered groups you are not allowed to delegate to another rule, thus by using the unordered group solution we would need to inline our body elements. The third alternative is to define a manual validator, similar to the above one, that checks for an Achievement to contain exactly one instance of Text and Experience.
Task: Try to implement the third alternative by adding a new validation rule to the AchievementJavaValidator that checks for having exactly one instance of Text and Experience. Show the warning at the name of the achievement.
The Domain Model language should support the notion of Packages in order to avoid name clashes and to better fit with Java. A Package may contain Achievements and other packages. In order to allow for names in references, we will also add a way to declare imports. In the end we will have something like these two files:
package basicachievements achievement Reader experience 1 text "You read a document, congratulations!" end achievement Coder experience 101 text "101 coding skills proven." end achievement Tutorial text "Hurrah you have finished the Tutorial achievement by reading and coding!" experience 1 requires Reader Coder end end
import basicachievements.* achievement Bookworm experience 11 text "You are a real book-worm." requires Reader end
Since the Model does not only contain Achievements anymore, we need to add an additional delegation rule there to allow package declarations. There may also be achievements that we don't want to put into packages.
Model: elements+=AbstractElement*; AbstractElement: PackageDeclaration | Achievement | PackageImport ;
A PackageDeclaration in turn looks pretty much as expected. It contains a number of Imports and Achievements.
PackageDeclaration: 'package' name = QualifiedName (elements += AbstractElement)* 'end' ; QualifiedName: ID ('.' ID)* ;
The QualifiedName is a little special. It does not contain any assignments. Therefore, it serves as a data type rule, which returns a String. So the feature name of a Package is still of type String.
Imports can be defined in a very convenient way with Xtext. If you use the name importedNamespace in a parser rule, the framework will treat the value as an import. It even supports wildcard and handles them as expected:
PackageImport: 'import' importedNamespace = QualifiedNameWithWildcard ; QualifiedNameWithWildcard: QualifiedName '.*'? ;
Similar to the rule QualifiedName, QualifiedNameWithWildcard returns a plain string.
The last step is to allow fully qualified names in cross references, too. Otherwise one could not refer to an achievement without adding an import statement.
Task: Add an alternative to the cross reference in the Requires rule to not only allow Achievement references but accept also QualifiedName references.
As soon as you generate the Xtext artifacts for a grammar, a code generator stub will be put into the runtime project of your language. This allows you to automatically generate Java or any other language derived from your DSL elements. The topic of code generation will not be covered in this tutorial. For examples and information see http://www.eclipse.org/Xtext/7languagesDoc.html