Xtext derives a metamodel from the grammar file by default. For more complex languages it is often better to define the metamodel manually since it allows much more control over the AST, the abstract syntax tree. The metamodel has to be an EMF metamodel, which could be defined in Ecore or Xcore. In this article I will show you the setup to build the model project and how to integrate it into the project build.
The Xcore SDK allows to define the metamodel in a DSL which has some advantages against the definition in Ecore. However, using Xcore adds some complexity to the toolchain. Some of these implications are adressed by Holger Schill's presentation "Using Xcore models with Xtext". The presentation also covers the build integration aspect, but with Maven and with the "old" Xtext generator workflows.
Nowadays often Gradle is chosen as the build system to use. The new Xtext project wizard introduced with Xtext 2.9 allows to choose Gradle as the build system and produces the necessary build files for the projects. When using Xtext's web integration it is even mandatory at this time to use Gradle.
The problem now is that when an Xcore metamodel should be used, there is not much known on how to integrate Xcore in a Gradle build. At GitHub there is a xcore-gradle-example, but this only works with Gradle 2.7 and not with the current version 3.1. Further, it uses the old Xtext gradle plugin and not the new Xtext Builder Plugin.
This article shows an example which uses the following technology stack:
All sources are available in our GitHub repository.
As a reference the default "Greeting DSL" is used which is produced by the Xtext project wizard. The following options where chosen on the Advanced Xtext Configuration page:
This creates the following projects:
org.eclipse.example.mydsl.parent | +--org.eclipse.example.mydsl | +--org.eclipse.example.mydsl.ide | +--org.eclipse.example.mydsl.web
For the parent project the latest gradle wrapper was added:
gradle wrapper --gradle-version 3.1
The Xcore model should be defined in a separate subproject org.eclipse.example.mydsl.model
. The new project is added as a subproject to the settings.gradle
of the parent project:
include 'org.xtext.example.mydsl.model' include 'org.xtext.example.mydsl' ... >
It contains the Xcore model mydsl.xcore
in the folder src
. The declared metamodel is equivalent to the one that Xtext would generate from the grammar definition:
@Ecore(nsPrefix="mydsl",nsURI="http://www.xtext.org/example/mydsl/MyDsl") @GenModel( bundleManifest="false", modelDirectory="org.xtext.example.mydsl.model/build/xcore/main", complianceLevel="8.0" ) package org.xtext.example.mydsl class Model { contains Greeting[] greetings } class Greeting { String name }
Note that the model directory is org.xtext.example.mydsl.model/build/xcore/main
. This directory is added as a source folder to the project.
To build the project with Gradle the following build.gradle
is used:
dependencies { compile "org.eclipse.xtext:org.eclipse.xtext:${xtextVersion}" compile "org.eclipse.xtext:org.eclipse.xtext.xbase:${xtextVersion}" compile 'org.eclipse.emf:org.eclipse.emf.ecore.xcore.lib:+' xtextLanguages 'org.eclipse.emf:org.eclipse.emf.ecore.xcore:+' xtextLanguages 'org.eclipse.emf:org.eclipse.emf.ecore.xcore.lib:+' xtextLanguages 'org.eclipse.emf:org.eclipse.emf.codegen.ecore:+' xtextLanguages 'org.eclipse.emf:org.eclipse.emf.codegen.ecore.xtext:+' xtextLanguages "org.eclipse.xtext:org.eclipse.xtext.ecore:${xtextVersion}" } sourceSets { main { resources { exclude '**/*.xcore' } } } xtext { version = "${xtextVersion}" languages { ecore { setup = 'org.eclipse.xtext.ecore.EcoreSupport' } codegen { setup = 'org.eclipse.emf.codegen.ecore.xtext.GenModelSupport' } xcore { setup = 'org.eclipse.emf.ecore.xcore.XcoreStandaloneSetup' generator.outlet.producesJava = true } } }
The main part is the configuration of the xtext-builder-plugin
. EcoreSupport
and GenModelSupport
have to be added as languages, since they are used by Xcore. For Xcore the producesJava
flag has to be set, so that the produced Java classes are compiled and packaged in the resulting jar file.
In the sourceSets section .xcore files are excluded from resources. Without that setting Gradle would copy the .xcore file during the build and within Eclipse there would be unwanted duplication errors.
In the DSL runtime project we have to make some changes in order to use the metamodel defined with Xcore.
First, a build dependency has to be added to the build.gradle
file:
dependencies { compile project(':org.xtext.example.mydsl.model') ... }
Next, two additional dependencies have to be added to the mwe2
configuration:
dependencies { mwe2 "org.eclipse.emf:org.eclipse.emf.mwe2.launch:2.8.3" mwe2 "org.eclipse.xtext:org.eclipse.xtext.xtext.generator:${xtextVersion}" // added for Xcore support mwe2 'org.eclipse.emf:org.eclipse.emf.ecore.xcore:+' mwe2 'org.eclipse.emf:org.eclipse.emf.codegen.ecore.xtext:+' }
Since the grammar's metamodel should not be generated anymore the grammar definition MyDsl.xtext
has to be changed slightly. Instead of declaring the Ecore package to generate
, we import
it:
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals import "http://www.xtext.org/example/mydsl/MyDsl" Model: greetings+=Greeting*; Greeting: 'Hello' name=ID '!';
Also the generator workflow file GenerateMyDsl.mwe2
has to be changed to use the manually defined metamodel.
First, the model project org.xtext.example.mydsl.model
is declared using a project mapping:
bean = org.eclipse.emf.mwe.utils.StandaloneSetup { projectMapping = { path = "${rootPath}/org.xtext.example.mydsl.model" projectName = "org.xtext.example.mydsl.model" } }
This is required in order to reference the mydsl.xcore
file later with a platform resource URI. Without this registration, the platform URI cannot be mapped. The mydsl.xcore
file is now declared as a referencedResource
for the MyDSL language configuration:
language = StandardLanguage { name = "org.xtext.example.mydsl.MyDsl" fileExtensions = "mydsl" referencedResource = "platform:/resource/org.xtext.example.mydsl.model/src/mydsl.xcore" ... }
Standalone Setup
When not using the DSL in the context of an Eclipse plugin, the metamodel's EPackage has to be registered in the EPackage registry. For the case that the metamodel is automatically generated, Xtext takes care of that registration in the generated base class of the DSL's standalone setup class.
Since the metamodel is not automatically registered in the case that it defines manually, the standalone setup class MyDslStandaloneSetup
has to be extended to do the registration process explicitly:
class MyDslStandaloneSetup extends MyDslStandaloneSetupGenerated { def static void doSetup() { new MyDslStandaloneSetup().createInjectorAndDoEMFRegistration() } override register(Injector injector) { if (!EPackage.Registry.INSTANCE.containsKey(MydslPackage.eNS_URI)) { EPackage.Registry.INSTANCE.put(MydslPackage.eNS_URI, MydslPackage.eINSTANCE); } super.register(injector) } }
The following image shows the resulting project structure with the files mydsl.xcore
, MyDsl.xtext
and GenerateMyDsl.mwe2
:
Using an Xcore metamodel for an Xtext grammar requires an additional subproject which holds the Xcore definition. The article showed the necessary setup to build the model project with Gradle and integrate it into the project build.
Some additional changes have to be done to the Xtext grammar to use the predefined metamodel and to integrate it into Xtext's generator workflow, but it's worth the effort especially when it comes to more complex languages.
The Gradle build configuration is a bit tricky, but taking this project as a reference it should be mostly copy/paste work.