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

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

sonar - squid:S2259 - Null pointers should not be dereferenced

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