RSS Feed
Download our iPhone app
Browse DevX
Sign up for e-mail newsletters from DevX


Eliminate Boilerplate Code with the PICA Technique for Java : Page 2

Combining reflection, dynamic proxies, and annotations proves to be such a powerful technique that it deserves its own name.


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<File> 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 );
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<File> searchPaths = new ArrayList<File>();
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<File> 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<the scalar type>. 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)

Close Icon
Thanks for your registration, follow us on our social networks to keep up-to-date