We will now implement a modified version of the Entities DSL that we implemented in Chapter 2, Creating Your First Xtext Language. This will allow us to implement a more complex DSL where, inside entities, we can also write operations apart from attributes. This is inspired by the Xtext Domain-Model example.
We create a new Xtext project with the following settings:
org.example.xbase.entities
org.example.xbase.entities.Entities
xentities
We define the rules for attributes using some rules inherited from the Xbase grammar:
grammar org.example.xbase.entities.Entities with org.eclipse.xtext.xbase.Xbase generate entities "http://www.example.org/xbase/entities/Entities" Model: entities+=Entity*; Entity: 'entity' name=ID ('extends' superType=JvmParameterizedTypeReference)? '{' attributes += Attribute* '}'; Attribute: 'attr' (type=JvmTypeReference)? name=ID ('=' initexpression=XExpression)? ';';
The rule for Entity
is similar to the corresponding rule of the Entities DSL of Chapter 2, Creating Your First Xtext Language. However, instead of referring to another Entity
in the feature superType
, we refer directly to a Java type; since Xbase implements the Java type system, an entity can extend any other Java type. Moreover, since an Entity
will correspond to a Java class (we will implement this mapping in the inferrer), it will still be able to have an entity as a super type, though it will specify it through the corresponding inferred Java class.
We refer to a Java type using the Xbase rule JvmParameterizedTypeReference
. As the name of the rule suggests, we can also specify type parameters, for instance, we can write:
entity MyList extends java.util.LinkedList<Iterable<String>> {}
Similarly, for attributes, we use Java types for specifying the type of attribute. In this case, we use the Xbase rule JvmTypeReference
; differently from JvmParameterizedTypeReference
, this rule also allows to specify types for lambdas. Thus, for instance, we can define an attribute as follows:
attr (String,int)=>Boolean c;
We also allow an attribute to specify an initialization expression using a generic Xbase expression, XExpression
. Note that both the type and the initialization expression are optional; this design choice will be clear after looking at the model inferrer:
class EntitiesJvmModelInferrer extends AbstractModelInferrer { @Inject extension JvmTypesBuilder def dispatch void infer(Entity entity, IJvmDeclaredTypeAcceptor acceptor, boolean isPreIndexingPhase) { acceptor.accept(entity.toClass("entities." + entity.name)) [ documentation = entity.documentation if (entity.superType != null) superTypes += entity.superType.cloneWithProxies for (a : entity.attributes) { val type = a.type ?: a.initexpression?.inferredType members += a.toField(a.name, type) [ documentation = a.documentation if (a.initexpression != null) initializer = a.initexpression ] members += a.toGetter(a.name, type) members += a.toSetter(a.name, type) } ] } }
Note that, in this example, we provide an infer
method for Entity
, not for the root Model
. In fact, the default implementation of the superclass AbstractModelInferrer
can be summarized as follows:
public void infer(EObject e, ...) { for (EObject child : e.eContents()) { infer(child, acceptor, preIndexingPhase); } }
It simply calls the infer
method on each element contained in the root of the model. Thus, we only need to provide a dispatch method infer
for each type of our model that we want to map to a Java model element. In the previous example, we needed to map the whole program, that is, the root model element, while in this example we map every entity of a program.
As we did in the previous section, we use an injected JvmTypesBuilder
extension to create the Java model elements and associate them with the elements of our DSL program AST.
First of all, we specify that the superclass of the mapped class will be the entity's superType
if one is given.
Note that we clone the type reference of superType
. This is required since superType
is an EMF containment reference. The referred element can be contained only in one container; thus, without the clone, the feature superType
would be set to null
after the assignment.
The clone is performed using cloneWithProxies
, which clones an EMF object without resolving cross references.
For each attribute, we create a Java field using toField
, which returns a JvmField
instance, and a getter and setter method using toGetter
and toSetter
, respectively (these are part of JvmTypesBuilder
). If an initialization expression is specified for the attribute, the corresponding Java field will be initialized with the Java code corresponding to the XExpression
.
The interesting thing in the mapping for attributes is that we use Xbase type inference mechanisms; if no type is specified for the attribute, the type of the Java field will be automatically inferred by Xbase using the type of the initialization expression. If neither the type nor the initialization expression is specified, Xbase will automatically infer the type Object
. If we specify both the type of the attribute and its initialization expression, Xbase will automatically check that the type of the initialization expression is conformant to the declared type. The following screenshot shows a validation error issued by Xbase:
Both for the mapped Java model class and Java model field we set the documentation
feature using the documentation attached to the program element. This way, if in the program we write a comment with /* */
before an entity or an attribute, in the generated Java code this will correspond to a JavaDoc
comment, as illustrated in the following screenshot:
Now we add operations to our entities, which will correspond to Java methods. Thus, we add the rule for Operation
. To keep the example simple and to concentrate on Xbase, we did not introduce an abstract element for both attributes and operations, and we require that operations are specified after the attributes:
Entity: 'entity' name=ID ('extends' superType=JvmParameterizedTypeReference)? '{' attributes += Attribute* operations += Operation* '}'; Operation: 'op' (type=JvmTypeReference)? name=ID '(' (params+=FullJvmFormalParameter (',' params+=FullJvmFormalParameter)*)? ')' body=XBlockExpression;
Here, we use the Xbase rule FullJvmFormalParameter
to specify parameters; parameters will have the syntactic shape of Java parameters, that is, a JvmTypeReference
stored in the feature parameterType
and a name. However, the keyword final
is not considered by the FullJvmFormalParameter
rule, in fact, just like in Xtend, Xbase parameters are implicitly final
. The body of an operation
is specified using the Xbase rule XBlockExpression
, which includes the curly brackets.
In our inferrer, we add the mapping to a Java model method with the following code:
for (op : entity.operations) { members += op.toMethod(op.name, op.type ?: inferredType) [ documentation = op.documentation for (p : op.params) { parameters += p.toParameter(p.name, p.parameterType) } body = op.body ] }
This is similar to what we did in the first example of this chapter. We still use toMethod
; however, this time, we have a corresponding element in our DSL, Operation
. Thus, we create a Java model parameter for each parameter defined in the program, and we use the Operation
instance's body as the body of the mapped Java model method. Also, for the operation, the return type can be omitted; in that case, the corresponding Java model method will have the return type that Xbase infers from the operation's XBlockExpression
.
The association of the method's body with the operation's body implicitly defines the scope of the XBlockExpression
object. Since an operation is mapped to a non-static Java method, in the operation's expressions you can automatically refer to the attributes and operations of the containing entity and of the entity's superType
, since they are mapped to Java fields and methods, respectively.
This can be seen in the following screenshot, where the operation accesses the entity's fields and the inherited method add
; Xbase automatically adds other tooling features, such as information hovering and the ability to jump to the corresponding Java method:
In fact, the scope for this
is implied by the association between the operation and the Java model method. The same holds for super
, as shown in the following example, where the operation m
overrides the one in the supertype
and can access the original version with super
:
entity Base { op m() { "Base" } } entity Extended extends Base { op m() { super.m() + "Extended" } }
Indeed, associating an Xbase expression with the body of a Java model method corresponds to making the expression logically contained in the Java method. This logical containment defines the scope of the Xbase expression.
In the examples we have shown so far, we have always associated an XExpression
with the body of a Java method or with the initializer of a field. There might be cases where you want to add a method to the Java model that does not necessarily correspond to an element of the DSL.
In these cases, you can specify an Xtend template string as the body of the created method. For instance, we can add to the mapped Java class a toString
method as follows:
members += entity.toMethod("toString", typeRef(String)) [ body = ''' return "entity «entity.name» { " + «FOR a : entity.attributes» " «a.name» = " + «a.name».toString() + " " + «ENDFOR» "}"; ''' ]
For instance, given this entity definition:
entity C { attr List l; attr s = "test"; }
The generated Java method will be:
public String toString() { return "entity C { " + " l = " + l.toString() + " " + " s = " + s.toString() + " " + "}"; }
As usual, remember to write JUnit tests for your DSL (see Chapter 7, Testing); for instance, this test checks the execution of the generated toString
Java method:
@RunWith(XtextRunner) @InjectWith(EntitiesInjectorProvider) class EntitiesCompilerTest { @Inject extension CompilationTestHelper @Inject extension ReflectExtensions @Test def void testGeneratedToStringExecution() { ''' entity C { attr l = newArrayList(1, 2, 3); attr s = "test"; }'''.compile[ val obj = it.compiledClass.newInstance ''' entity C { l = [1, 2, 3] s = test }'''.toString.assertEquals(obj.invoke("toString")) ] } }
We saw in Chapter 10, Scoping, that Xtext provides support for namespace-based imports for qualified names. Xbase provides an automatic mechanism for imports of Java types and many additional UI features for the Eclipse editor. To include such feature in a DSL that uses Xbase it is enough to use the rule XImportSection
; for instance, in our Xbase Entities DSL we modify the root rule as follows:
Model: importSection=XImportSection? entities+=Entity*;
The addition of this rule automatically adds to the DSL the same import mechanisms that you saw in Xtend; in addition to the standard Java imports, including static imports, you can now write import static extension
statements for static methods and those static methods will be available as extension methods in the program.
Besides this enhanced import statements, Xbase adds nice UI features in the editor of the DSL that reflect the ones of JDT and Xtend:
Xbase automatically performs type checking and other validation checks on the Xbase expressions used in your DSL. Other validation checks, concerning the structural parts of the DSL, are left to the language implementor. For example, we still need to implement checks on possible cycles in the inheritance hierarchy. In Chapter 4, Validation, we implemented a similar validation check. In this example, though, we cannot walk the hierarchy of entities, since an entity, in this DSL, does not extend another entity, it extends any Java type (superType
is a JvmParameterizedTypeReference
).
Thus, the search for possible cycles must be performed on the inferred Java model, in particular on JvmGenericType
objects, which are the ones created in the inferrer with the method toClass
. This is performed by the following recursive method:
def private boolean hasCycleInHierarchy(JvmGenericType t, Set<JvmGenericType> processed) { if (processed.contains(t)) return true processed.add(t) return t.superTypes.map[type].filter(JvmGenericType). exists[hasCycleInHierarchy(processed)] }
We use this method in this validator check method:
protected static val ISSUE_CODE_PREFIX = "org.example.xbase.entities."; public static val HIERARCHY_CYCLE = ISSUE_CODE_PREFIX + "HierarchyCycle"; @Inject extension IJvmModelAssociations @Check def checkNoCycleInEntityHierarchy(Entity entity) { val inferredJavaType = entity.jvmElements.filter(JvmGenericType).head if (inferredJavaType.hasCycleInHierarchy(newHashSet())) { error("cycle in hierarchy of entity '" + entity.name + "'", EntitiesPackage.eINSTANCE.entity_SuperType, HIERARCHY_CYCLE) } }
We call the recursive method passing the inferred JvmGenericType
for the given entity. The mapping between DSL elements and Java model elements is handled by Xbase. You can access such mapping by injecting an IJvmModelAssociations
. Note that, in general, you can infer several Java model elements from a single DSL element, that is why the method IJvmModelAssociations.getJvmElements
returns a set of associated inferred elements.
3.19.56.45