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