This post describes how you can support EMF EMaps in your Xtext grammar.
Xtext does not provide any direct support for EMaps, but since EMaps actually are little more than ELists of Map.Entry objects (see also
EMF FAQ), they are quite easy to work with.
Consider the example where we create a simple grammar to parse a properties file with sections (as supported by the
Python ConfigParser) as:
[test]
database=localhost
user=myapp
password=mysecret
The grammar (deliberately kept simple) may look something like this:
grammar org.xtext.example.config.Config with org.eclipse.xtext.common.Terminals
generate config "http://www.xtext.org/example/config/Config"
Config:
sections+=Section*;
Section:
'[' name=ID ']' properties+=Property*;
Property:
key=ID '=' value=(ID|STRING);
As a result we will end up with a Section class with a getProperties() method with the return type EList<Property>. But it would actually be more convenient if the return type were EMap<String, String>, as that would allow us to directly access specific properties.
It turns out to be very easy to achieve this in this case. All we have to do is add an import for the Ecore metamodel and set the return type of the Property rule to EStringToStringMapEntry which is defined in Ecore. It is also essential that the assigned features are called key and value:
grammar org.xtext.example.config.Config with org.eclipse.xtext.common.Terminals
import "http://www.eclipse.org/emf/2002/Ecore"
generate config "http://www.xtext.org/example/config/Config"
Config:
sections+=Section*;
Section:
'[' name=ID ']' properties+=Property*;
Property returns EStringToStringMapEntry:
key=ID '=' value=(ID|STRING);
We were able to reuse the EStringToStringMapEntry EClass of the Ecore metamodel. Of course this only works when both the key and the value are of type EString. If either of them is an attribute of another type (e.g. EInt) or a reference we need to create our own Map.Entry class. We can either do this by not generating the config metamodel (i.e. creating it by hand and importing it) or we can add a very simple post processor.
Let's try the latter. We change the Property rule in the grammar as follows (note the new return type):
Property returns StringToTypedValueMapEntry:
key=ID '=' value=TypedValue;
TypedValue:
text=(ID|STRING) | number=INT;
We have now defined our own EClass StringToTypedValueMapEntry. In order for the Ecore generator to properly generate the EMap accessor we must set the EClass' instance class name to java.util.Map$Entry. So let's do that by creating a file ConfigPostProcessor.ext next to Config.xtext with the following contents:
process(xtext::GeneratedMetamodel this):
ePackage.getEClassifier('StringToTypedValueMapEntry').setInstanceClassName('java.util.Map$Entry')
;
That's it! Note that we could just as well change the grammar so that either of the key and value features are assigned as cross references to other objects (as opposed to containment references and attributes).
Note: There is currently a
bug in Xtext 1.0.1 which prevents EMaps from being parsed correctly. It works correctly in earlier versions and upcoming versions of Xtext.