com.shaishavgandhi.navigator.FileWriter Maven / Gradle / Ivy
The newest version!
package com.shaishavgandhi.navigator;
import androidx.annotation.CheckResult;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.FieldSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import static com.shaishavgandhi.navigator.StringUtils.capitalize;
import static javax.lang.model.element.Modifier.FINAL;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
final class FileWriter {
private static final ClassName CONTEXT_CLASSNAME = ClassName.get("android.content", "Context");
private static final ClassName INTENT_CLASSNAME = ClassName.get("android.content", "Intent");
private static final ClassName BUNDLE_CLASSNAME = ClassName.get("android.os", "Bundle");
private static final ClassName ACTIVITY_CLASSNAME = ClassName.get("android.app", "Activity");
private static final ClassName FRAGMENT_CLASSNAME = ClassName.get("androidx.fragment.app", "Fragment");
private static final ClassName STRING_CLASS = ClassName.bestGuess("java.lang.String");
private static final String FLAGS = "flags";
private static final String ACTION = "action";
private List constructorParams = new ArrayList<>();
private List classBuilders = new ArrayList<>();
private HashMap typeMapper = new HashMap(){{
put("java.lang.String", "String");
put("java.lang.String[]", "StringArray");
put("java.util.ArrayList", "StringArrayList");
put("java.lang.Integer", "Int");
put("int", "Int");
put("int[]", "IntArray");
put("java.util.ArrayList", "IntegerArrayList");
put("java.lang.Long","Long");
put("long", "Long");
put("long[]", "LongArray");
put("double", "Double");
put("java.lang.Double", "Double");
put("double[]", "DoubleArray");
put("float", "Float");
put("java.lang.Float","Float");
put("float[]", "FloatArray");
put("byte", "Byte");
put("java.lang.Byte", "Byte");
put("byte[]", "ByteArray");
put("short", "Short");
put("java.lang.Short", "Short");
put("short[]", "ShortArray");
put("char", "Char");
put("java.lang.Character", "Char");
put("char[]", "CharArray");
put("java.lang.CharSequence", "CharSequence");
put("java.lang.CharSequence[]", "CharSequenceArray");
put("java.util.ArrayList", "CharSequenceArrayList");
put("android.util.Size", "Size");
put("android.util.SizeF", "SizeF");
put("boolean", "Boolean");
put("boolean[]", "BooleanArray");
put("java.lang.Boolean", "Boolean");
put("android.os.Parcelable", "Parcelable");
put("android.os.Parcelable[]", "ParcelableArray");
put("java.util.ArrayList", "ParcelableArrayList");
}};
private LinkedHashMap> annotationsPerClass;
private Types typeUtils;
private Elements elementUtils;
private List files = new ArrayList<>();
private Messager messager;
FileWriter(Types typeUtils, Elements elementUtils, LinkedHashMap> annotationsPerClass, Messager messager) {
this.typeUtils = typeUtils;
this.elementUtils = elementUtils;
this.annotationsPerClass = annotationsPerClass;
this.messager = messager;
}
protected void writeFiles() {
for (Map.Entry> item : annotationsPerClass.entrySet()) {
ClassName className = item.getKey().getJavaClass();
LinkedHashSet annotations = item.getValue();
writeBinder(item.getKey(), annotations);
writeBuilder(item.getKey(), annotations);
}
}
private void addBindingVariables(TypeSpec.Builder binder, ClassName activity, LinkedHashSet annotations) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("bind")
.addJavadoc(CodeBlock.builder()
.add("Binds the fields in {@link $T} annotated with {@link $T}\n", activity, Extra.class)
.add("\n")
.add("This requires that the fields in {@link $T} be at least package-private" +
" or if they \nare private, they have a defined setter in the class.\n", activity)
.add("You should call this method in an early part of the activity lifecycle like " +
"onCreate on onStart.\n\n")
.add("@param binder the activity/fragment whose variables are being bound\n")
.build())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC)
.addParameter(activity, "binder");
if (isActivity(activity)) {
builder.addStatement("$T bundle = $L.getIntent().getExtras()", BUNDLE_CLASSNAME,
"binder");
} else if (isFragment(activity)) {
builder.addStatement("$T bundle = $L.getArguments()", BUNDLE_CLASSNAME,
"binder");
} else {
messager.printMessage(Diagnostic.Kind.ERROR, "@Extra can only be applied to fields in" +
" Activities or Fragments");
}
builder.beginControlFlow("if (bundle != null)");
for (Element element: annotations) {
Set modifiers = element.getModifiers();
TypeName name = TypeName.get(element.asType());
String varKey = getQualifiedExtraFieldName(activity, element);
String varName = element.getSimpleName().toString();
MethodSpec.Builder nullableGetter = MethodSpec.methodBuilder("get" + capitalize(varName))
.addModifiers(PUBLIC)
.addJavadoc(CodeBlock.builder()
.add("Nullable getter for $L\n\n", varName)
.add("If you want a non-null instance of the type, see {@link #$L($T)}\n", "get" + capitalize(varName), name)
.add("\n")
.add("@return the $L\n", varName)
.build())
.returns(name);
MethodSpec.Builder nonNullGetter = MethodSpec.methodBuilder("get" + capitalize(varName))
.addModifiers(PUBLIC)
.addAnnotation(NonNull.class)
.addParameter(ParameterSpec.builder(name, "defaultValue")
.addModifiers(FINAL)
.addAnnotation(NonNull.class)
.build())
.addJavadoc(CodeBlock.builder()
.add("Non-null getter for $L\n", varName)
.add("\n")
.add("@param defaultValue the default value in case the key isn't present in the Bundle.\n")
.add("@return the $L\n", varName)
.build())
.returns(name);
builder.beginControlFlow("if ($L.containsKey($L))", "bundle", varKey);
nonNullGetter.beginControlFlow("if ($L.containsKey($L)\n" +
" && $L.get($L) != null)", "bundle", varKey, "bundle", varKey);
if (!name.isPrimitive()) {
nullableGetter.addAnnotation(Nullable.class);
nullableGetter.beginControlFlow("if ($L.containsKey($L))", "bundle", varKey);
}
String extraName = getExtraTypeName(element.asType());
if (extraName == null) {
if (isSerializable(typeUtils, elementUtils, element.asType())) {
// Add casting for serializable
builder.addStatement("$T $L = ($T) bundle.getSerializable($L)", name,
varName, name, varKey);
nullableGetter.addStatement("return ($T) bundle.getSerializable($L)", name, varKey);
nonNullGetter.addStatement("return ($T) bundle.getSerializable($L)", name, varKey);
} else {
messager.printMessage(Diagnostic.Kind.ERROR, element.getSimpleName().toString() + " cannot be put in Bundle");
}
} else {
if (extraName.equals("ParcelableArray") || extraName.equals("Serializable")) {
// Add extra casting. TODO: Refactor this to be more generic
builder.addStatement("$T $L = ($T) bundle.get" + extraName + "($L)", name,
varName, name, varKey);
nullableGetter.addStatement("return ($T) bundle.get" + extraName + "($L)", name, varKey);
nonNullGetter.addStatement("return ($T) bundle.get" + extraName + "($L)", name, varKey);
} else {
builder.addStatement("$T $L = bundle.get" + extraName + "($L)", name, varName,
varKey);
nullableGetter.addStatement("return bundle.get" + extraName + "($L)", varKey);
nonNullGetter.addStatement("return bundle.get" + extraName + "($L)", varKey);
}
}
if (modifiers.contains(PRIVATE)) {
// Use getter and setter
builder.addStatement("$L.set$L($L)", "binder", capitalize(varName), varName);
} else {
builder.addStatement("$L.$L = $L", "binder", varName, varName);
}
builder.endControlFlow();
if (!name.isPrimitive()) {
nullableGetter.endControlFlow();
nullableGetter.addStatement("return null");
}
nonNullGetter.endControlFlow();
nonNullGetter.addStatement("return defaultValue");
binder.addMethod(nullableGetter.build());
binder.addMethod(nonNullGetter.build());
}
builder.endControlFlow();
binder.addMethod(builder.build());
}
/**
* Returns whether the given class is a Fragment or not.
*
* @param className Class of element annotated with {@link Extra}
* @return boolean
*/
private boolean isFragment(ClassName className) {
TypeMirror currentClass = elementUtils.getTypeElement(className.toString()).asType();
boolean isFragment = false;
if (elementUtils.getTypeElement("android.support.v4.app.Fragment") != null) {
TypeMirror supportFragment = elementUtils.getTypeElement("android.support.v4.app.Fragment").asType();
isFragment = typeUtils.isSubtype(currentClass, supportFragment);
}
if (elementUtils.getTypeElement("android.app.Fragment") != null && !isFragment) {
TypeMirror fragment = elementUtils.getTypeElement("android.app.Fragment").asType();
isFragment = typeUtils.isSubtype(currentClass, fragment);
}
if (elementUtils.getTypeElement("androidx.fragment.app.Fragment") != null && !isFragment) {
TypeMirror fragment = elementUtils.getTypeElement("androidx.fragment.app.Fragment").asType();
isFragment = typeUtils.isSubtype(currentClass, fragment);
}
return isFragment;
}
/**
* Returns whether the given class is an Activity or not.
*
* @param className Class of element annotated with {@link Extra}
* @return boolean
*/
private boolean isActivity(ClassName className) {
if (elementUtils.getTypeElement("android.app.Activity") != null) {
TypeMirror activity = elementUtils.getTypeElement("android.app.Activity").asType();
TypeMirror currentClass = elementUtils.getTypeElement(className.toString()).asType();
return typeUtils.isSubtype(currentClass, activity);
}
return false;
}
private void writeBinder(QualifiedClassName qualifiedClassName, LinkedHashSet annotations) {
ClassName className = qualifiedClassName.getJavaClass();
ClassName binderClass = getBinderClass(className);
TypeSpec.Builder binder = TypeSpec.classBuilder(binderClass.simpleName())
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
addBindingVariables(binder, className, annotations);
// Add a private empty constructor
binder.addMethod(MethodSpec.constructorBuilder()
.addModifiers(PRIVATE)
.build());
// Add private constructor with arg as Bundle
binder.addMethod(MethodSpec.constructorBuilder()
.addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "bundle")
.addAnnotation(NonNull.class)
.build())
.addModifiers(PRIVATE)
.addStatement("this.bundle = bundle")
.build());
// Field for storing the Bundle
binder.addField(FieldSpec.builder(BUNDLE_CLASSNAME, "bundle")
.addModifiers(PRIVATE)
.addAnnotation(NonNull.class)
.build());
// Static method to get the binder
binder.addMethod(MethodSpec.methodBuilder("from")
.addModifiers(PUBLIC, STATIC)
.addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "bundle")
.addAnnotation(NonNull.class)
.addModifiers(FINAL)
.build())
.addJavadoc(CodeBlock.builder()
.add("Static factory method to instantiate {@link $T}\n", binderClass)
.add("\n")
.add("@param bundle non null bundle that will be used to unwrap the data.\n")
.add("@return the binder that will expose getters.\n")
.build())
.returns(binderClass)
.addStatement("return new $T(bundle)", binderClass)
.build());
TypeSpec binderResolved = binder.build();
files.add(JavaFile.builder(className.packageName(), binderResolved).build());
}
/**
* Get the `Binder` class of the given Activity/Fragment which is responsible
* for the binding logic of the `Bundle` to the `@Extra` . For example:
* MainActivity -> MainActivityBinder
*
* @param className Class of element annotated with {@link Extra}
* @return binder class
*/
private ClassName getBinderClass(ClassName className) {
return ClassName.bestGuess(className.packageName() + "." + className.simpleName() +
"Binder");
}
/**
* Get the `Builder` class of the given Activity/Fragment which is responsible
* for the builder logic and starting of the Activity. Given class `MainActivity`,
* builder would be `MainActivityBuilder`.
*
* @param className Class of element annotated with {@link Extra}
* @return builder class
*/
private ClassName getBuilderClass(ClassName className) {
return ClassName.bestGuess(className.packageName() + "." + className.simpleName() +
"Builder");
}
private void writeBuilder(QualifiedClassName qualifiedClassName, LinkedHashSet elements) {
ClassName activity = qualifiedClassName.getJavaClass();
String activityName = activity.simpleName();
TypeSpec.Builder builder = TypeSpec.classBuilder(activityName + "Builder")
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
if (isActivity(activity)) {
builder.addField(FieldSpec.builder(TypeName.INT, FLAGS)
.addModifiers(PRIVATE)
.initializer("$L", -1)
.build());
builder.addField(FieldSpec.builder(STRING_CLASS, ACTION)
.addModifiers(PRIVATE)
.build());
}
builder.addField(FieldSpec.builder(BUNDLE_CLASSNAME, "extras")
.addModifiers(PRIVATE).build());
ClassName builderClass = getBuilderClass(activity);
// Constructor
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder()
.addModifiers(PRIVATE);
// Set intent flags
MethodSpec setFlagsMethod = setFlagsMethod(builderClass);
// Set action
MethodSpec setActionMethod = setActionMethod(builderClass);
// Static method to prepare activity
MethodSpec.Builder prepareMethodBuilder = getPrepareActivityMethod(builderClass);
// Bundle builder
MethodSpec.Builder bundleBuilder = getExtrasBundle();
// TODO: There must be a better way for this with JavaPoet. Right now
// I manually append each parameter and remove commas and close the bracket
StringBuilder returnStatement = new StringBuilder("return new $T(");
for (Element element: elements) {
TypeMirror typeMirror = element.asType();
if (typeMirror == null) {
continue;
}
// Add as field parameter
String name = element.getSimpleName().toString();
AnnotationSpec nullability = getNullabilityFor(element);
List annotations = getAnnotationsForElement(element);
annotations.add(nullability);
builder.addField(FieldSpec.builder(TypeName.get(typeMirror), name, PRIVATE)
.addAnnotations(annotations)
.build());
// Add the extra name as public static variable
addKeyToClass(element, builder);
// Add to constructor
ParameterSpec parameter = getParameter(element, annotations);
AnnotationSpec nullable = AnnotationSpec.builder(ClassName.get(Nullable.class)).build();
if (!parameter.annotations.contains(nullable)) {
constructorBuilder.addParameter(parameter);
constructorBuilder.addStatement("this.$L = $L", parameter.name, parameter.name);
// Add to static prepare method
prepareMethodBuilder.addParameter(parameter);
constructorParams.add(parameter);
// Append to return statement
returnStatement.append(parameter.name);
returnStatement.append(", ");
} else {
addFieldToBuilder(builder, element, builderClass, annotations);
}
String extraName = getExtraTypeName(element.asType());
if (extraName == null) {
// Put to bundle
bundleBuilder.addStatement("bundle.putSerializable($L, $L)", getExtraFieldName(element),
parameter.name);
} else {
// Put to bundle
bundleBuilder.addStatement("bundle.put" + extraName + "($L, $L)", getExtraFieldName(element),
parameter.name);
}
}
classBuilders.add(new ClassBuilder(qualifiedClassName, constructorParams));
constructorParams = new ArrayList<>();
// Sanitize return statement
if (returnStatement.charAt(returnStatement.length() - 1) == ' ') {
returnStatement.deleteCharAt(returnStatement.length() - 2);
returnStatement.deleteCharAt(returnStatement.length() - 1);
}
returnStatement.append(")");
prepareMethodBuilder.addStatement(returnStatement.toString(), builderClass);
bundleBuilder.beginControlFlow("if ($L != null)", "extras");
bundleBuilder.addStatement("bundle.putAll($L)", "extras");
bundleBuilder.endControlFlow();
bundleBuilder.addStatement("return bundle");
MethodSpec bundle = bundleBuilder.build();
// Destination intent
MethodSpec destinationIntentMethod = getDestinationIntentMethod(activityName, bundle);
// Start activity
MethodSpec startActivityMethod = getStartActivityMethod(destinationIntentMethod, CONTEXT_CLASSNAME);
// Start activity from fragment
MethodSpec startActivityFromFragmentMethod = getStartActivityMethod(destinationIntentMethod, FRAGMENT_CLASSNAME);
// Start activity with extras
MethodSpec startActivityExtrasMethod = getStartActivityWithExtras(destinationIntentMethod, CONTEXT_CLASSNAME);
// Start activity from fragment with extras
MethodSpec startActivityFromFragmentExtrasMethod = getStartActivityWithExtras(destinationIntentMethod, FRAGMENT_CLASSNAME);
// Start for result
MethodSpec startForResultMethod = getStartForResultMethod(destinationIntentMethod, ACTIVITY_CLASSNAME);
// Start from fragment with result
MethodSpec startForResultFromFragmentMethod = getStartForResultMethod(destinationIntentMethod, FRAGMENT_CLASSNAME);
// Start result with extras
MethodSpec startResultExtrasMethod = getStartForResultWithExtras(destinationIntentMethod, ACTIVITY_CLASSNAME);
// Start result from fragment with extras
MethodSpec startResultFromFragmentExtrasMethod = getStartForResultWithExtras(destinationIntentMethod, FRAGMENT_CLASSNAME);
MethodSpec setExtrasMethod = getExtrasSetterMethod(builderClass);
builder.addMethod(prepareMethodBuilder.build());
if (isActivity(activity)) {
// Add activity specific methods
builder.addMethod(startActivityMethod);
builder.addMethod(startActivityFromFragmentMethod);
builder.addMethod(startForResultMethod);
builder.addMethod(startForResultFromFragmentMethod);
builder.addMethod(destinationIntentMethod);
builder.addMethod(startResultExtrasMethod);
builder.addMethod(startResultFromFragmentExtrasMethod);
builder.addMethod(startActivityExtrasMethod);
builder.addMethod(startActivityFromFragmentExtrasMethod);
builder.addMethod(setFlagsMethod);
builder.addMethod(setActionMethod);
}
builder.addMethod(bundle);
builder.addMethod(setExtrasMethod);
TypeSpec builderInnerClass = builder.addMethod(constructorBuilder.build()).build();
JavaFile file = JavaFile.builder(activity.packageName(), builderInnerClass).build();
files.add(file);
}
/**
* Add static final key that is used to bind each extra
* from the bundle.
*
* @param element to be added
* @param builder `Builder` class
*/
private void addKeyToClass(Element element, TypeSpec.Builder builder) {
NParameter extraName = getVariableKey(element);
if (!extraName.getCustomKey()) {
builder.addField(FieldSpec.builder(STRING_CLASS, getExtraFieldName(extraName),
Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
.initializer("\"$L\"", extraName.getName())
.build());
}
}
private String getExtraFieldName(Element element) {
return getExtraFieldName(getVariableKey(element));
}
private String getQualifiedExtraFieldName(ClassName bindingClass, Element element) {
NParameter param = getVariableKey(element);
if (!param.getCustomKey()) {
return bindingClass.simpleName() + "Builder." + getExtraFieldName(getVariableKey(element));
}
return getExtraFieldName(param);
}
private String getExtraFieldName(NParameter parameter) {
StringBuilder builder = new StringBuilder("EXTRA");
if (!parameter.getCustomKey()) {
for (String word : splitByCasing(parameter.getName())) {
builder.append("_");
builder.append(word.toUpperCase());
}
return builder.toString();
} else {
return "\""+ parameter.getName() +"\"";
}
}
private String[] splitByCasing(String variable) {
return variable.split("(? annotations) {
String variableName = element.getSimpleName().toString();
MethodSpec.Builder setter = MethodSpec.methodBuilder("set" + capitalize(variableName))
.addModifiers(Modifier.FINAL, Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(TypeName.get(element.asType()), variableName)
.addAnnotations(annotations)
.build())
.addAnnotation(CheckResult.class)
.addStatement("this.$L = $L", variableName, variableName)
.returns(builderClass)
.addStatement("return this");
builder.addMethod(setter.build());
}
/**
* Generates the `startForResult` method which is added to
* the `Builder` class.
*
* @param destinationIntent the intent to start the Activity.
* @return `startForResult` method.
*/
private MethodSpec getStartForResultMethod(MethodSpec destinationIntent, ClassName originClass) {
final String parameterName = originClass.simpleName().toLowerCase();
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("startForResult")
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(originClass, parameterName)
.addAnnotation(NonNull.class)
.build())
.addParameter(TypeName.INT, "requestCode");
String context = resolveContext(originClass);
methodBuilder.addStatement("$T intent = $N($L)", INTENT_CLASSNAME,
destinationIntent, context);
// Add JavaDoc
methodBuilder.addJavadoc(CodeBlock.builder()
.add("Terminating method in builder. Passes the built bundle, sets any \n")
.add("{@link android.content.Intent} flags if any and starts the activity with \n")
.add("the provided requestCode.\n")
.add("\n")
.add("@param $L\n", parameterName)
.add("@param $L\n", "requestCode")
.build());
// Start for result
methodBuilder.addStatement("$L.startActivityForResult($L, $L)", parameterName,
"intent", "requestCode");
return methodBuilder.build();
}
private MethodSpec getDestinationIntentMethod(String activityName, MethodSpec bundle) {
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("getDestinationIntent")
.addModifiers(PRIVATE)
.addAnnotation(NonNull.class)
.addParameter(ParameterSpec.builder(CONTEXT_CLASSNAME, "context")
.addAnnotation(NonNull.class)
.build()
).returns(INTENT_CLASSNAME);
methodBuilder.addStatement("$T intent = new $T($L, $L)", INTENT_CLASSNAME,
INTENT_CLASSNAME, "context", activityName + ".class");
methodBuilder.addJavadoc(CodeBlock.builder()
.add("Returns the {@link android.content.Intent} that will be used to start the Activity.\n")
.add("\n")
.add("Sets optional fields like {@link flags}, {@link action} if they are supplied by\n")
.add("you in the builder methods.\n")
.add("\n")
.add("@param $L the context used in Intent.\n", "context")
.add("@return the constructed Intent\n")
.build());
// Put extras
methodBuilder.addStatement("intent.putExtras($N())", bundle);
//Add flags if any
addOptionalAttributes(methodBuilder);
methodBuilder.addStatement("return intent");
return methodBuilder.build();
}
/**
* Generates the `startForResult` method with overload for a `Bundle`. This
* is added to the `Builder` class to start an Activity with a result.
*
* @param destinationIntent the Intent to be started.
* @return `startForResult` method
*/
private MethodSpec getStartForResultWithExtras(MethodSpec destinationIntent, ClassName originClass) {
final String parameterName = originClass.simpleName().toLowerCase();
MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder("startForResult")
.addModifiers(Modifier.PUBLIC)
.addParameter(ParameterSpec.builder(originClass, parameterName)
.addAnnotation(NonNull.class)
.build()
)
.addParameter(TypeName.INT, "requestCode")
.addParameter(ParameterSpec.builder(BUNDLE_CLASSNAME, "extras", Modifier.FINAL)
.addAnnotation(Nullable.class).build());
String context = resolveContext(originClass);
methodBuilder.addStatement("$T intent = $N($L)", INTENT_CLASSNAME,
destinationIntent, context);
// Add JavaDoc
methodBuilder.addJavadoc(CodeBlock.builder()
.add("Terminating method in builder. Passes the built bundle, sets any \n")
.add("{@link android.content.Intent} flags if any and starts the activity with \n")
.add("the provided requestCode and {@link android.os.Bundle extras}.\n")
.add("\n")
.add("@param $L\n", parameterName)
.add("@param $L\n", "requestCode")
.build());
// Start activity for result
methodBuilder.addStatement("$L.startActivityForResult($L, $L, $L)", parameterName,
"intent", "requestCode", "extras");
return methodBuilder.build();
}
/**
* Returns a partially generated static factory method that is added to the
* `Builder` for easy access. i.e `MainActivityBuilder.builder(args)`
*
* @param builderClass `Builder` returned for a chaining API
* @return Partially generated method that is appended later.
*/
private MethodSpec.Builder getPrepareActivityMethod(ClassName builderClass) {
MethodSpec.Builder prepareMethodBuilder = MethodSpec.methodBuilder("builder");
prepareMethodBuilder.addModifiers(Modifier.STATIC, Modifier.FINAL, Modifier.PUBLIC);
prepareMethodBuilder.returns(builderClass);
prepareMethodBuilder.addAnnotation(CheckResult.class);
return prepareMethodBuilder;
}
/**
* Adds optional attributes like `setFlags` and `setAction`
* to the given builder.
*
* @param builder `Builder` class
*/
private void addOptionalAttributes(MethodSpec.Builder builder) {
builder.beginControlFlow("if ($L != -1)", FLAGS);
builder.addStatement("$L.setFlags($L)", "intent", FLAGS);
builder.endControlFlow();
builder.beginControlFlow("if ($L != null)", ACTION);
builder.addStatement("$L.setAction($L)", "intent", ACTION);
builder.endControlFlow();
}
/**
* Get {@link ParameterSpec} from given element.
* Checks for nullability, types etc and returns
* the parameter.
*
* @param element to be converted to Parameter
* @return {@link ParameterSpec}
*/
private ParameterSpec getParameter(Element element, List annotations) {
TypeMirror typeMirror = element.asType();
String name = element.getSimpleName().toString();
ParameterSpec.Builder parameterBuilder = ParameterSpec.builder(TypeName.get(typeMirror), name);
parameterBuilder.addModifiers(Modifier.FINAL);
parameterBuilder.addAnnotations(annotations);
return parameterBuilder.build();
}
private List getAnnotationsForElement(Element element) {
List annotations = new ArrayList<>();
for (AnnotationMirror annotation: element.getAnnotationMirrors()) {
if (isAnnotationWhitelisted(annotation)) {
continue;
}
annotations.add(AnnotationSpec.builder(ClassName.bestGuess(annotation.getAnnotationType().toString()))
.build());
}
return annotations;
}
public static boolean isAnnotationWhitelisted(AnnotationMirror annotation) {
return annotation.getAnnotationType().toString().contains("Nullable")
|| annotation.getAnnotationType().toString().contains("NonNull")
|| annotation.getAnnotationType().toString().contains("NotNull")
|| annotation.getAnnotationType().toString().contains("Extra")
|| annotation.getAnnotationType().toString().contains("Optional");
}
/**
* Returns nullability annotation for given element.
* Checks for Jetbrains' {@link org.jetbrains.annotations.Nullable}
* as well as Android's {@link Nullable}.
*
* @param element annotated with {@link Extra}
* @return Nullability annotation
*/
@NonNull private AnnotationSpec getNullabilityFor(Element element) {
// Check both Jetbrains and Android nullable annotations since
// Kotlin nulls are annotated with Jetbrains @Nullable
if (element.getAnnotation(Nullable.class) == null
&& element.getAnnotation(org.jetbrains.annotations.Nullable.class) == null
&& element.getAnnotation(Optional.class) == null) {
return AnnotationSpec.builder(ClassName.get(NonNull.class)).build();
} else {
return AnnotationSpec.builder(ClassName.get(Nullable.class)).build();
}
}
/**
* Returns the extra type name for given type.
* This method is useful in mapping from types
* like Parcelable, ParcelableArrayList etc. to
* the appropriate bundle type.
*
* @param typeMirror of the element
* @return type
*/
private String getExtraTypeName(TypeMirror typeMirror) {
String result = typeMapper.get(typeMirror.toString());
if (result == null) {
if (isParcelable(typeUtils, elementUtils, typeMirror)) {
result = "Parcelable";
} else if (isParcelableList(typeUtils, elementUtils, typeMirror)) {
result = "ParcelableArrayList";
} else if (isSparseParcelableArrayList(typeUtils, elementUtils, typeMirror)) {
result = "SparseParcelableArray";
} else if (isParcelableArray(typeUtils, elementUtils, typeMirror)) {
result = "ParcelableArray";
}
}
return result;
}
public static boolean isParcelable(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
return typeUtils.isAssignable(typeMirror, elementUtils.getTypeElement("android.os.Parcelable")
.asType());
}
public static boolean isParcelableArray(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
return typeUtils.isAssignable(typeMirror, typeUtils.getArrayType(elementUtils
.getTypeElement("android.os.Parcelable").asType()));
}
public static boolean isParcelableList(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
DeclaredType type = typeUtils.getDeclaredType(elementUtils.getTypeElement("java.util" +
".ArrayList"), elementUtils.getTypeElement("android.os.Parcelable").asType());
if (typeUtils.isAssignable(typeUtils.erasure(typeMirror), type)) {
List extends TypeMirror> typeArguments = ((DeclaredType) typeMirror).getTypeArguments();
return typeArguments != null && typeArguments.size() >= 1 &&
typeUtils.isAssignable(typeArguments.get(0), elementUtils.getTypeElement
("android.os.Parcelable").asType());
}
return false;
}
public static boolean isSparseParcelableArrayList(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
DeclaredType type = typeUtils.getDeclaredType(elementUtils.getTypeElement("android.util.SparseArray"),
elementUtils.getTypeElement("android.os.Parcelable").asType());
if (typeUtils.isAssignable(typeUtils.erasure(typeMirror), type)) {
List extends TypeMirror> typeArguments = ((DeclaredType) typeMirror).getTypeArguments();
return typeArguments != null && typeArguments.size() >= 1 &&
typeUtils.isAssignable(typeArguments.get(0), elementUtils.getTypeElement
("android.os.Parcelable").asType());
}
return false;
}
public static boolean isSerializable(Types typeUtils, Elements elementUtils, TypeMirror typeMirror) {
return typeUtils.isAssignable(typeMirror, elementUtils.getTypeElement("java.io.Serializable")
.asType());
}
NParameter getVariableKey(Element element) {
if (element.getAnnotation(Extra.class).key().isEmpty()) {
return new NParameter(element.asType(), element.getSimpleName().toString(), false);
} else {
return new NParameter(element.asType(), element.getAnnotation(Extra.class).key(), true);
}
}
protected List getFiles() {
return files;
}
public List getClassBuilders() {
return classBuilders;
}
}