blob: 841ba30f22fba393044d40fd2d02090d511cdda7 [file] [log] [blame]
/*
* Copyright (C) 2008 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.tests.sigtest;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Vector;
/**
* This class represents a class description loaded from JDiff file.
* And it is used for signature test.
*/
public class JDiffClassDescription {
// Indicates that the class is an annotation
private static final int CLASS_MODIFIER_ANNOTATION = 0x00002000;
// Indicates that the class is an enum
private static final int CLASS_MODIFIER_ENUM = 0x00004000;
// Indicates that the method is a bridge method
private static final int METHOD_MODIFIER_BRIDGE = 0x00000040;
// Indicates that the method is declared to take a variable number of arguments
private static final int METHOD_MODIFIER_VAR_ARGS = 0x00000080;
// Indicates that the method is a synthetic method
private static final int METHOD_MODIFIER_SYNTHETIC = 0x00001000;
enum JDiffType {
INTERFACE, CLASS
}
@SuppressWarnings("unchecked")
private Class mClass;
private String mPackageName;
private String mShortClassName;
/**
* package name + short class name
*/
private String mAbsoluteClassName;
private int mModifier;
private String mExtendedClass;
private ArrayList<String> mImplInterface;
private ArrayList<JDiffField> mJdiffField;
private ArrayList<JDiffMethod> mJdiffMethod;
private ArrayList<JDiffConstructor> mJdiffConstructor;
private Vector<String> mInnerClassList;
private ResultObserver mResultObserver;
private JDiffType mClassType;
public JDiffClassDescription() {
mImplInterface = new ArrayList<String>();
mJdiffMethod = new ArrayList<JDiffMethod>();
mJdiffField = new ArrayList<JDiffField>();
mJdiffConstructor = new ArrayList<JDiffConstructor>();
}
/**
* Add implemented interface name.
*
* @param iname name of interface
*/
public void addImplInterface(String iname) {
mImplInterface.add(iname);
}
/**
* Add a field.
*
* @param field Object which contains field information
*/
public void addField(JDiffField field) {
mJdiffField.add(field);
}
/**
* Add a method.
*
* @param method Object which contains method information
*/
public void addMethod(JDiffMethod method) {
mJdiffMethod.add(method);
}
/**
* Add a constructor.
*
* @param tc Object which contains constructor information
*/
public void addConstructor(JDiffConstructor tc) {
mJdiffConstructor.add(tc);
}
abstract private static class JDiffElement {
protected String mName;
protected int mModifier;
protected JDiffElement(String name, int modifier) {
mName = name;
mModifier = modifier;
}
}
/**
* Object which contains field information.
*/
final static class JDiffField extends JDiffElement {
private String mFieldType;
public JDiffField(String name, String fieldType, int modifier) {
super(name, modifier);
mFieldType = fieldType;
}
/**
* Make a readable string according to the class name specified.
*
* @param className The specified class name.
* @return A readable string to represent this field along with the class name.
*/
public String toReadableString(String className) {
return className + "#" + mName + "(" + mFieldType + ")";
}
}
/**
* Object which contains method information.
*/
static class JDiffMethod extends JDiffElement {
protected String mReturnType;
protected ArrayList<String> mParamList;
protected ArrayList<String> mExceptionList;
public JDiffMethod(String name, int modifier, String returnType) {
super(name, modifier);
if (returnType == null) {
mReturnType = "void";
} else {
mReturnType = scrubJdiffParamType(returnType);
}
mParamList = new ArrayList<String>();
mExceptionList = new ArrayList<String>();
}
/**
* Add parameter.
*
* @param param parameter type
*/
public void addParam(String param) {
mParamList.add(scrubJdiffParamType(param));
}
/**
* Add exception.
*
* @param exceptionName name of exception
*/
public void addException(String exceptionName) {
mExceptionList.add(exceptionName);
}
/**
* Make a readable string according to the class name specified.
*
* @param className The specified class name.
* @return A readable string to represent this method along with the class name.
*/
public String toReadableString(String className) {
return className + "#" + mName + "(" + convertParamList(mParamList) + ")";
}
/**
* Convert parameter array to one string
*
* @param params parameter array
* @return converted parameter string
*/
private static String convertParamList(final ArrayList<String> params) {
StringBuffer paramList = new StringBuffer();
if (params != null) {
for (String str : params) {
paramList.append(str + ", ");
}
if (params.size() > 0) {
paramList.delete(paramList.length() - 2, paramList.length());
}
}
return paramList.toString();
}
}
/**
* Object which contains constructor information.
*/
final static class JDiffConstructor extends JDiffMethod {
public JDiffConstructor(String name, int modifier) {
super(name, modifier, null);
}
public JDiffConstructor(String name, String[] param, int modifier) {
super(name, modifier, null);
for (int i = 0; i < param.length; i++) {
addParam(param[i]);
}
}
}
/**
* Check test class' name, modifier, fields, constructors, and methods.
*/
public void checkSignatureCompliance() {
checkClassCompliance();
if (mClass != null) {
checkFieldsCompliance();
checkConstructorCompliance();
checkMethodCompliance();
}
mImplInterface.clear();
mJdiffMethod.clear();
mJdiffField.clear();
mJdiffConstructor.clear();
}
/**
* Check whether the method parsed from JDiff xml file and Java
* reflection are compliant.
*/
private void checkMethodCompliance() {
Method[] methods = mClass.getDeclaredMethods();
for (JDiffMethod method : mJdiffMethod) {
try {
// this is because jdiff think a method in an interface is not abstract
if (JDiffType.INTERFACE.equals(mClassType)) {
method.mModifier |= Modifier.ABSTRACT;
}
Method m = lookupMethod(method, methods);
if (m == null) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISSING_METHOD,
method.toReadableString(mAbsoluteClassName));
} else {
if (m.isVarArgs()) {
method.mModifier |= METHOD_MODIFIER_VAR_ARGS;
}
if (m.isBridge()) {
method.mModifier |= METHOD_MODIFIER_BRIDGE;
}
if (m.isSynthetic()) {
method.mModifier |= METHOD_MODIFIER_SYNTHETIC;
}
// FIXME: A workaround to fix the final mismatch on enumeration
if (mClass.isEnum() && method.mName.equals("values")) {
return;
}
if (m.getModifiers() != method.mModifier) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISMATCH_METHOD,
method.toReadableString(mAbsoluteClassName));
}
}
} catch (Exception e) {
SignatureTestLog.e("Got exception when checking method compliance", e);
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.CAUGHT_EXCEPTION,
method.toReadableString(mAbsoluteClassName));
}
}
}
/**
* Lookup a method from constant pool by reflection.
*
* @param method to be tested method
* @param methods all methods which are from java reflection
* @return available test method
*/
@SuppressWarnings("unchecked")
private Method lookupMethod(JDiffMethod method, Method[] methods) {
boolean found = false;
for (Method m : methods) {
if (method.mName.equals(m.getName())) {
String jdiffReturnType = method.mReturnType;
String reflectionReturnType = typeToString(m.getGenericReturnType());
ArrayList<String> jdiffParamList = method.mParamList;
if (jdiffReturnType.equals(reflectionReturnType)) {
Type[] params = m.getGenericParameterTypes();
if (jdiffParamList.size() == params.length) {
// Possible match
found = true;
for (int i = 0; i < jdiffParamList.size(); i++) {
if (!compareParam(jdiffParamList.get(i), typeToString(params[i]))) {
found = false;
break;
}
}
if (found) {
return m;
}
}
}
}
}
return null;
}
/**
* Compare the param from jdiff and param from reflection.
*
* @param jdiffParam param parsed from the jdiff xml file.
* @param reflectionParam param got from the Java reflection.
* @return True if the two params match, otherwise return false.
*/
private static boolean compareParam(String jdiffParam, String reflectionParam) {
if (jdiffParam == null || reflectionParam == null) {
return false;
}
// Most things aren't varargs, so just do a simple compare
// first.
if (jdiffParam.equals(reflectionParam)) {
return true;
}
// Check for varargs. jdiff reports varargs as ..., while
// reflection reports them as []
int jdiffParamEndOffset = jdiffParam.indexOf("...");
int reflectionParamEndOffset = reflectionParam.indexOf("[]");
if (jdiffParamEndOffset != -1 && reflectionParamEndOffset != -1) {
jdiffParam = jdiffParam.substring(0, jdiffParamEndOffset);
reflectionParam = reflectionParam.substring(0, reflectionParamEndOffset);
return jdiffParam.equals(reflectionParam);
}
return false;
}
/**
* Check whether the constructor parsed from JDiff xml file and
* Java reflection are compliant.
*/
@SuppressWarnings("unchecked")
private void checkConstructorCompliance() {
for (JDiffConstructor con : mJdiffConstructor) {
try {
Constructor c = lookupConstructor(con, mClass);
if (c == null) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISSING_METHOD,
con.toReadableString(mAbsoluteClassName));
} else {
if (c.isVarArgs()) {// some method's parameter are variable args
con.mModifier |= METHOD_MODIFIER_VAR_ARGS;
}
if (c.getModifiers() != con.mModifier) {
mResultObserver.notifyFailure(
SignatureTestActivity.FAILURE_TYPE.MISMATCH_METHOD,
con.toReadableString(mAbsoluteClassName));
}
}
} catch (Exception e) {
SignatureTestLog.e("Got exception when checking constructor compliance", e);
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.CAUGHT_EXCEPTION,
con.toReadableString(mAbsoluteClassName));
}
}
}
/**
* Search available constructor.
*
* @param jdiffDes constructor which is to be tested
* @param cons all constructors which are from java reflection
* @return available constructor
*/
@SuppressWarnings("unchecked")
private static Constructor lookupConstructor(JDiffConstructor jdiffDes,
Class clazz) {
for (Constructor c : clazz.getDeclaredConstructors()) {
Type[] params = c.getGenericParameterTypes();
boolean isStaticClass = ((clazz.getModifiers() & Modifier.STATIC) != 0);
int startParamOffset = 0;
int numberOfParams = params.length;
// non-static inner class -> skip implicit parent pointer
// as first arg
if (clazz.isMemberClass() && !isStaticClass && params.length >= 1) {
startParamOffset = 1;
--numberOfParams;
}
ArrayList<String> jdiffParamList = jdiffDes.mParamList;
if (jdiffParamList.size() == numberOfParams) {
boolean isFound = true;
// i counts jdiff params, j counts reflected params
int i = 0;
int j = startParamOffset;
while (i < jdiffParamList.size()) {
if (!compareParam(jdiffParamList.get(i), typeToString(params[j]))) {
isFound = false;
break;
}
++i;
++j;
}
if (isFound) {
return c;
}
}
}
return null;
}
/**
* Check all fields in test class.
*/
private void checkFieldsCompliance() {
Field[] fields = mClass.getDeclaredFields();
for (JDiffField field : mJdiffField) {
try {
Field f = lookupField(fields, field);
if (f == null) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISSING_FIELD,
field.toReadableString(mAbsoluteClassName));
} else {
if (f.getModifiers() != field.mModifier
|| !f.getType().getCanonicalName().equals(field.mFieldType)) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISMATCH_FIELD,
field.toReadableString(mAbsoluteClassName));
}
}
} catch (Exception e) {
SignatureTestLog.e("Got exception when checking field compliance", e);
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.CAUGHT_EXCEPTION,
field.toReadableString(mAbsoluteClassName));
}
}
}
/**
* Lookup field from constant pool by reflection.
*
* @param fields all fields which are from java reflection
* @param field field which is to be tested
* @return available test field
*/
private static Field lookupField(Field[] fields, JDiffField field){
for (Field f : fields) {
if (f.getName().equals(field.mName)) {
return f;
}
}
return null;
}
/**
* Check class information.
*/
@SuppressWarnings("unchecked")
private void checkClassCompliance() {
try {
mClass = null;
mAbsoluteClassName = mPackageName + "." + mShortClassName;
if (mShortClassName.indexOf(".") != -1) {
// The class is an inner class
mClass = lookupClass();
} else {
try {
mClass = Class.forName(mAbsoluteClassName);
} catch (ClassNotFoundException e) {
// Ignore the exception, will handle it later
if (mAbsoluteClassName.equals("java.util.prefs.Preferences")) {
//FIXME: A workaround to fix the loading error for java.util.prefs.Preferences
SignatureTestLog.d("java.util.prefs.Preferences workaround hit");
return;
}
} catch (java.lang.ExceptionInInitializerError e) {
// FIXME: This is a temp workaround to fix the ExceptionInInitializerError
SignatureTestLog.d("ExceptionInInitializerError workaround hit");
return;
}
}
if (mClass == null) {
// No class found, notify the observer according to the class type
if (JDiffType.INTERFACE.equals(mClassType)) {
if (mAbsoluteClassName.equals("android.widget.PopupWindow.OnDismissListener")) {
// FIXME: A workaround to fix the visibility problem of
// android.widget.PopupWindow.OnDismissListener.
SignatureTestLog.d("android.widget.PopupWindow.OnDismissListener workaround hit");
return;
}
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISSING_INTERFACE,
mAbsoluteClassName);
} else {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISSING_CLASS,
mAbsoluteClassName);
}
return;
}
int realModifer = mClass.getModifiers();
if (isAnnotation()) {
realModifer &= ~CLASS_MODIFIER_ANNOTATION;
}
if (mClass.isInterface()) {
realModifer &= ~Modifier.INTERFACE;
}
if (isEnumType() && mClass.isEnum()) {
realModifer &= ~CLASS_MODIFIER_ENUM;
}
if (realModifer != mModifier || (isEnumType() != mClass.isEnum())) {
logMismatchInterfaceSignature(mAbsoluteClassName);
return;
}
if (mClass.isAnnotation()) {
// check annotation
boolean found = false;
for (String inter : mImplInterface) {
if ("java.lang.annotation.Annotation".equals(inter))
{
found = true;
}
}
if (!found) {
logMismatchInterfaceSignature(mAbsoluteClassName);
}
} else {
// check father class
Class superClass = mClass.getSuperclass();
if (mExtendedClass != null || superClass != null) {
if (superClass == null
|| !superClass.getCanonicalName().equals(mExtendedClass)){
if (mAbsoluteClassName.equals("android.hardware.SensorManager")) {
// FIXME: Please see Issue 1496822 for more information
} else {
logMismatchInterfaceSignature(mAbsoluteClassName);
return;
}
}
}
// check implements interface
Class[] interfaces = mClass.getInterfaces();
HashSet<String> interFaceSet = new HashSet<String>();
for (Class c : interfaces) {
interFaceSet.add(c.getCanonicalName());
}
for (String inter : mImplInterface) {
if (!interFaceSet.contains(inter)) {
logMismatchInterfaceSignature(mAbsoluteClassName);
break;
}
}
}
} catch (Exception e) {
SignatureTestLog.e("Got exception when checking field compliance", e);
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.CAUGHT_EXCEPTION,
mAbsoluteClassName);
}
}
private void logMismatchInterfaceSignature(String classFullName) {
if (JDiffType.INTERFACE.equals(mClassType)) {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISMATCH_INTERFACE,
classFullName);
} else {
mResultObserver.notifyFailure(SignatureTestActivity.FAILURE_TYPE.MISMATCH_CLASS,
classFullName);
}
}
/**
* Whether this class is enum.
*
* @return true if this class is enum
*/
private boolean isEnumType() {
return "java.lang.Enum".equals(mExtendedClass);
}
/**
* Search available class which is the corresponding test class.
*
* @return available class if found, else return null.
*/
@SuppressWarnings("unchecked")
private Class lookupClass() {
mInnerClassList = new Vector<String>();
String[] strArray = mShortClassName.split("\\.");
for (String str : strArray) {
mInnerClassList.add(str);
}
String fullName = mPackageName + "." + mInnerClassList.remove(0);
try {
Class c = Class.forName(fullName);
return lookupClass(c, fullName);
} catch (ClassNotFoundException e) {
return null;
}
}
/**
* Search aim class
*
* @param c class which contains test class
* @param fullName class' full name
* @return The aim class, or null if not found.
*/
@SuppressWarnings("unchecked")
private Class lookupClass(Class c, String fullName) {
if (mInnerClassList.size() == 0){
return null;
}
fullName += "." + mInnerClassList.remove(0);
Class[]cs = c.getClasses();
for (int i = 0; i < cs.length; i++) {
String className = cs[i].getCanonicalName();
if (fullName.equals(className)) {
return mInnerClassList.size() == 0 ? cs[i] : lookupClass(cs[i], className);
}
}
return null;
}
/**
* Whether this class is annotation.
*
* @return true if this class is Annotation.
*/
private boolean isAnnotation() {
if (mImplInterface.contains("java.lang.annotation.Annotation")) {
return true;
}
return false;
}
public void setPackageName(String packageName) {
mPackageName = packageName;
}
public void setClassName(String className) {
mShortClassName = className;
}
public String getClassName() {
return mShortClassName;
}
public void setModifier(int modifier) {
mModifier = modifier;
}
public void setType(JDiffType type) {
mClassType = type;
}
public void setExtendsClass(String extendsClass) {
mExtendedClass = extendsClass;
}
public void registerResultObserver(ResultObserver resultObserver) {
mResultObserver = resultObserver;
}
/**
* A helper function for typeToString that converts WildcardType
* array into a jdiff compatible string.
*
* @param types array of types to format.
* @return the jdiff formatted string.
*/
private static String concatWildcardTypes(Type[] types) {
StringBuffer sb = new StringBuffer();
int elementNum = 0;
for (Type t : types) {
sb.append(typeToString(t));
if (++elementNum < types.length) {
sb.append(" & ");
}
}
return sb.toString();
}
/**
* Converts a Type into a jdiff compatible String. The returned
* types from this function should match the same Strings that
* jdiff is providing to us.
*
* @param type the type to convert.
* @return the jdiff formatted string.
*/
private static String typeToString(Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) type;
StringBuffer sb = new StringBuffer();
sb.append(typeToString(pt.getRawType()));
sb.append("<");
int elementNum = 0;
Type[] types = pt.getActualTypeArguments();
for (Type t : types) {
sb.append(typeToString(t));
if (++elementNum < types.length) {
sb.append(", ");
}
}
sb.append(">");
return sb.toString();
} else if (type instanceof TypeVariable) {
return ((TypeVariable) type).getName();
} else if (type instanceof Class) {
return ((Class) type).getCanonicalName();
} else if (type instanceof GenericArrayType) {
String typeName = typeToString(((GenericArrayType) type).getGenericComponentType());
return typeName + "[]";
} else if (type instanceof WildcardType) {
WildcardType wt = (WildcardType) type;
Type[] lowerBounds = wt.getLowerBounds();
if (lowerBounds.length == 0) {
String name = "? extends " + concatWildcardTypes(wt.getUpperBounds());
// Special case for ?
if (name.equals("? extends java.lang.Object")) {
return "?";
} else {
return name;
}
} else {
String name = concatWildcardTypes(wt.getUpperBounds()) +
" super " +
concatWildcardTypes(wt.getLowerBounds());
// Another special case for ?
name = name.replace("java.lang.Object", "?");
return name;
}
} else {
throw new RuntimeException("Got an unknown java.lang.Type");
}
}
/**
* Cleans up jdiff parameters to canonicalize them.
*
* @param paramType the parameter from jdiff.
* @return the scrubbed version of the parameter.
*/
private static String scrubJdiffParamType(String paramType) {
// <? extends java.lang.Object and <?> are the same, so
// canonicalize them to one form.
return paramType.replace("<? extends java.lang.Object>", "<?>");
}
}