001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.gui.io; 003 004import static org.openstreetmap.josm.tools.I18n.tr; 005 006import java.awt.Component; 007import java.io.File; 008import java.io.FileOutputStream; 009import java.io.IOException; 010import java.io.InputStream; 011import java.io.OutputStream; 012import java.net.MalformedURLException; 013import java.net.URL; 014import java.nio.charset.StandardCharsets; 015import java.nio.file.Files; 016import java.nio.file.StandardCopyOption; 017import java.util.Enumeration; 018import java.util.zip.ZipEntry; 019import java.util.zip.ZipFile; 020 021import org.openstreetmap.josm.Main; 022import org.openstreetmap.josm.gui.PleaseWaitDialog; 023import org.openstreetmap.josm.gui.PleaseWaitRunnable; 024import org.openstreetmap.josm.tools.HttpClient; 025import org.openstreetmap.josm.tools.Utils; 026import org.xml.sax.SAXException; 027 028/** 029 * Asynchronous task for downloading and unpacking arbitrary file lists 030 * Shows progress bar when downloading 031 */ 032public class DownloadFileTask extends PleaseWaitRunnable { 033 private final String address; 034 private final File file; 035 private final boolean mkdir; 036 private final boolean unpack; 037 038 /** 039 * Creates the download task 040 * 041 * @param parent the parent component relative to which the {@link PleaseWaitDialog} is displayed 042 * @param address the URL to download 043 * @param file The destination file 044 * @param mkdir {@code true} if the destination directory must be created, {@code false} otherwise 045 * @param unpack {@code true} if zip archives must be unpacked recursively, {@code false} otherwise 046 * @throws IllegalArgumentException if {@code parent} is null 047 */ 048 public DownloadFileTask(Component parent, String address, File file, boolean mkdir, boolean unpack) { 049 super(parent, tr("Downloading file"), false); 050 this.address = address; 051 this.file = file; 052 this.mkdir = mkdir; 053 this.unpack = unpack; 054 } 055 056 private static class DownloadException extends Exception { 057 /** 058 * Constructs a new {@code DownloadException}. 059 * @param message the detail message. The detail message is saved for 060 * later retrieval by the {@link #getMessage()} method. 061 * @param cause the cause (which is saved for later retrieval by the 062 * {@link #getCause()} method). (A <tt>null</tt> value is 063 * permitted, and indicates that the cause is nonexistent or unknown.) 064 */ 065 DownloadException(String message, Throwable cause) { 066 super(message, cause); 067 } 068 } 069 070 private boolean canceled; 071 private HttpClient downloadConnection; 072 073 private synchronized void closeConnectionIfNeeded() { 074 if (downloadConnection != null) { 075 downloadConnection.disconnect(); 076 } 077 downloadConnection = null; 078 } 079 080 @Override 081 protected void cancel() { 082 this.canceled = true; 083 closeConnectionIfNeeded(); 084 } 085 086 @Override 087 protected void finish() { 088 // Do nothing 089 } 090 091 /** 092 * Performs download. 093 * @throws DownloadException if the URL is invalid or if any I/O error occurs. 094 */ 095 public void download() throws DownloadException { 096 try { 097 if (mkdir) { 098 File newDir = file.getParentFile(); 099 if (!newDir.exists()) { 100 Utils.mkDirs(newDir); 101 } 102 } 103 104 URL url = new URL(address); 105 long size; 106 synchronized (this) { 107 downloadConnection = HttpClient.create(url).useCache(false); 108 downloadConnection.connect(); 109 size = downloadConnection.getResponse().getContentLength(); 110 } 111 112 progressMonitor.setTicksCount(100); 113 progressMonitor.subTask(tr("Downloading File {0}: {1} bytes...", file.getName(), size)); 114 115 try ( 116 InputStream in = downloadConnection.getResponse().getContent(); 117 OutputStream out = new FileOutputStream(file) 118 ) { 119 byte[] buffer = new byte[32_768]; 120 int count = 0; 121 long p1 = 0; 122 long p2; 123 for (int read = in.read(buffer); read != -1; read = in.read(buffer)) { 124 out.write(buffer, 0, read); 125 count += read; 126 if (canceled) break; 127 p2 = 100L * count / size; 128 if (p2 != p1) { 129 progressMonitor.setTicks((int) p2); 130 p1 = p2; 131 } 132 } 133 } 134 if (!canceled) { 135 Main.info(tr("Download finished")); 136 if (unpack) { 137 Main.info(tr("Unpacking {0} into {1}", file.getAbsolutePath(), file.getParent())); 138 unzipFileRecursively(file, file.getParent()); 139 Utils.deleteFile(file); 140 } 141 } 142 } catch (MalformedURLException e) { 143 String msg = tr("Cannot download file ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", 144 file.getName(), address); 145 Main.warn(msg); 146 throw new DownloadException(msg, e); 147 } catch (IOException e) { 148 if (canceled) 149 return; 150 throw new DownloadException(e.getMessage(), e); 151 } finally { 152 closeConnectionIfNeeded(); 153 } 154 } 155 156 @Override 157 protected void realRun() throws SAXException, IOException { 158 if (canceled) return; 159 try { 160 download(); 161 } catch (DownloadException e) { 162 Main.error(e); 163 } 164 } 165 166 /** 167 * Replies true if the task was canceled by the user 168 * 169 * @return {@code true} if the task was canceled by the user, {@code false} otherwise 170 */ 171 public boolean isCanceled() { 172 return canceled; 173 } 174 175 /** 176 * Recursive unzipping function 177 * TODO: May be placed somewhere else - Tools.Utils? 178 * @param file zip file 179 * @param dir output directory 180 * @throws IOException if any I/O error occurs 181 */ 182 public static void unzipFileRecursively(File file, String dir) throws IOException { 183 try (ZipFile zf = new ZipFile(file, StandardCharsets.UTF_8)) { 184 Enumeration<? extends ZipEntry> es = zf.entries(); 185 while (es.hasMoreElements()) { 186 ZipEntry ze = es.nextElement(); 187 File newFile = new File(dir, ze.getName()); 188 if (ze.isDirectory()) { 189 Utils.mkDirs(newFile); 190 } else try (InputStream is = zf.getInputStream(ze)) { 191 Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); 192 } 193 } 194 } 195 } 196}