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

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

fix unit test (forgot data file + headless mode)

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