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

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

sonar - squid:S3878 - Arrays should not be created for varargs parameters

  • Property svn:eol-style set to native
File size: 64.0 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(
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 try {
613 RestartAction.restartJOSM();
614 } catch (IOException e) {
615 Main.error(e);
616 }
617 } else {
618 Main.warn("No plugin downloaded, restart canceled");
619 }
620 });
621 } else {
622 Main.warn("No plugin to download, operation canceled");
623 }
624 });
625 }
626
627 private static void alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) {
628 HelpAwareOptionPane.showOptionDialog(
629 parent,
630 tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>"
631 +"You have to update JOSM in order to use this plugin.</html>",
632 plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString()
633 ),
634 tr("Warning"),
635 JOptionPane.WARNING_MESSAGE,
636 ht("/Plugin/Loading#JOSMUpdateRequired")
637 );
638 }
639
640 /**
641 * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
642 * current JOSM version must be compatible with the plugin and no other plugins this plugin
643 * depends on should be missing.
644 *
645 * @param parent The parent Component used to display error popup
646 * @param plugins the collection of all loaded plugins
647 * @param plugin the plugin for which preconditions are checked
648 * @return true, if the preconditions are met; false otherwise
649 */
650 public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) {
651
652 // make sure the plugin is compatible with the current JOSM version
653 //
654 int josmVersion = Version.getInstance().getVersion();
655 if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
656 alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion);
657 return false;
658 }
659
660 // Add all plugins already loaded (to include early plugins when checking late ones)
661 Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
662 for (PluginProxy proxy : pluginList) {
663 allPlugins.add(proxy.getPluginInformation());
664 }
665
666 return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true);
667 }
668
669 /**
670 * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met.
671 * No other plugins this plugin depends on should be missing.
672 *
673 * @param parent The parent Component used to display error popup. If parent is
674 * null, the error popup is suppressed
675 * @param plugins the collection of all loaded plugins
676 * @param plugin the plugin for which preconditions are checked
677 * @param local Determines if the local or up-to-date plugin dependencies are to be checked.
678 * @return true, if the preconditions are met; false otherwise
679 * @since 5601
680 */
681 public static boolean checkRequiredPluginsPreconditions(Component parent, Collection<PluginInformation> plugins,
682 PluginInformation plugin, boolean local) {
683
684 String requires = local ? plugin.localrequires : plugin.requires;
685
686 // make sure the dependencies to other plugins are not broken
687 //
688 if (requires != null) {
689 Set<String> pluginNames = new HashSet<>();
690 for (PluginInformation pi: plugins) {
691 pluginNames.add(pi.name);
692 }
693 Set<String> missingPlugins = new HashSet<>();
694 List<String> requiredPlugins = local ? plugin.getLocalRequiredPlugins() : plugin.getRequiredPlugins();
695 for (String requiredPlugin : requiredPlugins) {
696 if (!pluginNames.contains(requiredPlugin)) {
697 missingPlugins.add(requiredPlugin);
698 }
699 }
700 if (!missingPlugins.isEmpty()) {
701 if (parent != null) {
702 alertMissingRequiredPlugin(parent, plugin.name, missingPlugins);
703 }
704 return false;
705 }
706 }
707 return true;
708 }
709
710 /**
711 * Get the class loader for loading plugin code.
712 *
713 * @return the class loader
714 */
715 public static synchronized DynamicURLClassLoader getPluginClassLoader() {
716 if (pluginClassLoader == null) {
717 pluginClassLoader = AccessController.doPrivileged((PrivilegedAction<DynamicURLClassLoader>)
718 () -> new DynamicURLClassLoader(new URL[0], Main.class.getClassLoader()));
719 sources.add(0, pluginClassLoader);
720 }
721 return pluginClassLoader;
722 }
723
724 /**
725 * Add more plugins to the plugin class loader.
726 *
727 * @param plugins the plugins that should be handled by the plugin class loader
728 */
729 public static void extendPluginClassLoader(Collection<PluginInformation> plugins) {
730 // iterate all plugins and collect all libraries of all plugins:
731 File pluginDir = Main.pref.getPluginsDirectory();
732 DynamicURLClassLoader cl = getPluginClassLoader();
733
734 for (PluginInformation info : plugins) {
735 if (info.libraries == null) {
736 continue;
737 }
738 for (URL libUrl : info.libraries) {
739 cl.addURL(libUrl);
740 }
741 File pluginJar = new File(pluginDir, info.name + ".jar");
742 I18n.addTexts(pluginJar);
743 URL pluginJarUrl = Utils.fileToURL(pluginJar);
744 cl.addURL(pluginJarUrl);
745 }
746 }
747
748 /**
749 * Loads and instantiates the plugin described by <code>plugin</code> using
750 * the class loader <code>pluginClassLoader</code>.
751 *
752 * @param parent The parent component to be used for the displayed dialog
753 * @param plugin the plugin
754 * @param pluginClassLoader the plugin class loader
755 */
756 public static void loadPlugin(Component parent, PluginInformation plugin, ClassLoader pluginClassLoader) {
757 String msg = tr("Could not load plugin {0}. Delete from preferences?", plugin.name);
758 try {
759 Class<?> klass = plugin.loadClass(pluginClassLoader);
760 if (klass != null) {
761 Main.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion));
762 PluginProxy pluginProxy = plugin.load(klass);
763 pluginList.add(pluginProxy);
764 Main.addAndFireMapFrameListener(pluginProxy);
765 }
766 msg = null;
767 } catch (PluginException e) {
768 pluginLoadingExceptions.put(plugin.name, e);
769 Main.error(e);
770 if (e.getCause() instanceof ClassNotFoundException) {
771 msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>"
772 + "Delete from preferences?</html>", Utils.escapeReservedCharactersHTML(plugin.name), plugin.className);
773 }
774 } catch (RuntimeException e) { // NOPMD
775 pluginLoadingExceptions.put(plugin.name, e);
776 Main.error(e);
777 }
778 if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) {
779 Main.pref.removeFromCollection("plugins", plugin.name);
780 }
781 }
782
783 /**
784 * Loads the plugin in <code>plugins</code> from locally available jar files into memory.
785 *
786 * @param parent The parent component to be used for the displayed dialog
787 * @param plugins the list of plugins
788 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
789 */
790 public static void loadPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
791 if (monitor == null) {
792 monitor = NullProgressMonitor.INSTANCE;
793 }
794 try {
795 monitor.beginTask(tr("Loading plugins ..."));
796 monitor.subTask(tr("Checking plugin preconditions..."));
797 List<PluginInformation> toLoad = new LinkedList<>();
798 for (PluginInformation pi: plugins) {
799 if (checkLoadPreconditions(parent, plugins, pi)) {
800 toLoad.add(pi);
801 }
802 }
803 // sort the plugins according to their "staging" equivalence class. The
804 // lower the value of "stage" the earlier the plugin should be loaded.
805 //
806 toLoad.sort(Comparator.comparingInt(o -> o.stage));
807 if (toLoad.isEmpty())
808 return;
809
810 extendPluginClassLoader(toLoad);
811 monitor.setTicksCount(toLoad.size());
812 for (PluginInformation info : toLoad) {
813 monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
814 loadPlugin(parent, info, getPluginClassLoader());
815 monitor.worked(1);
816 }
817 } finally {
818 monitor.finishTask();
819 }
820 }
821
822 /**
823 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} set to true.
824 *
825 * @param parent The parent component to be used for the displayed dialog
826 * @param plugins the collection of plugins
827 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
828 */
829 public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
830 List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size());
831 for (PluginInformation pi: plugins) {
832 if (pi.early) {
833 earlyPlugins.add(pi);
834 }
835 }
836 loadPlugins(parent, earlyPlugins, monitor);
837 }
838
839 /**
840 * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} set to false.
841 *
842 * @param parent The parent component to be used for the displayed dialog
843 * @param plugins the collection of plugins
844 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
845 */
846 public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
847 List<PluginInformation> latePlugins = new ArrayList<>(plugins.size());
848 for (PluginInformation pi: plugins) {
849 if (!pi.early) {
850 latePlugins.add(pi);
851 }
852 }
853 loadPlugins(parent, latePlugins, monitor);
854 }
855
856 /**
857 * Loads locally available plugin information from local plugin jars and from cached
858 * plugin lists.
859 *
860 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
861 * @return the list of locally available plugin information
862 *
863 */
864 private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
865 if (monitor == null) {
866 monitor = NullProgressMonitor.INSTANCE;
867 }
868 try {
869 ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
870 try {
871 task.run();
872 } catch (RuntimeException e) { // NOPMD
873 Main.error(e);
874 return null;
875 }
876 Map<String, PluginInformation> ret = new HashMap<>();
877 for (PluginInformation pi: task.getAvailablePlugins()) {
878 ret.put(pi.name, pi);
879 }
880 return ret;
881 } finally {
882 monitor.finishTask();
883 }
884 }
885
886 private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) {
887 StringBuilder sb = new StringBuilder();
888 sb.append("<html>")
889 .append(trn("JOSM could not find information about the following plugin:",
890 "JOSM could not find information about the following plugins:",
891 plugins.size()))
892 .append(Utils.joinAsHtmlUnorderedList(plugins))
893 .append(trn("The plugin is not going to be loaded.",
894 "The plugins are not going to be loaded.",
895 plugins.size()))
896 .append("</html>");
897 HelpAwareOptionPane.showOptionDialog(
898 parent,
899 sb.toString(),
900 tr("Warning"),
901 JOptionPane.WARNING_MESSAGE,
902 ht("/Plugin/Loading#MissingPluginInfos")
903 );
904 }
905
906 /**
907 * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
908 * out. This involves user interaction. This method displays alert and confirmation
909 * messages.
910 *
911 * @param parent The parent component to be used for the displayed dialog
912 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
913 * @return the set of plugins to load (as set of plugin names)
914 */
915 public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
916 if (monitor == null) {
917 monitor = NullProgressMonitor.INSTANCE;
918 }
919 try {
920 monitor.beginTask(tr("Determining plugins to load..."));
921 Set<String> plugins = new HashSet<>(Main.pref.getCollection("plugins", new LinkedList<String>()));
922 if (Main.isDebugEnabled()) {
923 Main.debug("Plugins list initialized to " + plugins);
924 }
925 String systemProp = System.getProperty("josm.plugins");
926 if (systemProp != null) {
927 plugins.addAll(Arrays.asList(systemProp.split(",")));
928 if (Main.isDebugEnabled()) {
929 Main.debug("josm.plugins system property set to '" + systemProp+"'. Plugins list is now " + plugins);
930 }
931 }
932 monitor.subTask(tr("Removing deprecated plugins..."));
933 filterDeprecatedPlugins(parent, plugins);
934 monitor.subTask(tr("Removing unmaintained plugins..."));
935 filterUnmaintainedPlugins(parent, plugins);
936 if (Main.isDebugEnabled()) {
937 Main.debug("Plugins list is finally set to " + plugins);
938 }
939 Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1, false));
940 List<PluginInformation> ret = new LinkedList<>();
941 if (infos != null) {
942 for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
943 String plugin = it.next();
944 if (infos.containsKey(plugin)) {
945 ret.add(infos.get(plugin));
946 it.remove();
947 }
948 }
949 }
950 if (!plugins.isEmpty()) {
951 alertMissingPluginInformation(parent, plugins);
952 }
953 return ret;
954 } finally {
955 monitor.finishTask();
956 }
957 }
958
959 private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
960 StringBuilder sb = new StringBuilder(128);
961 sb.append("<html>")
962 .append(trn(
963 "Updating the following plugin has failed:",
964 "Updating the following plugins has failed:",
965 plugins.size()))
966 .append("<ul>");
967 for (PluginInformation pi: plugins) {
968 sb.append("<li>").append(Utils.escapeReservedCharactersHTML(pi.name)).append("</li>");
969 }
970 sb.append("</ul>")
971 .append(trn(
972 "Please open the Preference Dialog after JOSM has started and try to update it manually.",
973 "Please open the Preference Dialog after JOSM has started and try to update them manually.",
974 plugins.size()))
975 .append("</html>");
976 HelpAwareOptionPane.showOptionDialog(
977 parent,
978 sb.toString(),
979 tr("Plugin update failed"),
980 JOptionPane.ERROR_MESSAGE,
981 ht("/Plugin/Loading#FailedPluginUpdated")
982 );
983 }
984
985 private static Set<PluginInformation> findRequiredPluginsToDownload(
986 Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
987 Set<PluginInformation> result = new HashSet<>();
988 for (PluginInformation pi : pluginsToUpdate) {
989 for (String name : pi.getRequiredPlugins()) {
990 try {
991 PluginInformation installedPlugin = PluginInformation.findPlugin(name);
992 if (installedPlugin == null) {
993 // New required plugin is not installed, find its PluginInformation
994 PluginInformation reqPlugin = null;
995 for (PluginInformation pi2 : allPlugins) {
996 if (pi2.getName().equals(name)) {
997 reqPlugin = pi2;
998 break;
999 }
1000 }
1001 // Required plugin is known but not already on download list
1002 if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
1003 result.add(reqPlugin);
1004 }
1005 }
1006 } catch (PluginException e) {
1007 Main.warn(tr("Failed to find plugin {0}", name));
1008 Main.error(e);
1009 }
1010 }
1011 }
1012 return result;
1013 }
1014
1015 /**
1016 * Updates the plugins in <code>plugins</code>.
1017 *
1018 * @param parent the parent component for message boxes
1019 * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
1020 * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
1021 * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
1022 * @return the list of plugins to load
1023 * @throws IllegalArgumentException if plugins is null
1024 */
1025 public static Collection<PluginInformation> updatePlugins(Component parent,
1026 Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
1027 Collection<PluginInformation> plugins = null;
1028 pluginDownloadTask = null;
1029 if (monitor == null) {
1030 monitor = NullProgressMonitor.INSTANCE;
1031 }
1032 try {
1033 monitor.beginTask("");
1034
1035 // try to download the plugin lists
1036 ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
1037 monitor.createSubTaskMonitor(1, false),
1038 Main.pref.getOnlinePluginSites(), displayErrMsg
1039 );
1040 task1.run();
1041 List<PluginInformation> allPlugins = task1.getAvailablePlugins();
1042
1043 try {
1044 plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
1045 // If only some plugins have to be updated, filter the list
1046 if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
1047 final Collection<String> pluginsWantedName = Utils.transform(pluginsWanted, piw -> piw.name);
1048 plugins = SubclassFilteredCollection.filter(plugins, pi -> pluginsWantedName.contains(pi.name));
1049 }
1050 } catch (RuntimeException e) { // NOPMD
1051 Main.warn(tr("Failed to download plugin information list"));
1052 Main.error(e);
1053 // don't abort in case of error, continue with downloading plugins below
1054 }
1055
1056 // filter plugins which actually have to be updated
1057 Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
1058 if (plugins != null) {
1059 for (PluginInformation pi: plugins) {
1060 if (pi.isUpdateRequired()) {
1061 pluginsToUpdate.add(pi);
1062 }
1063 }
1064 }
1065
1066 if (!pluginsToUpdate.isEmpty()) {
1067
1068 Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
1069
1070 if (allPlugins != null) {
1071 // Updated plugins may need additional plugin dependencies currently not installed
1072 //
1073 Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
1074 pluginsToDownload.addAll(additionalPlugins);
1075
1076 // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
1077 while (!additionalPlugins.isEmpty()) {
1078 // Install the additional plugins to load them later
1079 if (plugins != null)
1080 plugins.addAll(additionalPlugins);
1081 additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
1082 pluginsToDownload.addAll(additionalPlugins);
1083 }
1084 }
1085
1086 // try to update the locally installed plugins
1087 pluginDownloadTask = new PluginDownloadTask(
1088 monitor.createSubTaskMonitor(1, false),
1089 pluginsToDownload,
1090 tr("Update plugins")
1091 );
1092
1093 try {
1094 pluginDownloadTask.run();
1095 } catch (RuntimeException e) { // NOPMD
1096 Main.error(e);
1097 alertFailedPluginUpdate(parent, pluginsToUpdate);
1098 return plugins;
1099 }
1100
1101 // Update Plugin info for downloaded plugins
1102 refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
1103
1104 // notify user if downloading a locally installed plugin failed
1105 if (!pluginDownloadTask.getFailedPlugins().isEmpty()) {
1106 alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
1107 return plugins;
1108 }
1109 }
1110 } finally {
1111 monitor.finishTask();
1112 }
1113 if (pluginsWanted == null) {
1114 // if all plugins updated, remember the update because it was successful
1115 Main.pref.putInteger("pluginmanager.version", Version.getInstance().getVersion());
1116 Main.pref.put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1117 }
1118 return plugins;
1119 }
1120
1121 /**
1122 * Ask the user for confirmation that a plugin shall be disabled.
1123 *
1124 * @param parent The parent component to be used for the displayed dialog
1125 * @param reason the reason for disabling the plugin
1126 * @param name the plugin name
1127 * @return true, if the plugin shall be disabled; false, otherwise
1128 */
1129 public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1130 ButtonSpec[] options = new ButtonSpec[] {
1131 new ButtonSpec(
1132 tr("Disable plugin"),
1133 ImageProvider.get("dialogs", "delete"),
1134 tr("Click to delete the plugin ''{0}''", name),
1135 null /* no specific help context */
1136 ),
1137 new ButtonSpec(
1138 tr("Keep plugin"),
1139 ImageProvider.get("cancel"),
1140 tr("Click to keep the plugin ''{0}''", name),
1141 null /* no specific help context */
1142 )
1143 };
1144 return 0 == HelpAwareOptionPane.showOptionDialog(
1145 parent,
1146 reason,
1147 tr("Disable plugin"),
1148 JOptionPane.WARNING_MESSAGE,
1149 null,
1150 options,
1151 options[0],
1152 null // FIXME: add help topic
1153 );
1154 }
1155
1156 /**
1157 * Returns the plugin of the specified name.
1158 * @param name The plugin name
1159 * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1160 */
1161 public static Object getPlugin(String name) {
1162 for (PluginProxy plugin : pluginList) {
1163 if (plugin.getPluginInformation().name.equals(name))
1164 return plugin.plugin;
1165 }
1166 return null;
1167 }
1168
1169 public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1170 for (PluginProxy p : pluginList) {
1171 p.addDownloadSelection(downloadSelections);
1172 }
1173 }
1174
1175 public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1176 Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1177 for (PluginProxy plugin : pluginList) {
1178 settings.add(new PluginPreferenceFactory(plugin));
1179 }
1180 return settings;
1181 }
1182
1183 /**
1184 * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding
1185 * ".jar" files.
1186 *
1187 * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1188 * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1189 * installation of the respective plugin is silently skipped.
1190 *
1191 * @param dowarn if true, warning messages are displayed; false otherwise
1192 */
1193 public static void installDownloadedPlugins(boolean dowarn) {
1194 File pluginDir = Main.pref.getPluginsDirectory();
1195 if (!pluginDir.exists() || !pluginDir.isDirectory() || !pluginDir.canWrite())
1196 return;
1197
1198 final File[] files = pluginDir.listFiles((FilenameFilter) (dir, name) -> name.endsWith(".jar.new"));
1199 if (files == null)
1200 return;
1201
1202 for (File updatedPlugin : files) {
1203 final String filePath = updatedPlugin.getPath();
1204 File plugin = new File(filePath.substring(0, filePath.length() - 4));
1205 String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1206 if (plugin.exists() && !plugin.delete() && dowarn) {
1207 Main.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1208 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1209 "Skipping installation. JOSM is still going to load the old plugin version.",
1210 pluginName));
1211 continue;
1212 }
1213 try {
1214 // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1215 new JarFile(updatedPlugin).close();
1216 } catch (IOException e) {
1217 if (dowarn) {
1218 Main.warn(e, tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}",
1219 plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()));
1220 }
1221 continue;
1222 }
1223 // Install plugin
1224 if (!updatedPlugin.renameTo(plugin) && dowarn) {
1225 Main.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.",
1226 plugin.toString(), updatedPlugin.toString()));
1227 Main.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1228 "Skipping installation. JOSM is still going to load the old plugin version.",
1229 pluginName));
1230 }
1231 }
1232 }
1233
1234 /**
1235 * Determines if the specified file is a valid and accessible JAR file.
1236 * @param jar The file to check
1237 * @return true if file can be opened as a JAR file.
1238 * @since 5723
1239 */
1240 public static boolean isValidJar(File jar) {
1241 if (jar != null && jar.exists() && jar.canRead()) {
1242 try {
1243 new JarFile(jar).close();
1244 } catch (IOException e) {
1245 Main.warn(e);
1246 return false;
1247 }
1248 return true;
1249 } else if (jar != null) {
1250 Main.warn("Invalid jar file ''"+jar+"'' (exists: "+jar.exists()+", canRead: "+jar.canRead()+')');
1251 }
1252 return false;
1253 }
1254
1255 /**
1256 * Replies the updated jar file for the given plugin name.
1257 * @param name The plugin name to find.
1258 * @return the updated jar file for the given plugin name. null if not found or not readable.
1259 * @since 5601
1260 */
1261 public static File findUpdatedJar(String name) {
1262 File pluginDir = Main.pref.getPluginsDirectory();
1263 // Find the downloaded file. We have tried to install the downloaded plugins
1264 // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1265 File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1266 if (!isValidJar(downloadedPluginFile)) {
1267 downloadedPluginFile = new File(pluginDir, name + ".jar");
1268 if (!isValidJar(downloadedPluginFile)) {
1269 return null;
1270 }
1271 }
1272 return downloadedPluginFile;
1273 }
1274
1275 /**
1276 * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1277 * @param updatedPlugins The PluginInformation objects to update.
1278 * @since 5601
1279 */
1280 public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1281 if (updatedPlugins == null) return;
1282 for (PluginInformation pi : updatedPlugins) {
1283 File downloadedPluginFile = findUpdatedJar(pi.name);
1284 if (downloadedPluginFile == null) {
1285 continue;
1286 }
1287 try {
1288 pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1289 } catch (PluginException e) {
1290 Main.error(e);
1291 }
1292 }
1293 }
1294
1295 private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1296 final ButtonSpec[] options = new ButtonSpec[] {
1297 new ButtonSpec(
1298 tr("Update plugin"),
1299 ImageProvider.get("dialogs", "refresh"),
1300 tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1301 null /* no specific help context */
1302 ),
1303 new ButtonSpec(
1304 tr("Disable plugin"),
1305 ImageProvider.get("dialogs", "delete"),
1306 tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1307 null /* no specific help context */
1308 ),
1309 new ButtonSpec(
1310 tr("Keep plugin"),
1311 ImageProvider.get("cancel"),
1312 tr("Click to keep the plugin ''{0}''", plugin.getPluginInformation().name),
1313 null /* no specific help context */
1314 )
1315 };
1316
1317 final StringBuilder msg = new StringBuilder(256);
1318 msg.append("<html>")
1319 .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.",
1320 Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().name)))
1321 .append("<br>");
1322 if (plugin.getPluginInformation().author != null) {
1323 msg.append(tr("According to the information within the plugin, the author is {0}.",
1324 Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().author)))
1325 .append("<br>");
1326 }
1327 msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1328 .append("</html>");
1329
1330 try {
1331 FutureTask<Integer> task = new FutureTask<>(() -> HelpAwareOptionPane.showOptionDialog(
1332 Main.parent,
1333 msg.toString(),
1334 tr("Update plugins"),
1335 JOptionPane.QUESTION_MESSAGE,
1336 null,
1337 options,
1338 options[0],
1339 ht("/ErrorMessages#ErrorInPlugin")
1340 ));
1341 GuiHelper.runInEDT(task);
1342 return task.get();
1343 } catch (InterruptedException | ExecutionException e) {
1344 Main.warn(e);
1345 }
1346 return -1;
1347 }
1348
1349 /**
1350 * Replies the plugin which most likely threw the exception <code>ex</code>.
1351 *
1352 * @param ex the exception
1353 * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1354 */
1355 private static PluginProxy getPluginCausingException(Throwable ex) {
1356 PluginProxy err = null;
1357 StackTraceElement[] stack = ex.getStackTrace();
1358 // remember the error position, as multiple plugins may be involved, we search the topmost one
1359 int pos = stack.length;
1360 for (PluginProxy p : pluginList) {
1361 String baseClass = p.getPluginInformation().className;
1362 baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1363 for (int elpos = 0; elpos < pos; ++elpos) {
1364 if (stack[elpos].getClassName().startsWith(baseClass)) {
1365 pos = elpos;
1366 err = p;
1367 }
1368 }
1369 }
1370 return err;
1371 }
1372
1373 /**
1374 * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1375 * conditionally updates or deactivates the plugin, but asks the user first.
1376 *
1377 * @param e the exception
1378 * @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
1379 */
1380 public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1381 PluginProxy plugin = null;
1382 // Check for an explicit problem when calling a plugin function
1383 if (e instanceof PluginException) {
1384 plugin = ((PluginException) e).plugin;
1385 }
1386 if (plugin == null) {
1387 plugin = getPluginCausingException(e);
1388 }
1389 if (plugin == null)
1390 // don't know what plugin threw the exception
1391 return null;
1392
1393 Set<String> plugins = new HashSet<>(
1394 Main.pref.getCollection("plugins", Collections.<String>emptySet())
1395 );
1396 final PluginInformation pluginInfo = plugin.getPluginInformation();
1397 if (!plugins.contains(pluginInfo.name))
1398 // plugin not activated ? strange in this context but anyway, don't bother
1399 // the user with dialogs, skip conditional deactivation
1400 return null;
1401
1402 switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1403 case 0:
1404 // update the plugin
1405 updatePlugins(Main.parent, Collections.singleton(pluginInfo), null, true);
1406 return pluginDownloadTask;
1407 case 1:
1408 // deactivate the plugin
1409 plugins.remove(plugin.getPluginInformation().name);
1410 Main.pref.putCollection("plugins", plugins);
1411 GuiHelper.runInEDTAndWait(() -> JOptionPane.showMessageDialog(
1412 Main.parent,
1413 tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1414 tr("Information"),
1415 JOptionPane.INFORMATION_MESSAGE
1416 ));
1417 return null;
1418 default:
1419 // user doesn't want to deactivate the plugin
1420 return null;
1421 }
1422 }
1423
1424 /**
1425 * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1426 * @return The list of loaded plugins
1427 */
1428 public static Collection<String> getBugReportInformation() {
1429 final Collection<String> pl = new TreeSet<>(Main.pref.getCollection("plugins", new LinkedList<>()));
1430 for (final PluginProxy pp : pluginList) {
1431 PluginInformation pi = pp.getPluginInformation();
1432 pl.remove(pi.name);
1433 pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1434 ? pi.localversion : "unknown") + ')');
1435 }
1436 return pl;
1437 }
1438
1439 /**
1440 * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1441 * @return The list of loaded plugins (one "line" of Swing components per plugin)
1442 */
1443 public static JPanel getInfoPanel() {
1444 JPanel pluginTab = new JPanel(new GridBagLayout());
1445 for (final PluginProxy p : pluginList) {
1446 final PluginInformation info = p.getPluginInformation();
1447 String name = info.name
1448 + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1449 pluginTab.add(new JLabel(name), GBC.std());
1450 pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1451 pluginTab.add(new JButton(new PluginInformationAction(info)), GBC.eol());
1452
1453 JosmTextArea description = new JosmTextArea(info.description == null ? tr("no description available")
1454 : info.description);
1455 description.setEditable(false);
1456 description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1457 description.setLineWrap(true);
1458 description.setWrapStyleWord(true);
1459 description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1460 description.setBackground(UIManager.getColor("Panel.background"));
1461 description.setCaretPosition(0);
1462
1463 pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1464 }
1465 return pluginTab;
1466 }
1467
1468 /**
1469 * Returns the set of deprecated and unmaintained plugins.
1470 * @return set of deprecated and unmaintained plugins names.
1471 * @since 8938
1472 */
1473 public static Set<String> getDeprecatedAndUnmaintainedPlugins() {
1474 Set<String> result = new HashSet<>(DEPRECATED_PLUGINS.size() + UNMAINTAINED_PLUGINS.size());
1475 for (DeprecatedPlugin dp : DEPRECATED_PLUGINS) {
1476 result.add(dp.name);
1477 }
1478 result.addAll(UNMAINTAINED_PLUGINS);
1479 return result;
1480 }
1481
1482 private static class UpdatePluginsMessagePanel extends JPanel {
1483 private final JMultilineLabel lblMessage = new JMultilineLabel("");
1484 private final JCheckBox cbDontShowAgain = new JCheckBox(
1485 tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)"));
1486
1487 UpdatePluginsMessagePanel() {
1488 build();
1489 }
1490
1491 protected final void build() {
1492 setLayout(new GridBagLayout());
1493 GridBagConstraints gc = new GridBagConstraints();
1494 gc.anchor = GridBagConstraints.NORTHWEST;
1495 gc.fill = GridBagConstraints.BOTH;
1496 gc.weightx = 1.0;
1497 gc.weighty = 1.0;
1498 gc.insets = new Insets(5, 5, 5, 5);
1499 add(lblMessage, gc);
1500 lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1501
1502 gc.gridy = 1;
1503 gc.fill = GridBagConstraints.HORIZONTAL;
1504 gc.weighty = 0.0;
1505 add(cbDontShowAgain, gc);
1506 cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1507 }
1508
1509 public void setMessage(String message) {
1510 lblMessage.setText(message);
1511 }
1512
1513 public void initDontShowAgain(String preferencesKey) {
1514 String policy = Main.pref.get(preferencesKey, "ask");
1515 policy = policy.trim().toLowerCase(Locale.ENGLISH);
1516 cbDontShowAgain.setSelected(!"ask".equals(policy));
1517 }
1518
1519 public boolean isRememberDecision() {
1520 return cbDontShowAgain.isSelected();
1521 }
1522 }
1523}
Note: See TracBrowser for help on using the repository browser.