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