com.android.tools.idea.gradle.AndroidGradleJavaProjectModelModifier.java Source code

Java tutorial

Introduction

Here is the source code for com.android.tools.idea.gradle.AndroidGradleJavaProjectModelModifier.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.idea.gradle;

import com.android.builder.model.*;
import com.android.ide.common.repository.GradleVersion;
import com.android.tools.idea.gradle.dsl.model.GradleBuildModel;
import com.android.tools.idea.gradle.dsl.model.android.AndroidModel;
import com.android.tools.idea.gradle.dsl.model.android.CompileOptionsModel;
import com.android.tools.idea.gradle.dsl.model.dependencies.ArtifactDependencySpec;
import com.android.tools.idea.gradle.dsl.model.dependencies.DependenciesModel;
import com.android.tools.idea.gradle.dsl.model.java.JavaModel;
import com.android.tools.idea.gradle.project.facet.java.JavaFacet;
import com.android.tools.idea.gradle.project.model.AndroidModuleModel;
import com.android.tools.idea.gradle.project.sync.GradleSyncInvoker;
import com.android.tools.idea.gradle.project.sync.GradleSyncListener;
import com.android.tools.idea.testartifacts.scopes.TestArtifactSearchScopes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.command.undo.BasicUndoableAction;
import com.intellij.openapi.command.undo.UndoManager;
import com.intellij.openapi.command.undo.UnexpectedUndoException;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.DependencyScope;
import com.intellij.openapi.roots.ExternalLibraryDescriptor;
import com.intellij.openapi.roots.JavaProjectModelModifier;
import com.intellij.openapi.roots.OrderRootType;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.AsyncPromise;
import org.jetbrains.concurrency.Promise;
import org.jetbrains.concurrency.Promises;

import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static com.android.tools.idea.gradle.dsl.model.dependencies.CommonConfigurationNames.*;
import static com.android.tools.idea.gradle.util.GradleUtil.getDependencies;
import static com.android.tools.idea.gradle.util.GradleUtil.getGradlePath;
import static com.android.tools.idea.gradle.util.Projects.getAndroidModel;
import static com.android.tools.idea.gradle.util.Projects.isBuildWithGradle;
import static com.intellij.openapi.roots.libraries.LibraryUtil.findLibrary;
import static com.intellij.openapi.util.io.FileUtil.getNameWithoutExtension;
import static com.intellij.openapi.util.io.FileUtil.splitPath;
import static com.intellij.openapi.vfs.VfsUtilCore.virtualToIoFile;

public class AndroidGradleJavaProjectModelModifier extends JavaProjectModelModifier {
    @NotNull
    private static final Map<String, String> EXTERNAL_LIBRARY_VERSIONS = ImmutableMap.of(
            "net.jcip:jcip-annotations", "1.0", "org.jetbrains:annotations-java5", "15.0",
            "org.jetbrains:annotations", "15.0", "junit:junit", "4.12", "org.testng:testng", "6.9.6");

    @Nullable
    @Override
    public Promise<Void> addModuleDependency(@NotNull Module from, @NotNull Module to,
            @NotNull DependencyScope scope) {
        Project project = from.getProject();
        VirtualFile openedFile = FileEditorManagerEx.getInstanceEx(from.getProject()).getCurrentFile();
        String gradlePath = getGradlePath(to);
        GradleBuildModel buildModel = GradleBuildModel.get(from);

        if (buildModel != null && gradlePath != null) {
            DependenciesModel dependencies = buildModel.dependencies();
            String configurationName = getConfigurationName(from, scope, openedFile);
            dependencies.addModule(configurationName, gradlePath, null);

            new WriteCommandAction(project, "Add Gradle Module Dependency") {
                @Override
                protected void run(@NotNull Result result) throws Throwable {
                    buildModel.applyChanges();
                    registerUndoAction(project);
                }
            }.execute();
            return requestProjectSync(project);
        }

        if ((buildModel == null) ^ (gradlePath == null)) {
            // If one of them is gradle module and one of them are not, reject since this is invalid dependency
            return Promises.rejectedPromise();
        }
        return null;
    }

    @Nullable
    @Override
    public Promise<Void> addExternalLibraryDependency(@NotNull Collection<Module> modules,
            @NotNull ExternalLibraryDescriptor descriptor, @NotNull DependencyScope scope) {
        ArtifactDependencySpec dependencySpec = new ArtifactDependencySpec(descriptor.getLibraryArtifactId(),
                descriptor.getLibraryGroupId(), selectVersion(descriptor));
        return addExternalLibraryDependency(modules, dependencySpec, scope);
    }

