blob: e77d7e0829fa9275ef7a8e3e6dfc55f7ae49ffcd [file] [log] [blame]
/*
* Copyright (C) 2019 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 android.signature.cts.JDiffClassDescription.JDiffConstructor;
import android.signature.cts.JDiffClassDescription.JDiffField;
import android.signature.cts.JDiffClassDescription.JDiffMethod;
import android.util.Log;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.Spliterator;
import java.util.function.Consumer;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.GZIPInputStream;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
/**
* Parser for the XML representation of an API specification.
*/
class XmlApiParser extends ApiParser {
private static final String TAG_ROOT = "api";
private static final String TAG_PACKAGE = "package";
private static final String TAG_CLASS = "class";
private static final String TAG_INTERFACE = "interface";
private static final String TAG_IMPLEMENTS = "implements";
private static final String TAG_CONSTRUCTOR = "constructor";
private static final String TAG_METHOD = "method";
private static final String TAG_PARAM = "parameter";
private static final String TAG_EXCEPTION = "exception";
private static final String TAG_FIELD = "field";
private static final String ATTRIBUTE_NAME = "name";
private static final String ATTRIBUTE_TYPE = "type";
private static final String ATTRIBUTE_VALUE = "value";
private static final String ATTRIBUTE_EXTENDS = "extends";
private static final String ATTRIBUTE_RETURN = "return";
private static final String MODIFIER_ABSTRACT = "abstract";
private static final String MODIFIER_FINAL = "final";
private static final String MODIFIER_NATIVE = "native";
private static final String MODIFIER_PRIVATE = "private";
private static final String MODIFIER_PROTECTED = "protected";
private static final String MODIFIER_PUBLIC = "public";
private static final String MODIFIER_STATIC = "static";
private static final String MODIFIER_SYNCHRONIZED = "synchronized";
private static final String MODIFIER_TRANSIENT = "transient";
private static final String MODIFIER_VOLATILE = "volatile";
private static final String MODIFIER_VISIBILITY = "visibility";
private static final Set<String> KEY_TAG_SET;
static {
KEY_TAG_SET = new HashSet<>();
Collections.addAll(KEY_TAG_SET,
TAG_PACKAGE,
TAG_CLASS,
TAG_INTERFACE,
TAG_IMPLEMENTS,
TAG_CONSTRUCTOR,
TAG_METHOD,
TAG_PARAM,
TAG_EXCEPTION,
TAG_FIELD);
}
private final String tag;
private final boolean gzipped;
private final XmlPullParserFactory factory;
XmlApiParser(String tag, boolean gzipped) {
this.tag = tag;
this.gzipped = gzipped;
try {
factory = XmlPullParserFactory.newInstance();
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
}
}
/**
* Load field information from xml to memory.
*
* @param currentClass
* of the class being examined which will be shown in error messages
* @param parser
* The XmlPullParser which carries the xml information.
* @return the new field
*/
private static JDiffField loadFieldInfo(
JDiffClassDescription currentClass, XmlPullParser parser) {
String fieldName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
String fieldType = canonicalizeType(parser.getAttributeValue(null, ATTRIBUTE_TYPE));
int modifier = jdiffModifierToReflectionFormat(currentClass.getClassName(), parser);
String value = parser.getAttributeValue(null, ATTRIBUTE_VALUE);
if (currentClass.isEnumType() && fieldType.equals(currentClass.getAbsoluteClassName())
&& (value != null && value.startsWith("PsiEnumConstant:"))) {
// We don't need to check the value.
value = null;
}
// Canonicalize the expected value to ensure that it is consistent with the values obtained
// using reflection by ApiComplianceChecker.getFieldValueAsString(...).
if (value != null) {
// An unquoted null String value actually means null. It cannot be confused with a
// String containing the word null as that would be surrounded with double quotes.
if (value.equals("null")) {
value = null;
} else {
switch (fieldType) {
case "java.lang.String":
value = unescapeFieldStringValue(value);
break;
case "char":
// A character may be encoded in XML as its numeric value. Convert it to a
// string containing the single character.
try {
char c = (char) Integer.parseInt(value);
value = String.valueOf(c);
} catch (NumberFormatException e) {
// If not, it must be a string "'?'". Extract the second character,
// but we need to unescape it.
int len = value.length();
if (value.charAt(0) == '\'' && value.charAt(len - 1) == '\'') {
String sub = value.substring(1, len - 1);
value = unescapeFieldStringValue(sub);
} else {
throw new NumberFormatException(String.format(
"Cannot parse the value of field '%s': invalid number '%s'",
fieldName, value));
}
}
break;
case "double":
switch (value) {
case "(-1.0/0.0)":
value = "-Infinity";
break;
case "(0.0/0.0)":
value = "NaN";
break;
case "(1.0/0.0)":
value = "Infinity";
break;
}
break;
case "float":
switch (value) {
case "(-1.0f/0.0f)":
value = "-Infinity";
break;
case "(0.0f/0.0f)":
value = "NaN";
break;
case "(1.0f/0.0f)":
value = "Infinity";
break;
default:
// Remove the trailing f.
if (value.endsWith("f")) {
value = value.substring(0, value.length() - 1);
}
}
break;
case "long":
// Remove the trailing L.
if (value.endsWith("L")) {
value = value.substring(0, value.length() - 1);
}
break;
}
}
}
return new JDiffField(fieldName, fieldType, modifier, value);
}
/**
* Load method information from xml to memory.
*
* @param className
* of the class being examined which will be shown in error messages
* @param parser
* The XmlPullParser which carries the xml information.
* @return the newly loaded method.
*/
private static JDiffMethod loadMethodInfo(String className, XmlPullParser parser) {
String methodName = parser.getAttributeValue(null, ATTRIBUTE_NAME);
String returnType = parser.getAttributeValue(null, ATTRIBUTE_RETURN);
int modifier = jdiffModifierToReflectionFormat(className, parser);
return new JDiffMethod(methodName, modifier, canonicalizeType(returnType));
}
/**
* Load constructor information from xml to memory.
*
* @param parser
* The XmlPullParser which carries the xml information.
* @param currentClass
* the current class being loaded.
* @return the new constructor
*/
private static JDiffConstructor loadConstructorInfo(
XmlPullParser parser, JDiffClassDescription currentClass) {
String name = currentClass.getClassName();
int modifier = jdiffModifierToReflectionFormat(name, parser);
return new JDiffConstructor(name, modifier);
}
/**
* Load class or interface information to memory.
*
* @param parser
* The XmlPullParser which carries the xml information.
* @param isInterface
* true if the current class is an interface, otherwise is false.
* @param pkg
* the name of the java package this class can be found in.
* @return the new class description.
*/
private static JDiffClassDescription loadClassInfo(
XmlPullParser parser, boolean isInterface, String pkg) {
String className = parser.getAttributeValue(null, ATTRIBUTE_NAME);
JDiffClassDescription currentClass = new JDiffClassDescription(pkg, className);
currentClass.setType(isInterface ? JDiffClassDescription.JDiffType.INTERFACE :
JDiffClassDescription.JDiffType.CLASS);
String superClass = stripGenericsArgs(parser.getAttributeValue(null, ATTRIBUTE_EXTENDS));
int modifiers = jdiffModifierToReflectionFormat(className, parser);
if (isInterface) {
if (superClass != null) {
currentClass.addImplInterface(superClass);
}
} else {
if ("java.lang.annotation.Annotation".equals(superClass)) {
// ApiComplianceChecker expects "java.lang.annotation.Annotation" to be in
// the "impl interfaces".
currentClass.addImplInterface(superClass);
} else {
currentClass.setExtendsClass(superClass);
}
}
currentClass.setModifier(modifiers);
return currentClass;
}
/**
* Transfer string modifier to int one.
*
* @param name
* of the class/method/field being examined which will be shown in error messages
* @param parser
* XML resource parser
* @return converted modifier
*/
private static int jdiffModifierToReflectionFormat(String name, XmlPullParser parser) {
int modifier = 0;
for (int i = 0; i < parser.getAttributeCount(); i++) {
modifier |= modifierDescriptionToReflectedType(name, parser.getAttributeName(i),
parser.getAttributeValue(i));
}
return modifier;
}
/**
* Convert string modifier to int modifier.
*
* @param name
* of the class/method/field being examined which will be shown in error messages
* @param key
* modifier name
* @param value
* modifier value
* @return converted modifier value
*/
private static int modifierDescriptionToReflectedType(String name, String key, String value) {
switch (key) {
case MODIFIER_ABSTRACT:
return value.equals("true") ? Modifier.ABSTRACT : 0;
case MODIFIER_FINAL:
return value.equals("true") ? Modifier.FINAL : 0;
case MODIFIER_NATIVE:
return value.equals("true") ? Modifier.NATIVE : 0;
case MODIFIER_STATIC:
return value.equals("true") ? Modifier.STATIC : 0;
case MODIFIER_SYNCHRONIZED:
return value.equals("true") ? Modifier.SYNCHRONIZED : 0;
case MODIFIER_TRANSIENT:
return value.equals("true") ? Modifier.TRANSIENT : 0;
case MODIFIER_VOLATILE:
return value.equals("true") ? Modifier.VOLATILE : 0;
case MODIFIER_VISIBILITY:
switch (value) {
case MODIFIER_PRIVATE:
throw new RuntimeException("Private visibility found in API spec: " + name);
case MODIFIER_PROTECTED:
return Modifier.PROTECTED;
case MODIFIER_PUBLIC:
return Modifier.PUBLIC;
case "":
// If the visibility is "", it means it has no modifier.
// which is package private. We should return 0 for this modifier.
return 0;
default:
throw new RuntimeException("Unknown modifier found in API spec: " + value);
}
}
return 0;
}
@Override
public Stream<JDiffClassDescription> parseAsStream(VirtualPath path) {
XmlPullParser parser;
try {
parser = factory.newPullParser();
InputStream input = path.newInputStream();
if (gzipped) {
input = new GZIPInputStream(input);
}
parser.setInput(input, null);
return StreamSupport
.stream(new ClassDescriptionSpliterator(parser), false);
} catch (XmlPullParserException | IOException e) {
throw new RuntimeException("Could not parse " + path, e);
}
}
private static String stripGenericsArgs(String typeName) {
return typeName == null ? null : typeName.replaceFirst("<.*", "");
}
private class ClassDescriptionSpliterator implements Spliterator<JDiffClassDescription> {
private final XmlPullParser parser;
JDiffClassDescription currentClass = null;
String currentPackage = "";
JDiffMethod currentMethod = null;
ClassDescriptionSpliterator(XmlPullParser parser)
throws IOException, XmlPullParserException {
this.parser = parser;
logd(String.format("Name: %s", parser.getName()));
logd(String.format("Text: %s", parser.getText()));
logd(String.format("Namespace: %s", parser.getNamespace()));
logd(String.format("Line Number: %s", parser.getLineNumber()));
logd(String.format("Column Number: %s", parser.getColumnNumber()));
logd(String.format("Position Description: %s", parser.getPositionDescription()));
beginDocument(parser);
}
@Override
public boolean tryAdvance(Consumer<? super JDiffClassDescription> action) {
JDiffClassDescription classDescription;
try {
classDescription = next();
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException(e);
}
if (classDescription == null) {
return false;
}
action.accept(classDescription);
return true;
}
@Override
public Spliterator<JDiffClassDescription> trySplit() {
return null;
}
@Override
public long estimateSize() {
return Long.MAX_VALUE;
}
@Override
public int characteristics() {
return ORDERED | DISTINCT | NONNULL | IMMUTABLE;
}
private void beginDocument(XmlPullParser parser)
throws XmlPullParserException, IOException {
int type;
do {
type = parser.next();
} while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT);
if (type != XmlPullParser.START_TAG) {
throw new XmlPullParserException("No start tag found");
}
if (!parser.getName().equals(TAG_ROOT)) {
throw new XmlPullParserException("Unexpected start tag: found " + parser.getName() +
", expected " + TAG_ROOT);
}
}
private JDiffClassDescription next() throws IOException, XmlPullParserException {
int type;
while (true) {
do {
type = parser.next();
} while (type != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT
&& type != XmlPullParser.END_TAG);
if (type == XmlPullParser.END_DOCUMENT) {
logd("Reached end of document");
break;
}
String tagname = parser.getName();
if (type == XmlPullParser.END_TAG) {
if (TAG_CLASS.equals(tagname) || TAG_INTERFACE.equals(tagname)) {
logd("Reached end of class: " + currentClass);
return currentClass;
} else if (TAG_PACKAGE.equals(tagname)) {
currentPackage = "";
}
continue;
}
if (!KEY_TAG_SET.contains(tagname)) {
continue;
}
switch (tagname) {
case TAG_PACKAGE:
currentPackage = parser.getAttributeValue(null, ATTRIBUTE_NAME);
break;
case TAG_CLASS:
currentClass = loadClassInfo(parser, false, currentPackage);
break;
case TAG_INTERFACE:
currentClass = loadClassInfo(parser, true, currentPackage);
break;
case TAG_IMPLEMENTS:
currentClass.addImplInterface(stripGenericsArgs(
parser.getAttributeValue(null, ATTRIBUTE_NAME)));
break;
case TAG_CONSTRUCTOR:
JDiffConstructor constructor =
loadConstructorInfo(parser, currentClass);
currentClass.addConstructor(constructor);
currentMethod = constructor;
break;
case TAG_METHOD:
currentMethod = loadMethodInfo(currentClass.getClassName(), parser);
currentClass.addMethod(currentMethod);
break;
case TAG_PARAM:
String paramType = parser.getAttributeValue(null, ATTRIBUTE_TYPE);
currentMethod.addParam(canonicalizeType(paramType));
break;
case TAG_EXCEPTION:
currentMethod.addException(parser.getAttributeValue(null, ATTRIBUTE_TYPE));
break;
case TAG_FIELD:
JDiffField field = loadFieldInfo(currentClass, parser);
currentClass.addField(field);
break;
default:
throw new RuntimeException("unknown tag exception:" + tagname);
}
if (currentPackage != null) {
logd(String.format("currentPackage: %s", currentPackage));
}
if (currentClass != null) {
logd(String.format("currentClass: %s", currentClass.toSignatureString()));
}
if (currentMethod != null) {
logd(String.format("currentMethod: %s", currentMethod.toSignatureString()));
}
}
return null;
}
}
private void logd(String msg) {
Log.d(tag, msg);
}
// 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) {
// Skip over leading and trailing ".
int start = 0;
if (str.charAt(start) == '"') {
++start;
}
int end = str.length();
if (str.charAt(end - 1) == '"') {
--end;
}
// If there's no special encoding strings in the string then just return it without the
// leading and trailing "s.
if (str.indexOf('\\') == -1) {
return str.substring(start, end);
}
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 = start; i < end; 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();
}
/**
* Canonicalize a possibly generic type.
*/
private static String canonicalizeType(String type) {
// Remove trailing spaces after commas.
return type.replace(", ", ",");
}
}