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

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

simplify handling of ignored/traced exceptions

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