    @Nullable
    @Override
    public Promise<Void> addLibraryDependency(@NotNull Module from, @NotNull Library library,
            @NotNull DependencyScope scope) {
        if (!isBuildWithGradle(from)) {
            return null;
        }
        ArtifactDependencySpec dependencySpec = findNewExternalDependency(from.getProject(), library);
        if (dependencySpec == null) {
            return Promises.rejectedPromise();
        }
        return addExternalLibraryDependency(ImmutableList.of(from), dependencySpec, scope);
    }

    @Nullable
    private static Promise<Void> addExternalLibraryDependency(@NotNull Collection<Module> modules,
            @NotNull ArtifactDependencySpec dependencySpec, @NotNull DependencyScope scope) {
        Module firstModule = Iterables.getFirst(modules, null);
        if (firstModule == null) {
            return null;
        }
        Project project = firstModule.getProject();

        VirtualFile openedFile = FileEditorManagerEx.getInstanceEx(firstModule.getProject()).getCurrentFile();

        List<GradleBuildModel> buildModelsToUpdate = Lists.newArrayList();
        for (Module module : modules) {
            GradleBuildModel buildModel = GradleBuildModel.get(module);
            if (buildModel == null) {
                return null;
            }
            String configurationName = getConfigurationName(module, scope, openedFile);
            DependenciesModel dependencies = buildModel.dependencies();
            dependencies.addArtifact(configurationName, dependencySpec);
            buildModelsToUpdate.add(buildModel);
        }

        new WriteCommandAction(project, "Add Gradle Library Dependency") {
            @Override
            protected void run(@NotNull Result result) throws Throwable {
                for (GradleBuildModel buildModel : buildModelsToUpdate) {
                    buildModel.applyChanges();
                }
                registerUndoAction(project);
            }
        }.execute();

        return requestProjectSync(project);
    }

    @Nullable
    @Override
    public Promise<Void> changeLanguageLevel(@NotNull Module module, @NotNull LanguageLevel level) {
        Project project = module.getProject();
        if (!isBuildWithGradle(module)) {
            return null;
        }

        GradleBuildModel buildModel = GradleBuildModel.get(module);
        if (buildModel == null) {
            return null;
        }

        if (getAndroidModel(module) != null) {
            AndroidModel android = buildModel.android();
            if (android == null) {
                return null;
            }
            CompileOptionsModel compileOptions = android.compileOptions();
            compileOptions.setSourceCompatibility(level);
            compileOptions.setTargetCompatibility(level);
        } else {
            JavaFacet javaFacet = JavaFacet.getInstance(module);
            if (javaFacet == null || javaFacet.getJavaModuleModel() == null) {
                return null;
            }
            JavaModel javaModel = buildModel.java();
            javaModel.setSourceCompatibility(level);
            javaModel.setTargetCompatibility(level);
        }

        new WriteCommandAction(project, "Change Gradle Language Level") {
            @Override
            protected void run(@NotNull Result result) throws Throwable {
                buildModel.applyChanges();
                registerUndoAction(project);
            }
        }.execute();

        return requestProjectSync(project);
    }

    @NotNull
    private static String getConfigurationName(@NotNull Module module, @NotNull DependencyScope scope,
            @Nullable VirtualFile openedFile) {
        if (!scope.isForProductionCompile()) {
            TestArtifactSearchScopes testScopes = TestArtifactSearchScopes.get(module);

            if (testScopes != null && openedFile != null) {
                return testScopes.isAndroidTestSource(openedFile) ? ANDROID_TEST_COMPILE : TEST_COMPILE;
            }
        }
        return COMPILE;
    }

    @Nullable
    private static String selectVersion(@NotNull ExternalLibraryDescriptor descriptor) {
        String groupAndId = descriptor.getLibraryGroupId() + ":" + descriptor.getLibraryArtifactId();
        return EXTERNAL_LIBRARY_VERSIONS.get(groupAndId);
    }

