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

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

fix remaining checkstyle issues

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