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

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