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