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