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

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

deprecate openstreetbugs plugin (replaced by notes)

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