| /* |
| * Copyright (C) 2015 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.tools.lint; |
| |
| import static com.android.SdkConstants.ATTR_NAME; |
| import static com.android.SdkConstants.DOT_JAR; |
| import static com.android.SdkConstants.FN_ANNOTATIONS_ZIP; |
| import static com.android.SdkConstants.VALUE_FALSE; |
| import static com.android.SdkConstants.VALUE_TRUE; |
| import static com.android.tools.lint.checks.SupportAnnotationDetector.PERMISSION_ANNOTATION; |
| |
| import com.android.annotations.NonNull; |
| import com.android.annotations.Nullable; |
| import com.android.annotations.VisibleForTesting; |
| import com.android.builder.model.AndroidLibrary; |
| import com.android.builder.model.AndroidProject; |
| import com.android.builder.model.Dependencies; |
| import com.android.builder.model.Variant; |
| import com.android.tools.lint.client.api.JavaParser.DefaultTypeDescriptor; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedAnnotation.Value; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedClass; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedField; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedMethod; |
| import com.android.tools.lint.client.api.JavaParser.ResolvedPackage; |
| import com.android.tools.lint.client.api.JavaParser.TypeDescriptor; |
| import com.android.tools.lint.client.api.LintClient; |
| import com.android.tools.lint.detector.api.LintUtils; |
| import com.android.tools.lint.detector.api.Project; |
| import com.android.utils.XmlUtils; |
| import com.google.common.base.Charsets; |
| import com.google.common.base.Splitter; |
| import com.google.common.collect.ArrayListMultimap; |
| import com.google.common.collect.Lists; |
| import com.google.common.collect.Maps; |
| import com.google.common.collect.Multimap; |
| import com.google.common.collect.Sets; |
| import com.google.common.io.ByteStreams; |
| import com.google.common.io.Closeables; |
| import com.google.common.io.Files; |
| |
| import org.w3c.dom.Document; |
| import org.w3c.dom.Element; |
| import org.w3c.dom.Node; |
| import org.w3c.dom.NodeList; |
| |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.lang.reflect.Modifier; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.jar.JarInputStream; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| import java.util.zip.ZipEntry; |
| |
| /** |
| * Handler for IntelliJ database files for external annotations. |
| * It can be pointed to an annotations .jar file, which it then reads, |
| * and can return {@link ResolvedAnnotation} instances when queried |
| * for annotations on a {@link ResolvedClass} or a {@link ResolvedMethod}, |
| * including its parameters. |
| */ |
| public class ExternalAnnotationRepository { |
| public static final String SDK_ANNOTATIONS_PATH = "platform-tools/api/annotations.zip"; //$NON-NLS-1$ |
| public static final String FN_ANNOTATIONS_XML = "annotations.xml"; //$NON-NLS-1$ |
| |
| private static final boolean DEBUG = false; |
| |
| private static ExternalAnnotationRepository sSingleton; |
| |
| private final List<AnnotationsDatabase> mDatabases; |
| |
| private ExternalAnnotationRepository(@NonNull List<AnnotationsDatabase> databases) { |
| mDatabases = databases; |
| } |
| |
| @NonNull |
| public static synchronized ExternalAnnotationRepository get(@NonNull LintClient client) { |
| if (sSingleton == null) { |
| HashSet<AndroidLibrary> seen = Sets.newHashSet(); |
| Collection<Project> projects = client.getKnownProjects(); |
| List<File> files = Lists.newArrayListWithExpectedSize(2); |
| for (Project project : projects) { |
| if (project.isGradleProject()) { |
| Variant variant = project.getCurrentVariant(); |
| AndroidProject model = project.getGradleProjectModel(); |
| if (model != null && variant != null) { |
| Dependencies dependencies = variant.getMainArtifact().getDependencies(); |
| for (AndroidLibrary library : dependencies.getLibraries()) { |
| addLibraries(files, library, seen); |
| } |
| } |
| } |
| } |
| |
| File sdkAnnotations = client.findResource(SDK_ANNOTATIONS_PATH); |
| if (sdkAnnotations == null) { |
| // Until the SDK annotations are bundled in platform tools, provide |
| // a fallback for Gradle builds to point to a locally installed version |
| String path = System.getenv("SDK_ANNOTATIONS"); |
| if (path != null) { |
| sdkAnnotations = new File(path); |
| if (!sdkAnnotations.exists()) { |
| sdkAnnotations = null; |
| } |
| } |
| } |
| if (sdkAnnotations != null) { |
| files.add(sdkAnnotations); |
| } |
| |
| sSingleton = create(client, files); |
| } |
| |
| return sSingleton; |
| } |
| |
| @VisibleForTesting |
| @NonNull |
| static synchronized ExternalAnnotationRepository create( |
| @Nullable LintClient client, |
| @NonNull List<File> files) { |
| long begin; |
| if (DEBUG) { |
| begin = System.currentTimeMillis(); |
| } |
| |
| List<AnnotationsDatabase> databases = Lists.newArrayListWithExpectedSize(files.size()); |
| for (File file : files) { |
| try { |
| AnnotationsDatabase database = getDatabase(file); |
| if (database != null) { |
| databases.add(database); |
| } |
| } catch (IOException ioe) { |
| if (client != null) { |
| client.log(ioe, "Could not read %1$s", file.getPath()); |
| } else { |
| ioe.printStackTrace(); |
| } |
| } |
| } |
| |
| ExternalAnnotationRepository manager = new ExternalAnnotationRepository(databases); |
| |
| if (DEBUG) { |
| long end = System.currentTimeMillis(); |
| System.out.println("Initialization of annotations took " + (end - begin) + " ms"); |
| } |
| |
| return manager; |
| } |
| |
| private static void addLibraries( |
| @NonNull List<File> result, |
| @NonNull AndroidLibrary library, |
| Set<AndroidLibrary> seen) { |
| if (seen.contains(library)) { |
| return; |
| } |
| seen.add(library); |
| |
| // As of 1.2 this is available in the model: |
| // https://android-review.googlesource.com/#/c/137750/ |
| // Switch over to this when it's in more common usage |
| // (until it is, we'll pay for failed proxying errors) |
| File zip = new File(library.getResFolder().getParent(), FN_ANNOTATIONS_ZIP); |
| if (zip.exists()) { |
| result.add(zip); |
| } |
| |
| for (AndroidLibrary dependency : library.getLibraryDependencies()) { |
| addLibraries(result, dependency, seen); |
| } |
| } |
| |
| @Nullable |
| private static AnnotationsDatabase getDatabase( |
| @NonNull LintClient client, |
| @NonNull File file) { |
| try { |
| return file.isFile() ? new AnnotationsDatabase(file) : null; |
| } catch (IOException ioe) { |
| client.log(ioe, "Could not read %1$s", file.getPath()); |
| return null; |
| } |
| } |
| |
| @VisibleForTesting |
| @Nullable |
| static AnnotationsDatabase getDatabase(@NonNull File file) throws IOException { |
| return file.exists() ? new AnnotationsDatabase(file) : null; |
| } |
| |
| @Nullable |
| private static AnnotationsDatabase getDatabase( |
| @NonNull LintClient client, |
| @NonNull AndroidLibrary library) { |
| // As of 1.2 this is available in the model: |
| // https://android-review.googlesource.com/#/c/137750/ |
| // Switch over to this when it's in more common usage |
| // (until it is, we'll pay for failed proxying errors) |
| File zip = new File(library.getResFolder().getParent(), FN_ANNOTATIONS_ZIP); |
| return getDatabase(client, zip); |
| } |
| |
| // ---- Query methods ---- |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedMethod method, @NonNull String type) { |
| for (AnnotationsDatabase database : mDatabases) { |
| ResolvedAnnotation annotation = database.getAnnotation(method, type); |
| if (annotation != null) { |
| return annotation; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations(@NonNull ResolvedMethod method) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(method); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedMethod method, |
| int parameterIndex, @NonNull String type) { |
| for (AnnotationsDatabase database : mDatabases) { |
| ResolvedAnnotation annotation = database.getAnnotation(method, parameterIndex, type); |
| if (annotation != null) { |
| return annotation; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations( |
| @NonNull ResolvedMethod method, |
| int parameterIndex) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(method, |
| parameterIndex); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedClass cls, @NonNull String type) { |
| for (AnnotationsDatabase database : mDatabases) { |
| ResolvedAnnotation annotation = database.getAnnotation(cls, type); |
| if (annotation != null) { |
| return annotation; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations(@NonNull ResolvedClass cls) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(cls); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedField field, @NonNull String type) { |
| for (AnnotationsDatabase database : mDatabases) { |
| ResolvedAnnotation annotation = database.getAnnotation(field, type); |
| if (annotation != null) { |
| return annotation; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations(@NonNull ResolvedField field) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(field); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations(@NonNull ResolvedAnnotation cls) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(cls); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedPackage pkg, @NonNull String type) { |
| for (AnnotationsDatabase database : mDatabases) { |
| ResolvedAnnotation annotation = database.getAnnotation(pkg, type); |
| if (annotation != null) { |
| return annotation; |
| } |
| } |
| |
| return null; |
| } |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations(@NonNull ResolvedPackage pkg) { |
| for (AnnotationsDatabase database : mDatabases) { |
| Collection<ResolvedAnnotation> annotations = database.getAnnotations(pkg); |
| if (annotations != null) { |
| return annotations; |
| } |
| } |
| |
| return null; |
| } |
| |
| // ---- Reading from storage ---- |
| |
| private static final Pattern XML_SIGNATURE = Pattern.compile( |
| // Class (FieldName | Type? Name(ArgList) Argnum?) |
| "(\\S+) (\\S+|((.*)\\s+)?(\\S+)\\((.*)\\)( \\d+)?)"); |
| |
| /** Map from class fully qualified name to the class annotations info */ |
| // Query database |
| private static class ClassInfo { |
| public List<ResolvedAnnotation> annotations; |
| public Multimap<String,MethodInfo> methods; |
| public Map<String,FieldInfo> fields; |
| } |
| |
| private static class MethodInfo { |
| public String parameters; |
| public boolean constructor; |
| public List<ResolvedAnnotation> annotations; |
| public Multimap<Integer,ResolvedAnnotation> parameterAnnotations; |
| } |
| |
| private static class FieldInfo { |
| public List<ResolvedAnnotation> annotations; |
| } |
| |
| /** An {@linkplain AnnotationsDatabase} corresponds to a single external annotations .zip |
| * file (or if in the dev tree, a corresponding directory tree. |
| * <p> |
| * The SDK has an annotations database, and AAR libraries can also supply individual databases. |
| * The {@linkplain ExternalAnnotationRepository} class manages all of these and performs lookup |
| * into the various databases through a single entrypoint. |
| * */ |
| static class AnnotationsDatabase { |
| AnnotationsDatabase(@NonNull File file) throws IOException { |
| String path = file.getPath(); |
| if (path.endsWith(DOT_JAR) || path.endsWith(FN_ANNOTATIONS_ZIP)) { |
| initializeFromJar(file); |
| } else { |
| assert file.isDirectory() : file; |
| initializeFromDirectory(file); |
| } |
| } |
| |
| // ---- Query methods ---- |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedMethod method, |
| @NonNull String type) { |
| MethodInfo m = findMethod(method); |
| if (m == null) { |
| return null; |
| } |
| |
| if (m.annotations != null) { |
| for (ResolvedAnnotation annotation : m.annotations) { |
| if (type.equals(annotation.getSignature())) { |
| return annotation; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public List<ResolvedAnnotation> getAnnotations(@NonNull ResolvedMethod method) { |
| MethodInfo m = findMethod(method); |
| if (m == null) { |
| return null; |
| } |
| return m.annotations; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedMethod method, |
| int parameterIndex, @NonNull String type) { |
| MethodInfo m = findMethod(method); |
| if (m == null) { |
| return null; |
| } |
| |
| if (m.parameterAnnotations != null) { |
| Collection<ResolvedAnnotation> annotations = m.parameterAnnotations.get(parameterIndex); |
| if (annotations != null) { |
| for (ResolvedAnnotation annotation : annotations) { |
| if (type.equals(annotation.getSignature())) { |
| return annotation; |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public Collection<ResolvedAnnotation> getAnnotations( |
| @NonNull ResolvedMethod method, |
| int parameterIndex) { |
| MethodInfo m = findMethod(method); |
| if (m == null) { |
| return null; |
| } |
| |
| if (m.parameterAnnotations != null) { |
| return m.parameterAnnotations.get(parameterIndex); |
| } |
| |
| return m.annotations; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedClass cls, @NonNull String type) { |
| ClassInfo c = findClass(cls); |
| if (c == null) { |
| return null; |
| } |
| |
| if (c.annotations != null) { |
| for (ResolvedAnnotation annotation : c.annotations) { |
| if (type.equals(annotation.getSignature())) { |
| return annotation; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public List<ResolvedAnnotation> getAnnotations(@NonNull ResolvedClass cls) { |
| ClassInfo c = findClass(cls); |
| if (c == null) { |
| return null; |
| } |
| |
| return c.annotations; |
| } |
| |
| @Nullable |
| public List<ResolvedAnnotation> getAnnotations(@NonNull ResolvedAnnotation cls) { |
| ClassInfo c = findClass(cls); |
| if (c == null) { |
| return null; |
| } |
| |
| return c.annotations; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedPackage pkg, @NonNull String type) { |
| ClassInfo c = findPackage(pkg); |
| |
| if (c == null) { |
| return null; |
| } |
| |
| if (c.annotations != null) { |
| for (ResolvedAnnotation annotation : c.annotations) { |
| if (type.equals(annotation.getSignature())) { |
| return annotation; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public List<ResolvedAnnotation> getAnnotations(@NonNull ResolvedPackage pkg) { |
| ClassInfo c = findPackage(pkg); |
| if (c == null) { |
| return null; |
| } |
| |
| return c.annotations; |
| } |
| |
| @Nullable |
| public ResolvedAnnotation getAnnotation(@NonNull ResolvedField field, @NonNull String type) { |
| FieldInfo f = findField(field); |
| |
| if (f == null) { |
| return null; |
| } |
| if (f.annotations != null) { |
| for (ResolvedAnnotation annotation : f.annotations) { |
| if (type.equals(annotation.getSignature())) { |
| return annotation; |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| @Nullable |
| public List<ResolvedAnnotation> getAnnotations(@NonNull ResolvedField field) { |
| FieldInfo f = findField(field); |
| |
| if (f == null) { |
| return null; |
| } |
| return f.annotations; |
| } |
| |
| // ---- Initialization ---- |
| |
| private void initializeFromDirectory(File file) throws IOException { |
| if (file.isDirectory()) { |
| File[] files = file.listFiles(); |
| if (files != null) { |
| for (File f : files) { |
| initializeFromDirectory(f); |
| } |
| } |
| } else if (file.getPath().endsWith(FN_ANNOTATIONS_XML)) { |
| String xml = Files.toString(file, Charsets.UTF_8); |
| initializePackage(xml, file.getPath()); |
| } |
| } |
| |
| private void initializeFromJar(File file) throws IOException { |
| // Reads in an existing annotations jar and merges in entries found there |
| // with the annotations analyzed from source. |
| JarInputStream zis = null; |
| try { |
| @SuppressWarnings("IOResourceOpenedButNotSafelyClosed") |
| FileInputStream fis = new FileInputStream(file); |
| zis = new JarInputStream(fis); |
| ZipEntry entry = zis.getNextEntry(); |
| while (entry != null) { |
| if (entry.getName().endsWith(".xml")) { |
| byte[] bytes = ByteStreams.toByteArray(zis); |
| String xml = new String(bytes, Charsets.UTF_8); |
| initializePackage(xml, entry.getName()); |
| } |
| entry = zis.getNextEntry(); |
| } |
| } finally { |
| try { |
| Closeables.close(zis, true); |
| } catch (IOException e) { |
| // pass |
| } |
| } |
| } |
| |
| /** |
| * Takes the XML contents of an annotations.xml file, parses it and initialize |
| * the necessary data structures |
| */ |
| private void initializePackage(@NonNull String xml, @NonNull String path) |
| throws IOException { |
| try { |
| Document document = XmlUtils.parseDocument(xml, false); |
| |
| Element root = document.getDocumentElement(); |
| String rootTag = root.getTagName(); |
| assert rootTag.equals("root") : rootTag; |
| |
| for (Element item : LintUtils.getChildren(root)) { |
| String signature = item.getAttribute(ATTR_NAME); |
| if (signature == null || signature.equals("null")) { |
| continue; // malformed item |
| } |
| |
| signature = XmlUtils.fromXmlAttributeValue(signature); |
| Matcher matcher = XML_SIGNATURE.matcher(signature); |
| if (matcher.matches()) { |
| String containingClass = matcher.group(1); |
| if (containingClass == null) { |
| throw new IOException("Could not find class for " + signature); |
| } |
| String methodName = matcher.group(5); |
| if (methodName != null) { |
| String type = matcher.group(4); |
| boolean isConstructor = type == null; |
| String parameters = matcher.group(6); |
| mergeMethodOrParameter(item, matcher, containingClass, methodName, |
| isConstructor, parameters); |
| } else { |
| String fieldName = matcher.group(2); |
| mergeField(item, containingClass, fieldName); |
| } |
| } else if (signature.indexOf(' ') == -1 && signature.indexOf('.') != -1) { |
| mergeClass(item, signature); |
| } else { |
| throw new IOException("No merge match for signature " + signature); |
| } |
| } |
| } catch (Exception e) { |
| throw new IOException("Could not parse XML from " + path); |
| } |
| } |
| |
| // SDK annotations |
| private Map<String,ClassInfo> mClassMap = Maps.newHashMapWithExpectedSize(800); |
| |
| @Nullable |
| private ClassInfo findClass(@NonNull ResolvedClass cls) { |
| return mClassMap.get(cls.getName()); |
| } |
| |
| @Nullable |
| private ClassInfo findClass(@NonNull ResolvedAnnotation cls) { |
| return mClassMap.get(cls.getName()); |
| } |
| |
| private ClassInfo findPackage(@NonNull ResolvedPackage pkg) { |
| return mClassMap.get(pkg.getName() +".package-info"); |
| } |
| |
| @Nullable |
| private MethodInfo findMethod(@NonNull ResolvedMethod method) { |
| ClassInfo c = findClass(method.getContainingClass()); |
| if (c == null) { |
| return null; |
| } |
| if (c.methods == null) { |
| return null; |
| } |
| Collection<MethodInfo> methods = c.methods.get(method.getName()); |
| if (methods == null) { |
| return null; |
| } |
| boolean constructor = method.isConstructor(); |
| for (MethodInfo m : methods) { |
| if (constructor != m.constructor) { |
| continue; |
| } |
| // Check parameter types |
| // TODO: Perform faster parameter check! This is inefficient |
| // Stash parameter count such that I can quickly compare the two |
| String signature = m.parameters; |
| int index = 0; |
| boolean matches = true; |
| for (int i = 0, n = method.getArgumentCount(); i < n; i++) { |
| String parameterType = method.getArgumentType(i).getSignature(); |
| int length = parameterType.indexOf('<'); |
| if (length == -1) { |
| length = parameterType.length(); |
| } |
| if (!signature.regionMatches(false, index, parameterType, 0, length)) { |
| // Check if we have a varargs match: x... vs x[] |
| if (length <= 3 || index <= 3 || ((parameterType.charAt(length - 1) != '.') |
| && (signature.length() < index + length |
| || signature.charAt(index + length - 1) != '.')) |
| || !isVarArgsMatch(signature, index, parameterType, length)) { |
| matches = false; |
| break; |
| } |
| } |
| index += length; |
| if (i < n - 1) { |
| if (index == signature.length()) { |
| matches = false; |
| break; |
| } else if (signature.charAt(index) == '<') { |
| // Skip raw types |
| int balance = 1; |
| for (int j = index + 1, max = signature.length(); j < max; j++) { |
| char ch = signature.charAt(j); |
| if (ch == '<') { |
| balance++; |
| } else if (ch == '>') { |
| balance--; |
| if (balance == 0) { |
| index = j + 1; |
| break; |
| } |
| } |
| } |
| if (balance > 0) { |
| matches = false; |
| break; |
| } |
| } else if (signature.charAt(index) != ',') { |
| matches = false; |
| break; |
| } |
| } |
| index++; // skip comma |
| } |
| |
| if (matches) { |
| return m; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Checks whether the string at parameterType(0,length) and signature(index,index+length) |
| * are the same, except with one possibly ending with [] and the other with ... - if |
| * so these should be taken to match |
| */ |
| private static boolean isVarArgsMatch(String signature, int index, String parameterType, |
| int length) { |
| return parameterType.regionMatches(false, length - 3, "...", 0, 3) && |
| signature.regionMatches(false, index + length - 3, "[]", 0, 2) && |
| parameterType.regionMatches(false, 0, signature, index, length - 3) |
| || parameterType.regionMatches(false, length - 2, "[]", 0, 2) && |
| signature.regionMatches(false, index + length - 2, "...", 0, 3) && |
| parameterType.regionMatches(false, 0, signature, index, length - 2); |
| } |
| |
| @Nullable |
| private FieldInfo findField(@NonNull ResolvedField field) { |
| ClassInfo c = findClass(field.getContainingClass()); |
| if (c == null) { |
| return null; |
| } |
| if (c.fields == null) { |
| return null; |
| } |
| return c.fields.get(field.getName()); |
| } |
| |
| @NonNull |
| private MethodInfo createMethod(@NonNull String containingClass, @NonNull String methodName, |
| boolean constructor, @NonNull String parameters) { |
| ClassInfo cls = createClass(containingClass); |
| if (cls.methods != null) { |
| Collection<MethodInfo> methods = cls.methods.get(methodName); |
| if (methods != null) { |
| for (MethodInfo method : methods) { |
| if (parameters.equals(method.parameters) |
| && constructor == method.constructor) { |
| return method; |
| } |
| } |
| } |
| } |
| |
| MethodInfo method = new MethodInfo(); |
| method.parameters = parameters; |
| method.constructor = constructor; |
| |
| if (cls.methods == null) { |
| cls.methods = ArrayListMultimap.create(); // TODO: Size me |
| } |
| cls.methods.put(methodName, method); |
| return method; |
| } |
| |
| @NonNull |
| private ClassInfo createClass(@NonNull String containingClass) { |
| ClassInfo cls = mClassMap.get(containingClass); |
| if (cls == null) { |
| cls = new ClassInfo(); |
| mClassMap.put(containingClass, cls); |
| } |
| return cls; |
| } |
| |
| @NonNull |
| private FieldInfo createField(@NonNull String containingClass, @NonNull String fieldName) { |
| ClassInfo cls = createClass(containingClass); |
| if (cls.fields != null) { |
| FieldInfo field = cls.fields.get(fieldName); |
| if (field != null) { |
| return field; |
| } |
| } |
| |
| FieldInfo field = new FieldInfo(); |
| if (cls.fields == null) { |
| cls.fields = Maps.newHashMap(); // TODO: Size me |
| } |
| cls.fields.put(fieldName, field); |
| return field; |
| } |
| |
| private void mergeMethodOrParameter(Element item, Matcher matcher, String containingClass, |
| String methodName, boolean constructor, String parameters) { |
| parameters = fixParameterString(parameters); |
| |
| MethodInfo method = createMethod(containingClass, methodName, constructor, parameters); |
| List<ResolvedAnnotation> annotations = createAnnotations(item); |
| |
| String argNum = matcher.group(7); |
| if (argNum != null) { |
| argNum = argNum.trim(); |
| int parameter = Integer.parseInt(argNum); |
| |
| if (method.parameterAnnotations == null) { |
| // Do I know the parameter count here? |
| int parameterCount = 4; |
| method.parameterAnnotations = ArrayListMultimap |
| .create(parameterCount, annotations.size()); |
| } |
| for (ResolvedAnnotation annotation : annotations) { |
| method.parameterAnnotations.put(parameter, annotation); |
| } |
| } else { |
| if (method.annotations == null) { |
| method.annotations = Lists.newArrayListWithExpectedSize(annotations.size()); |
| } |
| method.annotations.addAll(annotations); |
| } |
| } |
| |
| private void mergeField(Element item, String containingClass, String fieldName) { |
| FieldInfo field = createField(containingClass, fieldName); |
| List<ResolvedAnnotation> annotations = createAnnotations(item); |
| if (field.annotations == null) { |
| field.annotations = Lists.newArrayListWithExpectedSize(annotations.size()); |
| } |
| field.annotations.addAll(annotations); |
| } |
| |
| private void mergeClass(Element item, String containingClass) { |
| ClassInfo cls = createClass(containingClass); |
| List<ResolvedAnnotation> annotations = createAnnotations(item); |
| if (cls.annotations == null) { |
| cls.annotations = Lists.newArrayListWithExpectedSize(annotations.size()); |
| } |
| cls.annotations.addAll(annotations); |
| } |
| |
| private List<ResolvedAnnotation> createAnnotations(Element itemElement) { |
| List<Element> children = getChildren(itemElement); |
| List<ResolvedAnnotation> result = Lists.newArrayListWithExpectedSize(children.size()); |
| for (Element annotationElement : children) { |
| ResolvedAnnotation annotation = createAnnotation(annotationElement); |
| result.add(annotation); |
| } |
| |
| return result; |
| } |
| |
| private static class ResolvedExternalAnnotation extends ResolvedAnnotation { |
| |
| @NonNull |
| private String mSignature; |
| |
| @Nullable |
| private List<Value> mValues; |
| |
| public ResolvedExternalAnnotation(@NonNull String signature) { |
| mSignature = signature; |
| } |
| |
| void addValue(@NonNull Value value) { |
| if (mValues == null) { |
| mValues = Lists.newArrayList(); |
| } |
| mValues.add(value); |
| } |
| |
| @NonNull |
| @Override |
| public String getName() { |
| return mSignature; |
| } |
| |
| @NonNull |
| @Override |
| public String getSignature() { |
| return mSignature; |
| } |
| |
| @Override |
| public int getModifiers() { |
| return Modifier.PUBLIC; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<ResolvedAnnotation> getAnnotations() { |
| return Collections.emptyList(); |
| } |
| |
| @Override |
| public boolean matches(@NonNull String name) { |
| return mSignature.equals(name); |
| } |
| |
| @NonNull |
| @Override |
| public TypeDescriptor getType() { |
| return new DefaultTypeDescriptor(mSignature); |
| } |
| |
| @Nullable |
| @Override |
| public ResolvedClass getClassType() { |
| // No nested annotations in the database |
| return null; |
| } |
| |
| @NonNull |
| @Override |
| public List<Value> getValues() { |
| return mValues == null ? Collections.<Value>emptyList() : mValues; |
| } |
| } |
| |
| private Map<String, ResolvedExternalAnnotation> mMarkerAnnotations = Maps.newHashMapWithExpectedSize(30); |
| |
| private ResolvedAnnotation createAnnotation(Element annotationElement) { |
| String tagName = annotationElement.getTagName(); |
| assert tagName.equals("annotation") : tagName; |
| String name = annotationElement.getAttribute(ATTR_NAME); |
| assert name != null && !name.isEmpty(); |
| |
| ResolvedExternalAnnotation annotation = mMarkerAnnotations.get(name); |
| if (annotation != null) { |
| return annotation; |
| } |
| |
| annotation = new ResolvedExternalAnnotation(name); |
| |
| List<Element> valueElements = getChildren(annotationElement); |
| if (valueElements.isEmpty() |
| // Permission annotations are sometimes used as marker annotations (on |
| // parameters) but that shouldn't let us conclude that any future |
| // permission annotations are |
| && !name.startsWith(PERMISSION_ANNOTATION)) { |
| mMarkerAnnotations.put(name, annotation); |
| return annotation; |
| } |
| |
| for (Element valueElement : valueElements) { |
| if (valueElement.getTagName().equals("val")) { |
| String valueName = valueElement.getAttribute(ATTR_NAME); |
| String valueString = valueElement.getAttribute("val"); |
| if (!valueName.isEmpty() && !valueString.isEmpty()) { |
| // Guess type |
| Object value; |
| if (valueString.equals(VALUE_TRUE)) { |
| value = true; |
| } else if (valueString.equals(VALUE_FALSE)) { |
| value = false; |
| } else if (valueString.startsWith("\"") && valueString.endsWith("\"") && |
| valueString.length() >= 2) { |
| value = valueString.substring(1, valueString.length() - 1); |
| } else if (valueString.startsWith("{") && valueString.endsWith("}")) { |
| // Array of values |
| String listString = valueString.substring(1, valueString.length() - 1); |
| // We don't know the types, but we'll assume that they're either |
| // all strings (the most common array type in our annotations), or |
| // field references. We can't know the types of the fields; it's |
| // not part of the annotation metadata. We'll place them in an Object[] |
| // for now. |
| boolean allStrings = true; |
| Splitter splitter = Splitter.on(',').omitEmptyStrings().trimResults(); |
| List<Object> result = Lists.newArrayList(); |
| for (String reference : splitter.split(listString)) { |
| if (reference.startsWith("\"")) { |
| result.add(reference.substring(1, reference.length() - 1)); |
| } else { |
| result.add(new ResolvedExternalField(reference)); |
| allStrings = false; |
| } |
| } |
| if (allStrings) { |
| value = result.toArray(new String[result.size()]); |
| } else { |
| value = result.toArray(); |
| } |
| |
| // We don't know the actual type of these fields; we'll assume they're |
| // a special form of |
| } else if (Character.isDigit(valueString.charAt(0))) { |
| try { |
| if (valueString.contains(".")) { |
| value = Double.parseDouble(valueString); |
| } else { |
| value = Long.parseLong(valueString); |
| } |
| } catch (NumberFormatException nufe) { |
| value = valueString; |
| } |
| } else { |
| value = valueString; // unknown type |
| } |
| annotation.addValue(new Value(valueName, value)); |
| } |
| } |
| } |
| |
| return annotation; |
| } |
| } |
| |
| /** Special implementation of a {@link ResolvedField} which can |
| * do equality comparisons with {@link EcjParser.EcjResolvedField} */ |
| private static class ResolvedExternalField extends ResolvedField { |
| private final String mSignature; |
| |
| public ResolvedExternalField(String signature) { |
| mSignature = signature; |
| assert mSignature.indexOf(' ') == -1 : '"' + mSignature + '"'; |
| } |
| |
| @NonNull |
| @Override |
| public String getName() { |
| return mSignature.substring(mSignature.lastIndexOf('.') + 1); |
| } |
| |
| @Override |
| public String getSignature() { |
| return mSignature; |
| } |
| |
| @Override |
| public boolean equals(Object obj) { |
| if (obj instanceof ResolvedExternalField) { |
| return mSignature.equals(((ResolvedExternalField)obj).mSignature); |
| } else if (obj instanceof ResolvedField) { |
| ResolvedField field = (ResolvedField)obj; |
| if (mSignature.endsWith(field.getName())) { |
| String signature = field.getContainingClass().getSignature() + |
| "." + field.getName(); |
| return mSignature.equals(signature); |
| } |
| return false; |
| } else { |
| return false; |
| } |
| } |
| |
| @Override |
| public int hashCode() { |
| return mSignature.hashCode(); |
| } |
| |
| @Override |
| public int getModifiers() { |
| return 0; |
| } |
| |
| @Override |
| public boolean matches(@NonNull String name) { |
| return mSignature.equals(name); |
| } |
| |
| @NonNull |
| @Override |
| public TypeDescriptor getType() { |
| return new DefaultTypeDescriptor(mSignature); |
| } |
| |
| @NonNull |
| @Override |
| public ResolvedClass getContainingClass() { |
| throw new UnsupportedOperationException(); |
| } |
| |
| @Override |
| public String getContainingClassName() { |
| return mSignature.substring(0, mSignature.lastIndexOf('.')); |
| } |
| |
| @Nullable |
| @Override |
| public Object getValue() { |
| return null; |
| } |
| |
| @NonNull |
| @Override |
| public Iterable<ResolvedAnnotation> getAnnotations() { |
| return Collections.emptyList(); |
| } |
| } |
| |
| @NonNull |
| private static List<Element> getChildren(@NonNull Element element) { |
| NodeList itemList = element.getChildNodes(); |
| int length = itemList.getLength(); |
| if (length == 0) { |
| return Collections.emptyList(); |
| } |
| List<Element> result = new ArrayList<Element>(Math.max(5, length / 2 + 1)); |
| for (int i = 0; i < length; i++) { |
| Node node = itemList.item(i); |
| if (node.getNodeType() != Node.ELEMENT_NODE) { |
| continue; |
| } |
| |
| result.add((Element) node); |
| } |
| |
| return result; |
| } |
| |
| // The parameter declaration used in XML files should not have duplicated spaces, |
| // and there should be no space after commas (we can't however strip out all spaces, |
| // since for example the spaces around the "extends" keyword needs to be there in |
| // types like Map<String,? extends Number> |
| private static String fixParameterString(String parameters) { |
| return parameters.replace(" ", " ").replace(", ", ","); |
| } |
| |
| /** For test usage only */ |
| @VisibleForTesting |
| static synchronized void set(ExternalAnnotationRepository singleton) { |
| assert singleton == null || sSingleton == null; |
| sSingleton = singleton; |
| } |
| } |