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