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

Last change on this file since 10965 was 10963, checked in by stoecker, 8 years ago

don't reference plugins already included in core

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