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

Revision 5194, 46.1 KB checked in by simon04, 5 weeks ago (diff)

about dialog: set cursor/caret position to 0 in order to scroll to top

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