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

Last change on this file since 11530 was 11530, checked in by Klumbumbus, 7 years ago

OpenStreetCam plugin replaced OpenStreetView plugin

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