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

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

fix #16835 - PluginHandlerTest: fix for non-headless mode by properly mocking dialogs (patch by ris)

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