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