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