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