blob: d96e5da0bcdbff3cace9f7b0c5aa60421e0fc57b [file] [log] [blame]
/*
* 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.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.
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;
}
}
}