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

Last change on this file since 16426 was 16426, checked in by simon04, 4 years ago

see #18712 - Add NetworkManager.isOffline(String) to test offline status of given URL

Deprecates OnlineResource.checkOfflineAccess(String, String)

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