com.doctusoft.bean.apt.AnnotationProcessor.java Source code

Java tutorial

Introduction

Here is the source code for com.doctusoft.bean.apt.AnnotationProcessor.java

Source

package com.doctusoft.bean.apt;

/*
 * #%L
 * ds-bean-apt
 * %%
 * Copyright (C) 2014 Doctusoft Ltd.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.io.Writer;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.FilerException;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.tools.Diagnostic.Kind;
import javax.tools.JavaFileObject;

import com.doctusoft.MethodRef;
import com.doctusoft.ObservableProperty;
import com.doctusoft.Property;
import com.doctusoft.bean.ModelObject;
import com.google.common.base.Joiner;
import com.google.common.base.Objects;
import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

@SupportedAnnotationTypes({ "com.doctusoft.Property", "com.doctusoft.ObservableProperty",
        "com.doctusoft.MethodRef" })
public class AnnotationProcessor extends AbstractProcessor {

    /**
     * Property descriptors by class typename
     */
    private Multimap<TypeElement, ElementDescriptor> elementDescriptors = ArrayListMultimap.create();
    private Set<TypeElement> typesToHandle = Sets.newHashSet();
    private Filer filer;
    /**
     * if it's a type parameter
     */
    private String currentFieldTypeName = null;

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // collect annotations and their elements
        List<PropertyDescriptor> descriptors = Lists.newArrayList();
        for (Element element : roundEnv.getElementsAnnotatedWith(com.doctusoft.Property.class)) {
            try {
                PropertyDescriptor descriptor = new PropertyDescriptor();
                descriptor.setElement(element);
                Property annotation = element.getAnnotation(com.doctusoft.Property.class);
                descriptor.setReadonly(annotation.readonly());
                descriptors.add(descriptor);
            } catch (Exception e) {
                throw new RuntimeException(
                        "Exception with element: " + element + ", " + element.getEnclosingElement(), e);
            }
        }
        for (Element element : roundEnv.getElementsAnnotatedWith(com.doctusoft.ObservableProperty.class)) {
            try {
                PropertyDescriptor descriptor = new PropertyDescriptor();
                descriptor.setElement(element);
                ObservableProperty annotation = element.getAnnotation(com.doctusoft.ObservableProperty.class);
                descriptor.setReadonly(annotation.readonly());
                descriptor.setObservable(true);
                descriptors.add(descriptor);
            } catch (Exception e) {
                throw new RuntimeException(
                        "Exception with element: " + element + ", " + element.getEnclosingElement(), e);
            }
        }
        // process annotations on types -> add an element for each of their fields
        for (PropertyDescriptor descriptor : Lists.newArrayList(descriptors)) {
            Element element = descriptor.getElement();
            try {
                if (element.getKind() == ElementKind.CLASS) {
                    typesToHandle.add((TypeElement) element);
                    for (Element enclosedElement : element.getEnclosedElements()) {
                        if (enclosedElement.getKind() == ElementKind.FIELD) {
                            PropertyDescriptor enclosedDescriptor = new PropertyDescriptor();
                            enclosedDescriptor.setElement(enclosedElement);
                            enclosedDescriptor.setReadonly(descriptor.isReadonly());
                            enclosedDescriptor.setObservable(descriptor.isObservable());
                            descriptors.add(enclosedDescriptor);
                        }
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException(
                        "Exception with element: " + element + ", " + element.getEnclosingElement(), e);
            }
        }
        // extract some more data
        for (PropertyDescriptor descriptor : descriptors) {
            Element element = descriptor.getElement();
            try {
                if (element.getKind() == ElementKind.FIELD) {
                    VariableElement variableElement = (VariableElement) element;
                    String fieldName = element.getSimpleName().toString();
                    if (fieldName.startsWith("$$"))
                        continue; // dont generate further descriptors for lombok-generated listener fields
                    descriptor.setFieldName(fieldName);
                    descriptor.setType(variableElement.asType());
                    descriptor.setElement(element);
                    Element enclosingElement = variableElement.getEnclosingElement();
                    elementDescriptors.put((TypeElement) enclosingElement, descriptor);
                    typesToHandle.add((TypeElement) enclosingElement);
                }
                if (element.getKind() == ElementKind.METHOD) {
                    ExecutableElement methodElement = (ExecutableElement) element;
                    // ensure that the method is on a getter
                    String fieldName = getFieldNameFromGetter(methodElement);
                    if (fieldName == null) {
                        processingEnv.getMessager().printMessage(Kind.ERROR, "@Property must be on a getter method",
                                methodElement);
                    }
                    fieldName = Character.toLowerCase(fieldName.charAt(0)) + fieldName.substring(1);
                    descriptor.setFieldName(fieldName);
                    ExecutableType type = (ExecutableType) methodElement.asType();
                    descriptor.setType(type.getReturnType());
                    descriptor.setElement(element);
                    Element enclosingElement = methodElement.getEnclosingElement();
                    elementDescriptors.put((TypeElement) enclosingElement, descriptor);
                    typesToHandle.add((TypeElement) enclosingElement);
                }
            } catch (Exception e) {
                throw new RuntimeException(
                        "Exception with element: " + element + ", " + element.getEnclosingElement(), e);
            }
        }
        for (Element element : roundEnv.getElementsAnnotatedWith(MethodRef.class)) {
            try {
                MethodDescriptor descriptor = new MethodDescriptor();
                ExecutableElement methodElement = (ExecutableElement) element;
                TypeMirror type = (TypeMirror) methodElement.getReturnType();
                descriptor.setType(type);
                descriptor.setElement(methodElement);
                descriptor.setMethodName(methodElement.getSimpleName().toString());
                Element enclosingElement = methodElement.getEnclosingElement();
                elementDescriptors.put((TypeElement) enclosingElement, descriptor);
                typesToHandle.add((TypeElement) enclosingElement);
            } catch (Exception e) {
                throw new RuntimeException(
                        "Exception with element: " + element + ", " + element.getEnclosingElement(), e);
            }
        }
        // emit source files
        filer = processingEnv.getFiler();
        for (TypeElement typeElement : typesToHandle) {
            try {
                Collection<ElementDescriptor> elementDescriptorsForType = elementDescriptors.get(typeElement);
                if (elementDescriptorsForType == null) {
                    elementDescriptorsForType = Lists.newArrayList();
                }
                emitClassSource(typeElement, elementDescriptorsForType);
            } catch (UnresolvedTypeException e) {
                // do nothing, we will not emit this source file, hoping that in the next round we'll succeed
                // (APT should get invoked again and again as long as new source files are generated)
            }
        }
        return true;
    }

    public String getFieldNameFromGetter(ExecutableElement element) {
        String methodName = element.getSimpleName().toString();
        String returnType = ((ExecutableType) element.asType()).getReturnType().toString();
        if (methodName.startsWith("get") || (methodName.startsWith("is") && returnType.equals("boolean"))) {
            if (methodName.startsWith("get")) {
                String fieldName = methodName.substring(3);
                if (fieldName.length() == 0)
                    return null; // not a valid getter
                return fieldName;
            }
            if (methodName.startsWith("is")) {
                String fieldName = methodName.substring(2);
                if (fieldName.length() == 0)
                    return null; // not a valid getter
                return fieldName;
            }
            return null;
        }
        return null; // this is not a getter
    }

    public void emitClassSource(TypeElement enclosingType, Collection<ElementDescriptor> descriptors) {
        try {
            // generate simple "MyClass_" named static descriptor class
            emitPropertyDescriptorClass(enclosingType, descriptors);
        } catch (FilerException e) {
            // the file probably already existed, nothing to do
            // TODO more concise exception handling
        } catch (UnresolvedTypeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("error creating source file for type: " + enclosingType, e);
        }
    }

    public void emitPropertyDescriptorClass(TypeElement enclosingType, Collection<ElementDescriptor> descriptors)
            throws Exception {
        PackageElement pck = (PackageElement) enclosingType.getEnclosingElement();
        ByteArrayOutputStream sourceBytes = new ByteArrayOutputStream();
        Writer writer = new PrintWriter(sourceBytes);
        writer.write("package " + pck.getQualifiedName() + ";\n\n");
        writer.write("import com.doctusoft.bean.ModelObject;\n");
        writer.write("import com.doctusoft.bean.Property;\n");
        writer.write("import com.doctusoft.bean.ObservableProperty;\n");
        writer.write("import com.doctusoft.bean.ListenerRegistration;\n");
        writer.write("import com.doctusoft.bean.ValueChangeListener;\n");
        writer.write("import com.doctusoft.bean.ModelObjectDescriptor;\n");
        writer.write("import com.doctusoft.bean.BeanPropertyChangeListener;\n");
        writer.write("import com.doctusoft.bean.internal.PropertyListeners;\n\n");
        writer.write("import com.doctusoft.bean.internal.BeanPropertyListeners;\n\n");
        writer.write("import com.google.common.collect.ImmutableList;\n");
        DeclaredType holderType = (DeclaredType) enclosingType.asType();
        String holderTypeSimpleName = ((TypeElement) holderType.asElement()).getSimpleName().toString();
        String holderTypeName = holderTypeSimpleName;
        if (!holderType.getTypeArguments().isEmpty()) {
            int parametersCount = holderType.getTypeArguments().size();
            holderTypeName += "<" + Strings.repeat("?,", parametersCount - 1) + "?>";
        }
        writer.write("\npublic class " + holderTypeSimpleName + "_ {\n");
        // write individual descriptors
        for (ElementDescriptor descriptor : descriptors) {
            if (descriptor instanceof PropertyDescriptor) {
                emitPropertyLiteral(writer, (PropertyDescriptor) descriptor, holderType, holderTypeSimpleName);
            }
            if (descriptor instanceof MethodDescriptor) {
                emitMethodLiteral(writer, (MethodDescriptor) descriptor, holderTypeName, holderTypeSimpleName);
            }
        }
        if (typeImplements(enclosingType, ModelObject.class.getName())) {
            emitObservableAttributesList(writer, descriptors, enclosingType);
            emitModelObjectDescriptor(writer, holderTypeName, enclosingType, !descriptors.isEmpty());
        }
        writer.write("\n}");
        writer.close();
        // open the file only after everything worked fine and the source is ready
        String fileName = enclosingType.getQualifiedName() + "_";
        JavaFileObject source = filer.createSourceFile(fileName);
        OutputStream os = source.openOutputStream();
        os.write(sourceBytes.toByteArray());
        os.close();
    }

    public void emitObservableAttributesList(Writer writer, Iterable<ElementDescriptor> descriptors,
            TypeElement holderType) throws Exception {
        // write a list of all descriptors
        writer.write(
                "\n    public static Iterable<ObservableProperty<?, ?>> observableProperties = ImmutableList.<ObservableProperty<?, ?>>builder().add(");
        List<String> descriptorFieldNames = Lists.newArrayList();
        for (ElementDescriptor descriptor : descriptors) {
            if (descriptor instanceof PropertyDescriptor) {
                PropertyDescriptor propertyDescriptor = (PropertyDescriptor) descriptor;
                if (propertyDescriptor.isObservable()) {
                    descriptorFieldNames.add("_" + propertyDescriptor.getFieldName());
                }
            }
        }
        writer.write(Joiner.on(", ").join(descriptorFieldNames));
        writer.write(")");
        // check for supertype
        emitObservableAttributesFromSuperclass(writer, holderType.getSuperclass());
        writer.write(".build();\n\n");
    }

    public void emitModelObjectDescriptor(Writer writer, String holderTypeName, TypeElement holderType,
            boolean hasProperties) throws Exception {
        String superModelClass = getNextSuperModelClass(writer, holderType.getSuperclass());
        writer.write("\n    public static final ModelObjectDescriptor<" + holderTypeName
                + "> descriptor =       new ModelObjectDescriptor<" + holderTypeName + ">() {\r\n" + "         \r\n"
                + "         @Override\r\n"
                + "         public Iterable<com.doctusoft.bean.ObservableProperty<?, ?>> getObservableProperties() {\r\n"
                + "            return observableProperties;\r\n" + "         }\r\n" + "         \r\n");
        if (!hasProperties && superModelClass == null) {
            writer.write("         @Override\r\n" + "         public ListenerRegistration addBeanChangeListener("
                    + holderTypeName + " bean, \r\n" + "               BeanPropertyChangeListener<" + holderTypeName
                    + "> listener) {\r\n"
                    + "            return new ListenerRegistration() { @Override public void removeHandler() {} };\r\n"
                    + "         }\r\n");
        } else if (superModelClass == null) {
            writer.write("         @Override\r\n" + "         public ListenerRegistration addBeanChangeListener("
                    + holderTypeName + " bean, \r\n" + "               BeanPropertyChangeListener<" + holderTypeName
                    + "> listener) {\r\n"
                    + "            if (bean.$$listeners == null) bean.$$listeners = new BeanPropertyListeners();\r\n"
                    + "            return bean.$$listeners.addListener(listener);\r\n" + "         }\r\n");
        } else {
            writer.write("         @Override\r\n" + "         public ListenerRegistration addBeanChangeListener("
                    + holderTypeName + " bean, \r\n" + "               BeanPropertyChangeListener<" + holderTypeName
                    + "> listener) {\r\n"
                    + "            if (bean.$$listeners == null) bean.$$listeners = new BeanPropertyListeners();\r\n"
                    + "            final ListenerRegistration localRegistration = bean.$$listeners.addListener(listener);\r\n"
                    + "            final ListenerRegistration superRegistration = " + superModelClass
                    + "_.descriptor.addBeanChangeListener(bean, (BeanPropertyChangeListener) listener);\r\n"
                    + "            return new ListenerRegistration() {\r\n"
                    + "               public void removeHandler() {\r\n"
                    + "                  localRegistration.removeHandler();\r\n"
                    + "                  superRegistration.removeHandler();\r\n" + "               }   \r\n"
                    + "            };\r\n" + "         }\r\n");

        }
        writer.write("      };\r\n" + "");
    }

    public void emitObservableAttributesFromSuperclass(Writer writer, TypeMirror typeMirror) throws Exception {
        String typeName = getNextSuperModelClass(writer, typeMirror);
        if (typeName != null) {
            writer.write(".addAll(" + typeName + "_.observableProperties)");
        }
    }

    public String getNextSuperModelClass(Writer writer, TypeMirror typeMirror) throws Exception {
        if (typeMirror.getKind() != TypeKind.DECLARED)
            return null;
        DeclaredType declaredType = (DeclaredType) typeMirror;
        TypeElement typeElement = (TypeElement) declaredType.asElement();
        if (!typeImplements(typeElement, ModelObject.class.getName())) // if it's not a ModelObject, we quit the recursion without generating
            return null;
        return typeElement.getQualifiedName().toString();
    }

    public void emitMethodLiteral(Writer writer, MethodDescriptor descriptor, String holderTypeName,
            String holderTypeSimpleName) throws Exception {
        ExecutableElement methodElement = (ExecutableElement) descriptor.getElement();
        int numParams = methodElement.getParameters().size();
        String staticClassName = "com.doctusoft.bean.ParametricClassMethodReferences.ClassMethodReference"
                + numParams;
        TypeMirror methodType = descriptor.getType();
        ProcessedTypeInfo methodTypeInfo = new ProcessedTypeInfo(methodType, descriptor.getElement());
        String parameterTypes = "";
        for (VariableElement variableElement : methodElement.getParameters()) {
            parameterTypes += "," + getTypeMirrorAsErasedString(variableElement.asType());
        }
        String parametricStaticClassName = staticClassName + "<" + holderTypeName + ","
                + methodTypeInfo.mappedTypeName + parameterTypes + ">";
        boolean voidType = methodTypeInfo.mappedTypeName.equals("Void");

        boolean hasDeclaredExceptions = !methodElement.getThrownTypes().isEmpty();

        writer.write("    public static final " + parametricStaticClassName + " __" + methodElement.getSimpleName()
                + " = new " + parametricStaticClassName + "() {\n" + "        public "
                + methodTypeInfo.mappedTypeName + " applyInner(" + holderTypeName
                + " object, Object [] arguments) {\n");
        if (hasDeclaredExceptions) {
            writer.write("            try {\n");
        }
        writer.write(
                "            " + (voidType ? "" : "return ") + "object." + methodElement.getSimpleName() + "(");
        List<String> parameterExpressions = Lists.newArrayList();
        for (int i = 0; i < methodElement.getParameters().size(); i++) {
            VariableElement variableElement = methodElement.getParameters().get(i);
            String typeCast = getTypeMirrorAsErasedString(variableElement.asType());
            if ("?".equals(typeCast)) {
                typeCast = "Object";
            }
            parameterExpressions.add("(" + typeCast + ") arguments[" + i + "]");
        }
        writer.write(Joiner.on(",").join(parameterExpressions));
        writer.write(");\n");
        if (voidType) {
            writer.write("        return null;\n");
        }
        if (hasDeclaredExceptions) {
            writer.write(
                    "        } catch (Exception e) { throw new com.doctusoft.bean.ReferencedInvocationException(e); }\n");
        }
        writer.write("        }\n");
        writer.write("        public String getName() { return \"" + methodElement.getSimpleName() + "\";}\n\n");
        writer.write("        public Class<" + holderTypeName + "> getParent() { return (Class)"
                + holderTypeSimpleName + ".class; }\n\n");
        writer.write("        public Class<" + methodTypeInfo.mappedTypeName + "> getReturnType() { return (Class)"
                + methodTypeInfo.typeLiteral + ".class; }\n\n");
        writer.write("    };\n\n");
    }

    protected class ProcessedTypeInfo {
        public String typeName;
        public String mappedTypeName;
        public String typeLiteral;

        public ProcessedTypeInfo(TypeMirror type, Element errorTarget) {
            typeName = type.toString();
            mappedTypeName = mapPrimitiveTypeNames(typeName);
            typeLiteral = mappedTypeName;
            currentFieldTypeName = null;
            if (type.getKind() == TypeKind.TYPEVAR) {
                currentFieldTypeName = typeName;
                typeLiteral = "Object";
                mappedTypeName = "Object";
                // type parameters of the enclosing type are erased due to the static declaration
                TypeVariable typeVar = (TypeVariable) type;
                TypeParameterElement typeParameterElement = (TypeParameterElement) typeVar.asElement();
                if (typeParameterElement.getBounds().size() > 0) {
                    // if the variable has bounds, eg 'T extends Comparable<T>', then the first bound is important, it will be the actual required type, see GenericBean2 in the test project
                    TypeMirror firstBound = typeParameterElement.getBounds().get(0);
                    String boundString = eraseTypeParametersFromString(firstBound.toString());
                    if (!"java.lang.Object".equals(boundString)) {
                        typeLiteral = boundString;
                        mappedTypeName = boundString;
                    }
                }
            }
            if (type.getKind() == TypeKind.DECLARED) {
                // the field type literal is the type qualified name without the type parameters
                DeclaredType declaredType = (DeclaredType) type;
                TypeElement typeElement = (TypeElement) declaredType.asElement();
                typeLiteral = typeElement.getQualifiedName().toString();
                // the type is present with the type parameters, but if the parameter is a type parameter of the enclosing type, it's replaced with a ? wildcard
                mappedTypeName = eraseTypeVariables(declaredType);
            }
            if (type.getKind() == TypeKind.ERROR) {
                // if the name contains dots then we assume it to be fully qualified
                if (!typeLiteral.contains(".")) {
                    // if it does not, the generated will would probably not compile (if the given type is not in the same package)
                    processingEnv.getMessager().printMessage(Kind.ERROR,
                            "Please use a fully qualified type literal", errorTarget);
                    throw new UnresolvedTypeException();
                }
            }
        }
    }

    public void emitPropertyLiteral(Writer writer, PropertyDescriptor descriptor, DeclaredType holderType,
            String holderTypeSimpleName) throws Exception {
        TypeMirror fieldType = descriptor.getType();
        ProcessedTypeInfo fieldTypeInfo = new ProcessedTypeInfo(fieldType, descriptor.getElement());
        String holderTypeName = eraseTypeVariables(holderType);
        String fieldName = descriptor.getFieldName();
        String capitalizedFieldName = Character.toUpperCase(fieldName.charAt(0)) + fieldName.substring(1);
        String getterName = "get" + capitalizedFieldName;
        if (fieldTypeInfo.typeName.equals("boolean")) {
            getterName = "is" + capitalizedFieldName;
        }
        String setterName = "set" + capitalizedFieldName;
        String interfaceToImplement = descriptor.isObservable() ? "ObservableProperty" : "Property";
        String listenersName = "$$" + fieldName + "$listeners";
        writer.write("    public static final " + interfaceToImplement + "<" + holderTypeName + ","
                + fieldTypeInfo.mappedTypeName + "> _" + fieldName + " = \n" + "    new " + interfaceToImplement
                + "<" + holderTypeName + "," + fieldTypeInfo.mappedTypeName + ">() {\n" + "    @Override public "
                + fieldTypeInfo.mappedTypeName + " getValue(" + holderTypeName + " instance) {\n"
                + "        return (" + fieldTypeInfo.typeLiteral + ") instance." + getterName + "();\n"
                + "    }\n");
        if (!descriptor.isReadonly()) {
            writer.write("    @Override public void setValue(" + holderTypeName + " instance, "
                    + fieldTypeInfo.mappedTypeName + " value) {\n" + "        ((" + holderTypeSimpleName
                    + ")instance)." + setterName + "((" + fieldTypeInfo.typeLiteral + ")value);\n" + "    }\n");
        } else {
            // readonly attribute
            writer.write("    @Override public void setValue(" + holderTypeName + " instance, "
                    + fieldTypeInfo.mappedTypeName + " value) {\n"
                    + "        throw new UnsupportedOperationException(\"The field " + fieldName + " on type "
                    + holderTypeName + " did not declare a setter.\");\n" + "    }\n");
        }
        if (descriptor.isObservable()) {
            // TODO thread-safe listeners instantiation
            writer.write("        @Override public ListenerRegistration addChangeListener(" + holderTypeName
                    + " object, ValueChangeListener<" + fieldTypeInfo.mappedTypeName + "> valueChangeListener) {\n"
                    + "            if (object." + listenersName + " == null) object." + listenersName
                    + " = new PropertyListeners();\n" + "            return object." + listenersName
                    + ".addListener(valueChangeListener);\n" + "        }\n");
            writer.write("        @Override public void fireListeners(" + holderTypeName + " object, "
                    + fieldTypeInfo.mappedTypeName + " newValue) {\n"
                    + "            if (object.$$listeners != null)\n"
                    + "                object.$$listeners.fireListeners(object, this, newValue);\n"
                    + "            if (object." + listenersName + "!= null)\n" + "                object."
                    + listenersName + ".fireListeners(newValue);\n" + "        }\n");
        }
        writer.write("    @Override public String getName() {\n" + "        return \"" + fieldName + "\";\n"
                + "    }\n" + "    @Override public Class<" + fieldTypeInfo.mappedTypeName + "> getType() {\n"
                + "        return (Class)" + fieldTypeInfo.typeLiteral + ".class;\n" + "    }\n"
                + "    @Override public Class<" + holderTypeName + "> getParent() {\n" + "        return (Class)"
                + holderTypeSimpleName + ".class;\n" + "    }\n" + "};\n\n");
    }

    /**
     * Recursively cans for type arguments at full depth and replaces type variables with ? wildcards. Returns the resulting type reference string 
     */
    public String eraseTypeVariables(DeclaredType declaredType) {
        String result = ((TypeElement) declaredType.asElement()).getQualifiedName().toString();
        if (!declaredType.getTypeArguments().isEmpty()) {
            List<String> parameterStrings = Lists.newArrayList();
            for (TypeMirror typeMirror : declaredType.getTypeArguments()) {
                parameterStrings.add(getTypeMirrorAsErasedString(typeMirror));
            }
            result += "<" + Joiner.on(",").join(parameterStrings) + ">";
        }
        return result;
    }

    public String getTypeMirrorAsErasedString(TypeMirror typeMirror) {
        if (typeMirror.getKind() == TypeKind.DECLARED) {
            // normal declared types parameters are kept
            return eraseTypeVariables((DeclaredType) typeMirror);

        }
        if (typeMirror.getKind() == TypeKind.TYPEVAR) {
            return "?";
        }
        if (typeMirror.getKind() == TypeKind.ERROR) {
            return "?";
        }
        if (typeMirror.getKind().isPrimitive()) {
            return mapPrimitiveTypeNames(typeMirror.toString());
        }
        return "?";
    }

    public static final Map<String, String> primitiveTypesMap = ImmutableMap.<String, String>builder()
            .put("void", "Void").put("boolean", "Boolean").put("byte", "Byte").put("short", "Short")
            .put("char", "Character").put("int", "Integer").put("float", "Float").put("long", "Long")
            .put("double", "Double").build();

    public static String mapPrimitiveTypeNames(String typeName) {
        return Objects.firstNonNull(primitiveTypesMap.get(typeName), typeName);
    }

    public static String eraseTypeParametersFromString(String typeString) {
        return typeString.replaceAll("\\<.*\\>", "");
    }

    public static boolean typeImplements(TypeElement typeElement, String ifName) {
        for (TypeMirror ifMirror : typeElement.getInterfaces()) {
            if (ifName.equals(eraseTypeParametersFromString(ifMirror.toString()))) {
                return true;
            }
        }
        if (typeElement.getSuperclass().getKind() == TypeKind.DECLARED) {
            TypeElement superTypeElement = (TypeElement) ((DeclaredType) typeElement.getSuperclass()).asElement();
            return typeImplements(superTypeElement, ifName);
        }
        return false;
    }

    public class UnresolvedTypeException extends RuntimeException {

    }
}