    @NotNull
    private static Promise<Void> requestProjectSync(@NotNull Project project) {
        AsyncPromise<Void> promise = new AsyncPromise<>();
        GradleSyncInvoker.Request request = new GradleSyncInvoker.Request().setGenerateSourcesOnSuccess(false);
        GradleSyncInvoker.getInstance().requestProjectSync(project, request, new GradleSyncListener.Adapter() {
            @Override
            public void syncSucceeded(@NotNull Project project) {
                promise.setResult(null);
            }

            @Override
            public void syncFailed(@NotNull Project project, @NotNull String errorMessage) {
                promise.setError(errorMessage);
            }
        });

        return promise;
    }

    private static void registerUndoAction(@NotNull Project project) {
        UndoManager.getInstance(project).undoableActionPerformed(new BasicUndoableAction() {
            @Override
            public void undo() throws UnexpectedUndoException {
                requestProjectSync(project);
            }

            @Override
            public void redo() throws UnexpectedUndoException {
                requestProjectSync(project);
            }
        });
    }

    /**
     * Given a library entry, find out its corresponded gradle dependency entry like 'group:name:version".
     */
    @Nullable
    private static ArtifactDependencySpec findNewExternalDependency(@NotNull Project project,
            @NotNull Library library) {
        if (library.getName() == null) {
            return null;
        }
        ArtifactDependencySpec result = null;
        for (Module module : ModuleManager.getInstance(project).getModules()) {
            AndroidModuleModel androidModuleModel = AndroidModuleModel.get(module);
            if (androidModuleModel != null && findLibrary(module, library.getName()) != null) {
                result = findNewExternalDependency(library, androidModuleModel);
                break;
            }
        }

        if (result == null) {
            result = findNewExternalDependencyByExaminingPath(library);
        }
        return result;
    }

    @Nullable
    private static ArtifactDependencySpec findNewExternalDependency(@NotNull Library library,
            @NotNull AndroidModuleModel androidModel) {
        GradleVersion modelVersion = androidModel.getModelVersion();

        JavaLibrary matchedLibrary = null;
        for (BaseArtifact testArtifact : androidModel.getTestArtifactsInSelectedVariant()) {
            matchedLibrary = findMatchedLibrary(library, testArtifact, modelVersion);
            if (matchedLibrary != null) {
                break;
            }
        }
        if (matchedLibrary == null) {
            Variant selectedVariant = androidModel.getSelectedVariant();
            matchedLibrary = findMatchedLibrary(library, selectedVariant.getMainArtifact(), modelVersion);
        }
        if (matchedLibrary == null) {
            return null;
        }

        // TODO use getRequestedCoordinates once the interface is fixed.
        MavenCoordinates coordinates = matchedLibrary.getResolvedCoordinates();
        if (coordinates == null) {
            return null;
        }
        return new ArtifactDependencySpec(coordinates.getArtifactId(), coordinates.getGroupId(),
                coordinates.getVersion());
    }

    @Nullable
    private static JavaLibrary findMatchedLibrary(@NotNull Library library, @NotNull BaseArtifact artifact,
            @Nullable GradleVersion modelVersion) {
        Dependencies dependencies = getDependencies(artifact, modelVersion);
        for (JavaLibrary gradleLibrary : dependencies.getJavaLibraries()) {
            String libraryName = getNameWithoutExtension(gradleLibrary.getJarFile());
            if (libraryName.equals(library.getName())) {
                return gradleLibrary;
            }
        }
        return null;
    }

    /**
     * Gradle dependencies are stored in following path:  xxx/:groupId/:artifactId/:version/xxx/:artifactId-:version.jar
     * therefor, if we can't get the artifact information from model, then try to extract from path.
     */
    @Nullable
    private static ArtifactDependencySpec findNewExternalDependencyByExaminingPath(@NotNull Library library) {
        VirtualFile[] files = library.getFiles(OrderRootType.CLASSES);
        if (files.length == 0) {
            return null;
        }
        File file = virtualToIoFile(files[0]);
        String libraryName = library.getName();
        if (libraryName == null) {
            return null;
        }

        List<String> pathSegments = splitPath(file.getPath());

        for (int i = 1; i < pathSegments.size() - 2; i++) {
            if (libraryName.startsWith(pathSegments.get(i))) {
                String groupId = pathSegments.get(i - 1);
                String artifactId = pathSegments.get(i);
                String version = pathSegments.get(i + 1);
                if (libraryName.endsWith(version)) {
                    return new ArtifactDependencySpec(artifactId, groupId, version);
                }
            }
        }
        return null;
    }
}