blob: 68da33a90cb5f929d622d11e16808e9d57fdb7da [file] [log] [blame]
/*
* 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.tools.idea.gradle.parser.GradleSettingsFile;
import com.android.tools.idea.gradle.util.GradleUtil;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.*;
import com.google.common.collect.*;
import com.intellij.ide.util.projectWizard.ModuleWizardStep;
import com.intellij.ide.util.projectWizard.WizardContext;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.options.ConfigurationException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.containers.HashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.gradle.util.GradleConstants;
import java.io.File;
import java.io.IOException;
import java.util.*;
import static com.android.tools.idea.gradle.project.ProjectImportUtil.findImportTarget;
import static com.google.common.base.Predicates.in;
import static com.google.common.base.Predicates.not;
import static com.google.common.base.Predicates.notNull;
/**
* Creates new project module from source files with Gradle configuration.
*/
public final class GradleModuleImporter extends ModuleImporter {
private final Logger LOG = Logger.getInstance(getClass());
@Nullable private final Project myProject;
private final boolean myIsWizard;
public GradleModuleImporter(@NotNull WizardContext context) {
this(context.getProject(), true);
}
public GradleModuleImporter(@NotNull Project project) {
this(project, false);
}
private GradleModuleImporter(@Nullable Project project, boolean isWizard) {
myIsWizard = isWizard;
myProject = project;
}
public static boolean isGradleProject(VirtualFile importSource) {
VirtualFile target = findImportTarget(importSource);
return target != null && GradleConstants.EXTENSION.equals(target.getExtension());
}
@Override
public boolean isStepVisible(ModuleWizardStep step) {
return false;
}
@Override
public List<? extends ModuleWizardStep> createWizardSteps() {
return Collections.emptyList();
}
@Override
public void importProjects(Map<String, VirtualFile> projects) {
try {
importModules(this, projects, myProject, null);
}
catch (IOException e) {
LOG.error(e);
}
catch (ConfigurationException e) {
LOG.error(e);
}
}
@Override
public boolean isValid() {
return true;
}
@Override
public boolean canImport(VirtualFile importSource) {
try {
return isGradleProject(importSource) && (myIsWizard || findModules(importSource).size() == 1);
}
catch (IOException e) {
LOG.error(e);
return false;
}
}
@Override
public Set<ModuleToImport> findModules(VirtualFile importSource) throws IOException {
assert myProject != null;
return getRelatedProjects(importSource, myProject);
}
/**
* Find related modules that should be imported into Android Studio together with the project user chose so it could be built.
* <p/>
* Top-level use-cases:
* 1. If the user selects top-level project (e.g. the one with settings.gradle) Android Studio will import all its sub-projects.
* 2. For leaf projects (ones with build.gradle), Android Studio will import selected project and the projects it depends on.
*
* @param sourceProject the destinationProject that user wants to import
* @param destinationProject destination destinationProject
* @return mapping from module name to {@code VirtualFile} containing module contents. Values will be null if the module location was not
* found.
*/
@NotNull
public static Set<ModuleToImport> getRelatedProjects(@NotNull VirtualFile sourceProject, @NotNull Project destinationProject) {
VirtualFile settingsGradle = sourceProject.findFileByRelativePath(SdkConstants.FN_SETTINGS_GRADLE);
if (settingsGradle != null) {
return buildModulesSet(getSubProjects(settingsGradle, destinationProject),
GradleProjectDependencyParser.newInstance(destinationProject));
}
else {
return getRequiredProjects(sourceProject, destinationProject);
}
}
/**
* Find direct and transitive dependency projects.
*/
@NotNull
private static Set<ModuleToImport> getRequiredProjects(VirtualFile sourceProject, Project destinationProject) {
GradleSiblingLookup subProjectLocations = new GradleSiblingLookup(sourceProject, destinationProject);
Function<VirtualFile, Iterable<String>> parser = GradleProjectDependencyParser.newInstance(destinationProject);
Map<String, VirtualFile> modules = Maps.newHashMap();
List<VirtualFile> toAnalyze = Lists.newLinkedList();
toAnalyze.add(sourceProject);
while (!toAnalyze.isEmpty()) {
Set<String> dependencies = Sets.newHashSet(Iterables.concat(Iterables.transform(toAnalyze, parser)));
Iterable<String> notAnalyzed = Iterables.filter(dependencies, not(in(modules.keySet())));
// Turns out, Maps#toMap does not allow null values...
Map<String, VirtualFile> dependencyToLocation = Maps.newHashMap();
for (String dependency : notAnalyzed) {
dependencyToLocation.put(dependency, subProjectLocations.apply(dependency));
}
modules.putAll(dependencyToLocation);
toAnalyze = FluentIterable.from(dependencyToLocation.values()).filter(notNull()).toList();
}
modules.put(subProjectLocations.getPrimaryProjectName(), sourceProject);
return buildModulesSet(modules, parser);
}
private static Set<ModuleToImport> buildModulesSet(Map<String, VirtualFile> modules, Function<VirtualFile, Iterable<String>> parser) {
Set<ModuleToImport> modulesSet = new HashSet<ModuleToImport>(modules.size());
for (Map.Entry<String, VirtualFile> entry : modules.entrySet()) {
VirtualFile location = entry.getValue();
Supplier<Iterable<String>> dependencyComputer;
if (location != null) {
dependencyComputer = Suppliers.compose(parser, Suppliers.ofInstance(location));
}
else {
dependencyComputer = Suppliers.<Iterable<String>>ofInstance(ImmutableSet.<String>of());
}
modulesSet.add(new ModuleToImport(entry.getKey(), location, dependencyComputer));
}
return modulesSet;
}
@NotNull
public static Map<String, VirtualFile> getSubProjects(@NotNull final VirtualFile settingsGradle, Project destinationProject) {
final GradleSettingsFile settingsFile = new GradleSettingsFile(settingsGradle, destinationProject);
Map<String, File> allProjects = settingsFile.getModulesWithLocation();
return Maps.transformValues(allProjects, new ResolvePath(VfsUtilCore.virtualToIoFile(settingsGradle.getParent())));
}
/**
* Import set of gradle modules into existing Android Studio project. Note that no validation will be performed, modules will
* be copied as is and settings.xml will be updated for the imported modules. It is callers' responsibility to ensure content
* can be copied to the target directory and that module list is valid.
*
* @param modules mapping between module names and locations on the filesystem. Neither name nor location should be null
* @param project project to import the modules to
* @param listener optional object that gets notified of operation success or failure
*/
@VisibleForTesting
static void importModules(@NotNull final Object requestor,
@NotNull final Map<String, VirtualFile> modules,
@Nullable final Project project,
@Nullable final GradleSyncListener listener) throws IOException, ConfigurationException {
String error = validateProjectsForImport(modules);
if (error != null) {
if (listener != null && project != null) {
listener.syncFailed(project, error);
return;
}
else {
throw new IOException(error);
}
}
assert project != null;
Throwable throwable = new WriteCommandAction.Simple(project) {
@Override
protected void run() throws Throwable {
copyAndRegisterModule(requestor, modules, project, listener);
}
@Override
public boolean isSilentExecution() {
return true;
}
}.execute().getThrowable();
rethrowAsProperlyTypedException(throwable);
}
/**
* Ensures that we know paths for all projects we are trying to import.
*
* @return message string if import is not possible or <code>null</code> otherwise
*/
@Nullable
private static String validateProjectsForImport(@NotNull Map<String, VirtualFile> modules) {
Set<String> projects = new TreeSet<String>();
for (Map.Entry<String, VirtualFile> mapping : modules.entrySet()) {
if (mapping.getValue() == null) {
projects.add(mapping.getKey());
}
}
if (projects.isEmpty()) {
return null;
}
else if (projects.size() == 1) {
return String.format("Sources for module '%1$s' were not found", Iterables.getFirst(projects, null));
}
else {
String projectsList = Joiner.on("', '").join(projects);
return String.format("Sources were not found for modules '%1$s'", projectsList);
}
}
/**
* Recover actual type of the exception.
*/
private static void rethrowAsProperlyTypedException(Throwable throwable) throws IOException, ConfigurationException {
if (throwable != null) {
Throwables.propagateIfPossible(throwable, IOException.class, ConfigurationException.class);
throw new IllegalStateException(throwable);
}
}
/**
* Copy modules and adds it to settings.gradle
*/
private static void copyAndRegisterModule(@NotNull Object requestor,
@NotNull Map<String, VirtualFile> modules,
@NotNull Project project,
@Nullable GradleSyncListener listener) throws IOException, ConfigurationException {
VirtualFile projectRoot = project.getBaseDir();
if (projectRoot.findChild(SdkConstants.FN_SETTINGS_GRADLE) == null) {
projectRoot.createChildData(requestor, SdkConstants.FN_SETTINGS_GRADLE);
}
GradleSettingsFile gradleSettingsFile = GradleSettingsFile.get(project);
assert gradleSettingsFile != null : "File should have been created";
for (Map.Entry<String, VirtualFile> module : modules.entrySet()) {
String name = module.getKey();
File targetFile = GradleUtil.getModuleDefaultPath(projectRoot, name);
VirtualFile moduleSource = module.getValue();
if (moduleSource != null) {
if (!VfsUtilCore.isAncestor(projectRoot, moduleSource, true)) {
VirtualFile target = VfsUtil.createDirectoryIfMissing(targetFile.getAbsolutePath());
if (target == null) {
throw new IOException(String.format("Unable to create directory %1$s", targetFile));
}
if (target.exists()) {
target.delete(requestor);
}
moduleSource.copy(requestor, target.getParent(), target.getName());
}
else {
targetFile = VfsUtilCore.virtualToIoFile(moduleSource);
}
}
gradleSettingsFile.addModule(name, targetFile);
}
GradleProjectImporter.getInstance().requestProjectSync(project, false, listener);
}
/**
* Resolves paths that may be either relative to provided directory or absolute.
*/
private static class ResolvePath implements Function<File, VirtualFile> {
private final File mySourceDir;
public ResolvePath(File sourceDir) {
mySourceDir = sourceDir;
}
@Override
public VirtualFile apply(File path) {
if (!path.isAbsolute()) {
path = new File(mySourceDir, path.getPath());
}
return VfsUtil.findFileByIoFile(path, true);
}
}
}