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

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

see #14200 - deprecate MovementAlert plugin

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