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}