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

Last change on this file since 11397 was 11397, checked in by Don-vip, 9 months ago

sonar - squid:S2259 - Null pointers should not be dereferenced

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