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