com.android.tools.idea.gradle.project.AndroidGradleProjectData.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.gradle.project.AndroidGradleProjectData.java

Source

/*
 * 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.tools.idea.gradle.project;

import com.android.SdkConstants;
import com.android.annotations.VisibleForTesting;
import com.android.builder.model.AndroidProject;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.gradle.GradleSyncState;
import com.android.tools.idea.gradle.IdeaAndroidProject;
import com.android.tools.idea.gradle.IdeaGradleProject;
import com.android.tools.idea.gradle.JavaModel;
import com.android.tools.idea.gradle.facet.AndroidGradleFacet;
import com.android.tools.idea.gradle.facet.JavaGradleFacet;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.android.tools.idea.gradle.util.LocalProperties;
import com.android.tools.idea.gradle.util.Projects;
import com.android.tools.idea.sdk.DefaultSdks;
import com.android.tools.idea.startup.AndroidStudioSpecificInitializer;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.hash.Hashing;
import com.google.common.io.Closeables;
import com.google.common.io.Files;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.projectRoots.SdkAdditionalData;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import org.gradle.tooling.model.UnsupportedMethodException;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidPlatform;
import org.jetbrains.android.sdk.AndroidSdkAdditionalData;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

import static com.android.SdkConstants.FN_FRAMEWORK_LIBRARY;
import static com.intellij.openapi.roots.OrderRootType.CLASSES;

/**
 * The Project data that needs to be persisted for it to be possible to reload the Project without the need of calling Gradle to
 * regenerate this objects.
 */
public class AndroidGradleProjectData implements Serializable {
    @NotNull
    @NonNls
    private static final String STATE_FILE_NAME = "model_data.bin";
    private static final boolean ENABLED = !Boolean.getBoolean("studio.disable.synccache");

    @SuppressWarnings("unchecked")
    private static final Set<Class<?>> SUPPORTED_TYPES = ImmutableSet.of(File.class, Boolean.class, String.class,
            Integer.class, Collection.class, Map.class, Set.class);

    private static final Logger LOG = Logger.getInstance(AndroidGradleProjectData.class);

    /**
     * A map from module name to its data
     */
    private Map<String, ModuleData> myData = Maps.newHashMap();

    /**
     * A set of files and their MD5 that this data depends on.
     */
    private Map<String, byte[]> myFileChecksums = Maps.newHashMap();

    /**
     * The model version
     */
    private String myGradlePluginVersion = SdkConstants.GRADLE_PLUGIN_LATEST_VERSION;

    /**
     * The last time a sync was done.
     */
    private long myLastGradleSyncTimestamp = -1L;

    private AndroidGradleProjectData() {
    }

    public static void removeFrom(@NotNull Project project) {
        if (!ENABLED) {
            return;
        }
        try {
            File stateFile = getProjectStateFile(project);
            if (stateFile.isFile()) {
                FileUtil.delete(stateFile);
            }
        } catch (IOException e) {
            LOG.warn(String.format("Failed to remove state for project %1$s'", project.getName()));
        }
    }

    /**
     * Persists the gradle model of this project to disk.
     *
     * @param project the project to get the data from.
     */
    public static void save(@NotNull Project project) {
        if (!ENABLED) {
            return;
        }
        try {
            AndroidGradleProjectData data = createFrom(project);
            if (data != null) {
                File file = getProjectStateFile(project);
                FileUtil.ensureExists(file.getParentFile());
                data.saveTo(file);
            }
        } catch (IOException e) {
            LOG.info(String.format("Error while saving persistent state from project '%1$s'", project.getName()),
                    e);
        }
    }

