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    }
1087
1088    /**
1089     * Determines if the specified file is a valid and accessible JAR file.
1090     * @param jar The fil to check
1091     * @return true if file can be opened as a JAR file.
1092     * @since 5723
1093     */
1094    public static boolean isValidJar(File jar) {
1095        if (jar != null && jar.exists() && jar.canRead()) {
1096            try {
1097                new JarFile(jar).close();
1098            } catch (Exception e) {
1099                return false;
1100            }
1101            return true;
1102        }
1103        return false;
1104    }
1105
1106    /**
1107     * Replies the updated jar file for the given plugin name.
1108     * @param name The plugin name to find.
1109     * @return the updated jar file for the given plugin name. null if not found or not readable.
1110     * @since 5601
1111     */
1112    public static File findUpdatedJar(String name) {
1113        File pluginDir = Main.pref.getPluginsDirectory();
1114        // Find the downloaded file. We have tried to install the downloaded plugins
1115        // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1116        File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1117        if (!isValidJar(downloadedPluginFile)) {
1118            downloadedPluginFile = new File(pluginDir, name + ".jar");
1119            if (!isValidJar(downloadedPluginFile)) {
1120                return null;
1121            }
1122        }
1123        return downloadedPluginFile;
1124    }
1125
1126    /**
1127     * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1128     * @param updatedPlugins The PluginInformation objects to update.
1129     * @since 5601
1130     */
1131    public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1132        if (updatedPlugins == null) return;
1133        for (PluginInformation pi : updatedPlugins) {
1134            File downloadedPluginFile = findUpdatedJar(pi.name);
1135            if (downloadedPluginFile == null) {
1136                continue;
1137            }
1138            try {
1139                pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1140            } catch(PluginException e) {
1141                Main.error(e);
1142            }
1143        }
1144    }
1145
1146    private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1147        final ButtonSpec[] options = new ButtonSpec[] {
1148                new ButtonSpec(
1149                        tr("Update plugin"),
1150                        ImageProvider.get("dialogs", "refresh"),
1151                        tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1152                        null /* no specific help context */
1153                ),
1154                new ButtonSpec(
1155                        tr("Disable plugin"),
1156                        ImageProvider.get("dialogs", "delete"),
1157                        tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1158                        null /* no specific help context */
1159                ),
1160                new ButtonSpec(
1161                        tr("Keep plugin"),
1162                        ImageProvider.get("cancel"),
1163                        tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name),
1164                        null /* no specific help context */
1165                )
1166        };
1167
1168        final StringBuilder msg = new StringBuilder();
1169        msg.append("<html>");
1170        msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name));
1171        msg.append("<br>");
1172        if (plugin.getPluginInformation().author != null) {
1173            msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author));
1174            msg.append("<br>");
1175        }
1176        msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."));
1177        msg.append("</html>");
1178
1179        try {
1180            FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
1181                @Override
1182                public Integer call() {
1183                    return HelpAwareOptionPane.showOptionDialog(
1184                            Main.parent,
1185                            msg.toString(),
1186                            tr("Update plugins"),
1187                            JOptionPane.QUESTION_MESSAGE,
1188                            null,
1189                            options,
1190                            options[0],
1191                            ht("/ErrorMessages#ErrorInPlugin")
1192                    );
1193                }
1194            });
1195            GuiHelper.runInEDT(task);
1196            return task.get();
1197        } catch (InterruptedException | ExecutionException e) {
1198            Main.warn(e);
1199        }
1200        return -1;
1201    }
1202
1203    /**
1204     * Replies the plugin which most likely threw the exception <code>ex</code>.
1205     *
1206     * @param ex the exception
1207     * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1208     */
1209    private static PluginProxy getPluginCausingException(Throwable ex) {
1210        PluginProxy err = null;
1211        StackTraceElement[] stack = ex.getStackTrace();
1212        /* remember the error position, as multiple plugins may be involved,
1213           we search the topmost one */
1214        int pos = stack.length;
1215        for (PluginProxy p : pluginList) {
1216            String baseClass = p.getPluginInformation().className;
1217            baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1218            for (int elpos = 0; elpos < pos; ++elpos) {
1219                if (stack[elpos].getClassName().startsWith(baseClass)) {
1220                    pos = elpos;
1221                    err = p;
1222                }
1223            }
1224        }
1225        return err;
1226    }
1227
1228    /**
1229     * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1230     * conditionally updates or deactivates the plugin, but asks the user first.
1231     *
1232     * @param e the exception
1233     * @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
1234     */
1235    public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1236        PluginProxy plugin = null;
1237        // Check for an explicit problem when calling a plugin function
1238        if (e instanceof PluginException) {
1239            plugin = ((PluginException) e).plugin;
1240        }
1241        if (plugin == null) {
1242            plugin = getPluginCausingException(e);
1243        }
1244        if (plugin == null)
1245            // don't know what plugin threw the exception
1246            return null;
1247
1248        Set<String> plugins = new HashSet<>(
1249                Main.pref.getCollection("plugins",Collections.<String> emptySet())
1250        );
1251        final PluginInformation pluginInfo = plugin.getPluginInformation();
1252        if (! plugins.contains(pluginInfo.name))
1253            // plugin not activated ? strange in this context but anyway, don't bother
1254            // the user with dialogs, skip conditional deactivation
1255            return null;
1256
1257        switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1258        case 0:
1259            // update the plugin
1260            updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1261            return pluginDownloadTask;
1262        case 1:
1263            // deactivate the plugin
1264            plugins.remove(plugin.getPluginInformation().name);
1265            Main.pref.putCollection("plugins", plugins);
1266            GuiHelper.runInEDTAndWait(new Runnable() {
1267                @Override
1268                public void run() {
1269                    JOptionPane.showMessageDialog(
1270                            Main.parent,
1271                            tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1272                            tr("Information"),
1273                            JOptionPane.INFORMATION_MESSAGE
1274                    );
1275                }
1276            });
1277            return null;
1278        default:
1279            // user doesn't want to deactivate the plugin
1280            return null;
1281        }
1282    }
1283
1284    /**
1285     * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1286     * @return The list of loaded plugins (one plugin per line)
1287     */
1288    public static String getBugReportText() {
1289        StringBuilder text = new StringBuilder();
1290        LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
1291        for (final PluginProxy pp : pluginList) {
1292            PluginInformation pi = pp.getPluginInformation();
1293            pl.remove(pi.name);
1294            pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1295                    ? pi.localversion : "unknown") + ")");
1296        }
1297        Collections.sort(pl);
1298        if (!pl.isEmpty()) {
1299            text.append("Plugins:\n");
1300        }
1301        for (String s : pl) {
1302            text.append("- ").append(s).append("\n");
1303        }
1304        return text.toString();
1305    }
1306
1307    /**
1308     * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1309     * @return The list of loaded plugins (one "line" of Swing components per plugin)
1310     */
1311    public static JPanel getInfoPanel() {
1312        JPanel pluginTab = new JPanel(new GridBagLayout());
1313        for (final PluginProxy p : pluginList) {
1314            final PluginInformation info = p.getPluginInformation();
1315            String name = info.name
1316            + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1317            pluginTab.add(new JLabel(name), GBC.std());
1318            pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1319            pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1320                @Override
1321                public void actionPerformed(ActionEvent event) {
1322                    StringBuilder b = new StringBuilder();
1323                    for (Entry<String, String> e : info.attr.entrySet()) {
1324                        b.append(e.getKey());
1325                        b.append(": ");
1326                        b.append(e.getValue());
1327                        b.append("\n");
1328                    }
1329                    JosmTextArea a = new JosmTextArea(10, 40);
1330                    a.setEditable(false);
1331                    a.setText(b.toString());
1332                    a.setCaretPosition(0);
1333                    JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1334                            JOptionPane.INFORMATION_MESSAGE);
1335                }
1336            }), GBC.eol());
1337
1338            JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available")
1339                    : info.description));
1340            description.setEditable(false);
1341            description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1342            description.setLineWrap(true);
1343            description.setWrapStyleWord(true);
1344            description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1345            description.setBackground(UIManager.getColor("Panel.background"));
1346            description.setCaretPosition(0);
1347
1348            pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1349        }
1350        return pluginTab;
1351    }
1352
1353    private static class UpdatePluginsMessagePanel extends JPanel {
1354        private JMultilineLabel lblMessage;
1355        private JCheckBox cbDontShowAgain;
1356
1357        protected final void build() {
1358            setLayout(new GridBagLayout());
1359            GridBagConstraints gc = new GridBagConstraints();
1360            gc.anchor = GridBagConstraints.NORTHWEST;
1361            gc.fill = GridBagConstraints.BOTH;
1362            gc.weightx = 1.0;
1363            gc.weighty = 1.0;
1364            gc.insets = new Insets(5,5,5,5);
1365            add(lblMessage = new JMultilineLabel(""), gc);
1366            lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1367
1368            gc.gridy = 1;
1369            gc.fill = GridBagConstraints.HORIZONTAL;
1370            gc.weighty = 0.0;
1371            add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc);
1372            cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1373        }
1374
1375        public UpdatePluginsMessagePanel() {
1376            build();
1377        }
1378
1379        public void setMessage(String message) {
1380            lblMessage.setText(message);
1381        }
1382
1383        public void initDontShowAgain(String preferencesKey) {
1384            String policy = Main.pref.get(preferencesKey, "ask");
1385            policy = policy.trim().toLowerCase();
1386            cbDontShowAgain.setSelected(!"ask".equals(policy));
1387        }
1388
1389        public boolean isRememberDecision() {
1390            return cbDontShowAgain.isSelected();
1391        }
1392    }
1393}