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