Sunday, 17 November 2013

Java Collection Literals

The blog post I've had by far the most feedback on is Java Multiline String. It seems to have found a niche with a few programmers for quickly defining formatted SQL and XML for unit testing.

After reading Steve Yegge's post which covered the lack of Java syntax for defining literal data objects, I thought the multiline string code could be extended to provide this kind of syntax for Java Collections.

The code below, like the multiline string code, uses Javadoc comments, annotations and annotation processors to generate code at compile time to initialise and populate a collection.

Collection fields are annotated with an annotation which specifies which annotation processor to use. The annotation processor uses the fields Javadoc comment, which contains a JSON string, to generate additional code to populate the field.

For example, the annotation used to initialise a List field as an ArrayList:

ArrayList.java

package org.adrianwalker.collectionliterals;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.SOURCE)
public @interface ArrayList {
}

The code generation is handled using an annotation processor for each annotation, with common code being inherited from an abstract class:

AbstractCollectionProcessor.java

package org.adrianwalker.collectionliterals;

import com.sun.source.tree.ClassTree;
import com.sun.source.util.Trees;
import com.sun.tools.javac.code.Flags;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.model.JavacElements;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import com.sun.tools.javac.util.Names;
import java.util.Collection;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import org.codehaus.jackson.map.ObjectMapper;

public abstract class AbstractCollectionProcessor<C> extends AbstractProcessor {

  private static final String THIS = "this";
  private JavacElements elementUtils;
  private TreeMaker maker;
  private Trees trees;
  private Names names;

  @Override
  public void init(final ProcessingEnvironment procEnv) {
    super.init(procEnv);
    JavacProcessingEnvironment javacProcessingEnv = (JavacProcessingEnvironment) procEnv;
    Context context = javacProcessingEnv.getContext();
    this.elementUtils = javacProcessingEnv.getElementUtils();
    this.maker = TreeMaker.instance(context);
    this.trees = Trees.instance(procEnv);
    this.names = Names.instance(context);
  }

  public JavacElements getElementUtils() {
    return elementUtils;
  }

  public TreeMaker getMaker() {
    return maker;
  }

  public Trees getTrees() {
    return trees;
  }

  public Names getNames() {
    return names;
  }

  @Override
  public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {

    Set<? extends Element> fields = roundEnv.getElementsAnnotatedWith(getAnnotationClass());

    for (Element field : fields) {
      String docComment = elementUtils.getDocComment(field);

      if (null == docComment) {
        continue;
      }

      processField(field, parseJson(docComment));
    }

    return true;
  }

  private C parseJson(final String json) throws ProcessorException {

    ObjectMapper mapper = new ObjectMapper();

    C collection;
    try {
      collection = (C) mapper.readValue(json, getCollectionClass());
    } catch (final Throwable t) {
      throw new ProcessorException(t);
    }

    return collection;
  }

  private void processField(final Element field, final C collection) {

    JCTree fieldTree = elementUtils.getTree(field);
    JCTree.JCVariableDecl fieldVariable = (JCTree.JCVariableDecl) fieldTree;
    Symbol.VarSymbol fieldSym = fieldVariable.sym;
    boolean isStatic = fieldSym.isStatic();
    JCTree.JCExpression fieldEx;

    if (!isStatic) {
      fieldEx = maker.Ident(names.fromString(THIS));
      fieldEx = maker.Select(fieldEx, fieldSym.name);
    } else {
      fieldEx = maker.QualIdent(fieldSym);
    }

    Symbol.ClassSymbol collectionClass = elementUtils.getTypeElement(getCollectionClassName());
    JCTree.JCExpression collectionEx = maker.QualIdent(collectionClass);

    JCTree.JCNewClass newCollection = maker.NewClass(
            null,
            List.<JCTree.JCExpression>nil(),
            collectionEx,
            List.<JCTree.JCExpression>nil(), null);

    JCTree.JCAssign assign = maker.Assign(fieldEx, newCollection);
    JCTree.JCStatement assignStatement = maker.Exec(assign);

    JCExpression collectionMethodEx = maker.Select(fieldEx, names.fromString(getCollectionMethodName()));

    ListBuffer blockBuffer = new ListBuffer();
    blockBuffer.add(assignStatement);
    blockBuffer.appendList(processCollection(collection, collectionMethodEx));

    long flags = Flags.BLOCK;
    if (isStatic) {
      flags |= Flags.STATIC;
    }

    JCTree.JCBlock block = maker.Block(flags, blockBuffer.toList());

    TypeElement type = (TypeElement) field.getEnclosingElement();
    ClassTree cls = trees.getTree(type);

    JCTree.JCClassDecl cd = (JCTree.JCClassDecl) cls;

    cd.defs = cd.defs.append(block);
  }

  public List processCollection(final C collection, final JCTree.JCExpression collectionMethodEx) {
    ListBuffer collectionMethodBuffer = new ListBuffer();

    for (Object element : (Collection) collection) {

      ListBuffer addBuffer = new ListBuffer();
      addBuffer.add(getMaker().Literal(element));

      JCMethodInvocation addInv = getMaker().Apply(
              List.<JCTree.JCExpression>nil(),
              collectionMethodEx,
              addBuffer.toList());
      JCTree.JCStatement addStatement = getMaker().Exec(addInv);

      collectionMethodBuffer.add(addStatement);
    }

    return collectionMethodBuffer.toList();
  }

  public abstract Class getAnnotationClass();

  public abstract String getCollectionClassName();

  public abstract Class getCollectionClass();

  public abstract String getCollectionMethodName();
}

