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