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

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

fix #13254 - Deprecate proj4j plugin

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