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

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

see #11468, #11518: missed another dummy plugin: RemoveRedundantPoints ==> unmaintained

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