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

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

see #15301 - Obsolete plugin: czechaddress

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