After you've developed you first own DSL, the question arises, how the behavior and the semantics of the language can be customized. Therefore a couple of mini-tutorials are available, that illustrate common use cases when crafting an own DSL.
These lessons are independent from each other. Each of them will be based on the language that was build in the first domain model tutorial.
That is, the syntax and the grammar for the language look like this:
package java.lang {
datatype String
}
package my.company.blog {
import java.lang.*
import my.company.common.*
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
}
}
grammar org.eclipse.xtext.example.Domainmodel with
org.eclipse.xtext.common.Terminals
generate domainmodel "http://www.eclipse.org/xtext/example/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]
;
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. Let's dive into Xtend and see how you can integrate your own code generator with Eclipse.
In this lesson you'll generate Java Beans for entities that are defined in the domain model DSL. For each Entity, a Java class is generated and each Feature will lead to a private field in that class and public getters and setters. For the sake of simplicity, we'll use fully qualified names for all over the generated code.
package my.company.blog;
public class HasAuthor {
private java.lang.String author;
public java.lang.String getAuthor() {
return author;
}
public void setAuthor(java.lang.String author) {
this.author = author;
}
}
First of all, locate the file DomainmodelGenerator.xtend in the package org.eclipse.xtext.example.generator. This Xtend class is used to generate code for your models in the standalone scenario and in the interactive Eclipse environment.
package org.eclipse.xtext.example.generator
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
class DomainmodelGenerator implements IGenerator {
override void doGenerate(Resource resource, IFileSystemAccess fsa) {
}
}
Let's make the implementation more meaningful and start the implementation. The strategy is, to find all entities with a resource and trigger code generation for each one.
import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
class DomainmodelGenerator implements IGenerator {
..
}
override void doGenerate(Resource resource, IFileSystemAccess fsa) {
for(e: resource.allContentsIterable.filter(typeof(Entity))) {
}
}
override void doGenerate(Resource resource, IFileSystemAccess fsa) {
for(e: resource.allContentsIterable.filter(typeof(Entity))) {
fsa.generateFile(
e.fullyQualifiedName.toString.replace(".", "/") + ".java",
e.compile)
}
}
def compile(Entity e) '''
package «e.eContainer.fullyQualifiedName»;
public class «e.name» {
}
'''
def compile(Entity e) '''
«IF e.eContainer != null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» {
}
'''
def compile(Entity e) '''
«IF e.eContainer != null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» «IF e.superType != null
»extends «e.superType.fullyQualifiedName» «ENDIF»{
}
'''
def compile(Feature f) '''
private «f.type.fullyQualifiedName» «f.name»;
public «f.type.fullyQualifiedName» get«f.name.toFirstUpper»() {
return «f.name»;
}
public void set«f.name.toFirstUpper»(«f.type.fullyQualifiedName» «f.name») {
this.«f.name» = «f.name»;
}
'''
def compile(Entity e) '''
«IF e.eContainer != null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» «IF e.superType != null
»extends «e.superType.fullyQualifiedName» «ENDIF»{
«FOR f:e.features»
«f.compile»
«ENDFOR»
}
'''
The final code generator looks pretty much like the following code snippet. Now you can give it a try! Launch a new Eclipse Application (Run As -> Eclipse Application on the Xtext project) and create a dmodel file in a Java Project. Now simply create a new source folder src-gen in the that project and see how the compiler will pick up your sample Entities and generate Java code for them.
package org.eclipse.xtext.example.generator
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.IGenerator
import org.eclipse.xtext.generator.IFileSystemAccess
import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
import org.eclipse.xtext.example.domainmodel.*
import org.eclipse.xtext.naming.IQualifiedNameProvider
import com.google.inject.Inject
class DomainmodelGenerator implements IGenerator {
@Inject extension IQualifiedNameProvider nameProvider
override void doGenerate(Resource resource, IFileSystemAccess fsa) {
for(e: resource.allContentsIterable.filter(typeof(Entity))) {
fsa.generateFile(
e.fullyQualifiedName.toString.replace(".", "/") + ".java",
e.compile)
}
}
def compile(Entity e) '''
«IF e.eContainer != null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» «IF e.superType != null
»extends «e.superType.fullyQualifiedName» «ENDIF»{
«FOR f:e.features»
«f.compile»
«ENDFOR»
}
'''
def compile(Feature f) '''
private «f.type.fullyQualifiedName» «f.name»;
public «f.type.fullyQualifiedName» get«f.name.toFirstUpper»() {
return «f.name»;
}
public void set«f.name.toFirstUpper»(«f.type.fullyQualifiedName» «f.name») {
this.«f.name» = «f.name»;
}
'''
}
If you want to play around with Xtend, you can try to use the Xtend tutorial which can be materialized into your workspace. Simply choose New -> Example -> Xtend Tutorial and have a look at Xtend's features. As a small exercise, you could implement support for the many attribute of a Feature or enforce naming conventions, e.g. field names should start with an underscore.
Automated tests are crucial for the maintainability and the quality of a software product. That's why it is strongly recommended to write unit tests for your language, too. The Xtext project wizard creates a test project for that purpose. It simplifies the setup procedure both for the eclipse agnostic tests and the ui tests for Junit4.
This tutorial is about testing the parser and the linker for the Domainmodel. It leverages Xtend to write the testcase.
@InjectWith(typeof(DomainmodelInjectorProvider))
@RunWith(typeof(XtextRunner))
class ParserTest {
}
@Inject
ParseHelper<Domainmodel> parser
@Test
def void parseDomainmodel() {
val model = parser.parse(
"entity MyEntity {
parent: MyEntity
}")
val entity = model.elements.head as Entity
assertSame(entity, entity.features.head.type)
}
One of the main advantages of DSLs is the possibility to statically validate domain specific constraints. This can be achieved by means of static analysis. Because this is a common use case, Xtext provides a dedicated hook for this kind of validation rules. In this lesson, we want to ensure that the name of an Entity starts with an upper-case letter and that all features have distinct names across the inheritance relationship of an Entity.
Try to locate the class DomainmodelJavaValidator in the package org.eclipse.xtext.example.validation. It can be found in the language plug-in. Defining the constraint itself is only a matter of a few lines of code:
@Check
public void checkNameStartsWithCapital(Entity entity) {
if (!Character.isUpperCase(entity.getName().charAt(0))) {
warning("Name should start with a capital",
DomainmodelPackage.Literals.TYPE__NAME);
}
}
Any name for the method will do. The important thing is the @Check (src) annotation that advises the framework to use the method as a validation rule. If the name starts with a lower case letter, a warning will be attached to the name of the Entity.
The second validation rule is straight-forward, too. We traverse the inheritance hierarchy of the Entity and look for features with equal names.
@Check
public void checkFeatureNameIsUnique(Feature f) {
Entity superEntity = ((Entity) f.eContainer()).getSuperType();
while(superEntity != null) {
for(Feature other: superEntity.getFeatures()) {
if (f.getName().equals(other.getName())) {
error("Feature names have to be unique",
DomainmodelPackage.Literals.FEATURE__NAME);
return;
}
}
superEntity = superEntity.getSuperType();
}
}
The sibling features, that are defined in the same entity, are automatically validated by the Xtext framework. Therefore, they do not have to be checked twice.