package com.kboyarshinov.activityscreen.processor;
import com.google.common.collect.Iterables;
import com.kboyarshinov.activityscreen.processor.typechecks.TypeElements;
import com.squareup.javapoet.*;
import org.apache.commons.lang3.text.WordUtils;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/**
* @author Kirill Boyarshinov
*/
public final class CodeGenerator {
private final ClassName intentClassName = ClassName.get("android.content", "Intent");
private final ClassName activityClassName = ClassName.get("android.app", "Activity");
private final ClassName bundleClassName = ClassName.get("android.os", "Bundle");
private final Elements elementUtils;
private final Types typeUtils;
private final Filer filer;
private final TypeElements typeElements;
public CodeGenerator(Elements elementUtils, Types typeUtils, Filer filer) {
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
this.filer = filer;
this.typeElements = new TypeElements(elementUtils, typeUtils);
}
/**
* Generate the java code
*
* @throws java.io.IOException
*/
public void generate(Collection<ActivityScreenAnnotatedClass> annotatedClasses) throws IOException, UnsupportedTypeException {
for (ActivityScreenAnnotatedClass annotatedClass : annotatedClasses) {
TypeElement annotatedClassElement = annotatedClass.getTypeElement();
Name activitySimpleName = annotatedClassElement.getSimpleName();
String screenClassName = activitySimpleName + ActivityScreenAnnotatedClass.SUFFIX;
PackageElement pkg = elementUtils.getPackageOf(annotatedClassElement);
String packageName = pkg.isUnnamed() ? "" : pkg.getQualifiedName().toString();
ClassName activityScreenClassName = ClassName.get(packageName, screenClassName);
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(screenClassName);
// collect parameters
Set<ActivityArgAnnotatedField> requiredFields = annotatedClass.getRequiredFields();
Set<ActivityArgAnnotatedField> optionalFields = annotatedClass.getOptionalFields();
List<Argument> requiredArguments = new ArrayList<Argument>(requiredFields.size());
List<Argument> optionalArguments = new ArrayList<Argument>(optionalFields.size());
for (ActivityArgAnnotatedField field : requiredFields) {
requiredArguments.add(Argument.from(field, typeElements));
}
for (ActivityArgAnnotatedField field : optionalFields) {
optionalArguments.add(Argument.from(field, typeElements));
}
// add fields
for (Argument argument : requiredArguments) {
classBuilder.addField(argument.asField(Modifier.PUBLIC, Modifier.FINAL));
}
for (Argument argument : optionalArguments) {
classBuilder.addField(argument.asField(Modifier.PRIVATE));
}
// add constructor
MethodSpec.Builder costructorBuilder = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC);
for (Argument argument : requiredArguments) {
costructorBuilder.addParameter(argument.asParameter());
String name = argument.name();
costructorBuilder.addStatement("this.$L = $L", name, name);
}
MethodSpec constructor = costructorBuilder.build();
classBuilder.addMethod(constructor);
// add setters
for (Argument argument : optionalArguments) {
String name = argument.name();
MethodSpec setter = MethodSpec.methodBuilder(String.format("set%s", WordUtils.capitalize(argument.name()))).
addStatement("this.$L = $L", name, name).
addStatement("return this").
returns(activityScreenClassName).
addModifiers(Modifier.PUBLIC).build();
classBuilder.addMethod(setter);
}
// add getters
for (Argument argument : optionalArguments) {
MethodSpec getter = MethodSpec.methodBuilder(String.format("get%s", WordUtils.capitalize(argument.name()))).
addStatement("return $L", argument.name()).
returns(argument.typeName()).
addModifiers(Modifier.PUBLIC).build();
classBuilder.addMethod(getter);
}
// add open, openForResult, create methods
MethodSpec openMethod = generateOpenMethod(false);
MethodSpec openForResultMethod = generateOpenMethod(true);
MethodSpec createIntentMethod = generateToIntentMethod(activitySimpleName, requiredArguments, optionalArguments);
classBuilder.addMethod(openMethod).
addMethod(openForResultMethod).
addMethod(createIntentMethod);
// add inject method if needed
if (!requiredArguments.isEmpty() || !optionalArguments.isEmpty()) {
MethodSpec injectMethod = generateInjectMethod(annotatedClassElement, Iterables.concat(requiredArguments, optionalArguments));
classBuilder.addMethod(injectMethod);
}
if (!requiredArguments.isEmpty()) {
MethodSpec checkArgumentsMethod = generateCheckArgumentsMethod(requiredArguments);
classBuilder.addMethod(checkArgumentsMethod);
}
// write class to file
TypeSpec screenClass = classBuilder.addModifiers(Modifier.PUBLIC, Modifier.FINAL).build();
JavaFile javaFile = JavaFile.builder(packageName, screenClass).indent(" ").build();
javaFile.writeTo(filer);
}
}
private MethodSpec generateOpenMethod(boolean forResult) {
MethodSpec.Builder openMethodBuilder = MethodSpec.methodBuilder(forResult ? "openForResult" : "open").
addModifiers(Modifier.PUBLIC).
returns(void.class).
addParameter(activityClassName, "activity");
openMethodBuilder.addStatement("Intent intent = toIntent(activity)");
if (forResult) {
openMethodBuilder.addParameter(TypeName.INT, "requestCode");
openMethodBuilder.addStatement("activity.startActivityForResult(intent, requestCode)");
} else {
openMethodBuilder.addStatement("activity.startActivity(intent)");
}
return openMethodBuilder.build();
}
private MethodSpec generateToIntentMethod(Name activitySimpleName, List<Argument> requiredArguments, List<Argument> optionalArguments) {
MethodSpec.Builder createIntentBuilder = MethodSpec.methodBuilder("toIntent").
addModifiers(Modifier.PUBLIC).
addParameter(activityClassName, "activity").
returns(intentClassName);
createIntentBuilder.addStatement("$T intent = new $T(activity, $L.class)", intentClassName, intentClassName, activitySimpleName);
if (!requiredArguments.isEmpty() || !optionalArguments.isEmpty()) {
createIntentBuilder.addStatement("$T bundle = new $T()", bundleClassName, bundleClassName);
}
for (Argument argument : requiredArguments) {
argument.generatePutMethod(createIntentBuilder);
}
for (Argument argument : optionalArguments) {
boolean primitive = argument.typeName().isPrimitive();
if (!primitive)
createIntentBuilder.beginControlFlow("if ($L != null)", argument.name());
argument.generatePutMethod(createIntentBuilder);
if (!primitive)
createIntentBuilder.endControlFlow();
}
if (!requiredArguments.isEmpty() || !optionalArguments.isEmpty()) {
createIntentBuilder.addStatement("intent.putExtras(bundle)");
}
createIntentBuilder.addStatement("return intent");
return createIntentBuilder.build();
}
private MethodSpec generateCheckArgumentsMethod(List<Argument> requiredArguments) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("checkArguments").
addModifiers(Modifier.PRIVATE, Modifier.STATIC).
addParameter(bundleClassName, "bundle").
returns(void.class);
ClassName exception = ClassName.get(IllegalStateException.class);
for (Argument argument : requiredArguments) {
String key = argument.key();
builder.beginControlFlow("if (!bundle.containsKey($S))", key);
builder.addStatement("throw new $T(\"Required argument $L with key '$L' is not set\")", exception, argument.name(), key);
builder.endControlFlow();
}
return builder.build();
}
private MethodSpec generateInjectMethod(TypeElement annotatedClassElement, Iterable<Argument> arguments) {
TypeName activityTypeName = TypeName.get(annotatedClassElement.asType());
MethodSpec.Builder injectBuilder = MethodSpec.methodBuilder("inject").
addModifiers(Modifier.PUBLIC, Modifier.STATIC).
addParameter(activityTypeName, "activity").
returns(void.class);
ClassName npe = ClassName.get(NullPointerException.class);
injectBuilder.addStatement("$T bundle = activity.getIntent().getExtras()", bundleClassName);
injectBuilder.beginControlFlow("if (bundle == null)");
injectBuilder.addStatement("throw new $T(\"$T has empty Bundle. Use open() or openForResult() to launch activity.\")", npe, activityTypeName);
injectBuilder.endControlFlow();
injectBuilder.addStatement("checkArguments(bundle)");
for (Argument argument : arguments) {
argument.generateGetMethod(injectBuilder);
}
return injectBuilder.build();
}
}