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

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

fix #15742 - check downloaded plugin is valid *before* we delete any existing one (patch by ris)

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