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

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

josm-geojson plugin renamed to geojson

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