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