Introduction
Jedi tries tries to take the hard work out of using its APIs. In Java, one of the burdens of the more functional style promoted by Jedi is the syntactic clutter of anonymous classes for closures. Jedi helps with this by providing annotations which generate factory classes. These provide methods for creating closures which can be used with the Jedi API to invoke your code appropriately.
There are two types of annotations: those starting with @Jedi and those starting with @Sith. The two sets have correspondences but the thing to keep in mind is that @Jedi annotations allow a type to declare services available to others, whereas @Sith annotations declare that another type is required to be manipulated into providing services. @Jedi annotations should be used when you control the source code of the type for which you want to produce closures. @Sith annotations should be used (sparingly) when you do not control the relevant source code. To obtain the classes described below for any particular type, either @Jedi or @Sith annotations should be used. The annotations applying to a particular type must all be @Jedi or all @Sith. Mixing the two will cause unpredicatable results, e.g. galactic war.
Factory Classes
Three factory classes will be created for each annotated class. The factory classes are meant to support different coding and testing styles. Suppose the class containing the annotations is called Stormtrooper. The following three classes will be created:
| IStormtrooperClosureFactory | this interface has declarations for all of the appropriate factory methods. Each factory method will take parameters appropriate to the annotated method that generated it and will return an instance of an appropriate closure (see below). This is ideal for dependency injection and unit testing with mocks. |
| StormtrooperClosureFactory | this class implements IStormtrooperClosureFactory. Each factory method will create and return an instance of the appropriate closure (see below). This can be used as a default implementation of IStormTrooperClosureFactory in your production code. Exceptions thrown by the annotated method will be transformed into RuntimeExceptions. |
| StormtrooperStaticClosureFactory | this class has static factory methods and is ideally used with static imports (for brevity) in the client code. The factory methods have the same signature as those declared in IStormtrooperClosureFactory. Internally, each method delegates to an instance of IStormtrooperClosureFactory. This instance can be set using the setDelegate(IStormtrooperClosureFactory) method if you want to replace the default (during testing maybe). The default delegate can be restored with the useDefaultDelegate() method. |
These factories will be created in the same package as the type to which they apply. For @Jedi annotations this will be the type in which the annotation is placed. In the case of @Sith annotations, this will be the type which is specified in the anotation. Which directory they appear in depends on the settings you choose during compilation.
@Jedi Annotations
This is what the @Jedi annotation declarations look like (@JediFilter and @JediFunctor look the same with the obvious change in name):
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface JediCommand { String name() default ""; JediCut[] cut() default {}; }
The table below contains a set of examples. The first column shows a fragment of source code from a Stormtrooper class with one or more annotations, and the second shows the generated factory method signatures of the IStormtrooperClosureFactory. @JediFilter and @JediFunctor will have the same kind os effects, except that methods thus annotated must have a non-void return type, and @JediFilter behaves differently if the method it is annotating returns a boolean / Boolean as opposed to any other type (see the examples at the bottom of the table).
The Simplest Annotation
@JediCommand
public void attack() {... }
creates:
// The returned Command will invoke attack() // on any Stormtrooper with which it is executed Command <Stormtrooper> attackCommand();
Rename The Factory Method
@JediCommand(name = "pummelMercilessly") public void attack() {... }
creates:
// The returned Command will invoke attack() // on any Stormtrooper with which it is executed Command <Stormtrooper> pummelMercilesslyCommand();
Parameters
@JediCommand
public void attack(Target target) {... }
creates:
// The returned Command will invoke attack(target) // on any Stormtrooper with which it is executed Command <Stormtrooper> attackCommand(Target target); // When executed with an instance of Target, the returned // Command will invoke attack(target) on $receiver, // which must not be null Command <Target> attackProxyCommand(Stormtrooper $receiver);
More Parameters
@JediCommand
public void attack(Target target, Weapon weapon) {... }
creates:
// The returned Command will invoke attack(target, weapon) // on any Stormtrooper with which it is executed Command <Stormtrooper> attackCommand(Target target, Weapon weapon); // When executed with instances of Target and Weapon, the returned // Command2 will invoke attack(target, weapon) on $receiver, which // must not be null Command2 <Target, Weapon> attackProxyCommand2(Stormtrooper $receiver);
Cuts (see below)
@JediCommand(cut = { @JediCut(parameter = { "weapon"})})
public void attack(Target target, Weapon weapon) {... }
creates:
// The returned Command2 will invoke attack(target, weapon) // when it is executed with a Stormtrooper and a Weapon Command2 <Stormtrooper, Target> attackCommand2(Target target); // When executed with an instance of Target, the returned // Command will invoke attack(target, weapon) on $receiver, which // must not be null Command <Target> attackProxyCommand(Stormtrooper $receiver, Weapon Weapon);
@JediFilter On a Method with a Boolean Return Type
@JediFilter public boolean isAttacking() {... }
creates:
// The returned Filter will invoke isAttacking() // when it is executed with a Stormtrooper and // pass back the return value Filter <Stormtrooper> isAttackingFilter();
@JediFilter on a Method with a Non-Boolean Return Type
@JediFilter public String getDesignation() {... }
creates:
// The returned Filter will invoke getDesignation() when it // is executed with a Stormtrooper and compare the returned // designation with the given $testValue, which must not // be null Filter <Stormtrooper> getDesignationEqualsFilter(String $testValue); // The returned Filter will invoke getDesignation() when it // is executed with a Stormtrooper and determine if the returned // designation is in the given $testValue collection, which must // not be null Filter <Stormtrooper> getDesignationMembershipFilter(Collection<String> $testValue);
Cuts
public @interface JediCut { String name() default ""; String[] parameters() default {}; }
'Cuts' are a way of moving parameters between a closure's factory method parameter list and its execute method parameter list. The parameters move in different directions depending on whether the factory method is a Proxy type or not. Using the examples in the table above is the easiest way of describing their action. Look at the rows entitled 'More Parameters' and 'Cuts'. Both rows shows an annotated method with the same two parameters. When the factory classes are generated, two factory methods are created for this method in each case.
The first, attackCommand(Target, Weapon) takes the annotated method's parameters as factory method parameters. Every Stormtrooper which is passed to the execute method of the command will have their attack(Target, Weapon) method called with the same Target and Weapon . i.e. this is un underfunded army so they have to share weapons.
By contrast, in the 'Cuts' version, the weapon is cut. This has the effect of 'cutting' the weapon out of the parameter list that it would have been in (the factory method's list) and placing it in the only other available place which is the closure's execute method list. This means that the command now has two parameters, the Stormtrooper and the Weapon. i.e. the target has been chosen, but choice of Stormtroopers and their corresponding Weapons has been deferred.
In the Proxy versions of the factory methods, the situation is reversed. In the uncut version, the only thing determined by the factory method is the Stormtrooper which is going to be attacking. The command's execute method takes the Target and Weapon.
In the cut version, the weapon is hoisted out of the execute method's parameter list and placed into the factory method's parameter list. This means that the decisions has been made about which stormtrooper and which weapon to use, only the target remains to be chosen when the command is executed.
Renaming Cuts
As indicated by the @JediCommand declaration above, it is possible to specify a list of cuts for a method. Each of these will cause at least one factory method to be created. It is possible that this will cause factory method signature clashes, which the compiler will moan about. If this occurs, you can rename a cut, in a way similar to that shown in the table above; all that is required is to specify a name inside the @JediCut parameter list. This will override any name specified in the enclosing @Jedi annotation.
@Sith Annotations
@Sith annotations all start with the word 'Sith'. They can annotate methods, types or fields and require two parameters. They can be used when you do not control the source code of the classes for which you want to produce the factory types. Each @Jedi annotation has a corresponding @Sith annotation (i.e. @SithCommand, @SithFilter, @SithFunctor). Cuts and renames are not available for @Sith annotations.
The parameters required by all three @Sith annotations are the same:
- type - the Java type which you want to manipulate.
- methods - a list of method signatures for which you want to produce the closure type. Each signature is specified using another annotation, @SithMethod.
For example, the following annotation on any type, method or field in your code base will produce closure factories for java.util.Set.
@SithFilter(
type = Set.class,
methods = {
@SithMethod( name = "contains", parameterTypes = {Object.class})
}
)
@Sith annotations for a particular type are aggregated over the entire codebase and duplicates are eliminated. i.e. you can place @Sith annotations for the same type in many different source files and they will all be combined into one set of closure factories in the same package as the given type.
Destination Packages for @SithXXX Annotations
Jedi generally attempts to put generated factories into the same package as the type which the annotation is talking about. This works except when @SithXXX annotations refer to a type in the java.lang package; the annotation processor will not let Jedi write classes into the java.lang package. To circumvent this, any @SithXXX annotation that specifies a type whose package name begins with 'java.' will have the 'java' part of the package name replaced with 'sith', i.e. the closure factories for java.lang.String will be placed into the sith.lang package and the closure factories for java.util.Collection with be placed into the sith.util package.
@SithMethods Annotation
It appears that a particular annotation can only be used once to annotate a particular Java element. So, for example, if a method requires two @SithFilters the annotations can not both be placed on the method declaration. To get around this problem, the @SithMethods annotation has been introduced:
public @interface SithMethods { SithCommand[] commands() default {}; SithFilter[] filters() default {}; SithFunctor[] functors() default {}; }
Now, the method declaration would look like this:
@SithMethods(
filters = {
@SithFilter(type = String.class, methods = @SithMethod(name = "length")),
@SithFilter(type = Collection.class, methods = @SithMethod(name = "size"))
}
)
public void foo() {
...
}
Closure Equality
It can sometimes be useful to compare closures created by the generated factories for equality (unit testing, for example). This works perfectly well; each closure produced by a particular factory method is equal to every other closure produced by the same factory method, providing that the factory methods parameters are also equal according to the equals(Object) method (or == for primitives).
hashCode() will also yield consistent results, the calculation depending only on the hashCodes of the parameters.


