Eliminate Boilerplate Code with the PICA Technique for Java

eflection, dynamic proxies, and annotations are far from new capabilities in Java, having been introduced in JDK 1.1, 1.3, and 1.5, respectively. Lately, though, I’ve witnessed a spectacular combination of these features in a number of different projects. The combination is quite innovative, so much so that I was surprised no one had coined a name for it. So, I decided to call it the Proxied Interfaces Configured with Annotations (PICA) technique, which this article describes.

Introduction to the PICA Technique

The gist of the PICA technique is:

  1. Create an interface and mark some combination of the interface itself, its methods, and its methods’ parameters with annotations that serve to configure specific behaviors you want the methods to exhibit.
  2. Create an InvocationHandler that uses data from the annotations to direct the behaviors that should occur when the interface’s methods are invoked.
  3. Using java.lang.reflect.Proxy.newProxyInstance(), create an instance of a dynamic proxy that implements the interface and uses the invocation handler.

As an example of the PICA technique in action, look at JewelCli, one of a number of Java command-line parsing libraries. Many of these libraries ask their users to configure the command-line switches that should be recognized and parse the command line via the typical imperative programming model—lots of verbose calls to methods on Java APIs. For example, here is a usage scenario for Commons CLI:

CommandLineParser parser = new PosixParser();Options options = new Options();options.addOption("a", "all", false,    "do not hide entries starting with .");options.addOption("A", "almost-all", false,    "do not list implied . and .." );options.addOption("b", "escape", false,    "print octal escapes for nongraphic characters");options.addOption(OptionBuilder.withLongOpt( "block-size" )    .withDescription( "use SIZE-byte blocks" )    .hasArg()    .withArgName("SIZE")    .create());options.addOption("B", "ignore-backups", false,    "do not list implied entries ending with ~");options.addOption("c", false,    "with -lt: sort by, and show, ctime (time of last modification " +    "of file status information) with -l:show ctime and sort by name " +    "otherwise: sort by ctime" );options.addOption("C", false, "list entries by columns");String[] args = { "--block-size=10" };try {    CommandLine line = parser.parse(options, args);    if (line.hasOption("block-size")) {        System.out.println(line.getOptionValue("block-size"));    }}catch (ParseException exp) {    System.out.println("Unexpected exception:" + exp.getMessage());}

JewelCli adopts a more declarative approach. First, the programmer creates a Java interface, a PICA, as shown below:

package uk.co.flamingpenguin.jewel.cli.examples;import java.io.File;import java.util.List;import uk.co.flamingpenguin.jewel.cli.CommandLineInterface;import uk.co.flamingpenguin.jewel.cli.Option;import uk.co.flamingpenguin.jewel.cli.Unparsed;@CommandLineInterface(application="rm")public interface RmExample{   @Option(shortName="d", longName="directory",       description="unlink FILE, even if it is a " +       "non-empty directory (super-user only)")   boolean isRemoveNonEmptyDirectory();   @Option(shortName="f", description=      "ignore nonexistent files, never prompt")   boolean isForce();   @Option(shortName="i", description="prompt before any removal")   boolean isInteractive();   @Option(shortName={"r", "R"}, description=      "remove the contents of directories recursively")   boolean isRecursive();   @Option(shortName="v", description=      "explain what is being done")   boolean isVerbose();   @Option(description=      "display this help and exit")   boolean isHelp();   @Option(description=      "output version information and exit")   boolean isVersion();   @Unparsed(name="FILE")   List getFiles();}
Author’s Note: The preceding code example was borrowed from the JewelCli site.

Notice how each annotation on RmExample contributes to the desired behavior. The @CommandLineInterface annotation provides an application name used to generate a help screen for the command-line parser. The @Option annotation marks methods that, when invoked, access parsed options as described by the shortName and longName attributes, and specifies a description of the option to be used in command-line help. Such options are to be converted into instances of the method’s return type. Finally, @Unparsed marks a method that, when invoked, returns any non-option arguments parsed from a command line.

Next, the programmer supplies a CliFactory with the interface’s class object and the arguments to parse:

RmExample parsed = CliFactory.parseArguments(   RmExample.class, "-v", "-rf", "/tmp/*", "./tmp");

