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

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

fix javadoc errors/warnings seen with JDK9

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