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

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

add safeguard for circular references leading to infinite loop (ref [12924])

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