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

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

add unit tests, javadoc

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