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

Last change on this file since 8024 was 8024, checked in by bastiK, 9 years ago

see #11090 - make sure plugin preference tab is shown after runtime load

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