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

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

see #11390 - use functional comparators

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