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

Last change on this file was 19168, checked in by taylor.smock, 5 months ago

Fix #23772: Fix logging issue which indicated that a dependency was not found

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