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

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

see #15229 - deprecate Main.parent and Main itself

  • Property svn:eol-style set to native
File size: 68.3 KB
Line 
1// License: GPL. For details, see LICENSE file.
2package org.openstreetmap.josm.plugins;
3
4import static org.openstreetmap.josm.gui.help.HelpUtil.ht;
5import static org.openstreetmap.josm.tools.I18n.tr;
6import static org.openstreetmap.josm.tools.I18n.trn;
7
8import java.awt.Component;
9import java.awt.Font;
10import java.awt.GraphicsEnvironment;
11import java.awt.GridBagConstraints;
12import java.awt.GridBagLayout;
13import java.awt.Insets;
14import java.awt.event.ActionEvent;
15import java.io.File;
16import java.io.FilenameFilter;
17import java.io.IOException;
18import java.net.MalformedURLException;
19import java.net.URL;
20import java.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 alertJOSMUpdateRequired(Component parent, String plugin, int requiredVersion) {
639        HelpAwareOptionPane.showOptionDialog(
640                parent,
641                tr("<html>Plugin {0} requires JOSM version {1}. The current JOSM version is {2}.<br>"
642                        +"You have to update JOSM in order to use this plugin.</html>",
643                        plugin, Integer.toString(requiredVersion), Version.getInstance().getVersionString()
644                ),
645                tr("Warning"),
646                JOptionPane.WARNING_MESSAGE,
647                ht("/Plugin/Loading#JOSMUpdateRequired")
648        );
649    }
650
651    /**
652     * Checks whether all preconditions for loading the plugin <code>plugin</code> are met. The
653     * current JOSM version must be compatible with the plugin and no other plugins this plugin
654     * depends on should be missing.
655     *
656     * @param parent The parent Component used to display error popup
657     * @param plugins the collection of all loaded plugins
658     * @param plugin the plugin for which preconditions are checked
659     * @return true, if the preconditions are met; false otherwise
660     */
661    public static boolean checkLoadPreconditions(Component parent, Collection<PluginInformation> plugins, PluginInformation plugin) {
662
663        // make sure the plugin is compatible with the current JOSM version
664        //
665        int josmVersion = Version.getInstance().getVersion();
666        if (plugin.localmainversion > josmVersion && josmVersion != Version.JOSM_UNKNOWN_VERSION) {
667            alertJOSMUpdateRequired(parent, plugin.name, plugin.localmainversion);
668            return false;
669        }
670
671        // Add all plugins already loaded (to include early plugins when checking late ones)
672        Collection<PluginInformation> allPlugins = new HashSet<>(plugins);
673        for (PluginProxy proxy : pluginList) {
674            allPlugins.add(proxy.getPluginInformation());
675        }
676
677        return checkRequiredPluginsPreconditions(parent, allPlugins, plugin, true);
678    }
679
680    /**
681     * Checks if required plugins preconditions for loading the plugin <code>plugin</code> are met.
682     * No other plugins this plugin depends on should be missing.
683     *
684     * @param parent The parent Component used to display error popup. If parent is
685     * null, the error popup is suppressed
686     * @param plugins the collection of all loaded plugins
687     * @param plugin the plugin for which preconditions are checked
688     * @param local Determines if the local or up-to-date plugin dependencies are to be checked.
689     * @return true, if the preconditions are met; false otherwise
690     * @since 5601
691     */
692    public static boolean checkRequiredPluginsPreconditions(Component parent, Collection<PluginInformation> plugins,
693            PluginInformation plugin, boolean local) {
694
695        String requires = local ? plugin.localrequires : plugin.requires;
696
697        // make sure the dependencies to other plugins are not broken
698        //
699        if (requires != null) {
700            Set<String> pluginNames = new HashSet<>();
701            for (PluginInformation pi: plugins) {
702                pluginNames.add(pi.name);
703            }
704            Set<String> missingPlugins = new HashSet<>();
705            List<String> requiredPlugins = local ? plugin.getLocalRequiredPlugins() : plugin.getRequiredPlugins();
706            for (String requiredPlugin : requiredPlugins) {
707                if (!pluginNames.contains(requiredPlugin)) {
708                    missingPlugins.add(requiredPlugin);
709                }
710            }
711            if (!missingPlugins.isEmpty()) {
712                if (parent != null) {
713                    alertMissingRequiredPlugin(parent, plugin.name, missingPlugins);
714                }
715                return false;
716            }
717        }
718        return true;
719    }
720
721    /**
722     * Get class loader to locate resources from plugins.
723     *
724     * It joins URLs of all plugins, to find images, etc.
725     * (Not for loading Java classes - each plugin has a separate {@link PluginClassLoader}
726     * for that purpose.)
727     * @return class loader to locate resources from plugins
728     */
729    private static synchronized DynamicURLClassLoader getJoinedPluginResourceCL() {
730        if (joinedPluginResourceCL == null) {
731            joinedPluginResourceCL = AccessController.doPrivileged((PrivilegedAction<DynamicURLClassLoader>)
732                    () -> new DynamicURLClassLoader(new URL[0], PluginHandler.class.getClassLoader()));
733            sources.add(0, joinedPluginResourceCL);
734        }
735        return joinedPluginResourceCL;
736    }
737
738    /**
739     * Add more plugins to the joined plugin resource class loader.
740     *
741     * @param plugins the plugins to add
742     */
743    private static void extendJoinedPluginResourceCL(Collection<PluginInformation> plugins) {
744        // iterate all plugins and collect all libraries of all plugins:
745        File pluginDir = Preferences.main().getPluginsDirectory();
746        DynamicURLClassLoader cl = getJoinedPluginResourceCL();
747
748        for (PluginInformation info : plugins) {
749            if (info.libraries == null) {
750                continue;
751            }
752            for (URL libUrl : info.libraries) {
753                cl.addURL(libUrl);
754            }
755            File pluginJar = new File(pluginDir, info.name + ".jar");
756            I18n.addTexts(pluginJar);
757            URL pluginJarUrl = Utils.fileToURL(pluginJar);
758            cl.addURL(pluginJarUrl);
759        }
760    }
761
762    /**
763     * Loads and instantiates the plugin described by <code>plugin</code> using
764     * the class loader <code>pluginClassLoader</code>.
765     *
766     * @param parent The parent component to be used for the displayed dialog
767     * @param plugin the plugin
768     * @param pluginClassLoader the plugin class loader
769     */
770    private static void loadPlugin(Component parent, PluginInformation plugin, PluginClassLoader pluginClassLoader) {
771        String msg = tr("Could not load plugin {0}. Delete from preferences?", "'"+plugin.name+"'");
772        try {
773            Class<?> klass = plugin.loadClass(pluginClassLoader);
774            if (klass != null) {
775                Logging.info(tr("loading plugin ''{0}'' (version {1})", plugin.name, plugin.localversion));
776                PluginProxy pluginProxy = plugin.load(klass, pluginClassLoader);
777                pluginList.add(pluginProxy);
778                MainApplication.addAndFireMapFrameListener(pluginProxy);
779            }
780            msg = null;
781        } catch (PluginException e) {
782            pluginLoadingExceptions.put(plugin.name, e);
783            Logging.error(e);
784            if (e.getCause() instanceof ClassNotFoundException) {
785                msg = tr("<html>Could not load plugin {0} because the plugin<br>main class ''{1}'' was not found.<br>"
786                        + "Delete from preferences?</html>", "'"+Utils.escapeReservedCharactersHTML(plugin.name)+"'", plugin.className);
787            }
788        } catch (RuntimeException e) { // NOPMD
789            pluginLoadingExceptions.put(plugin.name, e);
790            Logging.error(e);
791        }
792        if (msg != null && confirmDisablePlugin(parent, msg, plugin.name)) {
793            PreferencesUtils.removeFromList(Config.getPref(), "plugins", plugin.name);
794        }
795    }
796
797    /**
798     * Loads the plugin in <code>plugins</code> from locally available jar files into memory.
799     *
800     * @param parent The parent component to be used for the displayed dialog
801     * @param plugins the list of plugins
802     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
803     */
804    public static void loadPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
805        if (monitor == null) {
806            monitor = NullProgressMonitor.INSTANCE;
807        }
808        try {
809            monitor.beginTask(tr("Loading plugins ..."));
810            monitor.subTask(tr("Checking plugin preconditions..."));
811            List<PluginInformation> toLoad = new LinkedList<>();
812            for (PluginInformation pi: plugins) {
813                if (checkLoadPreconditions(parent, plugins, pi)) {
814                    toLoad.add(pi);
815                }
816            }
817            // sort the plugins according to their "staging" equivalence class. The
818            // lower the value of "stage" the earlier the plugin should be loaded.
819            //
820            toLoad.sort(Comparator.comparingInt(o -> o.stage));
821            if (toLoad.isEmpty())
822                return;
823
824            Map<PluginInformation, PluginClassLoader> classLoaders = new HashMap<>();
825            for (PluginInformation info : toLoad) {
826                PluginClassLoader cl = AccessController.doPrivileged((PrivilegedAction<PluginClassLoader>)
827                    () -> new PluginClassLoader(
828                        info.libraries.toArray(new URL[0]),
829                        PluginHandler.class.getClassLoader(),
830                        null));
831                classLoaders.put(info, cl);
832            }
833
834            // resolve dependencies
835            for (PluginInformation info : toLoad) {
836                PluginClassLoader cl = classLoaders.get(info);
837                DEPENDENCIES:
838                for (String depName : info.getLocalRequiredPlugins()) {
839                    for (PluginInformation depInfo : toLoad) {
840                        if (depInfo.getName().equals(depName)) {
841                            cl.addDependency(classLoaders.get(depInfo));
842                            continue DEPENDENCIES;
843                        }
844                    }
845                    for (PluginProxy proxy : pluginList) {
846                        if (proxy.getPluginInformation().getName().equals(depName)) {
847                            cl.addDependency(proxy.getClassLoader());
848                            continue DEPENDENCIES;
849                        }
850                    }
851                    Logging.error("unable to find dependency " + depName + " for plugin " + info.getName());
852                }
853            }
854
855            extendJoinedPluginResourceCL(toLoad);
856            ImageProvider.addAdditionalClassLoaders(getResourceClassLoaders());
857            monitor.setTicksCount(toLoad.size());
858            for (PluginInformation info : toLoad) {
859                monitor.setExtraText(tr("Loading plugin ''{0}''...", info.name));
860                loadPlugin(parent, info, classLoaders.get(info));
861                monitor.worked(1);
862            }
863        } finally {
864            monitor.finishTask();
865        }
866    }
867
868    /**
869     * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} set to true.
870     *
871     * @param parent The parent component to be used for the displayed dialog
872     * @param plugins the collection of plugins
873     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
874     */
875    public static void loadEarlyPlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
876        List<PluginInformation> earlyPlugins = new ArrayList<>(plugins.size());
877        for (PluginInformation pi: plugins) {
878            if (pi.early) {
879                earlyPlugins.add(pi);
880            }
881        }
882        loadPlugins(parent, earlyPlugins, monitor);
883    }
884
885    /**
886     * Loads plugins from <code>plugins</code> which have the flag {@link PluginInformation#early} set to false.
887     *
888     * @param parent The parent component to be used for the displayed dialog
889     * @param plugins the collection of plugins
890     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
891     */
892    public static void loadLatePlugins(Component parent, Collection<PluginInformation> plugins, ProgressMonitor monitor) {
893        List<PluginInformation> latePlugins = new ArrayList<>(plugins.size());
894        for (PluginInformation pi: plugins) {
895            if (!pi.early) {
896                latePlugins.add(pi);
897            }
898        }
899        loadPlugins(parent, latePlugins, monitor);
900    }
901
902    /**
903     * Loads locally available plugin information from local plugin jars and from cached
904     * plugin lists.
905     *
906     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
907     * @return the list of locally available plugin information
908     *
909     */
910    private static Map<String, PluginInformation> loadLocallyAvailablePluginInformation(ProgressMonitor monitor) {
911        if (monitor == null) {
912            monitor = NullProgressMonitor.INSTANCE;
913        }
914        try {
915            ReadLocalPluginInformationTask task = new ReadLocalPluginInformationTask(monitor);
916            try {
917                task.run();
918            } catch (RuntimeException e) { // NOPMD
919                Logging.error(e);
920                return null;
921            }
922            Map<String, PluginInformation> ret = new HashMap<>();
923            for (PluginInformation pi: task.getAvailablePlugins()) {
924                ret.put(pi.name, pi);
925            }
926            return ret;
927        } finally {
928            monitor.finishTask();
929        }
930    }
931
932    private static void alertMissingPluginInformation(Component parent, Collection<String> plugins) {
933        StringBuilder sb = new StringBuilder();
934        sb.append("<html>")
935          .append(trn("JOSM could not find information about the following plugin:",
936                "JOSM could not find information about the following plugins:",
937                plugins.size()))
938          .append(Utils.joinAsHtmlUnorderedList(plugins))
939          .append(trn("The plugin is not going to be loaded.",
940                "The plugins are not going to be loaded.",
941                plugins.size()))
942          .append("</html>");
943        HelpAwareOptionPane.showOptionDialog(
944                parent,
945                sb.toString(),
946                tr("Warning"),
947                JOptionPane.WARNING_MESSAGE,
948                ht("/Plugin/Loading#MissingPluginInfos")
949        );
950    }
951
952    /**
953     * Builds the set of plugins to load. Deprecated and unmaintained plugins are filtered
954     * out. This involves user interaction. This method displays alert and confirmation
955     * messages.
956     *
957     * @param parent The parent component to be used for the displayed dialog
958     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
959     * @return the set of plugins to load (as set of plugin names)
960     */
961    public static List<PluginInformation> buildListOfPluginsToLoad(Component parent, ProgressMonitor monitor) {
962        if (monitor == null) {
963            monitor = NullProgressMonitor.INSTANCE;
964        }
965        try {
966            monitor.beginTask(tr("Determining plugins to load..."));
967            Set<String> plugins = new HashSet<>(Config.getPref().getList("plugins", new LinkedList<String>()));
968            Logging.debug("Plugins list initialized to {0}", plugins);
969            String systemProp = Utils.getSystemProperty("josm.plugins");
970            if (systemProp != null) {
971                plugins.addAll(Arrays.asList(systemProp.split(",")));
972                Logging.debug("josm.plugins system property set to '{0}'. Plugins list is now {1}", systemProp, plugins);
973            }
974            monitor.subTask(tr("Removing deprecated plugins..."));
975            filterDeprecatedPlugins(parent, plugins);
976            monitor.subTask(tr("Removing unmaintained plugins..."));
977            filterUnmaintainedPlugins(parent, plugins);
978            Logging.debug("Plugins list is finally set to {0}", plugins);
979            Map<String, PluginInformation> infos = loadLocallyAvailablePluginInformation(monitor.createSubTaskMonitor(1, false));
980            List<PluginInformation> ret = new LinkedList<>();
981            if (infos != null) {
982                for (Iterator<String> it = plugins.iterator(); it.hasNext();) {
983                    String plugin = it.next();
984                    if (infos.containsKey(plugin)) {
985                        ret.add(infos.get(plugin));
986                        it.remove();
987                    }
988                }
989            }
990            if (!plugins.isEmpty()) {
991                alertMissingPluginInformation(parent, plugins);
992            }
993            return ret;
994        } finally {
995            monitor.finishTask();
996        }
997    }
998
999    private static void alertFailedPluginUpdate(Component parent, Collection<PluginInformation> plugins) {
1000        StringBuilder sb = new StringBuilder(128);
1001        sb.append("<html>")
1002          .append(trn(
1003                "Updating the following plugin has failed:",
1004                "Updating the following plugins has failed:",
1005                plugins.size()))
1006          .append("<ul>");
1007        for (PluginInformation pi: plugins) {
1008            sb.append("<li>").append(Utils.escapeReservedCharactersHTML(pi.name)).append("</li>");
1009        }
1010        sb.append("</ul>")
1011          .append(trn(
1012                "Please open the Preference Dialog after JOSM has started and try to update it manually.",
1013                "Please open the Preference Dialog after JOSM has started and try to update them manually.",
1014                plugins.size()))
1015          .append("</html>");
1016        HelpAwareOptionPane.showOptionDialog(
1017                parent,
1018                sb.toString(),
1019                tr("Plugin update failed"),
1020                JOptionPane.ERROR_MESSAGE,
1021                ht("/Plugin/Loading#FailedPluginUpdated")
1022        );
1023    }
1024
1025    private static Set<PluginInformation> findRequiredPluginsToDownload(
1026            Collection<PluginInformation> pluginsToUpdate, List<PluginInformation> allPlugins, Set<PluginInformation> pluginsToDownload) {
1027        Set<PluginInformation> result = new HashSet<>();
1028        for (PluginInformation pi : pluginsToUpdate) {
1029            for (String name : pi.getRequiredPlugins()) {
1030                try {
1031                    PluginInformation installedPlugin = PluginInformation.findPlugin(name);
1032                    if (installedPlugin == null) {
1033                        // New required plugin is not installed, find its PluginInformation
1034                        PluginInformation reqPlugin = null;
1035                        for (PluginInformation pi2 : allPlugins) {
1036                            if (pi2.getName().equals(name)) {
1037                                reqPlugin = pi2;
1038                                break;
1039                            }
1040                        }
1041                        // Required plugin is known but not already on download list
1042                        if (reqPlugin != null && !pluginsToDownload.contains(reqPlugin)) {
1043                            result.add(reqPlugin);
1044                        }
1045                    }
1046                } catch (PluginException e) {
1047                    Logging.warn(tr("Failed to find plugin {0}", name));
1048                    Logging.error(e);
1049                }
1050            }
1051        }
1052        return result;
1053    }
1054
1055    /**
1056     * Updates the plugins in <code>plugins</code>.
1057     *
1058     * @param parent the parent component for message boxes
1059     * @param pluginsWanted the collection of plugins to update. Updates all plugins if {@code null}
1060     * @param monitor the progress monitor. Defaults to {@link NullProgressMonitor#INSTANCE} if null.
1061     * @param displayErrMsg if {@code true}, a blocking error message is displayed in case of I/O exception.
1062     * @return the list of plugins to load
1063     * @throws IllegalArgumentException if plugins is null
1064     */
1065    public static Collection<PluginInformation> updatePlugins(Component parent,
1066            Collection<PluginInformation> pluginsWanted, ProgressMonitor monitor, boolean displayErrMsg) {
1067        Collection<PluginInformation> plugins = null;
1068        pluginDownloadTask = null;
1069        if (monitor == null) {
1070            monitor = NullProgressMonitor.INSTANCE;
1071        }
1072        try {
1073            monitor.beginTask("");
1074
1075            // try to download the plugin lists
1076            ReadRemotePluginInformationTask task1 = new ReadRemotePluginInformationTask(
1077                    monitor.createSubTaskMonitor(1, false),
1078                    Preferences.main().getOnlinePluginSites(), displayErrMsg
1079            );
1080            task1.run();
1081            List<PluginInformation> allPlugins = task1.getAvailablePlugins();
1082
1083            try {
1084                plugins = buildListOfPluginsToLoad(parent, monitor.createSubTaskMonitor(1, false));
1085                // If only some plugins have to be updated, filter the list
1086                if (pluginsWanted != null && !pluginsWanted.isEmpty()) {
1087                    final Collection<String> pluginsWantedName = Utils.transform(pluginsWanted, piw -> piw.name);
1088                    plugins = SubclassFilteredCollection.filter(plugins, pi -> pluginsWantedName.contains(pi.name));
1089                }
1090            } catch (RuntimeException e) { // NOPMD
1091                Logging.warn(tr("Failed to download plugin information list"));
1092                Logging.error(e);
1093                // don't abort in case of error, continue with downloading plugins below
1094            }
1095
1096            // filter plugins which actually have to be updated
1097            Collection<PluginInformation> pluginsToUpdate = new ArrayList<>();
1098            if (plugins != null) {
1099                for (PluginInformation pi: plugins) {
1100                    if (pi.isUpdateRequired()) {
1101                        pluginsToUpdate.add(pi);
1102                    }
1103                }
1104            }
1105
1106            if (!pluginsToUpdate.isEmpty()) {
1107
1108                Set<PluginInformation> pluginsToDownload = new HashSet<>(pluginsToUpdate);
1109
1110                if (allPlugins != null) {
1111                    // Updated plugins may need additional plugin dependencies currently not installed
1112                    //
1113                    Set<PluginInformation> additionalPlugins = findRequiredPluginsToDownload(pluginsToUpdate, allPlugins, pluginsToDownload);
1114                    pluginsToDownload.addAll(additionalPlugins);
1115
1116                    // Iterate on required plugins, if they need themselves another plugins (i.e A needs B, but B needs C)
1117                    while (!additionalPlugins.isEmpty()) {
1118                        // Install the additional plugins to load them later
1119                        if (plugins != null)
1120                            plugins.addAll(additionalPlugins);
1121                        additionalPlugins = findRequiredPluginsToDownload(additionalPlugins, allPlugins, pluginsToDownload);
1122                        pluginsToDownload.addAll(additionalPlugins);
1123                    }
1124                }
1125
1126                // try to update the locally installed plugins
1127                pluginDownloadTask = new PluginDownloadTask(
1128                        monitor.createSubTaskMonitor(1, false),
1129                        pluginsToDownload,
1130                        tr("Update plugins")
1131                );
1132
1133                try {
1134                    pluginDownloadTask.run();
1135                } catch (RuntimeException e) { // NOPMD
1136                    Logging.error(e);
1137                    alertFailedPluginUpdate(parent, pluginsToUpdate);
1138                    return plugins;
1139                }
1140
1141                // Update Plugin info for downloaded plugins
1142                refreshLocalUpdatedPluginInfo(pluginDownloadTask.getDownloadedPlugins());
1143
1144                // notify user if downloading a locally installed plugin failed
1145                if (!pluginDownloadTask.getFailedPlugins().isEmpty()) {
1146                    alertFailedPluginUpdate(parent, pluginDownloadTask.getFailedPlugins());
1147                    return plugins;
1148                }
1149            }
1150        } finally {
1151            monitor.finishTask();
1152        }
1153        if (pluginsWanted == null) {
1154            // if all plugins updated, remember the update because it was successful
1155            Config.getPref().putInt("pluginmanager.version", Version.getInstance().getVersion());
1156            Config.getPref().put("pluginmanager.lastupdate", Long.toString(System.currentTimeMillis()));
1157        }
1158        return plugins;
1159    }
1160
1161    /**
1162     * Ask the user for confirmation that a plugin shall be disabled.
1163     *
1164     * @param parent The parent component to be used for the displayed dialog
1165     * @param reason the reason for disabling the plugin
1166     * @param name the plugin name
1167     * @return true, if the plugin shall be disabled; false, otherwise
1168     */
1169    public static boolean confirmDisablePlugin(Component parent, String reason, String name) {
1170        ButtonSpec[] options = new ButtonSpec[] {
1171                new ButtonSpec(
1172                        tr("Disable plugin"),
1173                        new ImageProvider("dialogs", "delete"),
1174                        tr("Click to delete the plugin ''{0}''", name),
1175                        null /* no specific help context */
1176                ),
1177                new ButtonSpec(
1178                        tr("Keep plugin"),
1179                        new ImageProvider("cancel"),
1180                        tr("Click to keep the plugin ''{0}''", name),
1181                        null /* no specific help context */
1182                )
1183        };
1184        return 0 == HelpAwareOptionPane.showOptionDialog(
1185                    parent,
1186                    reason,
1187                    tr("Disable plugin"),
1188                    JOptionPane.WARNING_MESSAGE,
1189                    null,
1190                    options,
1191                    options[0],
1192                    null // FIXME: add help topic
1193            );
1194    }
1195
1196    /**
1197     * Returns the plugin of the specified name.
1198     * @param name The plugin name
1199     * @return The plugin of the specified name, if installed and loaded, or {@code null} otherwise.
1200     */
1201    public static Object getPlugin(String name) {
1202        for (PluginProxy plugin : pluginList) {
1203            if (plugin.getPluginInformation().name.equals(name))
1204                return plugin.getPlugin();
1205        }
1206        return null;
1207    }
1208
1209    /**
1210     * Returns the plugin class loader for the plugin of the specified name.
1211     * @param name The plugin name
1212     * @return The plugin class loader for the plugin of the specified name, if
1213     * installed and loaded, or {@code null} otherwise.
1214     * @since 12323
1215     */
1216    public static PluginClassLoader getPluginClassLoader(String name) {
1217        for (PluginProxy plugin : pluginList) {
1218            if (plugin.getPluginInformation().name.equals(name))
1219                return plugin.getClassLoader();
1220        }
1221        return null;
1222    }
1223
1224    public static void addDownloadSelection(List<DownloadSelection> downloadSelections) {
1225        for (PluginProxy p : pluginList) {
1226            p.addDownloadSelection(downloadSelections);
1227        }
1228    }
1229
1230    public static Collection<PreferenceSettingFactory> getPreferenceSetting() {
1231        Collection<PreferenceSettingFactory> settings = new ArrayList<>();
1232        for (PluginProxy plugin : pluginList) {
1233            settings.add(new PluginPreferenceFactory(plugin));
1234        }
1235        return settings;
1236    }
1237
1238    /**
1239     * Installs downloaded plugins. Moves files with the suffix ".jar.new" to the corresponding ".jar" files.
1240     *
1241     * If {@code dowarn} is true, this methods emits warning messages on the console if a downloaded
1242     * but not yet installed plugin .jar can't be be installed. If {@code dowarn} is false, the
1243     * installation of the respective plugin is silently skipped.
1244     *
1245     * @param pluginsToLoad list of plugin informations to update
1246     * @param dowarn if true, warning messages are displayed; false otherwise
1247     * @since 13294
1248     */
1249    public static void installDownloadedPlugins(Collection<PluginInformation> pluginsToLoad, boolean dowarn) {
1250        File pluginDir = Preferences.main().getPluginsDirectory();
1251        if (!pluginDir.exists() || !pluginDir.isDirectory() || !pluginDir.canWrite())
1252            return;
1253
1254        final File[] files = pluginDir.listFiles((FilenameFilter) (dir, name) -> name.endsWith(".jar.new"));
1255        if (files == null)
1256            return;
1257
1258        for (File updatedPlugin : files) {
1259            final String filePath = updatedPlugin.getPath();
1260            File plugin = new File(filePath.substring(0, filePath.length() - 4));
1261            String pluginName = updatedPlugin.getName().substring(0, updatedPlugin.getName().length() - 8);
1262            try {
1263                // Check the plugin is a valid and accessible JAR file before installing it (fix #7754)
1264                new JarFile(updatedPlugin).close();
1265            } catch (IOException e) {
1266                if (dowarn) {
1267                    Logging.log(Logging.LEVEL_WARN, tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. {2}",
1268                            plugin.toString(), updatedPlugin.toString(), e.getLocalizedMessage()), e);
1269                }
1270                continue;
1271            }
1272            if (plugin.exists() && !plugin.delete() && dowarn) {
1273                Logging.warn(tr("Failed to delete outdated plugin ''{0}''.", plugin.toString()));
1274                Logging.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1275                        "Skipping installation. JOSM is still going to load the old plugin version.",
1276                        pluginName));
1277                continue;
1278            }
1279            // Install plugin
1280            if (updatedPlugin.renameTo(plugin)) {
1281                try {
1282                    // Update plugin URL
1283                    URL newPluginURL = plugin.toURI().toURL();
1284                    URL oldPluginURL = updatedPlugin.toURI().toURL();
1285                    pluginsToLoad.stream().filter(x -> x.libraries.contains(oldPluginURL)).forEach(
1286                            x -> Collections.replaceAll(x.libraries, oldPluginURL, newPluginURL));
1287                } catch (MalformedURLException e) {
1288                    Logging.warn(e);
1289                }
1290            } else if (dowarn) {
1291                Logging.warn(tr("Failed to install plugin ''{0}'' from temporary download file ''{1}''. Renaming failed.",
1292                        plugin.toString(), updatedPlugin.toString()));
1293                Logging.warn(tr("Failed to install already downloaded plugin ''{0}''. " +
1294                        "Skipping installation. JOSM is still going to load the old plugin version.",
1295                        pluginName));
1296            }
1297        }
1298    }
1299
1300    /**
1301     * Determines if the specified file is a valid and accessible JAR file.
1302     * @param jar The file to check
1303     * @return true if file can be opened as a JAR file.
1304     * @since 5723
1305     */
1306    public static boolean isValidJar(File jar) {
1307        if (jar != null && jar.exists() && jar.canRead()) {
1308            try {
1309                new JarFile(jar).close();
1310            } catch (IOException e) {
1311                Logging.warn(e);
1312                return false;
1313            }
1314            return true;
1315        } else if (jar != null) {
1316            Logging.warn("Invalid jar file ''"+jar+"'' (exists: "+jar.exists()+", canRead: "+jar.canRead()+')');
1317        }
1318        return false;
1319    }
1320
1321    /**
1322     * Replies the updated jar file for the given plugin name.
1323     * @param name The plugin name to find.
1324     * @return the updated jar file for the given plugin name. null if not found or not readable.
1325     * @since 5601
1326     */
1327    public static File findUpdatedJar(String name) {
1328        File pluginDir = Preferences.main().getPluginsDirectory();
1329        // Find the downloaded file. We have tried to install the downloaded plugins
1330        // (PluginHandler.installDownloadedPlugins). This succeeds depending on the platform.
1331        File downloadedPluginFile = new File(pluginDir, name + ".jar.new");
1332        if (!isValidJar(downloadedPluginFile)) {
1333            downloadedPluginFile = new File(pluginDir, name + ".jar");
1334            if (!isValidJar(downloadedPluginFile)) {
1335                return null;
1336            }
1337        }
1338        return downloadedPluginFile;
1339    }
1340
1341    /**
1342     * Refreshes the given PluginInformation objects with new contents read from their corresponding jar file.
1343     * @param updatedPlugins The PluginInformation objects to update.
1344     * @since 5601
1345     */
1346    public static void refreshLocalUpdatedPluginInfo(Collection<PluginInformation> updatedPlugins) {
1347        if (updatedPlugins == null) return;
1348        for (PluginInformation pi : updatedPlugins) {
1349            File downloadedPluginFile = findUpdatedJar(pi.name);
1350            if (downloadedPluginFile == null) {
1351                continue;
1352            }
1353            try {
1354                pi.updateFromJar(new PluginInformation(downloadedPluginFile, pi.name));
1355            } catch (PluginException e) {
1356                Logging.error(e);
1357            }
1358        }
1359    }
1360
1361    private static int askUpdateDisableKeepPluginAfterException(PluginProxy plugin) {
1362        final ButtonSpec[] options = new ButtonSpec[] {
1363                new ButtonSpec(
1364                        tr("Update plugin"),
1365                        new ImageProvider("dialogs", "refresh"),
1366                        tr("Click to update the plugin ''{0}''", plugin.getPluginInformation().name),
1367                        null /* no specific help context */
1368                ),
1369                new ButtonSpec(
1370                        tr("Disable plugin"),
1371                        new ImageProvider("dialogs", "delete"),
1372                        tr("Click to disable the plugin ''{0}''", plugin.getPluginInformation().name),
1373                        null /* no specific help context */
1374                ),
1375                new ButtonSpec(
1376                        tr("Keep plugin"),
1377                        new ImageProvider("cancel"),
1378                        tr("Click to keep the plugin ''{0}''", plugin.getPluginInformation().name),
1379                        null /* no specific help context */
1380                )
1381        };
1382
1383        final StringBuilder msg = new StringBuilder(256);
1384        msg.append("<html>")
1385           .append(tr("An unexpected exception occurred that may have come from the ''{0}'' plugin.",
1386                   Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().name)))
1387           .append("<br>");
1388        if (plugin.getPluginInformation().author != null) {
1389            msg.append(tr("According to the information within the plugin, the author is {0}.",
1390                    Utils.escapeReservedCharactersHTML(plugin.getPluginInformation().author)))
1391               .append("<br>");
1392        }
1393        msg.append(tr("Try updating to the newest version of this plugin before reporting a bug."))
1394           .append("</html>");
1395
1396        try {
1397            FutureTask<Integer> task = new FutureTask<>(() -> HelpAwareOptionPane.showOptionDialog(
1398                    MainApplication.getMainFrame(),
1399                    msg.toString(),
1400                    tr("Update plugins"),
1401                    JOptionPane.QUESTION_MESSAGE,
1402                    null,
1403                    options,
1404                    options[0],
1405                    ht("/ErrorMessages#ErrorInPlugin")
1406            ));
1407            GuiHelper.runInEDT(task);
1408            return task.get();
1409        } catch (InterruptedException | ExecutionException e) {
1410            Logging.warn(e);
1411        }
1412        return -1;
1413    }
1414
1415    /**
1416     * Replies the plugin which most likely threw the exception <code>ex</code>.
1417     *
1418     * @param ex the exception
1419     * @return the plugin; null, if the exception probably wasn't thrown from a plugin
1420     */
1421    private static PluginProxy getPluginCausingException(Throwable ex) {
1422        PluginProxy err = null;
1423        List<StackTraceElement> stack = new ArrayList<>();
1424        Set<Throwable> seen = new HashSet<>();
1425        Throwable current = ex;
1426        while (current != null) {
1427            seen.add(current);
1428            stack.addAll(Arrays.asList(current.getStackTrace()));
1429            Throwable cause = current.getCause();
1430            if (cause != null && seen.contains(cause)) {
1431                break; // circular refernce
1432            }
1433            current = cause;
1434        }
1435
1436        // remember the error position, as multiple plugins may be involved, we search the topmost one
1437        int pos = stack.size();
1438        for (PluginProxy p : pluginList) {
1439            String baseClass = p.getPluginInformation().className;
1440            baseClass = baseClass.substring(0, baseClass.lastIndexOf('.'));
1441            for (int elpos = 0; elpos < pos; ++elpos) {
1442                if (stack.get(elpos).getClassName().startsWith(baseClass)) {
1443                    pos = elpos;
1444                    err = p;
1445                }
1446            }
1447        }
1448        return err;
1449    }
1450
1451    /**
1452     * Checks whether the exception <code>e</code> was thrown by a plugin. If so,
1453     * conditionally updates or deactivates the plugin, but asks the user first.
1454     *
1455     * @param e the exception
1456     * @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
1457     */
1458    public static PluginDownloadTask updateOrdisablePluginAfterException(Throwable e) {
1459        PluginProxy plugin = null;
1460        // Check for an explicit problem when calling a plugin function
1461        if (e instanceof PluginException) {
1462            plugin = ((PluginException) e).plugin;
1463        }
1464        if (plugin == null) {
1465            plugin = getPluginCausingException(e);
1466        }
1467        if (plugin == null)
1468            // don't know what plugin threw the exception
1469            return null;
1470
1471        Set<String> plugins = new HashSet<>(Config.getPref().getList("plugins"));
1472        final PluginInformation pluginInfo = plugin.getPluginInformation();
1473        if (!plugins.contains(pluginInfo.name))
1474            // plugin not activated ? strange in this context but anyway, don't bother
1475            // the user with dialogs, skip conditional deactivation
1476            return null;
1477
1478        switch (askUpdateDisableKeepPluginAfterException(plugin)) {
1479        case 0:
1480            // update the plugin
1481            updatePlugins(MainApplication.getMainFrame(), Collections.singleton(pluginInfo), null, true);
1482            return pluginDownloadTask;
1483        case 1:
1484            // deactivate the plugin
1485            plugins.remove(plugin.getPluginInformation().name);
1486            Config.getPref().putList("plugins", new ArrayList<>(plugins));
1487            GuiHelper.runInEDTAndWait(() -> JOptionPane.showMessageDialog(
1488                    MainApplication.getMainFrame(),
1489                    tr("The plugin has been removed from the configuration. Please restart JOSM to unload the plugin."),
1490                    tr("Information"),
1491                    JOptionPane.INFORMATION_MESSAGE
1492            ));
1493            return null;
1494        default:
1495            // user doesn't want to deactivate the plugin
1496            return null;
1497        }
1498    }
1499
1500    /**
1501     * Returns the list of loaded plugins as a {@code String} to be displayed in status report. Useful for bug reports.
1502     * @return The list of loaded plugins
1503     */
1504    public static Collection<String> getBugReportInformation() {
1505        final Collection<String> pl = new TreeSet<>(Config.getPref().getList("plugins", new LinkedList<>()));
1506        for (final PluginProxy pp : pluginList) {
1507            PluginInformation pi = pp.getPluginInformation();
1508            pl.remove(pi.name);
1509            pl.add(pi.name + " (" + (pi.localversion != null && !pi.localversion.isEmpty()
1510                    ? pi.localversion : "unknown") + ')');
1511        }
1512        return pl;
1513    }
1514
1515    /**
1516     * Returns the list of loaded plugins as a {@code JPanel} to be displayed in About dialog.
1517     * @return The list of loaded plugins (one "line" of Swing components per plugin)
1518     */
1519    public static JPanel getInfoPanel() {
1520        JPanel pluginTab = new JPanel(new GridBagLayout());
1521        for (final PluginProxy p : pluginList) {
1522            final PluginInformation info = p.getPluginInformation();
1523            String name = info.name
1524            + (info.version != null && !info.version.isEmpty() ? " Version: " + info.version : "");
1525            pluginTab.add(new JLabel(name), GBC.std());
1526            pluginTab.add(Box.createHorizontalGlue(), GBC.std().fill(GBC.HORIZONTAL));
1527            pluginTab.add(new JButton(new PluginInformationAction(info)), GBC.eol());
1528
1529            JosmTextArea description = new JosmTextArea(info.description == null ? tr("no description available")
1530                    : info.description);
1531            description.setEditable(false);
1532            description.setFont(new JLabel().getFont().deriveFont(Font.ITALIC));
1533            description.setLineWrap(true);
1534            description.setWrapStyleWord(true);
1535            description.setBorder(BorderFactory.createEmptyBorder(0, 20, 0, 0));
1536            description.setBackground(UIManager.getColor("Panel.background"));
1537            description.setCaretPosition(0);
1538
1539            pluginTab.add(description, GBC.eop().fill(GBC.HORIZONTAL));
1540        }
1541        return pluginTab;
1542    }
1543
1544    /**
1545     * Returns the set of deprecated and unmaintained plugins.
1546     * @return set of deprecated and unmaintained plugins names.
1547     * @since 8938
1548     */
1549    public static Set<String> getDeprecatedAndUnmaintainedPlugins() {
1550        Set<String> result = new HashSet<>(DEPRECATED_PLUGINS.size() + UNMAINTAINED_PLUGINS.size());
1551        for (DeprecatedPlugin dp : DEPRECATED_PLUGINS) {
1552            result.add(dp.name);
1553        }
1554        result.addAll(UNMAINTAINED_PLUGINS);
1555        return result;
1556    }
1557
1558    private static class UpdatePluginsMessagePanel extends JPanel {
1559        private final JMultilineLabel lblMessage = new JMultilineLabel("");
1560        private final JCheckBox cbDontShowAgain = new JCheckBox(
1561                tr("Do not ask again and remember my decision (go to Preferences->Plugins to change it later)"));
1562
1563        UpdatePluginsMessagePanel() {
1564            build();
1565        }
1566
1567        protected final void build() {
1568            setLayout(new GridBagLayout());
1569            GridBagConstraints gc = new GridBagConstraints();
1570            gc.anchor = GridBagConstraints.NORTHWEST;
1571            gc.fill = GridBagConstraints.BOTH;
1572            gc.weightx = 1.0;
1573            gc.weighty = 1.0;
1574            gc.insets = new Insets(5, 5, 5, 5);
1575            add(lblMessage, gc);
1576            lblMessage.setFont(lblMessage.getFont().deriveFont(Font.PLAIN));
1577
1578            gc.gridy = 1;
1579            gc.fill = GridBagConstraints.HORIZONTAL;
1580            gc.weighty = 0.0;
1581            add(cbDontShowAgain, gc);
1582            cbDontShowAgain.setFont(cbDontShowAgain.getFont().deriveFont(Font.PLAIN));
1583        }
1584
1585        public void setMessage(String message) {
1586            lblMessage.setText(message);
1587        }
1588
1589        public void initDontShowAgain(String preferencesKey) {
1590            String policy = Config.getPref().get(preferencesKey, "ask");
1591            policy = policy.trim().toLowerCase(Locale.ENGLISH);
1592            cbDontShowAgain.setSelected(!"ask".equals(policy));
1593        }
1594
1595        public boolean isRememberDecision() {
1596            return cbDontShowAgain.isSelected();
1597        }
1598    }
1599}
Note: See TracBrowser for help on using the repository browser.