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

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

fix #10564 - add GeoJSON import (adapted from geojson plugin)

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