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