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

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

unban CADTools - see https://github.com/ROTARIUANAMARIA/CADTools/issues/1

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