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

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

see #8465 - use diamond operator where applicable

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