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