/*
* 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.wizard;
import com.android.tools.idea.actions.AndroidImportModuleAction;
import com.android.tools.idea.templates.TemplateManager;
import com.google.common.collect.Lists;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorBuilder;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.platform.templates.github.DownloadUtil;
import com.intellij.platform.templates.github.Outcome;
import com.intellij.platform.templates.github.ZipUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.concurrent.Callable;
import static com.android.SdkConstants.FN_BUILD_GRADLE;
import static com.android.SdkConstants.FN_SETTINGS_GRADLE;
/**
* A wizard to create a new object (either by inflating a template or by instantiating the object
* directly) from a Github repository. This wizard will clone to the Github repository to the local
* disk and then scan it for templates and samples.
*/
public class NewFromGithubWizard extends TemplateWizard implements TemplateWizardStep.UpdateListener {
private static final Logger LOG = Logger.getInstance(NewFromGithubWizard.class);
private static final String WIZARD_TITLE = "From GitHub";
private static final long TIMEOUT = 60 * 60 * 1000; // 1 Hour's worth of Millis
@Nullable private final VirtualFile myTargetFile;
private Module myModule;
private TemplateWizardState myWizardState = new TemplateWizardState();
private ChooseGithubRepositoryStep myChooseGithubRepositoryStep;
public NewFromGithubWizard(@Nullable Project project, @Nullable Module module, @Nullable VirtualFile targetFile) {
super("From GitHub", project);
myModule = module;
myTargetFile = targetFile;
init();
}
@Override
protected void init() {
myChooseGithubRepositoryStep = new ChooseGithubRepositoryStep(myWizardState, myProject, myModule, this);
mySteps.add(0, myChooseGithubRepositoryStep);
super.init();
}
/**
* Container for results obtained by an attempt to download a repository from Github.
*/
public static class GithubRepoContents {
public List<File> templateFolders;
public List<File> sampleRoots;
public File rootFolder;
public String errorMessage;
}
/**
* Download a Github repository and search it for templates and Android Studio projects. Returns 2 lists of files, one pointing
* to directories that contain templates and the other to directories that contain projects.
* @param url The github URL to retrieve
* @param branch The name of the branch to retrieve. If not specified, it is assumed to be "master"
* @param cacheDirectory An optional location to cache the downloaded repository. If not specified it will default to a working
* directory inside the operating system's temporary folder (e.g. /tmp)
* @return A GithubRepoContents instance. If the download failed, then the errorMessage member will be set. If the errorMessage
* is null, then both templateFolders and sampleRoots will NOT be null, but MAY be empty.
*/
@NotNull
public static GithubRepoContents downloadGithubRepo(@NotNull Project project, @NotNull String url, @Nullable String branch,
@Nullable File cacheDirectory) {
GithubRepoContents returnValue = new GithubRepoContents();
if (cacheDirectory == null) {
cacheDirectory = new File(FileUtil.getTempDirectory(), "github_cache");
}
if (branch == null || branch.trim().isEmpty()) {
branch = "master";
}
URL parsedUrl;
try {
parsedUrl = new URL(url);
}
catch (MalformedURLException e) {
returnValue.errorMessage = "Malformed URL";
return returnValue;
}
String repositoryName = "Github" + parsedUrl.getPath().replace('/', '-');
final File outputFile = new File(cacheDirectory, repositoryName + ".zip");
final String finalUrl = url + "/zipball/" + branch;
File unzippedDir = new File(cacheDirectory, repositoryName);
// TODO: Do some smarter caching here
if (!unzippedDir.exists() || unzippedDir.lastModified() == 0 ||
(System.currentTimeMillis() - unzippedDir.lastModified()) > TIMEOUT) {
FileUtil.delete(unzippedDir);
try {
Outcome<File> outcome = DownloadUtil.provideDataWithProgressSynchronously(
project,
"Downloading project from GitHub",
"Downloading zip archive" + DownloadUtil.CONTENT_LENGTH_TEMPLATE + " ...",
new Callable<File>() {
@Override
public File call() throws Exception {
ProgressIndicator progress = ProgressManager.getInstance().getProgressIndicator();
DownloadUtil.downloadAtomically(progress, finalUrl, outputFile);
return outputFile;
}
}, null
);
Exception e = outcome.getException();
if (e != null) {
throw e;
}
ZipUtil.unzip(ProgressManager.getInstance().getProgressIndicator(), unzippedDir, outputFile, null, null, true);
}
catch (Exception e) {
returnValue.errorMessage = "Could not download specified project from Github. Check the URL and branchname.\n\n" + e.getMessage();
return returnValue;
}
outputFile.delete();
}
/**
* We don't know beforehand what kind of repo we just downloaded.
* Our return value is a thin wrapper around 2 lists, one of files which point to the root
* folder of samples, one of files which point to the root folder of templates.
*/
returnValue.rootFolder = unzippedDir;
returnValue.templateFolders = TemplateManager.getTemplatesFromDirectory(unzippedDir, true);
returnValue.sampleRoots = findSamplesInDirectory(unzippedDir, true);
return returnValue;
}
/**
* Returns a list of files which point to the roots of projects/modules within the given folder.
* If the recursive parameter is not set, we will only check to see if the given directory is a gradle based project and return
* a singleton list. Otherwise, we recurse down, finding all project/module roots (folders containing a build.gradle file) and return
* them as a list.
* @param directory The root directory to check
* @param recursive Whether we should look several levels down.
* @return A list of root directories of projects and/or modules
*/
@NotNull
public static List<File> findSamplesInDirectory(@NotNull File directory, boolean recursive) {
List<File> samples = Lists.newArrayList();
if (new File(directory, FN_BUILD_GRADLE).exists() || new File(directory, FN_SETTINGS_GRADLE).exists()) {
samples.add(directory);
}
if (recursive) {
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
samples.addAll(findSamplesInDirectory(file, true));
}
}
}
}
return samples;
}
/**
* Launch the NewTemplateObjectWizard to instantiate a template contained in the downloaded repository described by
* the given GithubRepoContents.
* @return null if the given repository is valid and contains at least one template, an error message otherwise.
*/
@Nullable
public static String runTemplateWizard(@Nullable Project project, @Nullable Module module, @Nullable VirtualFile targetLocation,
@NotNull GithubRepoContents githubRepoContents) {
if (githubRepoContents.templateFolders == null || githubRepoContents.templateFolders.isEmpty()) {
String error;
if (githubRepoContents.errorMessage != null) {
error = githubRepoContents.errorMessage;
} else {
error = "No templates found. Please check the repository that you are attempting to import from.";
}
return error;
}
NewTemplateObjectWizard wizard;
if (githubRepoContents.templateFolders.size() == 1) {
wizard = new NewTemplateObjectWizard(project, module, targetLocation, WIZARD_TITLE, githubRepoContents.templateFolders.get(0));
} else {
wizard = new NewTemplateObjectWizard(project, module, targetLocation, WIZARD_TITLE, githubRepoContents.templateFolders);
}
wizard.show();
if (wizard.isOK()) {
wizard.createTemplateObject();
}
return null;
}
/**
* Launch the ImportModuleWizard to import a module contained in the downloaded repository described by
* the given GithubRepoContents.
* @return null if the given repository is valid and contains at least one sample, an error message otherwise.
*/
@Nullable
private static String runImportWizard(Project project, Module module, GithubRepoContents githubRepoContents) {
if (githubRepoContents.sampleRoots == null || githubRepoContents.sampleRoots.isEmpty()) {
String error;
if (githubRepoContents.errorMessage != null) {
error = githubRepoContents.errorMessage;
} else {
error = "No importable samples found. Please check the repository that you are attempting to import from.";
}
return error;
}
VirtualFile sourceFolder;
if (githubRepoContents.sampleRoots.size() == 1) {
sourceFolder = VfsUtil.findFileByIoFile(githubRepoContents.rootFolder, false);
} else {
ChooseFromFileListDialog dialog = new ChooseFromFileListDialog(project, githubRepoContents.sampleRoots);
dialog.show();
if (dialog.isOK()) {
sourceFolder = VfsUtil.findFileByIoFile(dialog.getChosenFile(), false);
} else {
return null;
}
}
if (sourceFolder == null) {
return "No file selected";
}
try {
AndroidImportModuleAction.importGradleSubprojectAsModule(sourceFolder, project);
}
catch (IOException e) {
return "An error occurred while importing a sample from github: " + e.getMessage();
}
return null;
}
private void setErrorHtml(String s) {
((TemplateWizardStep)mySteps.get(getCurrentStep())).setErrorHtml(s);
}
@Override
protected void doOKAction() {
GithubRepoContents downloadResult = downloadGithubRepo(myProject, myChooseGithubRepositoryStep.getUrl(),
myChooseGithubRepositoryStep.getBranch(), null);
if (downloadResult.errorMessage != null) {
setErrorHtml(downloadResult.errorMessage);
return;
}
// Should never occur
assert downloadResult.templateFolders != null && downloadResult.sampleRoots != null;
super.doOKAction();
String error;
if (!downloadResult.templateFolders.isEmpty()) {
error = runTemplateWizard(myProject, myModule, myTargetFile, downloadResult);
} else {
error = runImportWizard(myProject, myModule, downloadResult);
}
if (error != null) {
LOG.error(error);
}
}
}