In this tutorial we will basically implement the domain model language again, but this time, we will make use of the special JVM support shipped with Xtext 2.1. This kind of language really is a sweet spot for DSLs, so feel free to use this as a blueprint and add your project specific features later on.
The revised domain model language supports expressions and cross links to Java types. It is directly translated to Java source code. The syntax should look very familiar. Here is an example :
import java.util.List
package my.model {
entity Person {
name: String
firstName: String
friends: List<Person>
address : Address
op getFullName() : String {
return firstName + " " + name;
}
op getFriendsSortedByFullName() : List<Person> {
return friends.sortBy( f | f.fullName);
}
}
entity Address {
street: String
zip: String
city: String
}
}
As you can see, it supports all kinds of advanced features such as Java generics and full expressions even including closures. Don't panic you'll not have to implement these concepts on your own but will reuse a lot of helpful infrastructure to build the language.
We will now walk through the five! little steps needed to get this language fully working including its compiler.
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've successfully finished the wizard, you'll 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 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 '!';
Please replace that grammar definition with the one for our language:
grammar org.example.domainmodel.DomainModel with
org.eclipse.xtext.xbase.Xbase
generate domainmodel "http://www.example.org/domainmodel/Domainmodel"
DomainModel:
elements+=AbstractElement*;
AbstractElement:
PackageDeclaration | Entity | Import;
PackageDeclaration:
'package' name=QualifiedName '{'
elements+=AbstractElement*
'}';
Import:
'import' importedNamespace=QualifiedNameWithWildCard;
QualifiedNameWithWildCard :
QualifiedName ('.' '*')?;
Entity:
'entity' name=ValidID
('extends' superType=JvmTypeReference)? '{'
features+=Feature*
'}';
Feature:
Property | Operation;
Property:
name=ValidID ':' type=JvmTypeReference;
Operation:
'op' name=ValidID
'('(params+=FullJvmFormalParameter
(',' params+=FullJvmFormalParameter)*)?')'
':' type=JvmTypeReference
body=XBlockExpression;
Let's have a look at what the different grammar constructs mean:
grammar org.example.domainmodel.DomainModel with
org.eclipse.xtext.xbase.Xbase
DomainModel:
elements+=AbstractElement*;
AbstractElement:
PackageDeclaration | Entity | Import;
PackageDeclaration:
'package' name=QualifiedName '{'
elements+=AbstractElement*
'}';
Import:
'import' importedNamespace=QualifiedNameWithWildCard;
QualifiedNameWithWildCard :
QualifiedName ('.' '*')?;
Entity:
'entity' name=ValidID
('extends' superType=JvmTypeReference)? '{'
features+=Feature*
'}';
Feature:
Property | Operation;
Property:
name=ValidID ':' type=JvmTypeReference;
Operation:
'op' name=ValidID
'('(params+=FullJvmFormalParameter
(',' params+=FullJvmFormalParameter)*)?')'
':' type=JvmTypeReference
body=XBlockExpression;
{
return "Hello World" + "!"
}
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 right click in the grammar editor. From the opened context menu, choose
Run As -> Generate Xtext Artifacts.
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.
The syntax alone is not enough to make the language work. We need to map the domain specific concepts to some other language in order to tell Xtext how it is executed. Usually you define a code generator or an interpreter for that matter, but languages using Xbase can omit this step and make use of the IJvmModelInferrer (src).
The idea is that you translate your language concepts to any number of Java types (JvmDeclaredType (src) ). Such a type can be a Java class, Java interface, Java annotation type or a Java enum and may contain any valid members. In the end you as a language developer are responsible to create a correct model according to the Java language.
By mapping your language concepts to Java elements, you implicitly tell Xtext in what kind of scopes the various expressions live and what return types are expected from them. Xtext 2.1 also comes with a code generator which can translate that Java model into readable Java code, including the expressions.
If you have already triggered the 'Generate Xtext Artifacts' action, you should find a stub called org/example/domainmodel/jvmmodel/DomainModelJvmModelInferrer.xtend in the src folder. Please replace its contents with the following :
package org.example.domainmodel.jvmmodel
import com.google.inject.Inject
import org.eclipse.xtext.common.types.JvmDeclaredType
import org.eclipse.xtext.naming.IQualifiedNameProvider
import org.eclipse.xtext.util.IAcceptor
import org.eclipse.xtext.xbase.jvmmodel.AbstractModelInferrer
import org.eclipse.xtext.xbase.jvmmodel.JvmTypesBuilder
import org.example.domainmodel.domainmodel.Entity
import org.example.domainmodel.domainmodel.Operation
import org.example.domainmodel.domainmodel.Property
class DomainModelJvmModelInferrer extends AbstractModelInferrer {
/**
* a builder API to programmatically create Jvm elements
* in readable way.
*/
@Inject extension JvmTypesBuilder
@Inject extension IQualifiedNameProvider
def dispatch void infer(Entity element,
IAcceptor<JvmDeclaredType> acceptor,
boolean isPrelinkingPhase) {
acceptor.accept(element.toClass(element.fullyQualifiedName) [
documentation = element.documentation
for (feature : element.features) {
switch feature {
Property : {
members += feature.toField(feature.name, feature.type)
members += feature.toSetter(feature.name, feature.type)
members += feature.toGetter(feature.name, feature.type)
}
Operation : {
members += feature.toMethod(feature.name, feature.type) [
for (p : feature.params) {
parameters += p.toParameter(p.name, p.parameterType)
}
documentation = feature.documentation
body = feature.body
]
}
}
}
])
}
}
Let's go through the code to get an idea of what's going on. (Please also refer to the JavaDoc of the involved API, especially the JvmTypesBuilder (src).)
def dispatch void infer(Entity element,
IAcceptor<JvmDeclaredType> acceptor,
boolean isPrelinkingPhase) { // (1)
acceptor.accept(element.toClass(element.fullyQualifiedName) [
...
for (feature : element.features) {
switch feature { // (4)
Property : {
// ...
}
Operation : {
// ...
}
}
}
Property : {
members += feature.toField(feature.name, feature.type) // (5)
members += feature.toSetter(feature.name, feature.type)
members += feature.toGetter(feature.name, feature.type)
}
Operation : {
members += feature.toMethod(feature.name, feature.type) [
for (p : feature.params) {
parameters += p.toParameter(p.name, p.parameterType)
}
documentation = feature.documentation
body = feature.body
]
}
We are now able to test the IDE integration, by spawning a new Eclipse using our plug-ins. To do so just use the launch shortcut called "Launch Runtime Eclipse", clicking on the green play button in the tool bar.
In the new workbench, create a Java project (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 rich functionality it provides. You should also have a look at the preferences of your language to find out what can be individually configured to your users needs.
Have fun!