The parseArguments() method handles parsing the command-line switches and their arguments (if any), and arranges for them to be accessed via the returned instance of the given interface. Behind the scenes, CliFactory creates a dynamic proxy that implements RmExample and provides it with an InvocationHandler, which intercepts calls to the interface’s methods and responds to them by returning the results of options that correspond to the methods.

The volume of client code required to parse the arguments and retrieve their type-converted values is very small indeed. CliFactory does the grunt work of processing a PICA and argument list behind the scenes. All the caller must do is provide a PICA type. Even a quick glance at the PICA class shows that it’s easy to see all the supported command-line switches, their intended types, and how they contribute to a help screen, without having to wade through lots of clunky API calls.

How PICA Works

How does all this magic work? For answers, here’s an in-depth examination of a utility that uses a PICA. You can download the source code to follow along.This utility takes its inspiration from a blog post that describes the beginnings of a PICA-style factory for typed access to entries in properties files. The idea is to treat the contents of a Java properties file as though it were an instance of a PICA. The methods of the PICA correspond to keys of the properties file and coerce the values corresponding to those keys to the specified return types.

Here’s an example of a property-bound PICA:

public interface ApplicationConfig {    @BoundProperty( "request.recipient" )    @DefaultsTo( "http://example.com" )    URL requestRecipient();    @DefaultsTo( "30000" )    long timeoutInMilliseconds();    @BoundProperty( "search.paths" )    @ValuesSeparatedBy( "\s*,\s*" )    List searchPaths();}

You can probably imagine the code you’d usually have to write to work with these properties: spin up a Properties object, possibly load() it off the classpath from an InputStream obtained via Class.getResourceAsStream(), then remember what keys you want, and convert the values to the types you want by hand:

InputStream propertyIn = getClass().getResourceAsStream(   "/app-config.properties" );Properties properties = new Properties();properties.load( propertyIn );propertyIn.close();URL requestRecipient = new URL( properties.getProperty(   "request.recipient") );long timeoutInMilliseconds = Long.valueOf(   properties.getProperty("timeoutInMilliseconds") );String[] pieces = properties.getProperty(   "search.paths").split( "\s*,\s*" );List searchPaths = new ArrayList();for (String each : pieces)   searchPaths.add(new File(each));

With this utility, all you’d have to do is:

    ApplicationConfig config =       PropertyBinder.forType( ApplicationConfig.class ).bind( ... );    URL requestRecipient = config.requestRecipient();    long timeoutInMilliseconds = config.timeoutInMilliseconds();    List searchPaths = config.searchPaths();

The argument to bind() can be an InputStream, a File, or a Properties object.

By calling methods on config, you can get the values that correspond either to the method’s name, or to the keys named in the methods’ @BoundProperty markers, or the values given by @DefaultsTo markers if the key is not present in the bound properties file. Property methods that return arrays or Lists can also specify a regular expression via @ValuesSeparatedBy that serves to separate multiple values in the property’s value—if not so marked, the methods will use a single comma as the separator, with no surrounding whitespace.

You can see that clients of PropertyBinder have a much more pleasant experience than they would without it.

Taking a deeper dive, requesting a PICA instance via PropertyBinder.forType() delegates directly to a SchemaValidator. The SchemaValidator is responsible for ensuring that the markings and types on the PICA type are logically consistent. When it is satisfied, the SchemaValidator returns a ValidatedSchema, which holds the various discoveries the validator made while validating the PICA type—default values, value separators, and property keys.

Every method declared on the PICA corresponds to a property from a properties file. If the method is marked with @BoundProperty, the annotation value is used as the property key. Otherwise, it uses the fully qualified name of the PICA, plus a period, plus the name of the method.