This class can be extended to create a concrete implementation, for an ArrayList field:

ArrayListProcessor.java

package org.adrianwalker.collectionliterals;

import java.util.ArrayList;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;

@SupportedAnnotationTypes({"org.adrianwalker.collectionliterals.ArrayList"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class ArrayListProcessor extends AbstractCollectionProcessor<ArrayList<Object>> {

  @Override
  public Class getAnnotationClass() {
    return org.adrianwalker.collectionliterals.ArrayList.class;
  }

  @Override
  public Class getCollectionClass() {
    return java.util.ArrayList.class;
  }

  @Override
  public String getCollectionClassName() {
    return getCollectionClass().getName();
  }

  @Override
  public String getCollectionMethodName() {
    return "add";
  }
}

Because Java Maps don't implement the Collection interface, the processor for a HashMap field is a bit more complicated:

HashMapProcessor.java

package org.adrianwalker.collectionliterals;

import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCMethodInvocation;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import java.util.HashMap;
import java.util.Map.Entry;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;

@SupportedAnnotationTypes({"org.adrianwalker.collectionliterals.HashMap"})
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class HashMapProcessor extends AbstractCollectionProcessor<HashMap<String, Object>> {

  @Override
  public List processCollection(final HashMap<String, Object> collection, final JCTree.JCExpression collectionMethodEx) {
    ListBuffer collectionMethodBuffer = new ListBuffer();

    for (Entry<String, Object> entry : collection.entrySet()) {

      ListBuffer putBuffer = new ListBuffer();
      putBuffer.add(getMaker().Literal(entry.getKey()));
      putBuffer.add(getMaker().Literal(entry.getValue()));

      JCMethodInvocation putInv = getMaker().Apply(
              List.<JCTree.JCExpression>nil(),
              collectionMethodEx,
              putBuffer.toList());
      JCTree.JCStatement putStatement = getMaker().Exec(putInv);

      collectionMethodBuffer.add(putStatement);
    }

    return collectionMethodBuffer.toList();
  }

  @Override
  public Class getAnnotationClass() {
    return org.adrianwalker.collectionliterals.HashMap.class;
  }

  @Override
  public Class getCollectionClass() {
    return java.util.HashMap.class;
  }

  @Override
  public String getCollectionClassName() {
    return getCollectionClass().getName();
  }

  @Override
  public String getCollectionMethodName() {
    return "put";
  }
}

The code to use the annotations would look something like this for a class with static fields:

CollectionLiteralUsageStatic.java

package org.adrianwalker.collectionliterals;

import java.util.List;
import java.util.Map;
import java.util.Set;

public final class CollectionLiteralUsageStatic {

  /**
   [1,2,3,4,5,6,7,8,9,10]
   */
  @ArrayList
  private static List<Integer> list;
  /**
   ["0","1","2","3","4","5","6","7",
   "8","9","A","B","C","D","E","F"]
   */
  @HashSet
  private static Set<String> set;  
  /**
   {
     "name":"Adrian Walker", 
     "age":31 
   }
   */
  @HashMap
  private static Map<String, Object> map;

  public static void main(final String[] args) {
    System.out.println(list);
    System.out.println(set);
    System.out.println(map);
  }
}

And example code for classes with instance fields:

package org.adrianwalker.collectionliterals;

import java.util.List;
import java.util.Map;
import java.util.Set;

public final class CollectionLiteralUsage {

  /**
   [1,2,3,4,5,6,7,8,9,10]
   */
  @LinkedList
  private List<Integer> list;
  /**
   ["0","1","2","3","4","5","6","7", 
   "8","9","A","B","C","D","E","F"]
   */
  @HashSet
  private Set<String> set;
  /**
   {
     "name":"Adrian Walker", 
     "age":31 
   }
   */
  @HashMap
  private Map<String, Object> map;

  public List getList() {
    return list;
  }

  public Set<String> getSet() {
    return set;
  }

  public Map getMap() {
    return map;
  }

  public static void main(final String[] args) {
    CollectionLiteralUsage collectionUsage = new CollectionLiteralUsage();
    System.out.println(collectionUsage.getList());
    System.out.println(collectionUsage.getSet());
    System.out.println(collectionUsage.getMap());
  }
}

As before with the multiline code, remember to include an <annotationProcessor> element when compiling classes which use the annotations when using Maven.

pom.xml

...
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-compiler-plugin</artifactId>
  <version>2.3.2</version>
  <configuration>
    <source>1.6</source>
    <target>1.6</target>
    <annotationProcessors>
      <annotationProcessor>
        org.adrianwalker.collectionliterals.ArrayListProcessor
      </annotationProcessor>
      <annotationProcessor>
        org.adrianwalker.collectionliterals.LinkedListProcessor
      </annotationProcessor>
      <annotationProcessor>
        org.adrianwalker.collectionliterals.HashSetProcessor
      </annotationProcessor>
      <annotationProcessor>
        org.adrianwalker.collectionliterals.HashMapProcessor
      </annotationProcessor>
    </annotationProcessors>
  </configuration>
</plugin>
...

A commenter on Stack Overflow had expressed an issue with this approach having 'a hard dependency on Maven', which is not right. The javac command has direct support for specifying annotation processors: http://docs.oracle.com/javase/6/docs/technotes/tools/solaris/javac.html#processing

Limitations

Currently the code does not support nested data structures, for example this JSON string cannot be used to generate code:

{
  "name":"Adrian Walker",
  "age":31,
  "address":[
    "line1",
    "line2",
    "line3"
  ]
}

Source Code

  • Annotations, annotation processors and example usage code available in GitHub - collection-literals