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

Last change on this file since 11320 was 11288, checked in by simon04, 7 years ago

see #13376 - Use TimeUnit instead of combinations of 1000/60/60/24

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