source: josm/trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java @ 6340

Last change on this file since 6340 was 6340, checked in by Don-vip, 5 years ago

refactor of some GUI/widgets classes (impacts some plugins):

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