| 1 | // License: GPL. Copyright 2007 by Immanuel Scholz and others
|
|---|
| 2 | package org.openstreetmap.josm.plugins;
|
|---|
| 3 |
|
|---|
| 4 | import static org.openstreetmap.josm.tools.I18n.tr;
|
|---|
| 5 |
|
|---|
| 6 | import java.awt.Image;
|
|---|
| 7 | import java.io.File;
|
|---|
| 8 | import java.io.FileInputStream;
|
|---|
| 9 | import java.io.IOException;
|
|---|
| 10 | import java.io.InputStream;
|
|---|
| 11 | import java.lang.reflect.Constructor;
|
|---|
| 12 | import java.lang.reflect.InvocationTargetException;
|
|---|
| 13 | import java.net.MalformedURLException;
|
|---|
| 14 | import java.net.URL;
|
|---|
| 15 | import java.util.ArrayList;
|
|---|
| 16 | import java.util.Collection;
|
|---|
| 17 | import java.util.LinkedList;
|
|---|
| 18 | import java.util.List;
|
|---|
| 19 | import java.util.Map;
|
|---|
| 20 | import java.util.TreeMap;
|
|---|
| 21 | import java.util.jar.Attributes;
|
|---|
| 22 | import java.util.jar.JarInputStream;
|
|---|
| 23 | import java.util.jar.Manifest;
|
|---|
| 24 | import javax.swing.ImageIcon;
|
|---|
| 25 |
|
|---|
| 26 | import org.openstreetmap.josm.Main;
|
|---|
| 27 | import org.openstreetmap.josm.data.Version;
|
|---|
| 28 | import org.openstreetmap.josm.tools.ImageProvider;
|
|---|
| 29 | import org.openstreetmap.josm.tools.LanguageInfo;
|
|---|
| 30 |
|
|---|
| 31 | /**
|
|---|
| 32 | * Encapsulate general information about a plugin. This information is available
|
|---|
| 33 | * without the need of loading any class from the plugin jar file.
|
|---|
| 34 | *
|
|---|
| 35 | * @author imi
|
|---|
| 36 | */
|
|---|
| 37 | public class PluginInformation {
|
|---|
| 38 | public File file = null;
|
|---|
| 39 | public String name = null;
|
|---|
| 40 | public int mainversion = 0;
|
|---|
| 41 | public String className = null;
|
|---|
| 42 | public boolean oldmode = false;
|
|---|
| 43 | public String requires = null;
|
|---|
| 44 | public String link = null;
|
|---|
| 45 | public String description = null;
|
|---|
| 46 | public boolean early = false;
|
|---|
| 47 | public String author = null;
|
|---|
| 48 | public int stage = 50;
|
|---|
| 49 | public String version = null;
|
|---|
| 50 | public String localversion = null;
|
|---|
| 51 | public String downloadlink = null;
|
|---|
| 52 | public String iconPath;
|
|---|
| 53 | public ImageIcon icon;
|
|---|
| 54 | public List<URL> libraries = new LinkedList<URL>();
|
|---|
| 55 | public final Map<String, String> attr = new TreeMap<String, String>();
|
|---|
| 56 |
|
|---|
| 57 | /**
|
|---|
| 58 | * Creates a plugin information object by reading the plugin information from
|
|---|
| 59 | * the manifest in the plugin jar.
|
|---|
| 60 | *
|
|---|
| 61 | * The plugin name is derived from the file name.
|
|---|
| 62 | *
|
|---|
| 63 | * @param file the plugin jar file
|
|---|
| 64 | * @throws PluginException if reading the manifest fails
|
|---|
| 65 | */
|
|---|
| 66 | public PluginInformation(File file) throws PluginException{
|
|---|
| 67 | this(file, file.getName().substring(0, file.getName().length()-4));
|
|---|
| 68 | }
|
|---|
| 69 |
|
|---|
| 70 | /**
|
|---|
| 71 | * Creates a plugin information object for the plugin with name {@code name}.
|
|---|
| 72 | * Information about the plugin is extracted from the maifest file in the the plugin jar
|
|---|
| 73 | * {@code file}.
|
|---|
| 74 | * @param file the plugin jar
|
|---|
| 75 | * @param name the plugin name
|
|---|
| 76 | * @throws PluginException thrown if reading the manifest file fails
|
|---|
| 77 | */
|
|---|
| 78 | public PluginInformation(File file, String name) throws PluginException{
|
|---|
| 79 | this.name = name;
|
|---|
| 80 | this.file = file;
|
|---|
| 81 | JarInputStream jar = null;
|
|---|
| 82 | try {
|
|---|
| 83 | jar = new JarInputStream(new FileInputStream(file));
|
|---|
| 84 | Manifest manifest = jar.getManifest();
|
|---|
| 85 | if (manifest == null)
|
|---|
| 86 | throw new PluginException(name, tr("The plugin file ''{0}'' does not include a Manifest.", file.toString()));
|
|---|
| 87 | scanManifest(manifest, false);
|
|---|
| 88 | libraries.add(0, fileToURL(file));
|
|---|
| 89 | } catch (IOException e) {
|
|---|
| 90 | throw new PluginException(name, e);
|
|---|
| 91 | } finally {
|
|---|
| 92 | if (jar != null) {
|
|---|
| 93 | try {
|
|---|
| 94 | jar.close();
|
|---|
| 95 | } catch(IOException e) { /* ignore */ }
|
|---|
| 96 | }
|
|---|
| 97 | }
|
|---|
| 98 | }
|
|---|
| 99 |
|
|---|
| 100 | /**
|
|---|
| 101 | * Creates a plugin information object by reading plugin information in Manifest format
|
|---|
| 102 | * from the input stream {@code manifestStream}.
|
|---|
| 103 | *
|
|---|
| 104 | * @param manifestStream the stream to read the manifest from
|
|---|
| 105 | * @param name the plugin name
|
|---|
| 106 | * @param url the download URL for the plugin
|
|---|
| 107 | * @throws PluginException thrown if the plugin information can't be read from the input stream
|
|---|
| 108 | */
|
|---|
| 109 | public PluginInformation(InputStream manifestStream, String name, String url) throws PluginException {
|
|---|
| 110 | this.name = name;
|
|---|
| 111 | try {
|
|---|
| 112 | Manifest manifest = new Manifest();
|
|---|
| 113 | manifest.read(manifestStream);
|
|---|
| 114 | if(url != null) {
|
|---|
| 115 | downloadlink = url;
|
|---|
| 116 | }
|
|---|
| 117 | scanManifest(manifest, url != null);
|
|---|
| 118 | } catch (IOException e) {
|
|---|
| 119 | throw new PluginException(name, e);
|
|---|
| 120 | }
|
|---|
| 121 | }
|
|---|
| 122 |
|
|---|
| 123 | /**
|
|---|
| 124 | * Updates the plugin information of this plugin information object with the
|
|---|
| 125 | * plugin information in a plugin information object retrieved from a plugin
|
|---|
| 126 | * update site.
|
|---|
| 127 | *
|
|---|
| 128 | * @param other the plugin information object retrieved from the update
|
|---|
| 129 | * site
|
|---|
| 130 | */
|
|---|
| 131 | public void updateFromPluginSite(PluginInformation other) {
|
|---|
| 132 | this.mainversion = other.mainversion;
|
|---|
| 133 | this.className = other.className;
|
|---|
| 134 | this.requires = other.requires;
|
|---|
| 135 | this.link = other.link;
|
|---|
| 136 | this.description = other.description;
|
|---|
| 137 | this.early = other.early;
|
|---|
| 138 | this.author = other.author;
|
|---|
| 139 | this.stage = other.stage;
|
|---|
| 140 | this.version = other.version;
|
|---|
| 141 | this.downloadlink = other.downloadlink;
|
|---|
| 142 | this.icon = other.icon;
|
|---|
| 143 | this.iconPath = other.iconPath;
|
|---|
| 144 | this.libraries = other.libraries;
|
|---|
| 145 | this.attr.clear();
|
|---|
| 146 | this.attr.putAll(other.attr);
|
|---|
| 147 | }
|
|---|
| 148 |
|
|---|
| 149 | private void scanManifest(Manifest manifest, boolean oldcheck){
|
|---|
| 150 | String lang = LanguageInfo.getLanguageCodeManifest();
|
|---|
| 151 | Attributes attr = manifest.getMainAttributes();
|
|---|
| 152 | className = attr.getValue("Plugin-Class");
|
|---|
| 153 | String s = attr.getValue(lang+"Plugin-Link");
|
|---|
| 154 | if(s == null) {
|
|---|
| 155 | s = attr.getValue("Plugin-Link");
|
|---|
| 156 | }
|
|---|
| 157 | if(s != null) {
|
|---|
| 158 | try {
|
|---|
| 159 | URL url = new URL(s);
|
|---|
| 160 | } catch (MalformedURLException e) {
|
|---|
| 161 | System.out.println(tr("Invalid URL ''{0}'' in plugin {1}", s, name));
|
|---|
| 162 | s = null;
|
|---|
| 163 | }
|
|---|
| 164 | }
|
|---|
| 165 | link = s;
|
|---|
| 166 | requires = attr.getValue("Plugin-Requires");
|
|---|
| 167 | s = attr.getValue(lang+"Plugin-Description");
|
|---|
| 168 | if(s == null)
|
|---|
| 169 | {
|
|---|
| 170 | s = attr.getValue("Plugin-Description");
|
|---|
| 171 | if(s != null) {
|
|---|
| 172 | s = tr(s);
|
|---|
| 173 | }
|
|---|
| 174 | }
|
|---|
| 175 | description = s;
|
|---|
| 176 | early = Boolean.parseBoolean(attr.getValue("Plugin-Early"));
|
|---|
| 177 | String stageStr = attr.getValue("Plugin-Stage");
|
|---|
| 178 | stage = stageStr == null ? 50 : Integer.parseInt(stageStr);
|
|---|
| 179 | version = attr.getValue("Plugin-Version");
|
|---|
| 180 | try { mainversion = Integer.parseInt(attr.getValue("Plugin-Mainversion")); }
|
|---|
| 181 | catch(NumberFormatException e) {}
|
|---|
| 182 | author = attr.getValue("Author");
|
|---|
| 183 | iconPath = attr.getValue("Plugin-Icon");
|
|---|
| 184 | if (iconPath != null && file != null) {
|
|---|
| 185 | // extract icon from the plugin jar file
|
|---|
| 186 | icon = ImageProvider.getIfAvailable(null, null, null, iconPath, file);
|
|---|
| 187 | }
|
|---|
| 188 | if(oldcheck && mainversion > Version.getInstance().getVersion())
|
|---|
| 189 | {
|
|---|
| 190 | int myv = Version.getInstance().getVersion();
|
|---|
| 191 | for(Map.Entry<Object, Object> entry : attr.entrySet())
|
|---|
| 192 | {
|
|---|
| 193 | try {
|
|---|
| 194 | String key = ((Attributes.Name)entry.getKey()).toString();
|
|---|
| 195 | if(key.endsWith("_Plugin-Url"))
|
|---|
| 196 | {
|
|---|
| 197 | int mv = Integer.parseInt(key.substring(0,key.length()-11));
|
|---|
| 198 | if(mv <= myv && (mv > mainversion || mainversion > myv))
|
|---|
| 199 | {
|
|---|
| 200 | String v = (String)entry.getValue();
|
|---|
| 201 | int i = v.indexOf(";");
|
|---|
| 202 | if(i > 0)
|
|---|
| 203 | {
|
|---|
| 204 | downloadlink = v.substring(i+1);
|
|---|
| 205 | mainversion = mv;
|
|---|
| 206 | version = v.substring(0,i);
|
|---|
| 207 | oldmode = true;
|
|---|
| 208 | }
|
|---|
| 209 | }
|
|---|
| 210 | }
|
|---|
| 211 | }
|
|---|
| 212 | catch(Exception e) { e.printStackTrace(); }
|
|---|
| 213 | }
|
|---|
| 214 | }
|
|---|
| 215 |
|
|---|
| 216 | String classPath = attr.getValue(Attributes.Name.CLASS_PATH);
|
|---|
| 217 | if (classPath != null) {
|
|---|
| 218 | for (String entry : classPath.split(" ")) {
|
|---|
| 219 | File entryFile;
|
|---|
| 220 | if (new File(entry).isAbsolute() || file == null) {
|
|---|
| 221 | entryFile = new File(entry);
|
|---|
| 222 | } else {
|
|---|
| 223 | entryFile = new File(file.getParent(), entry);
|
|---|
| 224 | }
|
|---|
| 225 |
|
|---|
| 226 | libraries.add(fileToURL(entryFile));
|
|---|
| 227 | }
|
|---|
| 228 | }
|
|---|
| 229 | for (Object o : attr.keySet()) {
|
|---|
| 230 | this.attr.put(o.toString(), attr.getValue(o.toString()));
|
|---|
| 231 | }
|
|---|
| 232 | }
|
|---|
| 233 |
|
|---|
| 234 | /**
|
|---|
| 235 | * Replies the description as HTML document, including a link to a web page with
|
|---|
| 236 | * more information, provided such a link is available.
|
|---|
| 237 | *
|
|---|
| 238 | * @return the description as HTML document
|
|---|
| 239 | */
|
|---|
| 240 | public String getDescriptionAsHtml() {
|
|---|
| 241 | StringBuilder sb = new StringBuilder();
|
|---|
| 242 | sb.append("<html><body>");
|
|---|
| 243 | sb.append(description == null ? tr("no description available") : description);
|
|---|
| 244 | if (link != null) {
|
|---|
| 245 | sb.append(" <a href=\"").append(link).append("\">").append(tr("More info...")).append("</a>");
|
|---|
| 246 | }
|
|---|
| 247 | sb.append("</body></html>");
|
|---|
| 248 | return sb.toString();
|
|---|
| 249 | }
|
|---|
| 250 |
|
|---|
| 251 | /**
|
|---|
| 252 | * Load and instantiate the plugin
|
|---|
| 253 | *
|
|---|
| 254 | * @param the plugin class
|
|---|
| 255 | * @return the instantiated and initialized plugin
|
|---|
| 256 | */
|
|---|
| 257 | public PluginProxy load(Class<?> klass) throws PluginException{
|
|---|
| 258 | try {
|
|---|
| 259 | Constructor<?> c = klass.getConstructor(PluginInformation.class);
|
|---|
| 260 | Object plugin = c.newInstance(this);
|
|---|
| 261 | return new PluginProxy(plugin, this);
|
|---|
| 262 | } catch(NoSuchMethodException e) {
|
|---|
| 263 | throw new PluginException(name, e);
|
|---|
| 264 | } catch(IllegalAccessException e) {
|
|---|
| 265 | throw new PluginException(name, e);
|
|---|
| 266 | } catch (InstantiationException e) {
|
|---|
| 267 | throw new PluginException(name, e);
|
|---|
| 268 | } catch(InvocationTargetException e) {
|
|---|
| 269 | throw new PluginException(name, e);
|
|---|
| 270 | }
|
|---|
| 271 | }
|
|---|
| 272 |
|
|---|
| 273 | /**
|
|---|
| 274 | * Load the class of the plugin
|
|---|
| 275 | *
|
|---|
| 276 | * @param classLoader the class loader to use
|
|---|
| 277 | * @return the loaded class
|
|---|
| 278 | */
|
|---|
| 279 | public Class<?> loadClass(ClassLoader classLoader) throws PluginException {
|
|---|
| 280 | if (className == null)
|
|---|
| 281 | return null;
|
|---|
| 282 | try{
|
|---|
| 283 | Class<?> realClass = Class.forName(className, true, classLoader);
|
|---|
| 284 | return realClass;
|
|---|
| 285 | } catch (ClassNotFoundException e) {
|
|---|
| 286 | throw new PluginException(name, e);
|
|---|
| 287 | } catch(ClassCastException e) {
|
|---|
| 288 | throw new PluginException(name, e);
|
|---|
| 289 | }
|
|---|
| 290 | }
|
|---|
| 291 |
|
|---|
| 292 | public static URL fileToURL(File f) {
|
|---|
| 293 | try {
|
|---|
| 294 | return f.toURI().toURL();
|
|---|
| 295 | } catch (MalformedURLException ex) {
|
|---|
| 296 | return null;
|
|---|
| 297 | }
|
|---|
| 298 | }
|
|---|
| 299 |
|
|---|
| 300 | /**
|
|---|
| 301 | * Try to find a plugin after some criterias. Extract the plugin-information
|
|---|
| 302 | * from the plugin and return it. The plugin is searched in the following way:
|
|---|
| 303 | *
|
|---|
| 304 | *<li>first look after an MANIFEST.MF in the package org.openstreetmap.josm.plugins.<plugin name>
|
|---|
| 305 | * (After removing all fancy characters from the plugin name).
|
|---|
| 306 | * If found, the plugin is loaded using the bootstrap classloader.
|
|---|
| 307 | *<li>If not found, look for a jar file in the user specific plugin directory
|
|---|
| 308 | * (~/.josm/plugins/<plugin name>.jar)
|
|---|
| 309 | *<li>If not found and the environment variable JOSM_RESOURCES + "/plugins/" exist, look there.
|
|---|
| 310 | *<li>Try for the java property josm.resources + "/plugins/" (set via java -Djosm.plugins.path=...)
|
|---|
| 311 | *<li>If the environment variable ALLUSERSPROFILE and APPDATA exist, look in
|
|---|
| 312 | * ALLUSERSPROFILE/<the last stuff from APPDATA>/JOSM/plugins.
|
|---|
| 313 | * (*sic* There is no easy way under Windows to get the All User's application
|
|---|
| 314 | * directory)
|
|---|
| 315 | *<li>Finally, look in some typical unix paths:<ul>
|
|---|
| 316 | * <li>/usr/local/share/josm/plugins/
|
|---|
| 317 | * <li>/usr/local/lib/josm/plugins/
|
|---|
| 318 | * <li>/usr/share/josm/plugins/
|
|---|
| 319 | * <li>/usr/lib/josm/plugins/
|
|---|
| 320 | *
|
|---|
| 321 | * If a plugin class or jar file is found earlier in the list but seem not to
|
|---|
| 322 | * be working, an PluginException is thrown rather than continuing the search.
|
|---|
| 323 | * This is so JOSM can detect broken user-provided plugins and do not go silently
|
|---|
| 324 | * ignore them.
|
|---|
| 325 | *
|
|---|
| 326 | * The plugin is not initialized. If the plugin is a .jar file, it is not loaded
|
|---|
| 327 | * (only the manifest is extracted). In the classloader-case, the class is
|
|---|
| 328 | * bootstraped (e.g. static {} - declarations will run. However, nothing else is done.
|
|---|
| 329 | *
|
|---|
| 330 | * @param pluginName The name of the plugin (in all lowercase). E.g. "lang-de"
|
|---|
| 331 | * @return Information about the plugin or <code>null</code>, if the plugin
|
|---|
| 332 | * was nowhere to be found.
|
|---|
| 333 | * @throws PluginException In case of broken plugins.
|
|---|
| 334 | */
|
|---|
| 335 | public static PluginInformation findPlugin(String pluginName) throws PluginException {
|
|---|
| 336 | String name = pluginName;
|
|---|
| 337 | name = name.replaceAll("[-. ]", "");
|
|---|
| 338 | InputStream manifestStream = PluginInformation.class.getResourceAsStream("/org/openstreetmap/josm/plugins/"+name+"/MANIFEST.MF");
|
|---|
| 339 | if (manifestStream != null)
|
|---|
| 340 | return new PluginInformation(manifestStream, pluginName, null);
|
|---|
| 341 |
|
|---|
| 342 | Collection<String> locations = getPluginLocations();
|
|---|
| 343 |
|
|---|
| 344 | for (String s : locations) {
|
|---|
| 345 | File pluginFile = new File(s, pluginName + ".jar");
|
|---|
| 346 | if (pluginFile.exists()) {
|
|---|
| 347 | PluginInformation info = new PluginInformation(pluginFile);
|
|---|
| 348 | return info;
|
|---|
| 349 | }
|
|---|
| 350 | }
|
|---|
| 351 | return null;
|
|---|
| 352 | }
|
|---|
| 353 |
|
|---|
| 354 | public static Collection<String> getPluginLocations() {
|
|---|
| 355 | Collection<String> locations = Main.pref.getAllPossiblePreferenceDirs();
|
|---|
| 356 | Collection<String> all = new ArrayList<String>(locations.size());
|
|---|
| 357 | for (String s : locations) {
|
|---|
| 358 | all.add(s+"plugins");
|
|---|
| 359 | }
|
|---|
| 360 | return all;
|
|---|
| 361 | }
|
|---|
| 362 |
|
|---|
| 363 | /**
|
|---|
| 364 | * Replies true if the plugin with the given information is most likely outdated with
|
|---|
| 365 | * respect to the referenceVersion.
|
|---|
| 366 | *
|
|---|
| 367 | * @param referenceVersion the reference version. Can be null if we don't know a
|
|---|
| 368 | * reference version
|
|---|
| 369 | *
|
|---|
| 370 | * @return true, if the plugin needs to be updated; false, otherweise
|
|---|
| 371 | */
|
|---|
| 372 | public boolean isUpdateRequired(String referenceVersion) {
|
|---|
| 373 | if (this.downloadlink == null) return false;
|
|---|
| 374 | if (this.version == null && referenceVersion!= null)
|
|---|
| 375 | return true;
|
|---|
| 376 | if (this.version != null && !this.version.equals(referenceVersion))
|
|---|
| 377 | return true;
|
|---|
| 378 | return false;
|
|---|
| 379 | }
|
|---|
| 380 |
|
|---|
| 381 | /**
|
|---|
| 382 | * Replies true if this this plugin should be updated/downloaded because either
|
|---|
| 383 | * it is not available locally (its local version is null) or its local version is
|
|---|
| 384 | * older than the available version on the server.
|
|---|
| 385 | *
|
|---|
| 386 | * @return true if the plugin should be updated
|
|---|
| 387 | */
|
|---|
| 388 | public boolean isUpdateRequired() {
|
|---|
| 389 | if (this.downloadlink == null) return false;
|
|---|
| 390 | if (this.localversion == null) return true;
|
|---|
| 391 | return isUpdateRequired(this.localversion);
|
|---|
| 392 | }
|
|---|
| 393 |
|
|---|
| 394 | protected boolean matches(String filter, String value) {
|
|---|
| 395 | if (filter == null) return true;
|
|---|
| 396 | if (value == null) return false;
|
|---|
| 397 | return value.toLowerCase().contains(filter.toLowerCase());
|
|---|
| 398 | }
|
|---|
| 399 |
|
|---|
| 400 | /**
|
|---|
| 401 | * Replies true if either the name, the description, or the version match (case insensitive)
|
|---|
| 402 | * one of the words in filter. Replies true if filter is null.
|
|---|
| 403 | *
|
|---|
| 404 | * @param filter the filter expression
|
|---|
| 405 | * @return true if this plugin info matches with the filter
|
|---|
| 406 | */
|
|---|
| 407 | public boolean matches(String filter) {
|
|---|
| 408 | if (filter == null) return true;
|
|---|
| 409 | String words[] = filter.split("\\s+");
|
|---|
| 410 | for (String word: words) {
|
|---|
| 411 | if (matches(word, name)
|
|---|
| 412 | || matches(word, description)
|
|---|
| 413 | || matches(word, version)
|
|---|
| 414 | || matches(word, localversion))
|
|---|
| 415 | return true;
|
|---|
| 416 | }
|
|---|
| 417 | return false;
|
|---|
| 418 | }
|
|---|
| 419 |
|
|---|
| 420 | /**
|
|---|
| 421 | * Replies the name of the plugin
|
|---|
| 422 | */
|
|---|
| 423 | public String getName() {
|
|---|
| 424 | return name;
|
|---|
| 425 | }
|
|---|
| 426 |
|
|---|
| 427 | /**
|
|---|
| 428 | * Sets the name
|
|---|
| 429 | * @param name
|
|---|
| 430 | */
|
|---|
| 431 | public void setName(String name) {
|
|---|
| 432 | this.name = name;
|
|---|
| 433 | }
|
|---|
| 434 |
|
|---|
| 435 | public ImageIcon getScaledIcon() {
|
|---|
| 436 | if (icon == null)
|
|---|
| 437 | return null;
|
|---|
| 438 | return new ImageIcon(icon.getImage().getScaledInstance(24, 24, Image.SCALE_SMOOTH));
|
|---|
| 439 | }
|
|---|
| 440 | }
|
|---|