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

Last change on this file since 17534 was 17511, checked in by Don-vip, 3 years ago

fix #20458 - don't clear plugin classloaders each time we load a category of plugins

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