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

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

fix potential NPEs and Sonar issues related to serialization

  • 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 if plugins is null
886 */
887 public static Collection<PluginInformation> updatePlugins(Component parent,
888 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
889 Collection<PluginInformation> plugins = null;
890 pluginDownloadTask = null;
891 if (monitor == null) {
892 monitor = NullProgressMonitor.INSTANCE;
893 }
894 try {
895 monitor.beginTask("");
896 ExecutorService service = Executors.newSingleThreadExecutor();
897
898 // try to download the plugin lists
899 //
900 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
901 monitor.createSubTaskMonitor(1, false),
902 Main.pref.getPluginSites(), displayErrMsg
903 );
904 Future<?> future = service.submit(task1);
905 List<PluginInformation> allPlugins = null;
906
907 try {
908 future.get();
909 allPlugins = task1.getAvailablePlugins();
910 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
911 // If only some plugins have to be updated, filter the list
912 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
913 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
914 PluginInformation pi = it.next();
915 boolean found = false;
916 for (PluginInformation piw : pluginsWanted) {
917 if (pi.name.equals(piw.name)) {
918 found = true;
919 break;
920 }
921 }
922 if (!found) {
923 it.remove();
924 }
925 }
926 }
927 } catch (ExecutionException e) {
928 Main.warn(tr("Failed to download plugin information list")+": ExecutionException");
929 Main.error(e);
930 // don't abort in case of error, continue with downloading plugins below
931 } catch (InterruptedException e) {
932 Main.warn(tr("Failed to download plugin information list")+": InterruptedException");
933 // don't abort in case of error, continue with downloading plugins below
934 }
935
936 // filter plugins which actually have to be updated
937 //
938 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
939 for (PluginInformation pi: plugins) {
940 if (pi.isUpdateRequired()) {
941 pluginsToUpdate.add(pi);
942 }
943 }
944
945 if (!pluginsToUpdate.isEmpty()) {
946
947 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
948
949 if (allPlugins != null) {
950 // Updated plugins may need additional plugin dependencies currently not installed
951 //
952 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
953 pluginsToDownload.addAll(additionalPlugins);
954
955 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
956 while (!additionalPlugins.isEmpty()) {
957 // Install the additional plugins to load them later
958 plugins.addAll(additionalPlugins);
959 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
960 pluginsToDownload.addAll(additionalPlugins);
961 }
962 }
963
964 // try to update the locally installed plugins
965 //
966 pluginDownloadTask = new PluginDownloadTask(
967 monitor.createSubTaskMonitor(1,false),
968 pluginsToDownload,
969 tr("Update plugins")
970 );
971
972 future = service.submit(pluginDownloadTask);
973 try {
974 future.get();
975 } catch(ExecutionException e) {
976 Main.error(e);
977 alertFailedPluginUpdate(parent, pluginsToUpdate);
978 return plugins;
979 } catch(InterruptedException e) {
980 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while updating plugins");
981 alertFailedPluginUpdate(parent, pluginsToUpdate);
982 return plugins;
983 }
984
985 // Update Plugin info for downloaded plugins
986 //
987 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
988
989 // notify user if downloading a locally installed plugin failed
990 //
991 if (! pluginDownloadTask.getFailedPlugins().isEmpty()) {
992 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
993 return plugins;
994 }
995 }
996 } finally {
997 monitor.finishTask();
998 }
999 if (pluginsWanted == null) {
1000 // if all plugins updated, remember the update because it was successful
1001 //
1002 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1003 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1004 }
1005 return plugins;
1006 }
1007
1008 /**
1009 * Ask the user for confirmation that a plugin shall be disabled.
1010 *
1011 * @param parent The parent component to be used for the displayed dialog
1012 * @param reason the reason for disabling the plugin
1013 * @param name the plugin name
1014 * @return true, if the plugin shall be disabled; false, otherwise
1015 */
1016 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1017 ButtonSpec [] options = new ButtonSpec[] {
1018 new ButtonSpec(
1019 tr("Disable plugin"),
1020 ImageProvider.get("dialogs", "delete"),
1021 tr("Click to delete the plugin ''{0}''", name),
1022 null /* no specific help context */
1023 ),
1024 new ButtonSpec(
1025 tr("Keep plugin"),
1026 ImageProvider.get("cancel"),
1027 tr("Click to keep the plugin ''{0}''", name),
1028 null /* no specific help context */
1029 )
1030 };
1031 int ret = HelpAwareOptionPane.showOptionDialog(
1032 parent,
1033 reason,
1034 tr("Disable plugin"),
1035 JOptionPane.WARNING_MESSAGE,
1036 null,
1037 options,
1038 options[0],
1039 null // FIXME: add help topic
1040 );
1041 return ret == 0;
1042 }
1043
1044 /**
1045 * Returns the plugin of the specified name.
1046 * @param name The plugin name
1047 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1048 */
1049 public static Object getPlugin(String name) {
1050 for (PluginProxy plugin : pluginList)
1051 if (plugin.getPluginInformation().name.equals(name))
1052 return plugin.plugin;
1053 return null;
1054 }
1055
1056 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1057 for (PluginProxy p : pluginList) {
1058 p.addDownloadSelection(downloadSelections);
1059 }
1060 }
1061
1062 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1063 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1064 for (PluginProxy plugin : pluginList) {
1065 settings.add(new PluginPreferenceFactory(plugin));
1066 }
1067 return settings;
1068 }
1069
1070 /**
1071 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1072 * ".jar" files.
1073 *
1074 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1075 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1076 * installation of the respective plugin is silently skipped.
1077 *
1078 * @param dowarn if true, warning messages are displayed; false otherwise
1079 */
1080 public static void installDownloadedPlugins(boolean dowarn) {
1081 File pluginDir = Main.pref.getPluginsDirectory();
1082 if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite())
1083 return;
1084
1085 final File[] files = pluginDir.listFiles(new FilenameFilter() {
1086 @Override
1087 public boolean accept(File dir, String name) {
1088 return name.endsWith(".jar.new");
1089 }});
1090 if (files == null)
1091 return;
1092
1093 for (File updatedPlugin : files) {
1094 final String filePath = updatedPlugin.getPath();
1095 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1096 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1097 if (plugin.exists() && !plugin.delete() && dowarn) {
1098 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1099 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1100 continue;
1101 }
1102 try {
1103 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1104 new JarFile(updatedPlugin).close();
1105 } catch (Exception e) {
1106 if (dowarn) {
1107 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}", plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1108 }
1109 continue;
1110 }
1111 // Install plugin
1112 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1113 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.", plugin.toString(), updatedPlugin.toString()));
1114 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName));
1115 }
1116 }
1117 }
1118
1119 /**
1120 * Determines if the specified file is a valid and accessible JAR file.
1121 * @param jar The fil to check
1122 * @return true if file can be opened as a JAR file.
1123 * @since 5723
1124 */
1125 public static boolean isValidJar(File jar) {
1126 if (jar != null && jar.exists() && jar.canRead()) {
1127 try {
1128 new JarFile(jar).close();
1129 } catch (Exception e) {
1130 return false;
1131 }
1132 return true;
1133 }
1134 return false;
1135 }
1136
1137 /**
1138 * Replies the updated jar file for the given plugin name.
1139 * @param name The plugin name to find.
1140 * @return the updated jar file for the given plugin name. null if not found or not readable.
1141 * @since 5601
1142 */
1143 public static File findUpdatedJar(String name) {
1144 File pluginDir = Main.pref.getPluginsDirectory();
1145 // Find the downloaded file. We have tried to install the downloaded plugins
1146 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1147 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1148 if (!isValidJar(downloadedPluginFile)) {
1149 downloadedPluginFile = new File(pluginDir, name + ".jar");
1150 if (!isValidJar(downloadedPluginFile)) {
1151 return null;
1152 }
1153 }
1154 return downloadedPluginFile;
1155 }
1156
1157 /**
1158 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1159 * @param updatedPlugins The PluginInformation objects to update.
1160 * @since 5601
1161 */
1162 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1163 if (updatedPlugins == null) return;
1164 for (PluginInformation pi : updatedPlugins) {
1165 File downloadedPluginFile = findUpdatedJar(pi.name);
1166 if (downloadedPluginFile == null) {
1167 continue;
1168 }
1169 try {
1170 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1171 } catch(PluginException e) {
1172 Main.error(e);
1173 }
1174 }
1175 }
1176
1177 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1178 final ButtonSpec[] options = new ButtonSpec[] {
1179 new ButtonSpec(
1180 tr("Update plugin"),
1181 ImageProvider.get("dialogs", "refresh"),
1182 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1183 null /* no specific help context */
1184 ),
1185 new ButtonSpec(
1186 tr("Disable plugin"),
1187 ImageProvider.get("dialogs", "delete"),
1188 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1189 null /* no specific help context */
1190 ),
1191 new ButtonSpec(
1192 tr("Keep plugin"),
1193 ImageProvider.get("cancel"),
1194 tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name),
1195 null /* no specific help context */
1196 )
1197 };
1198
1199 final StringBuilder msg = new StringBuilder();
1200 msg.append("<html>");
1201 msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name));
1202 msg.append("<br>");
1203 if (plugin.getPluginInformation().author != null) {
1204 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author));
1205 msg.append("<br>");
1206 }
1207 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."));
1208 msg.append("</html>");
1209
1210 try {
1211 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
1212 @Override
1213 public Integer call() {
1214 return HelpAwareOptionPane.showOptionDialog(
1215 Main.parent,
1216 msg.toString(),
1217 tr("Update plugins"),
1218 JOptionPane.QUESTION_MESSAGE,
1219 null,
1220 options,
1221 options[0],
1222 ht("/ErrorMessages#ErrorInPlugin")
1223 );
1224 }
1225 });
1226 GuiHelper.runInEDT(task);
1227 return task.get();
1228 } catch (InterruptedException | ExecutionException e) {
1229 Main.warn(e);
1230 }
1231 return -1;
1232 }
1233
1234 /**
1235 * Replies the plugin which most likely threw the exception <code>ex</code>.
1236 *
1237 * @param ex the exception
1238 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1239 */
1240 private static PluginProxy getPluginCausingException(Throwable ex) {
1241 PluginProxy err = null;
1242 StackTraceElement[] stack = ex.getStackTrace();
1243 /* remember the error position, as multiple plugins may be involved,
1244 we search the topmost one */
1245 int pos = stack.length;
1246 for (PluginProxy p : pluginList) {
1247 String baseClass = p.getPluginInformation().className;
1248 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1249 for (int elpos = 0; elpos < pos; ++elpos) {
1250 if (stack[elpos].getClassName().startsWith(baseClass)) {
1251 pos = elpos;
1252 err = p;
1253 }
1254 }
1255 }
1256 return err;
1257 }
1258
1259 /**
1260 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1261 * conditionally updates or deactivates the plugin, but asks the user first.
1262 *
1263 * @param e the exception
1264 * @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
1265 */
1266 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1267 PluginProxy plugin = null;
1268 // Check for an explicit problem when calling a plugin function
1269 if (e instanceof PluginException) {
1270 plugin = ((PluginException) e).plugin;
1271 }
1272 if (plugin == null) {
1273 plugin = getPluginCausingException(e);
1274 }
1275 if (plugin == null)
1276 // don't know what plugin threw the exception
1277 return null;
1278
1279 Set<String> plugins = new HashSet<>(
1280 Main.pref.getCollection("plugins",Collections.<String> emptySet())
1281 );
1282 final PluginInformation pluginInfo = plugin.getPluginInformation();
1283 if (! plugins.contains(pluginInfo.name))
1284 // plugin not activated ? strange in this context but anyway, don't bother
1285 // the user with dialogs, skip conditional deactivation
1286 return null;
1287
1288 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1289 case 0:
1290 // update the plugin
1291 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1292 return pluginDownloadTask;
1293 case 1:
1294 // deactivate the plugin
1295 plugins.remove(plugin.getPluginInformation().name);
1296 Main.pref.putCollection("plugins", plugins);
1297 GuiHelper.runInEDTAndWait(new Runnable() {
1298 @Override
1299 public void run() {
1300 JOptionPane.showMessageDialog(
1301 Main.parent,
1302 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1303 tr("Information"),
1304 JOptionPane.INFORMATION_MESSAGE
1305 );
1306 }
1307 });
1308 return null;
1309 default:
1310 // user doesn't want to deactivate the plugin
1311 return null;
1312 }
1313 }
1314
1315 /**
1316 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1317 * @return The list of loaded plugins (one plugin per line)
1318 */
1319 public static String getBugReportText() {
1320 StringBuilder text = new StringBuilder();
1321 LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
1322 for (final PluginProxy pp : pluginList) {
1323 PluginInformation pi = pp.getPluginInformation();
1324 pl.remove(pi.name);
1325 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1326 ? pi.localversion : "unknown") + ")");
1327 }
1328 Collections.sort(pl);
1329 if (!pl.isEmpty()) {
1330 text.append("Plugins:\n");
1331 }
1332 for (String s : pl) {
1333 text.append("- ").append(s).append("\n");
1334 }
1335 return text.toString();
1336 }
1337
1338 /**
1339 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1340 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1341 */
1342 public static JPanel getInfoPanel() {
1343 JPanel pluginTab = new JPanel(new GridBagLayout());
1344 for (final PluginProxy p : pluginList) {
1345 final PluginInformation info = p.getPluginInformation();
1346 String name = info.name
1347 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1348 pluginTab.add(new JLabel(name), GBC.std());
1349 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1350 pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1351 @Override
1352 public void actionPerformed(ActionEvent event) {
1353 StringBuilder b = new StringBuilder();
1354 for (Entry<String, String> e : info.attr.entrySet()) {
1355 b.append(e.getKey());
1356 b.append(": ");
1357 b.append(e.getValue());
1358 b.append("\n");
1359 }
1360 JosmTextArea a = new JosmTextArea(10, 40);
1361 a.setEditable(false);
1362 a.setText(b.toString());
1363 a.setCaretPosition(0);
1364 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1365 JOptionPane.INFORMATION_MESSAGE);
1366 }
1367 }), GBC.eol());
1368
1369 JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available")
1370 : info.description));
1371 description.setEditable(false);
1372 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1373 description.setLineWrap(true);
1374 description.setWrapStyleWord(true);
1375 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1376 description.setBackground(UIManager.getColor("Panel.background"));
1377 description.setCaretPosition(0);
1378
1379 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1380 }
1381 return pluginTab;
1382 }
1383
1384 private static class UpdatePluginsMessagePanel extends JPanel {
1385 private JMultilineLabel lblMessage;
1386 private JCheckBox cbDontShowAgain;
1387
1388 protected final void build() {
1389 setLayout(new GridBagLayout());
1390 GridBagConstraints gc = new GridBagConstraints();
1391 gc.anchor = GridBagConstraints.NORTHWEST;
1392 gc.fill = GridBagConstraints.BOTH;
1393 gc.weightx = 1.0;
1394 gc.weighty = 1.0;
1395 gc.insets = new Insets(5,5,5,5);
1396 add(lblMessage = new JMultilineLabel(""), gc);
1397 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1398
1399 gc.gridy = 1;
1400 gc.fill = GridBagConstraints.HORIZONTAL;
1401 gc.weighty = 0.0;
1402 add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc);
1403 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1404 }
1405
1406 public UpdatePluginsMessagePanel() {
1407 build();
1408 }
1409
1410 public void setMessage(String message) {
1411 lblMessage.setText(message);
1412 }
1413
1414 public void initDontShowAgain(String preferencesKey) {
1415 String policy = Main.pref.get(preferencesKey, "ask");
1416 policy = policy.trim().toLowerCase();
1417 cbDontShowAgain.setSelected(!"ask".equals(policy));
1418 }
1419
1420 public boolean isRememberDecision() {
1421 return cbDontShowAgain.isSelected();
1422 }
1423 }
1424}
Note: See TracBrowser for help on using the repository browser.