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

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

fix #8885 (see #4614) - add offline mode with new command line argument --offline which can take one of several of these values (comma separated):

  • josm_website: to disable all accesses to JOSM website (when not cached, disables Getting Started page, help, plugin list, styles, imagery, presets, rules)
  • osm_api: to disable all accesses to OSM API (disables download, upload, changeset queries, history, user message notification)
  • all: alias to disable all values. Currently equivalent to "josm_website,osm_api"

Plus improved javadoc, fixed EDT violations, and fixed a bug with HTTP redirection sent without "Location" header

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