/* * Copyright 2016 Google Inc. * * 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. */ package com.google.android.libraries.remixer.annotation.processor; import com.google.android.libraries.remixer.Callback; import com.google.android.libraries.remixer.DataType; import com.google.android.libraries.remixer.Remixer; import com.google.android.libraries.remixer.Variable; import com.google.common.base.Strings; import com.squareup.javapoet.ClassName; import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.MethodSpec; import com.squareup.javapoet.ParameterSpec; import com.squareup.javapoet.ParameterizedTypeName; import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; import java.util.Locale; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; import javax.lang.model.element.TypeElement; /** * A method annotated by one of the *Method annotation. * * <p>Any *Method annotation must have a corresponding subclass of this class and it must * implement {@link #getKey()}, {@link #getVariableType()}, and * {@link #addSetupStatements(MethodSpec.Builder)}. */ abstract class MethodAnnotation { /** * Suffix to append to a variable name to hold the callback to be used by the variable. */ static final String CALLBACK_NAME_SUFFIX = "_callback"; /** * Suffix to append to a variable that holds the generated variable. */ static final String REMIXER_ITEM_SUFFIX = "_remixer_item"; /** * Statement to create the callback variable. * * <p>Would expand to something in the form of {@code CallbackType callbackVariable = new * CallbackType(activity)} */ static final String NEW_CALLBACK_STATEMENT = "$L $L = new $L(activity)"; /** * Statement to initialize a variable after it's been constructed. */ static final String INIT_VARIABLE_STATEMENT = "$L.init()"; /** * Statement to add a variable to the current remixer. */ static final String ADD_VARIABLE_STATEMENT = "remixer.addItem($L)"; protected static final String ACTIVITY_NAME = "activity"; /** * The element where the annotation was found. */ final ExecutableElement sourceMethod; /** * The type element which contains the annotated method. */ private final TypeElement sourceClass; /** * The data type this RemixerItem has. */ protected final DataType dataType; /** * The key for this Variable. If the annotation has an empty key then it uses the source method's * name as key. */ protected final String key; /** * Title to display for this variable. If empty it will fall back to the key. */ protected final String title; /** * The layoutId to pass to the constructor of the variable. This will be used to inflate the UI. */ final int layoutId; /** * The builder type to instantiate for this method annotation. */ protected final TypeName builderType; /** * Name of the variable holding the callback corresponding to this MethodAnnotation in the * generated setup code. */ protected final String callbackName; /** * Name of the variable holding the RemixerItem builder/instance corresponding to this * MethodAnnotation in the generated setup code. */ protected final String remixerItemName; /** * The name of the class to generate. */ String generatedClassName; /** * The name of the class refered to by {@code sourceClass}, that is, the class where that contains * the annotated method. */ private final TypeName sourceClassName; /** * A FieldSpec for a field in the generated class that will contain the current activity. */ private final FieldSpec activityField; MethodAnnotation( TypeElement sourceClass, ExecutableElement sourceMethod, DataType dataType, TypeName builderType, String key, String title, int layoutId) throws RemixerAnnotationException { this.sourceClass = sourceClass; this.sourceMethod = sourceMethod; this.dataType = dataType; this.builderType = builderType; this.key = Strings.isNullOrEmpty(key) ? sourceMethod.getSimpleName().toString() : key; key = this.key; if (!KeyChecker.isValidKey(key)) { throw new RemixerAnnotationException(sourceMethod, "Invalid key used, " + key); } this.title = Strings.isNullOrEmpty(title) ? key : title; this.layoutId = layoutId; sourceClassName = ClassName.get(sourceClass); activityField = FieldSpec.builder( sourceClassName, "activity", Modifier.PRIVATE, Modifier.FINAL).build(); generatedClassName = String.format(Locale.getDefault(), "Generated_%s", key); callbackName = key + CALLBACK_NAME_SUFFIX; remixerItemName = key + REMIXER_ITEM_SUFFIX; } public String getKey() { return key; } ExecutableElement getSourceMethod() { return sourceMethod; } private void createBuilder(MethodSpec.Builder methodBuilder) { // Create the callback variable. methodBuilder.addStatement( "$L $L = new $L(activity)", generatedClassName, callbackName, generatedClassName); // Create the builder and start filling common things. methodBuilder.addStatement("$T $L = new $T()", builderType, remixerItemName, builderType); methodBuilder.addStatement("$L.setKey($S)", remixerItemName, key); methodBuilder.addStatement("$L.setTitle($S)", remixerItemName, title); methodBuilder.addStatement("$L.setLayoutId($L)", remixerItemName, layoutId); methodBuilder.addStatement("$L.setContext(activity)", remixerItemName); methodBuilder.addStatement("$L.setCallback($L)", remixerItemName, callbackName); } /** * Adds all the code statements necessary to initialize a {@link RemixerItem} that corresponds to * the annotation. * * @param methodBuilder A Method builder has an instance of {@link Remixer} called * {@code remixer} and a context object called {@code activity}. This builder corresponds to a * method in the class that will contain the class generated by * {@link #generateCallbackClass()}. */ final void addSetupStatements(MethodSpec.Builder methodBuilder) { // Create the callback variable. methodBuilder.addStatement( "$L $L = new $L(activity)", generatedClassName, callbackName, generatedClassName); // Create the builder and start filling common things. methodBuilder.addStatement("$T $L = new $T()", builderType, remixerItemName, builderType); // Since the data type passed during annotation is a compile-time version, and the runtime // version may be different, get it at runtime using the registered data types for the Remixer // instance. methodBuilder.addStatement( "$L.setDataType($T.getInstance().getDataType($S))", remixerItemName, ClassName.get(Remixer.class), dataType.getName()); methodBuilder.addStatement("$L.setKey($S)", remixerItemName, key); methodBuilder.addStatement("$L.setTitle($S)", remixerItemName, title); methodBuilder.addStatement("$L.setLayoutId($L)", remixerItemName, layoutId); methodBuilder.addStatement("$L.setContext(activity)", remixerItemName); methodBuilder.addStatement("$L.setCallback($L)", remixerItemName, callbackName); addSpecificSetupStatements(methodBuilder); methodBuilder.addStatement("remixer.addItem($L.build())", remixerItemName); } /** * Adds all the statements necessary to initialize the {@link RemixerItem} that are specific to * the concrete subclass of {@code RemixerItem} this {@code MethodAnnotation} generates. * @param methodBuilder A Method builder has an instance of {@link Remixer} called * {@code remixer} and a context object called {@code activity}. It also has a * {@link com.google.android.libraries.remixer.RemixerItem.Builder} called * {@code remixerItemName} that corresponds to this {@code MethodAnnotation}. This method * builder corresponds to a method in the class that will contain the class generated by * {@link #generateCallbackClass()}. */ protected abstract void addSpecificSetupStatements(MethodSpec.Builder methodBuilder); /** * Generates a class named {@code generatedClassName} which is an implementation of * {@code Callback} that calls the {@code sourceMethod} on the activity. */ TypeSpec generateCallbackClass() { MethodSpec method = getCallbackMethodSpec(); MethodSpec constructor = MethodSpec.constructorBuilder() .addParameter(sourceClassName, "activity") .addStatement("this.$N = $N", "activity", "activity").build(); return TypeSpec.classBuilder(generatedClassName) .addSuperinterface(getCallbackSuperinterface()) .addField(activityField) .addModifiers(Modifier.STATIC) .addMethod(constructor) .addMethod(method) .build(); } TypeElement getSourceClass() { return sourceClass; } /** * Returns the type name for the interface to implement on the callback class. */ protected TypeName getCallbackSuperinterface() { return ParameterizedTypeName.get( ClassName.get(Callback.class), getVariableType()); } /** * Generates the parameter spec for the {@link Callback#onValueSet} implementation * generated by this annotation. This parameter is of type {@code Variable<Type>}. */ private ParameterSpec getVariableParameterSpec() { return ParameterSpec.builder( ParameterizedTypeName.get(ClassName.get(Variable.class), getVariableType()), "variable") .build(); } /** * Generates the method spec for the implementation of {@link Callback#onValueSet}. */ protected MethodSpec getCallbackMethodSpec() { return MethodSpec.methodBuilder("onValueSet") .addModifiers(Modifier.PUBLIC) .addAnnotation(Override.class) .returns(void.class) .addParameter(getVariableParameterSpec()) .addStatement("activity.$L(variable.getSelectedValue())", sourceMethod.getSimpleName()) .build(); } /** * Returns the type to use to parametrize the Variable objects when generating code. */ private final TypeName getVariableType() { return ClassName.get(dataType.getRuntimeType()); } }