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

Last change on this file since 11530 was 11530, checked in by Klumbumbus, 7 months ago

OpenStreetCam plugin replaced OpenStreetView plugin

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