package com.sora.util.akatsuki;
import static com.sora.util.akatsuki.SourceUtils.T;
import static com.sora.util.akatsuki.SourceUtils.var;
import java.io.IOException;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.processing.Filer;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import com.google.common.base.Strings;
import com.sora.util.akatsuki.ArgConcludingBuilder.VoidBuilder;
import com.sora.util.akatsuki.ArgConfig.BuilderType.Check;
import com.sora.util.akatsuki.ArgConfig.Order;
import com.sora.util.akatsuki.ArgConfig.Sort;
import com.sora.util.akatsuki.BundleContext.SimpleBundleContext;
import com.sora.util.akatsuki.BundleRetainerClassBuilder.Direction;
import com.sora.util.akatsuki.Internal.ClassArgBuilder;
import com.sora.util.akatsuki.analyzers.CascadingTypeAnalyzer;
import com.sora.util.akatsuki.analyzers.CascadingTypeAnalyzer.Analysis;
import com.sora.util.akatsuki.analyzers.CascadingTypeAnalyzer.InvocationType;
import com.sora.util.akatsuki.analyzers.Element;
import com.sora.util.akatsuki.models.ClassInfo;
import com.sora.util.akatsuki.models.FieldModel;
import com.sora.util.akatsuki.models.SourceClassModel;
import com.sora.util.akatsuki.models.SourceMappingModel;
import com.sora.util.akatsuki.models.SourceTreeModel;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeName;
import com.squareup.javapoet.TypeSpec;
import com.squareup.javapoet.TypeSpec.Builder;
import com.squareup.javapoet.TypeVariableName;
// Spec: every package has it's own Builder...
class ArgumentBuilderModel extends SourceMappingModel {
public static final Function<String, String> BUILDER_NAME_FUNCTION = n -> n
+ Internal.BUILDER_CLASS_SUFFIX;
private final Map<TypeMirror, DeclaredType> SUPPORTED_TYPES = new HashMap<>();
private final TypeName bundleTypeName;
private final ClassInfo info;
private final Optional<String> enclosingClass;
protected ArgumentBuilderModel(ProcessorContext context, SourceClassModel classModel,
SourceTreeModel treeModel, Optional<String> enclosingClass) {
super(context, classModel, treeModel);
bundleTypeName = ClassName.get(AndroidTypes.Bundle.asMirror(context));
initializeSupportedTypes();
this.enclosingClass = enclosingClass;
ClassInfo info = classModel.asClassInfo().withNameTransform(BUILDER_NAME_FUNCTION);
this.info = enclosingClass.isPresent() ? info.withEnclosingClasses(enclosingClass.get())
: info;
}
// TODO this is bad (we cannot cache mirrors across rounds, they explode!)
private void initializeSupportedTypes() {
if (SUPPORTED_TYPES.isEmpty()) {
SUPPORTED_TYPES.put(mirror("android.app.Fragment"),
mirror(FragmentConcludingBuilder.class));
SUPPORTED_TYPES.put(mirror("android.support.v4.app.Fragment"),
mirror(FragmentConcludingBuilder.class));
SUPPORTED_TYPES.put(mirror("android.app.Activity"),
mirror(ActivityConcludingBuilder.class));
SUPPORTED_TYPES.put(mirror("android.app.Service"),
mirror(ServiceConcludingBuilder.class));
}
}
private DeclaredType mirror(Class<?> clazz) {
return (DeclaredType) context.utils().of(clazz);
}
private DeclaredType mirror(String className) {
return (DeclaredType) context.utils().of(className);
}
public Optional<TypeSpec> build() {
Optional<SourceClassModel> superModel = classModel()
.directSuperModelWithAnnotation(Arg.class);
if (!classModel().containsAnyAnnotation(Arg.class) && !superModel.isPresent())
return Optional.empty();
return createBuilderForModel(classModel(), superModel).map(Builder::build);
}
public ClassInfo classInfo() {
return info;
}
@Override
public void writeToFile(Filer filer) throws IOException {
if (enclosingClass.isPresent())
throw new RuntimeException(
"Enclosing class exists, please write the enclosing class instead of this inner class");
throw new UnsupportedOperationException("not implemented yet");
}
// private ClassName createBuilderClassName(String modelFqpn, String
// builderName) {
// return ClassName.get(modelFqpn, enclosingClassName, builderName);
// }
private Optional<TypeSpec.Builder> createBuilderForModel(SourceClassModel model,
Optional<SourceClassModel> superModel) {
// get the config, if none, copy the config from parent
ArgConfig config = model.annotation(ArgConfig.class).orElse(
superModel.map(sm -> sm.annotation(ArgConfig.class).orElse(null)).orElse(null));
// if still none, use the global default
if (config == null) {
config = context.config().argConfig();
}
if (!config.enabled())
return Optional.empty();
// our target type's class
ClassName targetTypeName = ClassName.get(model.originatingElement());
// generate builder's name
final TypeSpec.Builder builderTypeBuilder = TypeSpec.classBuilder(info.className)
.addModifiers(Modifier.PUBLIC, Modifier.STATIC);
SimpleBundleContext bundleContext = new SimpleBundleContext("", "bundle");
// filter, sort, and order our fields
Predicate<FieldModel> modelPredicate = fm -> fm.annotation(Arg.class).isPresent()
&& !fm.annotation(Arg.class).get().skip();
Stream<FieldModel> fieldStream = model.fields().stream().filter(modelPredicate);
final Sort sort = config.sort();
final List<FieldModel> fields;
switch (sort) {
case CODE:
// don't touch the list
fields = fieldStream.collect(Collectors.toList());
break;
case RANDOM:
// who uses this anyway?
fields = fieldStream.collect(Collectors.toList());
Collections.shuffle(fields, new Random());
break;
default:
fields = fieldStream.sorted((lhs, rhs) -> {
switch (sort) {
case INDEX:
return Integer.compare(lhs.annotation(Arg.class).get().index(),
rhs.annotation(Arg.class).get().index());
case LEXICOGRAPHICAL:
return lhs.name().compareTo(rhs.name());
default:
throw new AssertionError("Unexpected sort type:" + sort);
}
}).collect(Collectors.toList());
break;
}
BundleRetainerClassBuilder retainerClassBuilder = new BundleRetainerClassBuilder(context,
model, EnumSet.of(Direction.RESTORE), classInfo -> {
ClassInfo info = classInfo.withNameTransform(name -> Internal
.generateRetainerClassName(name + Internal.BUILDER_CLASS_SUFFIX));
if (enclosingClass.isPresent())
info = info.withEnclosingClasses(enclosingClass.get());
info = info.withEnclosingClasses(classInfo().className);
return info;
} , classInfo -> {
String className = classInfo.className;
ClassInfo info = classInfo.withNameTransform(name -> Internal
.generateRetainerClassName(name + Internal.BUILDER_CLASS_SUFFIX));
if (enclosingClass.isPresent())
info = info.withEnclosingClasses(enclosingClass.get());
info = info.withEnclosingClasses(className + Internal.BUILDER_CLASS_SUFFIX);
return info;
}).withFieldPredicate(modelPredicate);
// discard the value so that we don't overwrite them if value also stored with @Retained
retainerClassBuilder.withAnalysisTransformation((context1, direction, element,
analysis) -> analysis.transform(s -> s + ";\n{{bundle}}.remove({{keyName}})\n"));
builderTypeBuilder
.addType(retainerClassBuilder.build().addModifiers(Modifier.STATIC).build());
if (config.order() == Order.DSC)
Collections.reverse(fields);
final Optional<DeclaredType> possibleConcluderType = context.utils()
.getClassFromAnnotationMethod(config::concludingBuilder, VoidBuilder.class);
DeclaredType concluderType = possibleConcluderType.orElse(SUPPORTED_TYPES.keySet().stream()
.filter(m -> context.utils().isAssignable(model.mirror(), m, true)).findFirst()
.map(SUPPORTED_TYPES::get)
.orElseThrow(() -> new RuntimeException(model.fullyQualifiedName()
+ " is not supported directly(detected as " + model.mirror() + ")."
+ " @Arg supports Fragment, Activity and Service natively,"
+ " you may set specify a concludingBuilder in @ArgConfig "
+ "to describe how this class should be built.\n"
+ Utils.listSuperclass(model.mirror()))));
final BuilderCodeGenerator generator;
switch (config.type().returnType) {
case CHAINED:
// chained builder
generator = new ChainedBuilderGenerator();
break;
case VOID:
// pain old builder
generator = new VoidBuilderGenerator();
break;
default:
case SUBCLASSED:
// advanced stuff
generator = new SubclassBuilderGenerator();
break;
}
generator.build(
new PartialModel(config, model.fullyQualifiedPackageName(), info.className,
targetTypeName, concluderType),
builderTypeBuilder, bundleContext, fields, superModel);
return Optional.of(builderTypeBuilder);
}
private static class PartialModel {
final ArgConfig config;
final String builderFqpn;
final String builderSimpleName;
final ClassName targetClassName;
final DeclaredType concludingBuilder;
public PartialModel(ArgConfig config, String builderFqpn, String builderSimpleName,
ClassName targetClassName, DeclaredType concludingBuilder) {
this.config = config;
this.builderFqpn = builderFqpn;
this.builderSimpleName = builderSimpleName;
this.targetClassName = targetClassName;
this.concludingBuilder = concludingBuilder;
}
public ParameterizedTypeName concludingBuilderTypeName() {
return ParameterizedTypeName.get(
ClassName.get((TypeElement) concludingBuilder.asElement()),
TypeVariableName.get("T"));
}
public ClassName get(String name) {
return ClassName.get(builderFqpn, builderSimpleName, name);
}
private void extendConcludingBuilder(ProcessorContext context, Builder builderTypeBuilder,
DeclaredType concludingBuilder) {
if (context.utils().isAssignable(concludingBuilder,
context.utils().of(ClassArgBuilder.class), true)) {
// implement the constructor with class parameters
// TODO that bundle part is redundant... consider improving
builderTypeBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(ClassName.get(AndroidTypes.Bundle.asMirror(context)),
"bundle")
.addCode("super(bundle, $L.class);", targetClassName).build());
} else if (context.utils().isAssignable(concludingBuilder,
context.utils().of(ArgConcludingBuilder.class), true)) {
// this is probably an user implemented one
builderTypeBuilder.addMethod(MethodSpec.constructorBuilder()
.addCode("this.bundle = new Bundle();").build());
} else {
throw new AssertionError(concludingBuilder + " is not supported, @ArgConfig should"
+ " not allow this in the first place...");
}
}
private void extendConcludingBuilder(ProcessorContext context, Builder builderTypeBuilder) {
extendConcludingBuilder(context, builderTypeBuilder, this.concludingBuilder);
}
private void returnNewConcludingBuilder(ProcessorContext context,
MethodSpec.Builder setterBuilder, DeclaredType concludingBuilder) {
setterBuilder.addCode("return new $T($L, $L);", concludingBuilder, "bundle()",
"targetClass()");
setterBuilder.returns(TypeName.get(concludingBuilder));
}
}
private interface BuilderCodeGenerator {
void build(PartialModel model, Builder builderTypeBuilder,
SimpleBundleContext bundleContext, List<FieldModel> fields,
Optional<SourceClassModel> superClass);
}
private class ChainedBuilderGenerator implements BuilderCodeGenerator {
@Override
public void build(PartialModel model, Builder builderTypeBuilder,
SimpleBundleContext bundleContext, List<FieldModel> fields,
Optional<SourceClassModel> superClass) {
// TODO what if out target class has some generic parameter of
// <A,B...>?
TypeName builderClassName = ParameterizedTypeName.get(info.toClassName(), T, var("BT"));
builderTypeBuilder.addTypeVariable(TypeVariableName.get("T", model.targetClassName));
builderTypeBuilder.addTypeVariable(TypeVariableName.get("BT", builderClassName));
if (superClass.isPresent()) {
// implement our parent
ClassInfo superClassInfo = superClass.get().asClassInfo();
if (enclosingClass.isPresent())
superClassInfo = superClassInfo.withEnclosingClasses(enclosingClass.get());
superClassInfo = superClassInfo.withNameTransform(BUILDER_NAME_FUNCTION);
ClassName superClassName = superClassInfo.toClassName();
builderTypeBuilder
.superclass(ParameterizedTypeName.get(superClassName, T, var("BT")));
} else {
// or inherit the class containing our build method
builderTypeBuilder.superclass(model.concludingBuilderTypeName());
}
builderTypeBuilder.addMethod(
MethodSpec.constructorBuilder().addParameter(bundleTypeName, "bundle")
.addCode("super(bundle, $L.class);", model.targetClassName).build());
// if (!superClass.isPresent()) {
// we are the first!
builderTypeBuilder.addMethod(MethodSpec.constructorBuilder()
.addParameter(bundleTypeName, "bundle").addParameter(Class.class, "clazz")
.addCode("super(bundle, clazz);").addModifiers(Modifier.PUBLIC).build())
.build();
// }
CodeBlock.Builder block = CodeBlock.builder();
// enable check if runtime check is required and whether the class
// contains non-optional fields at all
boolean appendCheck = model.config.type().check == Check.RUNTIME
&& fields.stream().anyMatch(f -> !f.annotation(Arg.class).get().optional());
if (appendCheck) {
block.addStatement("$T<$T> missing = new $T<>()", Set.class, String.class,
HashSet.class);
}
for (FieldModel field : fields) {
Arg arg = field.annotation(Arg.class).get();
Element<TypeMirror> element = new Element<>(field);
String setterName = arg.value();
if (Strings.isNullOrEmpty(setterName)) {
setterName = field.name();
// TODO allow and apply field name transform here
}
CascadingTypeAnalyzer<?, ? extends TypeMirror, ? extends Analysis> analyzer = context
.resolver().resolve(element);
Analysis analysis = analyzer.transform(bundleContext, element, InvocationType.SAVE);
MethodSpec.Builder setterBuilder = MethodSpec.methodBuilder(setterName)
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(field.type()), setterName)
.addCode(analysis.emit());
setBuilderReturnSpec(var("BT"), setterBuilder);
builderTypeBuilder.addMethod(setterBuilder.build());
if (appendCheck && !arg.optional()) {
block.addStatement("if(!bundle.containsKey($1S)) missing.add($1S)", setterName);
}
}
if (appendCheck) {
block.addStatement("if(missing.isEmpty()) throw new RuntimeException(\"Some or "
+ "all of the non-optional values \"+missing+\" not set!\")");
MethodSpec.Builder checkMethodBuilder = MethodSpec.methodBuilder("check")
.addModifiers(Modifier.PUBLIC).returns(Void.TYPE).addCode(block.build());
builderTypeBuilder.addMethod(checkMethodBuilder.build());
}
}
public void setBuilderReturnSpec(TypeName self, MethodSpec.Builder builder) {
builder.returns(self).addCode("return ($T)this;", self);
}
}
private class VoidBuilderGenerator extends ChainedBuilderGenerator {
@Override
public void setBuilderReturnSpec(TypeName self, MethodSpec.Builder builder) {
builder.returns(TypeName.VOID);
}
}
private class SubclassBuilderGenerator implements BuilderCodeGenerator {
@Override
public void build(PartialModel model, Builder builderTypeBuilder,
SimpleBundleContext bundleContext, List<FieldModel> fields,
Optional<SourceClassModel> superClass) {
// last field returns a concluding builder
TypeName previousName = model.concludingBuilderTypeName();
// last to first
int last = fields.size() - 1;
for (int i = last; i >= 0; i--) {
FieldModel field = fields.get(i);
Arg arg = field.annotation(Arg.class).get();
Element<TypeMirror> element = new Element<>(field);
String setterName = arg.value();
if (Strings.isNullOrEmpty(setterName)) {
setterName = field.name();
// TODO allow and apply field name transform here
}
CascadingTypeAnalyzer<?, ? extends TypeMirror, ? extends Analysis> analyzer = context
.resolver().resolve(element);
Analysis analysis = analyzer.transform(bundleContext, element, InvocationType.SAVE);
MethodSpec.Builder setterBuilder = MethodSpec.methodBuilder(setterName)
.addModifiers(Modifier.PUBLIC)
.addParameter(ClassName.get(field.type()), setterName)
.addCode(analysis.emit()).returns(previousName);
if (i == 0) {
// entry point is a method
if (i == last) {
setterBuilder.addCode("return new $T($L, $L);", previousName, "bundle()",
"targetClass()");
} else {
setterBuilder.addCode("return new $T();", previousName);
}
builderTypeBuilder.addMethod(setterBuilder.build());
// we're almost done
// if the first field is optional, the whole builder can be
// concluded without any setter, otherwise, implement our
// base arg builder
builderTypeBuilder.superclass(arg.optional() ? model.concludingBuilderTypeName()
: ParameterizedTypeName.get(
ClassName.get(Internal.ClassArgBuilder.class),
model.targetClassName));
model.extendConcludingBuilder(context, builderTypeBuilder);
} else {
// start chaining
Builder builder = TypeSpec
.classBuilder(Utils.toCapitalCase(setterName) + "Builder")
.addModifiers(Modifier.PUBLIC);
if (arg.optional())
builder.superclass(previousName);
if (i == last) {
if (arg.optional()) {
// last optional field builder needs to implement
// concluding
// builder
builder.addMethod(MethodSpec.constructorBuilder()
.addCode("super($1L" + ".this" + ".$2L, " + "$1L.this.$3L);",
model.builderSimpleName, "bundle()", "targetClass()")
.build());
setterBuilder.addCode("return this;");
} else {
// last non optional field builder needs to return a
// new
// concluding builder
setterBuilder.addCode("return new $T($L, $L);", previousName,
"bundle" + "()", "targetClass()");
}
} else {
// everything in between returns the next builder
setterBuilder.addCode("return new $T();", previousName);
}
builder.addMethod(setterBuilder.build());
TypeSpec typeSpec = builder.build();
builderTypeBuilder.addType(typeSpec);
previousName = model.get(typeSpec.name);
}
}
}
}
}