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