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

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

fix #18576 - fix PluginHandlerTest#testBuildListOfPluginsToLoad unit test

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