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

Last change on this file since 7248 was 7048, checked in by Don-vip, 10 years ago

sonar - Throwable and Error classes should not be caught

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