I thought I'd share another little trick in Xtext. This trick is actually documented well at various places on the web, but to be able to do what I wanted to do, I had to pull information from many different locations. So… I thought it may be a good idea to collect the tips here and share them with my readers.
What I want to achieve?
Occasionally, I run into parsing problems that cannot be solved directly in Xtext. The example I'll show you is to parse a cardinality expression.
What I want to parse is a UML-ish cardinality in the form:
E.g.:
[0..1], [1..10], [1..*]In my case I have a model (or meta-model if you so prefer) where I want to create a cardinality object that has two attributes, both being of type int. The lower bound is typically 0 or 1 (theoretically, any number, but practically either 0 or 1). The upper bound can be 1, n (>1) or '*' which I translate to -1 in my model (similar to what ecore does if you know EMF).
The first (erroneous) attempt
It is tempting at first to try to do something like this:
'[' lowerBound=INT '..' (upperBound= INT | upperBound=*)']';
Value converters
The trick to make this work is to use a value converter. Let me jump straight to it and show you the Xtext fragment.
'[' lowerBound=ElementBound '..' upperBound=ElementBound ']';
ElementBound returns ecore::EIntegerObject: // datatype rule handled by a value converter
'*' | INT;
We then have to provide a value converter that can transform the parsed text (an INT or '*') to an integer. We do so by writing a simple Java class.
Writing and attaching the value converter
To create the value converter you create a new Java class. This class can be named anything you want. In Xtext you just have to make sure it is available in the classpath used when parsing. In practical terms, that means you'll place it somewhere in the source directory of your parser project. The example below, I've placed the value converter in a package called 'com.inferdata.converter'. I've named the class 'CardinalityConverter', but as I said, the class name and package name is your choice.
The class needs to implement the following one method. The method constructs a value converter for the particular rule that needs custom conversion.
In general, you would also want to ensure that you subclass org.eclipse.xtext.common.services.DefaultTerminalConverters. The reason is that this class already knows how to transform cross-links, INT, String, etc that you probably rely on elsewhere.
Here is a working converter:
import org.eclipse.xtext.common.services.DefaultTerminalConverters;
import org.eclipse.xtext.conversion.IValueConverter;
import org.eclipse.xtext.conversion.ValueConverter;
import org.eclipse.xtext.conversion.ValueConverterException;
import org.eclipse.xtext.parsetree.AbstractNode;
import org.eclipse.xtext.util.Strings;
public class MyLangValueConverter extends DefaultTerminalConverters {
@ValueConverter(rule = "ElementBound")
public IValueConverter
return new IValueConverter
public Integer toValue(String string, AbstractNode node) {
if (Strings.isEmpty(string))
throw new ValueConverterException("Couldn't convert empty string to int", node, null);
else if ("*".equals(string.trim()))
return -1;
try {
return Integer.parseInt(string);
} catch (NumberFormatException e) {
throw new ValueConverterException("Couldn't convert '"+string+"' to int", node, e);
}
}
public String toString(Integer value) {
return ((value == -1) ? "*" : Integer.toString(value));
}
};
Explaining the converter code
The first method is a factory method to create an object that converts the result of the ElementBounds rule to an Integer.
public IValueConverter
- The method must have an annotation of type ValueConverter. This rule must specify one attribute, the rule to which the method applies. In our case we are providing a converter for the rule 'ElementBound'
- The method must return an instance implementing the IValueConverter of the type we want to construct. In our case, we are converting to an Integer, hence, we must return an instance that implements IValueConverter
- The method must be named the same as the rule. In our case, that means that the method must be named 'ElementBound'
The IValueConverter
- Integer toValue(String s, AbstractNode n)
One to translate from a string to an Integer (Integer, because that was our generic type parameter). We also get access to the AbstractNode in the parse tree (which we will not need in this example) - String toString(Integer value)
The inverse operation of 'toVlaue'. Translate from an Integer to a String
The internal algorithms in the methods are rather straight forward, and I will not explain them here.
Configuring the value converter
Xtext uses GUICE to configure the value converter. You can read about the Google-Guice at Google's site. Basically, GUICE is used to inject the value converter into the framework.
The file we need to modify to inject our new value converter is (typically) placed in the same directory where you have your xtext grammar file. It is called /* Use this class to register components to be used within the IDE.
import com.inferdata.converter.MyLangValueConverter;
import org.eclipse.xtext.conversion.IValueConverterService;
*/
public class MyLangRuntimeModule extends gov.mt.dli.AbstractSDOLRuntimeModule {
@Override
public Class extends IValueConverterService> bindIValueConverterService() {
return MyLangValueConverter.class;
}
}
Conclusion
In some cases, it is necessary to build custom value converters for some of the rules in Xtext. This need may go away (or lessen) with new versions of Xtexts.
To attach a new value converter you have to:
- Modify the xtext grammar
- Define the rule specifying a 'returns' clause
- Create a value converter factory class that instantiates a specialized value converter for each of the rules that needs customized conversions
- Annotated factory method with the annotation @ValueConverter(rule = "
") - Name the method after your rule
- Return an instance that implements IValueConverter
where, TYPE is whatever your return from the xtext specialized clause - You typically want this class to extend the default factory class provided by the Xtext framework
- Provide a specialized implementation of the IValueConverter (we used an anonymous inner class in our example)
- Implements toValue that converts from a string to the type you require
- Implements toString that converts from the value back into a string
- Reconfigure the parser using Google's GUICE
- Provide a bind statement that binds the value conversion service to your specialized factory class