/*
* Copyright (C) 2014 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.build.gradle.tasks.annotations;
import com.android.annotations.NonNull;
import com.android.annotations.VisibleForTesting;
import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Reads a signature file in the format of the new API files in frameworks/base/api */
public class ApiDatabase {
@NonNull
private final List<String> lines;
/** Map from class name to set of field names */
@NonNull private final Map<String,Set<String>> fieldMap =
Maps.newHashMapWithExpectedSize(1000);
/** Map from class name to map of method names whose values are overloaded signatures */
@NonNull private final Map<String,Map<String,List<String>>> methodMap =
Maps.newHashMapWithExpectedSize(1000);
@NonNull private final Map<String, List<String>> inheritsFrom =
Maps.newHashMapWithExpectedSize(1000);
@NonNull private final Map<String,Set<String>> intFieldMap =
Maps.newHashMapWithExpectedSize(1000);
@NonNull private final Set<String> classSet =
Sets.newHashSetWithExpectedSize(1000);
public ApiDatabase(@NonNull List<String> lines) {
this.lines = lines;
readApi();
}
public ApiDatabase(@NonNull File api) throws IOException {
this(Files.readLines(api, Charsets.UTF_8));
}
public boolean hasMethod(String className, String methodName, String arguments) {
// Perform raw lookup
className = getRawClass(className);
methodName = getRawMethod(methodName);
arguments = getRawParameterList(arguments);
Map<String, List<String>> methods = methodMap.get(className);
if (methods != null) {
List<String> strings = methods.get(methodName);
if (strings != null && strings.contains(arguments)) {
return true;
}
}
List<String> inheritsFrom = this.inheritsFrom.get(className);
if (inheritsFrom != null) {
for (String clz : inheritsFrom) {
if (hasMethod(clz, methodName, arguments)) {
return true;
}
}
}
return false;
}
public boolean hasField(String className, String fieldName) {
Set<String> fields = fieldMap.get(className);
if (fields != null && fields.contains(fieldName)) {
return true;
}
List<String> inheritsFrom = this.inheritsFrom.get(className);
if (inheritsFrom != null) {
for (String clz : inheritsFrom) {
if (hasField(clz, fieldName)) {
return true;
}
}
}
return false;
}
public boolean hasClass(String className) {
return classSet.contains(className);
}
public Set<String> getDeclaredIntFields(String className) {
return intFieldMap.get(className);
}
private void readApi() {
String MODIFIERS =
"((deprecated|public|static|private|protected|final|abstract|\\s*)\\s+)*";
Pattern PACKAGE = Pattern.compile("package (\\S+) \\{");
Pattern CLASS =
Pattern.compile(MODIFIERS
+ "(class|interface|enum)\\s+(\\S+)\\s+(extends (.+))?(implements (.+))?(.*)\\{");
Pattern METHOD = Pattern.compile("(method|ctor)\\s+" +
MODIFIERS + "(.+)??\\s+(\\S+)\\s*\\((.*)\\)(.*);");
Pattern CTOR = Pattern.compile("(method|ctor)\\s+.*\\((.*)\\)(.*);");
Pattern FIELD = Pattern.compile("(enum_constant|field)\\s+" +
MODIFIERS + "(.+)\\s+(\\S+)\\s*;");
String currentPackage = null;
String currentClass = null;
for (String line : lines) {
line = line.trim();
if (line.isEmpty() || line.equals("}")) {
continue;
}
if (line.startsWith("method ")) {
Matcher matcher = METHOD.matcher(line);
if (!matcher.matches()) {
Extractor.warning("Warning: Did not match as a member: " + line);
} else {
assert currentClass != null;
Map<String,List<String>> memberMap = methodMap.get(currentClass);
if (memberMap == null) {
memberMap = Maps.newHashMap();
methodMap.put(currentClass, memberMap);
methodMap.put(getRawClass(currentClass), memberMap);
}
String methodName = matcher.group(5);
List<String> signatures = memberMap.get(methodName);
if (signatures == null) {
signatures = Lists.newArrayList();
memberMap.put(methodName, signatures);
memberMap.put(getRawMethod(methodName), signatures);
}
String signature = matcher.group(6);
signature = signature.trim().replace(" ", "").replace(" ", "");
// normalize varargs: allow lookup with both formats
signatures.add(signature);
if (signature.endsWith("...")) {
signatures.add(signature.substring(0, signature.length() - 3) + "[]");
} else if (signature.endsWith("[]") && !signature.endsWith("[][]")) {
signatures.add(signature.substring(0, signature.length() - 2) + "...");
}
String raw = getRawParameterList(signature);
if (!signatures.contains(raw)) {
signatures.add(raw);
}
}
} else if (line.startsWith("ctor ")) {
Matcher matcher = CTOR.matcher(line);
if (!matcher.matches()) {
Extractor.warning("Warning: Did not match as a member: " + line);
} else {
assert currentClass != null;
Map<String,List<String>> memberMap = methodMap.get(currentClass);
if (memberMap == null) {
memberMap = Maps.newHashMap();
methodMap.put(currentClass, memberMap);
methodMap.put(getRawClass(currentClass), memberMap);
}
@SuppressWarnings("UnnecessaryLocalVariable")
String methodName = currentClass;
List<String> signatures = memberMap.get(methodName);
if (signatures == null) {
signatures = Lists.newArrayList();
memberMap.put(methodName, signatures);
String constructor = methodName.substring(methodName.lastIndexOf('.') + 1);
memberMap.put(constructor, signatures);
memberMap.put(getRawMethod(methodName), signatures);
memberMap.put(getRawMethod(constructor), signatures);
}
String signature = matcher.group(2);
signature = signature.trim().replace(" ", "").replace(" ", "");
if (signature.endsWith("...")) {
signatures.add(signature.substring(0, signature.length() - 3) + "[]");
} else if (signature.endsWith("[]") && !signature.endsWith("[][]")) {
signatures.add(signature.substring(0, signature.length() - 2) + "...");
}
signatures.add(signature);
String raw = getRawMethod(signature);
if (!signatures.contains(raw)) {
signatures.add(raw);
}
}
} else if (line.startsWith("enum_constant ") || line.startsWith("field ")) {
int equals = line.indexOf('=');
if (equals != -1) {
line = line.substring(0, equals).trim();
int semi = line.indexOf(';');
if (semi == -1) {
line = line + ';';
}
} else if (!line.endsWith(";")) {
int semi = line.indexOf(';');
if (semi != -1) {
line = line.substring(0, semi + 1);
}
}
Matcher matcher = FIELD.matcher(line);
if (!matcher.matches()) {
Extractor.warning("Warning: Did not match as a member: " + line);
} else {
assert currentClass != null;
String fieldName = matcher.group(5);
Set<String> fieldSet = fieldMap.get(currentClass);
if (fieldSet == null) {
fieldSet = Sets.newHashSet();
fieldMap.put(currentClass, fieldSet);
}
fieldSet.add(fieldName);
String type = matcher.group(4);
if (type.equals("int")) {
fieldSet = intFieldMap.get(currentClass);
if (fieldSet == null) {
fieldSet = Sets.newHashSet();
intFieldMap.put(currentClass, fieldSet);
}
fieldSet.add(fieldName);
}
}
} else if (line.startsWith("package ")) {
Matcher matcher = PACKAGE.matcher(line);
if (!matcher.matches()) {
Extractor.warning("Warning: Did not match as a package: " + line);
} else {
currentPackage = matcher.group(1);
}
} else {
Matcher matcher = CLASS.matcher(line);
if (!matcher.matches()) {
Extractor.warning("Warning: Did not match as a class/interface: " + line);
} else {
currentClass = currentPackage + '.' + matcher.group(4);
classSet.add(currentClass);
String superClass = matcher.group(6);
if (superClass != null) {
Splitter splitter = Splitter.on(' ').trimResults().omitEmptyStrings();
for (String from : splitter.split(superClass)) {
if (from.equals("implements")) { // workaround for broken regexp
continue;
}
addInheritsFrom(currentClass, from);
}
addInheritsFrom(currentClass, superClass.trim());
}
String implementsList = matcher.group(8);
if (implementsList != null) {
Splitter splitter = Splitter.on(' ').trimResults().omitEmptyStrings();
for (String from : splitter.split(implementsList)) {
addInheritsFrom(currentClass, from);
}
}
}
}
}
}
private void addInheritsFrom(String cls, String inheritsFrom) {
List<String> list = this.inheritsFrom.get(cls);
if (list == null) {
list = Lists.newArrayList();
this.inheritsFrom.put(cls, list);
}
list.add(inheritsFrom);
}
/** Drop generic type variables from a class name */
@VisibleForTesting
static String getRawClass(@NonNull String name) {
int index = name.indexOf('<');
if (index != -1) {
int end = name.indexOf('>', index + 1);
if (end == -1 || end == name.length() - 1) {
return name.substring(0, index);
} else {
// e.g. test.pkg.ArrayAdapter<T>.Inner
return name.substring(0, index) + name.substring(end + 1);
}
}
return name;
}
/** Drop generic type variables from a method or constructor name */
@VisibleForTesting
static String getRawMethod(@NonNull String name) {
int index = name.indexOf('<');
if (index != -1) {
return name.substring(0, index);
}
return name;
}
/** Drop generic type variables and varargs to produce a raw signature */
@VisibleForTesting
static String getRawParameterList(String signature) {
if (signature.indexOf('<') == -1 && !signature.endsWith("...")) {
return signature;
}
int n = signature.length();
StringBuilder sb = new StringBuilder(n);
int start = 0;
while (true) {
int index = signature.indexOf('<', start);
if (index == -1) {
sb.append(signature.substring(start));
break;
}
sb.append(signature.substring(start, index));
int balance = 1;
for (int i = index + 1; i < n; i++) {
char c = signature.charAt(i);
if (c == '<') {
balance++;
} else if (c == '>') {
balance--;
if (balance == 0) {
start = i + 1;
break;
}
}
}
}
// Normalize varargs... to []
if (sb.length() > 3
&& sb.charAt(sb.length() - 1) == '.'
&& sb.charAt(sb.length() - 2) == '.'
&& sb.charAt(sb.length() - 3) == '.') {
sb.setLength(sb.length() - 3);
sb.append('[').append(']');
}
return sb.toString();
}
}