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