// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.gui.io;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Component;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.gui.PleaseWaitDialog;
import org.openstreetmap.josm.gui.PleaseWaitRunnable;
import org.openstreetmap.josm.tools.HttpClient;
import org.openstreetmap.josm.tools.Utils;
import org.xml.sax.SAXException;
/**
* Asynchronous task for downloading and unpacking arbitrary file lists
* Shows progress bar when downloading
*/
public class DownloadFileTask extends PleaseWaitRunnable {
private final String address;
private final File file;
private final boolean mkdir;
private final boolean unpack;
/**
* Creates the download task
*
* @param parent the parent component relative to which the {@link PleaseWaitDialog} is displayed
* @param address the URL to download
* @param file The destination file
* @param mkdir {@code true} if the destination directory must be created, {@code false} otherwise
* @param unpack {@code true} if zip archives must be unpacked recursively, {@code false} otherwise
* @throws IllegalArgumentException if {@code parent} is null
*/
public DownloadFileTask(Component parent, String address, File file, boolean mkdir, boolean unpack) {
super(parent, tr("Downloading file"), false);
this.address = address;
this.file = file;
this.mkdir = mkdir;
this.unpack = unpack;
}
private static class DownloadException extends Exception {
/**
* Constructs a new {@code DownloadException}.
* @param message the detail message. The detail message is saved for
* later retrieval by the {@link #getMessage()} method.
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A <tt>null</tt> value is
* permitted, and indicates that the cause is nonexistent or unknown.)
*/
DownloadException(String message, Throwable cause) {
super(message, cause);
}
}
private boolean canceled;
private HttpClient downloadConnection;
private synchronized void closeConnectionIfNeeded() {
if (downloadConnection != null) {
downloadConnection.disconnect();
}
downloadConnection = null;
}
@Override
protected void cancel() {
this.canceled = true;
closeConnectionIfNeeded();
}
@Override
protected void finish() {
// Do nothing
}
/**
* Performs download.
* @throws DownloadException if the URL is invalid or if any I/O error occurs.
*/
public void download() throws DownloadException {
try {
if (mkdir) {
File newDir = file.getParentFile();
if (!newDir.exists()) {
Utils.mkDirs(newDir);
}
}
URL url = new URL(address);
long size;
synchronized (this) {
downloadConnection = HttpClient.create(url).useCache(false);
downloadConnection.connect();
size = downloadConnection.getResponse().getContentLength();
}
progressMonitor.setTicksCount(100);
progressMonitor.subTask(tr("Downloading File {0}: {1} bytes...", file.getName(), size));
try (
InputStream in = downloadConnection.getResponse().getContent();
OutputStream out = new FileOutputStream(file)
) {
byte[] buffer = new byte[32_768];
int count = 0;
long p1 = 0;
long p2;
for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
out.write(buffer, 0, read);
count += read;
if (canceled) break;
p2 = 100L * count / size;
if (p2 != p1) {
progressMonitor.setTicks((int) p2);
p1 = p2;
}
}
}
if (!canceled) {
Main.info(tr("Download finished"));
if (unpack) {
Main.info(tr("Unpacking {0} into {1}", file.getAbsolutePath(), file.getParent()));
unzipFileRecursively(file, file.getParent());
Utils.deleteFile(file);
}
}
} catch (MalformedURLException e) {
String msg = tr("Cannot download file ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.",
file.getName(), address);
Main.warn(msg);
throw new DownloadException(msg, e);
} catch (IOException e) {
if (canceled)
return;
throw new DownloadException(e.getMessage(), e);
} finally {
closeConnectionIfNeeded();
}
}
@Override
protected void realRun() throws SAXException, IOException {
if (canceled) return;
try {
download();
} catch (DownloadException e) {
Main.error(e);
}
}
/**
* Replies true if the task was canceled by the user
*
* @return {@code true} if the task was canceled by the user, {@code false} otherwise
*/
public boolean isCanceled() {
return canceled;
}
/**
* Recursive unzipping function
* TODO: May be placed somewhere else - Tools.Utils?
* @param file zip file
* @param dir output directory
* @throws IOException if any I/O error occurs
*/
public static void unzipFileRecursively(File file, String dir) throws IOException {
try (ZipFile zf = new ZipFile(file, StandardCharsets.UTF_8)) {
Enumeration<? extends ZipEntry> es = zf.entries();
while (es.hasMoreElements()) {
ZipEntry ze = es.nextElement();
File newFile = new File(dir, ze.getName());
if (ze.isDirectory()) {
Utils.mkDirs(newFile);
} else try (InputStream is = zf.getInputStream(ze)) {
Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
}
}
}
}
}