001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.awt.Component;
007import java.io.File;
008import java.io.FileOutputStream;
009import java.io.IOException;
010import java.io.InputStream;
011import java.io.OutputStream;
012import java.net.HttpURLConnection;
013import java.net.MalformedURLException;
014import java.net.URL;
015import java.util.Collection;
016import java.util.LinkedList;
017
018import org.openstreetmap.josm.Main;
019import org.openstreetmap.josm.data.Version;
020import org.openstreetmap.josm.gui.ExtendedDialog;
021import org.openstreetmap.josm.gui.PleaseWaitRunnable;
022import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
023import org.openstreetmap.josm.gui.progress.ProgressMonitor;
024import org.openstreetmap.josm.io.MirroredInputStream;
025import org.openstreetmap.josm.tools.CheckParameterUtil;
026import org.openstreetmap.josm.tools.Utils;
027import org.xml.sax.SAXException;
028
029
030/**
031 * Asynchronous task for downloading a collection of plugins.
032 *
033 * When the task is finished {@link #getDownloadedPlugins()} replies the list of downloaded plugins
034 * and {@link #getFailedPlugins()} replies the list of failed plugins.
035 *
036 */
037public class PluginDownloadTask extends PleaseWaitRunnable{
038    private final Collection<PluginInformation> toUpdate = new LinkedList<PluginInformation>();
039    private final Collection<PluginInformation> failed = new LinkedList<PluginInformation>();
040    private final Collection<PluginInformation> downloaded = new LinkedList<PluginInformation>();
041    private Exception lastException;
042    private boolean canceled;
043    private HttpURLConnection downloadConnection;
044
045    /**
046     * Creates the download task
047     *
048     * @param parent the parent component relative to which the {@link org.openstreetmap.josm.gui.PleaseWaitDialog} is displayed
049     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
050     * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog}
051     * @throws IllegalArgumentException thrown if toUpdate is null
052     */
053    public PluginDownloadTask(Component parent, Collection<PluginInformation> toUpdate, String title) throws IllegalArgumentException{
054        super(parent, title == null ? "" : title, false /* don't ignore exceptions */);
055        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
056        this.toUpdate.addAll(toUpdate);
057    }
058
059    /**
060     * Creates the task
061     *
062     * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
063     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
064     * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog}
065     * @throws IllegalArgumentException thrown if toUpdate is null
066     */
067    public PluginDownloadTask(ProgressMonitor monitor, Collection<PluginInformation> toUpdate, String title) {
068        super(title, monitor == null? NullProgressMonitor.INSTANCE: monitor, false /* don't ignore exceptions */);
069        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
070        this.toUpdate.addAll(toUpdate);
071    }
072
073    /**
074     * Sets the collection of plugins to update.
075     *
076     * @param toUpdate the collection of plugins to update. Must not be null.
077     * @throws IllegalArgumentException thrown if toUpdate is null
078     */
079    public void setPluginsToDownload(Collection<PluginInformation> toUpdate) throws IllegalArgumentException{
080        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
081        this.toUpdate.clear();
082        this.toUpdate.addAll(toUpdate);
083    }
084
085    @Override protected void cancel() {
086        this.canceled = true;
087        synchronized(this) {
088            if (downloadConnection != null) {
089                downloadConnection.disconnect();
090            }
091        }
092    }
093
094    @Override protected void finish() {}
095
096    protected void download(PluginInformation pi, File file) throws PluginDownloadException{
097        if (pi.mainversion > Version.getInstance().getVersion()) {
098            ExtendedDialog dialog = new ExtendedDialog(
099                    progressMonitor.getWindowParent(),
100                    tr("Skip download"),
101                    new String[] {
102                        tr("Download Plugin"),
103                        tr("Skip Download") }
104            );
105            dialog.setContent(tr("JOSM version {0} required for plugin {1}.", pi.mainversion, pi.name));
106            dialog.setButtonIcons(new String[] { "download.png", "cancel.png" });
107            dialog.showDialog();
108            int answer = dialog.getValue();
109            if (answer != 1)
110                throw new PluginDownloadException(tr("Download skipped"));
111        }
112        OutputStream out = null;
113        InputStream in = null;
114        try {
115            if (pi.downloadlink == null) {
116                String msg = tr("Cannot download plugin ''{0}''. Its download link is not known. Skipping download.", pi.name);
117                Main.warn(msg);
118                throw new PluginDownloadException(msg);
119            }
120            URL url = new URL(pi.downloadlink);
121            synchronized(this) {
122                downloadConnection = MirroredInputStream.connectFollowingRedirect(url);
123            }
124            in = downloadConnection.getInputStream();
125            out = new FileOutputStream(file);
126            byte[] buffer = new byte[8192];
127            for (int read = in.read(buffer); read != -1; read = in.read(buffer)) {
128                out.write(buffer, 0, read);
129            }
130        } catch (MalformedURLException e) {
131            String msg = tr("Cannot download plugin ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.", pi.name, pi.downloadlink);
132            Main.warn(msg);
133            throw new PluginDownloadException(msg);
134        } catch (IOException e) {
135            if (canceled)
136                return;
137            throw new PluginDownloadException(e);
138        } finally {
139            Utils.close(in);
140            synchronized(this) {
141                downloadConnection = null;
142            }
143            Utils.close(out);
144        }
145    }
146
147    @Override protected void realRun() throws SAXException, IOException {
148        File pluginDir = Main.pref.getPluginsDirectory();
149        if (!pluginDir.exists()) {
150            if (!pluginDir.mkdirs()) {
151                lastException = new PluginDownloadException(tr("Failed to create plugin directory ''{0}''", pluginDir.toString()));
152                failed.addAll(toUpdate);
153                return;
154            }
155        }
156        getProgressMonitor().setTicksCount(toUpdate.size());
157        for (PluginInformation d : toUpdate) {
158            if (canceled) return;
159            progressMonitor.subTask(tr("Downloading Plugin {0}...", d.name));
160            progressMonitor.worked(1);
161            File pluginFile = new File(pluginDir, d.name + ".jar.new");
162            try {
163                download(d, pluginFile);
164            } catch(PluginDownloadException e) {
165                e.printStackTrace();
166                failed.add(d);
167                continue;
168            }
169            downloaded.add(d);
170        }
171        PluginHandler.installDownloadedPlugins(false);
172    }
173
174    /**
175     * Replies true if the task was canceled by the user
176     *
177     * @return <code>true</code> if the task was stopped by the user
178     */
179    public boolean isCanceled() {
180        return canceled;
181    }
182
183    /**
184     * Replies the list of successfully downloaded plugins
185     *
186     * @return the list of successfully downloaded plugins
187     */
188    public Collection<PluginInformation> getFailedPlugins() {
189        return failed;
190    }
191
192    /**
193     * Replies the list of plugins whose download has failed
194     *
195     * @return the list of plugins whose download has failed
196     */
197    public Collection<PluginInformation> getDownloadedPlugins() {
198        return downloaded;
199    }
200}