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

Last change on this file was 18833, checked in by taylor.smock, 7 months ago

Fix #17052: Allow plugins to save state to session file

The primary feature request was for the TODO plugin to save the list elements for
a future session.

This allows plugins to register via ServiceLoader classes which need to be
called to save or restore their state.

In addition, this fixes an ordering issue with tests whereby the OsmApi cache
would be cleared, but the FakeOsmApi class would not recache itself when called.

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