001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Component; 009import java.awt.Font; 010import java.awt.GraphicsEnvironment; 011import java.awt.GridBagConstraints; 012import java.awt.GridBagLayout; 013import java.awt.Insets; 014import java.awt.event.ActionEvent; 015import java.io.File; 016import java.io.FilenameFilter; 017import java.io.IOException; 018import java.net.URL; 019import java.net.URLClassLoader; 020import java.security.AccessController; 021import java.security.PrivilegedAction; 022import java.util.ArrayList; 023import java.util.Arrays; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.Comparator; 027import java.util.HashMap; 028import java.util.HashSet; 029import java.util.Iterator; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Locale; 033import java.util.Map; 034import java.util.Map.Entry; 035import java.util.Set; 036import java.util.TreeSet; 037import java.util.concurrent.Callable; 038import java.util.concurrent.ExecutionException; 039import java.util.concurrent.FutureTask; 040import java.util.jar.JarFile; 041 042import javax.swing.AbstractAction; 043import javax.swing.BorderFactory; 044import javax.swing.Box; 045import javax.swing.JButton; 046import javax.swing.JCheckBox; 047import javax.swing.JLabel; 048import javax.swing.JOptionPane; 049import javax.swing.JPanel; 050import javax.swing.JScrollPane; 051import javax.swing.UIManager; 052 053import org.openstreetmap.josm.Main; 054import org.openstreetmap.josm.actions.RestartAction; 055import org.openstreetmap.josm.data.Version; 056import org.openstreetmap.josm.gui.HelpAwareOptionPane; 057import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 058import org.openstreetmap.josm.gui.download.DownloadSelection; 059import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 060import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 061import org.openstreetmap.josm.gui.progress.ProgressMonitor; 062import org.openstreetmap.josm.gui.util.GuiHelper; 063import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 064import org.openstreetmap.josm.gui.widgets.JosmTextArea; 065import org.openstreetmap.josm.io.OfflineAccessException; 066import org.openstreetmap.josm.io.OnlineResource; 067import org.openstreetmap.josm.tools.GBC; 068import org.openstreetmap.josm.tools.I18n; 069import org.openstreetmap.josm.tools.ImageProvider; 070import org.openstreetmap.josm.tools.Utils; 071 072/** 073 * PluginHandler is basically a collection of static utility functions used to bootstrap 074 * and manage the loaded plugins. 075 * @since 1326 076 */ 077public final class PluginHandler { 078 079 /** 080 * Deprecated plugins that are removed on start 081 */ 082 static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS; 083 static { 084 String inCore = tr("integrated into main program"); 085 086 DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] { 087 new DeprecatedPlugin("mappaint", inCore), 088 new DeprecatedPlugin("unglueplugin", inCore), 089 new DeprecatedPlugin("lang-de", inCore), 090 new DeprecatedPlugin("lang-en_GB", inCore), 091 new DeprecatedPlugin("lang-fr", inCore), 092 new DeprecatedPlugin("lang-it", inCore), 093 new DeprecatedPlugin("lang-pl", inCore), 094 new DeprecatedPlugin("lang-ro", inCore), 095 new DeprecatedPlugin("lang-ru", inCore), 096 new DeprecatedPlugin("ewmsplugin", inCore), 097 new DeprecatedPlugin("ywms", inCore), 098 new DeprecatedPlugin("tways-0.2", inCore), 099 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 & 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}