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

Last change on this file since 5266 was 5266, checked in by bastiK, 12 years ago

fixed majority of javadoc warnings by replacing "{@see" by "{@link"

  • Property svn:eol-style set to native
File size: 46.1 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 /* 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 {@link 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 {@link PluginInformation#early}
567 * set to true.
568 *
569 * @param plugins the collection of plugins
570 * @param monitor the progress monitor. Defaults to {@link 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 {@link PluginInformation#early}
584 * set to false.
585 *
586 * @param plugins the collection of plugins
587 * @param monitor the progress monitor. Defaults to {@link 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 {@link 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 {@link 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 {@link 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.