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.IOException;
009import java.io.InputStream;
010import java.net.MalformedURLException;
011import java.net.URL;
012import java.nio.file.Files;
013import java.nio.file.StandardCopyOption;
014import java.util.Collection;
015import java.util.LinkedList;
016
017import org.openstreetmap.josm.Main;
018import org.openstreetmap.josm.data.Version;
019import org.openstreetmap.josm.gui.ExtendedDialog;
020import org.openstreetmap.josm.gui.PleaseWaitRunnable;
021import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
022import org.openstreetmap.josm.gui.progress.ProgressMonitor;
023import org.openstreetmap.josm.tools.CheckParameterUtil;
024import org.openstreetmap.josm.tools.HttpClient;
025import org.xml.sax.SAXException;
026
027/**
028 * Asynchronous task for downloading a collection of plugins.
029 *
030 * When the task is finished {@link #getDownloadedPlugins()} replies the list of downloaded plugins
031 * and {@link #getFailedPlugins()} replies the list of failed plugins.
032 * @since 2817
033 */
034public class PluginDownloadTask extends PleaseWaitRunnable {
035
036    /**
037     * The accepted MIME types sent in the HTTP Accept header.
038     * @since 6867
039     */
040    public static final String PLUGIN_MIME_TYPES = "application/java-archive, application/zip; q=0.9, application/octet-stream; q=0.5";
041
042    private final Collection<PluginInformation> toUpdate = new LinkedList<>();
043    private final Collection<PluginInformation> failed = new LinkedList<>();
044    private final Collection<PluginInformation> downloaded = new LinkedList<>();
045    private Exception lastException;
046    private boolean canceled;
047    private HttpClient downloadConnection;
048
049    /**
050     * Creates the download task
051     *
052     * @param parent the parent component relative to which the {@link org.openstreetmap.josm.gui.PleaseWaitDialog} is displayed
053     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
054     * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog}
055     * @throws IllegalArgumentException if toUpdate is null
056     */
057    public PluginDownloadTask(Component parent, Collection<PluginInformation> toUpdate, String title) {
058        super(parent, title == null ? "" : title, false /* don't ignore exceptions */);
059        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
060        this.toUpdate.addAll(toUpdate);
061    }
062
063    /**
064     * Creates the task
065     *
066     * @param monitor a progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null
067     * @param toUpdate a collection of plugin descriptions for plugins to update/download. Must not be null.
068     * @param title the title to display in the {@link org.openstreetmap.josm.gui.PleaseWaitDialog}
069     * @throws IllegalArgumentException if toUpdate is null
070     */
071    public PluginDownloadTask(ProgressMonitor monitor, Collection<PluginInformation> toUpdate, String title) {
072        super(title, monitor == null ? NullProgressMonitor.INSTANCE : monitor, false /* don't ignore exceptions */);
073        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
074        this.toUpdate.addAll(toUpdate);
075    }
076
077    /**
078     * Sets the collection of plugins to update.
079     *
080     * @param toUpdate the collection of plugins to update. Must not be null.
081     * @throws IllegalArgumentException if toUpdate is null
082     */
083    public void setPluginsToDownload(Collection<PluginInformation> toUpdate) {
084        CheckParameterUtil.ensureParameterNotNull(toUpdate, "toUpdate");
085        this.toUpdate.clear();
086        this.toUpdate.addAll(toUpdate);
087    }
088
089    @Override
090    protected void cancel() {
091        this.canceled = true;
092        synchronized (this) {
093            if (downloadConnection != null) {
094                downloadConnection.disconnect();
095            }
096        }
097    }
098
099    @Override
100    protected void finish() {
101        // Do nothing. Error/success feedback is managed in PluginPreference.notifyDownloadResults()
102    }
103
104    protected void download(PluginInformation pi, File file) throws PluginDownloadException {
105        if (pi.mainversion > Version.getInstance().getVersion()) {
106            ExtendedDialog dialog = new ExtendedDialog(
107                    progressMonitor.getWindowParent(),
108                    tr("Skip download"),
109                    new String[] {
110                        tr("Download Plugin"),
111                        tr("Skip Download") }
112            );
113            dialog.setContent(tr("JOSM version {0} required for plugin {1}.", pi.mainversion, pi.name));
114            dialog.setButtonIcons(new String[] {"download", "cancel"});
115            dialog.showDialog();
116            int answer = dialog.getValue();
117            if (answer != 1)
118                throw new PluginDownloadException(tr("Download skipped"));
119        }
120        try {
121            if (pi.downloadlink == null) {
122                String msg = tr("Cannot download plugin ''{0}''. Its download link is not known. Skipping download.", pi.name);
123                Main.warn(msg);
124                throw new PluginDownloadException(msg);
125            }
126            URL url = new URL(pi.downloadlink);
127            synchronized (this) {
128                downloadConnection = HttpClient.create(url)
129                        .setAccept(PLUGIN_MIME_TYPES);
130                downloadConnection.connect();
131            }
132            try (InputStream in = downloadConnection.getResponse().getContent()) {
133                Files.copy(in, file.toPath(), StandardCopyOption.REPLACE_EXISTING);
134            }
135        } catch (MalformedURLException e) {
136            String msg = tr("Cannot download plugin ''{0}''. Its download link ''{1}'' is not a valid URL. Skipping download.",
137                    pi.name, pi.downloadlink);
138            Main.warn(msg);
139            throw new PluginDownloadException(msg, e);
140        } catch (IOException e) {
141            if (canceled)
142                return;
143            throw new PluginDownloadException(e);
144        } finally {
145            synchronized (this) {
146                downloadConnection = null;
147            }
148        }
149    }
150
151    @Override
152    protected void realRun() throws SAXException, IOException {
153        File pluginDir = Main.pref.getPluginsDirectory();
154        if (!pluginDir.exists() && !pluginDir.mkdirs()) {
155            String message = tr("Failed to create plugin directory ''{0}''", pluginDir.toString());
156            lastException = new PluginDownloadException(message);
157            Main.error(message);
158            failed.addAll(toUpdate);
159            return;
160        }
161        getProgressMonitor().setTicksCount(toUpdate.size());
162        for (PluginInformation d : toUpdate) {
163            if (canceled)
164                return;
165            String message = tr("Downloading Plugin {0}...", d.name);
166            Main.info(message);
167            progressMonitor.subTask(message);
168            progressMonitor.worked(1);
169            File pluginFile = new File(pluginDir, d.name + ".jar.new");
170            try {
171                download(d, pluginFile);
172            } catch (PluginDownloadException e) {
173                lastException = e;
174                Main.error(e);
175                failed.add(d);
176                continue;
177            }
178            downloaded.add(d);
179        }
180        PluginHandler.installDownloadedPlugins(false);
181    }
182
183    /**
184     * Replies true if the task was canceled by the user
185     *
186     * @return <code>true</code> if the task was stopped by the user
187     */
188    public boolean isCanceled() {
189        return canceled;
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> getFailedPlugins() {
198        return failed;
199    }
200
201    /**
202     * Replies the list of successfully downloaded plugins.
203     *
204     * @return the list of successfully downloaded plugins
205     */
206    public Collection<PluginInformation> getDownloadedPlugins() {
207        return downloaded;
208    }
209
210    /**
211     * Replies the last exception that occured during download, or {@code null}.
212     * @return the last exception that occured during download, or {@code null}
213     * @since 9621
214     */
215    public Exception getLastException() {
216        return lastException;
217    }
218}