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 Map
s 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