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

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

PluginHandler: deprecate kendzi3d-jogl plugin, remove unused code, fix findbugs warning, add unit tests

  • Property svn:eol-style set to native
File size: 64.1 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.help.HelpUtil;
59import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
60import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
61import org.openstreetmap.josm.gui.progress.ProgressMonitor;
62import org.openstreetmap.josm.gui.util.GuiHelper;
63import org.openstreetmap.josm.gui.widgets.JMultilineLabel;
64import org.openstreetmap.josm.gui.widgets.JosmTextArea;
65import org.openstreetmap.josm.io.OfflineAccessException;
66import org.openstreetmap.josm.io.OnlineResource;
67import org.openstreetmap.josm.tools.GBC;
68import org.openstreetmap.josm.tools.I18n;
69import org.openstreetmap.josm.tools.ImageProvider;
70import org.openstreetmap.josm.tools.Utils;
71
72/**
73 * PluginHandler is basically a collection of static utility functions used to bootstrap
74 * and manage the loaded plugins.
75 * @since 1326
76 */
77public final class PluginHandler {
78
79 /**
80 * Deprecated plugins that are removed on start
81 */
82 protected static final Collection<DeprecatedPlugin> DEPRECATED_PLUGINS;
83 static {
84 String IN_CORE = tr("integrated into main program");
85
86 DEPRECATED_PLUGINS = Arrays.asList(new DeprecatedPlugin[] {
87 new DeprecatedPlugin("mappaint", IN_CORE),
88 new DeprecatedPlugin("unglueplugin", IN_CORE),
89 new DeprecatedPlugin("lang-de", IN_CORE),
90 new DeprecatedPlugin("lang-en_GB", IN_CORE),
91 new DeprecatedPlugin("lang-fr", IN_CORE),
92 new DeprecatedPlugin("lang-it", IN_CORE),
93 new DeprecatedPlugin("lang-pl", IN_CORE),
94 new DeprecatedPlugin("lang-ro", IN_CORE),
95 new DeprecatedPlugin("lang-ru", IN_CORE),
96 new DeprecatedPlugin("ewmsplugin", IN_CORE),
97 new DeprecatedPlugin("ywms", IN_CORE),
98 new DeprecatedPlugin("tways-0.2", IN_CORE),
99 new DeprecatedPlugin("geotagged", IN_CORE),
100 new DeprecatedPlugin("landsat", tr("replaced by new {0} plugin", "lakewalker")),
101 new DeprecatedPlugin("namefinder", IN_CORE),
102 new DeprecatedPlugin("waypoints", IN_CORE),
103 new DeprecatedPlugin("slippy_map_chooser", IN_CORE),
104 new DeprecatedPlugin("tcx-support", tr("replaced by new {0} plugin", "dataimport")),
105 new DeprecatedPlugin("usertools", IN_CORE),
106 new DeprecatedPlugin("AgPifoJ", IN_CORE),
107 new DeprecatedPlugin("utilsplugin", IN_CORE),
108 new DeprecatedPlugin("ghost", IN_CORE),
109 new DeprecatedPlugin("validator", IN_CORE),
110 new DeprecatedPlugin("multipoly", IN_CORE),
111 new DeprecatedPlugin("multipoly-convert", IN_CORE),
112 new DeprecatedPlugin("remotecontrol", IN_CORE),
113 new DeprecatedPlugin("imagery", IN_CORE),
114 new DeprecatedPlugin("slippymap", IN_CORE),
115 new DeprecatedPlugin("wmsplugin", IN_CORE),
116 new DeprecatedPlugin("ParallelWay", IN_CORE),
117 new DeprecatedPlugin("dumbutils", tr("replaced by new {0} plugin", "utilsplugin2")),
118 new DeprecatedPlugin("ImproveWayAccuracy", IN_CORE),
119 new DeprecatedPlugin("Curves", tr("replaced by new {0} plugin", "utilsplugin2")),
120 new DeprecatedPlugin("epsg31287", tr("replaced by new {0} plugin", "proj4j")),
121 new DeprecatedPlugin("licensechange", tr("no longer required")),
122 new DeprecatedPlugin("restart", IN_CORE),
123 new DeprecatedPlugin("wayselector", IN_CORE),
124 new DeprecatedPlugin("openstreetbugs", tr("replaced by new {0} plugin", "notes")),
125 new DeprecatedPlugin("nearclick", tr("no longer required")),
126 new DeprecatedPlugin("notes", IN_CORE),
127 new DeprecatedPlugin("mirrored_download", IN_CORE),
128 new DeprecatedPlugin("ImageryCache", IN_CORE),
129 new DeprecatedPlugin("commons-imaging", tr("replaced by new {0} plugin", "apache-commons")),
130 new DeprecatedPlugin("missingRoads", tr("replaced by new {0} plugin", "ImproveOsm")),
131 new DeprecatedPlugin("trafficFlowDirection", tr("replaced by new {0} plugin", "ImproveOsm")),
132 new DeprecatedPlugin("kendzi3d-jogl", tr("replaced by new {0} plugin", "jogl")),
133 });
134 }
135
136 private PluginHandler() {
137 // Hide default constructor for utils classes
138 }
139
140 /**
141 * Description of a deprecated plugin
142 */
143 public static class DeprecatedPlugin implements Comparable<DeprecatedPlugin> {
144 /** Plugin name */
145 public final String name;
146 /** Short explanation about deprecation, can be {@code null} */
147 public final String reason;
148
149 /**
150 * Constructs a new {@code DeprecatedPlugin} with a given reason.
151 * @param name The plugin name
152 * @param reason The reason about deprecation
153 */
154 public DeprecatedPlugin(String name, String reason) {
155 this.name = name;
156 this.reason = reason;
157 }
158
159 @Override
160 public int hashCode() {
161 final int prime = 31;
162 int result = prime + ((name == null) ? 0 : name.hashCode());
163 return prime * result + ((reason == null) ? 0 : reason.hashCode());
164 }
165
166 @Override
167 public boolean equals(Object obj) {
168 if (this == obj)
169 return true;
170 if (obj == null)
171 return false;
172 if (getClass() != obj.getClass())
173 return false;
174 DeprecatedPlugin other = (DeprecatedPlugin) obj;
175 if (name == null) {
176 if (other.name != null)
177 return false;
178 } else if (!name.equals(other.name))
179 return false;
180 if (reason == null) {
181 if (other.reason != null)
182 return false;
183 } else if (!reason.equals(other.reason))
184 return false;
185 return true;
186 }
187
188 @Override
189 public int compareTo(DeprecatedPlugin o) {
190 int d = name.compareTo(o.name);
191 if (d == 0)
192 d = reason.compareTo(o.reason);
193 return d;
194 }
195 }
196
197 /**
198 * ClassLoader that makes the addURL method of URLClassLoader public.
199 *
200 * Like URLClassLoader, but allows to add more URLs after construction.
201 */
202 public static class DynamicURLClassLoader extends URLClassLoader {
203
204 /**
205 * Constructs a new {@code DynamicURLClassLoader}.
206 * @param urls the URLs from which to load classes and resources
207 * @param parent the parent class loader for delegation
208 */
209 public DynamicURLClassLoader(URL[] urls, ClassLoader parent) {
210 super(urls, parent);
211 }
212
213 @Override
214 public void addURL(URL url) {
215 super.addURL(url);
216 }
217 }
218
219 /**
220 * List of unmaintained plugins. Not really up-to-date as the vast majority of plugins are not maintained after a few months, sadly...
221 */
222 static final String[] UNMAINTAINED_PLUGINS = new String[] {
223 "gpsbabelgui",
224 "Intersect_way",
225 "CADTools", // See #11438, #11518, https://github.com/ROTARIUANAMARIA/CADTools/issues/1
226 "ContourOverlappingMerge", // See #11202, #11518, https://github.com/bularcasergiu/ContourOverlappingMerge/issues/1
227 "LaneConnector", // See #11468, #11518, https://github.com/TrifanAdrian/LanecConnectorPlugin/issues/1
228 "Remove.redundant.points" // See #11468, #11518, https://github.com/bularcasergiu/RemoveRedundantPoints (not even created an issue...)
229 };
230
231 /**
232 * Default time-based update interval, in days (pluginmanager.time-based-update.interval)
233 */
234 public static final int DEFAULT_TIME_BASED_UPDATE_INTERVAL = 30;
235
236 /**
237 * All installed and loaded plugins (resp. their main classes)
238 */
239 public static final Collection<PluginProxy> pluginList = new LinkedList<>();
240
241 /**
242 * All exceptions that occured during plugin loading
243 * @since 8938
244 */
245 public static final Map<String, Exception> pluginLoadingExceptions = new HashMap<>();
246
247 /**
248 * Global plugin ClassLoader.
249 */
250 private static DynamicURLClassLoader pluginClassLoader;
251
252 /**
253 * Add here all ClassLoader whose resource should be searched.
254 */
255 private static final List<ClassLoader> sources = new LinkedList<>();
256 static {
257 try {
258 sources.add(ClassLoader.getSystemClassLoader());
259 sources.add(org.openstreetmap.josm.gui.MainApplication.class.getClassLoader());
260 } catch (SecurityException ex) {
261 sources.add(ImageProvider.class.getClassLoader());
262 }
263 }
264
265 private static PluginDownloadTask pluginDownloadTask;
266
267 public static Collection<ClassLoader> getResourceClassLoaders() {
268 return Collections.unmodifiableCollection(sources);
269 }
270
271 /**
272 * Removes deprecated plugins from a collection of plugins. Modifies the
273 * collection <code>plugins</code>.
274 *
275 * Also notifies the user about removed deprecated plugins
276 *
277 * @param parent The parent Component used to display warning popup
278 * @param plugins the collection of plugins
279 */
280 private static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) {
281 Set<DeprecatedPlugin> removedPlugins = new TreeSet<>();
282 for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) {
283 if (plugins.contains(depr.name)) {
284 plugins.remove(depr.name);
285 Main.pref.removeFromCollection("plugins", depr.name);
286 removedPlugins.add(depr);
287 }
288 }
289 if (removedPlugins.isEmpty())
290 return;
291
292 // notify user about removed deprecated plugins
293 //
294 StringBuilder sb = new StringBuilder(32);
295 sb.append("<html>")
296 .append(trn(
297 "The following plugin is no longer necessary and has been deactivated:",
298 "The following plugins are no longer necessary and have been deactivated:",
299 removedPlugins.size()))
300 .append("<ul>");
301 for (DeprecatedPlugin depr: removedPlugins) {
302 sb.append("<li>").append(depr.name);
303 if (depr.reason != null) {
304 sb.append(" (").append(depr.reason).append(')');
305 }
306 sb.append("</li>");
307 }
308 sb.append("</ul></html>");
309 JOptionPane.showMessageDialog(
310 parent,
311 sb.toString(),
312 tr("Warning"),
313 JOptionPane.WARNING_MESSAGE
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 private 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 HelpUtil.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 HelpUtil.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 if (!GraphicsEnvironment.isHeadless()) {
859 HelpAwareOptionPane.showOptionDialog(
860 parent,
861 sb.toString(),
862 tr("Warning"),
863 JOptionPane.WARNING_MESSAGE,
864 HelpUtil.ht("/Plugin/Loading#MissingPluginInfos")
865 );
866 }
867 }
868
869 /**
870 * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
871 * out. This involves user interaction. This method displays alert and confirmation
872 * messages.
873 *
874 * @param parent The parent component to be used for the displayed dialog
875 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
876 * @return the set of plugins to load (as set of plugin names)
877 */
878 public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
879 if (monitor == null) {
880 monitor = NullProgressMonitor.INSTANCE;
881 }
882 try {
883 monitor.beginTask(tr("Determine plugins to load..."));
884 Set<String> plugins = new HashSet<>();
885 plugins.addAll(Main.pref.getCollection("plugins", new LinkedList<String>()));
886 if (System.getProperty("josm.plugins") != null) {
887 plugins.addAll(Arrays.asList(System.getProperty("josm.plugins").split(",")));
888 }
889 monitor.subTask(tr("Removing deprecated plugins..."));
890 filterDeprecatedPlugins(parent, plugins);
891 monitor.subTask(tr("Removing unmaintained plugins..."));
892 filterUnmaintainedPlugins(parent, plugins);
893 Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1, false));
894 List<PluginInformation> ret = new LinkedList<>();
895 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
896 String plugin = it.next();
897 if (infos.containsKey(plugin)) {
898 ret.add(infos.get(plugin));
899 it.remove();
900 }
901 }
902 if (!plugins.isEmpty()) {
903 alertMissingPluginInformation(parent, plugins);
904 }
905 return ret;
906 } finally {
907 monitor.finishTask();
908 }
909 }
910
911 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
912 StringBuilder sb = new StringBuilder();
913 sb.append("<html>")
914 .append(trn(
915 "Updating the following plugin has failed:",
916 "Updating the following plugins has failed:",
917 plugins.size()))
918 .append("<ul>");
919 for (PluginInformation pi: plugins) {
920 sb.append("<li>").append(pi.name).append("</li>");
921 }
922 sb.append("</ul>")
923 .append(trn(
924 "Please open the Preference Dialog after JOSM has started and try to update it manually.",
925 "Please open the Preference Dialog after JOSM has started and try to update them manually.",
926 plugins.size()))
927 .append("</html>");
928 HelpAwareOptionPane.showOptionDialog(
929 parent,
930 sb.toString(),
931 tr("Plugin update failed"),
932 JOptionPane.ERROR_MESSAGE,
933 HelpUtil.ht("/Plugin/Loading#FailedPluginUpdated")
934 );
935 }
936
937 private static Set<PluginInformation> findRequiredPluginsToDownload(
938 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
939 Set<PluginInformation> result = new HashSet<>();
940 for (PluginInformation pi : pluginsToUpdate) {
941 for (String name : pi.getRequiredPlugins()) {
942 try {
943 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
944 if (installedPlugin == null) {
945 // New required plugin is not installed, find its PluginInformation
946 PluginInformation reqPlugin = null;
947 for (PluginInformation pi2 : allPlugins) {
948 if (pi2.getName().equals(name)) {
949 reqPlugin = pi2;
950 break;
951 }
952 }
953 // Required plugin is known but not already on download list
954 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
955 result.add(reqPlugin);
956 }
957 }
958 } catch (PluginException e) {
959 Main.warn(tr("Failed to find plugin {0}", name));
960 Main.error(e);
961 }
962 }
963 }
964 return result;
965 }
966
967 /**
968 * Updates the plugins in <code>plugins</code>.
969 *
970 * @param parent the parent component for message boxes
971 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
972 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
973 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
974 * @return the list of plugins to load
975 * @throws IllegalArgumentException if plugins is null
976 */
977 public static Collection<PluginInformation> updatePlugins(Component parent,
978 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
979 Collection<PluginInformation> plugins = null;
980 pluginDownloadTask = null;
981 if (monitor == null) {
982 monitor = NullProgressMonitor.INSTANCE;
983 }
984 try {
985 monitor.beginTask("");
986
987 // try to download the plugin lists
988 //
989 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
990 monitor.createSubTaskMonitor(1, false),
991 Main.pref.getOnlinePluginSites(), displayErrMsg
992 );
993 task1.run();
994 List<PluginInformation> allPlugins = null;
995
996 try {
997 allPlugins = task1.getAvailablePlugins();
998 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
999 // If only some plugins have to be updated, filter the list
1000 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
1001 for (Iterator<PluginInformation> it = plugins.iterator(); it.hasNext();) {
1002 PluginInformation pi = it.next();
1003 boolean found = false;
1004 for (PluginInformation piw : pluginsWanted) {
1005 if (pi.name.equals(piw.name)) {
1006 found = true;
1007 break;
1008 }
1009 }
1010 if (!found) {
1011 it.remove();
1012 }
1013 }
1014 }
1015 } catch (RuntimeException e) {
1016 Main.warn(tr("Failed to download plugin information list"));
1017 Main.error(e);
1018 // don't abort in case of error, continue with downloading plugins below
1019 }
1020
1021 // filter plugins which actually have to be updated
1022 //
1023 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
1024 for (PluginInformation pi: plugins) {
1025 if (pi.isUpdateRequired()) {
1026 pluginsToUpdate.add(pi);
1027 }
1028 }
1029
1030 if (!pluginsToUpdate.isEmpty()) {
1031
1032 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
1033
1034 if (allPlugins != null) {
1035 // Updated plugins may need additional plugin dependencies currently not installed
1036 //
1037 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
1038 pluginsToDownload.addAll(additionalPlugins);
1039
1040 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
1041 while (!additionalPlugins.isEmpty()) {
1042 // Install the additional plugins to load them later
1043 plugins.addAll(additionalPlugins);
1044 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
1045 pluginsToDownload.addAll(additionalPlugins);
1046 }
1047 }
1048
1049 // try to update the locally installed plugins
1050 //
1051 pluginDownloadTask = new PluginDownloadTask(
1052 monitor.createSubTaskMonitor(1, false),
1053 pluginsToDownload,
1054 tr("Update plugins")
1055 );
1056
1057 try {
1058 pluginDownloadTask.run();
1059 } catch (RuntimeException e) {
1060 Main.error(e);
1061 alertFailedPluginUpdate(parent, pluginsToUpdate);
1062 return plugins;
1063 }
1064
1065 // Update Plugin info for downloaded plugins
1066 //
1067 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
1068
1069 // notify user if downloading a locally installed plugin failed
1070 //
1071 if (!pluginDownloadTask.getFailedPlugins().isEmpty()) {
1072 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
1073 return plugins;
1074 }
1075 }
1076 } finally {
1077 monitor.finishTask();
1078 }
1079 if (pluginsWanted == null) {
1080 // if all plugins updated, remember the update because it was successful
1081 //
1082 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1083 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1084 }
1085 return plugins;
1086 }
1087
1088 /**
1089 * Ask the user for confirmation that a plugin shall be disabled.
1090 *
1091 * @param parent The parent component to be used for the displayed dialog
1092 * @param reason the reason for disabling the plugin
1093 * @param name the plugin name
1094 * @return true, if the plugin shall be disabled; false, otherwise
1095 */
1096 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1097 ButtonSpec[] options = new ButtonSpec[] {
1098 new ButtonSpec(
1099 tr("Disable plugin"),
1100 ImageProvider.get("dialogs", "delete"),
1101 tr("Click to delete the plugin ''{0}''", name),
1102 null /* no specific help context */
1103 ),
1104 new ButtonSpec(
1105 tr("Keep plugin"),
1106 ImageProvider.get("cancel"),
1107 tr("Click to keep the plugin ''{0}''", name),
1108 null /* no specific help context */
1109 )
1110 };
1111 int ret = -1;
1112 if (!GraphicsEnvironment.isHeadless()) {
1113 ret = HelpAwareOptionPane.showOptionDialog(
1114 parent,
1115 reason,
1116 tr("Disable plugin"),
1117 JOptionPane.WARNING_MESSAGE,
1118 null,
1119 options,
1120 options[0],
1121 null // FIXME: add help topic
1122 );
1123 }
1124 return ret == 0;
1125 }
1126
1127 /**
1128 * Returns the plugin of the specified name.
1129 * @param name The plugin name
1130 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1131 */
1132 public static Object getPlugin(String name) {
1133 for (PluginProxy plugin : pluginList) {
1134 if (plugin.getPluginInformation().name.equals(name))
1135 return plugin.plugin;
1136 }
1137 return null;
1138 }
1139
1140 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1141 for (PluginProxy p : pluginList) {
1142 p.addDownloadSelection(downloadSelections);
1143 }
1144 }
1145
1146 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1147 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1148 for (PluginProxy plugin : pluginList) {
1149 settings.add(new PluginPreferenceFactory(plugin));
1150 }
1151 return settings;
1152 }
1153
1154 /**
1155 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1156 * ".jar" files.
1157 *
1158 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1159 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1160 * installation of the respective plugin is silently skipped.
1161 *
1162 * @param dowarn if true, warning messages are displayed; false otherwise
1163 */
1164 public static void installDownloadedPlugins(boolean dowarn) {
1165 File pluginDir = Main.pref.getPluginsDirectory();
1166 if (!pluginDir.exists() || !pluginDir.isDirectory() || !pluginDir.canWrite())
1167 return;
1168
1169 final File[] files = pluginDir.listFiles(new FilenameFilter() {
1170 @Override
1171 public boolean accept(File dir, String name) {
1172 return name.endsWith(".jar.new");
1173 }
1174 });
1175 if (files == null)
1176 return;
1177
1178 for (File updatedPlugin : files) {
1179 final String filePath = updatedPlugin.getPath();
1180 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1181 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1182 if (plugin.exists() && !plugin.delete() && dowarn) {
1183 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1184 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1185 "Skipping installation. JOSM is still going to load the old plugin version.",
1186 pluginName));
1187 continue;
1188 }
1189 try {
1190 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1191 new JarFile(updatedPlugin).close();
1192 } catch (Exception e) {
1193 if (dowarn) {
1194 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}",
1195 plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1196 }
1197 continue;
1198 }
1199 // Install plugin
1200 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1201 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.",
1202 plugin.toString(), updatedPlugin.toString()));
1203 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1204 "Skipping installation. JOSM is still going to load the old plugin version.",
1205 pluginName));
1206 }
1207 }
1208 }
1209
1210 /**
1211 * Determines if the specified file is a valid and accessible JAR file.
1212 * @param jar The file to check
1213 * @return true if file can be opened as a JAR file.
1214 * @since 5723
1215 */
1216 public static boolean isValidJar(File jar) {
1217 if (jar != null && jar.exists() && jar.canRead()) {
1218 try {
1219 new JarFile(jar).close();
1220 } catch (Exception e) {
1221 Main.warn(e);
1222 return false;
1223 }
1224 return true;
1225 } else if (jar != null) {
1226 Main.warn("Invalid jar file ''"+jar+"'' (exists: "+jar.exists()+", canRead: "+jar.canRead()+")");
1227 }
1228 return false;
1229 }
1230
1231 /**
1232 * Replies the updated jar file for the given plugin name.
1233 * @param name The plugin name to find.
1234 * @return the updated jar file for the given plugin name. null if not found or not readable.
1235 * @since 5601
1236 */
1237 public static File findUpdatedJar(String name) {
1238 File pluginDir = Main.pref.getPluginsDirectory();
1239 // Find the downloaded file. We have tried to install the downloaded plugins
1240 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1241 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1242 if (!isValidJar(downloadedPluginFile)) {
1243 downloadedPluginFile = new File(pluginDir, name + ".jar");
1244 if (!isValidJar(downloadedPluginFile)) {
1245 return null;
1246 }
1247 }
1248 return downloadedPluginFile;
1249 }
1250
1251 /**
1252 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1253 * @param updatedPlugins The PluginInformation objects to update.
1254 * @since 5601
1255 */
1256 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1257 if (updatedPlugins == null) return;
1258 for (PluginInformation pi : updatedPlugins) {
1259 File downloadedPluginFile = findUpdatedJar(pi.name);
1260 if (downloadedPluginFile == null) {
1261 continue;
1262 }
1263 try {
1264 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1265 } catch (PluginException e) {
1266 Main.error(e);
1267 }
1268 }
1269 }
1270
1271 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1272 final ButtonSpec[] options = new ButtonSpec[] {
1273 new ButtonSpec(
1274 tr("Update plugin"),
1275 ImageProvider.get("dialogs", "refresh"),
1276 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1277 null /* no specific help context */
1278 ),
1279 new ButtonSpec(
1280 tr("Disable plugin"),
1281 ImageProvider.get("dialogs", "delete"),
1282 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1283 null /* no specific help context */
1284 ),
1285 new ButtonSpec(
1286 tr("Keep plugin"),
1287 ImageProvider.get("cancel"),
1288 tr("Click to keep the plugin ''{0}''", plugin.getPluginInformation().name),
1289 null /* no specific help context */
1290 )
1291 };
1292
1293 final StringBuilder msg = new StringBuilder();
1294 msg.append("<html>")
1295 .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.", plugin.getPluginInformation().name))
1296 .append("<br>");
1297 if (plugin.getPluginInformation().author != null) {
1298 msg.append(tr("According to the information within the plugin, the author is {0}.", plugin.getPluginInformation().author))
1299 .append("<br>");
1300 }
1301 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1302 .append("</html>");
1303
1304 try {
1305 FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
1306 @Override
1307 public Integer call() {
1308 return HelpAwareOptionPane.showOptionDialog(
1309 Main.parent,
1310 msg.toString(),
1311 tr("Update plugins"),
1312 JOptionPane.QUESTION_MESSAGE,
1313 null,
1314 options,
1315 options[0],
1316 ht("/ErrorMessages#ErrorInPlugin")
1317 );
1318 }
1319 });
1320 GuiHelper.runInEDT(task);
1321 return task.get();
1322 } catch (InterruptedException | ExecutionException e) {
1323 Main.warn(e);
1324 }
1325 return -1;
1326 }
1327
1328 /**
1329 * Replies the plugin which most likely threw the exception <code>ex</code>.
1330 *
1331 * @param ex the exception
1332 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1333 */
1334 private static PluginProxy getPluginCausingException(Throwable ex) {
1335 PluginProxy err = null;
1336 StackTraceElement[] stack = ex.getStackTrace();
1337 // remember the error position, as multiple plugins may be involved, we search the topmost one
1338 int pos = stack.length;
1339 for (PluginProxy p : pluginList) {
1340 String baseClass = p.getPluginInformation().className;
1341 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1342 for (int elpos = 0; elpos < pos; ++elpos) {
1343 if (stack[elpos].getClassName().startsWith(baseClass)) {
1344 pos = elpos;
1345 err = p;
1346 }
1347 }
1348 }
1349 return err;
1350 }
1351
1352 /**
1353 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1354 * conditionally updates or deactivates the plugin, but asks the user first.
1355 *
1356 * @param e the exception
1357 * @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
1358 */
1359 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1360 PluginProxy plugin = null;
1361 // Check for an explicit problem when calling a plugin function
1362 if (e instanceof PluginException) {
1363 plugin = ((PluginException) e).plugin;
1364 }
1365 if (plugin == null) {
1366 plugin = getPluginCausingException(e);
1367 }
1368 if (plugin == null)
1369 // don't know what plugin threw the exception
1370 return null;
1371
1372 Set<String> plugins = new HashSet<>(
1373 Main.pref.getCollection("plugins", Collections.<String>emptySet())
1374 );
1375 final PluginInformation pluginInfo = plugin.getPluginInformation();
1376 if (!plugins.contains(pluginInfo.name))
1377 // plugin not activated ? strange in this context but anyway, don't bother
1378 // the user with dialogs, skip conditional deactivation
1379 return null;
1380
1381 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1382 case 0:
1383 // update the plugin
1384 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1385 return pluginDownloadTask;
1386 case 1:
1387 // deactivate the plugin
1388 plugins.remove(plugin.getPluginInformation().name);
1389 Main.pref.putCollection("plugins", plugins);
1390 GuiHelper.runInEDTAndWait(new Runnable() {
1391 @Override
1392 public void run() {
1393 JOptionPane.showMessageDialog(
1394 Main.parent,
1395 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1396 tr("Information"),
1397 JOptionPane.INFORMATION_MESSAGE
1398 );
1399 }
1400 });
1401 return null;
1402 default:
1403 // user doesn't want to deactivate the plugin
1404 return null;
1405 }
1406 }
1407
1408 /**
1409 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1410 * @return The list of loaded plugins (one plugin per line)
1411 */
1412 public static String getBugReportText() {
1413 StringBuilder text = new StringBuilder();
1414 List<String> pl = new LinkedList<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
1415 for (final PluginProxy pp : pluginList) {
1416 PluginInformation pi = pp.getPluginInformation();
1417 pl.remove(pi.name);
1418 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1419 ? pi.localversion : "unknown") + ')');
1420 }
1421 Collections.sort(pl);
1422 if (!pl.isEmpty()) {
1423 text.append("Plugins:\n");
1424 }
1425 for (String s : pl) {
1426 text.append("- ").append(s).append('\n');
1427 }
1428 return text.toString();
1429 }
1430
1431 /**
1432 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1433 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1434 */
1435 public static JPanel getInfoPanel() {
1436 JPanel pluginTab = new JPanel(new GridBagLayout());
1437 for (final PluginProxy p : pluginList) {
1438 final PluginInformation info = p.getPluginInformation();
1439 String name = info.name
1440 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1441 pluginTab.add(new JLabel(name), GBC.std());
1442 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1443 pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1444 @Override
1445 public void actionPerformed(ActionEvent event) {
1446 StringBuilder b = new StringBuilder();
1447 for (Entry<String, String> e : info.attr.entrySet()) {
1448 b.append(e.getKey());
1449 b.append(": ");
1450 b.append(e.getValue());
1451 b.append('\n');
1452 }
1453 JosmTextArea a = new JosmTextArea(10, 40);
1454 a.setEditable(false);
1455 a.setText(b.toString());
1456 a.setCaretPosition(0);
1457 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1458 JOptionPane.INFORMATION_MESSAGE);
1459 }
1460 }), GBC.eol());
1461
1462 JosmTextArea description = new JosmTextArea(info.description == null ? tr("no description available")
1463 : info.description);
1464 description.setEditable(false);
1465 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1466 description.setLineWrap(true);
1467 description.setWrapStyleWord(true);
1468 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1469 description.setBackground(UIManager.getColor("Panel.background"));
1470 description.setCaretPosition(0);
1471
1472 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1473 }
1474 return pluginTab;
1475 }
1476
1477 /**
1478 * Returns the set of deprecated and unmaintained plugins.
1479 * @return set of deprecated and unmaintained plugins names.
1480 * @since 8938
1481 */
1482 public static Set<String> getDeprecatedAndUnmaintainedPlugins() {
1483 Set<String> result = new HashSet<>(DEPRECATED_PLUGINS.size() + UNMAINTAINED_PLUGINS.length);
1484 for (DeprecatedPlugin dp : DEPRECATED_PLUGINS) {
1485 result.add(dp.name);
1486 }
1487 result.addAll(Arrays.asList(UNMAINTAINED_PLUGINS));
1488 return result;
1489 }
1490
1491 private static class UpdatePluginsMessagePanel extends JPanel {
1492 private JMultilineLabel lblMessage;
1493 private JCheckBox cbDontShowAgain;
1494
1495 protected final void build() {
1496 setLayout(new GridBagLayout());
1497 GridBagConstraints gc = new GridBagConstraints();
1498 gc.anchor = GridBagConstraints.NORTHWEST;
1499 gc.fill = GridBagConstraints.BOTH;
1500 gc.weightx = 1.0;
1501 gc.weighty = 1.0;
1502 gc.insets = new Insets(5, 5, 5, 5);
1503 add(lblMessage = new JMultilineLabel(""), gc);
1504 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1505
1506 gc.gridy = 1;
1507 gc.fill = GridBagConstraints.HORIZONTAL;
1508 gc.weighty = 0.0;
1509 add(cbDontShowAgain = new JCheckBox(
1510 tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)")), gc);
1511 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1512 }
1513
1514 UpdatePluginsMessagePanel() {
1515 build();
1516 }
1517
1518 public void setMessage(String message) {
1519 lblMessage.setText(message);
1520 }
1521
1522 public void initDontShowAgain(String preferencesKey) {
1523 String policy = Main.pref.get(preferencesKey, "ask");
1524 policy = policy.trim().toLowerCase(Locale.ENGLISH);
1525 cbDontShowAgain.setSelected(!"ask".equals(policy));
1526 }
1527
1528 public boolean isRememberDecision() {
1529 return cbDontShowAgain.isSelected();
1530 }
1531 }
1532}
Note: See TracBrowser for help on using the repository browser.