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