source: josm/trunk/src/org/openstreetmap/josm/plugins/PluginInformation.java @ 5241

Revision 5121, 17.0 KB checked in by Don-vip, 2 months ago (diff)

Enhancements in plugin dependencies system (view "requires" in plugin prefs + auto selection of required plugins + alert when unselecting a plugin still required)

  • Property svn:eol-style set to native
Line 
1// License: GPL. Copyright 2007 by Immanuel Scholz and others
2package org.openstreetmap.josm.plugins;
3
4import static org.openstreetmap.josm.tools.I18n.tr;
5
6import java.awt.Image;
7import java.awt.image.BufferedImage;
8import java.io.File;
9import java.io.FileInputStream;
10import java.io.IOException;
11import java.io.InputStream;
12import java.lang.reflect.Constructor;
13import java.lang.reflect.InvocationTargetException;
14import java.net.MalformedURLException;
15import java.net.URL;
16import java.util.ArrayList;
17import java.util.Collection;
18import java.util.LinkedList;
19import java.util.List;
20import java.util.Map;
21import java.util.TreeMap;
22import java.util.jar.Attributes;
23import java.util.jar.JarInputStream;
24import java.util.jar.Manifest;
25import javax.swing.ImageIcon;
26
27import org.openstreetmap.josm.Main;
28import org.openstreetmap.josm.data.Version;
29import org.openstreetmap.josm.tools.ImageProvider;
30import 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 */
38public 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>&nbsp;</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}
Note: See TracBrowser for help on using the repository browser.