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