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

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

fix #14613 - Special HTML characters not escaped in GUI error messages

  • Property svn:eol-style set to native
File size: 63.3 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 public static Collection<ClassLoader> getResourceClassLoaders() {
284 return Collections.unmodifiableCollection(sources);
285 }
286
287 /**
288 * Removes deprecated plugins from a collection of plugins. Modifies the
289 * collection <code>plugins</code>.
290 *
291 * Also notifies the user about removed deprecated plugins
292 *
293 * @param parent The parent Component used to display warning popup
294 * @param plugins the collection of plugins
295 */
296 static void filterDeprecatedPlugins(Component parent, Collection<String> plugins) {
297 Set<DeprecatedPlugin> removedPlugins = new TreeSet<>();
298 for (DeprecatedPlugin depr : DEPRECATED_PLUGINS) {
299 if (plugins.contains(depr.name)) {
300 plugins.remove(depr.name);
301 Main.pref.removeFromCollection("plugins", depr.name);
302 removedPlugins.add(depr);
303 }
304 }
305 if (removedPlugins.isEmpty())
306 return;
307
308 // notify user about removed deprecated plugins
309 //
310 StringBuilder sb = new StringBuilder(32);
311 sb.append("<html>")
312 .append(trn(
313 "The following plugin is no longer necessary and has been deactivated:",
314 "The following plugins are no longer necessary and have been deactivated:",
315 removedPlugins.size()))
316 .append("<ul>");
317 for (DeprecatedPlugin depr: removedPlugins) {
318 sb.append("<li>").append(depr.name);
319 if (depr.reason != null) {
320 sb.append(" (").append(depr.reason).append(')');
321 }
322 sb.append("</li>");
323 }
324 sb.append("</ul></html>");
325 if (!GraphicsEnvironment.isHeadless()) {
326 JOptionPane.showMessageDialog(
327 parent,
328 sb.toString(),
329 tr("Warning"),
330 JOptionPane.WARNING_MESSAGE
331 );
332 }
333 }
334
335 /**
336 * Removes unmaintained plugins from a collection of plugins. Modifies the
337 * collection <code>plugins</code>. Also removes the plugin from the list
338 * of plugins in the preferences, if necessary.
339 *
340 * Asks the user for every unmaintained plugin whether it should be removed.
341 * @param parent The parent Component used to display warning popup
342 *
343 * @param plugins the collection of plugins
344 */
345 static void filterUnmaintainedPlugins(Component parent, Collection<String> plugins) {
346 for (String unmaintained : UNMAINTAINED_PLUGINS) {
347 if (!plugins.contains(unmaintained)) {
348 continue;
349 }
350 String msg = tr("<html>Loading of the plugin \"{0}\" was requested."
351 + "<br>This plugin is no longer developed and very likely will produce errors."
352 +"<br>It should be disabled.<br>Delete from preferences?</html>",
353 Utils.escapeReservedCharactersHTML(unmaintained));
354 if (confirmDisablePlugin(parent, msg, unmaintained)) {
355 Main.pref.removeFromCollection("plugins", unmaintained);
356 plugins.remove(unmaintained);
357 }
358 }
359 }
360
361 /**
362 * Checks whether the locally available plugins should be updated and
363 * asks the user if running an update is OK. An update is advised if
364 * JOSM was updated to a new version since the last plugin updates or
365 * if the plugins were last updated a long time ago.
366 *
367 * @param parent the parent component relative to which the confirmation dialog
368 * is to be displayed
369 * @return true if a plugin update should be run; false, otherwise
370 */
371 public static boolean checkAndConfirmPluginUpdate(Component parent) {
372 if (!checkOfflineAccess()) {
373 Main.info(tr("{0} not available (offline mode)", tr("Plugin update")));
374 return false;
375 }
376 String message = null;
377 String togglePreferenceKey = null;
378 int v = Version.getInstance().getVersion();
379 if (Main.pref.getInteger("pluginmanager.version", 0) < v) {
380 message =
381 "<html>"
382 + tr("You updated your JOSM software.<br>"
383 + "To prevent problems the plugins should be updated as well.<br><br>"
384 + "Update plugins now?"
385 )
386 + "</html>";
387 togglePreferenceKey = "pluginmanager.version-based-update.policy";
388 } else {
389 long tim = System.currentTimeMillis();
390 long last = Main.pref.getLong("pluginmanager.lastupdate", 0);
391 Integer maxTime = Main.pref.getInteger("pluginmanager.time-based-update.interval", DEFAULT_TIME_BASED_UPDATE_INTERVAL);
392 long d = TimeUnit.MILLISECONDS.toDays(tim - last);
393 if ((last <= 0) || (maxTime <= 0)) {
394 Main.pref.put("pluginmanager.lastupdate", Long.toString(tim));
395 } else if (d > maxTime) {
396 message =
397 "<html>"
398 + tr("Last plugin update more than {0} days ago.", d)
399 + "</html>";
400 togglePreferenceKey = "pluginmanager.time-based-update.policy";
401 }
402 }
403 if (message == null) return false;
404
405 UpdatePluginsMessagePanel pnlMessage = new UpdatePluginsMessagePanel();
406 pnlMessage.setMessage(message);
407 pnlMessage.initDontShowAgain(togglePreferenceKey);
408
409 // check whether automatic update at startup was disabled
410 //
411 String policy = Main.pref.get(togglePreferenceKey, "ask").trim().toLowerCase(Locale.ENGLISH);
412 switch(policy) {
413 case "never":
414 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
415 Main.info(tr("Skipping plugin update after JOSM upgrade. Automatic update at startup is disabled."));
416 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
417 Main.info(tr("Skipping plugin update after elapsed update interval. Automatic update at startup is disabled."));
418 }
419 return false;
420
421 case "always":
422 if ("pluginmanager.version-based-update.policy".equals(togglePreferenceKey)) {
423 Main.info(tr("Running plugin update after JOSM upgrade. Automatic update at startup is enabled."));
424 } else if ("pluginmanager.time-based-update.policy".equals(togglePreferenceKey)) {
425 Main.info(tr("Running plugin update after elapsed update interval. Automatic update at startup is disabled."));
426 }
427 return true;
428
429 case "ask":
430 break;
431
432 default:
433 Main.warn(tr("Unexpected value ''{0}'' for preference ''{1}''. Assuming value ''ask''.", policy, togglePreferenceKey));
434 }
435
436 ButtonSpec[] options = new ButtonSpec[] {
437 new ButtonSpec(
438 tr("Update plugins"),
439 ImageProvider.get("dialogs", "refresh"),
440 tr("Click to update the activated plugins"),
441 null /* no specific help context */
442 ),
443 new ButtonSpec(
444 tr("Skip update"),
445 ImageProvider.get("cancel"),
446 tr("Click to skip updating the activated plugins"),
447 null /* no specific help context */
448 )
449 };
450
451 int ret = HelpAwareOptionPane.showOptionDialog(
452 parent,
453 pnlMessage,
454 tr("Update plugins"),
455 JOptionPane.WARNING_MESSAGE,
456 null,
457 options,
458 options[0],
459 ht("/Preferences/Plugins#AutomaticUpdate")
460 );
461
462 if (pnlMessage.isRememberDecision()) {
463 switch(ret) {
464 case 0:
465 Main.pref.put(togglePreferenceKey, "always");
466 break;
467 case JOptionPane.CLOSED_OPTION:
468 case 1:
469 Main.pref.put(togglePreferenceKey, "never");
470 break;
471 default: // Do nothing
472 }
473 } else {
474 Main.pref.put(togglePreferenceKey, "ask");
475 }
476 return ret == 0;
477 }
478
479 private static boolean checkOfflineAccess() {
480 if (Main.isOffline(OnlineResource.ALL)) {
481 return false;
482 }
483 if (Main.isOffline(OnlineResource.JOSM_WEBSITE)) {
484 for (String updateSite : Main.pref.getPluginSites()) {
485 try {
486 OnlineResource.JOSM_WEBSITE.checkOfflineAccess(updateSite, Main.getJOSMWebsite());
487 } catch (OfflineAccessException e) {
488 Main.trace(e);
489 return false;
490 }
491 }
492 }
493 return true;
494 }
495
496 /**
497 * Alerts the user if a plugin required by another plugin is missing, and offer to download them &amp; restart JOSM
498 *
499 * @param parent The parent Component used to display error popup
500 * @param plugin the plugin
501 * @param missingRequiredPlugin the missing required plugin
502 */
503 private static void alertMissingRequiredPlugin(Component parent, String plugin, Set<String> missingRequiredPlugin) {
504 StringBuilder sb = new StringBuilder(48);
505 sb.append("<html>")
506 .append(trn("Plugin {0} requires a plugin which was not found. The missing plugin is:",
507 "Plugin {0} requires {1} plugins which were not found. The missing plugins are:",
508 missingRequiredPlugin.size(),
509 Utils.escapeReservedCharactersHTML(plugin),
510 missingRequiredPlugin.size()))
511 .append(Utils.joinAsHtmlUnorderedList(missingRequiredPlugin))
512 .append("</html>");
513 ButtonSpec[] specs = new ButtonSpec[] {
514 new ButtonSpec(
515 tr("Download and restart"),
516 ImageProvider.get("restart"),
517 trn("Click to download missing plugin and restart JOSM",
518 "Click to download missing plugins and restart JOSM",
519 missingRequiredPlugin.size()),
520 null /* no specific help text */
521 ),
522 new ButtonSpec(
523 tr("Continue"),
524 ImageProvider.get("ok"),
525 trn("Click to continue without this plugin",
526 "Click to continue without these plugins",
527 missingRequiredPlugin.size()),
528 null /* no specific help text */
529 )
530 };
531 if (0 == HelpAwareOptionPane.showOptionDialog(
532 parent,
533 sb.toString(),
534 tr("Error"),
535 JOptionPane.ERROR_MESSAGE,
536 null, /* no special icon */
537 specs,
538 specs[0],
539 ht("/Plugin/Loading#MissingRequiredPlugin"))) {
540 downloadRequiredPluginsAndRestart(parent, missingRequiredPlugin);
541 }
542 }
543
544 private static void downloadRequiredPluginsAndRestart(final Component parent, final Set<String> missingRequiredPlugin) {
545 // Update plugin list
546 final ReadRemotePluginInformationTask pluginInfoDownloadTask = new ReadRemotePluginInformationTask(
547 Main.pref.getOnlinePluginSites());
548 Main.worker.submit(pluginInfoDownloadTask);
549
550 // Continuation
551 Main.worker.submit(() -> {
552 // Build list of plugins to download
553 Set<PluginInformation> toDownload = new HashSet<>(pluginInfoDownloadTask.getAvailablePlugins());
554 toDownload.removeIf(info -> !missingRequiredPlugin.contains(info.getName()));
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>", Utils.escapeReservedCharactersHTML(plugin.name), plugin.className);
727 }
728 } catch (RuntimeException e) { // NOPMD
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) { // NOPMD
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 if (infos != null) {
896 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
897 String plugin = it.next();
898 if (infos.containsKey(plugin)) {
899 ret.add(infos.get(plugin));
900 it.remove();
901 }
902 }
903 }
904 if (!plugins.isEmpty()) {
905 alertMissingPluginInformation(parent, plugins);
906 }
907 return ret;
908 } finally {
909 monitor.finishTask();
910 }
911 }
912
913 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
914 StringBuilder sb = new StringBuilder(128);
915 sb.append("<html>")
916 .append(trn(
917 "Updating the following plugin has failed:",
918 "Updating the following plugins has failed:",
919 plugins.size()))
920 .append("<ul>");
921 for (PluginInformation pi: plugins) {
922 sb.append("<li>").append(Utils.escapeReservedCharactersHTML(pi.name)).append("</li>");
923 }
924 sb.append("</ul>")
925 .append(trn(
926 "Please open the Preference Dialog after JOSM has started and try to update it manually.",
927 "Please open the Preference Dialog after JOSM has started and try to update them manually.",
928 plugins.size()))
929 .append("</html>");
930 HelpAwareOptionPane.showOptionDialog(
931 parent,
932 sb.toString(),
933 tr("Plugin update failed"),
934 JOptionPane.ERROR_MESSAGE,
935 ht("/Plugin/Loading#FailedPluginUpdated")
936 );
937 }
938
939 private static Set<PluginInformation> findRequiredPluginsToDownload(
940 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
941 Set<PluginInformation> result = new HashSet<>();
942 for (PluginInformation pi : pluginsToUpdate) {
943 for (String name : pi.getRequiredPlugins()) {
944 try {
945 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
946 if (installedPlugin == null) {
947 // New required plugin is not installed, find its PluginInformation
948 PluginInformation reqPlugin = null;
949 for (PluginInformation pi2 : allPlugins) {
950 if (pi2.getName().equals(name)) {
951 reqPlugin = pi2;
952 break;
953 }
954 }
955 // Required plugin is known but not already on download list
956 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
957 result.add(reqPlugin);
958 }
959 }
960 } catch (PluginException e) {
961 Main.warn(tr("Failed to find plugin {0}", name));
962 Main.error(e);
963 }
964 }
965 }
966 return result;
967 }
968
969 /**
970 * Updates the plugins in <code>plugins</code>.
971 *
972 * @param parent the parent component for message boxes
973 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
974 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
975 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
976 * @return the list of plugins to load
977 * @throws IllegalArgumentException if plugins is null
978 */
979 public static Collection<PluginInformation> updatePlugins(Component parent,
980 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
981 Collection<PluginInformation> plugins = null;
982 pluginDownloadTask = null;
983 if (monitor == null) {
984 monitor = NullProgressMonitor.INSTANCE;
985 }
986 try {
987 monitor.beginTask("");
988
989 // try to download the plugin lists
990 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
991 monitor.createSubTaskMonitor(1, false),
992 Main.pref.getOnlinePluginSites(), displayErrMsg
993 );
994 task1.run();
995 List<PluginInformation> allPlugins = task1.getAvailablePlugins();
996
997 try {
998 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
999 // If only some plugins have to be updated, filter the list
1000 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
1001 final Collection<String> pluginsWantedName = Utils.transform(pluginsWanted, piw -> piw.name);
1002 plugins = SubclassFilteredCollection.filter(plugins, pi -> pluginsWantedName.contains(pi.name));
1003 }
1004 } catch (RuntimeException e) { // NOPMD
1005 Main.warn(tr("Failed to download plugin information list"));
1006 Main.error(e);
1007 // don't abort in case of error, continue with downloading plugins below
1008 }
1009
1010 // filter plugins which actually have to be updated
1011 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
1012 if (plugins != null) {
1013 for (PluginInformation pi: plugins) {
1014 if (pi.isUpdateRequired()) {
1015 pluginsToUpdate.add(pi);
1016 }
1017 }
1018 }
1019
1020 if (!pluginsToUpdate.isEmpty()) {
1021
1022 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
1023
1024 if (allPlugins != null) {
1025 // Updated plugins may need additional plugin dependencies currently not installed
1026 //
1027 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
1028 pluginsToDownload.addAll(additionalPlugins);
1029
1030 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
1031 while (!additionalPlugins.isEmpty()) {
1032 // Install the additional plugins to load them later
1033 if (plugins != null)
1034 plugins.addAll(additionalPlugins);
1035 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
1036 pluginsToDownload.addAll(additionalPlugins);
1037 }
1038 }
1039
1040 // try to update the locally installed plugins
1041 pluginDownloadTask = new PluginDownloadTask(
1042 monitor.createSubTaskMonitor(1, false),
1043 pluginsToDownload,
1044 tr("Update plugins")
1045 );
1046
1047 try {
1048 pluginDownloadTask.run();
1049 } catch (RuntimeException e) { // NOPMD
1050 Main.error(e);
1051 alertFailedPluginUpdate(parent, pluginsToUpdate);
1052 return plugins;
1053 }
1054
1055 // Update Plugin info for downloaded plugins
1056 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
1057
1058 // notify user if downloading a locally installed plugin failed
1059 if (!pluginDownloadTask.getFailedPlugins().isEmpty()) {
1060 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
1061 return plugins;
1062 }
1063 }
1064 } finally {
1065 monitor.finishTask();
1066 }
1067 if (pluginsWanted == null) {
1068 // if all plugins updated, remember the update because it was successful
1069 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1070 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1071 }
1072 return plugins;
1073 }
1074
1075 /**
1076 * Ask the user for confirmation that a plugin shall be disabled.
1077 *
1078 * @param parent The parent component to be used for the displayed dialog
1079 * @param reason the reason for disabling the plugin
1080 * @param name the plugin name
1081 * @return true, if the plugin shall be disabled; false, otherwise
1082 */
1083 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1084 ButtonSpec[] options = new ButtonSpec[] {
1085 new ButtonSpec(
1086 tr("Disable plugin"),
1087 ImageProvider.get("dialogs", "delete"),
1088 tr("Click to delete the plugin ''{0}''", name),
1089 null /* no specific help context */
1090 ),
1091 new ButtonSpec(
1092 tr("Keep plugin"),
1093 ImageProvider.get("cancel"),
1094 tr("Click to keep the plugin ''{0}''", name),
1095 null /* no specific help context */
1096 )
1097 };
1098 return 0 == HelpAwareOptionPane.showOptionDialog(
1099 parent,
1100 reason,
1101 tr("Disable plugin"),
1102 JOptionPane.WARNING_MESSAGE,
1103 null,
1104 options,
1105 options[0],
1106 null // FIXME: add help topic
1107 );
1108 }
1109
1110 /**
1111 * Returns the plugin of the specified name.
1112 * @param name The plugin name
1113 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1114 */
1115 public static Object getPlugin(String name) {
1116 for (PluginProxy plugin : pluginList) {
1117 if (plugin.getPluginInformation().name.equals(name))
1118 return plugin.plugin;
1119 }
1120 return null;
1121 }
1122
1123 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1124 for (PluginProxy p : pluginList) {
1125 p.addDownloadSelection(downloadSelections);
1126 }
1127 }
1128
1129 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1130 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1131 for (PluginProxy plugin : pluginList) {
1132 settings.add(new PluginPreferenceFactory(plugin));
1133 }
1134 return settings;
1135 }
1136
1137 /**
1138 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1139 * ".jar" files.
1140 *
1141 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1142 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1143 * installation of the respective plugin is silently skipped.
1144 *
1145 * @param dowarn if true, warning messages are displayed; false otherwise
1146 */
1147 public static void installDownloadedPlugins(boolean dowarn) {
1148 File pluginDir = Main.pref.getPluginsDirectory();
1149 if (!pluginDir.exists() || !pluginDir.isDirectory() || !pluginDir.canWrite())
1150 return;
1151
1152 final File[] files = pluginDir.listFiles((FilenameFilter) (dir, name) -> name.endsWith(".jar.new"));
1153 if (files == null)
1154 return;
1155
1156 for (File updatedPlugin : files) {
1157 final String filePath = updatedPlugin.getPath();
1158 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1159 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1160 if (plugin.exists() && !plugin.delete() && dowarn) {
1161 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1162 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1163 "Skipping installation. JOSM is still going to load the old plugin version.",
1164 pluginName));
1165 continue;
1166 }
1167 try {
1168 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1169 new JarFile(updatedPlugin).close();
1170 } catch (IOException e) {
1171 if (dowarn) {
1172 Main.warn(e, tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}",
1173 plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1174 }
1175 continue;
1176 }
1177 // Install plugin
1178 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1179 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.",
1180 plugin.toString(), updatedPlugin.toString()));
1181 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1182 "Skipping installation. JOSM is still going to load the old plugin version.",
1183 pluginName));
1184 }
1185 }
1186 }
1187
1188 /**
1189 * Determines if the specified file is a valid and accessible JAR file.
1190 * @param jar The file to check
1191 * @return true if file can be opened as a JAR file.
1192 * @since 5723
1193 */
1194 public static boolean isValidJar(File jar) {
1195 if (jar != null && jar.exists() && jar.canRead()) {
1196 try {
1197 new JarFile(jar).close();
1198 } catch (IOException e) {
1199 Main.warn(e);
1200 return false;
1201 }
1202 return true;
1203 } else if (jar != null) {
1204 Main.warn("Invalid jar file ''"+jar+"'' (exists: "+jar.exists()+", canRead: "+jar.canRead()+')');
1205 }
1206 return false;
1207 }
1208
1209 /**
1210 * Replies the updated jar file for the given plugin name.
1211 * @param name The plugin name to find.
1212 * @return the updated jar file for the given plugin name. null if not found or not readable.
1213 * @since 5601
1214 */
1215 public static File findUpdatedJar(String name) {
1216 File pluginDir = Main.pref.getPluginsDirectory();
1217 // Find the downloaded file. We have tried to install the downloaded plugins
1218 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1219 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1220 if (!isValidJar(downloadedPluginFile)) {
1221 downloadedPluginFile = new File(pluginDir, name + ".jar");
1222 if (!isValidJar(downloadedPluginFile)) {
1223 return null;
1224 }
1225 }
1226 return downloadedPluginFile;
1227 }
1228
1229 /**
1230 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1231 * @param updatedPlugins The PluginInformation objects to update.
1232 * @since 5601
1233 */
1234 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1235 if (updatedPlugins == null) return;
1236 for (PluginInformation pi : updatedPlugins) {
1237 File downloadedPluginFile = findUpdatedJar(pi.name);
1238 if (downloadedPluginFile == null) {
1239 continue;
1240 }
1241 try {
1242 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1243 } catch (PluginException e) {
1244 Main.error(e);
1245 }
1246 }
1247 }
1248
1249 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1250 final ButtonSpec[] options = new ButtonSpec[] {
1251 new ButtonSpec(
1252 tr("Update plugin"),
1253 ImageProvider.get("dialogs", "refresh"),
1254 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1255 null /* no specific help context */
1256 ),
1257 new ButtonSpec(
1258 tr("Disable plugin"),
1259 ImageProvider.get("dialogs", "delete"),
1260 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1261 null /* no specific help context */
1262 ),
1263 new ButtonSpec(
1264 tr("Keep plugin"),
1265 ImageProvider.get("cancel"),
1266 tr("Click to keep the plugin ''{0}''", plugin.getPluginInformation().name),
1267 null /* no specific help context */
1268 )
1269 };
1270
1271 final StringBuilder msg = new StringBuilder(256);
1272 msg.append("<html>")
1273 .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.",
1274 Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().name)))
1275 .append("<br>");
1276 if (plugin.getPluginInformation().author != null) {
1277 msg.append(tr("According to the information within the plugin, the author is {0}.",
1278 Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().author)))
1279 .append("<br>");
1280 }
1281 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1282 .append("</html>");
1283
1284 try {
1285 FutureTask<Integer> task = new FutureTask<>(() -> HelpAwareOptionPane.showOptionDialog(
1286 Main.parent,
1287 msg.toString(),
1288 tr("Update plugins"),
1289 JOptionPane.QUESTION_MESSAGE,
1290 null,
1291 options,
1292 options[0],
1293 ht("/ErrorMessages#ErrorInPlugin")
1294 ));
1295 GuiHelper.runInEDT(task);
1296 return task.get();
1297 } catch (InterruptedException | ExecutionException e) {
1298 Main.warn(e);
1299 }
1300 return -1;
1301 }
1302
1303 /**
1304 * Replies the plugin which most likely threw the exception <code>ex</code>.
1305 *
1306 * @param ex the exception
1307 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1308 */
1309 private static PluginProxy getPluginCausingException(Throwable ex) {
1310 PluginProxy err = null;
1311 StackTraceElement[] stack = ex.getStackTrace();
1312 // remember the error position, as multiple plugins may be involved, we search the topmost one
1313 int pos = stack.length;
1314 for (PluginProxy p : pluginList) {
1315 String baseClass = p.getPluginInformation().className;
1316 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1317 for (int elpos = 0; elpos < pos; ++elpos) {
1318 if (stack[elpos].getClassName().startsWith(baseClass)) {
1319 pos = elpos;
1320 err = p;
1321 }
1322 }
1323 }
1324 return err;
1325 }
1326
1327 /**
1328 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1329 * conditionally updates or deactivates the plugin, but asks the user first.
1330 *
1331 * @param e the exception
1332 * @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
1333 */
1334 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1335 PluginProxy plugin = null;
1336 // Check for an explicit problem when calling a plugin function
1337 if (e instanceof PluginException) {
1338 plugin = ((PluginException) e).plugin;
1339 }
1340 if (plugin == null) {
1341 plugin = getPluginCausingException(e);
1342 }
1343 if (plugin == null)
1344 // don't know what plugin threw the exception
1345 return null;
1346
1347 Set<String> plugins = new HashSet<>(
1348 Main.pref.getCollection("plugins", Collections.<String>emptySet())
1349 );
1350 final PluginInformation pluginInfo = plugin.getPluginInformation();
1351 if (!plugins.contains(pluginInfo.name))
1352 // plugin not activated ? strange in this context but anyway, don't bother
1353 // the user with dialogs, skip conditional deactivation
1354 return null;
1355
1356 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1357 case 0:
1358 // update the plugin
1359 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1360 return pluginDownloadTask;
1361 case 1:
1362 // deactivate the plugin
1363 plugins.remove(plugin.getPluginInformation().name);
1364 Main.pref.putCollection("plugins", plugins);
1365 GuiHelper.runInEDTAndWait(() -> JOptionPane.showMessageDialog(
1366 Main.parent,
1367 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1368 tr("Information"),
1369 JOptionPane.INFORMATION_MESSAGE
1370 ));
1371 return null;
1372 default:
1373 // user doesn't want to deactivate the plugin
1374 return null;
1375 }
1376 }
1377
1378 /**
1379 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1380 * @return The list of loaded plugins
1381 */
1382 public static Collection<String> getBugReportInformation() {
1383 final Collection<String> pl = new TreeSet<>(Main.pref.getCollection("plugins", new LinkedList<>()));
1384 for (final PluginProxy pp : pluginList) {
1385 PluginInformation pi = pp.getPluginInformation();
1386 pl.remove(pi.name);
1387 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1388 ? pi.localversion : "unknown") + ')');
1389 }
1390 return pl;
1391 }
1392
1393 /**
1394 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1395 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1396 */
1397 public static JPanel getInfoPanel() {
1398 JPanel pluginTab = new JPanel(new GridBagLayout());
1399 for (final PluginProxy p : pluginList) {
1400 final PluginInformation info = p.getPluginInformation();
1401 String name = info.name
1402 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1403 pluginTab.add(new JLabel(name), GBC.std());
1404 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1405 pluginTab.add(new JButton(new AbstractAction(tr("Information")) {
1406 @Override
1407 public void actionPerformed(ActionEvent event) {
1408 StringBuilder b = new StringBuilder();
1409 for (Entry<String, String> e : info.attr.entrySet()) {
1410 b.append(e.getKey());
1411 b.append(": ");
1412 b.append(e.getValue());
1413 b.append('\n');
1414 }
1415 JosmTextArea a = new JosmTextArea(10, 40);
1416 a.setEditable(false);
1417 a.setText(b.toString());
1418 a.setCaretPosition(0);
1419 JOptionPane.showMessageDialog(Main.parent, new JScrollPane(a), tr("Plugin information"),
1420 JOptionPane.INFORMATION_MESSAGE);
1421 }
1422 }), GBC.eol());
1423
1424 JosmTextArea description = new JosmTextArea(info.description == null ? tr("no description available")
1425 : info.description);
1426 description.setEditable(false);
1427 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1428 description.setLineWrap(true);
1429 description.setWrapStyleWord(true);
1430 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1431 description.setBackground(UIManager.getColor("Panel.background"));
1432 description.setCaretPosition(0);
1433
1434 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1435 }
1436 return pluginTab;
1437 }
1438
1439 /**
1440 * Returns the set of deprecated and unmaintained plugins.
1441 * @return set of deprecated and unmaintained plugins names.
1442 * @since 8938
1443 */
1444 public static Set<String> getDeprecatedAndUnmaintainedPlugins() {
1445 Set<String> result = new HashSet<>(DEPRECATED_PLUGINS.size() + UNMAINTAINED_PLUGINS.size());
1446 for (DeprecatedPlugin dp : DEPRECATED_PLUGINS) {
1447 result.add(dp.name);
1448 }
1449 result.addAll(UNMAINTAINED_PLUGINS);
1450 return result;
1451 }
1452
1453 private static class UpdatePluginsMessagePanel extends JPanel {
1454 private final JMultilineLabel lblMessage = new JMultilineLabel("");
1455 private final JCheckBox cbDontShowAgain = new JCheckBox(
1456 tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)"));
1457
1458 UpdatePluginsMessagePanel() {
1459 build();
1460 }
1461
1462 protected final void build() {
1463 setLayout(new GridBagLayout());
1464 GridBagConstraints gc = new GridBagConstraints();
1465 gc.anchor = GridBagConstraints.NORTHWEST;
1466 gc.fill = GridBagConstraints.BOTH;
1467 gc.weightx = 1.0;
1468 gc.weighty = 1.0;
1469 gc.insets = new Insets(5, 5, 5, 5);
1470 add(lblMessage, gc);
1471 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1472
1473 gc.gridy = 1;
1474 gc.fill = GridBagConstraints.HORIZONTAL;
1475 gc.weighty = 0.0;
1476 add(cbDontShowAgain, gc);
1477 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1478 }
1479
1480 public void setMessage(String message) {
1481 lblMessage.setText(message);
1482 }
1483
1484 public void initDontShowAgain(String preferencesKey) {
1485 String policy = Main.pref.get(preferencesKey, "ask");
1486 policy = policy.trim().toLowerCase(Locale.ENGLISH);
1487 cbDontShowAgain.setSelected(!"ask".equals(policy));
1488 }
1489
1490 public boolean isRememberDecision() {
1491 return cbDontShowAgain.isSelected();
1492 }
1493 }
1494}
Note: See TracBrowser for help on using the repository browser.