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