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