Xtext’s integration of Xtend based code generators is nice. If you save changed (or add/remove) DSL model files an incremental build will automatically be triggered and even removes derived artifacts if you remove the source from which it is generated. The IGenerator interface takes a Resource, and you can generate 1 or multiple target artifacts from the model elements within that resource.
However, the API does not allow to create a single artifact from multiple input models. But this is a common use case if you do serious code generation. I will show you how you could extend Xtext’s Domainmodel example to generate a simple text file that lists the name of all Entity instances of all .dmodel files that are reachable in the built project.
Adding an extended generator interface
Create an interface IGenerator2 that extends the IGenerator interface:
public interface IGenerator2 extends IGenerator {
/**
* @param input - the input for which to generate resources
* @param fsa - file system access to be used to generate files
*/
public void doGenerate(ResourceSet input, IFileSystemAccess fsa);
}
The generator class will later implement this interface and generate a single artifact from all Entity instances in all resources.
Extending the JavaProjectBasedBuilderParticipant
The JavaProjectBasedBuilderParticipant is used by default to invoke the IGenerator implementation that was bound to the runtime module. We will extend this class to add additional logic and inject an IGenerator2 instance that will be invoked once per triggered build.
The builder participant is an UI concept, so create the extended class JavaProjectBasedBuilderParticipant2 in the UI plugin:
package org.eclipse.xtext.builder;
import java.util.List;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.xtext.builder.JavaProjectBasedBuilderParticipant;
import org.eclipse.xtext.generator.IFileSystemAccess;
import org.eclipse.xtext.generator.IGenerator2;
import org.eclipse.xtext.resource.IContainer;
import org.eclipse.xtext.resource.IResourceDescription;
import org.eclipse.xtext.resource.IResourceDescription.Delta;
import org.eclipse.xtext.resource.IResourceDescriptions;
import org.eclipse.xtext.resource.impl.ResourceDescriptionsProvider;
import com.google.inject.Inject;
public class JavaProjectBasedBuilderParticipant2 extends JavaProjectBasedBuilderParticipant {
@Inject
private ResourceDescriptionsProvider resourceDescriptionsProvider;
@Inject
private IContainer.Manager containerManager;
@Inject (optional=true)
private IGenerator2 generator;
protected ThreadLocal<Boolean> buildSemaphor = new ThreadLocal<Boolean>();
@Override
public void build(IBuildContext context, IProgressMonitor monitor) throws CoreException {
buildSemaphor.set(false);
super.build(context, monitor);
}
@Override
protected void handleChangedContents(Delta delta, IBuildContext context, IFileSystemAccess fileSystemAccess) {
super.handleChangedContents(delta, context, fileSystemAccess);
if (!buildSemaphor.get() && generator != null) {
invokeGenerator(delta, context, fileSystemAccess);
}
}
@Override
protected void handleDeletion(Delta delta, IBuildContext context, IFileSystemAccess fileSystemAccess) {
super.handleDeletion(delta, context, fileSystemAccess);
if (!buildSemaphor.get() && generator != null) {
invokeGenerator(delta, context, fileSystemAccess);
}
}
private void invokeGenerator (Delta delta, IBuildContext context, IFileSystemAccess fileSystemAccess) {
buildSemaphor.set(true);
Resource resource = context.getResourceSet().getResource(delta.getUri(), true);
if (shouldGenerate(resource, context)) {
IResourceDescriptions index = resourceDescriptionsProvider.createResourceDescriptions();
IResourceDescription resDesc = index.getResourceDescription(resource.getURI());
List<IContainer> visibleContainers = containerManager.getVisibleContainers(resDesc, index);
for (IContainer c : visibleContainers) {
for (IResourceDescription rd : c.getResourceDescriptions()) {
context.getResourceSet().getResource(rd.getURI(), true);
}
}
generator.doGenerate(context.getResourceSet(), fileSystemAccess);
}
}
}
Note that IGenerator instance will be invoked once per changed resource, so potentially multiple times. We have to make sure that the IGenerator2 instance will be invoked just once. Therefore a thread local semaphor flag will be queried before the generator is called. In this scenario it does not matter whether a change or deletion should be handled. The generator always processes the whole model once.
The most important thing is to populate the BuildContext’s ResourceSet with all relevant model resources. It is important here collect the resources that are in scope of the built project. The right way to do this is to use the language’s IContainer.Manager to collect the resources that are within the visible containers. Each of them is queried for its contained IResourceDescription instances, from which the Resource instances are created.
Extending the generator class
Now open the generator class DomainmodelGenerator and implement IGenerator2. Add an implementation of the method from the extended interface:
...
// makes allContentsIterable available
import static extension org.eclipse.xtext.xtend2.lib.ResourceExtensions.*
...
class DomainmodelGenerator implements IGenerator2 {
...
override void doGenerate(ResourceSet rs, IFileSystemAccess fsa) {
val Iterable<Entity> entities = rs.resources.map(r|r.allContentsIterable.filter(typeof(Entity))).flatten
fsa.generateFile("entities.txt", entities.compile)
}
def compile (Iterable<Entity> entities) '''
«FOR e : entities SEPARATOR ", "»«e.name»«ENDFOR»
'''
...
}
Guice configuration
Now bind the extended builder particpant in the UI module:
public class DomainmodelUiModule extends AbstractDomainmodelUiModule {
@Override
public Class<? extends IXtextBuilderParticipant> bindIXtextBuilderParticipant() {
return JavaProjectBasedBuilderParticipant2.class;
}
}
… and the generator to the Runtime module:
public class DomainmodelRuntimeModule extends AbstractDomainmodelRuntimeModule {
public Class<? extends IGenerator2> bindIGenerator2 () {
return DomainmodelGenerator.class;
}
}
Result & Conclusion
After restarting your runtime workspace the generator will produce an “entities.txt” file to the src-gen folder of a project with .dmodel files, which will simply list the names of all found Entity instances.
This scenario is quite common, so it would be great if the Xtext framework could provide built-in support for that. I have therefore raised a feature request.