com.android.tools.lint.ExternalAnnotationRepository.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.lint.ExternalAnnotationRepository.java

Source

/*
 * 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;
    }
}