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.awt.Image;
007import java.awt.image.BufferedImage;
008import java.io.File;
009import java.io.FileInputStream;
010import java.io.IOException;
011import java.io.InputStream;
012import java.lang.reflect.Constructor;
013import java.lang.reflect.InvocationTargetException;
014import java.net.MalformedURLException;
015import java.net.URL;
016import java.util.ArrayList;
017import java.util.Collection;
018import java.util.LinkedList;
019import java.util.List;
020import java.util.Map;
021import java.util.TreeMap;
022import java.util.jar.Attributes;
023import java.util.jar.JarInputStream;
024import java.util.jar.Manifest;
025
026import javax.swing.ImageIcon;
027
028import org.openstreetmap.josm.Main;
029import org.openstreetmap.josm.data.Version;
030import org.openstreetmap.josm.tools.ImageProvider;
031import org.openstreetmap.josm.tools.LanguageInfo;
032import org.openstreetmap.josm.tools.Utils;
033
034/**
035 * Encapsulate general information about a plugin. This information is available
036 * without the need of loading any class from the plugin jar file.
037 *
038 * @author imi
039 */
040public class PluginInformation {
041    public File file = null;
042    public String name = null;
043    public int mainversion = 0;
044    public int localmainversion = 0;
045    public String className = null;
046    public boolean oldmode = false;
047    public String requires = null;
048    public String localrequires = null;
049    public String link = null;
050    public String description = null;
051    public boolean early = false;
052    public String author = null;
053    public int stage = 50;
054    public String version = null;
055    public String localversion = null;
056    public String downloadlink = null;
057    public String iconPath;
058    public ImageIcon icon;
059    public List<URL> libraries = new LinkedList<URL>();
060    public final Map<String, String> attr = new TreeMap<String, String>();
061
062    private static final ImageIcon emptyIcon = new ImageIcon(new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB));
063
064    /**
065     * Creates a plugin information object by reading the plugin information from
066     * the manifest in the plugin jar.
067     *
068     * The plugin name is derived from the file name.
069     *
070     * @param file the plugin jar file
071     * @throws PluginException if reading the manifest fails
072     */
073    public PluginInformation(File file) throws PluginException{
074        this(file, file.getName().substring(0, file.getName().length()-4));
075    }
076
077    /**
078     * Creates a plugin information object for the plugin with name {@code name}.
079     * Information about the plugin is extracted from the manifest file in the plugin jar
080     * {@code file}.
081     * @param file the plugin jar
082     * @param name the plugin name
083     * @throws PluginException thrown if reading the manifest file fails
084     */
085    public PluginInformation(File file, String name) throws PluginException {
086        if (!PluginHandler.isValidJar(file)) {
087            throw new PluginException(name, tr("Invalid jar file ''{0}''", file));
088        }
089        this.name = name;
090        this.file = file;
091        FileInputStream fis = null;
092        JarInputStream jar = null;
093        try {
094            fis = new FileInputStream(file);
095            jar = new JarInputStream(fis);
096            Manifest manifest = jar.getManifest();
097            if (manifest == null)
098                throw new PluginException(name, tr("The plugin file ''{0}'' does not include a Manifest.", file.toString()));
099            scanManifest(manifest, false);
100            libraries.add(0, fileToURL(file));
101        } catch (IOException e) {
102            throw new PluginException(name, e);
103        } finally {
104            Utils.close(jar);
105            Utils.close(fis);
106        }
107    }
108
109    /**
110     * Creates a plugin information object by reading plugin information in Manifest format
111     * from the input stream {@code manifestStream}.
112     *
113     * @param manifestStream the stream to read the manifest from
114     * @param name the plugin name
115     * @param url the download URL for the plugin
116     * @throws PluginException thrown if the plugin information can't be read from the input stream
117     */
118    public PluginInformation(InputStream manifestStream, String name, String url) throws PluginException {
119        this.name = name;
120        try {
121            Manifest manifest = new Manifest();
122            manifest.read(manifestStream);
123            if(url != null) {
124                downloadlink = url;
125            }
126            scanManifest(manifest, url != null);
127        } catch (IOException e) {
128            throw new PluginException(name, e);
129        }
130    }
131
132    /**
133     * Updates the plugin information of this plugin information object with the
134     * plugin information in a plugin information object retrieved from a plugin
135     * update site.
136     *
137     * @param other the plugin information object retrieved from the update
138     * site
139     */
140    public void updateFromPluginSite(PluginInformation other) {
141        this.mainversion = other.mainversion;
142        this.className = other.className;
143        this.requires = other.requires;
144        this.link = other.link;
145        this.description = other.description;
146        this.early = other.early;
147        this.author = other.author;
148        this.stage = other.stage;
149        this.version = other.version;
150        this.downloadlink = other.downloadlink;
151        this.icon = other.icon;
152        this.iconPath = other.iconPath;
153        this.libraries = other.libraries;
154        this.attr.clear();
155        this.attr.putAll(other.attr);
156    }
157
158    /**
159     * Updates the plugin information of this plugin information object with the
160     * plugin information in a plugin information object retrieved from a plugin
161     * jar.
162     *
163     * @param other the plugin information object retrieved from the jar file
164     * @since 5601
165     */
166    public void updateFromJar(PluginInformation other) {
167        updateLocalInfo(other);
168        if (other.icon != null) {
169            this.icon = other.icon;
170        }
171        this.early = other.early;
172        this.className = other.className;
173        this.libraries = other.libraries;
174        this.stage = other.stage;
175    }
176
177    private void scanManifest(Manifest manifest, boolean oldcheck){
178        String lang = LanguageInfo.getLanguageCodeManifest();
179        Attributes attr = manifest.getMainAttributes();
180        className = attr.getValue("Plugin-Class");
181        String s = attr.getValue(lang+"Plugin-Link");
182        if(s == null) {
183            s = attr.getValue("Plugin-Link");
184        }
185        if(s != null) {
186            try {
187                new URL(s);
188            } catch (MalformedURLException e) {
189                Main.info(tr("Invalid URL ''{0}'' in plugin {1}", s, name));
190                s = null;
191            }
192        }
193        link = s;
194        requires = attr.getValue("Plugin-Requires");
195        s = attr.getValue(lang+"Plugin-Description");
196        if(s == null)
197        {
198            s = attr.getValue("Plugin-Description");
199            if(s != null) {
200                try {
201                    s = tr(s);
202                } catch (IllegalArgumentException e) {
203                    Main.info(tr("Invalid plugin description ''{0}'' in plugin {1}", s, name));
204                }
205            }
206        }
207        description = s;
208        early = Boolean.parseBoolean(attr.getValue("Plugin-Early"));
209        String stageStr = attr.getValue("Plugin-Stage");
210        stage = stageStr == null ? 50 : Integer.parseInt(stageStr);
211        version = attr.getValue("Plugin-Version");
212        try {
213            mainversion = Integer.parseInt(attr.getValue("Plugin-Mainversion"));
214        } catch(NumberFormatException e) {
215            Main.warn(e);
216        }
217        author = attr.getValue("Author");
218        iconPath = attr.getValue("Plugin-Icon");
219        if (iconPath != null && file != null) {
220            // extract icon from the plugin jar file
221            icon = new ImageProvider(iconPath).setArchive(file).setMaxWidth(24).setMaxHeight(24).setOptional(true).get();
222        }
223        if(oldcheck && mainversion > Version.getInstance().getVersion())
224        {
225            int myv = Version.getInstance().getVersion();
226            for(Map.Entry<Object, Object> entry : attr.entrySet())
227            {
228                try {
229                    String key = ((Attributes.Name)entry.getKey()).toString();
230                    if(key.endsWith("_Plugin-Url"))
231                    {
232                        int mv = Integer.parseInt(key.substring(0,key.length()-11));
233                        if(mv <= myv && (mv > mainversion || mainversion > myv))
234                        {
235                            String v = (String)entry.getValue();
236                            int i = v.indexOf(';');
237                            if(i > 0)
238                            {
239                                downloadlink = v.substring(i+1);
240                                mainversion = mv;
241                                version = v.substring(0,i);
242                                oldmode = true;
243                            }
244                        }
245                    }
246                }
247                catch(Exception e) { e.printStackTrace(); }
248            }
249        }
250
251        String classPath = attr.getValue(Attributes.Name.CLASS_PATH);
252        if (classPath != null) {
253            for (String entry : classPath.split(" ")) {
254                File entryFile;
255                if (new File(entry).isAbsolute() || file == null) {
256                    entryFile = new File(entry);
257                } else {
258                    entryFile = new File(file.getParent(), entry);
259                }
260
261                libraries.add(fileToURL(entryFile));
262            }
263        }
264        for (Object o : attr.keySet()) {
265            this.attr.put(o.toString(), attr.getValue(o.toString()));
266        }
267    }
268
269    /**
270     * Replies the description as HTML document, including a link to a web page with
271     * more information, provided such a link is available.
272     *
273     * @return the description as HTML document
274     */
275    public String getDescriptionAsHtml() {
276        StringBuilder sb = new StringBuilder();
277        sb.append("<html><body>");
278        sb.append(description == null ? tr("no description available") : description);
279        if (link != null) {
280            sb.append(" <a href=\"").append(link).append("\">").append(tr("More info...")).append("</a>");
281        }
282        if (downloadlink != null && !downloadlink.startsWith("http://svn.openstreetmap.org/applications/editors/josm/dist/")
283        && !downloadlink.startsWith("http://trac.openstreetmap.org/browser/applications/editors/josm/dist/")) {
284            sb.append("<p>&nbsp;</p><p>"+tr("<b>Plugin provided by an external source:</b> {0}", downloadlink)+"</p>");
285        }
286        sb.append("</body></html>");
287        return sb.toString();
288    }
289
290    /**
291     * Load and instantiate the plugin
292     *
293     * @param klass the plugin class
294     * @return the instantiated and initialized plugin
295     */
296    public PluginProxy load(Class<?> klass) throws PluginException{
297        try {
298            Constructor<?> c = klass.getConstructor(PluginInformation.class);
299            Object plugin = c.newInstance(this);
300            return new PluginProxy(plugin, this);
301        } catch(NoSuchMethodException e) {
302            throw new PluginException(name, e);
303        } catch(IllegalAccessException e) {
304            throw new PluginException(name, e);
305        } catch (InstantiationException e) {
306            throw new PluginException(name, e);
307        } catch(InvocationTargetException e) {
308            throw new PluginException(name, e);
309        }
310    }
311
312    /**
313     * Load the class of the plugin
314     *
315     * @param classLoader the class loader to use
316     * @return the loaded class
317     */
318    public Class<?> loadClass(ClassLoader classLoader) throws PluginException {
319        if (className == null)
320            return null;
321        try{
322            Class<?> realClass = Class.forName(className, true, classLoader);
323            return realClass;
324        } catch (ClassNotFoundException e) {
325            throw new PluginException(name, e);
326        } catch (ClassCastException e) {
327            throw new PluginException(name, e);
328        }
329    }
330
331    public static URL fileToURL(File f) {
332        try {
333            return f.toURI().toURL();
334        } catch (MalformedURLException ex) {
335            return null;
336        }
337    }
338
339    /**
340     * Try to find a plugin after some criterias. Extract the plugin-information
341     * from the plugin and return it. The plugin is searched in the following way:
342     *
343     *<li>first look after an MANIFEST.MF in the package org.openstreetmap.josm.plugins.<plugin name>
344     *    (After removing all fancy characters from the plugin name).
345     *    If found, the plugin is loaded using the bootstrap classloader.
346     *<li>If not found, look for a jar file in the user specific plugin directory
347     *    (~/.josm/plugins/<plugin name>.jar)
348     *<li>If not found and the environment variable JOSM_RESOURCES + "/plugins/" exist, look there.
349     *<li>Try for the java property josm.resources + "/plugins/" (set via java -Djosm.plugins.path=...)
350     *<li>If the environment variable ALLUSERSPROFILE and APPDATA exist, look in
351     *    ALLUSERSPROFILE/<the last stuff from APPDATA>/JOSM/plugins.
352     *    (*sic* There is no easy way under Windows to get the All User's application
353     *    directory)
354     *<li>Finally, look in some typical unix paths:<ul>
355     *    <li>/usr/local/share/josm/plugins/
356     *    <li>/usr/local/lib/josm/plugins/
357     *    <li>/usr/share/josm/plugins/
358     *    <li>/usr/lib/josm/plugins/
359     *
360     * If a plugin class or jar file is found earlier in the list but seem not to
361     * be working, an PluginException is thrown rather than continuing the search.
362     * This is so JOSM can detect broken user-provided plugins and do not go silently
363     * ignore them.
364     *
365     * The plugin is not initialized. If the plugin is a .jar file, it is not loaded
366     * (only the manifest is extracted). In the classloader-case, the class is
367     * bootstraped (e.g. static {} - declarations will run. However, nothing else is done.
368     *
369     * @param pluginName The name of the plugin (in all lowercase). E.g. "lang-de"
370     * @return Information about the plugin or <code>null</code>, if the plugin
371     *         was nowhere to be found.
372     * @throws PluginException In case of broken plugins.
373     */
374    public static PluginInformation findPlugin(String pluginName) throws PluginException {
375        String name = pluginName;
376        name = name.replaceAll("[-. ]", "");
377        InputStream manifestStream = PluginInformation.class.getResourceAsStream("/org/openstreetmap/josm/plugins/"+name+"/MANIFEST.MF");
378        if (manifestStream != null)
379            return new PluginInformation(manifestStream, pluginName, null);
380
381        Collection<String> locations = getPluginLocations();
382
383        for (String s : locations) {
384            File pluginFile = new File(s, pluginName + ".jar");
385            if (pluginFile.exists()) {
386                PluginInformation info = new PluginInformation(pluginFile);
387                return info;
388            }
389        }
390        return null;
391    }
392
393    public static Collection<String> getPluginLocations() {
394        Collection<String> locations = Main.pref.getAllPossiblePreferenceDirs();
395        Collection<String> all = new ArrayList<String>(locations.size());
396        for (String s : locations) {
397            all.add(s+"plugins");
398        }
399        return all;
400    }
401
402    /**
403     * Replies true if the plugin with the given information is most likely outdated with
404     * respect to the referenceVersion.
405     *
406     * @param referenceVersion the reference version. Can be null if we don't know a
407     * reference version
408     *
409     * @return true, if the plugin needs to be updated; false, otherweise
410     */
411    public boolean isUpdateRequired(String referenceVersion) {
412        if (this.downloadlink == null) return false;
413        if (this.version == null && referenceVersion!= null)
414            return true;
415        if (this.version != null && !this.version.equals(referenceVersion))
416            return true;
417        return false;
418    }
419
420    /**
421     * Replies true if this this plugin should be updated/downloaded because either
422     * it is not available locally (its local version is null) or its local version is
423     * older than the available version on the server.
424     *
425     * @return true if the plugin should be updated
426     */
427    public boolean isUpdateRequired() {
428        if (this.downloadlink == null) return false;
429        if (this.localversion == null) return true;
430        return isUpdateRequired(this.localversion);
431    }
432
433    protected boolean matches(String filter, String value) {
434        if (filter == null) return true;
435        if (value == null) return false;
436        return value.toLowerCase().contains(filter.toLowerCase());
437    }
438
439    /**
440     * Replies true if either the name, the description, or the version match (case insensitive)
441     * one of the words in filter. Replies true if filter is null.
442     *
443     * @param filter the filter expression
444     * @return true if this plugin info matches with the filter
445     */
446    public boolean matches(String filter) {
447        if (filter == null) return true;
448        String[] words = filter.split("\\s+");
449        for (String word: words) {
450            if (matches(word, name)
451                    || matches(word, description)
452                    || matches(word, version)
453                    || matches(word, localversion))
454                return true;
455        }
456        return false;
457    }
458
459    /**
460     * Replies the name of the plugin
461     */
462    public String getName() {
463        return name;
464    }
465
466    /**
467     * Sets the name
468     * @param name
469     */
470    public void setName(String name) {
471        this.name = name;
472    }
473
474    /**
475     * Replies the plugin icon, scaled to 24x24 pixels.
476     * @return the plugin icon, scaled to 24x24 pixels.
477     */
478    public ImageIcon getScaledIcon() {
479        if (icon == null)
480            return emptyIcon;
481        return new ImageIcon(icon.getImage().getScaledInstance(24, 24, Image.SCALE_SMOOTH));
482    }
483
484    @Override
485    public String toString() {
486        return getName();
487    }
488
489    private static List<String> getRequiredPlugins(String pluginList) {
490        List<String> requiredPlugins = new ArrayList<String>();
491        if (pluginList != null) {
492            for (String s : pluginList.split(";")) {
493                String plugin = s.trim();
494                if (!plugin.isEmpty()) {
495                    requiredPlugins.add(plugin);
496                }
497            }
498        }
499        return requiredPlugins;
500    }
501
502    /**
503     * Replies the list of plugins required by the up-to-date version of this plugin.
504     * @return List of plugins required. Empty if no plugin is required.
505     * @since 5601
506     */
507    public List<String> getRequiredPlugins() {
508        return getRequiredPlugins(requires);
509    }
510
511    /**
512     * Replies the list of plugins required by the local instance of this plugin.
513     * @return List of plugins required. Empty if no plugin is required.
514     * @since 5601
515     */
516    public List<String> getLocalRequiredPlugins() {
517        return getRequiredPlugins(localrequires);
518    }
519
520    /**
521     * Updates the local fields ({@link #localversion}, {@link #localmainversion}, {@link #localrequires})
522     * to values contained in the up-to-date fields ({@link #version}, {@link #mainversion}, {@link #requires})
523     * of the given PluginInformation.
524     * @param info The plugin information to get the data from.
525     * @since 5601
526     */
527    public void updateLocalInfo(PluginInformation info) {
528        if (info != null) {
529            this.localversion = info.version;
530            this.localmainversion = info.mainversion;
531            this.localrequires = info.requires;
532        }
533    }
534}