Xtext Tip: Migrating to manual EMF model

Xtext is very good at inferring a metamodel from a grammar but it has its limitations. After reaching a certain complexity, I think it is important to decouple the grammar from the metamodel. This is allows to separate concerns: the metamodel becomes the “business model layer”, and the grammar becomes the “presentation layer”. To do this, we need to transform our project to this new setup. In this post I explain how. The full code for this example can be found here.

Overview

There are several steps to perform this migration:

  • Move the generated model from the model/generated/ folder.
  • Update the location of the .ecore and .genmodel file in the files that reference them.
  • Update the grammar (.xtext file) so that the model is no longer generated.
  • Modify the MWE2 workflow to process automatically generate the model.

Move the generated model

Xtext generates an .ecore and a .genmodel file from the grammar at folder model/generated/. Since the file is no longer generated, it is a good idea to move it to another folder, for example, the model/folder. It is important remove the original .ecore and .genmodel files, since there might be strange errors in the editors if there are two .ecore files with the same identifiers.

Update the location

There are several files in the project that reference the files in the model/generated folder. These are:

  • build.properties, where we need to update the path in the bin.includes section.
  • plugin.xml, where we need to update the path to the genmodel file.

Update the grammar

If you are using a generated ecore file, then you have a line like this in your .xtext file:

generate block "http://www.idiomaticsoft.com/dsl/block/Block"

This line needs to be replaced by the following line:

import "http://www.idiomaticsoft.com/dsl/block/Block"

Thus indicating Xtext that it needs to import the metamodel with that identifier.

Modify the MWE2 workflow

The mwe2 workflow is heavily modified. We start from the the mwe2 file of the In a block language and transform it as follows:

module com.idiomaticsoft.dsl.block.GenerateBlock

import org.eclipse.xtext.xtext.generator.*
import org.eclipse.xtext.xtext.generator.model.project.*
import org.eclipse.emf.mwe.utils.*

var rootPath = ".."
var projectName = "com.idiomaticsoft.dsl.block"
var runtimeProject = "../${projectName}"

Workflow {
	// This cleans the directory before generating
	component = DirectoryCleaner {
		directory = "${runtimeProject}/src-gen"
	}
	// This handles the generation of the model classes
	component = org.eclipse.emf.mwe2.ecore.EcoreGenerator {
		genModel = "platform:/resource/com.idiomaticsoft.dsl.block/model/Block.genmodel"
		srcPath = "platform:/resource/com.idiomaticsoft.dsl.block/src-gen"
	}
	  // This handles the generation of the standalone setub
    bean = org.eclipse.emf.mwe.utils.StandaloneSetup {
        scanClassPath = true
        platformUri = "${rootPath}"
     }	

	component = XtextGenerator {
    // We tell the xtext generator to avoid cleaning, because otherwise it
    // would remove the classes generated by the ecore
		cleaner = {
			enabled = false
		} 
		configuration = {
			project = StandardProjectConfig {
				baseName = "com.idiomaticsoft.dsl.block"
				rootPath = rootPath
				runtimeTest = {
					enabled = true
				}
				eclipsePlugin = {
					enabled = true
				}
				eclipsePluginTest = {
					enabled = true
				}
				createEclipseMetaData = true
			}
			code = {
				encoding = "UTF-8"
				lineDelimiter = "\n"
				fileHeader = "/*\n * generated by Xtext \${version}\n */"
				preferXtendStubs = false
			}
		}
		language = StandardLanguage {
			name = "com.idiomaticsoft.dsl.block.Block"
			fileExtensions = "block"
      // we tell the generator that we are using our Block.genmodel file
			referencedResource = "platform:/resource/com.idiomaticsoft.dsl.block/model/Block.genmodel"
			serializer = {
				generateStub = false
			}
			validator = {
				composedCheck = "org.eclipse.xtext.validation.NamesAreUniqueValidator"
				// Generates checks for @Deprecated grammar annotations, an IssueProvider and a corresponding PropertyPage
				generateDeprecationValidation = true
			}
			generator = {
				generateXtendStub = true
			}
			junitSupport = {
				junitVersion = "5"
			}
		}
	}
}

Conclusion

After all those modifications, we can modifying our .ecore files separately and they will be integrated with our Xtext grammar.

comments powered by Disqus