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

Last change on this file since 8283 was 8126, checked in by Don-vip, 9 years ago

fix Sonar issue squid:S2444 - Lazy initialization of "static" fields should be "synchronized"

  • Property svn:eol-style set to native
File size: 58.8 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.security.AccessController;
19import java.security.PrivilegedAction;
20import java.util.ArrayList;
21import java.util.Arrays;
22import java.util.Collection;
23import java.util.Collections;
24import java.util.Comparator;
25import java.util.HashMap;
26import java.util.HashSet;
27import java.util.Iterator;
28import java.util.LinkedList;
29import java.util.List;
30import java.util.Map;
31import java.util.Map.Entry;
32import java.util.Set;
33import java.util.TreeSet;
34import java.util.concurrent.Callable;
35import java.util.concurrent.ExecutionException;
36import java.util.concurrent.ExecutorService;
37import java.util.concurrent.Executors;
38import java.util.concurrent.Future;
39import java.util.concurrent.FutureTask;
40import java.util.jar.JarFile;
41
42import javax.swing.AbstractAction;
43import javax.swing.BorderFactory;
44import javax.swing.Box;
45import javax.swing.JButton;
46import javax.swing.JCheckBox;
47import javax.swing.JLabel;
48import javax.swing.JOptionPane;
49import javax.swing.JPanel;
50import javax.swing.JScrollPane;
51import javax.swing.UIManager;
52
53import org.openstreetmap.josm.Main;
54import org.openstreetmap.josm.data.Version;
55import org.openstreetmap.josm.gui.HelpAwareOptionPane;
56import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
57import org.openstreetmap.josm.gui.download.DownloadSelection;
58import org.openstreetmap.josm.gui.help.HelpUtil;
59import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
60import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
61import org.openstreetmap.josm.gui.progress.ProgressMonitor;
62import org.openstreetmap.josm.gui.util.GuiHelper;
63import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
64import org.openstreetmap.josm.gui.widgets.JosmTextArea;
65import org.openstreetmap.josm.io.OfflineAccessException;
66import org.openstreetmap.josm.io.OnlineResource;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.I18n;
69import org.openstreetmap.josm.tools.ImageProvider;
70import org.openstreetmap.josm.tools.Utils;
71
72/**
73 * PluginHandler is basically a collection of static utility functions used to bootstrap
74 * and manage the loaded plugins.
75 * @since 1326
76 */
77public final class PluginHandler {
78
79 /**
80 * Deprecated plugins that are removed on start
81 */
82 public static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS;
83 static {
84 String IN_CORE = tr("integrated into main program");
85
86 DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] {
87 new DeprecatedPlugin("mappaint", IN_CORE),
88 new DeprecatedPlugin("unglueplugin", IN_CORE),
89 new DeprecatedPlugin("lang-de", IN_CORE),
90 new DeprecatedPlugin("lang-en_GB", IN_CORE),
91 new DeprecatedPlugin("lang-fr", IN_CORE),
92 new DeprecatedPlugin("lang-it", IN_CORE),
93 new DeprecatedPlugin("lang-pl", IN_CORE),
94 new DeprecatedPlugin("lang-ro", IN_CORE),
95 new DeprecatedPlugin("lang-ru", IN_CORE),
96 new DeprecatedPlugin("ewmsplugin", IN_CORE),
97 new DeprecatedPlugin("ywms", IN_CORE),
98 new DeprecatedPlugin("tways-0.2", IN_CORE),
99 new DeprecatedPlugin("geotagged", IN_CORE),
100 new DeprecatedPlugin("landsat", tr("replaced by new {0} plugin","lakewalker")),
101 new DeprecatedPlugin("namefinder", IN_CORE),
102 new DeprecatedPlugin("waypoints", IN_CORE),
103 new DeprecatedPlugin("slippy_map_chooser", IN_CORE),
104 new DeprecatedPlugin("tcx-support", tr("replaced by new {0} plugin","dataimport")),
105 new DeprecatedPlugin("usertools", IN_CORE),
106 new DeprecatedPlugin("AgPifoJ", IN_CORE),
107 new DeprecatedPlugin("utilsplugin", IN_CORE),
108 new DeprecatedPlugin("ghost", IN_CORE),
109 new DeprecatedPlugin("validator", IN_CORE),
110 new DeprecatedPlugin("multipoly", IN_CORE),
111 new DeprecatedPlugin("multipoly-convert", IN_CORE),
112 new DeprecatedPlugin("remotecontrol", IN_CORE),
113 new DeprecatedPlugin("imagery", IN_CORE),
114 new DeprecatedPlugin("slippymap", IN_CORE),
115 new DeprecatedPlugin("wmsplugin", IN_CORE),
116 new DeprecatedPlugin("ParallelWay", IN_CORE),
117 new DeprecatedPlugin("dumbutils", tr("replaced by new {0} plugin","utilsplugin2")),
118 new DeprecatedPlugin("ImproveWayAccuracy", IN_CORE),
119 new DeprecatedPlugin("Curves", tr("replaced by new {0} plugin","utilsplugin2")),
120 new DeprecatedPlugin("epsg31287", tr("replaced by new {0} plugin", "proj4j")),
121 new DeprecatedPlugin("licensechange", tr("no longer required")),
122 new DeprecatedPlugin("restart", IN_CORE),
123 new DeprecatedPlugin("wayselector", IN_CORE),
124 new DeprecatedPlugin("openstreetbugs", tr("replaced by new {0} plugin", "notes")),
125 new DeprecatedPlugin("nearclick", tr("no longer required")),
126 new DeprecatedPlugin("notes", IN_CORE),
127 });
128 }
129
130 private PluginHandler() {
131 // Hide default constructor for utils classes
132 }
133
134 /**
135 * Description of a deprecated plugin
136 */
137 public static class DeprecatedPlugin implements Comparable<DeprecatedPlugin> {
138 /** Plugin name */
139 public final String name;
140 /** Short explanation about deprecation, can be {@code null} */
141 public final String reason;
142 /** Code to run to perform migration, can be {@code null} */
143 private final Runnable migration;
144
145 /**
146 * Constructs a new {@code DeprecatedPlugin}.
147 * @param name The plugin name
148 */
149 public DeprecatedPlugin(String name) {
150 this(name, null, null);
151 }
152
153 /**
154 * Constructs a new {@code DeprecatedPlugin} with a given reason.
155 * @param name The plugin name
156 * @param reason The reason about deprecation
157 */
158 public DeprecatedPlugin(String name, String reason) {
159 this(name, reason, null);
160 }
161
162 /**
163 * Constructs a new {@code DeprecatedPlugin}.
164 * @param name The plugin name
165 * @param reason The reason about deprecation
166 * @param migration The code to run to perform migration
167 */
168 public DeprecatedPlugin(String name, String reason, Runnable migration) {
169 this.name = name;
170 this.reason = reason;
171 this.migration = migration;
172 }
173
174 /**
175 * Performs migration.
176 */
177 public void migrate() {
178 if (migration != null) {
179 migration.run();
180 }
181 }
182
183 @Override
184 public int compareTo(DeprecatedPlugin o) {
185 return name.compareTo(o.name);
186 }
187 }
188
189 /**
190 * ClassLoader that makes the addURL method of URLClassLoader public.
191 *
192 * Like URLClassLoader, but allows to add more URLs after construction.
193 */
194 public static class DynamicURLClassLoader extends URLClassLoader {
195
196 public DynamicURLClassLoader(URL[] urls, ClassLoader parent) {
197 super(urls, parent);
198 }
199
200 @Override
201 public void addURL(URL url) {
202 super.addURL(url);
203 }
204 }
205
206 /**
207 * List of unmaintained plugins. Not really up-to-date as the vast majority of plugins are not maintained after a few months, sadly...
208 */
209 private static final String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"};
210
211 /**
212 * Default time-based update interval, in days (pluginmanager.time-based-update.interval)
213 */
214 public static final int DEFAULT_TIME_BASED_UPDATE_INTERVAL = 30;
215
216 /**
217 * All installed and loaded plugins (resp. their main classes)
218 */
219 public static final Collection<PluginProxy> pluginList = new LinkedList<>();
220
221 /**
222 * Global plugin ClassLoader.
223 */
224 private static DynamicURLClassLoader pluginClassLoader;
225
226 /**
227 * Add here all ClassLoader whose resource should be searched.
228 */
229 private static final List<ClassLoader> sources = new LinkedList<>();
230
231 static {
232 try {
233 sources.add(ClassLoader.getSystemClassLoader());
234 sources.add(org.openstreetmap.josm.gui.MainApplication.class.getClassLoader());
235 } catch (SecurityException ex) {
236 sources.add(ImageProvider.class.getClassLoader());
237 }
238 }
239
240 private static PluginDownloadTask pluginDownloadTask = null;
241
242 public static Collection<ClassLoader> getResourceClassLoaders() {
243 return Collections.unmodifiableCollection(sources);
244 }
245
246 /**
247 * Removes deprecated plugins from a collection of plugins. Modifies the
248 * collection <code>plugins</code>.
249 *
250 * Also notifies the user about removed deprecated plugins
251 *
252 * @param parent The parent Component used to display warning popup
253 * @param plugins the collection of plugins
254 */
255 private static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) {
256 Set<DeprecatedPlugin> removedPlugins = new TreeSet<>();
257 for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) {
258 if (plugins.contains(depr.name)) {
259 plugins.remove(depr.name);
260 Main.pref.removeFromCollection("plugins", depr.name);
261 removedPlugins.add(depr);
262 depr.migrate();
263 }
264 }
265 if (removedPlugins.isEmpty())
266 return;
267
268 // notify user about removed deprecated plugins
269 //
270 StringBuilder sb = new StringBuilder();
271 sb.append("<html>");
272 sb.append(trn(
273 "The following plugin is no longer necessary and has been deactivated:",
274 "The following plugins are no longer necessary and have been deactivated:",
275 removedPlugins.size()
276 ));
277 sb.append("<ul>");
278 for (DeprecatedPlugin depr: removedPlugins) {
279 sb.append("<li>").append(depr.name);
280 if (depr.reason != null) {
281 sb.append(" (").append(depr.reason).append(")");
282 }
283 sb.append("</li>");
284 }
285 sb.append("</ul>");
286 sb.append("</html>");
287 JOptionPane.showMessageDialog(
288 parent,
289 sb.toString(),
290 tr("Warning"),
291 JOptionPane.WARNING_MESSAGE
292 );
293 }
294
295 /**
296 * Removes unmaintained plugins from a collection of plugins. Modifies the
297 * collection <code>plugins</code>. Also removes the plugin from the list
298 * of plugins in the preferences, if necessary.
299 *
300 * Asks the user for every unmaintained plugin whether it should be removed.
301 *
302 * @param plugins the collection of plugins
303 */
304 private static void filterUnmaintainedPlugins(Component parent, Collection<String> plugins) {
305 for (String unmaintained : UNMAINTAINED_PLUGINS) {
306 if (!plugins.contains(unmaintained)) {
307 continue;
308 }
309 String msg = tr("<html>Loading of the plugin \"{0}\" was requested."
310 + "<br>This plugin is no longer developed and very likely will produce errors."
311 +"<br>It should be disabled.<br>Delete from preferences?</html>", unmaintained);
312 if (confirmDisablePlugin(parent, msg,unmaintained)) {
313 Main.pref.removeFromCollection("plugins", unmaintained);
314 plugins.remove(unmaintained);
315 }
316 }
317 }
318
319 /**
320 * Checks whether the locally available plugins should be updated and
321 * asks the user if running an update is OK. An update is advised if
322 * JOSM was updated to a new version since the last plugin updates or
323 * if the plugins were last updated a long time ago.
324 *
325 * @param parent the parent component relative to which the confirmation dialog
326 * is to be displayed
327 * @return true if a plugin update should be run; false, otherwise
328 */
329 public static boolean checkAndConfirmPluginUpdate(Component parent) {
330 if (!checkOfflineAccess()) {
331 Main.info(tr("{0} not available (offline mode)", tr("Plugin update")));
332 return false;
333 }
334 String message = null;
335 String togglePreferenceKey = null;
336 int v = Version.getInstance().getVersion();
337 if (Main.pref.getInteger("pluginmanager.version", 0) < v) {
338 message =
339 "<html>"
340 + tr("You updated your JOSM software.<br>"
341 + "To prevent problems the plugins should be updated as well.<br><br>"
342 + "Update plugins now?"
343 )
344 + "</html>";
345 togglePreferenceKey = "pluginmanager.version-based-update.policy";
346 } else {
347 long tim = System.currentTimeMillis();
348 long last = Main.pref.getLong("pluginmanager.lastupdate", 0);
349 Integer maxTime = Main.pref.getInteger("pluginmanager.time-based-update.interval", DEFAULT_TIME_BASED_UPDATE_INTERVAL);
350 long d = (tim - last) / (24 * 60 * 60 * 1000L);
351 if ((last <= 0) || (maxTime <= 0)) {
352 Main.pref.put("pluginmanager.lastupdate", Long.toString(tim));
353 } else if (d > maxTime) {
354 message =
355 "<html>"
356 + tr("Last plugin update more than {0} days ago.", d)
357 + "</html>";
358 togglePreferenceKey = "pluginmanager.time-based-update.policy";
359 }
360 }
361 if (message == null) return false;
362
363 ButtonSpec [] options = new ButtonSpec[] {
364 new ButtonSpec(
365 tr("Update plugins"),
366 ImageProvider.get("dialogs", "refresh"),
367 tr("Click to update the activated plugins"),
368 null /* no specific help context */
369 ),
370 new ButtonSpec(
371 tr("Skip update"),
372 ImageProvider.get("cancel"),
373 tr("Click to skip updating the activated plugins"),
374 null /* no specific help context */
375 )
376 };
377
378 UpdatePluginsMessagePanel pnlMessage = new UpdatePluginsMessagePanel();
379 pnlMessage.setMessage(message);
380 pnlMessage.initDontShowAgain(togglePreferenceKey);
381
382 // check whether automatic update at startup was disabled
383 //
384 String policy = Main.pref.get(togglePreferenceKey, "ask").trim().toLowerCase();
385 switch(policy) {
386 case "never":
387 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
388 Main.info(tr("Skipping plugin update after JOSM upgrade. Automatic update at startup is disabled."));
389 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
390 Main.info(tr("Skipping plugin update after elapsed update interval. Automatic update at startup is disabled."));
391 }
392 return false;
393
394 case "always":
395 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
396 Main.info(tr("Running plugin update after JOSM upgrade. Automatic update at startup is enabled."));
397 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
398 Main.info(tr("Running plugin update after elapsed update interval. Automatic update at startup is disabled."));
399 }
400 return true;
401
402 case "ask":
403 break;
404
405 default:
406 Main.warn(tr("Unexpected value ''{0}'' for preference ''{1}''. Assuming value ''ask''.", policy, togglePreferenceKey));
407 }
408
409 int ret = HelpAwareOptionPane.showOptionDialog(
410 parent,
411 pnlMessage,
412 tr("Update plugins"),
413 JOptionPane.WARNING_MESSAGE,
414 null,
415 options,
416 options[0],
417 ht("/Preferences/Plugins#AutomaticUpdate")
418 );
419
420 if (pnlMessage.isRememberDecision()) {
421 switch(ret) {
422 case 0:
423 Main.pref.put(togglePreferenceKey, "always");
424 break;
425 case JOptionPane.CLOSED_OPTION:
426 case 1:
427 Main.pref.put(togglePreferenceKey, "never");
428 break;
429 }
430 } else {
431 Main.pref.put(togglePreferenceKey, "ask");
432 }
433 return ret == 0;
434 }
435
436 private static boolean checkOfflineAccess() {
437 if (Main.isOffline(OnlineResource.ALL)) {
438 return false;
439 }
440 if (Main.isOffline(OnlineResource.JOSM_WEBSITE)) {
441 for (String updateSite : Main.pref.getPluginSites()) {
442 try {
443 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(updateSite, Main.getJOSMWebsite());
444 } catch (OfflineAccessException e) {
445 if (Main.isTraceEnabled()) {
446 Main.trace(e.getMessage());
447 }
448 return false;
449 }
450 }
451 }
452 return true;
453 }
454
455 /**
456 * Alerts the user if a plugin required by another plugin is missing
457 *
458 * @param parent The parent Component used to display error popup
459 * @param plugin the plugin
460 * @param missingRequiredPlugin the missing required plugin
461 */
462 private static void alertMissingRequiredPlugin(Component parent, String plugin, Set<String> missingRequiredPlugin) {
463 StringBuilder sb = new StringBuilder();
464 sb.append("<html>");
465 sb.append(trn("Plugin {0} requires a plugin which was not found. The missing plugin is:",
466 "Plugin {0} requires {1} plugins which were not found. The missing plugins are:",
467 missingRequiredPlugin.size(),
468 plugin,
469 missingRequiredPlugin.size()
470 ));
471 sb.append(Utils.joinAsHtmlUnorderedList(missingRequiredPlugin));
472 sb.append("</html>");
473 JOptionPane.showMessageDialog(
474 parent,
475 sb.toString(),
476 tr("Error"),
477 JOptionPane.ERROR_MESSAGE
478 );
479 }
480
481 private static void alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) {
482 HelpAwareOptionPane.showOptionDialog(
483 parent,
484 tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>"
485 +"You have to update JOSM in order to use this plugin.</html>",
486 plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString()
487 ),
488 tr("Warning"),
489 JOptionPane.WARNING_MESSAGE,
490 HelpUtil.ht("/Plugin/Loading#JOSMUpdateRequired")
491 );
492 }
493
494 /**
495 * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
496 * current JOSM version must be compatible with the plugin and no other plugins this plugin
497 * depends on should be missing.
498 *
499 * @param parent The parent Component used to display error popup
500 * @param plugins the collection of all loaded plugins
501 * @param plugin the plugin for which preconditions are checked
502 * @return true, if the preconditions are met; false otherwise
503 */
504 public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) {
505
506 // make sure the plugin is compatible with the current JOSM version
507 //
508 int josmVersion = Version.getInstance().getVersion();
509 if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
510 alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion);
511 return false;
512 }
513
514 // Add all plugins already loaded (to include early plugins when checking late ones)
515 Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
516 for (PluginProxy proxy : pluginList) {
517 allPlugins.add(proxy.getPluginInformation());
518 }
519
520 return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true);
521 }
522
523 /**
524 * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met.
525 * No other plugins this plugin depends on should be missing.
526 *
527 * @param parent The parent Component used to display error popup. If parent is
528 * null, the error popup is suppressed
529 * @param plugins the collection of all loaded plugins
530 * @param plugin the plugin for which preconditions are checked
531 * @param local Determines if the local or up-to-date plugin dependencies are to be checked.
532 * @return true, if the preconditions are met; false otherwise
533 * @since 5601
534 */
535 public static boolean checkRequiredPluginsPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin, boolean local) {
536
537 String requires = local ? plugin.localrequires : plugin.requires;
538
539 // make sure the dependencies to other plugins are not broken
540 //
541 if (requires != null) {
542 Set<String> pluginNames = new HashSet<>();
543 for (PluginInformation pi: plugins) {
544 pluginNames.add(pi.name);
545 }
546 Set<String> missingPlugins = new HashSet<>();
547 List<String> requiredPlugins = local ? plugin.getLocalRequiredPlugins() : plugin.getRequiredPlugins();
548 for (String requiredPlugin : requiredPlugins) {
549 if (!pluginNames.contains(requiredPlugin)) {
550 missingPlugins.add(requiredPlugin);
551 }
552 }
553 if (!missingPlugins.isEmpty()) {
554 if (parent != null) {
555 alertMissingRequiredPlugin(parent, plugin.name, missingPlugins);
556 }
557 return false;
558 }
559 }
560 return true;
561 }
562
563 /**
564 * Get the class loader for loading plugin code.
565 *
566 * @return the class loader
567 */
568 public static synchronized DynamicURLClassLoader getPluginClassLoader() {
569 if (pluginClassLoader == null) {
570 pluginClassLoader = AccessController.doPrivileged(new PrivilegedAction<DynamicURLClassLoader>() {
571 public DynamicURLClassLoader run() {
572 return new DynamicURLClassLoader(new URL[0], Main.class.getClassLoader());
573 }
574 });
575 sources.add(0, pluginClassLoader);
576 }
577 return pluginClassLoader;
578 }
579
580 /**
581 * Add more plugins to the plugin class loader.
582 *
583 * @param plugins the plugins that should be handled by the plugin class loader
584 */
585 public static void extendPluginClassLoader(Collection<PluginInformation> plugins) {
586 // iterate all plugins and collect all libraries of all plugins:
587 File pluginDir = Main.pref.getPluginsDirectory();
588 DynamicURLClassLoader cl = getPluginClassLoader();
589
590 for (PluginInformation info : plugins) {
591 if (info.libraries == null) {
592 continue;
593 }
594 for (URL libUrl : info.libraries) {
595 cl.addURL(libUrl);
596 }
597 File pluginJar = new File(pluginDir, info.name + ".jar");
598 I18n.addTexts(pluginJar);
599 URL pluginJarUrl = Utils.fileToURL(pluginJar);
600 cl.addURL(pluginJarUrl);
601 }
602 }
603
604 /**
605 * Loads and instantiates the plugin described by <code>plugin</code> using
606 * the class loader <code>pluginClassLoader</code>.
607 *
608 * @param parent The parent component to be used for the displayed dialog
609 * @param plugin the plugin
610 * @param pluginClassLoader the plugin class loader
611 */
612 public static void loadPlugin(Component parent, PluginInformation plugin, ClassLoader pluginClassLoader) {
613 String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name);
614 try {
615 Class<?> klass = plugin.loadClass(pluginClassLoader);
616 if (klass != null) {
617 Main.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion));
618 PluginProxy pluginProxy = plugin.load(klass);
619 pluginList.add(pluginProxy);
620 Main.addMapFrameListener(pluginProxy, true);
621 }
622 msg = null;
623 } catch (PluginException e) {
624 Main.error(e);
625 if (e.getCause() instanceof ClassNotFoundException) {
626 msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>"
627 + "Delete from preferences?</html>", plugin.name, plugin.className);
628 }
629 } catch (Exception e) {
630 Main.error(e);
631 }
632 if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) {
633 Main.pref.removeFromCollection("plugins", plugin.name);
634 }
635 }
636
637 /**
638 * Loads the plugin in <code>plugins</code> from locally available jar files into
639 * memory.
640 *
641 * @param parent The parent component to be used for the displayed dialog
642 * @param plugins the list of plugins
643 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
644 */
645 public static void loadPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
646 if (monitor == null) {
647 monitor = NullProgressMonitor.INSTANCE;
648 }
649 try {
650 monitor.beginTask(tr("Loading plugins ..."));
651 monitor.subTask(tr("Checking plugin preconditions..."));
652 List<PluginInformation> toLoad = new LinkedList<>();
653 for (PluginInformation pi: plugins) {
654 if (checkLoadPreconditions(parent, plugins, pi)) {
655 toLoad.add(pi);
656 }
657 }
658 // sort the plugins according to their "staging" equivalence class. The
659 // lower the value of "stage" the earlier the plugin should be loaded.
660 //
661 Collections.sort(
662 toLoad,
663 new Comparator<PluginInformation>() {
664 @Override
665 public int compare(PluginInformation o1, PluginInformation o2) {
666 if (o1.stage < o2.stage) return -1;
667 if (o1.stage == o2.stage) return 0;
668 return 1;
669 }
670 }
671 );
672 if (toLoad.isEmpty())
673 return;
674
675 extendPluginClassLoader(toLoad);
676 monitor.setTicksCount(toLoad.size());
677 for (PluginInformation info : toLoad) {
678 monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
679 loadPlugin(parent, info, getPluginClassLoader());
680 monitor.worked(1);
681 }
682 } finally {
683 monitor.finishTask();
684 }
685 }
686
687 /**
688 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
689 * set to true.
690 *
691 * @param plugins the collection of plugins
692 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
693 */
694 public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
695 List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size());
696 for (PluginInformation pi: plugins) {
697 if (pi.early) {
698 earlyPlugins.add(pi);
699 }
700 }
701 loadPlugins(parent, earlyPlugins, monitor);
702 }
703
704 /**
705 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
706 * set to false.
707 *
708 * @param parent The parent component to be used for the displayed dialog
709 * @param plugins the collection of plugins
710 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
711 */
712 public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
713 List<PluginInformation> latePlugins = new ArrayList<>(plugins.size());
714 for (PluginInformation pi: plugins) {
715 if (!pi.early) {
716 latePlugins.add(pi);
717 }
718 }
719 loadPlugins(parent, latePlugins, monitor);
720 }
721
722 /**
723 * Loads locally available plugin information from local plugin jars and from cached
724 * plugin lists.
725 *
726 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
727 * @return the list of locally available plugin information
728 *
729 */
730 private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
731 if (monitor == null) {
732 monitor = NullProgressMonitor.INSTANCE;
733 }
734 try {
735 ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
736 ExecutorService service = Executors.newSingleThreadExecutor();
737 Future<?> future = service.submit(task);
738 try {
739 future.get();
740 } catch(ExecutionException e) {
741 Main.error(e);
742 return null;
743 } catch(InterruptedException e) {
744 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while loading locally available plugin information");
745 return null;
746 }
747 HashMap<String, PluginInformation> ret = new HashMap<>();
748 for (PluginInformation pi: task.getAvailablePlugins()) {
749 ret.put(pi.name, pi);
750 }
751 return ret;
752 } finally {
753 monitor.finishTask();
754 }
755 }
756
757 private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) {
758 StringBuilder sb = new StringBuilder();
759 sb.append("<html>");
760 sb.append(trn("JOSM could not find information about the following plugin:",
761 "JOSM could not find information about the following plugins:",
762 plugins.size()));
763 sb.append(Utils.joinAsHtmlUnorderedList(plugins));
764 sb.append(trn("The plugin is not going to be loaded.",
765 "The plugins are not going to be loaded.",
766 plugins.size()));
767 sb.append("</html>");
768 HelpAwareOptionPane.showOptionDialog(
769 parent,
770 sb.toString(),
771 tr("Warning"),
772 JOptionPane.WARNING_MESSAGE,
773 HelpUtil.ht("/Plugin/Loading#MissingPluginInfos")
774 );
775 }
776
777 /**
778 * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
779 * out. This involves user interaction. This method displays alert and confirmation
780 * messages.
781 *
782 * @param parent The parent component to be used for the displayed dialog
783 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
784 * @return the set of plugins to load (as set of plugin names)
785 */
786 public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
787 if (monitor == null) {
788 monitor = NullProgressMonitor.INSTANCE;
789 }
790 try {
791 monitor.beginTask(tr("Determine plugins to load..."));
792 Set<String> plugins = new HashSet<>();
793 plugins.addAll(Main.pref.getCollection("plugins", new LinkedList<String>()));
794 if (System.getProperty("josm.plugins") != null) {
795 plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
796 }
797 monitor.subTask(tr("Removing deprecated plugins..."));
798 filterDeprecatedPlugins(parent, plugins);
799 monitor.subTask(tr("Removing unmaintained plugins..."));
800 filterUnmaintainedPlugins(parent, plugins);
801 Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1,false));
802 List<PluginInformation> ret = new LinkedList<>();
803 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
804 String plugin = it.next();
805 if (infos.containsKey(plugin)) {
806 ret.add(infos.get(plugin));
807 it.remove();
808 }
809 }
810 if (!plugins.isEmpty()) {
811 alertMissingPluginInformation(parent, plugins);
812 }
813 return ret;
814 } finally {
815 monitor.finishTask();
816 }
817 }
818
819 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
820 StringBuilder sb = new StringBuilder();
821 sb.append("<html>");
822 sb.append(trn(
823 "Updating the following plugin has failed:",
824 "Updating the following plugins has failed:",
825 plugins.size()
826 )
827 );
828 sb.append("<ul>");
829 for (PluginInformation pi: plugins) {
830 sb.append("<li>").append(pi.name).append("</li>");
831 }
832 sb.append("</ul>");
833 sb.append(trn(
834 "Please open the Preference Dialog after JOSM has started and try to update it manually.",
835 "Please open the Preference Dialog after JOSM has started and try to update them manually.",
836 plugins.size()
837 ));
838 sb.append("</html>");
839 HelpAwareOptionPane.showOptionDialog(
840 parent,
841 sb.toString(),
842 tr("Plugin update failed"),
843 JOptionPane.ERROR_MESSAGE,
844 HelpUtil.ht("/Plugin/Loading#FailedPluginUpdated")
845 );
846 }
847
848 private static Set<PluginInformation> findRequiredPluginsToDownload(
849 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
850 Set<PluginInformation> result = new HashSet<>();
851 for (PluginInformation pi : pluginsToUpdate) {
852 for (String name : pi.getRequiredPlugins()) {
853 try {
854 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
855 if (installedPlugin == null) {
856 // New required plugin is not installed, find its PluginInformation
857 PluginInformation reqPlugin = null;
858 for (PluginInformation pi2 : allPlugins) {
859 if (pi2.getName().equals(name)) {
860 reqPlugin = pi2;
861 break;
862 }
863 }
864 // Required plugin is known but not already on download list
865 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
866 result.add(reqPlugin);
867 }
868 }
869 } catch (PluginException e) {
870 Main.warn(tr("Failed to find plugin {0}", name));
871 Main.error(e);
872 }
873 }
874 }
875 return result;
876 }
877
878 /**
879 * Updates the plugins in <code>plugins</code>.
880 *
881 * @param parent the parent component for message boxes
882 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
883 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
884 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
885 * @throws IllegalArgumentException thrown if plugins is null
886 */
887 public static Collection<PluginInformation> updatePlugins(Component parent,
888 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg)
889 throws IllegalArgumentException {
890 Collection<PluginInformation> plugins = null;
891 pluginDownloadTask = null;
892 if (monitor == null) {
893 monitor = NullProgressMonitor.INSTANCE;
894 }
895 try {
896 monitor.beginTask("");
897 ExecutorService service = Executors.newSingleThreadExecutor();
898
899 // try to download the plugin lists
900 //
901 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
902 monitor.createSubTaskMonitor(1, false),
903 Main.pref.getPluginSites(), displayErrMsg
904 );
905 Future<?> future = service.submit(task1);
906 List<PluginInformation> allPlugins = null;
907
908 try {
909 future.get();
910 allPlugins = task1.getAvailablePlugins();
911 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
912 // If only some plugins have to be updated, filter the list
913 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
914 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
915 PluginInformation pi = it.next();
916 boolean found = false;
917 for (PluginInformation piw : pluginsWanted) {
918 if (pi.name.equals(piw.name)) {
919 found = true;
920 break;
921 }
922 }
923 if (!found) {
924 it.remove();
925 }
926 }
927 }
928 } catch (ExecutionException e) {
929 Main.warn(tr("Failed to download plugin information list")+": ExecutionException");
930 Main.error(e);
931 // don't abort in case of error, continue with downloading plugins below
932 } catch (InterruptedException e) {
933 Main.warn(tr("Failed to download plugin information list")+": InterruptedException");
934 // don't abort in case of error, continue with downloading plugins below
935 }
936
937 // filter plugins which actually have to be updated
938 //
939 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
940 for (PluginInformation pi: plugins) {
941 if (pi.isUpdateRequired()) {
942 pluginsToUpdate.add(pi);
943 }
944 }
945
946 if (!pluginsToUpdate.isEmpty()) {
947
948 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
949
950 if (allPlugins != null) {
951 // Updated plugins may need additional plugin dependencies currently not installed
952 //
953 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
954 pluginsToDownload.addAll(additionalPlugins);
955
956 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
957 while (!additionalPlugins.isEmpty()) {
958 // Install the additional plugins to load them later
959 plugins.addAll(additionalPlugins);
960 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
961 pluginsToDownload.addAll(additionalPlugins);
962 }
963 }
964
965 // try to update the locally installed plugins
966 //
967 pluginDownloadTask = new PluginDownloadTask(
968 monitor.createSubTaskMonitor(1,false),
969 pluginsToDownload,
970 tr("Update plugins")
971 );
972
973 future = service.submit(pluginDownloadTask);
974 try {
975 future.get();
976 } catch(ExecutionException e) {
977 Main.error(e);
978 alertFailedPluginUpdate(parent, pluginsToUpdate);
979 return plugins;
980 } catch(InterruptedException e) {
981 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while updating plugins");
982 alertFailedPluginUpdate(parent, pluginsToUpdate);
983 return plugins;
984 }
985
986 // Update Plugin info for downloaded plugins
987 //
988 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
989
990 // notify user if downloading a locally installed plugin failed
991 //
992 if (! pluginDownloadTask.getFailedPlugins().isEmpty()) {
993 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
994 return plugins;
995 }
996 }
997 } finally {
998 monitor.finishTask();
999 }
1000 if (pluginsWanted == null) {
1001 // if all plugins updated, remember the update because it was successful
1002 //
1003 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1004 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1005 }
1006 return plugins;
1007 }
1008
1009 /**
1010 * Ask the user for confirmation that a plugin shall be disabled.
1011 *
1012 * @param parent The parent component to be used for the displayed dialog
1013 * @param reason the reason for disabling the plugin
1014 * @param name the plugin name
1015 * @return true, if the plugin shall be disabled; false, otherwise
1016 */
1017 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1018 ButtonSpec [] options = new ButtonSpec[] {
1019 new ButtonSpec(
1020 tr("Disable plugin"),
1021 ImageProvider.get("dialogs", "delete"),
1022 tr("Click to delete the plugin ''{0}''", name),
1023 null /* no specific help context */
1024 ),
1025 new ButtonSpec(
1026 tr("Keep plugin"),
1027 ImageProvider.get("cancel"),
1028 tr("Click to keep the plugin ''{0}''", name),
1029 null /* no specific help context */
1030 )
1031 };
1032 int ret = HelpAwareOptionPane.showOptionDialog(
1033 parent,
1034 reason,
1035 tr("Disable plugin"),
1036 JOptionPane.WARNING_MESSAGE,
1037 null,
1038 options,
1039 options[0],
1040 null // FIXME: add help topic
1041 );
1042 return ret == 0;
1043 }
1044
1045 /**
1046 * Returns the plugin of the specified name.
1047 * @param name The plugin name
1048 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1049 */
1050 public static Object getPlugin(String name) {
1051 for (PluginProxy plugin : pluginList)
1052 if (plugin.getPluginInformation().name.equals(name))
1053 return plugin.plugin;
1054 return null;
1055 }
1056
1057 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1058 for (PluginProxy p : pluginList) {
1059 p.addDownloadSelection(downloadSelections);
1060 }
1061 }
1062
1063 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1064 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1065 for (PluginProxy plugin : pluginList) {
1066 settings.add(new PluginPreferenceFactory(plugin));
1067 }
1068 return settings;
1069 }
1070
1071 /**
1072 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1073 * ".jar" files.
1074 *
1075 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1076 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1077 * installation of the respective plugin is silently skipped.
1078 *
1079 * @param dowarn if true, warning messages are displayed; false otherwise
1080 */
1081 public static void installDownloadedPlugins(boolean dowarn) {
1082 File pluginDir = Main.pref.getPluginsDirectory();
1083 if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite())
1084 return;
1085
1086 final File[] files = pluginDir.listFiles(new FilenameFilter() {
1087 @Override
1088 public boolean accept(File dir, String name) {
1089 return name.endsWith(".jar.new");
1090 }});
1091
1092 for (File updatedPlugin : files) {
1093 final String filePath = updatedPlugin.getPath();
1094 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1095 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1096 if (plugin.exists() && !plugin.delete() && dowarn) {
1097 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1098 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1099 continue;
1100 }
1101 try {
1102 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1103 new JarFile(updatedPlugin).close();
1104 } catch (Exception e) {
1105 if (dowarn) {
1106 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}", plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1107 }
1108 continue;
1109 }
1110 // Install plugin
1111 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1112 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.", plugin.toString(), updatedPlugin.toString()));
1113 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1114 }
1115 }
1116 }
1117
1118 /**
1119 * Determines if the specified file is a valid and accessible JAR file.
1120 * @param jar The fil to check
1121 * @return true if file can be opened as a JAR file.
1122 * @since 5723
1123 */
1124 public static boolean isValidJar(File jar) {
1125 if (jar != null && jar.exists() && jar.canRead()) {
1126 try {
1127 new JarFile(jar).close();
1128 } catch (Exception e) {
1129 return false;
1130 }
1131 return true;
1132 }
1133 return false;
1134 }
1135
1136 /**
1137 * Replies the updated jar file for the given plugin name.
1138 * @param name The plugin name to find.
1139 * @return the updated jar file for the given plugin name. null if not found or not readable.
1140 * @since 5601
1141 */
1142 public static File findUpdatedJar(String name) {
1143 File pluginDir = Main.pref.getPluginsDirectory();
1144 // Find the downloaded file. We have tried to install the downloaded plugins
1145 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1146 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1147 if (!isValidJar(downloadedPluginFile)) {
1148 downloadedPluginFile = new File(pluginDir, name + ".jar");
1149 if (!isValidJar(downloadedPluginFile)) {
1150 return null;
1151 }
1152 }
1153 return downloadedPluginFile;
1154 }
1155
1156 /**
1157 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1158 * @param updatedPlugins The PluginInformation objects to update.
1159 * @since 5601
1160 */
1161 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1162 if (updatedPlugins == null) return;
1163 for (PluginInformation pi : updatedPlugins) {
1164 File downloadedPluginFile = findUpdatedJar(pi.name);
1165 if (downloadedPluginFile == null) {
1166 continue;
1167 }
1168 try {
1169 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1170 } catch(PluginException e) {
1171 Main.error(e);
1172 }
1173 }
1174 }
1175
1176 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1177 final ButtonSpec[] options = new ButtonSpec[] {
1178 new ButtonSpec(
1179 tr("Update plugin"),
1180 ImageProvider.get("dialogs", "refresh"),
1181 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1182 null /* no specific help context */
1183 ),
1184 new ButtonSpec(
1185 tr("Disable plugin"),
1186 ImageProvider.get("dialogs", "delete"),
1187 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1188 null /* no specific help context */
1189 ),
1190 new ButtonSpec(
1191 tr("Keep plugin"),
1192 ImageProvider.get("cancel"),
1193 tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name),
1194 null /* no specific help context */
1195 )
1196 };
1197
1198 final StringBuilder msg = new StringBuilder();
1199 msg.append("<html>");
1200 msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name));
1201 msg.append("<br>");
1202 if (plugin.getPluginInformation().author != null) {
1203 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author));
1204 msg.append("<br>");
1205 }
1206 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."));
1207 msg.append("</html>");
1208
1209 try {
1210 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
1211 @Override
1212 public Integer call() {
1213 return HelpAwareOptionPane.showOptionDialog(
1214 Main.parent,
1215 msg.toString(),
1216 tr("Update plugins"),
1217 JOptionPane.QUESTION_MESSAGE,
1218 null,
1219 options,
1220 options[0],
1221 ht("/ErrorMessages#ErrorInPlugin")
1222 );
1223 }
1224 });
1225 GuiHelper.runInEDT(task);
1226 return task.get();
1227 } catch (InterruptedException | ExecutionException e) {
1228 Main.warn(e);
1229 }
1230 return -1;
1231 }
1232
1233 /**
1234 * Replies the plugin which most likely threw the exception <code>ex</code>.
1235 *
1236 * @param ex the exception
1237 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1238 */
1239 private static PluginProxy getPluginCausingException(Throwable ex) {
1240 PluginProxy err = null;
1241 StackTraceElement[] stack = ex.getStackTrace();
1242 /* remember the error position, as multiple plugins may be involved,
1243 we search the topmost one */
1244 int pos = stack.length;
1245 for (PluginProxy p : pluginList) {
1246 String baseClass = p.getPluginInformation().className;
1247 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1248 for (int elpos = 0; elpos < pos; ++elpos) {
1249 if (stack[elpos].getClassName().startsWith(baseClass)) {
1250 pos = elpos;
1251 err = p;
1252 }
1253 }
1254 }
1255 return err;
1256 }
1257
1258 /**
1259 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1260 * conditionally updates or deactivates the plugin, but asks the user first.
1261 *
1262 * @param e the exception
1263 * @return plugin download task if the plugin has been updated to a newer version, {@code null} if it has been disabled or kept as it
1264 */
1265 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1266 PluginProxy plugin = null;
1267 // Check for an explicit problem when calling a plugin function
1268 if (e instanceof PluginException) {
1269 plugin = ((PluginException) e).plugin;
1270 }
1271 if (plugin == null) {
1272 plugin = getPluginCausingException(e);
1273 }
1274 if (plugin == null)
1275 // don't know what plugin threw the exception
1276 return null;
1277
1278 Set<String> plugins = new HashSet<>(
1279 Main.pref.getCollection("plugins",Collections.<String> emptySet())
1280 );
1281 final PluginInformation pluginInfo = plugin.getPluginInformation();
1282 if (! plugins.contains(pluginInfo.name))
1283 // plugin not activated ? strange in this context but anyway, don't bother
1284 // the user with dialogs, skip conditional deactivation
1285 return null;
1286
1287 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1288 case 0:
1289 // update the plugin
1290 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1291 return pluginDownloadTask;
1292 case 1:
1293 // deactivate the plugin
1294 plugins.remove(plugin.getPluginInformation().name);
1295 Main.pref.putCollection("plugins", plugins);
1296 GuiHelper.runInEDTAndWait(new Runnable() {
1297 @Override
1298 public void run() {
1299 JOptionPane.showMessageDialog(
1300 Main.parent,
1301 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1302 tr("Information"),
1303 JOptionPane.INFORMATION_MESSAGE
1304 );
1305 }
1306 });
1307 return null;
1308 default:
1309 // user doesn't want to deactivate the plugin
1310 return null;
1311 }
1312 }
1313
1314 /**
1315 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1316 * @return The list of loaded plugins (one plugin per line)
1317 */
1318 public static String getBugReportText() {
1319 StringBuilder text = new StringBuilder();
1320 LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
1321 for (final PluginProxy pp : pluginList) {
1322 PluginInformation pi = pp.getPluginInformation();
1323 pl.remove(pi.name);
1324 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1325 ? pi.localversion : "unknown") + ")");
1326 }
1327 Collections.sort(pl);
1328 if (!pl.isEmpty()) {
1329 text.append("Plugins:\n");
1330 }
1331 for (String s : pl) {
1332 text.append("- ").append(s).append("\n");
1333 }
1334 return text.toString();
1335 }
1336
1337 /**
1338 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1339 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1340 */
1341 public static JPanel getInfoPanel() {
1342 JPanel pluginTab = new JPanel(new GridBagLayout());
1343 for (final PluginProxy p : pluginList) {
1344 final PluginInformation info = p.getPluginInformation();
1345 String name = info.name
1346 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1347 pluginTab.add(new JLabel(name), GBC.std());
1348 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1349 pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1350 @Override
1351 public void actionPerformed(ActionEvent event) {
1352 StringBuilder b = new StringBuilder();
1353 for (Entry<String, String> e : info.attr.entrySet()) {
1354 b.append(e.getKey());
1355 b.append(": ");
1356 b.append(e.getValue());
1357 b.append("\n");
1358 }
1359 JosmTextArea a = new JosmTextArea(10, 40);
1360 a.setEditable(false);
1361 a.setText(b.toString());
1362 a.setCaretPosition(0);
1363 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1364 JOptionPane.INFORMATION_MESSAGE);
1365 }
1366 }), GBC.eol());
1367
1368 JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available")
1369 : info.description));
1370 description.setEditable(false);
1371 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1372 description.setLineWrap(true);
1373 description.setWrapStyleWord(true);
1374 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1375 description.setBackground(UIManager.getColor("Panel.background"));
1376 description.setCaretPosition(0);
1377
1378 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1379 }
1380 return pluginTab;
1381 }
1382
1383 private static class UpdatePluginsMessagePanel extends JPanel {
1384 private JMultilineLabel lblMessage;
1385 private JCheckBox cbDontShowAgain;
1386
1387 protected final void build() {
1388 setLayout(new GridBagLayout());
1389 GridBagConstraints gc = new GridBagConstraints();
1390 gc.anchor = GridBagConstraints.NORTHWEST;
1391 gc.fill = GridBagConstraints.BOTH;
1392 gc.weightx = 1.0;
1393 gc.weighty = 1.0;
1394 gc.insets = new Insets(5,5,5,5);
1395 add(lblMessage = new JMultilineLabel(""), gc);
1396 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1397
1398 gc.gridy = 1;
1399 gc.fill = GridBagConstraints.HORIZONTAL;
1400 gc.weighty = 0.0;
1401 add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc);
1402 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1403 }
1404
1405 public UpdatePluginsMessagePanel() {
1406 build();
1407 }
1408
1409 public void setMessage(String message) {
1410 lblMessage.setText(message);
1411 }
1412
1413 public void initDontShowAgain(String preferencesKey) {
1414 String policy = Main.pref.get(preferencesKey, "ask");
1415 policy = policy.trim().toLowerCase();
1416 cbDontShowAgain.setSelected(!"ask".equals(policy));
1417 }
1418
1419 public boolean isRememberDecision() {
1420 return cbDontShowAgain.isSelected();
1421 }
1422 }
1423}
Note: See TracBrowser for help on using the repository browser.