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