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

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

see #2452, see #16578 - drop videomapping plugin:

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