/* * 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.Bindable; import android.databinding.BindingBuildInfo; import android.databinding.tool.CompilerChef.BindableHolder; import android.databinding.tool.util.GenerationalClassUtil; import android.databinding.tool.util.L; import android.databinding.tool.util.Preconditions; import android.databinding.tool.writer.BRWriter; import android.databinding.tool.writer.JavaFileWriter; import java.io.Serializable; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Set; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; import javax.lang.model.type.TypeKind; import javax.lang.model.util.Types; // binding app info and library info are necessary to trigger this. @SupportedSourceVersion(SourceVersion.RELEASE_7) public class ProcessBindable extends ProcessDataBinding.ProcessingStep implements BindableHolder { Intermediate mProperties; HashMap<String, HashSet<String>> mLayoutVariables = new HashMap<String, HashSet<String>>(); @Override public boolean onHandleStep(RoundEnvironment roundEnv, ProcessingEnvironment processingEnv, BindingBuildInfo buildInfo) { if (mProperties == null) { mProperties = new IntermediateV1(buildInfo.modulePackage()); mergeLayoutVariables(); mLayoutVariables.clear(); TypeElement observableType = processingEnv.getElementUtils(). getTypeElement("android.databinding.Observable"); Types typeUtils = processingEnv.getTypeUtils(); for (Element element : AnnotationUtil .getElementsAnnotatedWith(roundEnv, Bindable.class)) { Element enclosingElement = element.getEnclosingElement(); ElementKind kind = enclosingElement.getKind(); if (kind != ElementKind.CLASS && kind != ElementKind.INTERFACE) { L.e("Bindable must be on a member field or method. The enclosing type is %s", enclosingElement.getKind()); } TypeElement enclosing = (TypeElement) enclosingElement; if (!typeUtils.isAssignable(enclosing.asType(), observableType.asType())) { L.e("Bindable must be on a member in an Observable class. %s is not Observable", enclosingElement.getSimpleName()); } String name = getPropertyName(element); if (name != null) { Preconditions .checkNotNull(mProperties, "Must receive app / library info before " + "Bindable fields."); mProperties.addProperty(enclosing.getQualifiedName().toString(), name); } } GenerationalClassUtil.writeIntermediateFile(processingEnv, mProperties.getPackage(), createIntermediateFileName(mProperties.getPackage()), mProperties); generateBRClasses(!buildInfo.isLibrary(), mProperties.getPackage()); } return false; } @Override public void addVariable(String variableName, String containingClassName) { HashSet<String> variableNames = mLayoutVariables.get(containingClassName); if (variableNames == null) { variableNames = new HashSet<String>(); mLayoutVariables.put(containingClassName, variableNames); } variableNames.add(variableName); } @Override public void onProcessingOver(RoundEnvironment roundEnvironment, ProcessingEnvironment processingEnvironment, BindingBuildInfo buildInfo) { } private String createIntermediateFileName(String appPkg) { return appPkg + GenerationalClassUtil.ExtensionFilter.BR.getExtension(); } private void generateBRClasses(boolean useFinalFields, String pkg) { L.d("************* Generating BR file %s. use final: %s", pkg, useFinalFields); HashSet<String> properties = new HashSet<String>(); mProperties.captureProperties(properties); List<Intermediate> previousIntermediates = loadPreviousBRFiles(); for (Intermediate intermediate : previousIntermediates) { intermediate.captureProperties(properties); } final JavaFileWriter writer = getWriter(); BRWriter brWriter = new BRWriter(properties, useFinalFields); writer.writeToFile(pkg + ".BR", brWriter.write(pkg)); //writeBRClass(useFinalFields, pkg, properties); if (useFinalFields) { // generate BR for all previous packages for (Intermediate intermediate : previousIntermediates) { writer.writeToFile(intermediate.getPackage() + ".BR", brWriter.write(intermediate.getPackage())); } } mCallback.onBrWriterReady(brWriter); } private String getPropertyName(Element element) { switch (element.getKind()) { case FIELD: return stripPrefixFromField((VariableElement) element); case METHOD: return stripPrefixFromMethod((ExecutableElement) element); default: L.e("@Bindable is not allowed on %s", element.getKind()); return null; } } private static String stripPrefixFromField(VariableElement element) { Name name = element.getSimpleName(); if (name.length() >= 2) { char firstChar = name.charAt(0); char secondChar = name.charAt(1); if (name.length() > 2 && firstChar == 'm' && secondChar == '_') { char thirdChar = name.charAt(2); if (Character.isJavaIdentifierStart(thirdChar)) { return "" + Character.toLowerCase(thirdChar) + name.subSequence(3, name.length()); } } else if ((firstChar == 'm' && Character.isUpperCase(secondChar)) || (firstChar == '_' && Character.isJavaIdentifierStart(secondChar))) { return "" + Character.toLowerCase(secondChar) + name.subSequence(2, name.length()); } } return name.toString(); } private String stripPrefixFromMethod(ExecutableElement element) { Name name = element.getSimpleName(); CharSequence propertyName; if (isGetter(element) || isSetter(element)) { propertyName = name.subSequence(3, name.length()); } else if (isBooleanGetter(element)) { propertyName = name.subSequence(2, name.length()); } else { L.e("@Bindable associated with method must follow JavaBeans convention %s", element); return null; } char firstChar = propertyName.charAt(0); return "" + Character.toLowerCase(firstChar) + propertyName.subSequence(1, propertyName.length()); } private void mergeLayoutVariables() { for (String containingClass : mLayoutVariables.keySet()) { for (String variable : mLayoutVariables.get(containingClass)) { mProperties.addProperty(containingClass, variable); } } } private static boolean prefixes(CharSequence sequence, String prefix) { boolean prefixes = false; if (sequence.length() > prefix.length()) { int count = prefix.length(); prefixes = true; for (int i = 0; i < count; i++) { if (sequence.charAt(i) != prefix.charAt(i)) { prefixes = false; break; } } } return prefixes; } private static boolean isGetter(ExecutableElement element) { Name name = element.getSimpleName(); return prefixes(name, "get") && Character.isJavaIdentifierStart(name.charAt(3)) && element.getParameters().isEmpty() && element.getReturnType().getKind() != TypeKind.VOID; } private static boolean isSetter(ExecutableElement element) { Name name = element.getSimpleName(); return prefixes(name, "set") && Character.isJavaIdentifierStart(name.charAt(3)) && element.getParameters().size() == 1 && element.getReturnType().getKind() == TypeKind.VOID; } private static boolean isBooleanGetter(ExecutableElement element) { Name name = element.getSimpleName(); return prefixes(name, "is") && Character.isJavaIdentifierStart(name.charAt(2)) && element.getParameters().isEmpty() && element.getReturnType().getKind() == TypeKind.BOOLEAN; } private List<Intermediate> loadPreviousBRFiles() { return GenerationalClassUtil .loadObjects(GenerationalClassUtil.ExtensionFilter.BR); } private interface Intermediate extends Serializable { void captureProperties(Set<String> properties); void addProperty(String className, String propertyName); boolean hasValues(); String getPackage(); } private static class IntermediateV1 implements Serializable, Intermediate { private static final long serialVersionUID = 2L; private String mPackage; private final HashMap<String, HashSet<String>> mProperties = new HashMap<String, HashSet<String>>(); public IntermediateV1(String aPackage) { mPackage = aPackage; } @Override public void captureProperties(Set<String> properties) { for (HashSet<String> propertySet : mProperties.values()) { properties.addAll(propertySet); } } @Override public void addProperty(String className, String propertyName) { HashSet<String> properties = mProperties.get(className); if (properties == null) { properties = new HashSet<String>(); mProperties.put(className, properties); } properties.add(propertyName); } @Override public boolean hasValues() { return !mProperties.isEmpty(); } @Override public String getPackage() { return mPackage; } } }