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

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

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