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

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

see #10930 - deprecate notes plugin

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