Eclipse Xtext – parsing and merging multiple artifacts
3 Comments Published November 9th, 2009 in Coding, Diploma ThesisXtext is an extended EBNF language for describing domain-specific languages (DSLs). The extended scope of expressions (compared to EBNF) allows the definition of a corresponding meta-model. Source code can be read by the Xtext parser component MweReader. MweReader is a configurable MWE workflow component, able to transform any artefact in conformity to an Xtext DSL into the target model.
However, some use cases may require transformation of more than one source file. In such cases, every single artefact needs its own workflow setup. Even then it is not possible
- defining multiple input files and directories (recursively read all files in a directory and its subdirectories ending with the DSL’s file extension)
- merging all target models into one
In such cases, another workflow component may help, called MweBulkReader. It combines all features of MweReader plus the aforementioned ones: Define multiple resources, files or folders. Folders are searched for all files with the DSL’s file extension (or a custom one). For each and every artefact MweReader is called, with a consecutively numbered output slot name (“model_{1}” set per default). In the end all created output slots are merged into one slot (named “model” or redefined by property “outputSlot”).
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Vector;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.mwe.core.WorkflowContext;
import org.eclipse.emf.mwe.core.issues.Issues;
import org.eclipse.emf.mwe.core.lib.AbstractWorkflowComponent2;
import org.eclipse.emf.mwe.core.monitor.ProgressMonitor;
import org.eclipse.xtext.ISetup;
import org.eclipse.xtext.MweReader;
import org.eclipse.xtext.resource.XtextResourceFactory;
/**
* Identical to MweReader but is further able to read a list of resources
* specified at <sourceDirectory>s and <Uri>s.
*
* If a resource is a directory it is recursively scanned for files, filtered by extension.
* File extension is defined by registered Xtext DSL or can be manually changed with <extension>.
* Files are parsed into enumerated model slots 'model_{0}'s or <outputSlotFormat>s
* The parser class has to be set by <register>.
* The parser's class path may be set with <classpathURIContext>.
* The parser's validation flag can be set using <validate>.
* A list of all parsed models is made available at slot 'model' or <outputSlot>
*
* @author Andreas Rentschler
*/
public class MweBulkReader extends AbstractWorkflowComponent2 {
public static final String DEFAULT_OUTPUT_SLOT_FORMAT = "model_{0}";
public static final String DEFAULT_OUTPUT_SLOT = "model";
private final Log log = LogFactory.getLog(getClass());
private ISetup setup = null;
public void setRegister(ISetup setup) {
this.setup = setup;
}
private String extension = null;
public void setExtension(String extension) {
this.extension = extension;
}
private String outputSlotFormat = DEFAULT_OUTPUT_SLOT_FORMAT;
public void setOutputSlotFormat(String outputSlotFormat) {
this.outputSlotFormat = outputSlotFormat;
}
private String outputSlot = DEFAULT_OUTPUT_SLOT;
/**
* Sets an accumulation model slot name. If set (non-null and non-empty),
* then a {@link List} of all models read in, is stored in the designated
* model slot. This way, using the {@link #getModelSlots()} method is not
* necessary.
*/
public void setOutputSlot(String outputSlot) {
this.outputSlot = outputSlot;
}
private List<String> sourceDirectories = new ArrayList<String>();
public void addSourceDirectory(String path) {
sourceDirectories.add(path);
}
public void addUri(String path) {
sourceDirectories.add(path);
}
private Object classpathURIContext;
public Object getClasspathURIContext() {
return classpathURIContext;
}
public void setClasspathURIContext(Object classpathURIContext) {
this.classpathURIContext = classpathURIContext;
}
private boolean validate = true;
public void setValidate(boolean validate) {
this.validate = validate;
}
@Override
protected void checkConfigurationInternal(Issues issues) {
if (setup == null)
issues.addError(this,"No setup has been registered (property 'register')");
if (extension == null) {
// register file extension
setup.createInjectorAndDoEMFRegistration();
// take latest file extension registered to Xtext
for (Entry<String, Object> e : Resource.Factory.Registry.INSTANCE.getExtensionToFactoryMap().entrySet()) {
if (e.getValue() instanceof XtextResourceFactory) {
extension = e.getKey();
}
}
if (extension != null)
log.info(getComponentName() + "(" + getId() + "): No explicit file extension configured, found '" + extension + "'");
}
if (extension == null)
issues.addError(this,"No file extension has been registered (property 'register')");
if (sourceDirectories.isEmpty())
issues.addError(this,"No resource uri configured (property 'uri')");
if (outputSlot == null)
issues.addWarning("no explicit output slot configured, using + '" + outputSlot + "'");
}
private int freeSlot = 0;
private List<String> modelSlots = new ArrayList<String>();
private WorkflowContext ctx;
private ProgressMonitor monitor;
private Issues issues;
@Override
protected void invokeInternal(WorkflowContext ctx, ProgressMonitor monitor, Issues issues) {
this.ctx = ctx;
this.monitor = monitor;
this.issues = issues;
for (String directory : sourceDirectories) {
File resource = new File(directory);
if (!resource.exists()) {
issues.addError("'" + resource + "' does not exist!");
} else {
if (resource.isDirectory())
addComponentsForDirectory(resource);
else
addComponentForFile(resource);
}
}
// merge all models into one
List<Object> list = new ArrayList<Object>();
for (String slotName : modelSlots) {
if (ctx.get(slotName) == null) {
// ensure that list of models doesn't contain EVoid elements
log.info(getComponentName() + "(" + getId() + "): Skipping empty slot '" + slotName + "'");
continue;
}
list.add(ctx.get(slotName));
}
ctx.set(outputSlot, list);
}
@Override
public String getLogMessage() {
return "Loading " + sourceDirectories.size() + (sourceDirectories.size() == 1? " resource" : " resources");
}
private void addComponentsForDirectory(File dir) {
log.info(getComponentName() + "(" + getId() + "): Registering models from path '" + dir + "'");
Collection<File> files = listFiles(dir, new FilenameFilter() {
public boolean accept(File directory, String filename) {
if (extension == null) return false;
int index = filename.lastIndexOf('.');
if (index < 0) {
return false;
}
return filename.substring(index + 1).equals(extension);
}
}, true);
for (File f : files) {
addComponentForFile(f);
}
}
private void addComponentForFile(File f) {
String uri = null;
try {
uri = URI.createFileURI(f.getCanonicalPath()).toFileString();
} catch (IOException e) {
e.printStackTrace();
}
String slot = MessageFormat.format(outputSlotFormat, freeSlot++);
modelSlots.add(slot);
String shortUri = (uri.length() > 57) ? "..." + uri.substring(uri.length() - 60) : uri;
log.info(getComponentName() + "(" + getId() + "): Parsing file:" + shortUri + " into slot '" + slot + "'");
MweReader reader = new MweReader();
reader.setContainer(getContainer());
reader.setLocation(getLocation());
//reader.setSkipOnErrors(true);
reader.setClasspathURIContext(classpathURIContext);
reader.setValidate(validate);
reader.setRegister(setup);
reader.setOutputSlot(slot);
reader.setUri("file:" + uri);
try {
reader.invoke(ctx, monitor, issues);
} catch (IndexOutOfBoundsException e) {
// catch weird bug (files producing no elements (not even a container element, maybe some comments)
log.info(getComponentName() + "(" + getId() + "): Skipping file because it is empty");
} catch (Exception e) {
log.info(getComponentName() + "(" + getId() + "): Skipping file because of exception " + e);
}
}
// list files in a directory filtered, optionally recursively
private Collection<File> listFiles(File directory,
FilenameFilter filter, boolean recurse) {
Vector<File> files = new Vector<File>();
File[] entries = directory.listFiles();
for (File entry : entries) {
if (filter == null || filter.accept(directory, entry.getName())) {
files.add(entry);
}
if (recurse && entry.isDirectory()) {
files.addAll(listFiles(entry, filter, recurse));
}
}
return files;
}
}
This is one example of application in a workflow configuration .mwe:
<workflow>
...
<!-- parse all Xtext artefacts in files ${sourceFile1}, ${sourceFile2} and folders ${sourcePath1}, ${sourcePath2} into slot 'mergedModel' -->
<component id="dsl2model" class="my.classpath.to.bulk.reader.MweBulkReader">
<!-- this class has been generated by the xtext generator -->
<register class="my.classpath.to.dsl.strategy.XXXStandaloneSetup"/>
<uri value="file:${sourceFile1}" />
<uri value="file:${sourceFile2}" />
<sourceDirectory value="${sourcePath1}" />
<sourceDirectory value="${sourcePath2}" />
<outputSlot value="mergedModel" />
</component>
...
</workflow>
I’m trying to use MweBulkReader to parse and merge multiple model files written in a DSL defined with xText. I feel like what I am doing should work, but I keep getting the following error:
“Skipping file because of exception org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException: org.xml.sax.SAXParseException: Content is not allowed in prolog.”
Do you have any idea why I might be hitting this error? I am able to call MWEReader directly from the MWE file to process my files one at a time, but not when I try to use the Bulk Reader.
Here is the contents of my MWE:
Here are the log messages:
0 [main] INFO eclipse.emf.mwe.core.WorkflowRunner – ————————————————————————————–
10 [main] INFO eclipse.emf.mwe.core.WorkflowRunner – EMF Modeling Workflow Engine 0.7.2, Build v200908120417
10 [main] INFO eclipse.emf.mwe.core.WorkflowRunner – (c) 2005-2009 openarchitectureware.org and contributors
10 [main] INFO eclipse.emf.mwe.core.WorkflowRunner – ————————————————————————————–
10 [main] INFO eclipse.emf.mwe.core.WorkflowRunner – running workflow: C:/Users/trohloff/Documents/JavaProjects/NGC/ProductSpecsGen4/com.hp.ngc.xtext.generator/src/workflow/NGCLGenerator.mwe
10 [main] INFO eclipse.emf.mwe.core.WorkflowRunner –
446 [main] INFO lipse.emf.mwe.utils.StandaloneSetup – Registering platform uri ‘C:\Users\trohloff\Documents\JavaProjects\NGC\ProductSpecsGen4′
581 [main] INFO e.core.container.CompositeComponent – DirectoryCleaner: cleaning directory ’src-gen/com/hp/ngc’
582 [main] INFO ipse.emf.mwe.utils.DirectoryCleaner – Cleaning C:\Users\trohloff\Documents\JavaProjects\NGC\ProductSpecsGen4\com.hp.ngc.xtext.generator\src-gen\com\hp\ngc
583 [main] INFO e.core.container.CompositeComponent – MweBulkReader(ngc2model): Loading 1 resource
583 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Registering models from path ‘..\runtime-EclipseApplication\Model’
587 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Parsing file:…ProductSpecsGen4\runtime-EclipseApplication\Model\common.ngc into slot ‘model_0′
1411 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Skipping file because of exception org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException: org.xml.sax.SAXParseException: Content is not allowed in prolog.
1416 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Parsing file:…roductSpecsGen4\runtime-EclipseApplication\Model\dl580g5.ngc into slot ‘model_1′
1455 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Skipping file because of exception org.eclipse.emf.ecore.resource.impl.ResourceSetImpl$1DiagnosticWrappedException: org.xml.sax.SAXParseException: Content is not allowed in prolog.
1455 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Skipping empty slot ‘model_0′
1455 [main] INFO workflow.MweBulkReader – MweBulkReader(ngc2model): Skipping empty slot ‘model_1′
1456 [main] INFO e.core.container.CompositeComponent – Generator: generating ‘templates::Template::main FOR model’ => []
1586 [main] ERROR org.eclipse.xpand2.Generator – Error in Component of type org.eclipse.xpand2.Generator:
EvaluationException : No Definition ‘templates::Template::main for List’ found!
[23,42] on line 1 ‘EXPAND templates::Template::main FOR model’
1589 [main] ERROR eclipse.emf.mwe.core.WorkflowRunner – Workflow interrupted. Reason: No Definition ‘templates::Template::main for List’ found!
1590 [main] ERROR eclipse.emf.mwe.core.WorkflowRunner – [ERROR]: No Definition ‘templates::Template::main for List’ found!(Element: EXPAND templates::Template::main FOR model; Reported by: Generator: generating ‘templates::Template::main FOR model’ => [])
Hello Thomas,
it definitly seems weird that the exception is only raised in the caller context of the bulk reader. There is one thread on the eclipse forums discussing a similar problem, just in case you haven’t found it yet:
http://www.eclipse.org/forums/index.php?t=msg&goto=485116&S=8e95881b48b5a3d8c28e7c07d4954c99
Don’t know if this helps, though. Without paying attention to the MWE context I would say such a SAX exception could originate from a false XML encoding, so maybe you should try a different encoding.
Regards,
Andreas
Your suggestions gave me enough of a hint to track down the issue. I’m not sure exactly why, but the file URI setup in the BulkReader was not properly processing my model files. I changed to a toURI() function on File and my system is able to process the files.
private void addComponentForFile(File f) {
String uri = null;
uri = f.toURI().toString();
//Removed the following in favor of the toURI function
// try {
// uri = URI.createFileURI(f.getCanonicalPath()).toFileString();
// } catch (IOException e) {
// e.printStackTrace();
// }
.
.
.
reader.setUri(uri);
//Removed in favor of toURI function
//reader.setUri(“file:” uri);
.
.
.
There is one more change I had to make to get this to work. The top level Element in the merged model appears to be something called ‘List’. I had to make a top level definition for List and then Expand FOREACH Model.
«DEFINE main FOR List»
«EXPAND declarations FOREACH this.typeSelect(Model)»
«ENDDEFINE»
«DEFINE declarations FOR Model»
«EXPAND classDeclaration FOREACH this.declarations.typeSelect(ClassDeclaration)»
«EXPAND partDeclaration FOREACH this.declarations.typeSelect(PartDeclaration)»
«EXPAND productDeclaration FOREACH this.declarations.typeSelect(ProductDeclaration)»
«ENDDEFINE»