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

Last change on this file since 18368 was 18368, checked in by taylor.smock, 2 years ago

fix #21819: MapDust is deprecated and API no longer works (patch by jBeata)

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