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