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