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

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

When doing a String.toLowerCase()/toUpperCase() call, use a Locale. This avoids problems with certain locales, i.e. Lithuanian or Turkish. See PMD UseLocaleWithCaseConversions rule and String.toLowerCase() javadoc.

  • Property svn:eol-style set to native
File size: 58.7 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.Locale;
31import java.util.Map;
32import java.util.Map.Entry;
33import java.util.Set;
34import java.util.TreeSet;
35import java.util.concurrent.Callable;
36import java.util.concurrent.ExecutionException;
37import java.util.concurrent.ExecutorService;
38import java.util.concurrent.Executors;
39import java.util.concurrent.Future;
40import java.util.concurrent.FutureTask;
41import java.util.jar.JarFile;
42
43import javax.swing.AbstractAction;
44import javax.swing.BorderFactory;
45import javax.swing.Box;
46import javax.swing.JButton;
47import javax.swing.JCheckBox;
48import javax.swing.JLabel;
49import javax.swing.JOptionPane;
50import javax.swing.JPanel;
51import javax.swing.JScrollPane;
52import javax.swing.UIManager;
53
54import org.openstreetmap.josm.Main;
55import org.openstreetmap.josm.data.Version;
56import org.openstreetmap.josm.gui.HelpAwareOptionPane;
57import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec;
58import org.openstreetmap.josm.gui.download.DownloadSelection;
59import org.openstreetmap.josm.gui.help.HelpUtil;
60import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
61import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
62import org.openstreetmap.josm.gui.progress.ProgressMonitor;
63import org.openstreetmap.josm.gui.util.GuiHelper;
64import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
65import org.openstreetmap.josm.gui.widgets.JosmTextArea;
66import org.openstreetmap.josm.io.OfflineAccessException;
67import org.openstreetmap.josm.io.OnlineResource;
68import org.openstreetmap.josm.tools.GBC;
69import org.openstreetmap.josm.tools.I18n;
70import org.openstreetmap.josm.tools.ImageProvider;
71import org.openstreetmap.josm.tools.Utils;
72
73/**
74 * PluginHandler is basically a collection of static utility functions used to bootstrap
75 * and manage the loaded plugins.
76 * @since 1326
77 */
78public final class PluginHandler {
79
80 /**
81 * Deprecated plugins that are removed on start
82 */
83 public static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS;
84 static {
85 String IN_CORE = tr("integrated into main program");
86
87 DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] {
88 new DeprecatedPlugin("mappaint", IN_CORE),
89 new DeprecatedPlugin("unglueplugin", IN_CORE),
90 new DeprecatedPlugin("lang-de", IN_CORE),
91 new DeprecatedPlugin("lang-en_GB", IN_CORE),
92 new DeprecatedPlugin("lang-fr", IN_CORE),
93 new DeprecatedPlugin("lang-it", IN_CORE),
94 new DeprecatedPlugin("lang-pl", IN_CORE),
95 new DeprecatedPlugin("lang-ro", IN_CORE),
96 new DeprecatedPlugin("lang-ru", IN_CORE),
97 new DeprecatedPlugin("ewmsplugin", IN_CORE),
98 new DeprecatedPlugin("ywms", IN_CORE),
99 new DeprecatedPlugin("tways-0.2", IN_CORE),
100 new DeprecatedPlugin("geotagged", IN_CORE),
101 new DeprecatedPlugin("landsat", tr("replaced by new {0} plugin","lakewalker")),
102 new DeprecatedPlugin("namefinder", IN_CORE),
103 new DeprecatedPlugin("waypoints", IN_CORE),
104 new DeprecatedPlugin("slippy_map_chooser", IN_CORE),
105 new DeprecatedPlugin("tcx-support", tr("replaced by new {0} plugin","dataimport")),
106 new DeprecatedPlugin("usertools", IN_CORE),
107 new DeprecatedPlugin("AgPifoJ", IN_CORE),
108 new DeprecatedPlugin("utilsplugin", IN_CORE),
109 new DeprecatedPlugin("ghost", IN_CORE),
110 new DeprecatedPlugin("validator", IN_CORE),
111 new DeprecatedPlugin("multipoly", IN_CORE),
112 new DeprecatedPlugin("multipoly-convert", IN_CORE),
113 new DeprecatedPlugin("remotecontrol", IN_CORE),
114 new DeprecatedPlugin("imagery", IN_CORE),
115 new DeprecatedPlugin("slippymap", IN_CORE),
116 new DeprecatedPlugin("wmsplugin", IN_CORE),
117 new DeprecatedPlugin("ParallelWay", IN_CORE),
118 new DeprecatedPlugin("dumbutils", tr("replaced by new {0} plugin","utilsplugin2")),
119 new DeprecatedPlugin("ImproveWayAccuracy", IN_CORE),
120 new DeprecatedPlugin("Curves", tr("replaced by new {0} plugin","utilsplugin2")),
121 new DeprecatedPlugin("epsg31287", tr("replaced by new {0} plugin", "proj4j")),
122 new DeprecatedPlugin("licensechange", tr("no longer required")),
123 new DeprecatedPlugin("restart", IN_CORE),
124 new DeprecatedPlugin("wayselector", IN_CORE),
125 new DeprecatedPlugin("openstreetbugs", tr("replaced by new {0} plugin", "notes")),
126 new DeprecatedPlugin("nearclick", tr("no longer required")),
127 new DeprecatedPlugin("notes", IN_CORE),
128 });
129 }
130
131 private PluginHandler() {
132 // Hide default constructor for utils classes
133 }
134
135 /**
136 * Description of a deprecated plugin
137 */
138 public static class DeprecatedPlugin implements Comparable<DeprecatedPlugin> {
139 /** Plugin name */
140 public final String name;
141 /** Short explanation about deprecation, can be {@code null} */
142 public final String reason;
143 /** Code to run to perform migration, can be {@code null} */
144 private final Runnable migration;
145
146 /**
147 * Constructs a new {@code DeprecatedPlugin}.
148 * @param name The plugin name
149 */
150 public DeprecatedPlugin(String name) {
151 this(name, null, null);
152 }
153
154 /**
155 * Constructs a new {@code DeprecatedPlugin} with a given reason.
156 * @param name The plugin name
157 * @param reason The reason about deprecation
158 */
159 public DeprecatedPlugin(String name, String reason) {
160 this(name, reason, null);
161 }
162
163 /**
164 * Constructs a new {@code DeprecatedPlugin}.
165 * @param name The plugin name
166 * @param reason The reason about deprecation
167 * @param migration The code to run to perform migration
168 */
169 public DeprecatedPlugin(String name, String reason, Runnable migration) {
170 this.name = name;
171 this.reason = reason;
172 this.migration = migration;
173 }
174
175 /**
176 * Performs migration.
177 */
178 public void migrate() {
179 if (migration != null) {
180 migration.run();
181 }
182 }
183
184 @Override
185 public int compareTo(DeprecatedPlugin o) {
186 return name.compareTo(o.name);
187 }
188 }
189
190 /**
191 * ClassLoader that makes the addURL method of URLClassLoader public.
192 *
193 * Like URLClassLoader, but allows to add more URLs after construction.
194 */
195 public static class DynamicURLClassLoader extends URLClassLoader {
196
197 public DynamicURLClassLoader(URL[] urls, ClassLoader parent) {
198 super(urls, parent);
199 }
200
201 @Override
202 public void addURL(URL url) {
203 super.addURL(url);
204 }
205 }
206
207 /**
208 * List of unmaintained plugins. Not really up-to-date as the vast majority of plugins are not maintained after a few months, sadly...
209 */
210 private static final String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"};
211
212 /**
213 * Default time-based update interval, in days (pluginmanager.time-based-update.interval)
214 */
215 public static final int DEFAULT_TIME_BASED_UPDATE_INTERVAL = 30;
216
217 /**
218 * All installed and loaded plugins (resp. their main classes)
219 */
220 public static final Collection<PluginProxy> pluginList = new LinkedList<>();
221
222 /**
223 * Global plugin ClassLoader.
224 */
225 private static DynamicURLClassLoader pluginClassLoader;
226
227 /**
228 * Add here all ClassLoader whose resource should be searched.
229 */
230 private static final List<ClassLoader> sources = new LinkedList<>();
231
232 static {
233 try {
234 sources.add(ClassLoader.getSystemClassLoader());
235 sources.add(org.openstreetmap.josm.gui.MainApplication.class.getClassLoader());
236 } catch (SecurityException ex) {
237 sources.add(ImageProvider.class.getClassLoader());
238 }
239 }
240
241 private static PluginDownloadTask pluginDownloadTask = null;
242
243 public static Collection<ClassLoader> getResourceClassLoaders() {
244 return Collections.unmodifiableCollection(sources);
245 }
246
247 /**
248 * Removes deprecated plugins from a collection of plugins. Modifies the
249 * collection <code>plugins</code>.
250 *
251 * Also notifies the user about removed deprecated plugins
252 *
253 * @param parent The parent Component used to display warning popup
254 * @param plugins the collection of plugins
255 */
256 private static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) {
257 Set<DeprecatedPlugin> removedPlugins = new TreeSet<>();
258 for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) {
259 if (plugins.contains(depr.name)) {
260 plugins.remove(depr.name);
261 Main.pref.removeFromCollection("plugins", depr.name);
262 removedPlugins.add(depr);
263 depr.migrate();
264 }
265 }
266 if (removedPlugins.isEmpty())
267 return;
268
269 // notify user about removed deprecated plugins
270 //
271 StringBuilder sb = new StringBuilder();
272 sb.append("<html>")
273 .append(trn(
274 "The following plugin is no longer necessary and has been deactivated:",
275 "The following plugins are no longer necessary and have been deactivated:",
276 removedPlugins.size()))
277 .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></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(Locale.ENGLISH);
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 .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 .append(Utils.joinAsHtmlUnorderedList(missingRequiredPlugin))
470 .append("</html>");
471 JOptionPane.showMessageDialog(
472 parent,
473 sb.toString(),
474 tr("Error"),
475 JOptionPane.ERROR_MESSAGE
476 );
477 }
478
479 private static void alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) {
480 HelpAwareOptionPane.showOptionDialog(
481 parent,
482 tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>"
483 +"You have to update JOSM in order to use this plugin.</html>",
484 plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString()
485 ),
486 tr("Warning"),
487 JOptionPane.WARNING_MESSAGE,
488 HelpUtil.ht("/Plugin/Loading#JOSMUpdateRequired")
489 );
490 }
491
492 /**
493 * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
494 * current JOSM version must be compatible with the plugin and no other plugins this plugin
495 * depends on should be missing.
496 *
497 * @param parent The parent Component used to display error popup
498 * @param plugins the collection of all loaded plugins
499 * @param plugin the plugin for which preconditions are checked
500 * @return true, if the preconditions are met; false otherwise
501 */
502 public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) {
503
504 // make sure the plugin is compatible with the current JOSM version
505 //
506 int josmVersion = Version.getInstance().getVersion();
507 if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
508 alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion);
509 return false;
510 }
511
512 // Add all plugins already loaded (to include early plugins when checking late ones)
513 Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
514 for (PluginProxy proxy : pluginList) {
515 allPlugins.add(proxy.getPluginInformation());
516 }
517
518 return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true);
519 }
520
521 /**
522 * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met.
523 * No other plugins this plugin depends on should be missing.
524 *
525 * @param parent The parent Component used to display error popup. If parent is
526 * null, the error popup is suppressed
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 if (parent != null) {
553 alertMissingRequiredPlugin(parent, plugin.name, missingPlugins);
554 }
555 return false;
556 }
557 }
558 return true;
559 }
560
561 /**
562 * Get the class loader for loading plugin code.
563 *
564 * @return the class loader
565 */
566 public static synchronized DynamicURLClassLoader getPluginClassLoader() {
567 if (pluginClassLoader == null) {
568 pluginClassLoader = AccessController.doPrivileged(new PrivilegedAction<DynamicURLClassLoader>() {
569 public DynamicURLClassLoader run() {
570 return new DynamicURLClassLoader(new URL[0], Main.class.getClassLoader());
571 }
572 });
573 sources.add(0, pluginClassLoader);
574 }
575 return pluginClassLoader;
576 }
577
578 /**
579 * Add more plugins to the plugin class loader.
580 *
581 * @param plugins the plugins that should be handled by the plugin class loader
582 */
583 public static void extendPluginClassLoader(Collection<PluginInformation> plugins) {
584 // iterate all plugins and collect all libraries of all plugins:
585 File pluginDir = Main.pref.getPluginsDirectory();
586 DynamicURLClassLoader cl = getPluginClassLoader();
587
588 for (PluginInformation info : plugins) {
589 if (info.libraries == null) {
590 continue;
591 }
592 for (URL libUrl : info.libraries) {
593 cl.addURL(libUrl);
594 }
595 File pluginJar = new File(pluginDir, info.name + ".jar");
596 I18n.addTexts(pluginJar);
597 URL pluginJarUrl = Utils.fileToURL(pluginJar);
598 cl.addURL(pluginJarUrl);
599 }
600 }
601
602 /**
603 * Loads and instantiates the plugin described by <code>plugin</code> using
604 * the class loader <code>pluginClassLoader</code>.
605 *
606 * @param parent The parent component to be used for the displayed dialog
607 * @param plugin the plugin
608 * @param pluginClassLoader the plugin class loader
609 */
610 public static void loadPlugin(Component parent, PluginInformation plugin, ClassLoader pluginClassLoader) {
611 String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name);
612 try {
613 Class<?> klass = plugin.loadClass(pluginClassLoader);
614 if (klass != null) {
615 Main.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion));
616 PluginProxy pluginProxy = plugin.load(klass);
617 pluginList.add(pluginProxy);
618 Main.addMapFrameListener(pluginProxy, true);
619 }
620 msg = null;
621 } catch (PluginException e) {
622 Main.error(e);
623 if (e.getCause() instanceof ClassNotFoundException) {
624 msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>"
625 + "Delete from preferences?</html>", plugin.name, plugin.className);
626 }
627 } catch (Exception e) {
628 Main.error(e);
629 }
630 if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) {
631 Main.pref.removeFromCollection("plugins", plugin.name);
632 }
633 }
634
635 /**
636 * Loads the plugin in <code>plugins</code> from locally available jar files into
637 * memory.
638 *
639 * @param parent The parent component to be used for the displayed dialog
640 * @param plugins the list of plugins
641 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
642 */
643 public static void loadPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
644 if (monitor == null) {
645 monitor = NullProgressMonitor.INSTANCE;
646 }
647 try {
648 monitor.beginTask(tr("Loading plugins ..."));
649 monitor.subTask(tr("Checking plugin preconditions..."));
650 List<PluginInformation> toLoad = new LinkedList<>();
651 for (PluginInformation pi: plugins) {
652 if (checkLoadPreconditions(parent, plugins, pi)) {
653 toLoad.add(pi);
654 }
655 }
656 // sort the plugins according to their "staging" equivalence class. The
657 // lower the value of "stage" the earlier the plugin should be loaded.
658 //
659 Collections.sort(
660 toLoad,
661 new Comparator<PluginInformation>() {
662 @Override
663 public int compare(PluginInformation o1, PluginInformation o2) {
664 if (o1.stage < o2.stage) return -1;
665 if (o1.stage == o2.stage) return 0;
666 return 1;
667 }
668 }
669 );
670 if (toLoad.isEmpty())
671 return;
672
673 extendPluginClassLoader(toLoad);
674 monitor.setTicksCount(toLoad.size());
675 for (PluginInformation info : toLoad) {
676 monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
677 loadPlugin(parent, info, getPluginClassLoader());
678 monitor.worked(1);
679 }
680 } finally {
681 monitor.finishTask();
682 }
683 }
684
685 /**
686 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
687 * set to true.
688 *
689 * @param plugins the collection of plugins
690 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
691 */
692 public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
693 List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size());
694 for (PluginInformation pi: plugins) {
695 if (pi.early) {
696 earlyPlugins.add(pi);
697 }
698 }
699 loadPlugins(parent, earlyPlugins, monitor);
700 }
701
702 /**
703 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early}
704 * set to false.
705 *
706 * @param parent The parent component to be used for the displayed dialog
707 * @param plugins the collection of plugins
708 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
709 */
710 public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
711 List<PluginInformation> latePlugins = new ArrayList<>(plugins.size());
712 for (PluginInformation pi: plugins) {
713 if (!pi.early) {
714 latePlugins.add(pi);
715 }
716 }
717 loadPlugins(parent, latePlugins, monitor);
718 }
719
720 /**
721 * Loads locally available plugin information from local plugin jars and from cached
722 * plugin lists.
723 *
724 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
725 * @return the list of locally available plugin information
726 *
727 */
728 private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
729 if (monitor == null) {
730 monitor = NullProgressMonitor.INSTANCE;
731 }
732 try {
733 ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
734 ExecutorService service = Executors.newSingleThreadExecutor();
735 Future<?> future = service.submit(task);
736 try {
737 future.get();
738 } catch(ExecutionException e) {
739 Main.error(e);
740 return null;
741 } catch(InterruptedException e) {
742 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while loading locally available plugin information");
743 return null;
744 }
745 Map<String, PluginInformation> ret = new HashMap<>();
746 for (PluginInformation pi: task.getAvailablePlugins()) {
747 ret.put(pi.name, pi);
748 }
749 return ret;
750 } finally {
751 monitor.finishTask();
752 }
753 }
754
755 private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) {
756 StringBuilder sb = new StringBuilder();
757 sb.append("<html>")
758 .append(trn("JOSM could not find information about the following plugin:",
759 "JOSM could not find information about the following plugins:",
760 plugins.size()))
761 .append(Utils.joinAsHtmlUnorderedList(plugins))
762 .append(trn("The plugin is not going to be loaded.",
763 "The plugins are not going to be loaded.",
764 plugins.size()))
765 .append("</html>");
766 HelpAwareOptionPane.showOptionDialog(
767 parent,
768 sb.toString(),
769 tr("Warning"),
770 JOptionPane.WARNING_MESSAGE,
771 HelpUtil.ht("/Plugin/Loading#MissingPluginInfos")
772 );
773 }
774
775 /**
776 * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
777 * out. This involves user interaction. This method displays alert and confirmation
778 * messages.
779 *
780 * @param parent The parent component to be used for the displayed dialog
781 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
782 * @return the set of plugins to load (as set of plugin names)
783 */
784 public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
785 if (monitor == null) {
786 monitor = NullProgressMonitor.INSTANCE;
787 }
788 try {
789 monitor.beginTask(tr("Determine plugins to load..."));
790 Set<String> plugins = new HashSet<>();
791 plugins.addAll(Main.pref.getCollection("plugins", new LinkedList<String>()));
792 if (System.getProperty("josm.plugins") != null) {
793 plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
794 }
795 monitor.subTask(tr("Removing deprecated plugins..."));
796 filterDeprecatedPlugins(parent, plugins);
797 monitor.subTask(tr("Removing unmaintained plugins..."));
798 filterUnmaintainedPlugins(parent, plugins);
799 Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1,false));
800 List<PluginInformation> ret = new LinkedList<>();
801 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
802 String plugin = it.next();
803 if (infos.containsKey(plugin)) {
804 ret.add(infos.get(plugin));
805 it.remove();
806 }
807 }
808 if (!plugins.isEmpty()) {
809 alertMissingPluginInformation(parent, plugins);
810 }
811 return ret;
812 } finally {
813 monitor.finishTask();
814 }
815 }
816
817 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
818 StringBuilder sb = new StringBuilder();
819 sb.append("<html>")
820 .append(trn(
821 "Updating the following plugin has failed:",
822 "Updating the following plugins has failed:",
823 plugins.size()))
824 .append("<ul>");
825 for (PluginInformation pi: plugins) {
826 sb.append("<li>").append(pi.name).append("</li>");
827 }
828 sb.append("</ul>")
829 .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 .append("</html>");
834 HelpAwareOptionPane.showOptionDialog(
835 parent,
836 sb.toString(),
837 tr("Plugin update failed"),
838 JOptionPane.ERROR_MESSAGE,
839 HelpUtil.ht("/Plugin/Loading#FailedPluginUpdated")
840 );
841 }
842
843 private static Set<PluginInformation> findRequiredPluginsToDownload(
844 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
845 Set<PluginInformation> result = new HashSet<>();
846 for (PluginInformation pi : pluginsToUpdate) {
847 for (String name : pi.getRequiredPlugins()) {
848 try {
849 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
850 if (installedPlugin == null) {
851 // New required plugin is not installed, find its PluginInformation
852 PluginInformation reqPlugin = null;
853 for (PluginInformation pi2 : allPlugins) {
854 if (pi2.getName().equals(name)) {
855 reqPlugin = pi2;
856 break;
857 }
858 }
859 // Required plugin is known but not already on download list
860 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
861 result.add(reqPlugin);
862 }
863 }
864 } catch (PluginException e) {
865 Main.warn(tr("Failed to find plugin {0}", name));
866 Main.error(e);
867 }
868 }
869 }
870 return result;
871 }
872
873 /**
874 * Updates the plugins in <code>plugins</code>.
875 *
876 * @param parent the parent component for message boxes
877 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
878 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
879 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
880 * @throws IllegalArgumentException if plugins is null
881 */
882 public static Collection<PluginInformation> updatePlugins(Component parent,
883 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
884 Collection<PluginInformation> plugins = null;
885 pluginDownloadTask = null;
886 if (monitor == null) {
887 monitor = NullProgressMonitor.INSTANCE;
888 }
889 try {
890 monitor.beginTask("");
891 ExecutorService service = Executors.newSingleThreadExecutor();
892
893 // try to download the plugin lists
894 //
895 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
896 monitor.createSubTaskMonitor(1, false),
897 Main.pref.getPluginSites(), displayErrMsg
898 );
899 Future<?> future = service.submit(task1);
900 List<PluginInformation> allPlugins = null;
901
902 try {
903 future.get();
904 allPlugins = task1.getAvailablePlugins();
905 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
906 // If only some plugins have to be updated, filter the list
907 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
908 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
909 PluginInformation pi = it.next();
910 boolean found = false;
911 for (PluginInformation piw : pluginsWanted) {
912 if (pi.name.equals(piw.name)) {
913 found = true;
914 break;
915 }
916 }
917 if (!found) {
918 it.remove();
919 }
920 }
921 }
922 } catch (ExecutionException e) {
923 Main.warn(tr("Failed to download plugin information list")+": ExecutionException");
924 Main.error(e);
925 // don't abort in case of error, continue with downloading plugins below
926 } catch (InterruptedException e) {
927 Main.warn(tr("Failed to download plugin information list")+": InterruptedException");
928 // don't abort in case of error, continue with downloading plugins below
929 }
930
931 // filter plugins which actually have to be updated
932 //
933 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
934 for (PluginInformation pi: plugins) {
935 if (pi.isUpdateRequired()) {
936 pluginsToUpdate.add(pi);
937 }
938 }
939
940 if (!pluginsToUpdate.isEmpty()) {
941
942 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
943
944 if (allPlugins != null) {
945 // Updated plugins may need additional plugin dependencies currently not installed
946 //
947 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
948 pluginsToDownload.addAll(additionalPlugins);
949
950 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
951 while (!additionalPlugins.isEmpty()) {
952 // Install the additional plugins to load them later
953 plugins.addAll(additionalPlugins);
954 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
955 pluginsToDownload.addAll(additionalPlugins);
956 }
957 }
958
959 // try to update the locally installed plugins
960 //
961 pluginDownloadTask = new PluginDownloadTask(
962 monitor.createSubTaskMonitor(1,false),
963 pluginsToDownload,
964 tr("Update plugins")
965 );
966
967 future = service.submit(pluginDownloadTask);
968 try {
969 future.get();
970 } catch(ExecutionException e) {
971 Main.error(e);
972 alertFailedPluginUpdate(parent, pluginsToUpdate);
973 return plugins;
974 } catch(InterruptedException e) {
975 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while updating plugins");
976 alertFailedPluginUpdate(parent, pluginsToUpdate);
977 return plugins;
978 }
979
980 // Update Plugin info for downloaded plugins
981 //
982 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
983
984 // notify user if downloading a locally installed plugin failed
985 //
986 if (! pluginDownloadTask.getFailedPlugins().isEmpty()) {
987 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
988 return plugins;
989 }
990 }
991 } finally {
992 monitor.finishTask();
993 }
994 if (pluginsWanted == null) {
995 // if all plugins updated, remember the update because it was successful
996 //
997 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
998 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
999 }
1000 return plugins;
1001 }
1002
1003 /**
1004 * Ask the user for confirmation that a plugin shall be disabled.
1005 *
1006 * @param parent The parent component to be used for the displayed dialog
1007 * @param reason the reason for disabling the plugin
1008 * @param name the plugin name
1009 * @return true, if the plugin shall be disabled; false, otherwise
1010 */
1011 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1012 ButtonSpec [] options = new ButtonSpec[] {
1013 new ButtonSpec(
1014 tr("Disable plugin"),
1015 ImageProvider.get("dialogs", "delete"),
1016 tr("Click to delete the plugin ''{0}''", name),
1017 null /* no specific help context */
1018 ),
1019 new ButtonSpec(
1020 tr("Keep plugin"),
1021 ImageProvider.get("cancel"),
1022 tr("Click to keep the plugin ''{0}''", name),
1023 null /* no specific help context */
1024 )
1025 };
1026 int ret = HelpAwareOptionPane.showOptionDialog(
1027 parent,
1028 reason,
1029 tr("Disable plugin"),
1030 JOptionPane.WARNING_MESSAGE,
1031 null,
1032 options,
1033 options[0],
1034 null // FIXME: add help topic
1035 );
1036 return ret == 0;
1037 }
1038
1039 /**
1040 * Returns the plugin of the specified name.
1041 * @param name The plugin name
1042 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1043 */
1044 public static Object getPlugin(String name) {
1045 for (PluginProxy plugin : pluginList)
1046 if (plugin.getPluginInformation().name.equals(name))
1047 return plugin.plugin;
1048 return null;
1049 }
1050
1051 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1052 for (PluginProxy p : pluginList) {
1053 p.addDownloadSelection(downloadSelections);
1054 }
1055 }
1056
1057 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1058 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1059 for (PluginProxy plugin : pluginList) {
1060 settings.add(new PluginPreferenceFactory(plugin));
1061 }
1062 return settings;
1063 }
1064
1065 /**
1066 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1067 * ".jar" files.
1068 *
1069 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1070 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1071 * installation of the respective plugin is silently skipped.
1072 *
1073 * @param dowarn if true, warning messages are displayed; false otherwise
1074 */
1075 public static void installDownloadedPlugins(boolean dowarn) {
1076 File pluginDir = Main.pref.getPluginsDirectory();
1077 if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite())
1078 return;
1079
1080 final File[] files = pluginDir.listFiles(new FilenameFilter() {
1081 @Override
1082 public boolean accept(File dir, String name) {
1083 return name.endsWith(".jar.new");
1084 }});
1085 if (files == null)
1086 return;
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 .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name))
1197 .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 .append("<br>");
1201 }
1202 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1203 .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 List <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(Locale.ENGLISH);
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.