package com.airbnb.epoxy;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterSpec;
import com.squareup.javapoet.TypeName;
import java.lang.annotation.Annotation;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
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.Name;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
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 static javax.lang.model.element.ElementKind.CLASS;
import static javax.lang.model.element.Modifier.PRIVATE;
import static javax.lang.model.element.Modifier.PROTECTED;
import static javax.lang.model.element.Modifier.PUBLIC;
import static javax.lang.model.element.Modifier.STATIC;
class Utils {
private static final Pattern PATTERN_STARTS_WITH_SET = Pattern.compile("set[A-Z]\\w*");
static final String EPOXY_MODEL_TYPE = "com.airbnb.epoxy.EpoxyModel<?>";
static final String UNTYPED_EPOXY_MODEL_TYPE = "com.airbnb.epoxy.EpoxyModel";
static final String EPOXY_MODEL_WITH_HOLDER_TYPE = "com.airbnb.epoxy.EpoxyModelWithHolder<?>";
static final String EPOXY_VIEW_HOLDER_TYPE = "com.airbnb.epoxy.EpoxyViewHolder";
static final String EPOXY_HOLDER_TYPE = "com.airbnb.epoxy.EpoxyHolder";
static final String ANDROID_VIEW_TYPE = "android.view.View";
static final String EPOXY_CONTROLLER_TYPE = "com.airbnb.epoxy.EpoxyController";
static final String VIEW_CLICK_LISTENER_TYPE = "android.view.View.OnClickListener";
static final String GENERATED_MODEL_INTERFACE = "com.airbnb.epoxy.GeneratedModel";
static final String MODEL_CLICK_LISTENER_TYPE = "com.airbnb.epoxy.OnModelClickListener";
static final String ON_BIND_MODEL_LISTENER_TYPE = "com.airbnb.epoxy.OnModelBoundListener";
static final String ON_UNBIND_MODEL_LISTENER_TYPE = "com.airbnb.epoxy.OnModelUnboundListener";
static final String WRAPPED_LISTENER_TYPE = "com.airbnb.epoxy.WrappedEpoxyModelClickListener";
static final String DATA_BINDING_MODEL_TYPE = "com.airbnb.epoxy.DataBindingEpoxyModel";
static void throwError(String msg, Object... args)
throws EpoxyProcessorException {
throw new EpoxyProcessorException(String.format(msg, args));
}
static Class<?> getClass(ClassName name) {
try {
return Class.forName(name.reflectionName());
} catch (ClassNotFoundException | NoClassDefFoundError e) {
return null;
}
}
static Class<? extends Annotation> getAnnotationClass(ClassName name) {
try {
return (Class<? extends Annotation>) getClass(name);
} catch (ClassCastException e) {
return null;
}
}
static Element getElementByName(ClassName name, Elements elements, Types types) {
try {
return elements.getTypeElement(name.reflectionName());
} catch (MirroredTypeException mte) {
return types.asElement(mte.getTypeMirror());
}
}
static Element getElementByName(String name, Elements elements, Types types) {
try {
return elements.getTypeElement(name);
} catch (MirroredTypeException mte) {
return types.asElement(mte.getTypeMirror());
}
}
static ClassName getClassName(String className) {
return ClassName.bestGuess(className);
}
static EpoxyProcessorException buildEpoxyException(String msg, Object... args) {
return new EpoxyProcessorException(String.format(msg, args));
}
static boolean isViewClickListenerType(TypeMirror type) {
return isSubtypeOfType(type, VIEW_CLICK_LISTENER_TYPE);
}
static boolean isIterableType(TypeElement element) {
return isSubtypeOfType(element.asType(), "java.lang.Iterable<?>");
}
static boolean isEpoxyModel(TypeMirror type) {
return isSubtypeOfType(type, EPOXY_MODEL_TYPE);
}
static boolean isController(TypeElement element) {
return isSubtypeOfType(element.asType(), EPOXY_CONTROLLER_TYPE);
}
static boolean isEpoxyModel(TypeElement type) {
return isEpoxyModel(type.asType());
}
static boolean isEpoxyModelWithHolder(TypeElement type) {
return isSubtypeOfType(type.asType(), EPOXY_MODEL_WITH_HOLDER_TYPE);
}
static boolean isDataBindingModel(TypeElement type) {
return isSubtypeOfType(type.asType(), DATA_BINDING_MODEL_TYPE);
}
static boolean isSubtypeOfType(TypeMirror typeMirror, String otherType) {
if (otherType.equals(typeMirror.toString())) {
return true;
}
if (typeMirror.getKind() != TypeKind.DECLARED) {
return false;
}
DeclaredType declaredType = (DeclaredType) typeMirror;
List<? extends TypeMirror> typeArguments = declaredType.getTypeArguments();
if (typeArguments.size() > 0) {
StringBuilder typeString = new StringBuilder(declaredType.asElement().toString());
typeString.append('<');
for (int i = 0; i < typeArguments.size(); i++) {
if (i > 0) {
typeString.append(',');
}
typeString.append('?');
}
typeString.append('>');
if (typeString.toString().equals(otherType)) {
return true;
}
}
Element element = declaredType.asElement();
if (!(element instanceof TypeElement)) {
return false;
}
TypeElement typeElement = (TypeElement) element;
TypeMirror superType = typeElement.getSuperclass();
if (isSubtypeOfType(superType, otherType)) {
return true;
}
for (TypeMirror interfaceType : typeElement.getInterfaces()) {
if (isSubtypeOfType(interfaceType, otherType)) {
return true;
}
}
return false;
}
/**
* Checks if two classes belong to the same package
*/
static boolean belongToTheSamePackage(TypeElement class1, TypeElement class2, Elements elements) {
Name package1 = elements.getPackageOf(class1).getQualifiedName();
Name package2 = elements.getPackageOf(class2).getQualifiedName();
return package1.equals(package2);
}
static boolean isSubtype(TypeElement e1, TypeElement e2, Types types) {
return isSubtype(e1.asType(), e2.asType(), types);
}
static boolean isSubtype(TypeMirror e1, TypeMirror e2, Types types) {
return types.isSubtype(e1, types.erasure(e2));
}
/**
* Checks if the given field has package-private visibility
*/
static boolean isFieldPackagePrivate(Element element) {
Set<Modifier> modifiers = element.getModifiers();
return !modifiers.contains(PUBLIC)
&& !modifiers.contains(PROTECTED)
&& !modifiers.contains(PRIVATE);
}
/**
* @return True if the clazz (or one of its superclasses) implements the given method. Returns
* false if the method doesn't exist anywhere in the class hierarchy or it is abstract.
*/
static boolean implementsMethod(TypeElement clazz, MethodSpec method, Types typeUtils) {
ExecutableElement methodOnClass = getMethodOnClass(clazz, method, typeUtils);
if (methodOnClass == null) {
return false;
}
Set<Modifier> modifiers = methodOnClass.getModifiers();
return !modifiers.contains(Modifier.ABSTRACT);
}
static TypeMirror getMethodReturnType(TypeElement clazz, MethodSpec method, Types typeUtils) {
ExecutableElement methodOnClass = getMethodOnClass(clazz, method, typeUtils);
if (methodOnClass == null) {
return null;
}
return methodOnClass.getReturnType();
}
/**
* @return The first element matching the given method in the class's hierarchy, or null if there
* is no match.
*/
static ExecutableElement getMethodOnClass(TypeElement clazz, MethodSpec method, Types typeUtils) {
if (clazz.asType().getKind() != TypeKind.DECLARED) {
return null;
}
for (Element subElement : clazz.getEnclosedElements()) {
if (subElement.getKind() == ElementKind.METHOD) {
ExecutableElement methodElement = ((ExecutableElement) subElement);
if (!methodElement.getSimpleName().toString().equals(method.name)) {
continue;
}
if (!areParamsTheSame(methodElement, method)) {
continue;
}
return methodElement;
}
}
TypeElement superClazz = (TypeElement) typeUtils.asElement(clazz.getSuperclass());
if (superClazz == null) {
return null;
}
return getMethodOnClass(superClazz, method, typeUtils);
}
private static boolean areParamsTheSame(ExecutableElement method1, MethodSpec method2) {
List<? extends VariableElement> params1 = method1.getParameters();
List<ParameterSpec> params2 = method2.parameters;
if (params1.size() != params2.size()) {
return false;
}
for (int i = 0; i < params1.size(); i++) {
VariableElement param1 = params1.get(i);
ParameterSpec param2 = params2.get(i);
if (!TypeName.get(param1.asType()).equals(param2.type)) {
return false;
}
}
return true;
}
/**
* Returns the type of the Epoxy model.
* <p>
* Eg for "class MyModel extends EpoxyModel<TextView>" it would return TextView.
*/
static TypeMirror getEpoxyObjectType(TypeElement clazz, Types typeUtils) {
if (clazz.getSuperclass().getKind() != TypeKind.DECLARED) {
return null;
}
DeclaredType superclass = (DeclaredType) clazz.getSuperclass();
TypeMirror recursiveResult =
getEpoxyObjectType((TypeElement) typeUtils.asElement(superclass), typeUtils);
if (recursiveResult != null && recursiveResult.getKind() != TypeKind.TYPEVAR) {
// Use the type on the parent highest in the class hierarchy so we can find the original type.
// We don't allow TypeVar since that is just type letter (eg T).
return recursiveResult;
}
List<? extends TypeMirror> superTypeArguments = superclass.getTypeArguments();
if (superTypeArguments.size() == 1) {
// If there is only one type then we use that
return superTypeArguments.get(0);
}
for (TypeMirror superTypeArgument : superTypeArguments) {
// The user might have added additional types to their class which makes it more difficult
// to figure out the base model type. We just look for the first type that is a view or
// view holder.
if (isSubtypeOfType(superTypeArgument, ANDROID_VIEW_TYPE)
|| isSubtypeOfType(superTypeArgument, EPOXY_HOLDER_TYPE)) {
return superTypeArgument;
}
}
return null;
}
static void validateFieldAccessibleViaGeneratedCode(Element fieldElement,
Class<?> annotationClass, ErrorLogger errorLogger, boolean skipPrivateFieldCheck) {
TypeElement enclosingElement = (TypeElement) fieldElement.getEnclosingElement();
// Verify method modifiers.
Set<Modifier> modifiers = fieldElement.getModifiers();
if ((modifiers.contains(PRIVATE) && !skipPrivateFieldCheck) || modifiers.contains(STATIC)) {
errorLogger.logError(
"%s annotations must not be on private or static fields. (class: %s, field: %s)",
annotationClass.getSimpleName(),
enclosingElement.getSimpleName(), fieldElement.getSimpleName());
}
// Nested classes must be static
if (enclosingElement.getNestingKind().isNested()) {
if (!enclosingElement.getModifiers().contains(STATIC)) {
errorLogger.logError(
"Nested classes with %s annotations must be static. (class: %s, field: %s)",
annotationClass.getSimpleName(),
enclosingElement.getSimpleName(), fieldElement.getSimpleName());
}
}
// Verify containing type.
if (enclosingElement.getKind() != CLASS) {
errorLogger
.logError("%s annotations may only be contained in classes. (class: %s, field: %s)",
annotationClass.getSimpleName(),
enclosingElement.getSimpleName(), fieldElement.getSimpleName());
}
// Verify containing class visibility is not private.
if (enclosingElement.getModifiers().contains(PRIVATE)) {
errorLogger.logError(
"%s annotations may not be contained in private classes. (class: %s, field: %s)",
annotationClass.getSimpleName(),
enclosingElement.getSimpleName(), fieldElement.getSimpleName());
}
}
static void validateFieldAccessibleViaGeneratedCode(Element fieldElement,
Class<?> annotationClass, ErrorLogger errorLogger) {
validateFieldAccessibleViaGeneratedCode(fieldElement, annotationClass, errorLogger, false);
}
static String capitalizeFirstLetter(String original) {
if (original == null || original.isEmpty()) {
return original;
}
return original.substring(0, 1).toUpperCase() + original.substring(1);
}
static String decapitalizeFirstLetter(String original) {
if (original == null || original.isEmpty()) {
return original;
}
return original.substring(0, 1).toLowerCase() + original.substring(1);
}
static boolean startsWithIs(String original) {
return original.startsWith("is") && original.length() > 2
&& Character.isUpperCase(original.charAt(2));
}
static boolean isSetterMethod(Element element) {
if (element.getKind() != ElementKind.METHOD) {
return false;
}
ExecutableElement method = (ExecutableElement) element;
String methodName = method.getSimpleName().toString();
return PATTERN_STARTS_WITH_SET.matcher(methodName).matches()
&& method.getParameters().size() == 1;
}
static String removeSetPrefix(String string) {
return String.valueOf(string.charAt(3)).toLowerCase() + string.substring(4);
}
}