001    // License: GPL. Copyright 2007 by Immanuel Scholz and others
002    package org.openstreetmap.josm.plugins;
003    
004    import static org.openstreetmap.josm.tools.I18n.tr;
005    
006    import java.awt.Image;
007    import java.awt.image.BufferedImage;
008    import java.io.File;
009    import java.io.FileInputStream;
010    import java.io.IOException;
011    import java.io.InputStream;
012    import java.lang.reflect.Constructor;
013    import java.lang.reflect.InvocationTargetException;
014    import java.net.MalformedURLException;
015    import java.net.URL;
016    import java.util.ArrayList;
017    import java.util.Collection;
018    import java.util.LinkedList;
019    import java.util.List;
020    import java.util.Map;
021    import java.util.TreeMap;
022    import java.util.jar.Attributes;
023    import java.util.jar.JarInputStream;
024    import java.util.jar.Manifest;
025    import javax.swing.ImageIcon;
026    
027    import org.openstreetmap.josm.Main;
028    import org.openstreetmap.josm.data.Version;
029    import org.openstreetmap.josm.tools.ImageProvider;
030    import org.openstreetmap.josm.tools.LanguageInfo;
031    
032    /**
033     * Encapsulate general information about a plugin. This information is available
034     * without the need of loading any class from the plugin jar file.
035     *
036     * @author imi
037     */
038    public class PluginInformation {
039        public File file = null;
040        public String name = null;
041        public int mainversion = 0;
042        public int localmainversion = 0;
043        public String className = null;
044        public boolean oldmode = false;
045        public String requires = null;
046        public String link = null;
047        public String description = null;
048        public boolean early = false;
049        public String author = null;
050        public int stage = 50;
051        public String version = null;
052        public String localversion = null;
053        public String downloadlink = null;
054        public String iconPath;
055        public ImageIcon icon;
056        public List<URL> libraries = new LinkedList<URL>();
057        public final Map<String, String> attr = new TreeMap<String, String>();
058        
059        private static final ImageIcon emptyIcon = new ImageIcon(new BufferedImage(24, 24, BufferedImage.TYPE_INT_ARGB));
060    
061        /**
062         * Creates a plugin information object by reading the plugin information from
063         * the manifest in the plugin jar.
064         *
065         * The plugin name is derived from the file name.
066         *
067         * @param file the plugin jar file
068         * @throws PluginException if reading the manifest fails
069         */
070        public PluginInformation(File file) throws PluginException{
071            this(file, file.getName().substring(0, file.getName().length()-4));
072        }
073    
074        /**
075         * Creates a plugin information object for the plugin with name {@code name}.
076         * Information about the plugin is extracted from the maifest file in the plugin jar
077         * {@code file}.
078         * @param file the plugin jar
079         * @param name the plugin name
080         * @throws PluginException thrown if reading the manifest file fails
081         */
082        public PluginInformation(File file, String name) throws PluginException{
083            this.name = name;
084            this.file = file;
085            FileInputStream fis = null;
086            JarInputStream jar = null;
087            try {
088                fis = new FileInputStream(file);
089                jar = new JarInputStream(fis);
090                Manifest manifest = jar.getManifest();
091                if (manifest == null)
092                    throw new PluginException(name, tr("The plugin file ''{0}'' does not include a Manifest.", file.toString()));
093                scanManifest(manifest, false);
094                libraries.add(0, fileToURL(file));
095            } catch (IOException e) {
096                throw new PluginException(name, e);
097            } finally {
098                if (jar != null) {
099                    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    }