001// License: GPL. For details, see LICENSE file.
002package org.openstreetmap.josm.plugins;
003
004import java.io.File;
005import java.io.FileNotFoundException;
006import java.io.IOException;
007import java.io.InputStream;
008import java.net.URL;
009import java.net.URLClassLoader;
010import java.nio.file.Files;
011import java.nio.file.StandardCopyOption;
012import java.security.AccessController;
013import java.security.PrivilegedAction;
014import java.util.List;
015
016import org.openstreetmap.josm.Main;
017import org.openstreetmap.josm.gui.MapFrame;
018import org.openstreetmap.josm.gui.MapFrameListener;
019import org.openstreetmap.josm.gui.download.DownloadSelection;
020import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
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    /**
056     * Creates the plugin
057     *
058     * @param info the plugin information describing the plugin.
059     */
060    public Plugin(PluginInformation info) {
061        this.info = info;
062    }
063
064    /**
065     * Replies the plugin information object for this plugin
066     *
067     * @return the plugin information object
068     */
069    public PluginInformation getPluginInformation() {
070        return info;
071    }
072
073    /**
074     * Sets the plugin information object for this plugin
075     *
076     * @param info the plugin information object
077     */
078    public void setPluginInformation(PluginInformation info) {
079        this.info = info;
080    }
081
082    /**
083     * @return The directory for the plugin to store all kind of stuff.
084     */
085    public String getPluginDir() {
086        return new File(Main.pref.getPluginsDirectory(), info.name).getPath();
087    }
088
089    @Override
090    public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {}
091
092    /**
093     * Called in the preferences dialog to create a preferences page for the plugin,
094     * if any available.
095     * @return the preferences dialog, or {@code null}
096     */
097    public PreferenceSetting getPreferenceSetting() {
098        return null;
099    }
100
101    /**
102     * Called in the download dialog to give the plugin a chance to modify the list
103     * of bounding box selectors.
104     * @param list list of bounding box selectors
105     */
106    public void addDownloadSelection(List<DownloadSelection> list) {}
107
108    /**
109     * Copies the resource 'from' to the file in the plugin directory named 'to'.
110     * @param from source file
111     * @param to target file
112     * @throws FileNotFoundException if the file exists but is a directory rather than a regular file,
113     * does not exist but cannot be created, or cannot be opened for any other reason
114     * @throws IOException if any other I/O error occurs
115     */
116    public void copy(String from, String to) throws IOException {
117        String pluginDirName = getPluginDir();
118        File pluginDir = new File(pluginDirName);
119        if (!pluginDir.exists()) {
120            Utils.mkDirs(pluginDir);
121        }
122        try (InputStream in = getClass().getResourceAsStream(from)) {
123            if (in == null) {
124                throw new IOException("Resource not found: "+from);
125            }
126            Files.copy(in, new File(pluginDirName, to).toPath(), StandardCopyOption.REPLACE_EXISTING);
127        }
128    }
129
130    /**
131     * Get a class loader for loading resources from the plugin jar.
132     *
133     * This can be used to avoid getting a file from another plugin that
134     * happens to have a file with the same file name and path.
135     *
136     * Usage: Instead of
137     *   getClass().getResource("/resources/pluginProperties.properties");
138     * write
139     *   getPluginResourceClassLoader().getResource("resources/pluginProperties.properties");
140     *
141     * (Note the missing leading "/".)
142     * @return a class loader for loading resources from the plugin jar
143     */
144    public ClassLoader getPluginResourceClassLoader() {
145        File pluginDir = Main.pref.getPluginsDirectory();
146        File pluginJar = new File(pluginDir, info.name + ".jar");
147        final URL pluginJarUrl = Utils.fileToURL(pluginJar);
148        return AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
149              @Override
150              public ClassLoader run() {
151                  return new URLClassLoader(new URL[] {pluginJarUrl}, Main.class.getClassLoader());
152              }
153        });
154    }
155}