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.io.FileNotFoundException; 008import java.io.IOException; 009import java.io.InputStream; 010import java.net.URL; 011import java.net.URLClassLoader; 012import java.nio.file.Files; 013import java.nio.file.StandardCopyOption; 014import java.security.AccessController; 015import java.security.PrivilegedAction; 016import java.util.List; 017 018import org.openstreetmap.josm.Main; 019import org.openstreetmap.josm.gui.MapFrame; 020import org.openstreetmap.josm.gui.MapFrameListener; 021import org.openstreetmap.josm.gui.download.DownloadSelection; 022import org.openstreetmap.josm.gui.preferences.PreferenceSetting; 023import org.openstreetmap.josm.spi.preferences.Config; 024import org.openstreetmap.josm.spi.preferences.IBaseDirectories; 025import org.openstreetmap.josm.tools.Logging; 026import org.openstreetmap.josm.tools.Utils; 027 028/** 029 * For all purposes of loading dynamic resources, the Plugin's class loader should be used 030 * (or else, the plugin jar will not be within the class path). 031 * 032 * A plugin may subclass this abstract base class (but it is optional). 033 * 034 * The actual implementation of this class is optional, as all functions will be called 035 * via reflection. This is to be able to change this interface without the need of 036 * recompiling or even breaking the plugins. If your class does not provide a 037 * function here (or does provide a function with a mismatching signature), it will not 038 * be called. That simple. 039 * 040 * Or in other words: See this base class as an documentation of what automatic callbacks 041 * are provided (you can register yourself to more callbacks in your plugin class 042 * constructor). 043 * 044 * Subclassing Plugin and overriding some functions makes it easy for you to keep sync 045 * with the correct actual plugin architecture of JOSM. 046 * 047 * @author Immanuel.Scholz 048 */ 049public abstract class Plugin implements MapFrameListener { 050 051 /** 052 * This is the info available for this plugin. You can access this from your 053 * constructor. 054 * 055 * (The actual implementation to request the info from a static variable 056 * is a bit hacky, but it works). 057 */ 058 private PluginInformation info; 059 060 private final IBaseDirectories pluginBaseDirectories = new PluginBaseDirectories(); 061 062 private class PluginBaseDirectories implements IBaseDirectories { 063 private File preferencesDir; 064 private File cacheDir; 065 private File userdataDir; 066 067 @Override 068 public File getPreferencesDirectory(boolean createIfMissing) { 069 if (preferencesDir == null) { 070 preferencesDir = Config.getDirs().getPreferencesDirectory(createIfMissing).toPath() 071 .resolve("plugins").resolve(info.name).toFile(); 072 } 073 if (createIfMissing && !preferencesDir.exists() && !preferencesDir.mkdirs()) { 074 Logging.error(tr("Failed to create missing plugin preferences directory: {0}", preferencesDir.getAbsoluteFile())); 075 } 076 return preferencesDir; 077 } 078 079 @Override 080 public File getUserDataDirectory(boolean createIfMissing) { 081 if (userdataDir == null) { 082 userdataDir = Config.getDirs().getUserDataDirectory(createIfMissing).toPath() 083 .resolve("plugins").resolve(info.name).toFile(); 084 } 085 if (createIfMissing && !userdataDir.exists() && !userdataDir.mkdirs()) { 086 Logging.error(tr("Failed to create missing plugin user data directory: {0}", userdataDir.getAbsoluteFile())); 087 } 088 return userdataDir; 089 } 090 091 @Override 092 public File getCacheDirectory(boolean createIfMissing) { 093 if (cacheDir == null) { 094 cacheDir = Config.getDirs().getCacheDirectory(createIfMissing).toPath() 095 .resolve("plugins").resolve(info.name).toFile(); 096 } 097 if (createIfMissing && !cacheDir.exists() && !cacheDir.mkdirs()) { 098 Logging.error(tr("Failed to create missing plugin cache directory: {0}", cacheDir.getAbsoluteFile())); 099 } 100 return cacheDir; 101 } 102 } 103 104 /** 105 * Creates the plugin 106 * 107 * @param info the plugin information describing the plugin. 108 */ 109 public Plugin(PluginInformation info) { 110 this.info = info; 111 } 112 113 /** 114 * Replies the plugin information object for this plugin 115 * 116 * @return the plugin information object 117 */ 118 public PluginInformation getPluginInformation() { 119 return info; 120 } 121 122 /** 123 * Sets the plugin information object for this plugin 124 * 125 * @param info the plugin information object 126 */ 127 public void setPluginInformation(PluginInformation info) { 128 this.info = info; 129 } 130 131 /** 132 * Get the directories where this plugin can store various files. 133 * @return the directories where this plugin can store files 134 * @since 13007 135 */ 136 public IBaseDirectories getPluginDirs() { 137 return pluginBaseDirectories; 138 } 139 140 /** 141 * @return The directory for the plugin to store all kind of stuff. 142 * @deprecated (since 13007) to get the same directory as this method, use {@code getPluginDirs().getUserDataDirectory(false)}. 143 * However, for files that can be characterized as cache or preferences, you are encouraged to use the appropriate 144 * {@link IBaseDirectories} method from {@link #getPluginDirs()}. 145 */ 146 @Deprecated 147 public String getPluginDir() { 148 return new File(Main.pref.getPluginsDirectory(), info.name).getPath(); 149 } 150 151 @Override 152 public void mapFrameInitialized(MapFrame oldFrame, MapFrame newFrame) {} 153 154 /** 155 * Called in the preferences dialog to create a preferences page for the plugin, 156 * if any available. 157 * @return the preferences dialog, or {@code null} 158 */ 159 public PreferenceSetting getPreferenceSetting() { 160 return null; 161 } 162 163 /** 164 * Called in the download dialog to give the plugin a chance to modify the list 165 * of bounding box selectors. 166 * @param list list of bounding box selectors 167 */ 168 public void addDownloadSelection(List<DownloadSelection> list) {} 169 170 /** 171 * Copies the resource 'from' to the file in the plugin directory named 'to'. 172 * @param from source file 173 * @param to target file 174 * @throws FileNotFoundException if the file exists but is a directory rather than a regular file, 175 * does not exist but cannot be created, or cannot be opened for any other reason 176 * @throws IOException if any other I/O error occurs 177 * @deprecated without replacement 178 */ 179 @Deprecated 180 public void copy(String from, String to) throws IOException { 181 String pluginDirName = getPluginDir(); 182 File pluginDir = new File(pluginDirName); 183 if (!pluginDir.exists()) { 184 Utils.mkDirs(pluginDir); 185 } 186 try (InputStream in = getClass().getResourceAsStream(from)) { 187 if (in == null) { 188 throw new IOException("Resource not found: "+from); 189 } 190 Files.copy(in, new File(pluginDirName, to).toPath(), StandardCopyOption.REPLACE_EXISTING); 191 } 192 } 193 194 /** 195 * Get a class loader for loading resources from the plugin jar. 196 * 197 * This can be used to avoid getting a file from another plugin that 198 * happens to have a file with the same file name and path. 199 * 200 * Usage: Instead of 201 * getClass().getResource("/resources/pluginProperties.properties"); 202 * write 203 * getPluginResourceClassLoader().getResource("resources/pluginProperties.properties"); 204 * 205 * (Note the missing leading "/".) 206 * @return a class loader for loading resources from the plugin jar 207 */ 208 public ClassLoader getPluginResourceClassLoader() { 209 File pluginDir = Main.pref.getPluginsDirectory(); 210 File pluginJar = new File(pluginDir, info.name + ".jar"); 211 final URL pluginJarUrl = Utils.fileToURL(pluginJar); 212 return AccessController.doPrivileged((PrivilegedAction<ClassLoader>) 213 () -> new URLClassLoader(new URL[] {pluginJarUrl}, Plugin.class.getClassLoader())); 214 } 215}