001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
005import static org.openstreetmap.josm.tools.I18n.tr;
006import static org.openstreetmap.josm.tools.I18n.trn;
007
008import java.awt.Component;
009import java.awt.Font;
010import java.awt.GridBagConstraints;
011import java.awt.GridBagLayout;
012import java.awt.Insets;
013import java.awt.event.ActionEvent;
014import java.io.File;
015import java.io.FilenameFilter;
016import java.net.URL;
017import java.net.URLClassLoader;
018import java.security.AccessController;
019import java.security.PrivilegedAction;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.Collection;
023import java.util.Collections;
024import java.util.Comparator;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Iterator;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031import java.util.Map.Entry;
032import java.util.Set;
033import java.util.TreeSet;
034import java.util.concurrent.Callable;
035import java.util.concurrent.ExecutionException;
036import java.util.concurrent.ExecutorService;
037import java.util.concurrent.Executors;
038import java.util.concurrent.Future;
039import java.util.concurrent.FutureTask;
040import java.util.jar.JarFile;
041
042import javax.swing.AbstractAction;
043import javax.swing.BorderFactory;
044import javax.swing.Box;
045import javax.swing.JButton;
046import javax.swing.JCheckBox;
047import javax.swing.JLabel;
048import javax.swing.JOptionPane;
049import javax.swing.JPanel;
050import javax.swing.JScrollPane;
051import javax.swing.UIManager;
052
053import org.openstreetmap.josm.Main;
054import org.openstreetmap.josm.data.Version;
055import org.openstreetmap.josm.gui.HelpAwareOptionPane;
056import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
057import org.openstreetmap.josm.gui.download.DownloadSelection;
058import org.openstreetmap.josm.gui.help.HelpUtil;
059import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
060import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
061import org.openstreetmap.josm.gui.progress.ProgressMonitor;
062import org.openstreetmap.josm.gui.util.GuiHelper;
063import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
064import org.openstreetmap.josm.gui.widgets.JosmTextArea;
065import org.openstreetmap.josm.io.OfflineAccessException;
066import org.openstreetmap.josm.io.OnlineResource;
067import org.openstreetmap.josm.tools.GBC;
068import org.openstreetmap.josm.tools.I18n;
069import org.openstreetmap.josm.tools.ImageProvider;
070import org.openstreetmap.josm.tools.Utils;
071
072/**
073 * PluginHandler is basically a collection of static utility functions used to bootstrap
074 * and manage the loaded plugins.
075 * @since 1326
076 */
077public final class PluginHandler {
078
079    /**
080     * Deprecated plugins that are removed on start
081     */
082    public static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS;
083    static {
084        String IN_CORE = tr("integrated into main program");
085
086        DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] {
087            new DeprecatedPlugin("mappaint", IN_CORE),
088            new DeprecatedPlugin("unglueplugin", IN_CORE),
089            new DeprecatedPlugin("lang-de", IN_CORE),
090            new DeprecatedPlugin("lang-en_GB", IN_CORE),
091            new DeprecatedPlugin("lang-fr", IN_CORE),
092            new DeprecatedPlugin("lang-it", IN_CORE),
093            new DeprecatedPlugin("lang-pl", IN_CORE),
094            new DeprecatedPlugin("lang-ro", IN_CORE),
095            new DeprecatedPlugin("lang-ru", IN_CORE),
096            new DeprecatedPlugin("ewmsplugin", IN_CORE),
097            new DeprecatedPlugin("ywms", IN_CORE),
098            new DeprecatedPlugin("tways-0.2", IN_CORE),
099            new DeprecatedPlugin("geotagged", IN_CORE),
100            new DeprecatedPlugin("landsat", tr("replaced by new {0} plugin","lakewalker")),
101            new DeprecatedPlugin("namefinder", IN_CORE),
102            new DeprecatedPlugin("waypoints", IN_CORE),
103            new DeprecatedPlugin("slippy_map_chooser", IN_CORE),
104            new DeprecatedPlugin("tcx-support", tr("replaced by new {0} plugin","dataimport")),
105            new DeprecatedPlugin("usertools", IN_CORE),
106            new DeprecatedPlugin("AgPifoJ", IN_CORE),
107            new DeprecatedPlugin("utilsplugin", IN_CORE),
108            new DeprecatedPlugin("ghost", IN_CORE),
109            new DeprecatedPlugin("validator", IN_CORE),
110            new DeprecatedPlugin("multipoly", IN_CORE),
111            new DeprecatedPlugin("multipoly-convert", IN_CORE),
112            new DeprecatedPlugin("remotecontrol", IN_CORE),
113            new DeprecatedPlugin("imagery", IN_CORE),
114            new DeprecatedPlugin("slippymap", IN_CORE),
115            new DeprecatedPlugin("wmsplugin", IN_CORE),
116            new DeprecatedPlugin("ParallelWay", IN_CORE),
117            new DeprecatedPlugin("dumbutils", tr("replaced by new {0} plugin","utilsplugin2")),
118            new DeprecatedPlugin("ImproveWayAccuracy", IN_CORE),
119            new DeprecatedPlugin("Curves", tr("replaced by new {0} plugin","utilsplugin2")),
120            new DeprecatedPlugin("epsg31287", tr("replaced by new {0} plugin", "proj4j")),
121            new DeprecatedPlugin("licensechange", tr("no longer required")),
122            new DeprecatedPlugin("restart", IN_CORE),
123            new DeprecatedPlugin("wayselector", IN_CORE),
124            new DeprecatedPlugin("openstreetbugs", tr("replaced by new {0} plugin", "notes")),
125            new DeprecatedPlugin("nearclick", tr("no longer required")),
126        });
127    }
128
129    private PluginHandler() {
130        // Hide default constructor for utils classes
131    }
132
133    /**
134     * Description of a deprecated plugin
135     */
136    public static class DeprecatedPlugin implements Comparable<DeprecatedPlugin> {
137        /** Plugin name */
138        public final String name;
139        /** Short explanation about deprecation, can be {@code null} */
140        public final String reason;
141        /** Code to run to perform migration, can be {@code null} */
142        private final Runnable migration;
143
144        /**
145         * Constructs a new {@code DeprecatedPlugin}.
146         * @param name The plugin name
147         */
148        public DeprecatedPlugin(String name) {
149            this(name, null, null);
150        }
151
152        /**
153         * Constructs a new {@code DeprecatedPlugin} with a given reason.
154         * @param name The plugin name
155         * @param reason The reason about deprecation
156         */
157        public DeprecatedPlugin(String name, String reason) {
158            this(name, reason, null);
159        }
160
161        /**
162         * Constructs a new {@code DeprecatedPlugin}.
163         * @param name The plugin name
164         * @param reason The reason about deprecation
165         * @param migration The code to run to perform migration
166         */
167        public DeprecatedPlugin(String name, String reason, Runnable migration) {
168            this.name = name;
169            this.reason = reason;
170            this.migration = migration;
171        }
172
173        /**
174         * Performs migration.
175         */
176        public void migrate() {
177            if (migration != null) {
178                migration.run();
179            }
180        }
181
182        @Override
183        public int compareTo(DeprecatedPlugin o) {
184            return name.compareTo(o.name);
185        }
186    }
187
188    /**
189     * List of unmaintained plugins. Not really up-to-date as the vast majority of plugins are not really maintained after a few months, sadly...
190     */
191    private static final String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"};
192
193    /**
194     * Default time-based update interval, in days (pluginmanager.time-based-update.interval)
195     */
196    public static final int DEFAULT_TIME_BASED_UPDATE_INTERVAL = 30;
197
198    /**
199     * All installed and loaded plugins (resp. their main classes)
200     */
201    public static final Collection<PluginProxy> pluginList = new LinkedList<>();
202
203    /**
204     * Add here all ClassLoader whose resource should be searched.
205     */
206    private static final List<ClassLoader> sources = new LinkedList<>();
207
208    static {
209        try {
210            sources.add(ClassLoader.getSystemClassLoader());
211            sources.add(org.openstreetmap.josm.gui.MainApplication.class.getClassLoader());
212        } catch (SecurityException ex) {
213            sources.add(ImageProvider.class.getClassLoader());
214        }
215    }
216
217    private static PluginDownloadTask pluginDownloadTask = null;
218
219    public static Collection<ClassLoader> getResourceClassLoaders() {
220        return Collections.unmodifiableCollection(sources);
221    }
222
223    /**
224     * Removes deprecated plugins from a collection of plugins. Modifies the
225     * collection <code>plugins</code>.
226     *
227     * Also notifies the user about removed deprecated plugins
228     *
229     * @param parent The parent Component used to display warning popup
230     * @param plugins the collection of plugins
231     */
232    private static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) {
233        Set<DeprecatedPlugin> removedPlugins = new TreeSet<>();
234        for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) {
235            if (plugins.contains(depr.name)) {
236                plugins.remove(depr.name);
237                Main.pref.removeFromCollection("plugins", depr.name);
238                removedPlugins.add(depr);
239                depr.migrate();
240            }
241        }
242        if (removedPlugins.isEmpty())
243            return;
244
245        // notify user about removed deprecated plugins
246        //
247        StringBuilder sb = new StringBuilder();
248        sb.append("<html>");
249        sb.append(trn(
250                "The following plugin is no longer necessary and has been deactivated:",
251                "The following plugins are no longer necessary and have been deactivated:",
252                removedPlugins.size()
253        ));
254        sb.append("<ul>");
255        for (DeprecatedPlugin depr: removedPlugins) {
256            sb.append("<li>").append(depr.name);
257            if (depr.reason != null) {
258                sb.append(" (").append(depr.reason).append(")");
259            }
260            sb.append("</li>");
261        }
262        sb.append("</ul>");
263        sb.append("</html>");
264        JOptionPane.showMessageDialog(
265                parent,
266                sb.toString(),
267                tr("Warning"),
268                JOptionPane.WARNING_MESSAGE
269        );
270    }
271
272    /**
273     * Removes unmaintained plugins from a collection of plugins. Modifies the
274     * collection <code>plugins</code>. Also removes the plugin from the list
275     * of plugins in the preferences, if necessary.
276     *
277     * Asks the user for every unmaintained plugin whether it should be removed.
278     *
279     * @param plugins the collection of plugins
280     */
281    private static void filterUnmaintainedPlugins(Component parent, Collection<String> plugins) {
282        for (String unmaintained : UNMAINTAINED_PLUGINS) {
283            if (!plugins.contains(unmaintained)) {
284                continue;
285            }
286            String msg =  tr("<html>Loading of the plugin \"{0}\" was requested."
287                    + "<br>This plugin is no longer developed and very likely will produce errors."
288                    +"<br>It should be disabled.<br>Delete from preferences?</html>", unmaintained);
289            if (confirmDisablePlugin(parent, msg,unmaintained)) {
290                Main.pref.removeFromCollection("plugins", unmaintained);
291                plugins.remove(unmaintained);
292            }
293        }
294    }
295
296    /**
297     * Checks whether the locally available plugins should be updated and
298     * asks the user if running an update is OK. An update is advised if
299     * JOSM was updated to a new version since the last plugin updates or
300     * if the plugins were last updated a long time ago.
301     *
302     * @param parent the parent component relative to which the confirmation dialog
303     * is to be displayed
304     * @return true if a plugin update should be run; false, otherwise
305     */
306    public static boolean checkAndConfirmPluginUpdate(Component parent) {
307        if (!checkOfflineAccess()) {
308            Main.info(tr("{0} not available (offline mode)", tr("Plugin update")));
309            return false;
310        }
311        String message = null;
312        String togglePreferenceKey = null;
313        int v = Version.getInstance().getVersion();
314        if (Main.pref.getInteger("pluginmanager.version", 0) < v) {
315            message =
316                "<html>"
317                + tr("You updated your JOSM software.<br>"
318                        + "To prevent problems the plugins should be updated as well.<br><br>"
319                        + "Update plugins now?"
320                )
321                + "</html>";
322            togglePreferenceKey = "pluginmanager.version-based-update.policy";
323        }  else {
324            long tim = System.currentTimeMillis();
325            long last = Main.pref.getLong("pluginmanager.lastupdate", 0);
326            Integer maxTime = Main.pref.getInteger("pluginmanager.time-based-update.interval", DEFAULT_TIME_BASED_UPDATE_INTERVAL);
327            long d = (tim - last) / (24 * 60 * 60 * 1000L);
328            if ((last <= 0) || (maxTime <= 0)) {
329                Main.pref.put("pluginmanager.lastupdate", Long.toString(tim));
330            } else if (d > maxTime) {
331                message =
332                    "<html>"
333                    + tr("Last plugin update more than {0} days ago.", d)
334                    + "</html>";
335                togglePreferenceKey = "pluginmanager.time-based-update.policy";
336            }
337        }
338        if (message == null) return false;
339
340        ButtonSpec [] options = new ButtonSpec[] {
341                new ButtonSpec(
342                        tr("Update plugins"),
343                        ImageProvider.get("dialogs", "refresh"),
344                        tr("Click to update the activated plugins"),
345                        null /* no specific help context */
346                ),
347                new ButtonSpec(
348                        tr("Skip update"),
349                        ImageProvider.get("cancel"),
350                        tr("Click to skip updating the activated plugins"),
351                        null /* no specific help context */
352                )
353        };
354
355        UpdatePluginsMessagePanel pnlMessage = new UpdatePluginsMessagePanel();
356        pnlMessage.setMessage(message);
357        pnlMessage.initDontShowAgain(togglePreferenceKey);
358
359        // check whether automatic update at startup was disabled
360        //
361        String policy = Main.pref.get(togglePreferenceKey, "ask").trim().toLowerCase();
362        switch(policy) {
363        case "never":
364            if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
365                Main.info(tr("Skipping plugin update after JOSM upgrade. Automatic update at startup is disabled."));
366            } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
367                Main.info(tr("Skipping plugin update after elapsed update interval. Automatic update at startup is disabled."));
368            }
369            return false;
370
371        case "always":
372            if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
373                Main.info(tr("Running plugin update after JOSM upgrade. Automatic update at startup is enabled."));
374            } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
375                Main.info(tr("Running plugin update after elapsed update interval. Automatic update at startup is disabled."));
376            }
377            return true;
378
379        case "ask":
380            break;
381
382        default:
383            Main.warn(tr("Unexpected value ''{0}'' for preference ''{1}''. Assuming value ''ask''.", policy, togglePreferenceKey));
384        }
385
386        int ret = HelpAwareOptionPane.showOptionDialog(
387                parent,
388                pnlMessage,
389                tr("Update plugins"),
390                JOptionPane.WARNING_MESSAGE,
391                null,
392                options,
393                options[0],
394                ht("/Preferences/Plugins#AutomaticUpdate")
395        );
396
397        if (pnlMessage.isRememberDecision()) {
398            switch(ret) {
399            case 0:
400                Main.pref.put(togglePreferenceKey, "always");
401                break;
402            case JOptionPane.CLOSED_OPTION:
403            case 1:
404                Main.pref.put(togglePreferenceKey, "never");
405                break;
406            }
407        } else {
408            Main.pref.put(togglePreferenceKey, "ask");
409        }
410        return ret == 0;
411    }
412
413    private static boolean checkOfflineAccess() {
414        if (Main.isOffline(OnlineResource.ALL)) {
415            return false;
416        }
417        if (Main.isOffline(OnlineResource.JOSM_WEBSITE)) {
418            for (String updateSite : Main.pref.getPluginSites()) {
419                try {
420                    OnlineResource.JOSM_WEBSITE.checkOfflineAccess(updateSite, Main.getJOSMWebsite());
421                } catch (OfflineAccessException e) {
422                    if (Main.isTraceEnabled()) {
423                        Main.trace(e.getMessage());
424                    }
425                    return false;
426                }
427            }
428        }
429        return true;
430    }
431
432    /**
433     * Alerts the user if a plugin required by another plugin is missing
434     *
435     * @param parent The parent Component used to display error popup
436     * @param plugin the plugin
437     * @param missingRequiredPlugin the missing required plugin
438     */
439    private static void alertMissingRequiredPlugin(Component parent, String plugin, Set<String> missingRequiredPlugin) {
440        StringBuilder sb = new StringBuilder();
441        sb.append("<html>");
442        sb.append(trn("Plugin {0} requires a plugin which was not found. The missing plugin is:",
443                "Plugin {0} requires {1} plugins which were not found. The missing plugins are:",
444                missingRequiredPlugin.size(),
445                plugin,
446                missingRequiredPlugin.size()
447        ));
448        sb.append(Utils.joinAsHtmlUnorderedList(missingRequiredPlugin));
449        sb.append("</html>");
450        JOptionPane.showMessageDialog(
451                parent,
452                sb.toString(),
453                tr("Error"),
454                JOptionPane.ERROR_MESSAGE
455        );
456    }
457
458    private static void alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) {
459        HelpAwareOptionPane.showOptionDialog(
460                parent,
461                tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>"
462                        +"You have to update JOSM in order to use this plugin.</html>",
463                        plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString()
464                ),
465                tr("Warning"),
466                JOptionPane.WARNING_MESSAGE,
467                HelpUtil.ht("/Plugin/Loading#JOSMUpdateRequired")
468        );
469    }
470
471    /**
472     * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
473     * current JOSM version must be compatible with the plugin and no other plugins this plugin
474     * depends on should be missing.
475     *
476     * @param parent The parent Component used to display error popup
477     * @param plugins the collection of all loaded plugins
478     * @param plugin the plugin for which preconditions are checked
479     * @return true, if the preconditions are met; false otherwise
480     */
481    public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) {
482
483        // make sure the plugin is compatible with the current JOSM version
484        //
485        int josmVersion = Version.getInstance().getVersion();
486        if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
487            alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion);
488            return false;
489        }
490
491        // Add all plugins already loaded (to include early plugins when checking late ones)
492        Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
493        for (PluginProxy proxy : pluginList) {
494            allPlugins.add(proxy.getPluginInformation());
495        }
496
497        return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true);
498    }
499
500    /**
501     * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met.
502     * No other plugins this plugin depends on should be missing.
503     *
504     * @param parent The parent Component used to display error popup
505     * @param plugins the collection of all loaded plugins
506     * @param plugin the plugin for which preconditions are checked
507     * @param local Determines if the local or up-to-date plugin dependencies are to be checked.
508     * @return true, if the preconditions are met; false otherwise
509     * @since 5601
510     */
511    public static boolean checkRequiredPluginsPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin, boolean local) {
512
513        String requires = local ? plugin.localrequires : plugin.requires;
514
515        // make sure the dependencies to other plugins are not broken
516        //
517        if (requires != null) {
518            Set<String> pluginNames = new HashSet<>();
519            for (PluginInformation pi: plugins) {
520                pluginNames.add(pi.name);
521            }
522            Set<String> missingPlugins = new HashSet<>();
523            List<String> requiredPlugins = local ? plugin.getLocalRequiredPlugins() : plugin.getRequiredPlugins();
524            for (String requiredPlugin : requiredPlugins) {
525                if (!pluginNames.contains(requiredPlugin)) {
526                    missingPlugins.add(requiredPlugin);
527                }
528            }
529            if (!missingPlugins.isEmpty()) {
530                alertMissingRequiredPlugin(parent, plugin.name, missingPlugins);
531                return false;
532            }
533        }
534        return true;
535    }
536
537    /**
538     * Creates a class loader for loading plugin code.
539     *
540     * @param plugins the collection of plugins which are going to be loaded with this
541     * class loader
542     * @return the class loader
543     */
544    public static ClassLoader createClassLoader(Collection<PluginInformation> plugins) {
545        // iterate all plugins and collect all libraries of all plugins:
546        List<URL> allPluginLibraries = new LinkedList<>();
547        File pluginDir = Main.pref.getPluginsDirectory();
548
549        // Add all plugins already loaded (to include early plugins in the classloader, allowing late plugins to rely on early ones)
550        Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
551        for (PluginProxy proxy : pluginList) {
552            allPlugins.add(proxy.getPluginInformation());
553        }
554
555        for (PluginInformation info : allPlugins) {
556            if (info.libraries == null) {
557                continue;
558            }
559            allPluginLibraries.addAll(info.libraries);
560            File pluginJar = new File(pluginDir, info.name + ".jar");
561            I18n.addTexts(pluginJar);
562            URL pluginJarUrl = Utils.fileToURL(pluginJar);
563            allPluginLibraries.add(pluginJarUrl);
564        }
565
566        // create a classloader for all plugins:
567        final URL[] jarUrls = allPluginLibraries.toArray(new URL[allPluginLibraries.size()]);
568        return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
569            public ClassLoader run() {
570                return new URLClassLoader(jarUrls, Main.class.getClassLoader());
571            }
572      });
573    }
574
575    /**
576     * Loads and instantiates the plugin described by <code>plugin</code> using
577     * the class loader <code>pluginClassLoader</code>.
578     *
579     * @param parent The parent component to be used for the displayed dialog
580     * @param plugin the plugin
581     * @param pluginClassLoader the plugin class loader
582     */
583    public static void loadPlugin(Component parent, PluginInformation plugin, ClassLoader pluginClassLoader) {
584        String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name);
585        try {
586            Class<?> klass = plugin.loadClass(pluginClassLoader);
587            if (klass != null) {
588                Main.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion));
589                PluginProxy pluginProxy = plugin.load(klass);
590                pluginList.add(pluginProxy);
591                Main.addMapFrameListener(pluginProxy);
592            }
593            msg = null;
594        } catch (PluginException e) {
595            Main.error(e);
596            if (e.getCause() instanceof ClassNotFoundException) {
597                msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>"
598                        + "Delete from preferences?</html>", plugin.name, plugin.className);
599            }
600        }  catch (Exception e) {
601            Main.error(e);
602        }
603        if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) {
604            Main.pref.removeFromCollection("plugins", plugin.name);
605        }
606    }
607
608    /**
609     * Loads the plugin in <code>plugins</code> from locally available jar files into
610     * memory.
611     *
612     * @param parent The parent component to be used for the displayed dialog
613     * @param plugins the list of plugins
614     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
615     */
616    public static void loadPlugins(Component parent,Collection<PluginInformation> plugins, ProgressMonitor monitor) {
617        if (monitor == null) {
618            monitor = NullProgressMonitor.INSTANCE;
619        }
620        try {
621            monitor.beginTask(tr("Loading plugins ..."));
622            monitor.subTask(tr("Checking plugin preconditions..."));
623            List<PluginInformation> toLoad = new LinkedList<>();
624            for (PluginInformation pi: plugins) {
625                if (checkLoadPreconditions(parent, plugins, pi)) {
626                    toLoad.add(pi);
627                }
628            }
629            // sort the plugins according to their "staging" equivalence class. The
630            // lower the value of "stage" the earlier the plugin should be loaded.
631            //
632            Collections.sort(
633                    toLoad,
634                    new Comparator<PluginInformation>() {
635                        @Override
636                        public int compare(PluginInformation o1, PluginInformation o2) {
637                            if (o1.stage < o2.stage) return -1;
638                            if (o1.stage == o2.stage) return 0;
639                            return 1;
640                        }
641                    }
642            );
643            if (toLoad.isEmpty())
644                return;
645
646            ClassLoader pluginClassLoader = createClassLoader(toLoad);
647            sources.add(0, pluginClassLoader);
648            monitor.setTicksCount(toLoad.size());
649            for (PluginInformation info : toLoad) {
650                monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
651                loadPlugin(parent, info, pluginClassLoader);
652                monitor.worked(1);
653            }
654        } finally {
655            monitor.finishTask();
656        }
657    }
658
659    /**
660     * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
661     * set to true.
662     *
663     * @param plugins the collection of plugins
664     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
665     */
666    public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
667        List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size());
668        for (PluginInformation pi: plugins) {
669            if (pi.early) {
670                earlyPlugins.add(pi);
671            }
672        }
673        loadPlugins(parent, earlyPlugins, monitor);
674    }
675
676    /**
677     * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
678     * set to false.
679     *
680     * @param parent The parent component to be used for the displayed dialog
681     * @param plugins the collection of plugins
682     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
683     */
684    public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
685        List<PluginInformation> latePlugins = new ArrayList<>(plugins.size());
686        for (PluginInformation pi: plugins) {
687            if (!pi.early) {
688                latePlugins.add(pi);
689            }
690        }
691        loadPlugins(parent, latePlugins, monitor);
692    }
693
694    /**
695     * Loads locally available plugin information from local plugin jars and from cached
696     * plugin lists.
697     *
698     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
699     * @return the list of locally available plugin information
700     *
701     */
702    private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
703        if (monitor == null) {
704            monitor = NullProgressMonitor.INSTANCE;
705        }
706        try {
707            ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
708            ExecutorService service = Executors.newSingleThreadExecutor();
709            Future<?> future = service.submit(task);
710            try {
711                future.get();
712            } catch(ExecutionException e) {
713                Main.error(e);
714                return null;
715            } catch(InterruptedException e) {
716                Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while loading locally available plugin information");
717                return null;
718            }
719            HashMap<String, PluginInformation> ret = new HashMap<>();
720            for (PluginInformation pi: task.getAvailablePlugins()) {
721                ret.put(pi.name, pi);
722            }
723            return ret;
724        } finally {
725            monitor.finishTask();
726        }
727    }
728
729    private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) {
730        StringBuilder sb = new StringBuilder();
731        sb.append("<html>");
732        sb.append(trn("JOSM could not find information about the following plugin:",
733                "JOSM could not find information about the following plugins:",
734                plugins.size()));
735        sb.append(Utils.joinAsHtmlUnorderedList(plugins));
736        sb.append(trn("The plugin is not going to be loaded.",
737                "The plugins are not going to be loaded.",
738                plugins.size()));
739        sb.append("</html>");
740        HelpAwareOptionPane.showOptionDialog(
741                parent,
742                sb.toString(),
743                tr("Warning"),
744                JOptionPane.WARNING_MESSAGE,
745                HelpUtil.ht("/Plugin/Loading#MissingPluginInfos")
746        );
747    }
748
749    /**
750     * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
751     * out. This involves user interaction. This method displays alert and confirmation
752     * messages.
753     *
754     * @param parent The parent component to be used for the displayed dialog
755     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
756     * @return the set of plugins to load (as set of plugin names)
757     */
758    public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
759        if (monitor == null) {
760            monitor = NullProgressMonitor.INSTANCE;
761        }
762        try {
763            monitor.beginTask(tr("Determine plugins to load..."));
764            Set<String> plugins = new HashSet<>();
765            plugins.addAll(Main.pref.getCollection("plugins",  new LinkedList<String>()));
766            if (System.getProperty("josm.plugins") != null) {
767                plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
768            }
769            monitor.subTask(tr("Removing deprecated plugins..."));
770            filterDeprecatedPlugins(parent, plugins);
771            monitor.subTask(tr("Removing unmaintained plugins..."));
772            filterUnmaintainedPlugins(parent, plugins);
773            Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1,false));
774            List<PluginInformation> ret = new LinkedList<>();
775            for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
776                String plugin = it.next();
777                if (infos.containsKey(plugin)) {
778                    ret.add(infos.get(plugin));
779                    it.remove();
780                }
781            }
782            if (!plugins.isEmpty()) {
783                alertMissingPluginInformation(parent, plugins);
784            }
785            return ret;
786        } finally {
787            monitor.finishTask();
788        }
789    }
790
791    private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
792        StringBuilder sb = new StringBuilder();
793        sb.append("<html>");
794        sb.append(trn(
795                "Updating the following plugin has failed:",
796                "Updating the following plugins has failed:",
797                plugins.size()
798        )
799        );
800        sb.append("<ul>");
801        for (PluginInformation pi: plugins) {
802            sb.append("<li>").append(pi.name).append("</li>");
803        }
804        sb.append("</ul>");
805        sb.append(trn(
806                "Please open the Preference Dialog after JOSM has started and try to update it manually.",
807                "Please open the Preference Dialog after JOSM has started and try to update them manually.",
808                plugins.size()
809        ));
810        sb.append("</html>");
811        HelpAwareOptionPane.showOptionDialog(
812                parent,
813                sb.toString(),
814                tr("Plugin update failed"),
815                JOptionPane.ERROR_MESSAGE,
816                HelpUtil.ht("/Plugin/Loading#FailedPluginUpdated")
817        );
818    }
819
820    private static Set<PluginInformation> findRequiredPluginsToDownload(
821            Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
822        Set<PluginInformation> result = new HashSet<>();
823        for (PluginInformation pi : pluginsToUpdate) {
824            for (String name : pi.getRequiredPlugins()) {
825                try {
826                    PluginInformation installedPlugin = PluginInformation.findPlugin(name);
827                    if (installedPlugin == null) {
828                        // New required plugin is not installed, find its PluginInformation
829                        PluginInformation reqPlugin = null;
830                        for (PluginInformation pi2 : allPlugins) {
831                            if (pi2.getName().equals(name)) {
832                                reqPlugin = pi2;
833                                break;
834                            }
835                        }
836                        // Required plugin is known but not already on download list
837                        if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
838                            result.add(reqPlugin);
839                        }
840                    }
841                } catch (PluginException e) {
842                    Main.warn(tr("Failed to find plugin {0}", name));
843                    Main.error(e);
844                }
845            }
846        }
847        return result;
848    }
849
850    /**
851     * Updates the plugins in <code>plugins</code>.
852     *
853     * @param parent the parent component for message boxes
854     * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
855     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
856     * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
857     * @throws IllegalArgumentException thrown if plugins is null
858     */
859    public static Collection<PluginInformation> updatePlugins(Component parent,
860            Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg)
861            throws IllegalArgumentException {
862        Collection<PluginInformation> plugins = null;
863        pluginDownloadTask = null;
864        if (monitor == null) {
865            monitor = NullProgressMonitor.INSTANCE;
866        }
867        try {
868            monitor.beginTask("");
869            ExecutorService service = Executors.newSingleThreadExecutor();
870
871            // try to download the plugin lists
872            //
873            ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
874                    monitor.createSubTaskMonitor(1,false),
875                    Main.pref.getPluginSites(), displayErrMsg
876            );
877            Future<?> future = service.submit(task1);
878            List<PluginInformation> allPlugins = null;
879
880            try {
881                future.get();
882                allPlugins = task1.getAvailablePlugins();
883                plugins = buildListOfPluginsToLoad(parent,monitor.createSubTaskMonitor(1, false));
884                // If only some plugins have to be updated, filter the list
885                if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
886                    for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
887                        PluginInformation pi = it.next();
888                        boolean found = false;
889                        for (PluginInformation piw : pluginsWanted) {
890                            if (pi.name.equals(piw.name)) {
891                                found = true;
892                                break;
893                            }
894                        }
895                        if (!found) {
896                            it.remove();
897                        }
898                    }
899                }
900            } catch (ExecutionException e) {
901                Main.warn(tr("Failed to download plugin information list")+": ExecutionException");
902                Main.error(e);
903                // don't abort in case of error, continue with downloading plugins below
904            } catch (InterruptedException e) {
905                Main.warn(tr("Failed to download plugin information list")+": InterruptedException");
906                // don't abort in case of error, continue with downloading plugins below
907            }
908
909            // filter plugins which actually have to be updated
910            //
911            Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
912            for (PluginInformation pi: plugins) {
913                if (pi.isUpdateRequired()) {
914                    pluginsToUpdate.add(pi);
915                }
916            }
917
918            if (!pluginsToUpdate.isEmpty()) {
919
920                Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
921
922                if (allPlugins != null) {
923                    // Updated plugins may need additional plugin dependencies currently not installed
924                    //
925                    Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
926                    pluginsToDownload.addAll(additionalPlugins);
927
928                    // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
929                    while (!additionalPlugins.isEmpty()) {
930                        // Install the additional plugins to load them later
931                        plugins.addAll(additionalPlugins);
932                        additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
933                        pluginsToDownload.addAll(additionalPlugins);
934                    }
935                }
936
937                // try to update the locally installed plugins
938                //
939                pluginDownloadTask = new PluginDownloadTask(
940                        monitor.createSubTaskMonitor(1,false),
941                        pluginsToDownload,
942                        tr("Update plugins")
943                );
944
945                future = service.submit(pluginDownloadTask);
946                try {
947                    future.get();
948                } catch(ExecutionException e) {
949                    Main.error(e);
950                    alertFailedPluginUpdate(parent, pluginsToUpdate);
951                    return plugins;
952                } catch(InterruptedException e) {
953                    Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while updating plugins");
954                    alertFailedPluginUpdate(parent, pluginsToUpdate);
955                    return plugins;
956                }
957
958                // Update Plugin info for downloaded plugins
959                //
960                refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
961
962                // notify user if downloading a locally installed plugin failed
963                //
964                if (! pluginDownloadTask.getFailedPlugins().isEmpty()) {
965                    alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
966                    return plugins;
967                }
968            }
969        } finally {
970            monitor.finishTask();
971        }
972        if (pluginsWanted == null) {
973            // if all plugins updated, remember the update because it was successful
974            //
975            Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
976            Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
977        }
978        return plugins;
979    }
980
981    /**
982     * Ask the user for confirmation that a plugin shall be disabled.
983     *
984     * @param parent The parent component to be used for the displayed dialog
985     * @param reason the reason for disabling the plugin
986     * @param name the plugin name
987     * @return true, if the plugin shall be disabled; false, otherwise
988     */
989    public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
990        ButtonSpec [] options = new ButtonSpec[] {
991                new ButtonSpec(
992                        tr("Disable plugin"),
993                        ImageProvider.get("dialogs", "delete"),
994                        tr("Click to delete the plugin ''{0}''", name),
995                        null /* no specific help context */
996                ),
997                new ButtonSpec(
998                        tr("Keep plugin"),
999                        ImageProvider.get("cancel"),
1000                        tr("Click to keep the plugin ''{0}''", name),
1001                        null /* no specific help context */
1002                )
1003        };
1004        int ret = HelpAwareOptionPane.showOptionDialog(
1005                parent,
1006                reason,
1007                tr("Disable plugin"),
1008                JOptionPane.WARNING_MESSAGE,
1009                null,
1010                options,
1011                options[0],
1012                null // FIXME: add help topic
1013        );
1014        return ret == 0;
1015    }
1016
1017    /**
1018     * Returns the plugin of the specified name.
1019     * @param name The plugin name
1020     * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1021     */
1022    public static Object getPlugin(String name) {
1023        for (PluginProxy plugin : pluginList)
1024            if (plugin.getPluginInformation().name.equals(name))
1025                return plugin.plugin;
1026        return null;
1027    }
1028
1029    public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1030        for (PluginProxy p : pluginList) {
1031            p.addDownloadSelection(downloadSelections);
1032        }
1033    }
1034
1035    public static void getPreferenceSetting(Collection<PreferenceSettingFactory> settings) {
1036        for (PluginProxy plugin : pluginList) {
1037            settings.add(new PluginPreferenceFactory(plugin));
1038        }
1039    }
1040
1041    /**
1042     * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1043     * ".jar" files.
1044     *
1045     * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1046     * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1047     * installation of the respective plugin is sillently skipped.
1048     *
1049     * @param dowarn if true, warning messages are displayed; false otherwise
1050     */
1051    public static void installDownloadedPlugins(boolean dowarn) {
1052        File pluginDir = Main.pref.getPluginsDirectory();
1053        if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite())
1054            return;
1055
1056        final File[] files = pluginDir.listFiles(new FilenameFilter() {
1057            @Override
1058            public boolean accept(File dir, String name) {
1059                return name.endsWith(".jar.new");
1060            }});
1061
1062        for (File updatedPlugin : files) {
1063            final String filePath = updatedPlugin.getPath();
1064            File plugin = new File(filePath.substring(0, filePath.length() - 4));
1065            String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1066            if (plugin.exists() && !plugin.delete() && dowarn) {
1067                Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1068                Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1069                continue;
1070            }
1071            try {
1072                // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1073                new JarFile(updatedPlugin).close();
1074            } catch (Exception e) {
1075                if (dowarn) {
1076                    Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}", plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1077                }
1078                continue;
1079            }
1080            // Install plugin
1081            if (!updatedPlugin.renameTo(plugin) && dowarn) {
1082                Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.", plugin.toString(), updatedPlugin.toString()));
1083                Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1084            }
1085        }
1086        return;
1087    }
1088
1089    /**
1090     * Determines if the specified file is a valid and accessible JAR file.
1091     * @param jar The fil to check
1092     * @return true if file can be opened as a JAR file.
1093     * @since 5723
1094     */
1095    public static boolean isValidJar(File jar) {
1096        if (jar != null && jar.exists() && jar.canRead()) {
1097            try {
1098                new JarFile(jar).close();
1099            } catch (Exception e) {
1100                return false;
1101            }
1102            return true;
1103        }
1104        return false;
1105    }
1106
1107    /**
1108     * Replies the updated jar file for the given plugin name.
1109     * @param name The plugin name to find.
1110     * @return the updated jar file for the given plugin name. null if not found or not readable.
1111     * @since 5601
1112     */
1113    public static File findUpdatedJar(String name) {
1114        File pluginDir = Main.pref.getPluginsDirectory();
1115        // Find the downloaded file. We have tried to install the downloaded plugins
1116        // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1117        File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1118        if (!isValidJar(downloadedPluginFile)) {
1119            downloadedPluginFile = new File(pluginDir, name + ".jar");
1120            if (!isValidJar(downloadedPluginFile)) {
1121                return null;
1122            }
1123        }
1124        return downloadedPluginFile;
1125    }
1126
1127    /**
1128     * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1129     * @param updatedPlugins The PluginInformation objects to update.
1130     * @since 5601
1131     */
1132    public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1133        if (updatedPlugins == null) return;
1134        for (PluginInformation pi : updatedPlugins) {
1135            File downloadedPluginFile = findUpdatedJar(pi.name);
1136            if (downloadedPluginFile == null) {
1137                continue;
1138            }
1139            try {
1140                pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1141            } catch(PluginException e) {
1142                Main.error(e);
1143            }
1144        }
1145    }
1146
1147    private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1148        final ButtonSpec[] options = new ButtonSpec[] {
1149                new ButtonSpec(
1150                        tr("Update plugin"),
1151                        ImageProvider.get("dialogs", "refresh"),
1152                        tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1153                        null /* no specific help context */
1154                ),
1155                new ButtonSpec(
1156                        tr("Disable plugin"),
1157                        ImageProvider.get("dialogs", "delete"),
1158                        tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1159                        null /* no specific help context */
1160                ),
1161                new ButtonSpec(
1162                        tr("Keep plugin"),
1163                        ImageProvider.get("cancel"),
1164                        tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name),
1165                        null /* no specific help context */
1166                )
1167        };
1168
1169        final StringBuilder msg = new StringBuilder();
1170        msg.append("<html>");
1171        msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name));
1172        msg.append("<br>");
1173        if (plugin.getPluginInformation().author != null) {
1174            msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author));
1175            msg.append("<br>");
1176        }
1177        msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."));
1178        msg.append("</html>");
1179
1180        try {
1181            FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
1182                @Override
1183                public Integer call() {
1184                    return HelpAwareOptionPane.showOptionDialog(
1185                            Main.parent,
1186                            msg.toString(),
1187                            tr("Update plugins"),
1188                            JOptionPane.QUESTION_MESSAGE,
1189                            null,
1190                            options,
1191                            options[0],
1192                            ht("/ErrorMessages#ErrorInPlugin")
1193                    );
1194                }
1195            });
1196            GuiHelper.runInEDT(task);
1197            return task.get();
1198        } catch (InterruptedException | ExecutionException e) {
1199            Main.warn(e);
1200        }
1201        return -1;
1202    }
1203
1204    /**
1205     * Replies the plugin which most likely threw the exception <code>ex</code>.
1206     *
1207     * @param ex the exception
1208     * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1209     */
1210    private static PluginProxy getPluginCausingException(Throwable ex) {
1211        PluginProxy err = null;
1212        StackTraceElement[] stack = ex.getStackTrace();
1213        /* remember the error position, as multiple plugins may be involved,
1214           we search the topmost one */
1215        int pos = stack.length;
1216        for (PluginProxy p : pluginList) {
1217            String baseClass = p.getPluginInformation().className;
1218            baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1219            for (int elpos = 0; elpos < pos; ++elpos) {
1220                if (stack[elpos].getClassName().startsWith(baseClass)) {
1221                    pos = elpos;
1222                    err = p;
1223                }
1224            }
1225        }
1226        return err;
1227    }
1228
1229    /**
1230     * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1231     * conditionally updates or deactivates the plugin, but asks the user first.
1232     *
1233     * @param e the exception
1234     * @return plugin download task if the plugin has been updated to a newer version, {@code null} if it has been disabled or kept as it
1235     */
1236    public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1237        PluginProxy plugin = null;
1238        // Check for an explicit problem when calling a plugin function
1239        if (e instanceof PluginException) {
1240            plugin = ((PluginException) e).plugin;
1241        }
1242        if (plugin == null) {
1243            plugin = getPluginCausingException(e);
1244        }
1245        if (plugin == null)
1246            // don't know what plugin threw the exception
1247            return null;
1248
1249        Set<String> plugins = new HashSet<>(
1250                Main.pref.getCollection("plugins",Collections.<String> emptySet())
1251        );
1252        final PluginInformation pluginInfo = plugin.getPluginInformation();
1253        if (! plugins.contains(pluginInfo.name))
1254            // plugin not activated ? strange in this context but anyway, don't bother
1255            // the user with dialogs, skip conditional deactivation
1256            return null;
1257
1258        switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1259        case 0:
1260            // update the plugin
1261            updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1262            return pluginDownloadTask;
1263        case 1:
1264            // deactivate the plugin
1265            plugins.remove(plugin.getPluginInformation().name);
1266            Main.pref.putCollection("plugins", plugins);
1267            GuiHelper.runInEDTAndWait(new Runnable() {
1268                @Override
1269                public void run() {
1270                    JOptionPane.showMessageDialog(
1271                            Main.parent,
1272                            tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1273                            tr("Information"),
1274                            JOptionPane.INFORMATION_MESSAGE
1275                    );
1276                }
1277            });
1278            return null;
1279        default:
1280            // user doesn't want to deactivate the plugin
1281            return null;
1282        }
1283    }
1284
1285    /**
1286     * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1287     * @return The list of loaded plugins (one plugin per line)
1288     */
1289    public static String getBugReportText() {
1290        StringBuilder text = new StringBuilder();
1291        LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
1292        for (final PluginProxy pp : pluginList) {
1293            PluginInformation pi = pp.getPluginInformation();
1294            pl.remove(pi.name);
1295            pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1296                    ? pi.localversion : "unknown") + ")");
1297        }
1298        Collections.sort(pl);
1299        if (!pl.isEmpty()) {
1300            text.append("Plugins:\n");
1301        }
1302        for (String s : pl) {
1303            text.append("- ").append(s).append("\n");
1304        }
1305        return text.toString();
1306    }
1307
1308    /**
1309     * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1310     * @return The list of loaded plugins (one "line" of Swing components per plugin)
1311     */
1312    public static JPanel getInfoPanel() {
1313        JPanel pluginTab = new JPanel(new GridBagLayout());
1314        for (final PluginProxy p : pluginList) {
1315            final PluginInformation info = p.getPluginInformation();
1316            String name = info.name
1317            + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1318            pluginTab.add(new JLabel(name), GBC.std());
1319            pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1320            pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1321                @Override
1322                public void actionPerformed(ActionEvent event) {
1323                    StringBuilder b = new StringBuilder();
1324                    for (Entry<String, String> e : info.attr.entrySet()) {
1325                        b.append(e.getKey());
1326                        b.append(": ");
1327                        b.append(e.getValue());
1328                        b.append("\n");
1329                    }
1330                    JosmTextArea a = new JosmTextArea(10, 40);
1331                    a.setEditable(false);
1332                    a.setText(b.toString());
1333                    a.setCaretPosition(0);
1334                    JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1335                            JOptionPane.INFORMATION_MESSAGE);
1336                }
1337            }), GBC.eol());
1338
1339            JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available")
1340                    : info.description));
1341            description.setEditable(false);
1342            description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1343            description.setLineWrap(true);
1344            description.setWrapStyleWord(true);
1345            description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1346            description.setBackground(UIManager.getColor("Panel.background"));
1347            description.setCaretPosition(0);
1348
1349            pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1350        }
1351        return pluginTab;
1352    }
1353
1354    private static class UpdatePluginsMessagePanel extends JPanel {
1355        private JMultilineLabel lblMessage;
1356        private JCheckBox cbDontShowAgain;
1357
1358        protected final void build() {
1359            setLayout(new GridBagLayout());
1360            GridBagConstraints gc = new GridBagConstraints();
1361            gc.anchor = GridBagConstraints.NORTHWEST;
1362            gc.fill = GridBagConstraints.BOTH;
1363            gc.weightx = 1.0;
1364            gc.weighty = 1.0;
1365            gc.insets = new Insets(5,5,5,5);
1366            add(lblMessage = new JMultilineLabel(""), gc);
1367            lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1368
1369            gc.gridy = 1;
1370            gc.fill = GridBagConstraints.HORIZONTAL;
1371            gc.weighty = 0.0;
1372            add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc);
1373            cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1374        }
1375
1376        public UpdatePluginsMessagePanel() {
1377            build();
1378        }
1379
1380        public void setMessage(String message) {
1381            lblMessage.setText(message);
1382        }
1383
1384        public void initDontShowAgain(String preferencesKey) {
1385            String policy = Main.pref.get(preferencesKey, "ask");
1386            policy = policy.trim().toLowerCase();
1387            cbDontShowAgain.setSelected(!"ask".equals(policy));
1388        }
1389
1390        public boolean isRememberDecision() {
1391            return cbDontShowAgain.isSelected();
1392        }
1393    }
1394}