/*
* Copyright (C) 2015 The Android Open Source Project
*
* 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 android.databinding.annotationprocessor;
import android.databinding.BindingAdapter;
import android.databinding.BindingBuildInfo;
import android.databinding.BindingConversion;
import android.databinding.BindingMethod;
import android.databinding.BindingMethods;
import android.databinding.Untaggable;
import android.databinding.tool.reflection.ModelAnalyzer;
import android.databinding.tool.store.SetterStore;
import android.databinding.tool.util.L;
import android.databinding.tool.util.Preconditions;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
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.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.MirroredTypeException;
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;
public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep {
public ProcessMethodAdapters() {
}
@Override
public boolean onHandleStep(RoundEnvironment roundEnv,
ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) {
L.d("processing adapters");
final ModelAnalyzer modelAnalyzer = ModelAnalyzer.getInstance();
Preconditions.checkNotNull(modelAnalyzer, "Model analyzer should be"
+ " initialized first");
SetterStore store = SetterStore.get(modelAnalyzer);
clearIncrementalClasses(roundEnv, store);
addBindingAdapters(roundEnv, processingEnvironment, store);
addRenamed(roundEnv, processingEnvironment, store);
addConversions(roundEnv, processingEnvironment, store);
addUntaggable(roundEnv, processingEnvironment, store);
try {
store.write(buildInfo.modulePackage(), processingEnvironment);
} catch (IOException e) {
L.e(e, "Could not write BindingAdapter intermediate file.");
}
return true;
}
@Override
public void onProcessingOver(RoundEnvironment roundEnvironment,
ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) {
}
private void addBindingAdapters(RoundEnvironment roundEnv, ProcessingEnvironment
processingEnv, SetterStore store) {
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingAdapter.class)) {
if (element.getKind() != ElementKind.METHOD ||
!element.getModifiers().contains(Modifier.PUBLIC)) {
L.e("@BindingAdapter on invalid element: %s", element);
continue;
}
BindingAdapter bindingAdapter = element.getAnnotation(BindingAdapter.class);
ExecutableElement executableElement = (ExecutableElement) element;
List<? extends VariableElement> parameters = executableElement.getParameters();
if (bindingAdapter.value().length == 0) {
L.e("@BindingAdapter requires at least one attribute. %s", element);
continue;
}
final boolean takesComponent = takesComponent(executableElement, processingEnv);
final int startIndex = 1 + (takesComponent ? 1 : 0);
final int numAttributes = bindingAdapter.value().length;
final int numAdditionalArgs = parameters.size() - startIndex;
if (numAdditionalArgs == (2 * numAttributes)) {
// This BindingAdapter takes old and new values. Make sure they are properly ordered
Types typeUtils = processingEnv.getTypeUtils();
boolean hasParameterError = false;
for (int i = startIndex; i < numAttributes + startIndex; i++) {
if (!typeUtils.isSameType(parameters.get(i).asType(),
parameters.get(i + numAttributes).asType())) {
L.e("BindingAdapter %s: old values should be followed by new values. " +
"Parameter %d must be the same type as parameter %d.",
executableElement, i + 1, i + numAttributes + 1);
hasParameterError = true;
break;
}
}
if (hasParameterError) {
continue;
}
} else if (numAdditionalArgs != numAttributes) {
L.e("@BindingAdapter %s has %d attributes and %d value parameters. There should " +
"be %d or %d value parameters.", executableElement, numAttributes,
numAdditionalArgs, numAttributes, numAttributes * 2);
continue;
}
warnAttributeNamespaces(bindingAdapter.value());
try {
if (numAttributes == 1) {
final String attribute = bindingAdapter.value()[0];
L.d("------------------ @BindingAdapter for %s", element);
store.addBindingAdapter(processingEnv, attribute, executableElement,
takesComponent);
} else {
store.addBindingAdapter(processingEnv, bindingAdapter.value(),
executableElement, takesComponent);
}
} catch (IllegalArgumentException e) {
L.e(e, "@BindingAdapter for duplicate View and parameter type: %s", element);
}
}
}
private static boolean takesComponent(ExecutableElement executableElement,
ProcessingEnvironment processingEnvironment) {
List<? extends VariableElement> parameters = executableElement.getParameters();
Elements elementUtils = processingEnvironment.getElementUtils();
TypeMirror viewElement = elementUtils.getTypeElement("android.view.View").asType();
if (parameters.size() < 2) {
return false; // Validation will fail in the caller
}
TypeMirror parameter1 = parameters.get(0).asType();
Types typeUtils = processingEnvironment.getTypeUtils();
if (parameter1.getKind() == TypeKind.DECLARED &&
typeUtils.isAssignable(parameter1, viewElement)) {
return false; // first parameter is a View
}
if (parameters.size() < 3) {
TypeMirror viewStubProxy = elementUtils.
getTypeElement("android.databinding.ViewStubProxy").asType();
if (!typeUtils.isAssignable(parameter1, viewStubProxy)) {
L.e("@BindingAdapter %s is applied to a method that has two parameters, the " +
"first must be a View type", executableElement);
}
return false;
}
TypeMirror parameter2 = parameters.get(1).asType();
if (typeUtils.isAssignable(parameter2, viewElement)) {
return true; // second parameter is a View
}
L.e("@BindingAdapter %s is applied to a method that doesn't take a View subclass as the " +
"first or second parameter. When a BindingAdapter uses a DataBindingComponent, " +
"the component parameter is first and the View parameter is second, otherwise " +
"the View parameter is first.", executableElement);
return false;
}
private static void warnAttributeNamespace(String attribute) {
if (attribute.contains(":") && !attribute.startsWith("android:")) {
L.w("Application namespace for attribute %s will be ignored.", attribute);
}
}
private static void warnAttributeNamespaces(String[] attributes) {
for (String attribute : attributes) {
warnAttributeNamespace(attribute);
}
}
private void addRenamed(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv,
SetterStore store) {
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingMethods.class)) {
BindingMethods bindingMethods = element.getAnnotation(BindingMethods.class);
for (BindingMethod bindingMethod : bindingMethods.value()) {
final String attribute = bindingMethod.attribute();
final String method = bindingMethod.method();
warnAttributeNamespace(attribute);
String type;
try {
type = bindingMethod.type().getCanonicalName();
} catch (MirroredTypeException e) {
type = e.getTypeMirror().toString();
}
store.addRenamedMethod(attribute, type, method, (TypeElement) element);
}
}
}
private void addConversions(RoundEnvironment roundEnv,
ProcessingEnvironment processingEnv, SetterStore store) {
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingConversion.class)) {
if (element.getKind() != ElementKind.METHOD ||
!element.getModifiers().contains(Modifier.STATIC) ||
!element.getModifiers().contains(Modifier.PUBLIC)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BindingConversion is only allowed on public static methods: " + element);
continue;
}
ExecutableElement executableElement = (ExecutableElement) element;
if (executableElement.getParameters().size() != 1) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BindingConversion method should have one parameter: " + element);
continue;
}
if (executableElement.getReturnType().getKind() == TypeKind.VOID) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"@BindingConversion method must return a value: " + element);
continue;
}
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE,
"added conversion: " + element);
store.addConversionMethod(executableElement);
}
}
private void addUntaggable(RoundEnvironment roundEnv,
ProcessingEnvironment processingEnv, SetterStore store) {
for (Element element : AnnotationUtil.getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
Untaggable untaggable = element.getAnnotation(Untaggable.class);
store.addUntaggableTypes(untaggable.value(), (TypeElement) element);
}
}
private void clearIncrementalClasses(RoundEnvironment roundEnv, SetterStore store) {
HashSet<String> classes = new HashSet<String>();
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingAdapter.class)) {
TypeElement containingClass = (TypeElement) element.getEnclosingElement();
classes.add(containingClass.getQualifiedName().toString());
}
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingMethods.class)) {
classes.add(((TypeElement) element).getQualifiedName().toString());
}
for (Element element : AnnotationUtil
.getElementsAnnotatedWith(roundEnv, BindingConversion.class)) {
classes.add(((TypeElement) element.getEnclosingElement()).getQualifiedName().
toString());
}
for (Element element : AnnotationUtil.getElementsAnnotatedWith(roundEnv, Untaggable.class)) {
classes.add(((TypeElement) element).getQualifiedName().toString());
}
store.clear(classes);
}
}