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