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