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

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

sonar - squid:S2301 - Public methods should not contain selector arguments

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