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

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

see #15182 - deprecate Main.worker, replace it by gui.MainApplication.worker + code refactoring to make sure only editor packages use it

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