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

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

see #15182 - deprecate all Main logging methods and introduce suitable replacements in Logging for most of them

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