package com.hannesdorfmann.fragmentargs.processor; import com.hannesdorfmann.fragmentargs.FragmentArgs; import com.hannesdorfmann.fragmentargs.FragmentArgsInjector; import com.hannesdorfmann.fragmentargs.annotation.Arg; import com.hannesdorfmann.fragmentargs.annotation.FragmentArgsInherited; import com.hannesdorfmann.fragmentargs.annotation.FragmentWithArgs; import com.hannesdorfmann.fragmentargs.bundler.ArgsBundler; import com.hannesdorfmann.fragmentargs.repacked.com.squareup.javawriter.JavaWriter; import java.io.IOException; import java.io.Serializable; import java.io.Writer; import java.lang.annotation.Annotation; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; 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.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import javax.tools.JavaFileObject; /** * This is the annotation processor for FragmentArgs * * @author Hannes Dorfmann */ public class ArgProcessor extends AbstractProcessor { private static final String CUSTOM_BUNDLER_BUNDLE_KEY = "com.hannesdorfmann.fragmentargs.custom.bundler.2312A478rand."; private static final Map<String, String> ARGUMENT_TYPES = new HashMap<String, String>(20); /** * Annotation Processor Option */ private static final String OPTION_IS_LIBRARY = "fragmentArgsLib"; /** * Should the builder be annotated with support annotations? */ private static final String OPTION_SUPPORT_ANNOTATIONS = "fragmentArgsSupportAnnotations"; /** * Pass a list of additional annotations to annotate the generated builder classes */ private static final String OPTION_ADDITIONAL_BUILDER_ANNOTATIONS = "fragmentArgsBuilderAnnotations"; static { ARGUMENT_TYPES.put("java.lang.String", "String"); ARGUMENT_TYPES.put("int", "Int"); ARGUMENT_TYPES.put("java.lang.Integer", "Int"); ARGUMENT_TYPES.put("long", "Long"); ARGUMENT_TYPES.put("java.lang.Long", "Long"); ARGUMENT_TYPES.put("double", "Double"); ARGUMENT_TYPES.put("java.lang.Double", "Double"); ARGUMENT_TYPES.put("short", "Short"); ARGUMENT_TYPES.put("java.lang.Short", "Short"); ARGUMENT_TYPES.put("float", "Float"); ARGUMENT_TYPES.put("java.lang.Float", "Float"); ARGUMENT_TYPES.put("byte", "Byte"); ARGUMENT_TYPES.put("java.lang.Byte", "Byte"); ARGUMENT_TYPES.put("boolean", "Boolean"); ARGUMENT_TYPES.put("java.lang.Boolean", "Boolean"); ARGUMENT_TYPES.put("char", "Char"); ARGUMENT_TYPES.put("java.lang.Character", "Char"); ARGUMENT_TYPES.put("java.lang.CharSequence", "CharSequence"); ARGUMENT_TYPES.put("android.os.Bundle", "Bundle"); ARGUMENT_TYPES.put("android.os.Parcelable", "Parcelable"); } private Elements elementUtils; private Types typeUtils; private Filer filer; private TypeElement TYPE_FRAGMENT; private TypeElement TYPE_SUPPORT_FRAGMENT; private boolean supportAnnotations = true; @Override public Set<String> getSupportedAnnotationTypes() { Set<String> supportTypes = new LinkedHashSet<String>(); supportTypes.add(Arg.class.getCanonicalName()); supportTypes.add(FragmentWithArgs.class.getCanonicalName()); supportTypes.add(FragmentArgsInherited.class.getCanonicalName()); return supportTypes; } @Override public Set<String> getSupportedOptions() { Set<String> suppotedOptions = new LinkedHashSet<String>(); suppotedOptions.add(OPTION_IS_LIBRARY); suppotedOptions.add(OPTION_ADDITIONAL_BUILDER_ANNOTATIONS); suppotedOptions.add(OPTION_SUPPORT_ANNOTATIONS); return suppotedOptions; } @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); elementUtils = env.getElementUtils(); typeUtils = env.getTypeUtils(); filer = env.getFiler(); TYPE_FRAGMENT = elementUtils.getTypeElement("android.app.Fragment"); TYPE_SUPPORT_FRAGMENT = elementUtils.getTypeElement("android.support.v4.app.Fragment"); } protected String getOperation(ArgumentAnnotatedField arg) { String op = ARGUMENT_TYPES.get(arg.getRawType()); if (op != null) { if (arg.isArray()) { return op + "Array"; } else { return op; } } Elements elements = processingEnv.getElementUtils(); TypeMirror type = arg.getElement().asType(); Types types = processingEnv.getTypeUtils(); String[] arrayListTypes = new String[] { String.class.getName(), Integer.class.getName(), CharSequence.class.getName() }; String[] arrayListOps = new String[] {"StringArrayList", "IntegerArrayList", "CharSequenceArrayList"}; for (int i = 0; i < arrayListTypes.length; i++) { TypeMirror tm = getArrayListType(arrayListTypes[i]); if (types.isAssignable(type, tm)) { return arrayListOps[i]; } } if (types.isAssignable(type, getWildcardType(ArrayList.class.getName(), "android.os.Parcelable"))) { return "ParcelableArrayList"; } TypeMirror sparseParcelableArray = getWildcardType("android.util.SparseArray", "android.os.Parcelable"); if (types.isAssignable(type, sparseParcelableArray)) { return "SparseParcelableArray"; } if (types.isAssignable(type, elements.getTypeElement("android.os.Parcelable").asType())) { return "Parcelable"; } if (types.isAssignable(type, elements.getTypeElement(Serializable.class.getName()).asType())) { return "Serializable"; } return null; } private TypeMirror getWildcardType(String type, String elementType) { TypeElement arrayList = processingEnv.getElementUtils().getTypeElement(type); TypeMirror elType = processingEnv.getElementUtils().getTypeElement(elementType).asType(); return processingEnv.getTypeUtils() .getDeclaredType(arrayList, processingEnv.getTypeUtils().getWildcardType(elType, null)); } private TypeMirror getArrayListType(String elementType) { TypeElement arrayList = processingEnv.getElementUtils().getTypeElement("java.util.ArrayList"); TypeMirror elType = processingEnv.getElementUtils().getTypeElement(elementType).asType(); return processingEnv.getTypeUtils().getDeclaredType(arrayList, elType); } private void writePutArguments(JavaWriter jw, String sourceVariable, String bundleVariable, ArgumentAnnotatedField arg) throws IOException, ProcessingException { jw.emitEmptyLine(); if (!arg.isPrimitive() && !arg.isRequired()) { jw.beginControlFlow("if (%s == null)", sourceVariable); jw.emitStatement("throw new NullPointerException(\"Argument '%s' must not be null.\")", arg.getName()); jw.endControlFlow(); } if (arg.hasCustomBundler()) { jw.emitStatement("%s.putBoolean(\"%s\", true)", bundleVariable, CUSTOM_BUNDLER_BUNDLE_KEY + arg.getKey()); jw.emitStatement("%s.put(\"%s\", %s, %s)", arg.getBundlerFieldName(), arg.getKey(), sourceVariable, bundleVariable); } else { String op = getOperation(arg); if (op == null) { throw new ProcessingException(arg.getElement(), "Don't know how to put %s in a Bundle. This type is not supported by default. " + "However, you can specify your own %s implementation in @Arg( bundler = YourBundler.class)", arg.getElement().asType().toString(), ArgsBundler.class.getSimpleName()); } if ("Serializable".equals(op)) { processingEnv.getMessager() .printMessage(Diagnostic.Kind.WARNING, String.format("%1$s will be stored as Serializable", arg.getName()), arg.getElement()); } jw.emitStatement("%4$s.put%1$s(\"%2$s\", %3$s)", op, arg.getKey(), sourceVariable, bundleVariable); } } protected void writePackage(JavaWriter jw, TypeElement type) throws IOException { PackageElement pkg = processingEnv.getElementUtils().getPackageOf(type); if (!pkg.isUnnamed()) { jw.emitPackage(pkg.getQualifiedName().toString()); } else { jw.emitPackage(""); } } /** * Scans for @Arg annotations in the class itself and all super classes (complete inheritance * hierarchy) */ private AnnotatedFragment collectArgumentsForTypeInclSuperClasses(TypeElement type) throws ProcessingException { AnnotatedFragment fragment = new AnnotatedFragment(type); TypeElement currentClass = type; do { for (Element e : currentClass.getEnclosedElements()) { if (e.getKind() != ElementKind.FIELD) { fragment.checkAndAddSetterMethod(e); continue; } // It's a field Arg annotation = null; if ((annotation = e.getAnnotation(Arg.class)) != null) { ArgumentAnnotatedField annotatedField = new ArgumentAnnotatedField(e, (TypeElement) e.getEnclosingElement(), annotation); addAnnotatedField(annotatedField, fragment, annotation); } } TypeMirror superClassType = currentClass.getSuperclass(); if (superClassType.getKind() == TypeKind.NONE) { // Basis class (java.lang.Object) reached, so exit currentClass = null; break; } else { currentClass = (TypeElement) typeUtils.asElement(superClassType); } } while (currentClass != null); return fragment; } /** * Generates an error String with detailed information about that a field with the same name is * already defined in a super class */ private String getErrorMessageDuplicatedField(AnnotatedFragment fragment, TypeElement problemClass, String fieldName) { String base = "A field with the name '%s' in class %s is already annotated with @%s in super class %s ! " + "Fields name must be unique within inheritance hierarchy."; // Assumption: The problemClass is already a super class of the real problem, // So determine the real problem by searching for the subclass that cause this problem TypeElement otherClass = null; for (ArgumentAnnotatedField otherField : fragment.getAll()) { if (otherField.getVariableName().equals(fieldName)) { otherClass = otherField.getClassElement(); break; } } if (otherClass != null) { // Check who is the super class TypeElement currentClass = otherClass; while (currentClass != null) { TypeMirror currentClassSuperclass = currentClass.getSuperclass(); if (currentClassSuperclass == null || currentClassSuperclass.getKind() == TypeKind.NONE) { // They are not super classes break; } if (currentClass.getQualifiedName() != null && currentClass.getQualifiedName() .toString() .equals(problemClass.getQualifiedName().toString())) { // The problem causing class is a super class, so we found the superclass // and the sub class that cause the problem return String.format(base, fieldName, otherClass.getQualifiedName().toString(), Arg.class.getSimpleName(), problemClass.getQualifiedName()); } currentClass = (TypeElement) typeUtils.asElement(currentClassSuperclass); } } // Since the previous check wasn't successfull we can assume: // The problemClass must be a sub class, so find the super class that contains the field TypeMirror superClass = problemClass.getSuperclass(); TypeElement superClassElement = null; if (superClass == null) { return String.format( "A field with the name '%s' in class %s is already annotated with @%s in a super class or sub class! " + "Fields name must be unique within inheritance hierarchy.", fieldName, problemClass.getQualifiedName().toString(), Arg.class.getSimpleName()); } boolean superClassFound = false; while (superClass != null && superClass.getKind() != TypeKind.NONE && (superClassElement = (TypeElement) typeUtils.asElement(superClass)) != null) { for (Element e : superClassElement.getEnclosedElements()) { if (e.getKind() == ElementKind.FIELD && e.getSimpleName() != null && e.getSimpleName().toString() != null && e.getSimpleName() .toString() .equals(fieldName)) { superClassFound = true; break; } } if (superClassFound) { break; } superClass = superClassElement.getSuperclass(); } if (superClassElement == null) { // Should never be the case, however to ensure we return a error message without superclass return String.format( "A field with the name '%s' in class %s is already annotated with @%s in a " + "super class or sub class of %s ! " + "Fields name must be unique within inheritance hierarchy.", fieldName, problemClass.getQualifiedName().toString(), Arg.class.getSimpleName(), problemClass.getQualifiedName().toString()); } return String.format(base, fieldName, problemClass.getQualifiedName().toString(), Arg.class.getSimpleName(), superClassElement.getQualifiedName()); } /** * Checks if the annotated field can be added to the given fragment. Otherwise a error message * will be printed */ private void addAnnotatedField(ArgumentAnnotatedField annotatedField, AnnotatedFragment fragment, Arg annotation) throws ProcessingException { if (fragment.containsField(annotatedField)) { // A field already with the name is here throw new ProcessingException(annotatedField.getElement(), getErrorMessageDuplicatedField(fragment, annotatedField.getClassElement(), annotatedField.getVariableName())); } else if (fragment.containsBundleKey(annotatedField) != null) { // key for bundle is already in use ArgumentAnnotatedField otherField = fragment.containsBundleKey(annotatedField); throw new ProcessingException(annotatedField.getElement(), "The bundle key '%s' for field %s in %s is already used by another " + "argument in %s (field name is '%s'). Bundle keys must be unique in inheritance hierarchy!", annotatedField.getKey(), annotatedField.getVariableName(), annotatedField.getClassElement().getQualifiedName().toString(), otherField.getClassElement().getQualifiedName().toString(), otherField.getVariableName()); } else { if (annotation.required()) { fragment.addRequired(annotatedField); } else { fragment.addOptional(annotatedField); } } } /** * Checks if inheritance hiererachy should be scanned for @Args annotations as well * * @param type The Fragment class * @return true if super type should be scanned as well, otherwise false; */ private boolean shouldScanSuperClassesFragmentArgs(TypeElement type) throws ProcessingException { boolean scanSuperClasses = true; FragmentWithArgs fragmentWithArgs = type.getAnnotation(FragmentWithArgs.class); if (fragmentWithArgs != null) { scanSuperClasses = fragmentWithArgs.inherited(); } // DEPRECATED annotation FragmentArgsInherited inheritedFragmentArgs = type.getAnnotation(FragmentArgsInherited.class); if (inheritedFragmentArgs != null) { scanSuperClasses = inheritedFragmentArgs.value(); } if (fragmentWithArgs != null && inheritedFragmentArgs != null) { throw new ProcessingException(type, "Class %s is annotated with @%s and with the old deprecated @%s annotation. You have to migrate to the new annotation and use @%s only.", type.getSimpleName(), FragmentWithArgs.class.getSimpleName(), FragmentArgsInherited.class .getSimpleName(), FragmentWithArgs.class.getSimpleName()); } return scanSuperClasses; // Default value } /** * Collects the fields that are annotated by the fragmentarg */ private AnnotatedFragment collectArgumentsForType(TypeElement type) throws ProcessingException { // incl. super classes if (shouldScanSuperClassesFragmentArgs(type)) { return collectArgumentsForTypeInclSuperClasses(type); } // Without super classes (inheritance) Map<String, ExecutableElement> setterMethodsMap = new HashMap<String, ExecutableElement>(); AnnotatedFragment fragment = new AnnotatedFragment(type); for (Element element : type.getEnclosedElements()) { if (element.getKind() == ElementKind.FIELD) { Arg annotation = element.getAnnotation(Arg.class); if (annotation != null) { ArgumentAnnotatedField field = new ArgumentAnnotatedField(element, type, annotation); addAnnotatedField(field, fragment, annotation); } } else { // check for setter fragment.checkAndAddSetterMethod(element); } } return fragment; } @Override public boolean process(Set<? extends TypeElement> type, RoundEnvironment env) { Elements elementUtils = processingEnv.getElementUtils(); Types typeUtils = processingEnv.getTypeUtils(); Filer filer = processingEnv.getFiler(); // // Processor options // boolean isLibrary = false; String fragmentArgsLib = processingEnv.getOptions().get(OPTION_IS_LIBRARY); if (fragmentArgsLib != null && fragmentArgsLib.equalsIgnoreCase("true")) { isLibrary = true; } String supportAnnotationsStr = processingEnv.getOptions().get(OPTION_SUPPORT_ANNOTATIONS); if (supportAnnotationsStr != null && supportAnnotationsStr.equalsIgnoreCase("false")) { supportAnnotations = false; } String additionalBuilderAnnotations[] = {}; String builderAnnotationsStr = processingEnv.getOptions().get(OPTION_ADDITIONAL_BUILDER_ANNOTATIONS); if (builderAnnotationsStr != null && builderAnnotationsStr.length() > 0) { additionalBuilderAnnotations = builderAnnotationsStr.split(" "); // White space is delimiter } List<ProcessingException> processingExceptions = new ArrayList<ProcessingException>(); JavaWriter jw = null; // REMEMBER: It's a SET! it uses .equals() .hashCode() to determine if element already in set Set<TypeElement> fragmentClasses = new HashSet<TypeElement>(); Element[] origHelper = null; // Search for @Arg fields for (Element element : env.getElementsAnnotatedWith(Arg.class)) { try { TypeElement enclosingElement = (TypeElement) element.getEnclosingElement(); // Check if its a fragment if (!isFragmentClass(enclosingElement, TYPE_FRAGMENT, TYPE_SUPPORT_FRAGMENT)) { throw new ProcessingException(element, "@Arg can only be used on fragment fields (%s.%s)", enclosingElement.getQualifiedName(), element); } if (element.getModifiers().contains(Modifier.FINAL)) { throw new ProcessingException(element, "@Arg fields must not be final (%s.%s)", enclosingElement.getQualifiedName(), element); } if (element.getModifiers() .contains(Modifier.STATIC)) { throw new ProcessingException(element, "@Arg fields must not be static (%s.%s)", enclosingElement.getQualifiedName(), element); } // Skip abstract classes if (!enclosingElement.getModifiers().contains(Modifier.ABSTRACT)) { fragmentClasses.add(enclosingElement); } } catch (ProcessingException e) { processingExceptions.add(e); } } // Search for "just" @InheritedFragmentArgs --> DEPRECATED for (Element element : env.getElementsAnnotatedWith(FragmentArgsInherited.class)) { try { scanForAnnotatedFragmentClasses(env, FragmentArgsInherited.class, fragmentClasses, element); } catch (ProcessingException e) { processingExceptions.add(e); } } // Search for "just" @FragmentWithArgs for (Element element : env.getElementsAnnotatedWith(FragmentWithArgs.class)) { try { scanForAnnotatedFragmentClasses(env, FragmentWithArgs.class, fragmentClasses, element); } catch (ProcessingException e) { processingExceptions.add(e); } } // Store the key - value for the generated FragmentArtMap class Map<String, String> autoMapping = new HashMap<String, String>(); for (TypeElement fragmentClass : fragmentClasses) { JavaFileObject jfo = null; try { AnnotatedFragment fragment = collectArgumentsForType(fragmentClass); String builder = fragment.getSimpleName() + "Builder"; List<Element> originating = new ArrayList<Element>(10); originating.add(fragmentClass); TypeMirror superClass = fragmentClass.getSuperclass(); while (superClass.getKind() != TypeKind.NONE) { TypeElement element = (TypeElement) typeUtils.asElement(superClass); if (element.getQualifiedName().toString().startsWith("android.")) { break; } originating.add(element); superClass = element.getSuperclass(); } String qualifiedFragmentName = fragment.getQualifiedName().toString(); String qualifiedBuilderName = qualifiedFragmentName + "Builder"; Element[] orig = originating.toArray(new Element[originating.size()]); origHelper = orig; jfo = filer.createSourceFile(qualifiedBuilderName, orig); Writer writer = jfo.openWriter(); jw = new JavaWriter(writer); writePackage(jw, fragmentClass); jw.emitImports("android.os.Bundle"); if (supportAnnotations) { jw.emitImports("android.support.annotation.NonNull"); if (!fragment.getRequiredFields().isEmpty()) { jw.emitImports("android.support.annotation.Nullable"); } } jw.emitEmptyLine(); // Additional builder annotations for (String builderAnnotation : additionalBuilderAnnotations) { jw.emitAnnotation(builderAnnotation); } jw.beginType(builder, "class", EnumSet.of(Modifier.PUBLIC, Modifier.FINAL)); if (!fragment.getBundlerVariableMap().isEmpty()) { jw.emitEmptyLine(); for (Map.Entry<String, String> e : fragment.getBundlerVariableMap().entrySet()) { jw.emitField(e.getKey(), e.getValue(), EnumSet.of(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC), "new " + e.getKey() + "()"); } } jw.emitEmptyLine(); jw.emitField("Bundle", "mArguments", EnumSet.of(Modifier.PRIVATE, Modifier.FINAL), "new Bundle()"); jw.emitEmptyLine(); Set<ArgumentAnnotatedField> required = fragment.getRequiredFields(); String[] args = new String[required.size() * 2]; int index = 0; for (ArgumentAnnotatedField arg : required) { boolean annotate = supportAnnotations && !arg.isPrimitive(); args[index++] = annotate ? "@NonNull " + arg.getType() : arg.getType(); args[index++] = arg.getVariableName(); } jw.beginMethod(null, builder, EnumSet.of(Modifier.PUBLIC), args); for (ArgumentAnnotatedField arg : required) { writePutArguments(jw, arg.getVariableName(), "mArguments", arg); } jw.endMethod(); if (!required.isEmpty()) { jw.emitEmptyLine(); writeNewFragmentWithRequiredMethod(builder, fragmentClass, jw, args); } Set<ArgumentAnnotatedField> optionalArguments = fragment.getOptionalFields(); for (ArgumentAnnotatedField arg : optionalArguments) { writeBuilderMethod(builder, jw, arg); } jw.emitEmptyLine(); writeBuildBundleMethod(jw); jw.emitEmptyLine(); writeInjectMethod(jw, fragmentClass, fragment); jw.emitEmptyLine(); writeBuildMethod(jw, fragmentClass); jw.emitEmptyLine(); writeBuildSubclassMethod(jw, fragmentClass); jw.endType(); autoMapping.put(qualifiedFragmentName, qualifiedBuilderName); } catch (IOException e) { processingExceptions.add( new ProcessingException(fragmentClass, "Unable to write builder for type %s: %s", fragmentClass, e.getMessage())); } catch (ProcessingException e) { processingExceptions.add(e); if (jfo != null) { jfo.delete(); } } finally { if (jw != null) { try { jw.close(); } catch (IOException e1) { processingExceptions.add(new ProcessingException(fragmentClass, "Unable to close javawriter while generating builder for type %s: %s", fragmentClass, e1.getMessage())); } } } } // Write the automapping class if (origHelper != null && !isLibrary) { try { writeAutoMapping(autoMapping, origHelper); } catch (ProcessingException e) { processingExceptions.add(e); } } // Print errors for (ProcessingException e : processingExceptions) { error(e); } return true; } /** * Write the buildBundle() method * * @param jw The javawriter * @throws IOException */ private void writeBuildBundleMethod(JavaWriter jw) throws IOException { jw.beginMethod("Bundle", "buildBundle", EnumSet.of(Modifier.PUBLIC)); jw.emitStatement("return new Bundle(mArguments)"); jw.endMethod(); } /** * Scans a fragment for a given {@link FragmentWithArgs} annotation or ({@link * FragmentArgsInherited} (which is deprectated) * * @param env The round enviroment * @param annotationClass The annotation (.class) to scan for * @param fragmentClasses The set of classes already scanned (containing annotations) * @throws ProcessingException */ private void scanForAnnotatedFragmentClasses(RoundEnvironment env, Class<? extends Annotation> annotationClass, Set<TypeElement> fragmentClasses, Element element) throws ProcessingException { if (element.getKind() != ElementKind.CLASS) { throw new ProcessingException(element, "%s can only be applied on Fragment classes", annotationClass.getSimpleName()); } TypeElement classElement = (TypeElement) element; // Check if its a fragment if (!isFragmentClass(element, TYPE_FRAGMENT, TYPE_SUPPORT_FRAGMENT)) { throw new ProcessingException(element, "%s can only be used on fragments, but %s is not a subclass of fragment", annotationClass.getSimpleName(), classElement.getQualifiedName()); } // Skip abstract classes if (!classElement.getModifiers().contains(Modifier.ABSTRACT)) { fragmentClasses.add(classElement); } } /** * Checks if the given element is in a valid Fragment class */ private boolean isFragmentClass(Element classElement, TypeElement fragmentType, TypeElement supportFragmentType) { return (fragmentType != null && typeUtils.isSubtype(classElement.asType(), fragmentType.asType())) || (supportFragmentType != null && typeUtils.isSubtype( classElement.asType(), supportFragmentType.asType())); } /** * Key is the fully qualified fragment name, value is the fully qualified Builder class name */ private void writeAutoMapping(Map<String, String> mapping, Element[] element) throws ProcessingException { try { JavaFileObject jfo = filer.createSourceFile(FragmentArgs.AUTO_MAPPING_QUALIFIED_CLASS, element); Writer writer = jfo.openWriter(); JavaWriter jw = new JavaWriter(writer); // Package jw.emitPackage(FragmentArgs.AUTO_MAPPING_PACKAGE); // Imports jw.emitImports("android.os.Bundle"); // Class jw.beginType(FragmentArgs.AUTO_MAPPING_CLASS_NAME, "class", EnumSet.of(Modifier.PUBLIC, Modifier.FINAL), null, FragmentArgsInjector.class.getCanonicalName()); jw.emitEmptyLine(); // The mapping Method jw.beginMethod("void", "inject", EnumSet.of(Modifier.PUBLIC), "Object", "target"); jw.emitEmptyLine(); jw.emitStatement("Class<?> targetClass = target.getClass()"); jw.emitStatement("String targetName = targetClass.getCanonicalName()"); // TODO should be targetClass.getName()? Inner anonymous class not possible? for (Map.Entry<String, String> entry : mapping.entrySet()) { jw.emitEmptyLine(); jw.beginControlFlow("if ( %s.class.getName().equals(targetName) )", entry.getKey()); jw.emitStatement("%s.injectArguments( ( %s ) target)", entry.getValue(), entry.getKey()); jw.emitStatement("return"); jw.endControlFlow(); } // End Mapping method jw.endMethod(); jw.endType(); jw.close(); } catch (IOException e) { throw new ProcessingException(null, "Unable to write the automapping class for builder to fragment: %s: %s", FragmentArgs.AUTO_MAPPING_QUALIFIED_CLASS, e.getMessage()); } } private void writeNewFragmentWithRequiredMethod(String builder, TypeElement element, JavaWriter jw, String[] args) throws IOException { if (supportAnnotations) jw.emitAnnotation("NonNull"); jw.beginMethod(element.getQualifiedName().toString(), "new" + element.getSimpleName(), EnumSet.of(Modifier.STATIC, Modifier.PUBLIC), args); StringBuilder argNames = new StringBuilder(); for (int i = 1; i < args.length; i += 2) { argNames.append(args[i]); if (i < args.length - 1) { argNames.append(", "); } } jw.emitStatement("return new %1$s(%2$s).build()", builder, argNames); jw.endMethod(); } private void writeBuildMethod(JavaWriter jw, TypeElement element) throws IOException { if (supportAnnotations) { jw.emitAnnotation("NonNull"); } jw.beginMethod(element.getSimpleName().toString(), "build", EnumSet.of(Modifier.PUBLIC)); jw.emitStatement("%1$s fragment = new %1$s()", element.getSimpleName().toString()); jw.emitStatement("fragment.setArguments(mArguments)"); jw.emitStatement("return fragment"); jw.endMethod(); } private void writeBuildSubclassMethod(JavaWriter jw, TypeElement element) throws IOException { if (supportAnnotations) { jw.emitAnnotation("NonNull"); } jw.beginMethod("<F extends " + element.getSimpleName().toString() + "> F", "build", EnumSet.of(Modifier.PUBLIC), supportAnnotations ? "@NonNull F" : "F", "fragment"); jw.emitStatement("fragment.setArguments(mArguments)"); jw.emitStatement("return fragment"); jw.endMethod(); } private void writeInjectMethod(JavaWriter jw, TypeElement element, AnnotatedFragment fragment) throws IOException, ProcessingException { Set<ArgumentAnnotatedField> allArguments = fragment.getAll(); String fragmentType = supportAnnotations ? "@NonNull " + element.getSimpleName().toString() : element.getSimpleName().toString(); jw.beginMethod("void", "injectArguments", EnumSet.of(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC), fragmentType, "fragment"); if (!allArguments.isEmpty()) { jw.emitStatement("Bundle args = fragment.getArguments()"); // Check if bundle is null only if at least one required field if (!fragment.getRequiredFields().isEmpty()) { jw.beginControlFlow("if (args == null)"); jw.emitStatement( "throw new IllegalStateException(\"No arguments set. Have you setup this Fragment with the corresponding FragmentArgs Builder? \")"); jw.endControlFlow(); } } int setterAssignmentHelperCounter = 0; for (ArgumentAnnotatedField field : allArguments) { jw.emitEmptyLine(); // Check if the given setter is available String setterMethod = null; boolean useSetter = field.isUseSetterMethod(); if (useSetter) { ExecutableElement setterMethodElement = fragment.findSetterForField(field); setterMethod = setterMethodElement.getSimpleName().toString(); } // Args Bundler if (field.hasCustomBundler()) { String setterAssignmentHelperStr = null; String assignmentStr; if (useSetter) { setterAssignmentHelperStr = field.getType() + " value" + setterAssignmentHelperCounter + " = %s.get(\"%s\", args)"; assignmentStr = "fragment.%s( value" + setterAssignmentHelperCounter + " )"; setterAssignmentHelperCounter++; } else { assignmentStr = "fragment.%s = %s.get(\"%s\", args)"; } // Required if (field.isRequired()) { jw.beginControlFlow("if (!args.containsKey(" + JavaWriter.stringLiteral( CUSTOM_BUNDLER_BUNDLE_KEY + field.getKey()) + "))"); jw.emitStatement("throw new IllegalStateException(\"required argument %1$s is not set\")", field.getKey()); jw.endControlFlow(); if (useSetter) { jw.emitStatement(setterAssignmentHelperStr, field.getBundlerFieldName(), field.getKey()); jw.emitStatement(assignmentStr, setterMethod); } else { jw.emitStatement(assignmentStr, field.getName(), field.getBundlerFieldName(), field.getKey()); } } else { // not required bundler jw.beginControlFlow("if (args.getBoolean(" + JavaWriter.stringLiteral( CUSTOM_BUNDLER_BUNDLE_KEY + field.getKey()) + "))"); if (useSetter) { jw.emitStatement(setterAssignmentHelperStr, field.getBundlerFieldName(), field.getKey()); jw.emitStatement(assignmentStr, setterMethod); } else { jw.emitStatement(assignmentStr, field.getName(), field.getBundlerFieldName(), field.getKey()); } jw.endControlFlow(); } } else { // Build in functions String op = getOperation(field); if (op == null) { throw new ProcessingException(element, "Can't write injector, the type is not supported by default. " + "However, You can provide your own implementation by providing an %s like this: @Arg( bundler = YourBundler.class )", ArgsBundler.class.getSimpleName()); } String cast = "Serializable".equals(op) ? "(" + field.getType() + ") " : ""; if (!field.isRequired()) { jw.beginControlFlow( "if (args != null && args.containsKey(" + JavaWriter.stringLiteral(field.getKey()) + "))"); } else { jw.beginControlFlow( "if (!args.containsKey(" + JavaWriter.stringLiteral(field.getKey()) + "))"); jw.emitStatement("throw new IllegalStateException(\"required argument %1$s is not set\")", field.getKey()); jw.endControlFlow(); } if (useSetter) { jw.emitStatement( "%1$s value" + setterAssignmentHelperCounter + " = %4$sargs.get%2$s(\"%3$s\")", field.getType(), op, field.getKey(), cast); jw.emitStatement("fragment.%1$s(value" + setterAssignmentHelperCounter + ")", setterMethod); setterAssignmentHelperCounter++; } else { jw.emitStatement("fragment.%1$s = %4$sargs.get%2$s(\"%3$s\")", field.getName(), op, field.getKey(), cast); } if (!field.isRequired()) { jw.endControlFlow(); } } } jw.endMethod(); } private void writeBuilderMethod(String type, JavaWriter writer, ArgumentAnnotatedField arg) throws IOException, ProcessingException { writer.emitEmptyLine(); boolean annotate = supportAnnotations && !arg.isPrimitive(); String typeStr; if (annotate) { if (arg.isRequired()) { typeStr = "@NonNull " + arg.getType(); } else { typeStr = "@Nullable " + arg.getType(); } } else { typeStr = arg.getType(); } writer.beginMethod(type, arg.getVariableName(), EnumSet.of(Modifier.PUBLIC), typeStr, arg.getVariableName()); writePutArguments(writer, arg.getVariableName(), "mArguments", arg); writer.emitStatement("return this"); writer.endMethod(); } public void error(ProcessingException e) { String message = e.getMessage(); if (e.getMessageArgs().length > 0) { message = String.format(message, e.getMessageArgs()); } processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message, e.getElement()); } public void warn(Element element, String message, Object... args) { if (args.length > 0) { message = String.format(message, args); } processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message, element); } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } }