001// License: GPL. For details, see LICENSE file. 002package org.openstreetmap.josm.plugins; 003 004import static org.openstreetmap.josm.gui.help.HelpUtil.ht; 005import static org.openstreetmap.josm.tools.I18n.tr; 006import static org.openstreetmap.josm.tools.I18n.trn; 007 008import java.awt.Component; 009import java.awt.Font; 010import java.awt.GridBagConstraints; 011import java.awt.GridBagLayout; 012import java.awt.Insets; 013import java.awt.event.ActionEvent; 014import java.io.File; 015import java.io.FilenameFilter; 016import java.net.URL; 017import java.net.URLClassLoader; 018import java.security.AccessController; 019import java.security.PrivilegedAction; 020import java.util.ArrayList; 021import java.util.Arrays; 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.HashMap; 026import java.util.HashSet; 027import java.util.Iterator; 028import java.util.LinkedList; 029import java.util.List; 030import java.util.Map; 031import java.util.Map.Entry; 032import java.util.Set; 033import java.util.TreeSet; 034import java.util.concurrent.Callable; 035import java.util.concurrent.ExecutionException; 036import java.util.concurrent.ExecutorService; 037import java.util.concurrent.Executors; 038import java.util.concurrent.Future; 039import java.util.concurrent.FutureTask; 040import java.util.jar.JarFile; 041 042import javax.swing.AbstractAction; 043import javax.swing.BorderFactory; 044import javax.swing.Box; 045import javax.swing.JButton; 046import javax.swing.JCheckBox; 047import javax.swing.JLabel; 048import javax.swing.JOptionPane; 049import javax.swing.JPanel; 050import javax.swing.JScrollPane; 051import javax.swing.UIManager; 052 053import org.openstreetmap.josm.Main; 054import org.openstreetmap.josm.data.Version; 055import org.openstreetmap.josm.gui.HelpAwareOptionPane; 056import org.openstreetmap.josm.gui.HelpAwareOptionPane.ButtonSpec; 057import org.openstreetmap.josm.gui.download.DownloadSelection; 058import org.openstreetmap.josm.gui.help.HelpUtil; 059import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory; 060import org.openstreetmap.josm.gui.progress.NullProgressMonitor; 061import org.openstreetmap.josm.gui.progress.ProgressMonitor; 062import org.openstreetmap.josm.gui.util.GuiHelper; 063import org.openstreetmap.josm.gui.widgets.JMultilineLabel; 064import org.openstreetmap.josm.gui.widgets.JosmTextArea; 065import org.openstreetmap.josm.io.OfflineAccessException; 066import org.openstreetmap.josm.io.OnlineResource; 067import org.openstreetmap.josm.tools.GBC; 068import org.openstreetmap.josm.tools.I18n; 069import org.openstreetmap.josm.tools.ImageProvider; 070import org.openstreetmap.josm.tools.Utils; 071 072/** 073 * PluginHandler is basically a collection of static utility functions used to bootstrap 074 * and manage the loaded plugins. 075 * @since 1326 076 */ 077public final class PluginHandler { 078 079 /** 080 * Deprecated plugins that are removed on start 081 */ 082 public static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS; 083 static { 084 String IN_CORE = tr("integrated into main program"); 085 086 DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] { 087 new DeprecatedPlugin("mappaint", IN_CORE), 088 new DeprecatedPlugin("unglueplugin", IN_CORE), 089 new DeprecatedPlugin("lang-de", IN_CORE), 090 new DeprecatedPlugin("lang-en_GB", IN_CORE), 091 new DeprecatedPlugin("lang-fr", IN_CORE), 092 new DeprecatedPlugin("lang-it", IN_CORE), 093 new DeprecatedPlugin("lang-pl", IN_CORE), 094 new DeprecatedPlugin("lang-ro", IN_CORE), 095 new DeprecatedPlugin("lang-ru", IN_CORE), 096 new DeprecatedPlugin("ewmsplugin", IN_CORE), 097 new DeprecatedPlugin("ywms", IN_CORE), 098 new DeprecatedPlugin("tways-0.2", IN_CORE), 099 new DeprecatedPlugin("geotagged", IN_CORE), 100 new DeprecatedPlugin("landsat", tr("replaced by new {0} plugin","lakewalker")), 101 new DeprecatedPlugin("namefinder", IN_CORE), 102 new DeprecatedPlugin("waypoints", IN_CORE), 103 new DeprecatedPlugin("slippy_map_chooser", IN_CORE), 104 new DeprecatedPlugin("tcx-support", tr("replaced by new {0} plugin","dataimport")), 105 new DeprecatedPlugin("usertools", IN_CORE), 106 new DeprecatedPlugin("AgPifoJ", IN_CORE), 107 new DeprecatedPlugin("utilsplugin", IN_CORE), 108 new DeprecatedPlugin("ghost", IN_CORE), 109 new DeprecatedPlugin("validator", IN_CORE), 110 new DeprecatedPlugin("multipoly", IN_CORE), 111 new DeprecatedPlugin("multipoly-convert", IN_CORE), 112 new DeprecatedPlugin("remotecontrol", IN_CORE), 113 new DeprecatedPlugin("imagery", IN_CORE), 114 new DeprecatedPlugin("slippymap", IN_CORE), 115 new DeprecatedPlugin("wmsplugin", IN_CORE), 116 new DeprecatedPlugin("ParallelWay", IN_CORE), 117 new DeprecatedPlugin("dumbutils", tr("replaced by new {0} plugin","utilsplugin2")), 118 new DeprecatedPlugin("ImproveWayAccuracy", IN_CORE), 119 new DeprecatedPlugin("Curves", tr("replaced by new {0} plugin","utilsplugin2")), 120 new DeprecatedPlugin("epsg31287", tr("replaced by new {0} plugin", "proj4j")), 121 new DeprecatedPlugin("licensechange", tr("no longer required")), 122 new DeprecatedPlugin("restart", IN_CORE), 123 new DeprecatedPlugin("wayselector", IN_CORE), 124 new DeprecatedPlugin("openstreetbugs", tr("replaced by new {0} plugin", "notes")), 125 new DeprecatedPlugin("nearclick", tr("no longer required")), 126 }); 127 } 128 129 private PluginHandler() { 130 // Hide default constructor for utils classes 131 } 132 133 /** 134 * Description of a deprecated plugin 135 */ 136 public static class DeprecatedPlugin implements Comparable<DeprecatedPlugin> { 137 /** Plugin name */ 138 public final String name; 139 /** Short explanation about deprecation, can be {@code null} */ 140 public final String reason; 141 /** Code to run to perform migration, can be {@code null} */ 142 private final Runnable migration; 143 144 /** 145 * Constructs a new {@code DeprecatedPlugin}. 146 * @param name The plugin name 147 */ 148 public DeprecatedPlugin(String name) { 149 this(name, null, null); 150 } 151 152 /** 153 * Constructs a new {@code DeprecatedPlugin} with a given reason. 154 * @param name The plugin name 155 * @param reason The reason about deprecation 156 */ 157 public DeprecatedPlugin(String name, String reason) { 158 this(name, reason, null); 159 } 160 161 /** 162 * Constructs a new {@code DeprecatedPlugin}. 163 * @param name The plugin name 164 * @param reason The reason about deprecation 165 * @param migration The code to run to perform migration 166 */ 167 public DeprecatedPlugin(String name, String reason, Runnable migration) { 168 this.name = name; 169 this.reason = reason; 170 this.migration = migration; 171 } 172 173 /** 174 * Performs migration. 175 */ 176 public void migrate() { 177 if (migration != null) { 178 migration.run(); 179 } 180 } 181 182 @Override 183 public int compareTo(DeprecatedPlugin o) { 184 return name.compareTo(o.name); 185 } 186 } 187 188 /** 189 * List of unmaintained plugins. Not really up-to-date as the vast majority of plugins are not really maintained after a few months, sadly... 190 */ 191 private static final String [] UNMAINTAINED_PLUGINS = new String[] {"gpsbabelgui", "Intersect_way"}; 192 193 /** 194 * Default time-based update interval, in days (pluginmanager.time-based-update.interval) 195 */ 196 public static final int DEFAULT_TIME_BASED_UPDATE_INTERVAL = 30; 197 198 /** 199 * All installed and loaded plugins (resp. their main classes) 200 */ 201 public static final Collection<PluginProxy> pluginList = new LinkedList<>(); 202 203 /** 204 * Add here all ClassLoader whose resource should be searched. 205 */ 206 private static final List<ClassLoader> sources = new LinkedList<>(); 207 208 static { 209 try { 210 sources.add(ClassLoader.getSystemClassLoader()); 211 sources.add(org.openstreetmap.josm.gui.MainApplication.class.getClassLoader()); 212 } catch (SecurityException ex) { 213 sources.add(ImageProvider.class.getClassLoader()); 214 } 215 } 216 217 private static PluginDownloadTask pluginDownloadTask = null; 218 219 public static Collection<ClassLoader> getResourceClassLoaders() { 220 return Collections.unmodifiableCollection(sources); 221 } 222 223 /** 224 * Removes deprecated plugins from a collection of plugins. Modifies the 225 * collection <code>plugins</code>. 226 * 227 * Also notifies the user about removed deprecated plugins 228 * 229 * @param parent The parent Component used to display warning popup 230 * @param plugins the collection of plugins 231 */ 232 private static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) { 233 Set<DeprecatedPlugin> removedPlugins = new TreeSet<>(); 234 for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) { 235 if (plugins.contains(depr.name)) { 236 plugins.remove(depr.name); 237 Main.pref.removeFromCollection("plugins", depr.name); 238 removedPlugins.add(depr); 239 depr.migrate(); 240 } 241 } 242 if (removedPlugins.isEmpty()) 243 return; 244 245 // notify user about removed deprecated plugins 246 // 247 StringBuilder sb = new StringBuilder(); 248 sb.append("<html>"); 249 sb.append(trn( 250 "The following plugin is no longer necessary and has been deactivated:", 251 "The following plugins are no longer necessary and have been deactivated:", 252 removedPlugins.size() 253 )); 254 sb.append("<ul>"); 255 for (DeprecatedPlugin depr: removedPlugins) { 256 sb.append("<li>").append(depr.name); 257 if (depr.reason != null) { 258 sb.append(" (").append(depr.reason).append(")"); 259 } 260 sb.append("</li>"); 261 } 262 sb.append("</ul>"); 263 sb.append("</html>"); 264 JOptionPane.showMessageDialog( 265 parent, 266 sb.toString(), 267 tr("Warning"), 268 JOptionPane.WARNING_MESSAGE 269 ); 270 } 271 272 /** 273 * Removes unmaintained plugins from a collection of plugins. Modifies the 274 * collection <code>plugins</code>. Also removes the plugin from the list 275 * of plugins in the preferences, if necessary. 276 * 277 * Asks the user for every unmaintained plugin whether it should be removed. 278 * 279 * @param plugins the collection of plugins 280 */ 281 private static void filterUnmaintainedPlugins(Component parent, Collection<String> plugins) { 282 for (String unmaintained : UNMAINTAINED_PLUGINS) { 283 if (!plugins.contains(unmaintained)) { 284 continue; 285 } 286 String msg = tr("<html>Loading of the plugin \"{0}\" was requested." 287 + "<br>This plugin is no longer developed and very likely will produce errors." 288 +"<br>It should be disabled.<br>Delete from preferences?</html>", unmaintained); 289 if (confirmDisablePlugin(parent, msg,unmaintained)) { 290 Main.pref.removeFromCollection("plugins", unmaintained); 291 plugins.remove(unmaintained); 292 } 293 } 294 } 295 296 /** 297 * Checks whether the locally available plugins should be updated and 298 * asks the user if running an update is OK. An update is advised if 299 * JOSM was updated to a new version since the last plugin updates or 300 * if the plugins were last updated a long time ago. 301 * 302 * @param parent the parent component relative to which the confirmation dialog 303 * is to be displayed 304 * @return true if a plugin update should be run; false, otherwise 305 */ 306 public static boolean checkAndConfirmPluginUpdate(Component parent) { 307 if (!checkOfflineAccess()) { 308 Main.info(tr("{0} not available (offline mode)", tr("Plugin update"))); 309 return false; 310 } 311 String message = null; 312 String togglePreferenceKey = null; 313 int v = Version.getInstance().getVersion(); 314 if (Main.pref.getInteger("pluginmanager.version", 0) < v) { 315 message = 316 "<html>" 317 + tr("You updated your JOSM software.<br>" 318 + "To prevent problems the plugins should be updated as well.<br><br>" 319 + "Update plugins now?" 320 ) 321 + "</html>"; 322 togglePreferenceKey = "pluginmanager.version-based-update.policy"; 323 } else { 324 long tim = System.currentTimeMillis(); 325 long last = Main.pref.getLong("pluginmanager.lastupdate", 0); 326 Integer maxTime = Main.pref.getInteger("pluginmanager.time-based-update.interval", DEFAULT_TIME_BASED_UPDATE_INTERVAL); 327 long d = (tim - last) / (24 * 60 * 60 * 1000L); 328 if ((last <= 0) || (maxTime <= 0)) { 329 Main.pref.put("pluginmanager.lastupdate", Long.toString(tim)); 330 } else if (d > maxTime) { 331 message = 332 "<html>" 333 + tr("Last plugin update more than {0} days ago.", d) 334 + "</html>"; 335 togglePreferenceKey = "pluginmanager.time-based-update.policy"; 336 } 337 } 338 if (message == null) return false; 339 340 ButtonSpec [] options = new ButtonSpec[] { 341 new ButtonSpec( 342 tr("Update plugins"), 343 ImageProvider.get("dialogs", "refresh"), 344 tr("Click to update the activated plugins"), 345 null /* no specific help context */ 346 ), 347 new ButtonSpec( 348 tr("Skip update"), 349 ImageProvider.get("cancel"), 350 tr("Click to skip updating the activated plugins"), 351 null /* no specific help context */ 352 ) 353 }; 354 355 UpdatePluginsMessagePanel pnlMessage = new UpdatePluginsMessagePanel(); 356 pnlMessage.setMessage(message); 357 pnlMessage.initDontShowAgain(togglePreferenceKey); 358 359 // check whether automatic update at startup was disabled 360 // 361 String policy = Main.pref.get(togglePreferenceKey, "ask").trim().toLowerCase(); 362 switch(policy) { 363 case "never": 364 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) { 365 Main.info(tr("Skipping plugin update after JOSM upgrade. Automatic update at startup is disabled.")); 366 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) { 367 Main.info(tr("Skipping plugin update after elapsed update interval. Automatic update at startup is disabled.")); 368 } 369 return false; 370 371 case "always": 372 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) { 373 Main.info(tr("Running plugin update after JOSM upgrade. Automatic update at startup is enabled.")); 374 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) { 375 Main.info(tr("Running plugin update after elapsed update interval. Automatic update at startup is disabled.")); 376 } 377 return true; 378 379 case "ask": 380 break; 381 382 default: 383 Main.warn(tr("Unexpected value ''{0}'' for preference ''{1}''. Assuming value ''ask''.", policy, togglePreferenceKey)); 384 } 385 386 int ret = HelpAwareOptionPane.showOptionDialog( 387 parent, 388 pnlMessage, 389 tr("Update plugins"), 390 JOptionPane.WARNING_MESSAGE, 391 null, 392 options, 393 options[0], 394 ht("/Preferences/Plugins#AutomaticUpdate") 395 ); 396 397 if (pnlMessage.isRememberDecision()) { 398 switch(ret) { 399 case 0: 400 Main.pref.put(togglePreferenceKey, "always"); 401 break; 402 case JOptionPane.CLOSED_OPTION: 403 case 1: 404 Main.pref.put(togglePreferenceKey, "never"); 405 break; 406 } 407 } else { 408 Main.pref.put(togglePreferenceKey, "ask"); 409 } 410 return ret == 0; 411 } 412 413 private static boolean checkOfflineAccess() { 414 if (Main.isOffline(OnlineResource.ALL)) { 415 return false; 416 } 417 if (Main.isOffline(OnlineResource.JOSM_WEBSITE)) { 418 for (String updateSite : Main.pref.getPluginSites()) { 419 try { 420 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(updateSite, Main.getJOSMWebsite()); 421 } catch (OfflineAccessException e) { 422 if (Main.isTraceEnabled()) { 423 Main.trace(e.getMessage()); 424 } 425 return false; 426 } 427 } 428 } 429 return true; 430 } 431 432 /** 433 * Alerts the user if a plugin required by another plugin is missing 434 * 435 * @param parent The parent Component used to display error popup 436 * @param plugin the plugin 437 * @param missingRequiredPlugin the missing required plugin 438 */ 439 private static void alertMissingRequiredPlugin(Component parent, String plugin, Set<String> missingRequiredPlugin) { 440 StringBuilder sb = new StringBuilder(); 441 sb.append("<html>"); 442 sb.append(trn("Plugin {0} requires a plugin which was not found. The missing plugin is:", 443 "Plugin {0} requires {1} plugins which were not found. The missing plugins are:", 444 missingRequiredPlugin.size(), 445 plugin, 446 missingRequiredPlugin.size() 447 )); 448 sb.append(Utils.joinAsHtmlUnorderedList(missingRequiredPlugin)); 449 sb.append("</html>"); 450 JOptionPane.showMessageDialog( 451 parent, 452 sb.toString(), 453 tr("Error"), 454 JOptionPane.ERROR_MESSAGE 455 ); 456 } 457 458 private static void alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) { 459 HelpAwareOptionPane.showOptionDialog( 460 parent, 461 tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>" 462 +"You have to update JOSM in order to use this plugin.</html>", 463 plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString() 464 ), 465 tr("Warning"), 466 JOptionPane.WARNING_MESSAGE, 467 HelpUtil.ht("/Plugin/Loading#JOSMUpdateRequired") 468 ); 469 } 470 471 /** 472 * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The 473 * current JOSM version must be compatible with the plugin and no other plugins this plugin 474 * depends on should be missing. 475 * 476 * @param parent The parent Component used to display error popup 477 * @param plugins the collection of all loaded plugins 478 * @param plugin the plugin for which preconditions are checked 479 * @return true, if the preconditions are met; false otherwise 480 */ 481 public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) { 482 483 // make sure the plugin is compatible with the current JOSM version 484 // 485 int josmVersion = Version.getInstance().getVersion(); 486 if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) { 487 alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion); 488 return false; 489 } 490 491 // Add all plugins already loaded (to include early plugins when checking late ones) 492 Collection<PluginInformation> allPlugins = new HashSet<>(plugins); 493 for (PluginProxy proxy : pluginList) { 494 allPlugins.add(proxy.getPluginInformation()); 495 } 496 497 return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true); 498 } 499 500 /** 501 * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met. 502 * No other plugins this plugin depends on should be missing. 503 * 504 * @param parent The parent Component used to display error popup 505 * @param plugins the collection of all loaded plugins 506 * @param plugin the plugin for which preconditions are checked 507 * @param local Determines if the local or up-to-date plugin dependencies are to be checked. 508 * @return true, if the preconditions are met; false otherwise 509 * @since 5601 510 */ 511 public static boolean checkRequiredPluginsPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin, boolean local) { 512 513 String requires = local ? plugin.localrequires : plugin.requires; 514 515 // make sure the dependencies to other plugins are not broken 516 // 517 if (requires != null) { 518 Set<String> pluginNames = new HashSet<>(); 519 for (PluginInformation pi: plugins) { 520 pluginNames.add(pi.name); 521 } 522 Set<String> missingPlugins = new HashSet<>(); 523 List<String> requiredPlugins = local ? plugin.getLocalRequiredPlugins() : plugin.getRequiredPlugins(); 524 for (String requiredPlugin : requiredPlugins) { 525 if (!pluginNames.contains(requiredPlugin)) { 526 missingPlugins.add(requiredPlugin); 527 } 528 } 529 if (!missingPlugins.isEmpty()) { 530 alertMissingRequiredPlugin(parent, plugin.name, missingPlugins); 531 return false; 532 } 533 } 534 return true; 535 } 536 537 /** 538 * Creates a class loader for loading plugin code. 539 * 540 * @param plugins the collection of plugins which are going to be loaded with this 541 * class loader 542 * @return the class loader 543 */ 544 public static ClassLoader createClassLoader(Collection<PluginInformation> plugins) { 545 // iterate all plugins and collect all libraries of all plugins: 546 List<URL> allPluginLibraries = new LinkedList<>(); 547 File pluginDir = Main.pref.getPluginsDirectory(); 548 549 // Add all plugins already loaded (to include early plugins in the classloader, allowing late plugins to rely on early ones) 550 Collection<PluginInformation> allPlugins = new HashSet<>(plugins); 551 for (PluginProxy proxy : pluginList) { 552 allPlugins.add(proxy.getPluginInformation()); 553 } 554 555 for (PluginInformation info : allPlugins) { 556 if (info.libraries == null) { 557 continue; 558 } 559 allPluginLibraries.addAll(info.libraries); 560 File pluginJar = new File(pluginDir, info.name + ".jar"); 561 I18n.addTexts(pluginJar); 562 URL pluginJarUrl = Utils.fileToURL(pluginJar); 563 allPluginLibraries.add(pluginJarUrl); 564 } 565 566 // create a classloader for all plugins: 567 final URL[] jarUrls = allPluginLibraries.toArray(new URL[allPluginLibraries.size()]); 568 return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() { 569 public ClassLoader run() { 570 return new URLClassLoader(jarUrls, Main.class.getClassLoader()); 571 } 572 }); 573 } 574 575 /** 576 * Loads and instantiates the plugin described by <code>plugin</code> using 577 * the class loader <code>pluginClassLoader</code>. 578 * 579 * @param parent The parent component to be used for the displayed dialog 580 * @param plugin the plugin 581 * @param pluginClassLoader the plugin class loader 582 */ 583 public static void loadPlugin(Component parent, PluginInformation plugin, ClassLoader pluginClassLoader) { 584 String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name); 585 try { 586 Class<?> klass = plugin.loadClass(pluginClassLoader); 587 if (klass != null) { 588 Main.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion)); 589 PluginProxy pluginProxy = plugin.load(klass); 590 pluginList.add(pluginProxy); 591 Main.addMapFrameListener(pluginProxy); 592 } 593 msg = null; 594 } catch (PluginException e) { 595 Main.error(e); 596 if (e.getCause() instanceof ClassNotFoundException) { 597 msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>" 598 + "Delete from preferences?</html>", plugin.name, plugin.className); 599 } 600 } catch (Exception e) { 601 Main.error(e); 602 } 603 if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) { 604 Main.pref.removeFromCollection("plugins", plugin.name); 605 } 606 } 607 608 /** 609 * Loads the plugin in <code>plugins</code> from locally available jar files into 610 * memory. 611 * 612 * @param parent The parent component to be used for the displayed dialog 613 * @param plugins the list of plugins 614 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 615 */ 616 public static void loadPlugins(Component parent,Collection<PluginInformation> plugins, ProgressMonitor monitor) { 617 if (monitor == null) { 618 monitor = NullProgressMonitor.INSTANCE; 619 } 620 try { 621 monitor.beginTask(tr("Loading plugins ...")); 622 monitor.subTask(tr("Checking plugin preconditions...")); 623 List<PluginInformation> toLoad = new LinkedList<>(); 624 for (PluginInformation pi: plugins) { 625 if (checkLoadPreconditions(parent, plugins, pi)) { 626 toLoad.add(pi); 627 } 628 } 629 // sort the plugins according to their "staging" equivalence class. The 630 // lower the value of "stage" the earlier the plugin should be loaded. 631 // 632 Collections.sort( 633 toLoad, 634 new Comparator<PluginInformation>() { 635 @Override 636 public int compare(PluginInformation o1, PluginInformation o2) { 637 if (o1.stage < o2.stage) return -1; 638 if (o1.stage == o2.stage) return 0; 639 return 1; 640 } 641 } 642 ); 643 if (toLoad.isEmpty()) 644 return; 645 646 ClassLoader pluginClassLoader = createClassLoader(toLoad); 647 sources.add(0, pluginClassLoader); 648 monitor.setTicksCount(toLoad.size()); 649 for (PluginInformation info : toLoad) { 650 monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name)); 651 loadPlugin(parent, info, pluginClassLoader); 652 monitor.worked(1); 653 } 654 } finally { 655 monitor.finishTask(); 656 } 657 } 658 659 /** 660 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} 661 * set to true. 662 * 663 * @param plugins the collection of plugins 664 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 665 */ 666 public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) { 667 List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size()); 668 for (PluginInformation pi: plugins) { 669 if (pi.early) { 670 earlyPlugins.add(pi); 671 } 672 } 673 loadPlugins(parent, earlyPlugins, monitor); 674 } 675 676 /** 677 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} 678 * set to false. 679 * 680 * @param parent The parent component to be used for the displayed dialog 681 * @param plugins the collection of plugins 682 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 683 */ 684 public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) { 685 List<PluginInformation> latePlugins = new ArrayList<>(plugins.size()); 686 for (PluginInformation pi: plugins) { 687 if (!pi.early) { 688 latePlugins.add(pi); 689 } 690 } 691 loadPlugins(parent, latePlugins, monitor); 692 } 693 694 /** 695 * Loads locally available plugin information from local plugin jars and from cached 696 * plugin lists. 697 * 698 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 699 * @return the list of locally available plugin information 700 * 701 */ 702 private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) { 703 if (monitor == null) { 704 monitor = NullProgressMonitor.INSTANCE; 705 } 706 try { 707 ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor); 708 ExecutorService service = Executors.newSingleThreadExecutor(); 709 Future<?> future = service.submit(task); 710 try { 711 future.get(); 712 } catch(ExecutionException e) { 713 Main.error(e); 714 return null; 715 } catch(InterruptedException e) { 716 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while loading locally available plugin information"); 717 return null; 718 } 719 HashMap<String, PluginInformation> ret = new HashMap<>(); 720 for (PluginInformation pi: task.getAvailablePlugins()) { 721 ret.put(pi.name, pi); 722 } 723 return ret; 724 } finally { 725 monitor.finishTask(); 726 } 727 } 728 729 private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) { 730 StringBuilder sb = new StringBuilder(); 731 sb.append("<html>"); 732 sb.append(trn("JOSM could not find information about the following plugin:", 733 "JOSM could not find information about the following plugins:", 734 plugins.size())); 735 sb.append(Utils.joinAsHtmlUnorderedList(plugins)); 736 sb.append(trn("The plugin is not going to be loaded.", 737 "The plugins are not going to be loaded.", 738 plugins.size())); 739 sb.append("</html>"); 740 HelpAwareOptionPane.showOptionDialog( 741 parent, 742 sb.toString(), 743 tr("Warning"), 744 JOptionPane.WARNING_MESSAGE, 745 HelpUtil.ht("/Plugin/Loading#MissingPluginInfos") 746 ); 747 } 748 749 /** 750 * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered 751 * out. This involves user interaction. This method displays alert and confirmation 752 * messages. 753 * 754 * @param parent The parent component to be used for the displayed dialog 755 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 756 * @return the set of plugins to load (as set of plugin names) 757 */ 758 public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) { 759 if (monitor == null) { 760 monitor = NullProgressMonitor.INSTANCE; 761 } 762 try { 763 monitor.beginTask(tr("Determine plugins to load...")); 764 Set<String> plugins = new HashSet<>(); 765 plugins.addAll(Main.pref.getCollection("plugins", new LinkedList<String>())); 766 if (System.getProperty("josm.plugins") != null) { 767 plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(","))); 768 } 769 monitor.subTask(tr("Removing deprecated plugins...")); 770 filterDeprecatedPlugins(parent, plugins); 771 monitor.subTask(tr("Removing unmaintained plugins...")); 772 filterUnmaintainedPlugins(parent, plugins); 773 Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1,false)); 774 List<PluginInformation> ret = new LinkedList<>(); 775 for (Iterator<String> it = plugins.iterator(); it.hasNext();) { 776 String plugin = it.next(); 777 if (infos.containsKey(plugin)) { 778 ret.add(infos.get(plugin)); 779 it.remove(); 780 } 781 } 782 if (!plugins.isEmpty()) { 783 alertMissingPluginInformation(parent, plugins); 784 } 785 return ret; 786 } finally { 787 monitor.finishTask(); 788 } 789 } 790 791 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) { 792 StringBuilder sb = new StringBuilder(); 793 sb.append("<html>"); 794 sb.append(trn( 795 "Updating the following plugin has failed:", 796 "Updating the following plugins has failed:", 797 plugins.size() 798 ) 799 ); 800 sb.append("<ul>"); 801 for (PluginInformation pi: plugins) { 802 sb.append("<li>").append(pi.name).append("</li>"); 803 } 804 sb.append("</ul>"); 805 sb.append(trn( 806 "Please open the Preference Dialog after JOSM has started and try to update it manually.", 807 "Please open the Preference Dialog after JOSM has started and try to update them manually.", 808 plugins.size() 809 )); 810 sb.append("</html>"); 811 HelpAwareOptionPane.showOptionDialog( 812 parent, 813 sb.toString(), 814 tr("Plugin update failed"), 815 JOptionPane.ERROR_MESSAGE, 816 HelpUtil.ht("/Plugin/Loading#FailedPluginUpdated") 817 ); 818 } 819 820 private static Set<PluginInformation> findRequiredPluginsToDownload( 821 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) { 822 Set<PluginInformation> result = new HashSet<>(); 823 for (PluginInformation pi : pluginsToUpdate) { 824 for (String name : pi.getRequiredPlugins()) { 825 try { 826 PluginInformation installedPlugin = PluginInformation.findPlugin(name); 827 if (installedPlugin == null) { 828 // New required plugin is not installed, find its PluginInformation 829 PluginInformation reqPlugin = null; 830 for (PluginInformation pi2 : allPlugins) { 831 if (pi2.getName().equals(name)) { 832 reqPlugin = pi2; 833 break; 834 } 835 } 836 // Required plugin is known but not already on download list 837 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) { 838 result.add(reqPlugin); 839 } 840 } 841 } catch (PluginException e) { 842 Main.warn(tr("Failed to find plugin {0}", name)); 843 Main.error(e); 844 } 845 } 846 } 847 return result; 848 } 849 850 /** 851 * Updates the plugins in <code>plugins</code>. 852 * 853 * @param parent the parent component for message boxes 854 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null} 855 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null. 856 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception. 857 * @throws IllegalArgumentException thrown if plugins is null 858 */ 859 public static Collection<PluginInformation> updatePlugins(Component parent, 860 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) 861 throws IllegalArgumentException { 862 Collection<PluginInformation> plugins = null; 863 pluginDownloadTask = null; 864 if (monitor == null) { 865 monitor = NullProgressMonitor.INSTANCE; 866 } 867 try { 868 monitor.beginTask(""); 869 ExecutorService service = Executors.newSingleThreadExecutor(); 870 871 // try to download the plugin lists 872 // 873 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask( 874 monitor.createSubTaskMonitor(1,false), 875 Main.pref.getPluginSites(), displayErrMsg 876 ); 877 Future<?> future = service.submit(task1); 878 List<PluginInformation> allPlugins = null; 879 880 try { 881 future.get(); 882 allPlugins = task1.getAvailablePlugins(); 883 plugins = buildListOfPluginsToLoad(parent,monitor.createSubTaskMonitor(1, false)); 884 // If only some plugins have to be updated, filter the list 885 if (pluginsWanted != null && !pluginsWanted.isEmpty()) { 886 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) { 887 PluginInformation pi = it.next(); 888 boolean found = false; 889 for (PluginInformation piw : pluginsWanted) { 890 if (pi.name.equals(piw.name)) { 891 found = true; 892 break; 893 } 894 } 895 if (!found) { 896 it.remove(); 897 } 898 } 899 } 900 } catch (ExecutionException e) { 901 Main.warn(tr("Failed to download plugin information list")+": ExecutionException"); 902 Main.error(e); 903 // don't abort in case of error, continue with downloading plugins below 904 } catch (InterruptedException e) { 905 Main.warn(tr("Failed to download plugin information list")+": InterruptedException"); 906 // don't abort in case of error, continue with downloading plugins below 907 } 908 909 // filter plugins which actually have to be updated 910 // 911 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>(); 912 for (PluginInformation pi: plugins) { 913 if (pi.isUpdateRequired()) { 914 pluginsToUpdate.add(pi); 915 } 916 } 917 918 if (!pluginsToUpdate.isEmpty()) { 919 920 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate); 921 922 if (allPlugins != null) { 923 // Updated plugins may need additional plugin dependencies currently not installed 924 // 925 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload); 926 pluginsToDownload.addAll(additionalPlugins); 927 928 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C) 929 while (!additionalPlugins.isEmpty()) { 930 // Install the additional plugins to load them later 931 plugins.addAll(additionalPlugins); 932 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload); 933 pluginsToDownload.addAll(additionalPlugins); 934 } 935 } 936 937 // try to update the locally installed plugins 938 // 939 pluginDownloadTask = new PluginDownloadTask( 940 monitor.createSubTaskMonitor(1,false), 941 pluginsToDownload, 942 tr("Update plugins") 943 ); 944 945 future = service.submit(pluginDownloadTask); 946 try { 947 future.get(); 948 } catch(ExecutionException e) { 949 Main.error(e); 950 alertFailedPluginUpdate(parent, pluginsToUpdate); 951 return plugins; 952 } catch(InterruptedException e) { 953 Main.warn("InterruptedException in "+PluginHandler.class.getSimpleName()+" while updating plugins"); 954 alertFailedPluginUpdate(parent, pluginsToUpdate); 955 return plugins; 956 } 957 958 // Update Plugin info for downloaded plugins 959 // 960 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins()); 961 962 // notify user if downloading a locally installed plugin failed 963 // 964 if (! pluginDownloadTask.getFailedPlugins().isEmpty()) { 965 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins()); 966 return plugins; 967 } 968 } 969 } finally { 970 monitor.finishTask(); 971 } 972 if (pluginsWanted == null) { 973 // if all plugins updated, remember the update because it was successful 974 // 975 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion()); 976 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis())); 977 } 978 return plugins; 979 } 980 981 /** 982 * Ask the user for confirmation that a plugin shall be disabled. 983 * 984 * @param parent The parent component to be used for the displayed dialog 985 * @param reason the reason for disabling the plugin 986 * @param name the plugin name 987 * @return true, if the plugin shall be disabled; false, otherwise 988 */ 989 public static boolean confirmDisablePlugin(Component parent, String reason, String name) { 990 ButtonSpec [] options = new ButtonSpec[] { 991 new ButtonSpec( 992 tr("Disable plugin"), 993 ImageProvider.get("dialogs", "delete"), 994 tr("Click to delete the plugin ''{0}''", name), 995 null /* no specific help context */ 996 ), 997 new ButtonSpec( 998 tr("Keep plugin"), 999 ImageProvider.get("cancel"), 1000 tr("Click to keep the plugin ''{0}''", name), 1001 null /* no specific help context */ 1002 ) 1003 }; 1004 int ret = HelpAwareOptionPane.showOptionDialog( 1005 parent, 1006 reason, 1007 tr("Disable plugin"), 1008 JOptionPane.WARNING_MESSAGE, 1009 null, 1010 options, 1011 options[0], 1012 null // FIXME: add help topic 1013 ); 1014 return ret == 0; 1015 } 1016 1017 /** 1018 * Returns the plugin of the specified name. 1019 * @param name The plugin name 1020 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise. 1021 */ 1022 public static Object getPlugin(String name) { 1023 for (PluginProxy plugin : pluginList) 1024 if (plugin.getPluginInformation().name.equals(name)) 1025 return plugin.plugin; 1026 return null; 1027 } 1028 1029 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) { 1030 for (PluginProxy p : pluginList) { 1031 p.addDownloadSelection(downloadSelections); 1032 } 1033 } 1034 1035 public static void getPreferenceSetting(Collection<PreferenceSettingFactory> settings) { 1036 for (PluginProxy plugin : pluginList) { 1037 settings.add(new PluginPreferenceFactory(plugin)); 1038 } 1039 } 1040 1041 /** 1042 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding 1043 * ".jar" files. 1044 * 1045 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded 1046 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the 1047 * installation of the respective plugin is sillently skipped. 1048 * 1049 * @param dowarn if true, warning messages are displayed; false otherwise 1050 */ 1051 public static void installDownloadedPlugins(boolean dowarn) { 1052 File pluginDir = Main.pref.getPluginsDirectory(); 1053 if (! pluginDir.exists() || ! pluginDir.isDirectory() || ! pluginDir.canWrite()) 1054 return; 1055 1056 final File[] files = pluginDir.listFiles(new FilenameFilter() { 1057 @Override 1058 public boolean accept(File dir, String name) { 1059 return name.endsWith(".jar.new"); 1060 }}); 1061 1062 for (File updatedPlugin : files) { 1063 final String filePath = updatedPlugin.getPath(); 1064 File plugin = new File(filePath.substring(0, filePath.length() - 4)); 1065 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8); 1066 if (plugin.exists() && !plugin.delete() && dowarn) { 1067 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString())); 1068 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName)); 1069 continue; 1070 } 1071 try { 1072 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754) 1073 new JarFile(updatedPlugin).close(); 1074 } catch (Exception e) { 1075 if (dowarn) { 1076 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}", plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage())); 1077 } 1078 continue; 1079 } 1080 // Install plugin 1081 if (!updatedPlugin.renameTo(plugin) && dowarn) { 1082 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.", plugin.toString(), updatedPlugin.toString())); 1083 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. Skipping installation. JOSM is still going to load the old plugin version.", pluginName)); 1084 } 1085 } 1086 return; 1087 } 1088 1089 /** 1090 * Determines if the specified file is a valid and accessible JAR file. 1091 * @param jar The fil to check 1092 * @return true if file can be opened as a JAR file. 1093 * @since 5723 1094 */ 1095 public static boolean isValidJar(File jar) { 1096 if (jar != null && jar.exists() && jar.canRead()) { 1097 try { 1098 new JarFile(jar).close(); 1099 } catch (Exception e) { 1100 return false; 1101 } 1102 return true; 1103 } 1104 return false; 1105 } 1106 1107 /** 1108 * Replies the updated jar file for the given plugin name. 1109 * @param name The plugin name to find. 1110 * @return the updated jar file for the given plugin name. null if not found or not readable. 1111 * @since 5601 1112 */ 1113 public static File findUpdatedJar(String name) { 1114 File pluginDir = Main.pref.getPluginsDirectory(); 1115 // Find the downloaded file. We have tried to install the downloaded plugins 1116 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform. 1117 File downloadedPluginFile = new File(pluginDir, name + ".jar.new"); 1118 if (!isValidJar(downloadedPluginFile)) { 1119 downloadedPluginFile = new File(pluginDir, name + ".jar"); 1120 if (!isValidJar(downloadedPluginFile)) { 1121 return null; 1122 } 1123 } 1124 return downloadedPluginFile; 1125 } 1126 1127 /** 1128 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file. 1129 * @param updatedPlugins The PluginInformation objects to update. 1130 * @since 5601 1131 */ 1132 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) { 1133 if (updatedPlugins == null) return; 1134 for (PluginInformation pi : updatedPlugins) { 1135 File downloadedPluginFile = findUpdatedJar(pi.name); 1136 if (downloadedPluginFile == null) { 1137 continue; 1138 } 1139 try { 1140 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name)); 1141 } catch(PluginException e) { 1142 Main.error(e); 1143 } 1144 } 1145 } 1146 1147 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) { 1148 final ButtonSpec[] options = new ButtonSpec[] { 1149 new ButtonSpec( 1150 tr("Update plugin"), 1151 ImageProvider.get("dialogs", "refresh"), 1152 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name), 1153 null /* no specific help context */ 1154 ), 1155 new ButtonSpec( 1156 tr("Disable plugin"), 1157 ImageProvider.get("dialogs", "delete"), 1158 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name), 1159 null /* no specific help context */ 1160 ), 1161 new ButtonSpec( 1162 tr("Keep plugin"), 1163 ImageProvider.get("cancel"), 1164 tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name), 1165 null /* no specific help context */ 1166 ) 1167 }; 1168 1169 final StringBuilder msg = new StringBuilder(); 1170 msg.append("<html>"); 1171 msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name)); 1172 msg.append("<br>"); 1173 if (plugin.getPluginInformation().author != null) { 1174 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author)); 1175 msg.append("<br>"); 1176 } 1177 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug.")); 1178 msg.append("</html>"); 1179 1180 try { 1181 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() { 1182 @Override 1183 public Integer call() { 1184 return HelpAwareOptionPane.showOptionDialog( 1185 Main.parent, 1186 msg.toString(), 1187 tr("Update plugins"), 1188 JOptionPane.QUESTION_MESSAGE, 1189 null, 1190 options, 1191 options[0], 1192 ht("/ErrorMessages#ErrorInPlugin") 1193 ); 1194 } 1195 }); 1196 GuiHelper.runInEDT(task); 1197 return task.get(); 1198 } catch (InterruptedException | ExecutionException e) { 1199 Main.warn(e); 1200 } 1201 return -1; 1202 } 1203 1204 /** 1205 * Replies the plugin which most likely threw the exception <code>ex</code>. 1206 * 1207 * @param ex the exception 1208 * @return the plugin; null, if the exception probably wasn't thrown from a plugin 1209 */ 1210 private static PluginProxy getPluginCausingException(Throwable ex) { 1211 PluginProxy err = null; 1212 StackTraceElement[] stack = ex.getStackTrace(); 1213 /* remember the error position, as multiple plugins may be involved, 1214 we search the topmost one */ 1215 int pos = stack.length; 1216 for (PluginProxy p : pluginList) { 1217 String baseClass = p.getPluginInformation().className; 1218 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.')); 1219 for (int elpos = 0; elpos < pos; ++elpos) { 1220 if (stack[elpos].getClassName().startsWith(baseClass)) { 1221 pos = elpos; 1222 err = p; 1223 } 1224 } 1225 } 1226 return err; 1227 } 1228 1229 /** 1230 * Checks whether the exception <code>e</code> was thrown by a plugin. If so, 1231 * conditionally updates or deactivates the plugin, but asks the user first. 1232 * 1233 * @param e the exception 1234 * @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 1235 */ 1236 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) { 1237 PluginProxy plugin = null; 1238 // Check for an explicit problem when calling a plugin function 1239 if (e instanceof PluginException) { 1240 plugin = ((PluginException) e).plugin; 1241 } 1242 if (plugin == null) { 1243 plugin = getPluginCausingException(e); 1244 } 1245 if (plugin == null) 1246 // don't know what plugin threw the exception 1247 return null; 1248 1249 Set<String> plugins = new HashSet<>( 1250 Main.pref.getCollection("plugins",Collections.<String> emptySet()) 1251 ); 1252 final PluginInformation pluginInfo = plugin.getPluginInformation(); 1253 if (! plugins.contains(pluginInfo.name)) 1254 // plugin not activated ? strange in this context but anyway, don't bother 1255 // the user with dialogs, skip conditional deactivation 1256 return null; 1257 1258 switch (askUpdateDisableKeepPluginAfterException(plugin)) { 1259 case 0: 1260 // update the plugin 1261 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true); 1262 return pluginDownloadTask; 1263 case 1: 1264 // deactivate the plugin 1265 plugins.remove(plugin.getPluginInformation().name); 1266 Main.pref.putCollection("plugins", plugins); 1267 GuiHelper.runInEDTAndWait(new Runnable() { 1268 @Override 1269 public void run() { 1270 JOptionPane.showMessageDialog( 1271 Main.parent, 1272 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."), 1273 tr("Information"), 1274 JOptionPane.INFORMATION_MESSAGE 1275 ); 1276 } 1277 }); 1278 return null; 1279 default: 1280 // user doesn't want to deactivate the plugin 1281 return null; 1282 } 1283 } 1284 1285 /** 1286 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports. 1287 * @return The list of loaded plugins (one plugin per line) 1288 */ 1289 public static String getBugReportText() { 1290 StringBuilder text = new StringBuilder(); 1291 LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>())); 1292 for (final PluginProxy pp : pluginList) { 1293 PluginInformation pi = pp.getPluginInformation(); 1294 pl.remove(pi.name); 1295 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty() 1296 ? pi.localversion : "unknown") + ")"); 1297 } 1298 Collections.sort(pl); 1299 if (!pl.isEmpty()) { 1300 text.append("Plugins:\n"); 1301 } 1302 for (String s : pl) { 1303 text.append("- ").append(s).append("\n"); 1304 } 1305 return text.toString(); 1306 } 1307 1308 /** 1309 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog. 1310 * @return The list of loaded plugins (one "line" of Swing components per plugin) 1311 */ 1312 public static JPanel getInfoPanel() { 1313 JPanel pluginTab = new JPanel(new GridBagLayout()); 1314 for (final PluginProxy p : pluginList) { 1315 final PluginInformation info = p.getPluginInformation(); 1316 String name = info.name 1317 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : ""); 1318 pluginTab.add(new JLabel(name), GBC.std()); 1319 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); 1320 pluginTab.add(new JButton(new AbstractAction(tr("Information")) { 1321 @Override 1322 public void actionPerformed(ActionEvent event) { 1323 StringBuilder b = new StringBuilder(); 1324 for (Entry<String, String> e : info.attr.entrySet()) { 1325 b.append(e.getKey()); 1326 b.append(": "); 1327 b.append(e.getValue()); 1328 b.append("\n"); 1329 } 1330 JosmTextArea a = new JosmTextArea(10, 40); 1331 a.setEditable(false); 1332 a.setText(b.toString()); 1333 a.setCaretPosition(0); 1334 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"), 1335 JOptionPane.INFORMATION_MESSAGE); 1336 } 1337 }), GBC.eol()); 1338 1339 JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available") 1340 : info.description)); 1341 description.setEditable(false); 1342 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC)); 1343 description.setLineWrap(true); 1344 description.setWrapStyleWord(true); 1345 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0)); 1346 description.setBackground(UIManager.getColor("Panel.background")); 1347 description.setCaretPosition(0); 1348 1349 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL)); 1350 } 1351 return pluginTab; 1352 } 1353 1354 private static class UpdatePluginsMessagePanel extends JPanel { 1355 private JMultilineLabel lblMessage; 1356 private JCheckBox cbDontShowAgain; 1357 1358 protected final void build() { 1359 setLayout(new GridBagLayout()); 1360 GridBagConstraints gc = new GridBagConstraints(); 1361 gc.anchor = GridBagConstraints.NORTHWEST; 1362 gc.fill = GridBagConstraints.BOTH; 1363 gc.weightx = 1.0; 1364 gc.weighty = 1.0; 1365 gc.insets = new Insets(5,5,5,5); 1366 add(lblMessage = new JMultilineLabel(""), gc); 1367 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN)); 1368 1369 gc.gridy = 1; 1370 gc.fill = GridBagConstraints.HORIZONTAL; 1371 gc.weighty = 0.0; 1372 add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc); 1373 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN)); 1374 } 1375 1376 public UpdatePluginsMessagePanel() { 1377 build(); 1378 } 1379 1380 public void setMessage(String message) { 1381 lblMessage.setText(message); 1382 } 1383 1384 public void initDontShowAgain(String preferencesKey) { 1385 String policy = Main.pref.get(preferencesKey, "ask"); 1386 policy = policy.trim().toLowerCase(); 1387 cbDontShowAgain.setSelected(!"ask".equals(policy)); 1388 } 1389 1390 public boolean isRememberDecision() { 1391 return cbDontShowAgain.isSelected(); 1392 } 1393 } 1394}