blob: 6eafc3241f452424eb6c4cca0966f08827454b78 [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.signature.cts;
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.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Represents class descriptions loaded from a jdiff xml file. Used
* for CTS SignatureTests.
*/
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 takes 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;
private static final Set<String> HIDDEN_INTERFACE_WHITELIST = new HashSet<>();
static {
// Interfaces that define @hide or @SystemApi or @TestApi methods will by definition contain
// methods that do not appear in current.txt. Interfaces added to this
// list are probably not meant to be implemented in an application.
HIDDEN_INTERFACE_WHITELIST.add("public abstract boolean android.companion.DeviceFilter.matches(D)");
HIDDEN_INTERFACE_WHITELIST.add("public static <D> boolean android.companion.DeviceFilter.matches(android.companion.DeviceFilter<D>,D)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract java.lang.String android.companion.DeviceFilter.getDeviceDisplayName(D)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract int android.companion.DeviceFilter.getMediumType()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.nfc.tech.TagTechnology.reconnect() throws java.io.IOException");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.os.IBinder.shellCommand(java.io.FileDescriptor,java.io.FileDescriptor,java.io.FileDescriptor,java.lang.String[],android.os.ShellCallback,android.os.ResultReceiver) throws android.os.RemoteException");
HIDDEN_INTERFACE_WHITELIST.add("public abstract int android.text.ParcelableSpan.getSpanTypeIdInternal()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.text.ParcelableSpan.writeToParcelInternal(android.os.Parcel,int)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void android.view.WindowManager.requestAppKeyboardShortcuts(android.view.WindowManager$KeyboardShortcutsReceiver,int)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract boolean javax.microedition.khronos.egl.EGL10.eglReleaseThread()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract void org.w3c.dom.ls.LSSerializer.setFilter(org.w3c.dom.ls.LSSerializerFilter)");
HIDDEN_INTERFACE_WHITELIST.add("public abstract org.w3c.dom.ls.LSSerializerFilter org.w3c.dom.ls.LSSerializer.getFilter()");
HIDDEN_INTERFACE_WHITELIST.add("public abstract android.graphics.Region android.view.WindowManager.getCurrentImeTouchRegion()");
}
public enum JDiffType {
INTERFACE, CLASS
}
@SuppressWarnings("unchecked")
private Class<?> mClass;
// A map of field name to field of the fields contained in {@code mClass}
private Map<String, Field> mClassFieldMap;
private String mPackageName;
private String mShortClassName;
/**
* Package name + short class name
*/
private String mAbsoluteClassName;
private int mModifier;
private String mExtendedClass;
private List<String> implInterfaces = new ArrayList<String>();
private List<JDiffField> jDiffFields = new ArrayList<JDiffField>();
private List<JDiffMethod> jDiffMethods = new ArrayList<JDiffMethod>();
private List<JDiffConstructor> jDiffConstructors = new ArrayList<JDiffConstructor>();
private ResultObserver mResultObserver;
private JDiffType mClassType;
/**
* Creates a new JDiffClassDescription.
*
* @param pkg the java package this class will end up in.
* @param className the name of the class.
*/
public JDiffClassDescription(String pkg, String className) {
this(pkg, className, new ResultObserver() {
@Override
public void notifyFailure(FailureType type, String name, String errorMessage) {
// This is a null result observer that doesn't do anything.
}
});
}
/**
* Creates a new JDiffClassDescription with the specified results
* observer.
*
* @param pkg the java package this class belongs in.
* @param className the name of the class.
* @param resultObserver the resultObserver to get results with.
*/
public JDiffClassDescription(String pkg, String className, ResultObserver resultObserver) {
mPackageName = pkg;
mShortClassName = className;
mResultObserver = resultObserver;
}
/**
* adds implemented interface name.
*
* @param iname name of interface
*/
public void addImplInterface(String iname) {
implInterfaces.add(iname);
}
/**
* Adds a field.
*
* @param field the field to be added.
*/
public void addField(JDiffField field) {
jDiffFields.add(field);
}
/**
* Adds a method.
*
* @param method the method to be added.
*/
public void addMethod(JDiffMethod method) {
jDiffMethods.add(method);
}
/**
* Adds a constructor.
*
* @param tc the constructor to be added.
*/
public void addConstructor(JDiffConstructor tc) {
jDiffConstructors.add(tc);
}
static String convertModifiersToAccessLevel(int modifiers) {
if ((modifiers & Modifier.PUBLIC) != 0) {
return "public";
} else if ((modifiers & Modifier.PRIVATE) != 0) {
return "private";
} else if ((modifiers & Modifier.PROTECTED) != 0) {
return "protected";
} else {
// package protected
return "";
}
}
static String convertModifersToModifierString(int modifiers) {
StringBuffer sb = new StringBuffer();
boolean isFirst = true;
// order taken from Java Language Spec, sections 8.1.1, 8.3.1, and 8.4.3
if ((modifiers & Modifier.ABSTRACT) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("abstract");
}
if ((modifiers & Modifier.STATIC) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("static");
}
if ((modifiers & Modifier.FINAL) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("final");
}
if ((modifiers & Modifier.TRANSIENT) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("transient");
}
if ((modifiers & Modifier.VOLATILE) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("volatile");
}
if ((modifiers & Modifier.SYNCHRONIZED) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("synchronized");
}
if ((modifiers & Modifier.NATIVE) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("native");
}
if ((modifiers & Modifier.STRICT) != 0) {
if (isFirst) {
isFirst = false;
} else {
sb.append(" ");
}
sb.append("strictfp");
}
return sb.toString();
}
public abstract static class JDiffElement {
final String mName;
int mModifier;
public JDiffElement(String name, int modifier) {
mName = name;
mModifier = modifier;
}
}
/**
* Represents a field.
*/
public static final class JDiffField extends JDiffElement {
private String mFieldType;
private String mFieldValue;
public JDiffField(String name, String fieldType, int modifier, String value) {
super(name, modifier);
mFieldType = fieldType;
mFieldValue = value;
}
/**
* A string representation of the value within the field.
*/
public String getValueString() {
return mFieldValue;
}
/**
* 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 + ")";
}
public String toSignatureString() {
StringBuffer sb = new StringBuffer();
// access level
String accesLevel = convertModifiersToAccessLevel(mModifier);
if (!"".equals(accesLevel)) {
sb.append(accesLevel).append(" ");
}
String modifierString = convertModifersToModifierString(mModifier);
if (!"".equals(modifierString)) {
sb.append(modifierString).append(" ");
}
sb.append(mFieldType).append(" ");
sb.append(mName);
return sb.toString();
}
}
/**
* Represents a method.
*/
public 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>();
}
/**
* Adds a parameter.
*
* @param param parameter type
*/
public void addParam(String param) {
mParamList.add(scrubJdiffParamType(param));
}
/**
* Adds an exception.
*
* @param exceptionName name of exception
*/
public void addException(String exceptionName) {
mExceptionList.add(exceptionName);
}
/**
* Makes 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) + ")";
}
/**
* Converts a parameter array to a string
*
* @param params the array to convert
* @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();
}
public String toSignatureString() {
StringBuffer sb = new StringBuffer();
// access level
String accesLevel = convertModifiersToAccessLevel(mModifier);
if (!"".equals(accesLevel)) {
sb.append(accesLevel).append(" ");
}
String modifierString = convertModifersToModifierString(mModifier);
if (!"".equals(modifierString)) {
sb.append(modifierString).append(" ");
}
String returnType = getReturnType();
if (!"".equals(returnType)) {
sb.append(returnType).append(" ");
}
sb.append(mName);
sb.append("(");
for (int x = 0; x < mParamList.size(); x++) {
sb.append(mParamList.get(x));
if (x + 1 != mParamList.size()) {
sb.append(", ");
}
}
sb.append(")");
// does it throw?
if (mExceptionList.size() > 0) {
sb.append(" throws ");
for (int x = 0; x < mExceptionList.size(); x++) {
sb.append(mExceptionList.get(x));
if (x + 1 != mExceptionList.size()) {
sb.append(", ");
}
}
}
return sb.toString();
}
/**
* Gets the return type.
*
* @return the return type of this method.
*/
protected String getReturnType() {
return mReturnType;
}
}
/**
* Represents a constructor.
*/
public static final 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]);
}
}
/**
* Gets the return type.
*
* @return the return type of this method.
*/
@Override
protected String getReturnType() {
// Constructors have no return type.
return "";
}
}
/**
* Checks test class's name, modifier, fields, constructors, and
* methods.
*/
public void checkSignatureCompliance() {
checkClassCompliance();
if (mClass != null) {
mClassFieldMap = buildFieldMap(mClass);
checkFieldsCompliance();
checkConstructorCompliance();
checkMethodCompliance();
} else {
mClassFieldMap = null;
}
}
/**
* Checks to ensure that the modifiers value for two methods are
* compatible.
*
* Allowable differences are:
* - synchronized is allowed to be removed from an apiMethod
* that has it
* - the native modified is ignored
*
* @param apiMethod the method read from the api file.
* @param reflectedMethod the method found via reflections.
*/
private boolean areMethodsModifiedCompatible(JDiffMethod apiMethod ,
Method reflectedMethod) {
// If the apiMethod isn't synchronized
if (((apiMethod.mModifier & Modifier.SYNCHRONIZED) == 0) &&
// but the reflected method is
((reflectedMethod.getModifiers() & Modifier.SYNCHRONIZED) != 0)) {
// that is a problem
return false;
}
// Mask off NATIVE since it is a don't care. Also mask off
// SYNCHRONIZED since we've already handled that check.
int ignoredMods = (Modifier.NATIVE | Modifier.SYNCHRONIZED | Modifier.STRICT);
int mod1 = reflectedMethod.getModifiers() & ~ignoredMods;
int mod2 = apiMethod.mModifier & ~ignoredMods;
// We can ignore FINAL for classes
if ((mModifier & Modifier.FINAL) != 0) {
mod1 &= ~Modifier.FINAL;
mod2 &= ~Modifier.FINAL;
}
return mod1 == mod2;
}
/**
* Checks that the method found through reflection matches the
* specification from the API xml file.
*/
private void checkMethodCompliance() {
for (JDiffMethod method : jDiffMethods) {
try {
Method m = findMatchingMethod(method);
if (m == null) {
mResultObserver.notifyFailure(FailureType.MISSING_METHOD,
method.toReadableString(mAbsoluteClassName),
"No method with correct signature found:" +
method.toSignatureString());
} 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 (!areMethodsModifiedCompatible(method, m)) {
mResultObserver.notifyFailure(FailureType.MISMATCH_METHOD,
method.toReadableString(mAbsoluteClassName),
"Non-compatible method found when looking for " +
method.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking method compliance", e);
mResultObserver.notifyFailure(FailureType.CAUGHT_EXCEPTION,
method.toReadableString(mAbsoluteClassName),
"Exception!");
}
}
}
/**
* Checks if the two types of methods are the same.
*
* @param jDiffMethod the jDiffMethod to compare
* @param method the reflected method to compare
* @return true, if both methods are the same
*/
private boolean matches(JDiffMethod jDiffMethod, Method reflectedMethod) {
// If the method names aren't equal, the methods can't match.
if (!jDiffMethod.mName.equals(reflectedMethod.getName())) {
return false;
}
String jdiffReturnType = jDiffMethod.mReturnType;
String reflectionReturnType = typeToString(reflectedMethod.getGenericReturnType());
List<String> jdiffParamList = jDiffMethod.mParamList;
// Next, compare the return types of the two methods. If
// they aren't equal, the methods can't match.
if (!jdiffReturnType.equals(reflectionReturnType)) {
return false;
}
Type[] params = reflectedMethod.getGenericParameterTypes();
// Next, check the method parameters. If they have different
// parameter lengths, the two methods can't match.
if (jdiffParamList.size() != params.length) {
return false;
}
boolean piecewiseParamsMatch = true;
// Compare method parameters piecewise and return true if they all match.
for (int i = 0; i < jdiffParamList.size(); i++) {
piecewiseParamsMatch &= compareParam(jdiffParamList.get(i), params[i]);
}
if (piecewiseParamsMatch) {
return true;
}
/** NOTE: There are cases where piecewise method parameter checking
* fails even though the strings are equal, so compare entire strings
* against each other. This is not done by default to avoid a
* TransactionTooLargeException.
* Additionally, this can fail anyway due to extra
* information dug up by reflection.
*
* TODO: fix parameter equality checking and reflection matching
* See https://b.corp.google.com/issues/27726349
*/
StringBuilder reflectedMethodParams = new StringBuilder("");
StringBuilder jdiffMethodParams = new StringBuilder("");
for (int i = 0; i < jdiffParamList.size(); i++) {
jdiffMethodParams.append(jdiffParamList.get(i));
reflectedMethodParams.append(params[i]);
}
String jDiffFName = jdiffMethodParams.toString();
String refName = reflectedMethodParams.toString();
return jDiffFName.equals(refName);
}
/**
* Finds the reflected method specified by the method description.
*
* @param method description of the method to find
* @return the reflected method, or null if not found.
*/
@SuppressWarnings("unchecked")
private Method findMatchingMethod(JDiffMethod method) {
Method[] methods = mClass.getDeclaredMethods();
for (Method m : methods) {
if (matches(method, m)) {
return m;
}
}
return null;
}
/**
* Compares the parameter from the API and the parameter from
* reflection.
*
* @param jdiffParam param parsed from the API xml file.
* @param reflectionParamType param gotten from the Java reflection.
* @return True if the two params match, otherwise return false.
*/
private static boolean compareParam(String jdiffParam, Type reflectionParamType) {
if (jdiffParam == null) {
return false;
}
String reflectionParam = typeToString(reflectionParamType);
// 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;
}
/**
* Checks whether the constructor parsed from API xml file and
* Java reflection are compliant.
*/
@SuppressWarnings("unchecked")
private void checkConstructorCompliance() {
for (JDiffConstructor con : jDiffConstructors) {
try {
Constructor<?> c = findMatchingConstructor(con);
if (c == null) {
mResultObserver.notifyFailure(FailureType.MISSING_METHOD,
con.toReadableString(mAbsoluteClassName),
"No method with correct signature found:" +
con.toSignatureString());
} else {
if (c.isVarArgs()) {// some method's parameter are variable args
con.mModifier |= METHOD_MODIFIER_VAR_ARGS;
}
if (c.getModifiers() != con.mModifier) {
mResultObserver.notifyFailure(
FailureType.MISMATCH_METHOD,
con.toReadableString(mAbsoluteClassName),
"Non-compatible method found when looking for " +
con.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking constructor compliance", e);
mResultObserver.notifyFailure(FailureType.CAUGHT_EXCEPTION,
con.toReadableString(mAbsoluteClassName),
"Exception!");
}
}
}
/**
* Searches available constructor.
*
* @param jdiffDes constructor description to find.
* @return reflected constructor, or null if not found.
*/
@SuppressWarnings("unchecked")
private Constructor<?> findMatchingConstructor(JDiffConstructor jdiffDes) {
for (Constructor<?> c : mClass.getDeclaredConstructors()) {
Type[] params = c.getGenericParameterTypes();
boolean isStaticClass = ((mClass.getModifiers() & Modifier.STATIC) != 0);
int startParamOffset = 0;
int numberOfParams = params.length;
// non-static inner class -> skip implicit parent pointer
// as first arg
if (mClass.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), params[j])) {
isFound = false;
break;
}
++i;
++j;
}
if (isFound) {
return c;
}
}
}
return null;
}
/**
* Checks all fields in test class for compliance with the API
* xml.
*/
@SuppressWarnings("unchecked")
private void checkFieldsCompliance() {
for (JDiffField field : jDiffFields) {
try {
Field f = findMatchingField(field);
if (f == null) {
mResultObserver.notifyFailure(FailureType.MISSING_FIELD,
field.toReadableString(mAbsoluteClassName),
"No field with correct signature found:" +
field.toSignatureString());
} else if (f.getModifiers() != field.mModifier) {
mResultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
field.toReadableString(mAbsoluteClassName),
"Non-compatible field modifiers found when looking for " +
field.toSignatureString());
} else if (!checkFieldValueCompliance(field, f)) {
mResultObserver.notifyFailure(FailureType.MISMATCH_FIELD,
field.toReadableString(mAbsoluteClassName),
"Incorrect field value found when looking for " +
field.toSignatureString());
}else if (!f.getType().getCanonicalName().equals(field.mFieldType)) {
// type name does not match, but this might be a generic
String genericTypeName = null;
Type type = f.getGenericType();
if (type != null) {
genericTypeName = type instanceof Class ? ((Class) type).getName() :
type.toString().replace('$', '.');
}
if (genericTypeName == null || !genericTypeName.equals(field.mFieldType)) {
mResultObserver.notifyFailure(
FailureType.MISMATCH_FIELD,
field.toReadableString(mAbsoluteClassName),
"Non-compatible field type found when looking for " +
field.toSignatureString());
}
}
} catch (Exception e) {
loge("Got exception when checking field compliance", e);
mResultObserver.notifyFailure(
FailureType.CAUGHT_EXCEPTION,
field.toReadableString(mAbsoluteClassName),
"Exception!");
}
}
}
/**
* Checks whether the field values are compatible.
*
* @param apiField The field as defined by the platform API.
* @param deviceField The field as defined by the device under test.
*/
private boolean checkFieldValueCompliance(JDiffField apiField, Field deviceField)
throws IllegalAccessException {
if ((apiField.mModifier & Modifier.FINAL) == 0 ||
(apiField.mModifier & Modifier.STATIC) == 0) {
// Only final static fields can have fixed values.
return true;
}
if (apiField.getValueString() == null) {
// If we don't define a constant value for it, then it can be anything.
return true;
}
// Some fields may be protected or package-private
deviceField.setAccessible(true);
switch(apiField.mFieldType) {
case "byte":
return Objects.equals(apiField.getValueString(),
Byte.toString(deviceField.getByte(null)));
case "char":
return Objects.equals(apiField.getValueString(),
Integer.toString(deviceField.getChar(null)));
case "short":
return Objects.equals(apiField.getValueString(),
Short.toString(deviceField.getShort(null)));
case "int":
return Objects.equals(apiField.getValueString(),
Integer.toString(deviceField.getInt(null)));
case "long":
return Objects.equals(apiField.getValueString(),
Long.toString(deviceField.getLong(null)) + "L");
case "float":
return Objects.equals(apiField.getValueString(),
canonicalizeFloatingPoint(
Float.toString(deviceField.getFloat(null)), "f"));
case "double":
return Objects.equals(apiField.getValueString(),
canonicalizeFloatingPoint(
Double.toString(deviceField.getDouble(null)), ""));
case "boolean":
return Objects.equals(apiField.getValueString(),
Boolean.toString(deviceField.getBoolean(null)));
case "java.lang.String":
String value = apiField.getValueString();
// Remove the quotes the value string is wrapped in
value = unescapeFieldStringValue(value.substring(1, value.length() - 1));
return Objects.equals(value, deviceField.get(null));
default:
return true;
}
}
/**
* Canonicalize the string representation of floating point numbers.
*
* This needs to be kept in sync with the doclava canonicalization.
*/
private static final String canonicalizeFloatingPoint(String val, String suffix) {
if (val.equals("Infinity")) {
return "(1.0" + suffix + "/0.0" + suffix + ")";
} else if (val.equals("-Infinity")) {
return "(-1.0" + suffix + "/0.0" + suffix + ")";
} else if (val.equals("NaN")) {
return "(0.0" + suffix + "/0.0" + suffix + ")";
}
String str = val.toString();
if (str.indexOf('E') != -1) {
return str + suffix;
}
// 1.0 is the only case where a trailing "0" is allowed.
// 1.00 is canonicalized as 1.0.
int i = str.length() - 1;
int d = str.indexOf('.');
while (i >= d + 2 && str.charAt(i) == '0') {
str = str.substring(0, i--);
}
return str + suffix;
}
// This unescapes the string format used by doclava and so needs to be kept in sync with any
// changes made to that format.
private static String unescapeFieldStringValue(String str) {
final int N = str.length();
// If there's no special encoding strings in the string then just return it.
if (str.indexOf('\\') == -1) {
return str;
}
final StringBuilder buf = new StringBuilder(str.length());
char escaped = 0;
final int START = 0;
final int CHAR1 = 1;
final int CHAR2 = 2;
final int CHAR3 = 3;
final int CHAR4 = 4;
final int ESCAPE = 5;
int state = START;
for (int i=0; i<N; i++) {
final char c = str.charAt(i);
switch (state) {
case START:
if (c == '\\') {
state = ESCAPE;
} else {
buf.append(c);
}
break;
case ESCAPE:
switch (c) {
case '\\':
buf.append('\\');
state = START;
break;
case 't':
buf.append('\t');
state = START;
break;
case 'b':
buf.append('\b');
state = START;
break;
case 'r':
buf.append('\r');
state = START;
break;
case 'n':
buf.append('\n');
state = START;
break;
case 'f':
buf.append('\f');
state = START;
break;
case '\'':
buf.append('\'');
state = START;
break;
case '\"':
buf.append('\"');
state = START;
break;
case 'u':
state = CHAR1;
escaped = 0;
break;
}
break;
case CHAR1:
case CHAR2:
case CHAR3:
case CHAR4:
escaped <<= 4;
if (c >= '0' && c <= '9') {
escaped |= c - '0';
} else if (c >= 'a' && c <= 'f') {
escaped |= 10 + (c - 'a');
} else if (c >= 'A' && c <= 'F') {
escaped |= 10 + (c - 'A');
} else {
throw new RuntimeException(
"bad escape sequence: '" + c + "' at pos " + i + " in: \""
+ str + "\"");
}
if (state == CHAR4) {
buf.append(escaped);
state = START;
} else {
state++;
}
break;
}
}
if (state != START) {
throw new RuntimeException("unfinished escape sequence: " + str);
}
return buf.toString();
}
/**
* Finds the reflected field specified by the field description.
*
* @param field the field description to find
* @return the reflected field, or null if not found.
*/
private Field findMatchingField(JDiffField field) {
return mClassFieldMap.get(field.mName);
}
/**
* Gets the list of fields found within this class.
*
* @return the list of fields.
*/
public Collection<JDiffField> getFieldList() {
return jDiffFields;
}
/**
* Checks if the class under test has compliant modifiers compared to the API.
*
* @return true if modifiers are compliant.
*/
private boolean checkClassModifiersCompliance() {
int reflectionModifier = mClass.getModifiers();
int apiModifier = mModifier;
// If the api class isn't abstract
if (((apiModifier & Modifier.ABSTRACT) == 0) &&
// but the reflected class is
((reflectionModifier & Modifier.ABSTRACT) != 0) &&
// and it isn't an enum
!isEnumType()) {
// that is a problem
return false;
}
// ABSTRACT check passed, so mask off ABSTRACT
reflectionModifier &= ~Modifier.ABSTRACT;
apiModifier &= ~Modifier.ABSTRACT;
if (isAnnotation()) {
reflectionModifier &= ~CLASS_MODIFIER_ANNOTATION;
}
if (mClass.isInterface()) {
reflectionModifier &= ~(Modifier.INTERFACE);
}
if (isEnumType() && mClass.isEnum()) {
reflectionModifier &= ~CLASS_MODIFIER_ENUM;
}
return ((reflectionModifier == apiModifier) &&
(isEnumType() == mClass.isEnum()));
}
/**
* Checks if the class under test is compliant with regards to
* annnotations when compared to the API.
*
* @return true if the class is compliant
*/
private boolean checkClassAnnotationCompliace() {
if (mClass.isAnnotation()) {
// check annotation
for (String inter : implInterfaces) {
if ("java.lang.annotation.Annotation".equals(inter)) {
return true;
}
}
return false;
}
return true;
}
/**
* Checks if the class under test extends the proper classes
* according to the API.
*
* @return true if the class is compliant.
*/
private boolean checkClassExtendsCompliance() {
// Nothing to check if it doesn't extend anything.
if (mExtendedClass != null) {
Class<?> superClass = mClass.getSuperclass();
while (superClass != null) {
if (superClass.getCanonicalName().equals(mExtendedClass)) {
return true;
}
superClass = superClass.getSuperclass();
}
// Couldn't find a matching superclass.
return false;
}
return true;
}
/**
* Checks if the class under test implements the proper interfaces
* according to the API.
*
* @return true if the class is compliant
*/
private boolean checkClassImplementsCompliance() {
Class<?>[] interfaces = mClass.getInterfaces();
Set<String> interFaceSet = new HashSet<String>();
for (Class<?> c : interfaces) {
interFaceSet.add(c.getCanonicalName());
}
for (String inter : implInterfaces) {
if (!interFaceSet.contains(inter)) {
return false;
}
}
return true;
}
/**
* Validate that an interfaces method count is as expected.
*/
private List<String> checkInterfaceMethodCompliance() {
List<String> unexpectedMethods = new ArrayList<>();
for (Method method : mClass.getDeclaredMethods()) {
if (method.isDefault()) {
continue;
}
if (method.isSynthetic()) {
continue;
}
if (method.isBridge()) {
continue;
}
if (HIDDEN_INTERFACE_WHITELIST.contains(method.toGenericString())) {
continue;
}
boolean foundMatch = false;
for (JDiffMethod jdiffMethod : jDiffMethods) {
if (matches(jdiffMethod, method)) {
foundMatch = true;
}
}
if (!foundMatch) {
unexpectedMethods.add(method.toGenericString());
}
}
return unexpectedMethods;
}
/**
* Checks that the class found through reflection matches the
* specification from the API xml file.
*/
@SuppressWarnings("unchecked")
private void checkClassCompliance() {
try {
mAbsoluteClassName = mPackageName + "." + mShortClassName;
mClass = findMatchingClass();
if (mClass == null) {
// No class found, notify the observer according to the class type
if (JDiffType.INTERFACE.equals(mClassType)) {
mResultObserver.notifyFailure(FailureType.MISSING_INTERFACE,
mAbsoluteClassName,
"Classloader is unable to find " + mAbsoluteClassName);
} else {
mResultObserver.notifyFailure(FailureType.MISSING_CLASS,
mAbsoluteClassName,
"Classloader is unable to find " + mAbsoluteClassName);
}
return;
}
List<String> methods = checkInterfaceMethodCompliance();
if (JDiffType.INTERFACE.equals(mClassType) && methods.size() > 0) {
mResultObserver.notifyFailure(FailureType.MISMATCH_INTERFACE_METHOD,
mAbsoluteClassName, "Interfaces cannot be modified: "
+ mAbsoluteClassName + ": " + methods);
return;
}
if (!checkClassModifiersCompliance()) {
logMismatchInterfaceSignature(mAbsoluteClassName,
"Non-compatible class found when looking for " +
toSignatureString());
return;
}
if (!checkClassAnnotationCompliace()) {
logMismatchInterfaceSignature(mAbsoluteClassName,
"Annotation mismatch");
return;
}
if (!mClass.isAnnotation()) {
// check father class
if (!checkClassExtendsCompliance()) {
logMismatchInterfaceSignature(mAbsoluteClassName,
"Extends mismatch");
return;
}
// check implements interface
if (!checkClassImplementsCompliance()) {
logMismatchInterfaceSignature(mAbsoluteClassName,
"Implements mismatch");
return;
}
}
} catch (Exception e) {
loge("Got exception when checking field compliance", e);
mResultObserver.notifyFailure(
FailureType.CAUGHT_EXCEPTION,
mAbsoluteClassName,
"Exception!");
}
}
/**
* Convert the class into a printable signature string.
*
* @return the signature string
*/
public String toSignatureString() {
StringBuffer sb = new StringBuffer();
String accessLevel = convertModifiersToAccessLevel(mModifier);
if (!"".equals(accessLevel)) {
sb.append(accessLevel).append(" ");
}
if (!JDiffType.INTERFACE.equals(mClassType)) {
String modifierString = convertModifersToModifierString(mModifier);
if (!"".equals(modifierString)) {
sb.append(modifierString).append(" ");
}
sb.append("class ");
} else {
sb.append("interface ");
}
// class name
sb.append(mShortClassName);
// does it extends something?
if (mExtendedClass != null) {
sb.append(" extends ").append(mExtendedClass).append(" ");
}
// implements something?
if (implInterfaces.size() > 0) {
sb.append(" implements ");
for (int x = 0; x < implInterfaces.size(); x++) {
String interf = implInterfaces.get(x);
sb.append(interf);
// if not last elements
if (x + 1 != implInterfaces.size()) {
sb.append(", ");
}
}
}
return sb.toString();
}
private void logMismatchInterfaceSignature(String classFullName, String errorMessage) {
if (JDiffType.INTERFACE.equals(mClassType)) {
mResultObserver.notifyFailure(FailureType.MISMATCH_INTERFACE,
classFullName,
errorMessage);
} else {
mResultObserver.notifyFailure(FailureType.MISMATCH_CLASS,
classFullName,
errorMessage);
}
}
/**
* Sees if the class under test is actually an enum.
*
* @return true if this class is enum
*/
private boolean isEnumType() {
return "java.lang.Enum".equals(mExtendedClass);
}
/**
* Finds the reflected class for the class under test.
*
* @return the reflected class, or null if not found.
*/
@SuppressWarnings("unchecked")
private Class<?> findMatchingClass() {
// even if there are no . in the string, split will return an
// array of length 1
String[] classNameParts = mShortClassName.split("\\.");
String currentName = mPackageName + "." + classNameParts[0];
try {
// Check to see if the class we're looking for is the top
// level class.
Class<?> clz = Class.forName(currentName,
false,
this.getClass().getClassLoader());
if (clz.getCanonicalName().equals(mAbsoluteClassName)) {
return clz;
}
// Then it must be an inner class.
for (int x = 1; x < classNameParts.length; x++) {
clz = findInnerClassByName(clz, classNameParts[x]);
if (clz == null) {
return null;
}
if (clz.getCanonicalName().equals(mAbsoluteClassName)) {
return clz;
}
}
} catch (ClassNotFoundException e) {
loge("ClassNotFoundException for " + mPackageName + "." + mShortClassName, e);
return null;
}
return null;
}
/**
* Searches the class for the specified inner class.
*
* @param clz the class to search in.
* @param simpleName the simpleName of the class to find
* @returns the class being searched for, or null if it can't be found.
*/
private Class<?> findInnerClassByName(Class<?> clz, String simpleName) {
for (Class<?> c : clz.getDeclaredClasses()) {
if (c.getSimpleName().equals(simpleName)) {
return c;
}
}
return null;
}
/**
* Sees if the class under test is actually an annotation.
*
* @return true if this class is Annotation.
*/
private boolean isAnnotation() {
if (implInterfaces.contains("java.lang.annotation.Annotation")) {
return true;
}
return false;
}
/**
* Gets the class name for the class under test.
*
* @return the class name.
*/
public String getClassName() {
return mShortClassName;
}
/**
* Gets the package name + short class name
*
* @return The package + short class name
*/
public String getAbsoluteClassName() {
return mAbsoluteClassName;
}
/**
* Sets the modifier for the class under test.
*
* @param modifier the modifier
*/
public void setModifier(int modifier) {
mModifier = modifier;
}
/**
* Sets the return type for the class under test.
*
* @param type the return type
*/
public void setType(JDiffType type) {
mClassType = type;
}
/**
* Sets the class that is beign extended for the class under test.
*
* @param extendsClass the class being extended.
*/
public void setExtendsClass(String extendsClass) {
mExtendedClass = extendsClass;
}
/**
* Registers a ResultObserver to process the output from the
* compliance testing done in this class.
*
* @param resultObserver the observer to register.
*/
public void registerResultObserver(ResultObserver resultObserver) {
mResultObserver = resultObserver;
}
/**
* Converts WildcardType array into a jdiff compatible string..
* This is a helper function for typeToString.
*
* @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", "?")
.replace("? super java.lang.Object", "? super ?");
}
/**
* Scan a class (an its entire inheritance chain) for fields.
*
* @return a {@link Map} of fieldName to {@link Field}
*/
private static Map<String, Field> buildFieldMap(Class testClass) {
Map<String, Field> fieldMap = new HashMap<String, Field>();
// Scan the superclass
if (testClass.getSuperclass() != null) {
fieldMap.putAll(buildFieldMap(testClass.getSuperclass()));
}
// Scan the interfaces
for (Class interfaceClass : testClass.getInterfaces()) {
fieldMap.putAll(buildFieldMap(interfaceClass));
}
// Check the fields in the test class
for (Field field : testClass.getDeclaredFields()) {
fieldMap.put(field.getName(), field);
}
return fieldMap;
}
private static void loge(String message, Exception exception) {
System.err.println(String.format("%s: %s", message, exception));
}
}