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