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

Last change on this file since 17678 was 17678, checked in by Don-vip, 4 years ago

fix #20670 - deprecate ScoutSigns plugin (patch by jBeata)

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