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