This tutorial introduces the ELIDE Java preprocessor. ELIDE is a tool for incrementally extending Java with new design vocabulary: in particular, it allows the introduction of new modifier keywords which can trigger complex transformations on Java code.In this tutorial, we will show how to use ELIDE to introduce a simple "property" keyword that adds JavaBean style accessors to any instance variable. For example, the following class definition:public class ElideTest { property<> public int x; property<> public String[] y; }will generate the following code:public class ElideTest implements Serializable { private int x; private String[] y; public int getX() { return x; } public void setX(int x) { this.x = x; } public String[] getY() { return y; } public void setY(String[] y) { this.y = y; } }This example will take you from basic ELIDE concepts and API up some of the more advanced features of the tool. First, before we examine the framework underlying this language extension, let's make sure ELIDE is installed correctly on your system and that everything is working properly. You might also want to look over the Introduction to ELIDE Concepts.
Running ELIDE
ELIDE takes a list of Java files to be processed on the command line.
As a test, create a file named ElideTest.java with the following code:
Adding a new keyword to Java
Adding a transformation method to the Transform
The method itself is extremely simple: it takes a FieldNode as a parameter,
which represents the instance variable declaration to which the property<>
keyword was attached. This method will be called once for each time the
property<> keyword appears. The body of the method makes a simple call to
the FieldNode to change its access from public to private.
Running ELIDE with a transform
For ease of development, an automated build tool such as GNU make or Ant is
highly recommended (an Ant task for ELIDE is provided in elide.jar; see the
ANT.txt file), so that these three commands can
all be run at once. Try them out now. This time, the file generated in
output should have a subtle change: the instance variables that were marked
public in the original file are now marked private.
Extending a class
The Utils class provides a number of convenience methods such as methodCase (which capitalizes
the first letter of a string) - for a full reference, see the Transformation Definition Reference
Your class should currently look like this:
Controlling transformations
Finishing up
ELIDE is packaged as a single jar file, elide.jar. To run ELIDE, make
sure that this jar file is in the current directory, and run
"java -jar elide.jar".
public class ElideTest
{
public int x;
public String[] y;
}
Now run ELIDE on the file:
> java -jar elide.jar ElideTest.java
ELIDE will create a directory called "output", within which you will find
a new ElideTest.java file. It should be identical to the file you created,
with minor changes in indentation and line breaks. This is as it should be:
ELIDE does no processing of normal Java code.
To get ELIDE to do something to your code, you must introduce a new extension
to Java. This is done by subclassing ELIDE's Transform class. Since we want
to introduce the "property" keyword, we need to create a file called
Property.java. The skeleton should look like this:
package tutorial;
import ca.ubc.cs.elide.*;
import ca.ubc.cs.elide.nodes.*;
public class Property extends Transform
{
}
This code isn't going to do much, of course. We'll extend it later. For now,
modify your ElideTest file so that it looks like the version at the very beginning
of the tutorial - that is, add "property<>" in front of each instance variable
declaration. (Note that all of the keywords introduced by ELIDE are treated
in exactly the same way as modifiers like "public" and "private", so that you could in
fact write "public property<> int x" just as well as "property<> public int x",
and in general can string ELIDE keywords together anywhere you would put a modifier;
the only difference is that ELIDE keywords always have "<>" at the end, which may
contain an argument list for more complex transformations).
Each subclass of Transform can have any number of transformation methods; these are the methods
that specify the effects of a new keyword. Transformation methods must have two properties:
Here is an example transformation method; add it to your Property class:
satisfies<fieldAccess>
public void makeFieldPrivate(FieldNode target)
{
target.makePrivate();
}
The word "fieldAccess" inside satisfies<> is a description of the type of transformation this method
is performing. It is used to determine the order in which to perform some transformations; for more
information, see the Introduction to ELIDE
Concepts.
Once you've written a transformation method for it,
itroducing a new keyword into ELIDE is actually a three step process:
Once you are happy with a particular Transform you can skip the first
two steps. Until then, you'll need something like the following three commands,
in sequence:
> java -jar elide.jar Property.java -d working
The -d flag specifies which directory to output the new source files into;
you should use "working" for Transform files.
> javac -classpath elide.jar working/tutorial/*.java
Since we specified the "tutorial" package for Property.java, the processed file
will be placed in the tutorial directory within working/.
> java -jar elide.jar ElideTest.java -P tutorial
The -P flag specifies a package of Transform subclasses to use; when it hits a
keyword such as property<>, ELIDE will look for a matching class in this package.
The -P flag can be used multiple times for one run if you have multiple Transform packages.
A single keyword often has multiple effects, and so needs multiple
transformation methods. Add another method to the Property class, with the
following skeleton:
satisfies<methodDefinition>
public void addAccessors(FieldNode target)
{
}
We want this method to generate new accessor methods for the field it is given. To do so, first
we will need to get a representation of the class in which the field was defined, like so:
ClassNode targetClass = target.getDeclaringClass();
Next we will use the extend() method of ClassNode. This allows new members to be added to the class
simply by specifying their source code. extend() takes a simple String, but we will create the String
using a special string literal syntax introduced in ELIDE: instead of quotes, strings are enclosed between
%{ and }%. These strings have three special properties:
For example,
String text = %{ this is "test" number #{x + 5}#! }%;
is equivalent to
String text = " this is \"test\" number " + (x + 5) + "!";
Add the following code to your class:
String name = target.getName();
String type = target.getType();
String upperCaseName = Utils.methodCase(name);
targetClass.extend(%{
public #{type}# get#{upperCaseName}#()
{
return #{name}#;
}
public void set#{upperCaseName}# (#{type}# #{name}#)
{
this.#{name}# = #{name}#;
}
}%);
This adds getter and setter methods to the target class, leaving three
holes to be filled in by the name or type of the target instance variable.
package tutorial;
import ca.ubc.cs.elide.*;
import ca.ubc.cs.elide.nodes.*;
public class Property extends Transform
{
satisfies<fieldAccess>
public void makeFieldPrivate(FieldNode target)
{
target.makePrivate();
}
satisfies<methodDefinition>
public void addAccessors(FieldNode target)
{
ClassNode targetClass = target.getDeclaringClass();
String name = target.getName();
String type = target.getType();
String upperCaseName = Utils.methodCase(name);
targetClass.extend(%{
public #{type}# get#{upperCaseName}#()
{
return #{name}#;
}
public void set#{upperCaseName}# (#{type}# #{name}#)
{
this.#{name}# = #{name}#;
}
}%);
}
}
And if you now run ELIDE, you should get the following in output/ElideTest.java:
public class ElideTest extends Object
{
private int x;
private String[] y;
public int getX()
{
return x;
}
public void setX(int x)
{
this.x = x;
}
public String[] getY()
{
return y;
}
public void setY(String[] y)
{
this.y = y;
}
}
To conform to the JavaBeans spec, properties must be in a class that implements
the Serializable interface. It is a simple matter to add Serializable to the list
of interfaces implement by the target class; however, we only wish to do this once,
no matter how many properties the class has. ELIDE provides a once<> construct for
exactly this purpose. Add the following method to Property.java:
satisfies<interfaceDeclaration>
public void implementSerializable(FieldNode target)
{
ClassNode targetClass = target.getDeclaringClass();
once<targetClass, "implement serializable">
{
targetClass.add(new TypeNode("Serializable"));
}
}
The once<> keyword only executes the code within its block a single time
for the set of parameters passed to it, so for any given ClassNode, Serializable
will only be added the first time this method is called.
This tutorial doesn't implement anything like the full JavaBeans spec. A more complete
implementation is available in the examples directory, along with a number of other
useful and interesting transformations. Trying to understand those examples is an excellent
way to learn ELIDE. The JavaDoc for the nodes hierarchy in the html directory, and also contains
some small examples and tips. Finally, questions can be addressed to abryant@cs.ubc.ca
or catton@cs.ubc.ca.