| /* |
| * 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.InverseBindingAdapter; |
| import android.databinding.InverseBindingMethod; |
| import android.databinding.InverseBindingMethods; |
| 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; |
| |
| public class ProcessMethodAdapters extends ProcessDataBinding.ProcessingStep { |
| private final static String INVERSE_BINDING_EVENT_ATTR_SUFFIX = "AttrChanged"; |
| |
| 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, store); |
| addConversions(roundEnv, store); |
| addUntaggable(roundEnv, store); |
| addInverseAdapters(roundEnv, processingEnvironment, store); |
| addInverseMethods(roundEnv, 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(element, "@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(element, "@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(executableElement, "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(element, "@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(element, bindingAdapter.value()); |
| try { |
| if (numAttributes == 1) { |
| final String attribute = bindingAdapter.value()[0]; |
| store.addBindingAdapter(processingEnv, attribute, executableElement, |
| takesComponent); |
| } else { |
| store.addBindingAdapter(processingEnv, bindingAdapter.value(), |
| executableElement, takesComponent, bindingAdapter.requireAll()); |
| } |
| } catch (IllegalArgumentException e) { |
| L.e(element, "@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(executableElement, "@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(executableElement, "@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(Element element, String attribute) { |
| if (attribute.contains(":") && !attribute.startsWith("android:")) { |
| L.w(element, "Application namespace for attribute %s will be ignored.", attribute); |
| } |
| } |
| |
| private static void warnAttributeNamespaces(Element element, String[] attributes) { |
| for (String attribute : attributes) { |
| warnAttributeNamespace(element, attribute); |
| } |
| } |
| |
| private void addRenamed(RoundEnvironment roundEnv, 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(element, 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, 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)) { |
| L.e(element, "@BindingConversion is only allowed on public static methods %s", |
| element); |
| continue; |
| } |
| |
| ExecutableElement executableElement = (ExecutableElement) element; |
| if (executableElement.getParameters().size() != 1) { |
| L.e(element, "@BindingConversion method should have one parameter %s", element); |
| continue; |
| } |
| if (executableElement.getReturnType().getKind() == TypeKind.VOID) { |
| L.e(element, "@BindingConversion method must return a value %s", element); |
| continue; |
| } |
| store.addConversionMethod(executableElement); |
| } |
| } |
| |
| private void addInverseAdapters(RoundEnvironment roundEnv, |
| ProcessingEnvironment processingEnv, SetterStore store) { |
| for (Element element : AnnotationUtil |
| .getElementsAnnotatedWith(roundEnv, InverseBindingAdapter.class)) { |
| if (!element.getModifiers().contains(Modifier.PUBLIC)) { |
| L.e(element, "@InverseBindingAdapter must be associated with a public method"); |
| continue; |
| } |
| ExecutableElement executableElement = (ExecutableElement) element; |
| if (executableElement.getReturnType().getKind() == TypeKind.VOID) { |
| L.e(element, "@InverseBindingAdapter must have a non-void return type"); |
| continue; |
| } |
| final InverseBindingAdapter inverseBindingAdapter = |
| executableElement.getAnnotation(InverseBindingAdapter.class); |
| final String attribute = inverseBindingAdapter.attribute(); |
| warnAttributeNamespace(element, attribute); |
| final String event = inverseBindingAdapter.event().isEmpty() |
| ? inverseBindingAdapter.attribute() + INVERSE_BINDING_EVENT_ATTR_SUFFIX |
| : inverseBindingAdapter.event(); |
| warnAttributeNamespace(element, event); |
| final boolean takesComponent = takesComponent(executableElement, processingEnv); |
| final int expectedArgs = takesComponent ? 2 : 1; |
| final int numParameters = executableElement.getParameters().size(); |
| if (numParameters != expectedArgs) { |
| L.e(element, "@InverseBindingAdapter %s takes %s parameters, but %s parameters " + |
| "were expected", element, numParameters, expectedArgs); |
| continue; |
| } |
| try { |
| store.addInverseAdapter(processingEnv, attribute, event, executableElement, |
| takesComponent); |
| } catch (IllegalArgumentException e) { |
| L.e(element, "@InverseBindingAdapter for duplicate View and parameter type: %s", |
| element); |
| } |
| } |
| } |
| |
| private void addInverseMethods(RoundEnvironment roundEnv, SetterStore store) { |
| for (Element element : AnnotationUtil |
| .getElementsAnnotatedWith(roundEnv, InverseBindingMethods.class)) { |
| InverseBindingMethods bindingMethods = |
| element.getAnnotation(InverseBindingMethods.class); |
| |
| for (InverseBindingMethod bindingMethod : bindingMethods.value()) { |
| final String attribute = bindingMethod.attribute(); |
| final String method = bindingMethod.method(); |
| final String event = bindingMethod.event().isEmpty() |
| ? bindingMethod.attribute() + INVERSE_BINDING_EVENT_ATTR_SUFFIX |
| : bindingMethod.event(); |
| warnAttributeNamespace(element, attribute); |
| warnAttributeNamespace(element, event); |
| String type; |
| try { |
| type = bindingMethod.type().getCanonicalName(); |
| } catch (MirroredTypeException e) { |
| type = e.getTypeMirror().toString(); |
| } |
| store.addInverseMethod(attribute, event, type, method, (TypeElement) element); |
| } |
| } |
| } |
| |
| private void addUntaggable(RoundEnvironment roundEnv, 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); |
| } |
| } |