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