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