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

Last change on this file since 14253 was 14237, checked in by Don-vip, 6 years ago

NanoLog plugin has been restored in 2014: https://trac.openstreetmap.org/changeset/30493/subversion

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