Introduction to Active Annotations
Xtend 2.4 comes with a new powerful feature that allows developers to participate in the translation process of Xtend source code to Java code. The Active Annotations are useful in cases where Java requires you to write a lot of boilerplate manually. For instance, many of the good old design patterns fall into this category. With Active Annotations you no longer need to remember how the Visitor or the Observer pattern should be implemented. In Xtend 2.4 you can implement the expansion of such patterns in a library and let the compiler do the heavy lifting for you. To give you an example, lets have a look at the @Data annotation:
@Data class Address { String street String zip String city }
The @Data
annotation is part of Xtend and implements best practices for immutability for the annotated class. In particular it
- marks all fields as final,
- creates a single constructor,
- implements
hashCode()
andequals()
using the fields, and - creates a readable
toString()
implementation.
Though these idioms are often very useful, they may not always be exactly what you want. With Active Annotations you are no longer tight to the provided defaults but can roll out your own implementation which better suits your needs. Active Annotations are just library so they integrate easily with your existing development environment.
In this article we want to give you an overview of how Active Annotations work. As always this is best shown by example, so in the following we will develop our own little Observable
annotations, which generates getter and setter methods for annotated fields. The setter implementation in addition notifies any listening PropertyChangeListeners
.
Creating The Project
An Active Annotation is declared in its own project which will be processed by the compiler before the clients of the annotations are translated. This allows to use the annotation right within your development environment and implement it side by side with the client code. This clear separation allows to avoid any dependencies of the downstream project to the APIs for the annotation processor and solves a chicken-and-egg problem between the processor and the annotated elements. The annotation is usable without any further ado.
To get in the required dependencies for running the compiler tests, you should either create a project using M2E (the official Eclipse plug-in for Maven) or create an Eclipse plug-in project. M2E users can use the maven project wizard and select the archetype. You'll have to add the Xtend's maven repository (http://build.eclipse.org/common/xtend/maven/archetype-catalog.xml
), like shown in the following screenshot:
Test First
Active Annotations are executed in your IDE, whenever an annotated Xtend element is compiled. This is impressive, since whenever you change an annotation all clients get recompiled on the fly and you get instant feedback.
However, it doesn't allow you to debug your processor implementation nor to unit-test it. In order to do so you should write a test case.
The test will make use of the compiler, so we need to add a dependency to it. If you have choosen the Maven way
add the following dependency to you pom.xml
file.
<dependency> <groupId>org.eclipse.xtend</groupId> <artifactId>org.eclipse.xtend.standalone</artifactId> <version>2.4.0</version> <scope>test</scope> </dependency>
Note that we only need this dependency for testing, which is why we set the scope element to 'test'. Further more we need some test framework, we use Junit in this example.
If you choose to go with an OSGi bundle, add a require-bundle dependency in your Manifest.MF for the bundle org.eclipse.xtend.standalone
.
Let's start with a simple test case:
class ObservableTests { extension XtendCompilerTester compilerTester = XtendCompilerTester::newXtendCompilerTester(typeof(Observable)) @Test def void testObservable() { ''' import org.eclipse.example.activeannotations.Observable @Observable class Person { String name } '''.assertCompilesTo(''' import java.beans.PropertyChangeSupport; import org.eclipse.example.activeannotations.Observable; @Observable @SupressWarnings("all") public class Person { private String name; public String getName() { return this.name; } public void setName(final String name) { String _oldValue = this.name; this.name = name; _propertyChangeSupport.firePropertyChange( "name", _oldValue, name); } private PropertyChangeSupport _propertyChangeSupport = new PropertyChangeSupport(this); // method addPropertyChangeListener // method removePropertyChangeListener } ''') } }
We use the XtendCompilerTester
to validate the code which is emitted by the active annotation and the Xtend compiler. Since it is declared as an extension field it is possible to call its method assertCompilesTo()
as if they were defined on instances of java.lang.String
. This idiom makes the test case easy to read and write. Later-on you may want to explore the other methods that are provided by the compiler tester in order to directly execute the code that is produced from a given Xtend snippet.
Implementing The Active Annotation
Next up we want to fix the failing test. For that matter a new Xtend file called 'src/main/java/org/eclipse/example/activeannotations/Observable.xtend' should be created where we declare two elements: the annotation and its processor:
package org.eclipse.example.activeannotations @Active(typeof(ObservableCompilationParticipant)) annotation Observable { } class ObservableCompilationParticipant implements TransformationParticipant<MutableClassDeclaration> { override doTransform(List<? extends MutableClassDeclaration> classes, extension TransformationContext context) { // TODO add getters, setters, and property support } }
Active annotations are annotated with @Active
. Therein a single class implementing TransformationParticipant
must be referenced. We put that class right after the annotation and can now start implementing the method doTransform()
which gets called by the compiler during the translation to Java.
All annotated classes per compilation unit are passed into the processor as the first argument classes
. We simply iterate over the classes and therin over the declared fields in order to generate a getter and a setter method.
override doTransform(List<? extends MutableClassDeclaration> classes, extension TransformationContext context) { for (clazz : classes) { for (f : clazz.declaredFields) { val fieldName = f.simpleName val fieldType = f.type // add the getter method clazz.addMethod('get' + fieldName.toFirstUpper) [ returnType = fieldType body = ['''return this.«fieldName»;'''] ] // add the setter method ... } ... } }
To add a getter method we iterate the declared fields and add a method to the class. The addMethod()
operation takes two arguments, the method's name and an initializer block where we can set further properties like the return type, parameters, modifiers, etc. The method body is set with another block that is executed later when the actual Java code is generated. Here, we can generate Java code directly.
As you can see, the translation is two-fold, the coarse grained constructs such as type declarations and its members are created and modified on a structural basis. It is very similar to a mutable variation of the java.lang.reflect
API. On top of that, the finer grained elements such as statements and expressions are generated using Java's concrete syntax in plain text. Xtend's template expression helps to produce beautiful code.
The setter method is created in a similar way. We only want a setter if the field is not final, so we add a guard for that:
... for (f : clazz.declaredFields) { val fieldName = f.simpleName val fieldType = f.type // add the getter method ... // add the setter method if field is not final if (!field.isFinal) { clazz.addMethod('set' + fieldName.toFirstUpper) [ addParameter(fieldName, fieldtype) body = [''' «fieldType» _oldValue = this.«fieldName»; this.«fieldName» = «fieldName»; _propertyChangeSupport.firePropertyChange("«fieldName»", _oldValue, «fieldName»); '''] ] } ]
Many of the needed properties and methods are declared on the AST types itself, such as MutableMethodDeclaration
and the like. However some are added through extensions coming from TransformationContext
, which is the second argument to doTransform()
. The context exposes a handy API, for instance it allows to add error or warning messages and get some support for tracing as well as useful factories for creating type references. All these blend naturally into the interfaces that are provided on the AST level and allow to implement very advanced processors that validate project specific constraints or implement specific idioms.
Finally after the the getter and setter methods for the fields have been added we need to declare the field holding the listeners:
class ObservableCompilationParticipant implements TransformationParticipant<MutableClassDeclaration> { override doTransform(List<? extends MutableClassDeclaration> classes, extension TransformationContext context) { for (clazz : classes) { for (f : clazz.declaredFields) { val fieldName = f.simpleName val fieldType = f.type clazz.addMethod('get' + fieldName.toFirstUpper) [ returnType = fieldType body = ['''return this.«fieldName»;'''] ] clazz.addMethod('set' + fieldName.toFirstUpper) [ addParameter(fieldName, fieldType) body = [''' «fieldType» _oldValue = this.«fieldName»; this.«fieldName» = «fieldName»; _propertyChangeSupport.firePropertyChange("«fieldName»", _oldValue, «fieldName»); '''] ] } val changeSupportType = typeof(PropertyChangeSupport).newTypeReference clazz.addField("_propertyChangeSupport") [ type = changeSupportType initializer = ['''new «toJavaCode(changeSupportType)»(this)'''] ] // add method 'addPropertyChangeListener' // add method 'removePropertyChangeListener' } } }
Now you only have to add two additional methods for adding and removing listeners. We leave this as an excercise for the reader. You might also want to check if a getter or setter method has been explicitly implemented already. Users should be able to change the getter or setter semantic by explicitly implementing one or the other.
Wrap Up
Active Annotations are a powerful new concept which lets you solve a large class of problems that previously had to be solved in cumbersome ways, e.g. by IDE wizards, with code generation or simply by manually writing boilerplate code. It basically is a means of code generation, but its simple integration with your existing project structure and the fast development turnarounds diminish the typical downsides of code generation. Note, that in version 2.4 the Active Annotation-API is provisional, and might be changed in later releases.