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

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

deprecate commons-imaging plugin in favour of apache-commons + fix bug in plugin information update from jar seen in unit test

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