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