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

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

better handling of HelpAwareOptionPane.showOptionDialog in headless mode

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