001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import static org.openstreetmap.josm.tools.I18n.tr;
005
006import java.io.File;
007import java.net.URL;
008import java.net.URLClassLoader;
009import java.security.AccessController;
010import java.security.PrivilegedAction;
011import java.util.List;
012
013import org.openstreetmap.josm.Main;
014import org.openstreetmap.josm.gui.MapFrame;
015import org.openstreetmap.josm.gui.MapFrameListener;
016import org.openstreetmap.josm.gui.download.DownloadSelection;
017import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
018import org.openstreetmap.josm.spi.preferences.Config;
019import org.openstreetmap.josm.spi.preferences.IBaseDirectories;
020import org.openstreetmap.josm.tools.Logging;
021import org.openstreetmap.josm.tools.Utils;
022
023/**
024 * For all purposes of loading dynamic resources, the Plugin's class loader should be used
025 * (or else, the plugin jar will not be within the class path).
026 *
027 * A plugin may subclass this abstract base class (but it is optional).
028 *
029 * The actual implementation of this class is optional, as all functions will be called
030 * via reflection. This is to be able to change this interface without the need of
031 * recompiling or even breaking the plugins. If your class does not provide a
032 * function here (or does provide a function with a mismatching signature), it will not
033 * be called. That simple.
034 *
035 * Or in other words: See this base class as an documentation of what automatic callbacks
036 * are provided (you can register yourself to more callbacks in your plugin class
037 * constructor).
038 *
039 * Subclassing Plugin and overriding some functions makes it easy for you to keep sync
040 * with the correct actual plugin architecture of JOSM.
041 *
042 * @author Immanuel.Scholz
043 */
044public abstract class Plugin implements MapFrameListener {
045
046    /**
047     * This is the info available for this plugin. You can access this from your
048     * constructor.
049     *
050     * (The actual implementation to request the info from a static variable
051     * is a bit hacky, but it works).
052     */
053    private PluginInformation info;
054
055    private final IBaseDirectories pluginBaseDirectories = new PluginBaseDirectories();
056
057    private class PluginBaseDirectories implements IBaseDirectories {
058        private File preferencesDir;
059        private File cacheDir;
060        private File userdataDir;
061
062        @Override
063        public File getPreferencesDirectory(boolean createIfMissing) {
064            if (preferencesDir == null) {
065                preferencesDir = Config.getDirs().getPreferencesDirectory(createIfMissing).toPath()
066                        .resolve("plugins").resolve(info.name).toFile();
067            }
068            if (createIfMissing && !preferencesDir.exists() && !preferencesDir.mkdirs()) {
069                Logging.error(tr("Failed to create missing plugin preferences directory: {0}", preferencesDir.getAbsoluteFile()));
070            }
071            return preferencesDir;
072        }
073
074        @Override
075        public File getUserDataDirectory(boolean createIfMissing) {
076            if (userdataDir == null) {
077                userdataDir = Config.getDirs().getUserDataDirectory(createIfMissing).toPath()
078                        .resolve("plugins").resolve(info.name).toFile();
079            }
080            if (createIfMissing && !userdataDir.exists() && !userdataDir.mkdirs()) {
081                Logging.error(tr("Failed to create missing plugin user data directory: {0}", userdataDir.getAbsoluteFile()));
082            }
083            return userdataDir;
084        }
085
086        @Override
087        public File getCacheDirectory(boolean createIfMissing) {
088            if (cacheDir == null) {
089                cacheDir = Config.getDirs().getCacheDirectory(createIfMissing).toPath()
090                        .resolve("plugins").resolve(info.name).toFile();
091            }
092            if (createIfMissing && !cacheDir.exists() && !cacheDir.mkdirs()) {
093                Logging.error(tr("Failed to create missing plugin cache directory: {0}", cacheDir.getAbsoluteFile()));
094            }
095            return cacheDir;
096        }
097    }
098
099    /**
100     * Creates the plugin
101     *
102     * @param info the plugin information describing the plugin.
103     */
104    public Plugin(PluginInformation info) {
105        this.info = info;
106    }
107
108    /**
109     * Replies the plugin information object for this plugin
110     *
111     * @return the plugin information object
112     */
113    public PluginInformation getPluginInformation() {
114        return info;
115    }
116
117    /**
118     * Sets the plugin information object for this plugin
119     *
120     * @param info the plugin information object
121     */
122    public void setPluginInformation(PluginInformation info) {
123        this.info = info;
124    }
125
126    /**
127     * Get the directories where this plugin can store various files.
128     * @return the directories where this plugin can store files
129     * @since 13007
130     */
131    public IBaseDirectories getPluginDirs() {
132        return pluginBaseDirectories;
133    }
134
135    @Override
136    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {}
137
138    /**
139     * Called in the preferences dialog to create a preferences page for the plugin,
140     * if any available.
141     * @return the preferences dialog, or {@code null}
142     */
143    public PreferenceSetting getPreferenceSetting() {
144        return null;
145    }
146
147    /**
148     * Called in the download dialog to give the plugin a chance to modify the list
149     * of bounding box selectors.
150     * @param list list of bounding box selectors
151     */
152    public void addDownloadSelection(List<DownloadSelection> list) {}
153
154    /**
155     * Get a class loader for loading resources from the plugin jar.
156     *
157     * This can be used to avoid getting a file from another plugin that
158     * happens to have a file with the same file name and path.
159     *
160     * Usage: Instead of
161     *   getClass().getResource("/resources/pluginProperties.properties");
162     * write
163     *   getPluginResourceClassLoader().getResource("resources/pluginProperties.properties");
164     *
165     * (Note the missing leading "/".)
166     * @return a class loader for loading resources from the plugin jar
167     */
168    public ClassLoader getPluginResourceClassLoader() {
169        File pluginDir = Main.pref.getPluginsDirectory();
170        File pluginJar = new File(pluginDir, info.name + ".jar");
171        final URL pluginJarUrl = Utils.fileToURL(pluginJar);
172        return AccessController.doPrivileged((PrivilegedAction<ClassLoader>)
173                () -> new URLClassLoader(new URL[] {pluginJarUrl}, Plugin.class.getClassLoader()));
174    }
175}