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