/* * 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); } }