Active annotations use cases
Active Annotations is a language feature of Xtend to
- add domain specific invariants (custom validations)
- apply design patterns / programming idioms ( to avoid boilerplate code)
- handle cross cutting concerns (AOP)
- derive / synchronize other resources
So active annotations are an addition and in some cases even an alternative to the classic approach of defining domain specific languages and writing code generators for these DSLs.
This is especially true when your DSL tend to evolve to a full blown programming language with some domain specific customizations. In such a case you should consider reusing a general purpose language (GPL) like Xtend and customize it with active annotations to your needs. So you avoid the overhead of reimplementing a complete IDE infrastructure for a DSL.
So the active annotations mechanism isn’t a simple code generator (although you can use it in that way, too) but a transformation working on the Java model AST where you can add new fields and methods. After editor save these members are then immediately visible (in scoping, type computation and content assistance) when further editing the Xtend file.
In this blog post I want present you two use cases how active annotations eases programming.
In programming you should try to avoid situation where you have to keep things in sync manually. One such common situation is the handling of message bundles. It is always a good idea to extract and centralize messages so there is one place to adapt them or even add internationalized messages. These message strings may contain wildcards that can be bound from outside. In Eclipse OSGi there is already an abstract class, NLS, in place that enables handling of message bundles. Despite of that it is still up to the programmer to keep the keys in the message bundle manually in sync with the static String constants in the Java class.
Sven already blogged about how to externalize strings to a properties file and even derive methods to bind type safe the wild card parameters. I want to show you the other way round:
message.properties:
INVALID_TYPE_NAME=Entity name
{0} should start with a capital.INVALID_FEATURE_NAME=Feature name {0} in {1}
should start with a lowercase.EMPTY_PACKAGE_NAME=Package name cannot be empty.INVALID_PACKAGE_NAME=Invalid package name {0}.MISSING_TYPE=Missing {0} type {1}.
IssueCodes.xtend:
import de.abg.jreichert.activeanno.nls.NLS
@NLS(propertyFileName="messages")
class IssueCodes {
}
DomainmodelJavaValidator.java:
public class DomainmodelJavaValidator extends XbaseJavaValidator {
@Check
public void checkTypeNameStartsWithCapital(Entity entity) {
if(!Character.isUpperCase(entity.getName().charAt(0))) {
warning(
IssueCodes.getMessageForINVALID_TYPE_NAME(entity.getName()),
DomainmodelPackage.Literals.ABSTRACT_ELEMENT__NAME,
ValidationMessageAcceptor.INSIGNIFICANT_INDEX,
IssueCodes.INVALID_TYPE_NAME,
entity.getName()
);
}
}
...
}
You see that for each key in messages.properties a static String constant of same name is created. Moreover a method prefixed with getMessageFor[KEY_NAME] is derived for each key taking exactly so many parameters as place holders appearing in the message for this key in messages.properties.
The complete example can be found here, including the active annotation processor.
So every time you change messages.properties, code referencing then non existing keys or passing an invalid count of parameters will get error markers in IDE.
As properties file changes usually doesn’t trigger the Java builder there is an ANT builder added to the project that touches IssueCodes.xtend when messages.properties has been changed.
Some details about the implementation of the NLS active annotation:
- The plug-in using this annotation have to have
org.eclipse.osgi.util.NLS
on its classpath, this is checked by the call tofindTypeGlobally
: if the class cannot be resolved an error marker is created at@NLS
- By navigating over
annotatedClass.compilationUnit.filePath
you have access to the file path where the class resides that is annotated with @NLS. So the properties file can be accessed. - If there is no properties file with the name used for the annotation property
propertyFileName
or the properties file cannot be loaded appropriate error markers will be created. - As active annotations and Xtend itself doesn’t currently support a static initializer block, this is emulated by a static field. A function containing the initialization logic is called and assigned to this field.
- Before creating new fields or methods it is checked if there is already a
member with same name in place. In this case an error marker will be produced.
This is currently not very elaborated as it doesn’t take method overloading in
consideration, but for the
NLS
annotation the parameter count check is enough. - Via a regular expression the count of wildcards in a message is calculated and
exactly this number of Object parameters are then added to the
getMessageFor
method.
Since Xtend 2.5 it is possible to write
initializer = '''
new «Function0»<«String»>() {
public «string» apply() {
«NLS /* this is the class literal of org.eclipse.osgi.util.NLS */».initializeMessages(«annotatedClass.findDeclaredField(BUNDLE_NAME_FIELD).simpleName»,
«annotatedClass».class
);
return "";
}
}.apply();
'''
All class literals inside the rich string assigned to the initializer are now wrapped with toJava automatically. Compare this with the old notation used in the code on GitHub. This is much more readable now.
In the second example I want to handle the problem of building type safe SQL queries. Hibernate ORM defines a fluent API to create criteria queries, a programmatic, type-safe way to express a database query. It depends on a static meta model that enables static access to the meta data of the entities contained in the domain model. This meta model have to be generated by the JPA Static Metamodel Generator, a annotation processor. It requires a class defining volatile static fields corresponding to the attributes of the entity as input.
An alternative approach is JOOQ, but here you also have an extra generation step.
The third approach, Sculptor, is a generator framework to describe 3-tier enterprise applications following the domain driven design approach. It uses Xtext based DSLs to define entities, repositories, services and front end. Out of the DSL artifacts code for well established frameworks like JPA, Hibernate, Spring and Java EE is generated. Sculptor itself also ships with some useful static framework classes then called by the generated code. Similar to the before mentioned JPA Static Metamodel Generator Sculptor generates attribute accessor classes for every entity defined in the DSL to be used for a self defined critera query builder.
Wouldn’t it be nice to see immediately which queries break when you change your domain model?
In the following the static framework classes of Sculptor will be reused. But instead using the Sculptor DSL and generator these classes are combined with active annotations. Find the complete example here and in particular the the active annotation processing class.
So with annotating a class with @Entity
generates an id field and derives
the classes later to use when creating type safe database queries. With
annotating a class only with @EntityLiteral
will leave off adding the id field.
The @Property annotation will add getter and setter method for the annotated
field.
Having the domain model for a P2 repository structure (see Database.xtend for the complete entity model)
Location <>— * Unit <>— * Version
and each entity is annotated with @Entity
the following typed queries are
now possible (copied from LocationManager.xtend):
def Set getLocationURLsContainingUnitWithVersion(String unit, String version) {
val urls = newHashSet
val session = SessionManager::currentSession
val unitFindByCondition = new CustomJpaHibFindByConditionAccessImpl(Unit, session)
var unitCriteriaRoot = ConditionalCriteriaBuilder.criteriaFor(Unit)
unitCriteriaRoot = unitCriteriaRoot.withProperty(UnitLiterals.name()).eq(unit).and().withProperty(UnitLiterals.versions().name()).eq(version)
unitFindByCondition.addCondition(unitCriteriaRoot.buildSingle())
unitFindByCondition.performExecute
val unitIds = unitFindByCondition.getResult().map[id]
val locationFindByCondition = new CustomJpaHibFindByConditionAccessImpl(Location, session)
var locationCriteriaRoot = ConditionalCriteriaBuilder.criteriaFor(Location)
locationCriteriaRoot = locationCriteriaRoot.withProperty(
LocationLiterals.units().id()).in(unitIds)
locationFindByCondition.addCondition(locationCriteriaRoot.buildSingle())
locationFindByCondition.performExecute
val result = toLocationList(locationFindByCondition.getResult())
result.forEach[urls.add(url)]
urls
}
The method above will return the URLs of those locations that contain a unit with the given name and version.
The nice thing about the active annotation here is that if you rename
version’s attribute name to id and save this change will immediately produce an
error marker in LocationManager.xtend as now the access .name()
in the query
isn’t valid anymore.
Some implementation details about the active annotation here as well:
- the
EntityProcessor
callsEntityLiteralProcessor
(to create the literal classes) andPropertyProcessor
(to create getter and setter for the here create id field), so you see, it is possible to chain active annotation processors - all additionally created Java classes during active annotation processing have to be
registered globally in method
doRegisterGlobals
- the
EntityLiteralProcessor
checks fields for having the@Property
annotation – only for those fields corresponding methods in the literal classes are created - currently only constants can be used as values for annotation properties (both in active annotations as well as when creating new annotations during active annotation processing)
- AnnotationExtensions provides some common methods e.g. used to find existing annotations either by name or by name and property value
Besides the both use cases described above there are several other examples of how to use the power of active annotations:
- Value objects
- Logging
- Caching
- Micro benchmarking
- Extract interface
- Interceptor
- Concurrency
- Builder pattern
- Visitor pattern
- I18n – Externalizing strings
- Java from JSON
- ElasticSearch
- REST Server API
- GWT Programming
- JavaFX Properties /XtendFX
- Xtendroid
- and many more
I hope I was able to give you a good impression, what you can achieve with active annotations. If you want to start writing your own active annotation processors have a look at the official documentation and at this best practices guide as well. Also don’t hesitate to ask questions in the Xtend forum and filling feature requests or bugs here.