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