blob: ca40f2e83fbf624634ca343bdbbfd4c4247bc496 [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 com.android.databinding.reflection.annotation;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.android.databinding.reflection.Callable;
import com.android.databinding.reflection.ModelAnalyzer;
import com.android.databinding.reflection.ModelClass;
import com.android.databinding.reflection.ModelField;
import com.android.databinding.reflection.ModelMethod;
import com.android.databinding.reflection.TypeUtil;
import com.android.databinding.util.L;
import org.apache.commons.lang3.StringUtils;
import android.binding.Bindable;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
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.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
public class AnnotationAnalyzer extends ModelAnalyzer {
public static final Map<String, TypeKind> PRIMITIVE_TYPES =
new ImmutableMap.Builder<String, TypeKind>()
.put("boolean", TypeKind.BOOLEAN)
.put("byte", TypeKind.BYTE)
.put("short", TypeKind.SHORT)
.put("char", TypeKind.CHAR)
.put("int", TypeKind.INT)
.put("long", TypeKind.LONG)
.put("float", TypeKind.FLOAT)
.put("double", TypeKind.DOUBLE)
.build();
public final ProcessingEnvironment mProcessingEnv;
private AnnotationClass[] mListTypes;
private AnnotationClass mMapType;
private AnnotationClass mStringType;
private AnnotationClass mObjectType;
private AnnotationClass mObservableType;
private AnnotationClass mObservableListType;
private AnnotationClass mObservableMapType;
private AnnotationClass[] mObservableFieldTypes;
private AnnotationClass mIViewDataBinderType;
public AnnotationAnalyzer(ProcessingEnvironment processingEnvironment) {
mProcessingEnv = processingEnvironment;
setInstance(this);
}
public AnnotationClass[] getListTypes() {
if (mListTypes == null) {
Types typeUtil = getTypeUtils();
mListTypes = new AnnotationClass[LIST_CLASS_NAMES.length];
for (int i = 0; i < mListTypes.length; i++) {
TypeElement typeElement = findType(LIST_CLASS_NAMES[i]);
if (typeElement != null) {
mListTypes[i] = new AnnotationClass(typeUtil.erasure(typeElement.asType()));
}
}
}
return mListTypes;
}
public static AnnotationAnalyzer get() {
return (AnnotationAnalyzer) getInstance();
}
public AnnotationClass getMapType() {
if (mMapType == null) {
mMapType = loadClassErasure(MAP_CLASS_NAME);
}
return mMapType;
}
public AnnotationClass getStringType() {
if (mStringType == null) {
mStringType = new AnnotationClass(findType(STRING_CLASS_NAME).asType());
}
return mStringType;
}
public AnnotationClass getObjectType() {
if (mObjectType == null) {
mObjectType = new AnnotationClass(findType(OBJECT_CLASS_NAME).asType());
}
return mObjectType;
}
private AnnotationClass getObservableType() {
if (mObservableType == null) {
mObservableType = new AnnotationClass(findType(OBSERVABLE_CLASS_NAME).asType());
}
return mObservableType;
}
private AnnotationClass getObservableListType() {
if (mObservableListType == null) {
mObservableListType = loadClassErasure(OBSERVABLE_LIST_CLASS_NAME);
}
return mObservableListType;
}
private AnnotationClass getObservableMapType() {
if (mObservableMapType == null) {
mObservableMapType = loadClassErasure(OBSERVABLE_MAP_CLASS_NAME);
}
return mObservableMapType;
}
private AnnotationClass getIViewDataBinderType() {
if (mIViewDataBinderType == null) {
mIViewDataBinderType = new AnnotationClass(findType(VIEW_DATA_BINDING).asType());
}
return mIViewDataBinderType;
}
private AnnotationClass loadClassErasure(String className) {
Types typeUtils = getTypeUtils();
return new AnnotationClass(typeUtils.erasure(findType(className).asType()));
}
private AnnotationClass[] getObservableFieldTypes() {
if (mObservableFieldTypes == null) {
mObservableFieldTypes = new AnnotationClass[OBSERVABLE_FIELDS.length];
for (int i = 0; i < OBSERVABLE_FIELDS.length; i++) {
mObservableFieldTypes[i] = loadClassErasure(OBSERVABLE_FIELDS[i]);
}
}
return mObservableFieldTypes;
}
private TypeElement findType(String type) {
return mProcessingEnv.getElementUtils().getTypeElement(type);
}
@Override
public boolean isDataBinder(ModelClass modelClass) {
return getIViewDataBinderType().isAssignableFrom(modelClass);
}
@Override
public Callable findMethod(ModelClass modelClass, String name,
List<ModelClass> args, boolean staticAccess) {
AnnotationClass clazz = (AnnotationClass) modelClass;
// TODO implement properly
for (String methodName : new String[]{"set" + StringUtils.capitalize(name), name}) {
for (ModelMethod method : clazz.getMethods(methodName, args.size())) {
if (method.isStatic() == staticAccess) {
ModelClass[] parameters = method.getParameterTypes();
boolean parametersMatch = true;
boolean isVarArgs = ((AnnotationMethod) method).mExecutableElement.isVarArgs();
for (int i = 0; i < parameters.length; i++) {
if (isVarArgs && i == parameters.length - 1) {
ModelClass component = parameters[i].getComponentType();
for (int j = i; j < args.size(); j++) {
if (!component.isAssignableFrom(args.get(j))) {
parametersMatch = false;
break;
}
}
} else if (!parameters[i].isAssignableFrom(args.get(i))) {
parametersMatch = false;
break;
}
}
if (parametersMatch) {
return new Callable(Callable.Type.METHOD, methodName,
method.getReturnType(args), true, false);
}
}
}
}
String message = "cannot find method '" + name + "' in class " + clazz.toJavaCode();
IllegalArgumentException e = new IllegalArgumentException(message);
L.e(e, "cannot find method %s in class %s", name, clazz.toJavaCode());
throw e;
}
@Override
public boolean isObservable(ModelClass modelClass) {
AnnotationClass annotationClass = (AnnotationClass) modelClass;
return getObservableType().isAssignableFrom(annotationClass) ||
getObservableListType().isAssignableFrom(annotationClass) ||
getObservableMapType().isAssignableFrom(annotationClass);
}
@Override
public boolean isObservableField(ModelClass modelClass) {
AnnotationClass annotationClass = (AnnotationClass) modelClass;
AnnotationClass erasure = new AnnotationClass(
getTypeUtils().erasure(annotationClass.mTypeMirror));
for (AnnotationClass observableField : getObservableFieldTypes()) {
if (observableField.isAssignableFrom(erasure)) {
return true;
}
}
return false;
}
@Override
public boolean isBindable(ModelField field) {
VariableElement fieldElement = ((AnnotationField) field).mField;
return fieldElement.getAnnotation(Bindable.class) != null;
}
@Override
public boolean isBindable(ModelMethod method) {
ExecutableElement methodElement = ((AnnotationMethod) method).mExecutableElement;
return methodElement.getAnnotation(Bindable.class) != null;
}
@Override
public Callable findMethodOrField(ModelClass modelClass, String name, boolean staticAccess) {
Preconditions.checkNotNull(modelClass, "trying to find method %s on empty mode class.",
name);
AnnotationClass annotationClass = (AnnotationClass) modelClass;
for (String methodName :
new String[]{"get" + StringUtils.capitalize(name),
"is" + StringUtils.capitalize(name), name}) {
ModelMethod[] methods = modelClass.getMethods(methodName, 0);
for (ModelMethod modelMethod : methods) {
AnnotationMethod method = (AnnotationMethod) modelMethod;
if (method.isPublic() && method.isStatic() == staticAccess) {
final AnnotationField backingField = findField(annotationClass, name, true);
final Callable result = new Callable(Callable.Type.METHOD, methodName,
method.getReturnType(null), true, isBindable(method) ||
(backingField != null && isBindable(backingField)));
L.d("backing field for %s is %s", result, backingField);
return result;
}
}
}
AnnotationField field = findField(annotationClass, name, false);
if (field != null && field.mField.getModifiers().contains(Modifier.PUBLIC) &&
field.mField.getModifiers().contains(Modifier.STATIC) == staticAccess) {
AnnotationClass fieldType = new AnnotationClass(field.mField.asType());
return new Callable(Callable.Type.FIELD, name, fieldType,
!field.mField.getModifiers().contains(Modifier.FINAL)
|| isObservable(fieldType), isBindable(field));
}
throw new IllegalArgumentException(
"cannot find " + name + " in " +
((DeclaredType) annotationClass.mTypeMirror).asElement().getSimpleName());
}
private AnnotationField findField(AnnotationClass clazz, String name, boolean allowNonPublic) {
if (clazz == null || clazz.mTypeMirror.getKind() != TypeKind.DECLARED) {
return null;
}
TypeElement typeElement = (TypeElement) ((DeclaredType) clazz.mTypeMirror).asElement();
for (VariableElement field : ElementFilter.fieldsIn(
getElementUtils().getAllMembers(typeElement))) {
if (name.equals(stripFieldName(field.getSimpleName().toString()))) {
if (allowNonPublic || field.getModifiers().contains(Modifier.PUBLIC)) {
return new AnnotationField(typeElement, field);
}
}
}
return null; // nothing found
}
@Override
public AnnotationClass loadPrimitive(String className) {
TypeKind typeKind = PRIMITIVE_TYPES.get(className);
if (typeKind == null) {
return null;
} else {
Types typeUtils = getTypeUtils();
return new AnnotationClass(typeUtils.getPrimitiveType(typeKind));
}
}
private static String stripFieldName(String fieldName) {
if (fieldName.length() > 2) {
final char start = fieldName.charAt(2);
if (fieldName.startsWith("m_") && Character.isJavaIdentifierStart(start)) {
return Character.toLowerCase(start) + fieldName.substring(3);
}
}
if (fieldName.length() > 1) {
final char start = fieldName.charAt(1);
final char fieldIdentifier = fieldName.charAt(0);
final boolean strip;
if (fieldIdentifier == '_') {
strip = true;
} else if (fieldIdentifier == 'm' && Character.isJavaIdentifierStart(start) &&
!Character.isLowerCase(start)) {
strip = true;
} else {
strip = false; // not mUppercase format
}
if (strip) {
return Character.toLowerCase(start) + fieldName.substring(2);
}
}
return fieldName;
}
@Override
public AnnotationClass findClass(String className, Map<String, String> imports) {
className = className.trim();
int numDimensions = 0;
while (className.endsWith("[]")) {
numDimensions++;
className = className.substring(0, className.length() - 2);
}
AnnotationClass primitive = loadPrimitive(className);
if (primitive != null) {
return addDimension(primitive.mTypeMirror, numDimensions);
}
int templateOpenIndex = className.indexOf('<');
DeclaredType declaredType;
Elements elementUtils = getElementUtils();
if (templateOpenIndex < 0) {
TypeElement typeElement = getTypeElement(className, imports);
if (typeElement == null) {
return null;
}
declaredType = (DeclaredType) typeElement.asType();
} else {
int templateCloseIndex = className.lastIndexOf('>');
String paramStr = className.substring(templateOpenIndex + 1, templateCloseIndex);
String baseClassName = className.substring(0, templateOpenIndex);
TypeElement typeElement = getTypeElement(baseClassName, imports);
if (typeElement == null) {
L.e("cannot find type element for %s", baseClassName);
return null;
}
ArrayList<String> templateParameters = splitTemplateParameters(paramStr);
TypeMirror[] typeArgs = new TypeMirror[templateParameters.size()];
for (int i = 0; i < typeArgs.length; i++) {
typeArgs[i] = findClass(templateParameters.get(i), imports).mTypeMirror;
if (typeArgs[i] == null) {
L.e("cannot find type argument for %s in %s", templateParameters.get(i),
baseClassName);
return null;
}
}
Types typeUtils = getTypeUtils();
declaredType = typeUtils.getDeclaredType(typeElement, typeArgs);
}
return addDimension(declaredType, numDimensions);
}
private AnnotationClass addDimension(TypeMirror type, int numDimensions) {
while (numDimensions > 0) {
type = getTypeUtils().getArrayType(type);
numDimensions--;
}
return new AnnotationClass(type);
}
private TypeElement getTypeElement(String className, Map<String, String> imports) {
Elements elementUtils = getElementUtils();
if (className.indexOf('.') < 0 && imports != null) {
// try the imports
String importedClass = imports.get(className);
if (importedClass != null) {
className = importedClass;
}
}
if (className.indexOf('.') < 0) {
// try java.lang.
String javaLangClass = "java.lang." + className;
try {
TypeElement javaLang = elementUtils.getTypeElement(javaLangClass);
if (javaLang != null) {
return javaLang;
}
} catch (Exception e) {
// try the normal way
}
}
try {
return elementUtils.getTypeElement(className);
} catch (Exception e) {
return null;
}
}
private ArrayList<String> splitTemplateParameters(String templateParameters) {
ArrayList<String> list = new ArrayList<String>();
int index = 0;
int openCount = 0;
StringBuilder arg = new StringBuilder();
while (index < templateParameters.length()) {
char c = templateParameters.charAt(index);
if (c == ',' && openCount == 0) {
list.add(arg.toString());
arg.delete(0, arg.length());
} else if (!Character.isWhitespace(c)) {
arg.append(c);
if (c == '<') {
openCount++;
} else if (c == '>') {
openCount--;
}
}
index++;
}
list.add(arg.toString());
return list;
}
@Override
public List<URL> getResources(String name) {
ArrayList<URL> urls = new ArrayList<URL>();
try {
Enumeration<URL> resources = getClass().getClassLoader().getResources(name);
while (resources.hasMoreElements()) {
urls.add(resources.nextElement());
}
} catch (IOException e) {
L.e(e, "IOException while getting resources:");
}
return urls;
}
@Override
public ModelClass findClass(Class classType) {
return findClass(classType.getCanonicalName(), null);
}
public Types getTypeUtils() {
return mProcessingEnv.getTypeUtils();
}
public Elements getElementUtils() {
return mProcessingEnv.getElementUtils();
}
public ProcessingEnvironment getProcessingEnv() {
return mProcessingEnv;
}
@Override
public TypeUtil createTypeUtil() {
return new AnnotationTypeUtil(this);
}
}