After you have developed your first own DSL, the question arises how the behavior and the semantics of the language can be customized. Here you find a few mini-tutorials that illustrate common use cases when crafting your own DSL. These lessons are independent from each other. Each of them will be based on the language that was built in the previous domainmodel tutorial.
在你开发了第一个自己的DSL后,问题来了,如何定制语言的行为和语义。在这里,你可以找到一些小型的教程,说明在制作你自己的DSL时常见的用例。这些课程是相互独立的。每一个课程都是基于在前一个领域模型教程中构建的语言。
As soon as you generate the Xtext artifacts from the grammar, a code generator stub is 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.
一旦你从语法中生成Xtext工件,一个代码生成器存根就会被放入你的语言的运行时项目中。让我们深入了解Xtend,看看如何将你自己的代码生成器与Eclipse集成。
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.example.domainmodel.generator. This Xtend class is used to generate code for your models in the standalone scenario and in the interactive Eclipse environment. Let’s make the implementation more meaningful and start writing the code generator. The strategy is to find all entities within a resource and trigger code generation for each one.
首先,在 org.example.domainmodel.generator 包中找到文件 DomainmodelGenerator.xtend。 这个 Xtend 类用于在独立场景和交互式 Eclipse 环境中为您的模型生成代码。 让我们使实现更有意义并开始编写代码生成器。 该策略是查找资源中的所有实体并为每个实体触发代码生成。
First of all, you will have to filter the contents of the resource down to the defined entities. Therefore we need to iterate a resource with all its deeply nested elements. This can be achieved with the method getAllContents(). To use the resulting TreeIterator in a for loop, we use the extension method toIterable() from the built-in library class IteratorExtensions.
首先,你必须将资源的内容过滤到定义的实体。因此,我们需要对一个资源的所有深度嵌套元素进行遍历。这可以通过getAllContents()方法实现。为了在for循环中使用产生的TreeIterator,我们使用内置库类IteratorExtensions中的扩展方法toIterable()。
class DomainmodelGenerator extends AbstractGenerator {
override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
for (e : resource.allContents.toIterable.filter(Entity)) {
}
}
}
Now let’s answer the question how we determine the file name of the Java class that each Entity should yield. This information should be derived from the qualified name of the Entity since Java enforces this pattern. The qualified name itself has to be obtained from a special service that is available for each language. Fortunately, Xtend allows to reuse that one easily. We simply inject the IQualifiedNameProvider into the generator.
现在让我们来回答这个问题,我们如何确定每个Entity应该产生的Java类的文件名。这个信息应该从实体的限定名称中得到,因为Java强制执行这种模式。限定名本身必须从一个特殊的服务中获得,这个服务对每种语言都是可用的。幸运的是,Xtend允许轻松地重用该服务。我们只需将IQualifiedNameProvider注入到生成器中。
@Inject extension IQualifiedNameProvider
This allows to ask for the name of an entity. It is straightforward to convert the name into a file name:
这样可以直接得到实体的名称。将该名称转换为文件名是很简单的。
override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
for (e : resource.allContents.toIterable.filter(Entity)) {
fsa.generateFile(
e.fullyQualifiedName.toString("/") + ".java",
e.compile)
}
}
The next step is to write the actual template code for an entity. For now, the function Entity.compile does not exist, but it is easy to create it:下一步是为一个实体编写实际的模板代码。目前,Entity.compile这个函数并不存在,但要创建它很容易。
private def compile(Entity e) '''
package «e.eContainer.fullyQualifiedName»;
public class «e.name» {
}
'''
This small template is basically the first shot at a Java-Beans generator. However, it is currently rather incomplete and will fail if the Entity is not contained in a package. A small modification fixes this. The package declaration has to be wrapped in an IF expression:
这个小模板基本上是Java-Beans生成器的第一次尝试。然而,它目前是相当不完整的,如果实体不包含在一个包中,就会失败。一个小小的修改可以解决这个问题。包的声明必须被包裹在一个IF表达式中。
private def compile(Entity e) '''
«IF e.eContainer.fullyQualifiedName !== null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» {
}
'''
Let’s handle the superType of an Entity gracefully, too, by using another IF expression:
让我们也通过使用另一个IF表达式来优雅地处理一个实体的superType。
private def compile(Entity e) '''
«IF e.eContainer.fullyQualifiedName !== null»
package «e.eContainer.fullyQualifiedName»;
«ENDIF»
public class «e.name» «IF e.superType !== null
»extends «e.superType.fullyQualifiedName» «ENDIF»{
}
'''
Even though the template will compile the Entities without any complaints, it still lacks support for the Java properties that each of the declared features should yield. For that purpose, you have to create another Xtend function that compiles a single feature to the respective Java code.
尽管该模板会毫无怨言地编译实体,但它仍然缺乏对每个声明的特征应该产生的Java属性的支持。为此,你必须创建另一个Xtend函数,将单个特征编译为相应的Java代码。
private 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»;
}
'''
As you can see, there is nothing fancy about this one. Last but not least, we have to make sure that the function is actually used.
正如你所看到的,这个没有任何花哨之处。最后,我们必须确保该函数被实际使用。
private def compile(Entity e) '''
«IF e.eContainer.fullyQualifiedName !== 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 is listed below. 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. Eclipse will ask you to turn the Java project into an Xtext project then. Simply agree and create a new source folder src-gen in that project. Then you can see how the compiler will pick up your sample Entities and generate Java code for them.
最后的代码生成器列在下面。现在你可以试一试了 启动一个新的Eclipse应用程序(Eclipse Application on the Xtext project),并在一个Java项目中创建一个dmodel文件。然后Eclipse会要求你把这个Java项目变成一个Xtext项目。只需同意并在该项目中创建一个新的源代码文件夹 src-gen。然后你就可以看到编译器将如何接收你的样本实体并为它们生成Java代码。
package org.example.domainmodel.generator
import com.google.inject.Inject
import org.eclipse.emf.ecore.resource.Resource
import org.eclipse.xtext.generator.AbstractGenerator
import org.eclipse.xtext.generator.IFileSystemAccess2
import org.eclipse.xtext.generator.IGeneratorContext
import org.eclipse.xtext.naming.IQualifiedNameProvider
import org.example.domainmodel.domainmodel.Entity
import org.example.domainmodel.domainmodel.Feature
class DomainmodelGenerator extends AbstractGenerator {
@Inject extension IQualifiedNameProvider
override void doGenerate(Resource resource, IFileSystemAccess2 fsa, IGeneratorContext context) {
for (e : resource.allContents.toIterable.filter(Entity)) {
fsa.generateFile(
e.fullyQualifiedName.toString("/") + ".java",
e.compile)
}
}
private def compile(Entity e) '''
«IF e.eContainer.fullyQualifiedName !== 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»
}
'''
private 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 Examples → Xtend Introductory Examples and have a look at the features of Xtend. As a small exercise, you could implement support for the many attribute of a Feature or enforce naming conventions, e.g. generated field names should start with an underscore.
如果你想玩转 Xtend,你可以尝试使用 Xtend 教程,它可以具体化到你的工作区。 只需选择 New → Example → Xtend Examples → Xtend Introductory Examples 并查看 Xtend 的功能。 作为一个小练习,您可以实现对功能的 many 属性的支持或强制执行命名约定,例如 生成的字段名称应以下划线开头。
One of the main advantages of DSLs is the possibility to statically validate domain-specific constraints. 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.
DSL的主要优势之一是可以静态地验证特定领域的约束。因为这是一个常见的用例,Xtext为这种验证规则提供了一个专门的钩子。在本课中,我们要确保实体的名称以大写字母开头,并且所有的特征在实体的继承关系中都有不同的名称。
Locate the class DomainmodelValidator in the package org.example.domainmodel.validation of the language project. Defining the constraint itself is only a matter of a few lines of code:
在语言项目的 org.example.domainmodel.validation 包中找到类 DomainmodelValidator。 定义约束本身只是几行代码的问题:
public static final String INVALID_NAME = "invalidName";
@Check
public void checkNameStartsWithCapital(Entity entity) {
if (!Character.isUpperCase(entity.getName().charAt(0))) {
warning("Name should start with a capital",
DomainmodelPackage.Literals.TYPE__NAME,
INVALID_NAME);
}
}
The sibling features that are defined in the same entity are automatically validated by the Xtext framework, thus they do not have to be checked twice. Note that this implementation is not optimal in terms of execution time because the iteration over the features is done for all features of each entity.
定义在同一实体中的同级特征会被Xtext框架自动验证,因此它们不需要被检查两次。请注意,这种实现在执行时间上并不理想,因为对特征的迭代是针对每个实体的所有特征进行的。
You can determine when the @Check-annotated methods will be executed with the help of the CheckType enum. The default value is FAST, i.e. the checks will be executed on editing, saving/building or on request; also available are NORMAL (executed on build/save or on request) and EXPENSIVE (executed only on request).
你可以在CheckType枚举的帮助下决定何时执行@Check-annotated方法。默认值是FAST,也就是说,检查将在编辑、保存/构建或请求时执行;还有NORMAL(在构建/保存或请求时执行)和EXPENSIVE(仅在请求时执行)。
@Check(CheckType.NORMAL)
public void checkFeatureNameIsUnique(Feature feature) {
...
}
Automated tests are crucial for the maintainability and the quality of a software product. That is why it is strongly recommended to write unit tests for your language. The Xtext project wizard creates two test projects for that purpose. These simplify the setup procedure for testing the basic language features and the Eclipse UI integration.
自动测试对于软件产品的可维护性和质量至关重要。这就是为什么我们强烈建议为你的语言编写单元测试。Xtext 项目向导为此目的创建了两个测试项目。这些项目简化了测试基本语言功能和 Eclipse UI 集成的设置程序。
This tutorial is about testing the parser, the linker, the validator and the generator of the Domainmodel language. It leverages Xtend to write the test cases.
本教程是关于测试Domainmodel语言的解析器、链接器、验证器和生成器。它利用Xtend来编写测试案例。
@RunWith(XtextRunner)
@InjectWith(DomainmodelInjectorProvider)
class DomainmodelParsingTest {
@Inject
ParseHelper<Domainmodel> parseHelper
@Test
def void loadModel() {
val result = parseHelper.parse('''
Hello Xtext!
''')
Assert.assertNotNull(result)
val errors = result.eResource.errors
Assert.assertTrue('''Unexpected errors: «errors.join(", ")»''', errors.isEmpty)
}
}
Note: When using JUnit 5 the InjectionExtension is used instead of the XtextRunner. The Xtext code generator generates the example slightly different, depending on which option you have chosen in the New Xtext Project wizard.
注意:当使用 JUnit 5 时,将使用 InjectionExtension 而不是 XtextRunner。Xtext代码生成器生成的例子略有不同,这取决于你在新Xtext项目向导中选择的选项。
import static org.junit.Assert.*
...
@Test
def void parseDomainmodel() {
val model = parseHelper.parse(
"entity MyEntity {
parent: MyEntity
}")
val entity = model.elements.head as Entity
assertSame(entity, entity.features.head.type)
}
@Inject ParseHelper<Domainmodel> parseHelper
@Inject ValidationTestHelper validationTestHelper
@Test
def testValidModel() {
val entity = parseHelper.parse(
"entity MyEntity {
parent: MyEntity
}")
validationTestHelper.assertNoIssues(entity)
}
@Test
def testNameStartsWithCapitalWarning() {
val entity = parseHelper.parse(
"entity myEntity {
parent: myEntity
}")
validationTestHelper.assertWarning(entity,
DomainmodelPackage.Literals.ENTITY,
DomainmodelValidator.INVALID_NAME,
"Name should start with a capital"
)
}
You can further simplify the code by injecting ParseHelper and ValidationTestHelper as extensions. This feature of Xtend allows to add new methods to a given type without modifying it. You can read more about extension methods in the Xtend documentation. You can rewrite the code as follows:
你可以通过注入ParseHelper和ValidationTestHelper作为扩展来进一步简化代码。Xtend的这个特性允许在不修改一个给定类型的情况下添加新的方法。你可以在Xtend文档中阅读更多关于扩展方法的内容。你可以重写代码如下。
@Inject extension ParseHelper<Domainmodel>
@Inject extension ValidationTestHelper
@Test
def testValidModel() {
"entity MyEntity {
parent: MyEntity
}".parse.assertNoIssues
}
@Test
def testNameStartsWithCapitalWarning() {
"entity myEntity {
parent: myEntity
}".parse.assertWarning(
DomainmodelPackage.Literals.ENTITY,
DomainmodelValidator.INVALID_NAME,
"Name should start with a capital"
)
}
@Inject extension CompilationTestHelper
@Test def test() {
'''
datatype String
package my.company.blog {
entity Blog {
title: String
}
}
'''.assertCompilesTo('''
package my.company.blog;
public class Blog {
private String title;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
''')
}