SchemaValidator makes the following assertions about a given PICA type:

  • The type is an interface. If it weren’t, you wouldn’t be able to create a dynamic proxy for the PICA.
  • The type has no superinterfaces. If it did, you might have to handle methods other than bound property methods in the dynamic proxy’s InvocationHandler. Certainly you could check that all methods in the PICA’s superinterface hierarchy conform to the SchemaValidator constraints also, but it seems far-fetched to support inheritance of property-bound PICAs.
  • For every method declared on the PICA, assert that:
    • Aggregate return types for the methods are restricted to arrays and Lists.
    • If the method is marked with @ValuesSeparatedBy, the return type must be an aggregate type, and the separator the annotation specifies must be a legal regular expression.
    • If the method returns a scalar value, the return type must be any of the primitives or primitive wrappers, or any reference type which has either:
      • A public static method called valueOf() which takes one argument, of type String, and whose return type is the type itself
      • A public constructor which takes one argument, of type String
      • If the reference type has both the valueOf() method and the constructor, the valueOf() method will be used to convert String representations of the corresponding property value to the desired type. Aggregate return types must have a component type (for arrays) or generic type (for Lists) that meets the constraints for scalars as discussed above. List-returning methods can be declared as raw List, List, or List. In the first two cases, the underlying elements are Strings.
      • If a method is marked with @DefaultsTo, the value of that annotation must be convertible via the above strategies to the return type of the method.

After the SchemaValidator is satisfied with the PICA type, it returns a ValidatedSchema to PropertyBinder.forType() and the new PropertyBinder retains it. Then, when the PropertyBinder is asked to bind() to properties from a particular source, it turns to the ValidatedSchema to work the dynamic proxy magic. ValidatedSchema’s createTypedProxyFor() method creates a new dynamic proxy that implements the PICA interface, and whose InvocationHandler is a PropertyBinderInvocationHandler which is primed with the properties to bind to, and the ValidatedSchema. The proxy is then returned by bind() for the caller to use.

Now, when a method declared on the PICA is called on the PICA proxy, the PropertyBinderInvocationHandler asks the ValidatedSchema to convert() the value of the property named by the method’s @BoundProperty marker into the appropriate type. This value could be:

  • The value associated with the property key
  • The default value specified by @DefaultsTo
  • A “nil” value (null for scalars, a zero-length array for arrays, an empty list for Lists)

Further Exploration

I encourage you to take a long look at SchemaValidator and the work that it does. As you might imagine, it makes heavy use of Reflection to discover characteristics of the methods on the PICA and of the PICA annotations.

Also look at the declarations of the annotation interfaces. Here’s BoundProperty in a nutshell:

@Target( METHOD )@Retention( RUNTIME )public @interface BoundProperty {    String value();}

For the PICA technique to work, the annotation interfaces must be marked with the meta-annotation @Retention with a value of RetentionPolicy.RUNTIME. Otherwise, the annotations will not be available in the class files at runtime. Each of these interfaces is marked also with the meta-annotation @Target, with a value of ElementType.METHOD, to indicate that the annotations can be applied only to methods. Some PICA implementations use annotations that can be applied to other program elements, such as types and method parameters.

Recall that an InvocationHandler for a dynamic proxy is called upon to respond not just to the methods on the interfaces the proxy implements, but also the java.lang.Object methods equals(), hashCode(), and toString(). Therefore, the PropertyBinderInvocationHandler’s invoke() method first checks to see whether the method it was asked to handle was declared on java.lang.Object; if so, it then attempts to respond sensibly to equals() (uses identity-based equality on the proxy), hashCode() (gives the result of System.identityHashCode() on the proxy), or toString() (gives the name of the PICA interface plus the toString() value of all the bound properties).

You might be wondering how this utility is able to support PICA methods whose return type is a generic List, e.g. List. Aren’t generic types erased at compile time? They are for instances—at runtime I cannot reflect on a List instance and know that its compile time type was List. However, if you reflect on a Method whose return type is a generic type (or whose parameters are generic types), you can discover the properties of those declarations using Method.getGenericReturnType() and Method.getGenericParameterTypes(). These return instances of objects that implement Type. When the type is a generic type, the Type instances will be ParameterizedTypes. You can then ask the ParameterizedTypes object what its type arguments represent—specific classes, type variables, bounds, etc. Look at ListValueConverter to see how it uses these Reflection capabilities to deduce the element type of the Lists it is asked to create.

I hope you have as much fun digging into the details of the PICA technique as I did. Perhaps you will find novel ways to use PICA to eliminate repetitive, boilerplate imperative code in your project.

References:

Share the Post:
Share on facebook
Share on twitter
Share on linkedin

Related Posts