source: josm/trunk/src/org/openstreetmap/josm/plugins/PluginHandler.java@ 9972

Last change on this file since 9972 was 9972, checked in by Don-vip, 8 years ago

sonar - remove unnecessary fully qualified names

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