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

Last change on this file since 12841 was 12841, checked in by bastiK, 3 months ago

see #15229 - fix deprecations caused by [12840]

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