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

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

see #11390 - Java 8: use List.sort(Comparator) instead of Collections.sort(list, Comparator)

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