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

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

Robustness in "plugin.requires" parsing

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