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.IOException;
009import java.io.InputStream;
010import java.io.OutputStream;
011import java.net.MalformedURLException;
012import java.net.URL;
013import java.nio.charset.StandardCharsets;
014import java.nio.file.Files;
015import java.nio.file.InvalidPathException;
016import java.nio.file.StandardCopyOption;
017import java.util.Enumeration;
018import java.util.zip.ZipEntry;
019import java.util.zip.ZipFile;
020
021import org.openstreetmap.josm.gui.PleaseWaitDialog;
022import org.openstreetmap.josm.gui.PleaseWaitRunnable;
023import org.openstreetmap.josm.tools.HttpClient;
024import org.openstreetmap.josm.tools.Logging;
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 <code>null</code> 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 = Files.newOutputStream(file.toPath())
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                Logging.info(tr("Download finished"));
136                if (unpack) {
137                    Logging.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            Logging.warn(msg);
146            throw new DownloadException(msg, e);
147        } catch (IOException | InvalidPathException 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            Logging.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}