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        public DownloadException(String msg) {
057            super(msg);
058        }
059    }
060
061    private boolean canceled;
062    private HttpURLConnection downloadConnection;
063
064    private synchronized void closeConnectionIfNeeded() {
065        if (downloadConnection != null) {
066            downloadConnection.disconnect();
067        }
068        downloadConnection = null;
069    }
070
071
072    @Override
073    protected void cancel() {
074        this.canceled = true;
075        closeConnectionIfNeeded();
076    }
077
078    @Override
079    protected void finish() {}
080
081    /**
082     * Performs download.
083     * @throws DownloadException if the URL is invalid or if any I/O error occurs.
084     */
085    public void download() throws DownloadException {
086        try {
087            if (mkdir) {
088                File newDir = file.getParentFile();
089                if (!newDir.exists()) {
090                    newDir.mkdirs();
091                }
092            }
093
094            URL url = new URL(address);
095            int size;
096            synchronized(this) {
097                downloadConnection = Utils.openHttpConnection(url);
098                downloadConnection.setRequestProperty("Cache-Control", "no-cache");
099                downloadConnection.connect();
100                size = downloadConnection.getContentLength();
101            }
102
103            progressMonitor.setTicksCount(100);
104            progressMonitor.subTask(tr("Downloading File {0}: {1} bytes...", file.getName(),size));
105
106            try (
107                InputStream in = downloadConnection.getInputStream();
108                OutputStream out = new FileOutputStream(file)
109            ) {
110                byte[] buffer = new byte[32768];
111                int count=0;
112                int p1=0, p2=0;
113                for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
114                    out.write(buffer, 0, read);
115                    count+=read;
116                    if (canceled) break;
117                    p2 = 100 * count / size;
118                    if (p2!=p1) {
119                        progressMonitor.setTicks(p2);
120                        p1=p2;
121                    }
122                }
123            }
124            if (!canceled) {
125                Main.info(tr("Download finished"));
126                if (unpack) {
127                    Main.info(tr("Unpacking {0} into {1}", file.getAbsolutePath(), file.getParent()));
128                    unzipFileRecursively(file, file.getParent());
129                    file.delete();
130                }
131            }
132        } catch(MalformedURLException e) {
133            String msg = tr("Cannot download file ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", file.getName(), address);
134            Main.warn(msg);
135            throw new DownloadException(msg);
136        } catch (IOException e) {
137            if (canceled)
138                return;
139            throw new DownloadException(e.getMessage());
140        } finally {
141            closeConnectionIfNeeded();
142        }
143    }
144
145    @Override
146    protected void realRun() throws SAXException, IOException {
147        if (canceled) return;
148        try {
149            download();
150        } catch(DownloadException e) {
151            Main.error(e);
152        }
153    }
154
155    /**
156     * Replies true if the task was canceled by the user
157     *
158     * @return {@code true} if the task was canceled by the user, {@code false} otherwise
159     */
160    public boolean isCanceled() {
161        return canceled;
162    }
163
164    /**
165     * Recursive unzipping function
166     * TODO: May be placed somewhere else - Tools.Utils?
167     * @param file
168     * @param dir
169     * @throws IOException
170     */
171    public static void unzipFileRecursively(File file, String dir) throws IOException {
172        try (ZipFile zf = new ZipFile(file, StandardCharsets.UTF_8)) {
173            Enumeration<? extends ZipEntry> es = zf.entries();
174            while (es.hasMoreElements()) {
175                ZipEntry ze = es.nextElement();
176                File newFile = new File(dir, ze.getName());
177                if (ze.isDirectory()) {
178                    newFile.mkdirs();
179                } else try (
180                    InputStream is = zf.getInputStream(ze);
181                    OutputStream os = new BufferedOutputStream(new FileOutputStream(newFile))
182                ) {
183                    byte[] buffer = new byte[8192];
184                    int read;
185                    while ((read = is.read(buffer)) != -1) {
186                        os.write(buffer, 0, read);
187                    }
188                }
189            }
190        }
191    }
192}