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