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:
[lowerBound…upperBound]
E.g.:
[0..1], [1..10], [1..*]
The cardinality expression has one tricky part, how do we handle the '*'?
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:
Cardinality:
'[' lowerBound=INT '..' (upperBound= INT | upperBound=*)']';
But that will not work. The problem is of course that there is no way to assign a value for upperBound to -1. There has been some talk (in various newsgroup entries) to extend Xtext this way, but as it is now, we need to use another trick.
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.
Cardinality:
'[' lowerBound=ElementBound '..' upperBound=ElementBound ']';
ElementBound returns ecore::EIntegerObject: // datatype rule handled by a value converter
'*' | INT;
Notice in the example above that we have specified that ElementBound returns an integer object (ecore::EIntegerObject). The framework already knows how to convert INT to and integer, but it would fail if we entered '*'.
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:
package com.inferdata.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<Integer> ElementBound() {
return new IValueConverter<Integer>() {
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.
@ValueConverter(rule = "ElementBound")
public IValueConverter<Integer> ElementBound() { … }
There are several rules here that we have to follow:
- 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<Integer>
- 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> interface dictates that we have to provide two methods:
- 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 <NAME_OF_YOUR_LANGUAGE>RuntimeModule. It is important that you place it here as the other places that use GUICE are in generated code and hence will be overridden on subsequent code generation cycles.
package com.inferdata;
import com.inferdata.converter.MyLangValueConverter;
import org.eclipse.xtext.conversion.IValueConverterService;
/* Use this class to register components to be used within the IDE.
*/
public class MyLangRuntimeModule extends gov.mt.dli.AbstractSDOLRuntimeModule {
@Override
public Class<? extends IValueConverterService> bindIValueConverterService() {
return MyLangValueConverter.class;
}
}
As mentioned, Xtext here relies on GUICE to inject the code. What we've done is that we've replaced the default value converter with our own value converter called MyLangValueConverter. This is done with the bindIValueConverterService.
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
- 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 = "<THE NAME OF YOUR RULE>")
- Name the method after your rule
- Return an instance that implements IValueConverter<TYPE> 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
- Annotated factory method with the annotation @ValueConverter(rule = "<THE NAME OF YOUR RULE>")
- 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
- Implements toValue that converts from a string to the type you require
- Reconfigure the parser using Google's GUICE
- Provide a bind statement that binds the value conversion service to your specialized factory class
View comments