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

Last change on this file since 12322 was 12322, checked in by bastiK, 7 years ago

fixed #14901 - restrict plugin classpath

Separate class loader for each plugin instead of unified class loader.

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