blob: 20d889d1edfad3f620be6874d2df8bda634d6dd4 [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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.intellij.util.download.impl;
import com.google.common.base.Throwables;
import com.google.common.util.concurrent.AtomicDouble;
import com.intellij.ide.IdeBundle;
import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.fileChooser.FileChooser;
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
import com.intellij.openapi.progress.EmptyProgressIndicator;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.util.AbstractProgressIndicatorBase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.*;
import com.intellij.util.concurrency.BoundedTaskExecutor;
import com.intellij.util.containers.hash.LinkedHashMap;
import com.intellij.util.download.DownloadableFileDescription;
import com.intellij.util.download.FileDownloader;
import com.intellij.util.io.UrlConnectionUtil;
import com.intellij.util.net.IOExceptionDialog;
import com.intellij.util.net.NetUtils;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.ide.PooledThreadExecutor;
import javax.swing.*;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicLong;
/**
* @author nik
*/
public class FileDownloaderImpl implements FileDownloader {
private static final Logger LOG = Logger.getInstance(FileDownloaderImpl.class);
private static final int CONNECTION_TIMEOUT = 60*1000;
private static final int READ_TIMEOUT = 60*1000;
@NonNls private static final String LIB_SCHEMA = "lib://";
private final List<? extends DownloadableFileDescription> myFileDescriptions;
private JComponent myParentComponent;
private @Nullable Project myProject;
private String myDirectoryForDownloadedFilesPath;
private final String myDialogTitle;
public FileDownloaderImpl(@NotNull List<? extends DownloadableFileDescription> fileDescriptions,
final @Nullable Project project,
@Nullable JComponent parentComponent,
@NotNull String presentableDownloadName) {
myProject = project;
myFileDescriptions = fileDescriptions;
myParentComponent = parentComponent;
myDialogTitle = IdeBundle.message("progress.download.0.title", StringUtil.capitalize(presentableDownloadName));
}
@Nullable
@Override
public List<VirtualFile> downloadFilesWithProgress(@Nullable String targetDirectoryPath,
@Nullable Project project,
@Nullable JComponent parentComponent) {
final List<Pair<VirtualFile, DownloadableFileDescription>> pairs = downloadWithProgress(targetDirectoryPath, project, parentComponent);
if (pairs == null) return null;
List<VirtualFile> files = new ArrayList<VirtualFile>();
for (Pair<VirtualFile, DownloadableFileDescription> pair : pairs) {
files.add(pair.getFirst());
}
return files;
}
@Nullable
@Override
public List<Pair<VirtualFile, DownloadableFileDescription>> downloadWithProgress(@Nullable String targetDirectoryPath,
@Nullable Project project,
@Nullable JComponent parentComponent) {
File dir;
if (targetDirectoryPath != null) {
dir = new File(targetDirectoryPath);
}
else {
VirtualFile virtualDir = chooseDirectoryForFiles(project, parentComponent);
if (virtualDir != null) {
dir = VfsUtilCore.virtualToIoFile(virtualDir);
}
else {
return null;
}
}
return downloadWithProcess(dir, project, parentComponent);
}
@Nullable
private List<Pair<VirtualFile,DownloadableFileDescription>> downloadWithProcess(final File targetDir,
Project project,
JComponent parentComponent) {
final Ref<List<Pair<File, DownloadableFileDescription>>> localFiles = Ref.create(null);
final Ref<IOException> exceptionRef = Ref.create(null);
boolean completed = ProgressManager.getInstance().runProcessWithProgressSynchronously(new Runnable() {
@Override
public void run() {
try {
localFiles.set(download(targetDir));
}
catch (IOException e) {
exceptionRef.set(e);
}
}
}, myDialogTitle, true, project, parentComponent);
if (!completed) {
return null;
}
Exception exception = exceptionRef.get();
if (exception != null) {
final boolean tryAgain = IOExceptionDialog.showErrorDialog(myDialogTitle, exception.getMessage());
if (tryAgain) {
return downloadWithProcess(targetDir, project, parentComponent);
}
return null;
}
return findVirtualFiles(localFiles.get());
}
@NotNull
@Override
public List<Pair<File, DownloadableFileDescription>> download(@NotNull final File targetDir) throws IOException {
final List<Pair<File, DownloadableFileDescription>> downloadedFiles = new ArrayList<Pair<File, DownloadableFileDescription>>();
final List<Pair<File, DownloadableFileDescription>> existingFiles = new ArrayList<Pair<File, DownloadableFileDescription>>();
ProgressIndicator parentIndicator = ProgressManager.getInstance().getProgressIndicator();
if (parentIndicator == null) {
parentIndicator = new EmptyProgressIndicator();
}
try {
final ConcurrentTasksProgressManager progressManager = new ConcurrentTasksProgressManager(parentIndicator, myFileDescriptions.size());
parentIndicator.setText(IdeBundle.message("progress.downloading.0.files.text", myFileDescriptions.size()));
int maxParallelDownloads = Runtime.getRuntime().availableProcessors();
LOG.debug("Downloading " + myFileDescriptions.size() + " files using " + maxParallelDownloads + " threads");
long start = System.currentTimeMillis();
BoundedTaskExecutor executor = new BoundedTaskExecutor(PooledThreadExecutor.INSTANCE, maxParallelDownloads);
List<Future<Void>> results = new ArrayList<Future<Void>>();
final AtomicLong totalSize = new AtomicLong();
for (final DownloadableFileDescription description : myFileDescriptions) {
results.add(executor.submit(new Callable<Void>() {
@Override
public Void call() throws Exception {
SubTaskProgressIndicator indicator = progressManager.createSubTaskIndicator();
indicator.checkCanceled();
final File existing = new File(targetDir, description.getDefaultFileName());
final String url = description.getDownloadUrl();
if (url.startsWith(LIB_SCHEMA)) {
final String path = FileUtil.toSystemDependentName(StringUtil.trimStart(url, LIB_SCHEMA));
final File file = PathManager.findFileInLibDirectory(path);
existingFiles.add(Pair.create(file, description));
}
else if (url.startsWith(LocalFileSystem.PROTOCOL_PREFIX)) {
String path = FileUtil.toSystemDependentName(StringUtil.trimStart(url, LocalFileSystem.PROTOCOL_PREFIX));
File file = new File(path);
if (file.exists()) {
existingFiles.add(Pair.create(file, description));
}
}
else {
File downloaded;
try {
downloaded = downloadFile(description, existing, indicator);
}
catch (IOException e) {
throw new IOException(IdeBundle.message("error.file.download.failed", description.getDownloadUrl(), e.getMessage()), e);
}
if (FileUtil.filesEqual(downloaded, existing)) {
existingFiles.add(Pair.create(existing, description));
}
else {
totalSize.addAndGet(downloaded.length());
downloadedFiles.add(Pair.create(downloaded, description));
}
}
indicator.finished();
return null;
}
}));
}
for (Future<Void> result : results) {
try {
result.get();
}
catch (InterruptedException e) {
throw new ProcessCanceledException();
}
catch (ExecutionException e) {
Throwables.propagateIfInstanceOf(e.getCause(), IOException.class);
Throwables.propagateIfInstanceOf(e.getCause(), ProcessCanceledException.class);
LOG.error(e);
}
}
long duration = System.currentTimeMillis() - start;
LOG.debug("Downloaded " + StringUtil.formatFileSize(totalSize.get()) + " in " + StringUtil.formatDuration(duration) + "(" + duration + "ms)");
List<Pair<File, DownloadableFileDescription>> localFiles = new ArrayList<Pair<File, DownloadableFileDescription>>();
localFiles.addAll(moveToDir(downloadedFiles, targetDir));
localFiles.addAll(existingFiles);
return localFiles;
}
catch (ProcessCanceledException e) {
deleteFiles(downloadedFiles);
throw e;
}
catch (IOException e) {
deleteFiles(downloadedFiles);
throw e;
}
}
@Nullable
private static VirtualFile chooseDirectoryForFiles(Project project, JComponent parentComponent) {
final FileChooserDescriptor descriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor();
descriptor.setTitle(IdeBundle.message("dialog.directory.for.downloaded.files.title"));
final VirtualFile baseDir = project != null ? project.getBaseDir() : null;
return FileChooser.chooseFile(descriptor, parentComponent, project, baseDir);
}
private static List<Pair<File, DownloadableFileDescription>> moveToDir(List<Pair<File, DownloadableFileDescription>> downloadedFiles,
final File targetDir) throws IOException {
FileUtil.createDirectory(targetDir);
List<Pair<File, DownloadableFileDescription>> result = new ArrayList<Pair<File, DownloadableFileDescription>>();
for (Pair<File, DownloadableFileDescription> pair : downloadedFiles) {
final DownloadableFileDescription description = pair.getSecond();
final String fileName = description.generateFileName(new Condition<String>() {
@Override
public boolean value(String s) {
return !new File(targetDir, s).exists();
}
});
final File toFile = new File(targetDir, fileName);
FileUtil.rename(pair.getFirst(), toFile);
result.add(Pair.create(toFile, description));
}
return result;
}
@NotNull
private static List<Pair<VirtualFile, DownloadableFileDescription>> findVirtualFiles(List<Pair<File, DownloadableFileDescription>> ioFiles) {
List<Pair<VirtualFile,DownloadableFileDescription>> result = new ArrayList<Pair<VirtualFile, DownloadableFileDescription>>();
for (final Pair<File, DownloadableFileDescription> pair : ioFiles) {
final File ioFile = pair.getFirst();
VirtualFile libraryRootFile = new WriteAction<VirtualFile>() {
@Override
protected void run(final Result<VirtualFile> result) {
final String url = VfsUtil.getUrlForLibraryRoot(ioFile);
LocalFileSystem.getInstance().refreshAndFindFileByIoFile(ioFile);
result.setResult(VirtualFileManager.getInstance().refreshAndFindFileByUrl(url));
}
}.execute().getResultObject();
if (libraryRootFile != null) {
result.add(Pair.create(libraryRootFile, pair.getSecond()));
}
}
return result;
}
private static void deleteFiles(final List<Pair<File, DownloadableFileDescription>> pairs) {
for (Pair<File, DownloadableFileDescription> pair : pairs) {
FileUtil.delete(pair.getFirst());
}
}
@NotNull
private static File downloadFile(final @NotNull DownloadableFileDescription fileDescription, final @NotNull File existingFile,
final @NotNull ProgressIndicator indicator) throws IOException {
final String presentableUrl = fileDescription.getPresentableDownloadUrl();
indicator.setText2(IdeBundle.message("progress.connecting.to.download.file.text", presentableUrl));
indicator.setIndeterminate(true);
HttpURLConnection connection = (HttpURLConnection)new URL(fileDescription.getDownloadUrl()).openConnection();
connection.setConnectTimeout(CONNECTION_TIMEOUT);
connection.setReadTimeout(READ_TIMEOUT);
InputStream input = null;
BufferedOutputStream output = null;
boolean deleteFile = true;
File tempFile = null;
try {
final int responseCode = connection.getResponseCode();
if (responseCode != HttpURLConnection.HTTP_OK) {
throw new IOException(IdeBundle.message("error.connection.failed.with.http.code.N", responseCode));
}
final int size = connection.getContentLength();
if (existingFile.exists() && size == existingFile.length()) {
return existingFile;
}
tempFile = FileUtil.createTempFile("downloaded", "file");
input = UrlConnectionUtil.getConnectionInputStreamWithException(connection, indicator);
output = new BufferedOutputStream(new FileOutputStream(tempFile));
indicator.setText2(IdeBundle.message("progress.download.file.text", fileDescription.getPresentableFileName(), presentableUrl));
indicator.setIndeterminate(size == -1);
NetUtils.copyStreamContent(indicator, input, output, size);
deleteFile = false;
return tempFile;
}
finally {
if (input != null) {
input.close();
}
if (output != null) {
output.close();
}
if (deleteFile && tempFile != null) {
FileUtil.delete(tempFile);
}
connection.disconnect();
}
}
@NotNull
@Override
public FileDownloader toDirectory(@NotNull String directoryForDownloadedFilesPath) {
myDirectoryForDownloadedFilesPath = directoryForDownloadedFilesPath;
return this;
}
@Nullable
@Override
public VirtualFile[] download() {
List<VirtualFile> files = downloadFilesWithProgress(myDirectoryForDownloadedFilesPath, myProject, myParentComponent);
return files != null ? VfsUtilCore.toVirtualFileArray(files) : null;
}
@Nullable
@Override
public List<Pair<VirtualFile, DownloadableFileDescription>> downloadAndReturnWithDescriptions() {
return downloadWithProgress(myDirectoryForDownloadedFilesPath, myProject, myParentComponent);
}
private static class ConcurrentTasksProgressManager {
private final ProgressIndicator myParent;
private final int myTasksCount;
private final AtomicDouble myTotalFraction;
private final Object myLock = new Object();
private LinkedHashMap<SubTaskProgressIndicator, String> myText2Stack = new LinkedHashMap<SubTaskProgressIndicator, String>();
private ConcurrentTasksProgressManager(ProgressIndicator parent, int tasksCount) {
myParent = parent;
myTasksCount = tasksCount;
myTotalFraction = new AtomicDouble();
}
public void updateFraction(double delta) {
myTotalFraction.addAndGet(delta / myTasksCount);
myParent.setFraction(myTotalFraction.get());
}
public SubTaskProgressIndicator createSubTaskIndicator() {
return new SubTaskProgressIndicator(this);
}
public void setText2(@NotNull SubTaskProgressIndicator subTask, @Nullable String text) {
if (text != null) {
synchronized (myLock) {
myText2Stack.put(subTask, text);
}
myParent.setText2(text);
}
else {
String prev;
synchronized (myLock) {
myText2Stack.remove(subTask);
prev = myText2Stack.getLastValue();
}
if (prev != null) {
myParent.setText2(prev);
}
}
}
}
private static class SubTaskProgressIndicator extends AbstractProgressIndicatorBase {
private final AtomicDouble myFraction;
private final ConcurrentTasksProgressManager myProgressManager;
private SubTaskProgressIndicator(ConcurrentTasksProgressManager progressManager) {
myProgressManager = progressManager;
myFraction = new AtomicDouble();
}
@Override
public void setFraction(double newValue) {
double oldValue = myFraction.getAndSet(newValue);
myProgressManager.updateFraction(newValue - oldValue);
}
@Override
public void setIndeterminate(boolean indeterminate) {
if (myProgressManager.myTasksCount > 1) return;
super.setIndeterminate(indeterminate);
}
@Override
public void setText2(String text) {
myProgressManager.setText2(this, text);
}
@Override
public double getFraction() {
return myFraction.get();
}
public void finished() {
setFraction(1);
myProgressManager.setText2(this, null);
}
@Override
public boolean isCanceled() {
return super.isCanceled() || myProgressManager.myParent.isCanceled();
}
}
}