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

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

sonar - Performance - Method passes constant String of length 1 to character overridden method + add unit tests/javadoc

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