In this tutorial we will implement a small domain-specific language to model entities and properties similar to what you may know from Rails, Grails or Spring Roo. The syntax is very suggestive :
datatype String
entity Blog {
title: String
many posts: Post
}
entity HasAuthor {
author: String
}
entity Post extends HasAuthor {
title: String
content: String
many comments: Comment
}
entity Comment extends HasAuthor {
content: String
}
After you have installed Xtext on your machine, start Eclipse and set up a fresh workspace.
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 a meaningful project name, language name and file extension, e.g.
Main project name: | org.example.domainmodel |
Language name: | org.example.domainmodel.Domainmodel |
DSL-File extension: | dmodel |
Click on Finish to create the projects.
After you have successfully finished the wizard, you will find three new projects in your workspace.
org.example.domainmodel | Contains the grammar definition and all runtime components (parser, lexer, linker, validation, etc.) |
org.example.domainmodel.tests | Unit tests go here. |
org.example.domainmodel.ui | The Eclipse editor and all the other workbench related functionality. |
The wizard will automatically open the grammar file Domainmodel.xtext in the editor. As you can see that it already contains a simple Hello World grammar:
grammar org.example.domainmodel.Domainmodel with
org.eclipse.xtext.common.Terminals
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
Model:
greetings+=Greeting*;
Greeting:
'Hello' name=ID '!';
Let's now just replace that grammar definition with the one for our domain model language:
grammar org.example.domainmodel.Domainmodel with
org.eclipse.xtext.common.Terminals
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
Domainmodel :
(elements += Type)*
;
Type:
DataType | Entity
;
DataType:
'datatype' name = ID
;
Entity:
'entity' name = ID ('extends' superType = [Entity])? '{'
(features += Feature)*
'}'
;
Feature:
(many ?= 'many')? name = ID ':' type = [Type]
;
Let's have a more detailed look at what the different grammar rules mean:
Domainmodel :
(elements += Type)*
;
Type:
DataType | Entity
;
DataType:
'datatype' name = ID
;
Entity :
'entity' name = ID ('extends' superType = [Entity])? '{'
(features += Feature)*
'}'
;
Feature:
(many ?= 'many')? name = ID ':' type = [Type]
;
This domain model grammar already uses the most important concepts of Xtext's grammar language. you have learned that keywords are written as string literals and a simple assignment uses a plain equal sign (=) where the multi value assignment used a plus-equals (+=). We have also seen the boolean assignment operator (?=). Furthermore we saw how a cross reference can be declared and learned about different cardinalities (? = optional, * = any number, + = at least once). Please consult the Grammar Language Reference for more details. Let's now have a look what you can do with such a language description.
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 GenerateDomainmodel.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 spawn 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 (*.dmodel). 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.
After you have created the your first DSL and had a look at the editor, the language should be refined and incrementally enhanced. The Domain Model language should support the notion of Packages in order to avoid name clashes and to better fit with the target environment (Java). A Package may contain Types and other packages. In order to allow fort names in references, we will also add a way to declare imports.
In the end we want to be able to split the previously used model into to distinct files :
// datatypes.dmodel
datatype String
// commons.dmodel
package my.company.common {
entity HasAuthor {
author: String
}
}
// blogs.dmodel
package my.company.blog {
import my.company.common.*
entity Blog {
title: String
many posts: Post
}
entity Post extends my.company.common.HasAuthor {
title: String
content: String
many comments: Comment
}
entity Comment extends HasAuthor {
content: String
}
}
Let's start enhancing the grammar.
Domainmodel:
(elements += AbstractElement)*
;
AbstractElement:
PackageDeclaration | Type
;
PackageDeclaration:
'package' name = QualifiedName '{'
(elements += AbstractElement)*
'}'
;
AbstractElement:
PackageDeclaration | Type | Import
;
QualifiedName:
ID ('.' ID)*
;
Import:
'import' importedNamespace = QualifiedNameWithWildcard
;
QualifiedNameWithWildcard:
QualifiedName '.*'?
;
Entity:
'entity' name = ID
('extends' superType = [Entity | QualifiedName])?
'{'
(features += Feature)*
'}'
;
Feature:
(many ?= 'many')? name = ID ':' type = [Type | QualifiedName]
;
That's all for the grammar. It should now read as
grammar org.example.domainmodel.Domainmodel with
org.eclipse.xtext.common.Terminals
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
Domainmodel:
(elements += AbstractElement)*
;
PackageDeclaration:
'package' name = QualifiedName '{'
(elements += AbstractElement)*
'}'
;
AbstractElement:
PackageDeclaration | Type | Import
;
QualifiedName:
ID ('.' ID)*
;
Import:
'import' importedNamespace = QualifiedNameWithWildcard
;
QualifiedNameWithWildcard:
QualifiedName '.*'?
;
Type:
DataType | Entity
;
DataType:
'datatype' name=ID
;
Entity:
'entity' name = ID
('extends' superType = [Entity | QualifiedName])?
'{'
(features += Feature)*
'}'
;
Feature:
(many ?= 'many')? name = ID ':' type = [Type | QualifiedName]
;
You should regenerate the language infrastructure as described in the previous section, and give the editor another try. You can even split up your model into smaller parts and have cross-references across file boundaries.