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