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

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

see #16682 - add new plugin property Minimum-Java-Version

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