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.util.Enumeration;
017import java.util.zip.ZipEntry;
018import java.util.zip.ZipFile;
019
020import org.openstreetmap.josm.Main;
021import org.openstreetmap.josm.gui.PleaseWaitDialog;
022import org.openstreetmap.josm.gui.PleaseWaitRunnable;
023import org.openstreetmap.josm.tools.Utils;
024import org.xml.sax.SAXException;
025
026/**
027 * Asynchronous task for downloading and unpacking arbitrary file lists
028 * Shows progress bar when downloading
029 */
030public class DownloadFileTask extends PleaseWaitRunnable{
031    private final String address;
032    private final File file;
033    private final boolean mkdir;
034    private final boolean unpack;
035
036    /**
037     * Creates the download task
038     *
039     * @param parent the parent component relative to which the {@link PleaseWaitDialog} is displayed
040     * @param address the URL to download
041     * @param file The destination file
042     * @param mkdir {@code true} if the destination directory must be created, {@code false} otherwise
043     * @param unpack {@code true} if zip archives must be unpacked recursively, {@code false} otherwise
044     * @throws IllegalArgumentException if {@code parent} is null
045     */
046    public DownloadFileTask(Component parent, String address, File file, boolean mkdir, boolean unpack) {
047        super(parent, tr("Downloading file"), false);
048        this.address = address;
049        this.file = file;
050        this.mkdir = mkdir;
051        this.unpack = unpack;
052    }
053
054    private static class DownloadException extends Exception {
055        public DownloadException(String msg) {
056            super(msg);
057        }
058    }
059
060    private boolean canceled;
061    private HttpURLConnection downloadConnection;
062
063    private synchronized void closeConnectionIfNeeded() {
064        if (downloadConnection != null) {
065            downloadConnection.disconnect();
066        }
067        downloadConnection = null;
068    }
069
070
071    @Override
072    protected void cancel() {
073        this.canceled = true;
074        closeConnectionIfNeeded();
075    }
076
077    @Override
078    protected void finish() {}
079
080    /**
081     * Performs download.
082     * @throws DownloadException if the URL is invalid or if any I/O error occurs.
083     */
084    public void download() throws DownloadException {
085        OutputStream out = null;
086        InputStream in = null;
087        try {
088            if (mkdir) {
089                File newDir = file.getParentFile();
090                if (!newDir.exists()) {
091                    newDir.mkdirs();
092                }
093            }
094
095            URL url = new URL(address);
096            int size;
097            synchronized(this) {
098                downloadConnection = Utils.openHttpConnection(url);
099                downloadConnection.setRequestProperty("Cache-Control", "no-cache");
100                downloadConnection.connect();
101                size = downloadConnection.getContentLength();
102            }
103
104            progressMonitor.setTicksCount(100);
105            progressMonitor.subTask(tr("Downloading File {0}: {1} bytes...", file.getName(),size));
106
107            in = downloadConnection.getInputStream();
108            out = new FileOutputStream(file);
109            byte[] buffer = new byte[32768];
110            int count=0;
111            int p1=0, p2=0;
112            for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
113                out.write(buffer, 0, read);
114                count+=read;
115                if (canceled) break;
116                p2 = 100 * count / size;
117                if (p2!=p1) {
118                    progressMonitor.setTicks(p2);
119                    p1=p2;
120                }
121            }
122            Utils.close(out);
123            if (!canceled) {
124                Main.info(tr("Download finished"));
125                if (unpack) {
126                    Main.info(tr("Unpacking {0} into {1}", file.getAbsolutePath(), file.getParent()));
127                    unzipFileRecursively(file, file.getParent());
128                    file.delete();
129                }
130            }
131        } catch(MalformedURLException e) {
132            String msg = tr("Cannot download file ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", file.getName(), address);
133            Main.warn(msg);
134            throw new DownloadException(msg);
135        } catch (IOException e) {
136            if (canceled)
137                return;
138            throw new DownloadException(e.getMessage());
139        } finally {
140            closeConnectionIfNeeded();
141            Utils.close(out);
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            e.printStackTrace();
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        OutputStream os = null;
173        InputStream is = null;
174        ZipFile zf = null;
175        try {
176            zf = new ZipFile(file);
177            Enumeration<?> es = zf.entries();
178            ZipEntry ze;
179            while (es.hasMoreElements()) {
180                ze = (ZipEntry) es.nextElement();
181                File newFile = new File(dir, ze.getName());
182                if (ze.isDirectory()) {
183                    newFile.mkdirs();
184                } else {
185                    is = zf.getInputStream(ze);
186                    os = new BufferedOutputStream(new FileOutputStream(newFile));
187                    byte[] buffer = new byte[8192];
188                    int read;
189                    while ((read = is.read(buffer)) != -1) {
190                        os.write(buffer, 0, read);
191                    }
192                    Utils.close(os);
193                    Utils.close(is);
194                }
195            }
196        } finally {
197            Utils.close(zf);
198        }
199    }
200}
201