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

Last change on this file since 14145 was 14121, checked in by Don-vip, 6 years ago

see #15229 - deprecate all Main methods related to network features. New NetworkManager class

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