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