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

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

minor code style issues

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