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

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

sonar - squid:S2221 - "Exception" should not be caught when not required by called methods

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