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

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

sonar - squid:S2156 - "final" classes should not have "protected" members

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