    @Nullable
    @VisibleForTesting
    static AndroidGradleProjectData createFrom(@NotNull Project project) throws IOException {
        AndroidGradleProjectData data = new AndroidGradleProjectData();
        File rootDirPath = new File(project.getBasePath());
        Module[] modules = ModuleManager.getInstance(project).getModules();
        for (Module module : modules) {
            ModuleData moduleData = new ModuleData();

            moduleData.myName = module.getName();

            AndroidFacet androidFacet = AndroidFacet.getInstance(module);
            if (androidFacet != null) {
                IdeaAndroidProject ideaAndroidProject = androidFacet.getIdeaAndroidProject();
                if (ideaAndroidProject != null) {
                    moduleData.myAndroidProject = reproxy(AndroidProject.class, ideaAndroidProject.getDelegate());
                    moduleData.mySelectedVariant = ideaAndroidProject.getSelectedVariant().getName();
                } else {
                    LOG.warn(String.format(
                            "Trying to create project data from a not initialized project '%1$s'. Abort.",
                            project.getName()));
                    return null;
                }
            }

            AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module);
            if (gradleFacet != null) {
                IdeaGradleProject ideaGradleProject = gradleFacet.getGradleProject();
                if (ideaGradleProject != null) {
                    data.addFileDependency(rootDirPath, ideaGradleProject.getBuildFile());
                    moduleData.myIdeaGradleProject = ideaGradleProject;
                } else {
                    LOG.warn(String.format(
                            "Trying to create project data from a not initialized project '%1$s'. Abort.",
                            project.getName()));
                    return null;
                }
            }

            JavaGradleFacet javaFacet = JavaGradleFacet.getInstance(module);
            if (javaFacet != null) {
                moduleData.myJavaModel = javaFacet.getJavaModel();
            }

            if (Projects.isGradleProjectModule(module)) {
                data.addFileDependency(rootDirPath, GradleUtil.getGradleBuildFile(module));
                data.addFileDependency(rootDirPath, GradleUtil.getGradleSettingsFile(rootDirPath));
                data.addFileDependency(rootDirPath, new File(rootDirPath, SdkConstants.FN_GRADLE_PROPERTIES));
                data.addFileDependency(rootDirPath, new File(rootDirPath, SdkConstants.FN_LOCAL_PROPERTIES));
                data.addFileDependency(rootDirPath, getGradleUserSettingsFile());
            }

            data.myData.put(moduleData.myName, moduleData);
        }
        GradleSyncState syncState = GradleSyncState.getInstance(project);
        data.myLastGradleSyncTimestamp = syncState.getLastGradleSyncTimestamp();
        return data;
    }

    @Nullable
    public static File getGradleUserSettingsFile() {
        String homePath = System.getProperty("user.home");
        if (homePath == null) {
            return null;
        }
        return new File(homePath, FileUtil.join(SdkConstants.DOT_GRADLE, SdkConstants.FN_GRADLE_PROPERTIES));
    }

    @NotNull
    private static byte[] createChecksum(@NotNull File file) throws IOException {
        // For files tracked by the IDE we get the content from the virtual files, otherwise we revert to io.
        VirtualFile vf = VfsUtil.findFileByIoFile(file, true);
        byte[] data = new byte[] {};
        if (vf != null) {
            vf.refresh(false, false);
            if (vf.exists()) {
                data = vf.contentsToByteArray();
            }
        } else if (file.exists()) {
            data = Files.toByteArray(file);
        }
        return Hashing.md5().hashBytes(data).asBytes();
    }

    /**
     * Loads the gradle model persisted on disk for the given project.
     *
     * @param project the project for which to load the data.
     * @return whether the load was successful.
     */
    public static boolean loadFromDisk(@NotNull final Project project) {
        if (!ENABLED || needsAndroidSdkSync(project)) {
            return false;
        }
        try {
            return doLoadFromDisk(project);
        } catch (IOException e) {
            LOG.info(String.format("Error accessing state cache for project '%1$s', sync will be needed.",
                    project.getName()));
        } catch (ClassNotFoundException e) {
            LOG.info(String.format("Cannot recover state cache for project '%1$s', sync will be needed.",
                    project.getName()));
        }
        return false;
    }

    private static boolean needsAndroidSdkSync(@NotNull Project project) {
        if (AndroidStudioSpecificInitializer.isAndroidStudio()) {
            final File ideSdkPath = DefaultSdks.getDefaultAndroidHome();
            if (ideSdkPath != null) {
                if (needsLPreviewPlatformReset()) {
                    // reset the Android SDK home to force recreation of IDEA SDKs.
                    ApplicationManager.getApplication().runWriteAction(new Runnable() {
                        @Override
                        public void run() {
                            DefaultSdks.setDefaultAndroidHome(ideSdkPath, DefaultSdks.getDefaultJdk());
                        }
                    });
                    return true;
                }
                try {
                    LocalProperties localProperties = new LocalProperties(project);
                    File projectSdkPath = localProperties.getAndroidSdkPath();
                    return projectSdkPath == null || !FileUtil.filesEqual(ideSdkPath, projectSdkPath);
                } catch (IOException ignored) {
                }
            }
            return true;
        }
        return false;
    }

    private static boolean needsLPreviewPlatformReset() {
        // Repair SDK for 'android-L'. See: https://code.google.com/p/android/issues/detail?id=72589
        // TODO: remove this at some point (it's only there to upgrade user settings for people who used 0.8.0 and 0.8.1 with 20 and 21
        // installed simultaneously)
        for (Sdk sdk : DefaultSdks.getEligibleAndroidSdks()) {
            SdkAdditionalData additionalData = sdk.getSdkAdditionalData();
            if (additionalData instanceof AndroidSdkAdditionalData) {
                AndroidPlatform androidPlatform = ((AndroidSdkAdditionalData) additionalData).getAndroidPlatform();
                if (androidPlatform != null) {
                    IAndroidTarget target = androidPlatform.getTarget();
                    AndroidVersion version = target.getVersion();
                    if ("L".equals(version.getApiString()) && version.getApiLevel() == 20 && version.isPreview()) {
                        // This is "android-L"
                        String androidJarPath = target.getPath(IAndroidTarget.ANDROID_JAR);
                        File expectedPath = new File(androidJarPath);
                        VirtualFile[] libraryFiles = sdk.getRootProvider().getFiles(CLASSES);
                        for (VirtualFile libraryFile : libraryFiles) {
                            // Match the expected path of android.jar vs. the actual path. The expected path is the one coming from SDK Manager, while
                            // the actual path is the one in the IDEA SDK.
                            if (FN_FRAMEWORK_LIBRARY.equals(libraryFile.getName())) {
                                File actualPath = VfsUtilCore.virtualToIoFile(libraryFile);
                                return !FileUtil.filesEqual(expectedPath, actualPath);
                            }
                        }
                        // android.jar was never found.
                        return true;
                    }
                }
            }
        }
        return false;
    }

    private static boolean doLoadFromDisk(@NotNull Project project) throws IOException, ClassNotFoundException {
        FileInputStream fin = null;
        try {
            File rootDirPath = new File(FileUtil.toSystemDependentName(project.getBasePath()));
            File dataFile = getProjectStateFile(project);
            if (!dataFile.exists()) {
                return false;
            }
            fin = new FileInputStream(dataFile);
            ObjectInputStream ois = new ObjectInputStream(fin);
            try {
                AndroidGradleProjectData data = (AndroidGradleProjectData) ois.readObject();
                if (data.validate(rootDirPath)) {
                    if (data.applyTo(project)) {
                        PostProjectSetupTasksExecutor.getInstance(project).onProjectRestoreFromDisk();
                        return true;
                    }
                }
            } finally {
                Closeables.close(ois, false);
            }
        } finally {
            Closeables.close(fin, false);
        }
        return false;
    }

    @NotNull
    private static File getProjectStateFile(@NotNull Project project) throws IOException {
        Module projectModule = Projects.findGradleProjectModule(project);
        if (projectModule != null) {
            File buildFolderPath = Projects.getBuildFolderPath(projectModule);
            if (buildFolderPath != null) {
                return new File(buildFolderPath, FileUtil.join(AndroidProject.FD_INTERMEDIATES, STATE_FILE_NAME));
            }
        }
        // TODO: Once we upgrade to Gradle 2.0, we can get the build directory from there. For now assume "build".
        return new File(VfsUtilCore.virtualToIoFile(project.getBaseDir()),
                FileUtil.join(GradleUtil.BUILD_DIR_DEFAULT_NAME, AndroidProject.FD_INTERMEDIATES, STATE_FILE_NAME));
    }

    /**
     * Regenerate proxy objects with a serializable version of a proxy.
     * This method is intended to be run on objects that are a bag of properties, particularly custom Gradle model objects.
     * Here we assume that the given object can be represented as a map of method name to return value. The original object
     * is regenerated using this assumption which gives a serializable/deserializable object.
     *
     * If a method throws an exception it is assumed that it is the intended behaviour and the same exception will be thrown in the
     * reproxied counterpart. This is useful for Gradle model methods that are not present in the actual model object being used.
     *
     * @param object the object to 'reproxy'.
     * @param type   the runtime type of the object. This is the expected type of object, and must be a superclass or equals to T.
     * @param <T>    the type of the object.
     * @return the reproxied object.
     */
    @SuppressWarnings("unchecked")
    @Nullable
    @VisibleForTesting
    static <T> T reproxy(Type type, T object) {
        if (object == null) {
            return null;
        }
        if (object instanceof InvocationErrorValue) {
            return object;
        }

        if (type instanceof ParameterizedType) {
            ParameterizedType genericType = (ParameterizedType) type;
            if (genericType.getRawType() instanceof Class) {
                Class<?> genericClass = (Class<?>) genericType.getRawType();
                if (Collection.class.isAssignableFrom(genericClass)) {
                    Collection<Object> collection = (Collection<Object>) object;
                    Collection<Object> newCollection;
                    if (genericClass.isAssignableFrom(ArrayList.class)) {
                        newCollection = Lists.newArrayListWithCapacity(collection.size());
                    } else if (genericClass.isAssignableFrom(Set.class)) {
                        newCollection = Sets.newLinkedHashSet();
                    } else {
                        throw new IllegalStateException(
                                "Unsupported collection type: " + genericClass.getCanonicalName());
                    }
                    Type argument = genericType.getActualTypeArguments()[0];
                    for (Object item : collection) {
                        newCollection.add(reproxy(argument, item));
                    }
                    return (T) newCollection;
                } else if (Map.class.isAssignableFrom(genericClass)) {
                    Map<Object, Object> map = (Map<Object, Object>) object;
                    Map<Object, Object> newMap = Maps.newLinkedHashMap();
                    Type keyType = genericType.getActualTypeArguments()[0];
                    Type valueType = genericType.getActualTypeArguments()[1];
                    for (Map.Entry entry : map.entrySet()) {
                        newMap.put(reproxy(keyType, entry.getKey()), reproxy(valueType, entry.getValue()));
                    }
                    return (T) newMap;
                } else {
                    throw new IllegalStateException("Unsupported generic type: " + genericClass.getCanonicalName());
                }
            } else {
                throw new IllegalStateException("Unsupported raw type.");
            }
        }

        // Only modify proxy objects...
        if (!Proxy.isProxyClass(object.getClass())) {
            return object;
        }

        // ...that are not our own proxy.
        if (Proxy.getInvocationHandler(object) instanceof WrapperInvocationHandler) {
            return object;
        }

        Class<?>[] interfaces = object.getClass().getInterfaces();
        if (interfaces.length != 1) {
            throw new IllegalStateException("Cannot 'reproxy' a class with multiple interfaces");
        }
        Class<?> clazz = interfaces[0];

        final Map<String, Object> values = Maps.newHashMap();
        for (Method m : clazz.getMethods()) {
            try {
                if (Modifier.isPublic(m.getModifiers())) {
                    Object value;
                    try {
                        value = m.invoke(object);
                    } catch (InvocationTargetException e) {
                        value = new InvocationErrorValue(e.getCause());
                    }
                    values.put(m.toGenericString(), reproxy(m.getGenericReturnType(), value));
                }
            } catch (IllegalAccessException e) {
                throw new IllegalStateException("A non public method shouldn't have been called.", e);
            }
        }
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz },
                new WrapperInvocationHandler(values));
    }

    @VisibleForTesting
    static boolean isSupported(@NotNull Class<?> clazz) {
        return clazz.isPrimitive() || SUPPORTED_TYPES.contains(clazz);
    }

    /**
     * Adds a dependency to the content of the given virtual file. @see addFileDependency(File, File)
     */
    private void addFileDependency(File rootDirPath, @Nullable VirtualFile vf) throws IOException {
        addFileDependency(rootDirPath, vf != null ? VfsUtilCore.virtualToIoFile(vf) : null);
    }

    /**
     * Adds a dependency to the content of the given file.
     * <p/>
     * This method saves a checksum of the content of the given file along with its location. If this file's content is later found
     * to have changed, the persisted data will be considered invalid.
     *
     * @param rootDirPath the root directory.
     * @param file        the file to add the dependency for.
     * @throws IOException if there is a problem accessing the given file.
     */
    private void addFileDependency(File rootDirPath, @Nullable File file) throws IOException {
        if (file == null) {
            return;
        }
        String key;
        if (FileUtil.isAncestor(rootDirPath, file, true)) {
            key = FileUtil.getRelativePath(rootDirPath, file);
        } else {
            key = file.getAbsolutePath();
        }
        myFileChecksums.put(key, createChecksum(file));
    }

    /**
     * Validates that the received data can be applied to the project at rootDir.
     * <p/>
     * This validates that all the files this model depends on, still have the same content checksum and that the gradle model version
     * is still the same.
     *
     * @param rootDir the root directory where to find the files.
     * @return whether the data is still valid.
     * @throws IOException if there is a problem accessing these files.
     */
    private boolean validate(@NotNull File rootDir) throws IOException {
        if (!myGradlePluginVersion.equals(SdkConstants.GRADLE_PLUGIN_LATEST_VERSION)) {
            return false;
        }

        for (Map.Entry<String, byte[]> entry : myFileChecksums.entrySet()) {
            File file = new File(entry.getKey());
            if (!file.isAbsolute()) {
                file = new File(rootDir, file.getPath());
            }
            if (!Arrays.equals(entry.getValue(), createChecksum(file))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Applies this data to the given project.
     *
     * @param project the project to apply the data to.
     */
    @VisibleForTesting
    public boolean applyTo(Project project) {
        final Module[] modules = ModuleManager.getInstance(project).getModules();
        for (Module module : modules) {
            ModuleData data = myData.get(module.getName());
            // If no data is found, the cache doesn't match the project structure and we should resync.
            if (data == null) {
                return false;
            }

            AndroidFacet androidFacet = AndroidFacet.getInstance(module);
            String moduleFilePath = module.getModuleFilePath(); // System dependent absolute path.
            if (androidFacet != null) {
                if (data.myAndroidProject != null) {
                    File moduleFile = new File(moduleFilePath);
                    assert moduleFile.getParent() != null : moduleFile.getPath();
                    File moduleRootDirPath = moduleFile.getParentFile();
                    IdeaAndroidProject ideaAndroidProject = new IdeaAndroidProject(module.getName(),
                            moduleRootDirPath, data.myAndroidProject, data.mySelectedVariant);
                    androidFacet.setIdeaAndroidProject(ideaAndroidProject);
                } else {
                    return false;
                }
            }

            AndroidGradleFacet gradleFacet = AndroidGradleFacet.getInstance(module);
            if (gradleFacet != null) {
                gradleFacet.setGradleProject(data.myIdeaGradleProject);
            }

            JavaGradleFacet javaFacet = JavaGradleFacet.getInstance(module);
            if (javaFacet != null && data.myJavaModel != null) {
                javaFacet.setJavaModel(data.myJavaModel);
            }
        }
        GradleSyncState.getInstance(project).syncSkipped(myLastGradleSyncTimestamp);
        return true;
    }

    /**
     * Saves the data on the given project location.
     *
     * @param file the file where to save this data.
     */
    private void saveTo(File file) throws IOException {
        FileOutputStream fos = null;
        try {
            fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            try {
                oos.writeObject(this);
            } finally {
                Closeables.close(oos, false);
            }
        } finally {
            Closeables.close(fos, false);
        }
    }

    @VisibleForTesting
    Map<String, ModuleData> getModuleData() {
        return myData;
    }

    @VisibleForTesting
    Map<String, byte[]> getFileChecksums() {
        return myFileChecksums;
    }

    /**
     * The persistent data to store per project Module.
     */
    static class ModuleData implements Serializable {
        public String myName;
        public IdeaGradleProject myIdeaGradleProject;
        public AndroidProject myAndroidProject;
        public String mySelectedVariant;
        public JavaModel myJavaModel;
    }

    static class WrapperInvocationHandler implements InvocationHandler, Serializable {
        private static final Method TO_STRING = getObjectMethod("toString");
        private static final Method HASHCODE = getObjectMethod("hashCode");
        private static final Method EQUALS = getObjectMethod("equals", Object.class);
        @VisibleForTesting
        final Map<String, Object> values;

        WrapperInvocationHandler(@NotNull Map<String, Object> values) {
            this.values = values;
        }

        @NotNull
        private static Method getObjectMethod(@NotNull String name, @NotNull Class<?>... types) {
            try {
                return Object.class.getMethod(name, types);
            } catch (NoSuchMethodException e) {
                throw new IllegalStateException("Method should exist in Object", e);
            }
        }

        @Override
        public Object invoke(Object o, Method method, Object[] objects) throws Throwable {
            if (method.equals(TO_STRING)) {
                return method.invoke(this, objects);
            } else if (method.equals(HASHCODE)) {
                return method.invoke(this, objects);
            } else if (method.equals(EQUALS)) {
                return proxyEquals(objects[0]);
            } else {
                String key = method.toGenericString();
                if (!values.containsKey(key)) {
                    throw new UnsupportedMethodException("Method " + key + " not found");
                }
                Object value = values.get(key);
                if (value instanceof InvocationErrorValue) {
                    throw ((InvocationErrorValue) value).exception;
                }
                return value;
            }
        }

        private boolean proxyEquals(Object other) {
            return other != null && Proxy.isProxyClass(other.getClass())
                    && Proxy.getInvocationHandler(other).equals(this);
        }
    }

    private static class InvocationErrorValue implements Serializable {
        public Throwable exception;

        private InvocationErrorValue(Throwable exception) {
            this.exception = exception;
        }
    }
}