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