The Entities DSL with Xbase

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.

Creating the project

We create a new Xtext project with the following settings:

  • Project name: org.example.xbase.entities
  • Name: org.example.xbase.entities.Entities
  • Extensions: xentities

Defining attributes

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

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:

Defining attributes

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:

Defining attributes

Defining operations

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:

Defining operations

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.

Note

An Xbase expression can have only one logical container.

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"))
    ]
  }
}

Imports

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:

  • Warnings for unused imported types
  • An Organize Imports menu, available also with the keyboard shortcut Ctrl + Shift + O (CMD + Shift + O on Mac)
  • Automatic insertion of import statements: When you use the autocompletion for specifying a Java type reference in the program, the corresponding import statement is automatically added

Tip

Remember to merge the files plugin.xml and plugin.xml_gen in the UI project after running the MWE2 workflow; this is required to add the Organize Imports menu items in the editor.

Validation

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.19.56.45