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

Last change on this file since 11356 was 11345, checked in by simon04, 7 years ago

fix #14059 - Unmaintained plugins: NanoLog, irsrectify, surveyor2

  • Property svn:eol-style set to native
File size: 62.8 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 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
894 String plugin = it.next();
895 if (infos.containsKey(plugin)) {
896 ret.add(infos.get(plugin));
897 it.remove();
898 }
899 }
900 if (!plugins.isEmpty()) {
901 alertMissingPluginInformation(parent, plugins);
902 }
903 return ret;
904 } finally {
905 monitor.finishTask();
906 }
907 }
908
909 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
910 StringBuilder sb = new StringBuilder(128);
911 sb.append("<html>")
912 .append(trn(
913 "Updating the following plugin has failed:",
914 "Updating the following plugins has failed:",
915 plugins.size()))
916 .append("<ul>");
917 for (PluginInformation pi: plugins) {
918 sb.append("<li>").append(pi.name).append("</li>");
919 }
920 sb.append("</ul>")
921 .append(trn(
922 "Please open the Preference Dialog after JOSM has started and try to update it manually.",
923 "Please open the Preference Dialog after JOSM has started and try to update them manually.",
924 plugins.size()))
925 .append("</html>");
926 HelpAwareOptionPane.showOptionDialog(
927 parent,
928 sb.toString(),
929 tr("Plugin update failed"),
930 JOptionPane.ERROR_MESSAGE,
931 ht("/Plugin/Loading#FailedPluginUpdated")
932 );
933 }
934
935 private static Set<PluginInformation> findRequiredPluginsToDownload(
936 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
937 Set<PluginInformation> result = new HashSet<>();
938 for (PluginInformation pi : pluginsToUpdate) {
939 for (String name : pi.getRequiredPlugins()) {
940 try {
941 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
942 if (installedPlugin == null) {
943 // New required plugin is not installed, find its PluginInformation
944 PluginInformation reqPlugin = null;
945 for (PluginInformation pi2 : allPlugins) {
946 if (pi2.getName().equals(name)) {
947 reqPlugin = pi2;
948 break;
949 }
950 }
951 // Required plugin is known but not already on download list
952 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
953 result.add(reqPlugin);
954 }
955 }
956 } catch (PluginException e) {
957 Main.warn(tr("Failed to find plugin {0}", name));
958 Main.error(e);
959 }
960 }
961 }
962 return result;
963 }
964
965 /**
966 * Updates the plugins in <code>plugins</code>.
967 *
968 * @param parent the parent component for message boxes
969 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
970 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
971 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
972 * @return the list of plugins to load
973 * @throws IllegalArgumentException if plugins is null
974 */
975 public static Collection<PluginInformation> updatePlugins(Component parent,
976 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
977 Collection<PluginInformation> plugins = null;
978 pluginDownloadTask = null;
979 if (monitor == null) {
980 monitor = NullProgressMonitor.INSTANCE;
981 }
982 try {
983 monitor.beginTask("");
984
985 // try to download the plugin lists
986 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
987 monitor.createSubTaskMonitor(1, false),
988 Main.pref.getOnlinePluginSites(), displayErrMsg
989 );
990 task1.run();
991 List<PluginInformation> allPlugins = task1.getAvailablePlugins();
992
993 try {
994 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
995 // If only some plugins have to be updated, filter the list
996 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
997 final Collection<String> pluginsWantedName = Utils.transform(pluginsWanted, piw -> piw.name);
998 plugins = SubclassFilteredCollection.filter(plugins, pi -> pluginsWantedName.contains(pi.name));
999 }
1000 } catch (RuntimeException e) {
1001 Main.warn(tr("Failed to download plugin information list"));
1002 Main.error(e);
1003 // don't abort in case of error, continue with downloading plugins below
1004 }
1005
1006 // filter plugins which actually have to be updated
1007 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
1008 if (plugins != null) {
1009 for (PluginInformation pi: plugins) {
1010 if (pi.isUpdateRequired()) {
1011 pluginsToUpdate.add(pi);
1012 }
1013 }
1014 }
1015
1016 if (!pluginsToUpdate.isEmpty()) {
1017
1018 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
1019
1020 if (allPlugins != null) {
1021 // Updated plugins may need additional plugin dependencies currently not installed
1022 //
1023 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
1024 pluginsToDownload.addAll(additionalPlugins);
1025
1026 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
1027 while (!additionalPlugins.isEmpty()) {
1028 // Install the additional plugins to load them later
1029 if (plugins != null)
1030 plugins.addAll(additionalPlugins);
1031 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
1032 pluginsToDownload.addAll(additionalPlugins);
1033 }
1034 }
1035
1036 // try to update the locally installed plugins
1037 pluginDownloadTask = new PluginDownloadTask(
1038 monitor.createSubTaskMonitor(1, false),
1039 pluginsToDownload,
1040 tr("Update plugins")
1041 );
1042
1043 try {
1044 pluginDownloadTask.run();
1045 } catch (RuntimeException e) {
1046 Main.error(e);
1047 alertFailedPluginUpdate(parent, pluginsToUpdate);
1048 return plugins;
1049 }
1050
1051 // Update Plugin info for downloaded plugins
1052 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
1053
1054 // notify user if downloading a locally installed plugin failed
1055 if (!pluginDownloadTask.getFailedPlugins().isEmpty()) {
1056 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
1057 return plugins;
1058 }
1059 }
1060 } finally {
1061 monitor.finishTask();
1062 }
1063 if (pluginsWanted == null) {
1064 // if all plugins updated, remember the update because it was successful
1065 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1066 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1067 }
1068 return plugins;
1069 }
1070
1071 /**
1072 * Ask the user for confirmation that a plugin shall be disabled.
1073 *
1074 * @param parent The parent component to be used for the displayed dialog
1075 * @param reason the reason for disabling the plugin
1076 * @param name the plugin name
1077 * @return true, if the plugin shall be disabled; false, otherwise
1078 */
1079 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1080 ButtonSpec[] options = new ButtonSpec[] {
1081 new ButtonSpec(
1082 tr("Disable plugin"),
1083 ImageProvider.get("dialogs", "delete"),
1084 tr("Click to delete the plugin ''{0}''", name),
1085 null /* no specific help context */
1086 ),
1087 new ButtonSpec(
1088 tr("Keep plugin"),
1089 ImageProvider.get("cancel"),
1090 tr("Click to keep the plugin ''{0}''", name),
1091 null /* no specific help context */
1092 )
1093 };
1094 return 0 == HelpAwareOptionPane.showOptionDialog(
1095 parent,
1096 reason,
1097 tr("Disable plugin"),
1098 JOptionPane.WARNING_MESSAGE,
1099 null,
1100 options,
1101 options[0],
1102 null // FIXME: add help topic
1103 );
1104 }
1105
1106 /**
1107 * Returns the plugin of the specified name.
1108 * @param name The plugin name
1109 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1110 */
1111 public static Object getPlugin(String name) {
1112 for (PluginProxy plugin : pluginList) {
1113 if (plugin.getPluginInformation().name.equals(name))
1114 return plugin.plugin;
1115 }
1116 return null;
1117 }
1118
1119 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1120 for (PluginProxy p : pluginList) {
1121 p.addDownloadSelection(downloadSelections);
1122 }
1123 }
1124
1125 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1126 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1127 for (PluginProxy plugin : pluginList) {
1128 settings.add(new PluginPreferenceFactory(plugin));
1129 }
1130 return settings;
1131 }
1132
1133 /**
1134 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1135 * ".jar" files.
1136 *
1137 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1138 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1139 * installation of the respective plugin is silently skipped.
1140 *
1141 * @param dowarn if true, warning messages are displayed; false otherwise
1142 */
1143 public static void installDownloadedPlugins(boolean dowarn) {
1144 File pluginDir = Main.pref.getPluginsDirectory();
1145 if (!pluginDir.exists() || !pluginDir.isDirectory() || !pluginDir.canWrite())
1146 return;
1147
1148 final File[] files = pluginDir.listFiles((FilenameFilter) (dir, name) -> name.endsWith(".jar.new"));
1149 if (files == null)
1150 return;
1151
1152 for (File updatedPlugin : files) {
1153 final String filePath = updatedPlugin.getPath();
1154 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1155 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1156 if (plugin.exists() && !plugin.delete() && dowarn) {
1157 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1158 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1159 "Skipping installation. JOSM is still going to load the old plugin version.",
1160 pluginName));
1161 continue;
1162 }
1163 try {
1164 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1165 new JarFile(updatedPlugin).close();
1166 } catch (IOException e) {
1167 if (dowarn) {
1168 Main.warn(e, tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}",
1169 plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1170 }
1171 continue;
1172 }
1173 // Install plugin
1174 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1175 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.",
1176 plugin.toString(), updatedPlugin.toString()));
1177 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1178 "Skipping installation. JOSM is still going to load the old plugin version.",
1179 pluginName));
1180 }
1181 }
1182 }
1183
1184 /**
1185 * Determines if the specified file is a valid and accessible JAR file.
1186 * @param jar The file to check
1187 * @return true if file can be opened as a JAR file.
1188 * @since 5723
1189 */
1190 public static boolean isValidJar(File jar) {
1191 if (jar != null && jar.exists() && jar.canRead()) {
1192 try {
1193 new JarFile(jar).close();
1194 } catch (IOException e) {
1195 Main.warn(e);
1196 return false;
1197 }
1198 return true;
1199 } else if (jar != null) {
1200 Main.warn("Invalid jar file ''"+jar+"'' (exists: "+jar.exists()+", canRead: "+jar.canRead()+')');
1201 }
1202 return false;
1203 }
1204
1205 /**
1206 * Replies the updated jar file for the given plugin name.
1207 * @param name The plugin name to find.
1208 * @return the updated jar file for the given plugin name. null if not found or not readable.
1209 * @since 5601
1210 */
1211 public static File findUpdatedJar(String name) {
1212 File pluginDir = Main.pref.getPluginsDirectory();
1213 // Find the downloaded file. We have tried to install the downloaded plugins
1214 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1215 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1216 if (!isValidJar(downloadedPluginFile)) {
1217 downloadedPluginFile = new File(pluginDir, name + ".jar");
1218 if (!isValidJar(downloadedPluginFile)) {
1219 return null;
1220 }
1221 }
1222 return downloadedPluginFile;
1223 }
1224
1225 /**
1226 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1227 * @param updatedPlugins The PluginInformation objects to update.
1228 * @since 5601
1229 */
1230 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1231 if (updatedPlugins == null) return;
1232 for (PluginInformation pi : updatedPlugins) {
1233 File downloadedPluginFile = findUpdatedJar(pi.name);
1234 if (downloadedPluginFile == null) {
1235 continue;
1236 }
1237 try {
1238 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1239 } catch (PluginException e) {
1240 Main.error(e);
1241 }
1242 }
1243 }
1244
1245 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1246 final ButtonSpec[] options = new ButtonSpec[] {
1247 new ButtonSpec(
1248 tr("Update plugin"),
1249 ImageProvider.get("dialogs", "refresh"),
1250 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1251 null /* no specific help context */
1252 ),
1253 new ButtonSpec(
1254 tr("Disable plugin"),
1255 ImageProvider.get("dialogs", "delete"),
1256 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1257 null /* no specific help context */
1258 ),
1259 new ButtonSpec(
1260 tr("Keep plugin"),
1261 ImageProvider.get("cancel"),
1262 tr("Click to keep the plugin ''{0}''", plugin.getPluginInformation().name),
1263 null /* no specific help context */
1264 )
1265 };
1266
1267 final StringBuilder msg = new StringBuilder(256);
1268 msg.append("<html>")
1269 .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name))
1270 .append("<br>");
1271 if (plugin.getPluginInformation().author != null) {
1272 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author))
1273 .append("<br>");
1274 }
1275 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1276 .append("</html>");
1277
1278 try {
1279 FutureTask<Integer> task = new FutureTask<>(() -> HelpAwareOptionPane.showOptionDialog(
1280 Main.parent,
1281 msg.toString(),
1282 tr("Update plugins"),
1283 JOptionPane.QUESTION_MESSAGE,
1284 null,
1285 options,
1286 options[0],
1287 ht("/ErrorMessages#ErrorInPlugin")
1288 ));
1289 GuiHelper.runInEDT(task);
1290 return task.get();
1291 } catch (InterruptedException | ExecutionException e) {
1292 Main.warn(e);
1293 }
1294 return -1;
1295 }
1296
1297 /**
1298 * Replies the plugin which most likely threw the exception <code>ex</code>.
1299 *
1300 * @param ex the exception
1301 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1302 */
1303 private static PluginProxy getPluginCausingException(Throwable ex) {
1304 PluginProxy err = null;
1305 StackTraceElement[] stack = ex.getStackTrace();
1306 // remember the error position, as multiple plugins may be involved, we search the topmost one
1307 int pos = stack.length;
1308 for (PluginProxy p : pluginList) {
1309 String baseClass = p.getPluginInformation().className;
1310 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1311 for (int elpos = 0; elpos < pos; ++elpos) {
1312 if (stack[elpos].getClassName().startsWith(baseClass)) {
1313 pos = elpos;
1314 err = p;
1315 }
1316 }
1317 }
1318 return err;
1319 }
1320
1321 /**
1322 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1323 * conditionally updates or deactivates the plugin, but asks the user first.
1324 *
1325 * @param e the exception
1326 * @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
1327 */
1328 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1329 PluginProxy plugin = null;
1330 // Check for an explicit problem when calling a plugin function
1331 if (e instanceof PluginException) {
1332 plugin = ((PluginException) e).plugin;
1333 }
1334 if (plugin == null) {
1335 plugin = getPluginCausingException(e);
1336 }
1337 if (plugin == null)
1338 // don't know what plugin threw the exception
1339 return null;
1340
1341 Set<String> plugins = new HashSet<>(
1342 Main.pref.getCollection("plugins", Collections.<String>emptySet())
1343 );
1344 final PluginInformation pluginInfo = plugin.getPluginInformation();
1345 if (!plugins.contains(pluginInfo.name))
1346 // plugin not activated ? strange in this context but anyway, don't bother
1347 // the user with dialogs, skip conditional deactivation
1348 return null;
1349
1350 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1351 case 0:
1352 // update the plugin
1353 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1354 return pluginDownloadTask;
1355 case 1:
1356 // deactivate the plugin
1357 plugins.remove(plugin.getPluginInformation().name);
1358 Main.pref.putCollection("plugins", plugins);
1359 GuiHelper.runInEDTAndWait(() -> JOptionPane.showMessageDialog(
1360 Main.parent,
1361 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1362 tr("Information"),
1363 JOptionPane.INFORMATION_MESSAGE
1364 ));
1365 return null;
1366 default:
1367 // user doesn't want to deactivate the plugin
1368 return null;
1369 }
1370 }
1371
1372 /**
1373 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1374 * @return The list of loaded plugins
1375 */
1376 public static Collection<String> getBugReportInformation() {
1377 final Collection<String> pl = new TreeSet<>(Main.pref.getCollection("plugins", new LinkedList<>()));
1378 for (final PluginProxy pp : pluginList) {
1379 PluginInformation pi = pp.getPluginInformation();
1380 pl.remove(pi.name);
1381 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1382 ? pi.localversion : "unknown") + ')');
1383 }
1384 return pl;
1385 }
1386
1387 /**
1388 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1389 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1390 */
1391 public static JPanel getInfoPanel() {
1392 JPanel pluginTab = new JPanel(new GridBagLayout());
1393 for (final PluginProxy p : pluginList) {
1394 final PluginInformation info = p.getPluginInformation();
1395 String name = info.name
1396 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1397 pluginTab.add(new JLabel(name), GBC.std());
1398 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1399 pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1400 @Override
1401 public void actionPerformed(ActionEvent event) {
1402 StringBuilder b = new StringBuilder();
1403 for (Entry<String, String> e : info.attr.entrySet()) {
1404 b.append(e.getKey());
1405 b.append(": ");
1406 b.append(e.getValue());
1407 b.append('\n');
1408 }
1409 JosmTextArea a = new JosmTextArea(10, 40);
1410 a.setEditable(false);
1411 a.setText(b.toString());
1412 a.setCaretPosition(0);
1413 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1414 JOptionPane.INFORMATION_MESSAGE);
1415 }
1416 }), GBC.eol());
1417
1418 JosmTextArea description = new JosmTextArea(info.description == null ? tr("no description available")
1419 : info.description);
1420 description.setEditable(false);
1421 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1422 description.setLineWrap(true);
1423 description.setWrapStyleWord(true);
1424 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1425 description.setBackground(UIManager.getColor("Panel.background"));
1426 description.setCaretPosition(0);
1427
1428 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1429 }
1430 return pluginTab;
1431 }
1432
1433 /**
1434 * Returns the set of deprecated and unmaintained plugins.
1435 * @return set of deprecated and unmaintained plugins names.
1436 * @since 8938
1437 */
1438 public static Set<String> getDeprecatedAndUnmaintainedPlugins() {
1439 Set<String> result = new HashSet<>(DEPRECATED_PLUGINS.size() + UNMAINTAINED_PLUGINS.size());
1440 for (DeprecatedPlugin dp : DEPRECATED_PLUGINS) {
1441 result.add(dp.name);
1442 }
1443 result.addAll(UNMAINTAINED_PLUGINS);
1444 return result;
1445 }
1446
1447 private static class UpdatePluginsMessagePanel extends JPanel {
1448 private final JMultilineLabel lblMessage = new JMultilineLabel("");
1449 private final JCheckBox cbDontShowAgain = new JCheckBox(
1450 tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)"));
1451
1452 UpdatePluginsMessagePanel() {
1453 build();
1454 }
1455
1456 protected final void build() {
1457 setLayout(new GridBagLayout());
1458 GridBagConstraints gc = new GridBagConstraints();
1459 gc.anchor = GridBagConstraints.NORTHWEST;
1460 gc.fill = GridBagConstraints.BOTH;
1461 gc.weightx = 1.0;
1462 gc.weighty = 1.0;
1463 gc.insets = new Insets(5, 5, 5, 5);
1464 add(lblMessage, gc);
1465 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1466
1467 gc.gridy = 1;
1468 gc.fill = GridBagConstraints.HORIZONTAL;
1469 gc.weighty = 0.0;
1470 add(cbDontShowAgain, gc);
1471 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1472 }
1473
1474 public void setMessage(String message) {
1475 lblMessage.setText(message);
1476 }
1477
1478 public void initDontShowAgain(String preferencesKey) {
1479 String policy = Main.pref.get(preferencesKey, "ask");
1480 policy = policy.trim().toLowerCase(Locale.ENGLISH);
1481 cbDontShowAgain.setSelected(!"ask".equals(policy));
1482 }
1483
1484 public boolean isRememberDecision() {
1485 return cbDontShowAgain.isSelected();
1486 }
1487 }
1488}
Note: See TracBrowser for help on using the repository browser.