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 } 1087 1088 /** 1089 * Determines if the specified file is a valid and accessible JAR file. 1090 * @param jar The fil to check 1091 * @return true if file can be opened as a JAR file. 1092 * @since 5723 1093 */ 1094 public static boolean isValidJar(File jar) { 1095 if (jar != null && jar.exists() && jar.canRead()) { 1096 try { 1097 new JarFile(jar).close(); 1098 } catch (Exception e) { 1099 return false; 1100 } 1101 return true; 1102 } 1103 return false; 1104 } 1105 1106 /** 1107 * Replies the updated jar file for the given plugin name. 1108 * @param name The plugin name to find. 1109 * @return the updated jar file for the given plugin name. null if not found or not readable. 1110 * @since 5601 1111 */ 1112 public static File findUpdatedJar(String name) { 1113 File pluginDir = Main.pref.getPluginsDirectory(); 1114 // Find the downloaded file. We have tried to install the downloaded plugins 1115 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform. 1116 File downloadedPluginFile = new File(pluginDir, name + ".jar.new"); 1117 if (!isValidJar(downloadedPluginFile)) { 1118 downloadedPluginFile = new File(pluginDir, name + ".jar"); 1119 if (!isValidJar(downloadedPluginFile)) { 1120 return null; 1121 } 1122 } 1123 return downloadedPluginFile; 1124 } 1125 1126 /** 1127 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file. 1128 * @param updatedPlugins The PluginInformation objects to update. 1129 * @since 5601 1130 */ 1131 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) { 1132 if (updatedPlugins == null) return; 1133 for (PluginInformation pi : updatedPlugins) { 1134 File downloadedPluginFile = findUpdatedJar(pi.name); 1135 if (downloadedPluginFile == null) { 1136 continue; 1137 } 1138 try { 1139 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name)); 1140 } catch(PluginException e) { 1141 Main.error(e); 1142 } 1143 } 1144 } 1145 1146 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) { 1147 final ButtonSpec[] options = new ButtonSpec[] { 1148 new ButtonSpec( 1149 tr("Update plugin"), 1150 ImageProvider.get("dialogs", "refresh"), 1151 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name), 1152 null /* no specific help context */ 1153 ), 1154 new ButtonSpec( 1155 tr("Disable plugin"), 1156 ImageProvider.get("dialogs", "delete"), 1157 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name), 1158 null /* no specific help context */ 1159 ), 1160 new ButtonSpec( 1161 tr("Keep plugin"), 1162 ImageProvider.get("cancel"), 1163 tr("Click to keep the plugin ''{0}''",plugin.getPluginInformation().name), 1164 null /* no specific help context */ 1165 ) 1166 }; 1167 1168 final StringBuilder msg = new StringBuilder(); 1169 msg.append("<html>"); 1170 msg.append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name)); 1171 msg.append("<br>"); 1172 if (plugin.getPluginInformation().author != null) { 1173 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author)); 1174 msg.append("<br>"); 1175 } 1176 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug.")); 1177 msg.append("</html>"); 1178 1179 try { 1180 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() { 1181 @Override 1182 public Integer call() { 1183 return HelpAwareOptionPane.showOptionDialog( 1184 Main.parent, 1185 msg.toString(), 1186 tr("Update plugins"), 1187 JOptionPane.QUESTION_MESSAGE, 1188 null, 1189 options, 1190 options[0], 1191 ht("/ErrorMessages#ErrorInPlugin") 1192 ); 1193 } 1194 }); 1195 GuiHelper.runInEDT(task); 1196 return task.get(); 1197 } catch (InterruptedException | ExecutionException e) { 1198 Main.warn(e); 1199 } 1200 return -1; 1201 } 1202 1203 /** 1204 * Replies the plugin which most likely threw the exception <code>ex</code>. 1205 * 1206 * @param ex the exception 1207 * @return the plugin; null, if the exception probably wasn't thrown from a plugin 1208 */ 1209 private static PluginProxy getPluginCausingException(Throwable ex) { 1210 PluginProxy err = null; 1211 StackTraceElement[] stack = ex.getStackTrace(); 1212 /* remember the error position, as multiple plugins may be involved, 1213 we search the topmost one */ 1214 int pos = stack.length; 1215 for (PluginProxy p : pluginList) { 1216 String baseClass = p.getPluginInformation().className; 1217 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.')); 1218 for (int elpos = 0; elpos < pos; ++elpos) { 1219 if (stack[elpos].getClassName().startsWith(baseClass)) { 1220 pos = elpos; 1221 err = p; 1222 } 1223 } 1224 } 1225 return err; 1226 } 1227 1228 /** 1229 * Checks whether the exception <code>e</code> was thrown by a plugin. If so, 1230 * conditionally updates or deactivates the plugin, but asks the user first. 1231 * 1232 * @param e the exception 1233 * @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 1234 */ 1235 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) { 1236 PluginProxy plugin = null; 1237 // Check for an explicit problem when calling a plugin function 1238 if (e instanceof PluginException) { 1239 plugin = ((PluginException) e).plugin; 1240 } 1241 if (plugin == null) { 1242 plugin = getPluginCausingException(e); 1243 } 1244 if (plugin == null) 1245 // don't know what plugin threw the exception 1246 return null; 1247 1248 Set<String> plugins = new HashSet<>( 1249 Main.pref.getCollection("plugins",Collections.<String> emptySet()) 1250 ); 1251 final PluginInformation pluginInfo = plugin.getPluginInformation(); 1252 if (! plugins.contains(pluginInfo.name)) 1253 // plugin not activated ? strange in this context but anyway, don't bother 1254 // the user with dialogs, skip conditional deactivation 1255 return null; 1256 1257 switch (askUpdateDisableKeepPluginAfterException(plugin)) { 1258 case 0: 1259 // update the plugin 1260 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true); 1261 return pluginDownloadTask; 1262 case 1: 1263 // deactivate the plugin 1264 plugins.remove(plugin.getPluginInformation().name); 1265 Main.pref.putCollection("plugins", plugins); 1266 GuiHelper.runInEDTAndWait(new Runnable() { 1267 @Override 1268 public void run() { 1269 JOptionPane.showMessageDialog( 1270 Main.parent, 1271 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."), 1272 tr("Information"), 1273 JOptionPane.INFORMATION_MESSAGE 1274 ); 1275 } 1276 }); 1277 return null; 1278 default: 1279 // user doesn't want to deactivate the plugin 1280 return null; 1281 } 1282 } 1283 1284 /** 1285 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports. 1286 * @return The list of loaded plugins (one plugin per line) 1287 */ 1288 public static String getBugReportText() { 1289 StringBuilder text = new StringBuilder(); 1290 LinkedList <String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>())); 1291 for (final PluginProxy pp : pluginList) { 1292 PluginInformation pi = pp.getPluginInformation(); 1293 pl.remove(pi.name); 1294 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty() 1295 ? pi.localversion : "unknown") + ")"); 1296 } 1297 Collections.sort(pl); 1298 if (!pl.isEmpty()) { 1299 text.append("Plugins:\n"); 1300 } 1301 for (String s : pl) { 1302 text.append("- ").append(s).append("\n"); 1303 } 1304 return text.toString(); 1305 } 1306 1307 /** 1308 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog. 1309 * @return The list of loaded plugins (one "line" of Swing components per plugin) 1310 */ 1311 public static JPanel getInfoPanel() { 1312 JPanel pluginTab = new JPanel(new GridBagLayout()); 1313 for (final PluginProxy p : pluginList) { 1314 final PluginInformation info = p.getPluginInformation(); 1315 String name = info.name 1316 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : ""); 1317 pluginTab.add(new JLabel(name), GBC.std()); 1318 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL)); 1319 pluginTab.add(new JButton(new AbstractAction(tr("Information")) { 1320 @Override 1321 public void actionPerformed(ActionEvent event) { 1322 StringBuilder b = new StringBuilder(); 1323 for (Entry<String, String> e : info.attr.entrySet()) { 1324 b.append(e.getKey()); 1325 b.append(": "); 1326 b.append(e.getValue()); 1327 b.append("\n"); 1328 } 1329 JosmTextArea a = new JosmTextArea(10, 40); 1330 a.setEditable(false); 1331 a.setText(b.toString()); 1332 a.setCaretPosition(0); 1333 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"), 1334 JOptionPane.INFORMATION_MESSAGE); 1335 } 1336 }), GBC.eol()); 1337 1338 JosmTextArea description = new JosmTextArea((info.description == null ? tr("no description available") 1339 : info.description)); 1340 description.setEditable(false); 1341 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC)); 1342 description.setLineWrap(true); 1343 description.setWrapStyleWord(true); 1344 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0)); 1345 description.setBackground(UIManager.getColor("Panel.background")); 1346 description.setCaretPosition(0); 1347 1348 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL)); 1349 } 1350 return pluginTab; 1351 } 1352 1353 private static class UpdatePluginsMessagePanel extends JPanel { 1354 private JMultilineLabel lblMessage; 1355 private JCheckBox cbDontShowAgain; 1356 1357 protected final void build() { 1358 setLayout(new GridBagLayout()); 1359 GridBagConstraints gc = new GridBagConstraints(); 1360 gc.anchor = GridBagConstraints.NORTHWEST; 1361 gc.fill = GridBagConstraints.BOTH; 1362 gc.weightx = 1.0; 1363 gc.weighty = 1.0; 1364 gc.insets = new Insets(5,5,5,5); 1365 add(lblMessage = new JMultilineLabel(""), gc); 1366 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN)); 1367 1368 gc.gridy = 1; 1369 gc.fill = GridBagConstraints.HORIZONTAL; 1370 gc.weighty = 0.0; 1371 add(cbDontShowAgain = new JCheckBox(tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc); 1372 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN)); 1373 } 1374 1375 public UpdatePluginsMessagePanel() { 1376 build(); 1377 } 1378 1379 public void setMessage(String message) { 1380 lblMessage.setText(message); 1381 } 1382 1383 public void initDontShowAgain(String preferencesKey) { 1384 String policy = Main.pref.get(preferencesKey, "ask"); 1385 policy = policy.trim().toLowerCase(); 1386 cbDontShowAgain.setSelected(!"ask".equals(policy)); 1387 } 1388 1389 public boolean isRememberDecision() { 1390 return cbDontShowAgain.isSelected(); 1391 } 1392 } 1393}