001    /*
002     Copyright (C) 2003 Adam Olsen
003    
004     This program is free software; you can redistribute it and/or modify
005     it under the terms of the GNU General Public License as published by
006     the Free Software Foundation; either version 1, or (at your option)
007     any later version.
008    
009     This program is distributed in the hope that it will be useful,
010     but WITHOUT ANY WARRANTY; without even the implied warranty of
011     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
012     GNU General Public License for more details.
013    
014     You should have received a copy of the GNU General Public License
015     along with this program; if not, write to the Free Software
016     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
017     */
018    
019    package com.valhalla.pluginmanager;
020    
021    import java.io.File;
022    import java.io.FileOutputStream;
023    import java.io.InputStream;
024    import java.util.ArrayList;
025    import java.util.Hashtable;
026    import java.util.Properties;
027    import java.util.jar.JarEntry;
028    
029    import com.valhalla.settings.Settings;
030    
031    /**
032     * This class supports the loading of custom plugins from jar files. The main
033     * class in the Jar should be named after the jar filename and should be in the
034     * "package" String passed to the PluginLoader package. For example, the plugin
035     * LookAndFeel would be in a jar file called LookAndFeel.jar, and if the package
036     * parameter was com.valhalla.jbother.plugins there would need to be a class
037     * called <code>com.valhalla.jbother.plugins.LookAndFeel</code> in the jar
038     * file
039     *
040     * @author Adam Olsen
041     * @version 1.0
042     */
043    public class PluginLoader extends ClassLoader {
044        private static PluginLoader instance = null;
045    
046        private static int pluginAPIVersion = 91214;
047    
048        private String pluginDir = null;
049    
050        private Hashtable loadedClasses = new Hashtable(); // contains information
051                                                           // about what classes
052                                                           // have
053    
054        // already been loaded
055        private static ArrayList availablePlugins = new ArrayList(); // the list of
056                                                                     // available
057                                                                     // plugins
058    
059        private static ArrayList invalidPlugins = new ArrayList();
060    
061        private static ArrayList incompatiblePlugins = new ArrayList();
062    
063        private static ArrayList installedPlugins = new ArrayList();
064    
065        private static Hashtable loadedPlugins = new Hashtable();
066    
067        /**
068         * The default constructor
069         *
070         * @param pluginDir
071         *            the directory to search for plugins in .jar format
072         */
073        private PluginLoader() {
074        }
075    
076        /**
077         * Gets the singleton of this class
078         *
079         * @return the PluginLoader singleton
080         */
081        public static PluginLoader getInstance() {
082            if (instance == null)
083                instance = new PluginLoader();
084            return instance;
085        }
086    
087        public static PluginLoader getNewInstance() {
088            instance = new PluginLoader();
089            return instance;
090        }
091    
092        /**
093         * Finds which plugin contains the specified native library, extracts it to
094         * a temporary location, and returns the path to that location or null if
095         * the native library could not be found.
096         *
097         * @param the
098         *            name of the library
099         * @return the location of the newly created temporary file
100         */
101        public String findLibrary(String lib) {
102            String l = super.findLibrary(lib);
103            if (l != null)
104                return l;
105            PluginJAR jar = getPluginThatContains("native/" + lib);
106            if (jar == null)
107                return null;
108    
109            try {
110                JarEntry entry = jar.getJarEntry("native/" + lib);
111    
112                int index = lib.lastIndexOf('.');
113                String suffix = lib.substring(index, lib.length());
114                lib = lib.substring(0, index);
115                File temporaryDll = File.createTempFile(lib, suffix);
116    
117                InputStream inputStream = jar.getInputStream(entry);
118    
119                FileOutputStream outputStream = new FileOutputStream(temporaryDll);
120                byte[] array = new byte[8192];
121                for (int i = inputStream.read(array); i != -1; i = inputStream
122                        .read(array)) {
123                    outputStream.write(array, 0, i);
124                }
125    
126                outputStream.close();
127                temporaryDll.deleteOnExit();
128                com.valhalla.Logger.debug(temporaryDll.getPath());
129                return temporaryDll.getPath();
130            } catch (Exception ex) {
131                com.valhalla.Logger.logException(ex);
132                return null;
133            }
134        }
135    
136        /**
137         * @return a list of currently loaded plugins
138         */
139        public Hashtable getLoadedPlugins() {
140            return loadedPlugins;
141        }
142    
143        /**
144         * @return a list of available plugins
145         */
146        public ArrayList getAvailablePlugins() {
147            return availablePlugins;
148        }
149    
150        /**
151         * @return a list of installed plugins
152         */
153        public ArrayList getInstalledPlugins() {
154            return installedPlugins;
155        }
156    
157        /**
158         * @return a list of invalid plugins
159         */
160        public ArrayList getInvalidPlugins() {
161            return invalidPlugins;
162        }
163    
164        /**
165         * @return a list of incompatible plugins
166         */
167        public ArrayList getIncompatiblePlugins() {
168            return incompatiblePlugins;
169        }
170    
171        /**
172         * Returns the jar file for the specified plugin name
173         *
174         * @param name
175         *            the plugin name
176         * @return the jar file corresponding to the name
177         */
178        public PluginJAR getPlugin(String name) {
179            PluginJAR jar = null;
180            for (int i = 0; i < availablePlugins.size(); i++) {
181                PluginJAR temp = (PluginJAR) availablePlugins.get(i);
182                if (temp.getName().equals(name))
183                    jar = temp;
184            }
185    
186            return jar;
187        }
188    
189        /**
190         * Returns the PluginJAR that contains <tt>file</tt>
191         *
192         * @param file
193         *            the file to search for
194         * @return the PluginJAR that contains <tt>file</tt>
195         */
196        private PluginJAR getPluginThatContains(String file) {
197            PluginJAR jar = null;
198            for (int i = 0; i < availablePlugins.size(); i++) {
199                PluginJAR temp = (PluginJAR) availablePlugins.get(i);
200                if (temp.contains(file))
201                    jar = temp;
202            }
203    
204            return jar;
205        }
206    
207        /**
208         * Returns the plugin that is represented by location
209         *
210         * @param location
211         *            the location of the plugin
212         * @return the PluginJAR representing the specified location
213         */
214        private PluginJAR getPluginFromLocation(String location) {
215            PluginJAR jar = null;
216    
217            for (int i = 0; i < availablePlugins.size(); i++) {
218                PluginJAR temp = (PluginJAR) availablePlugins.get(i);
219                if (temp.getLocation().equals(location))
220                    jar = temp;
221            }
222    
223            return jar;
224        }
225    
226        /**
227         * Gets the current plugin API version
228         *
229         * @return the current plugin API version
230         */
231        public static int getAPIVersion() {
232            return pluginAPIVersion;
233        }
234    
235        /**
236         * Attempts to load the available plugins
237         */
238        public void loadPlugins() {
239            for (int i = 0; i < availablePlugins.size(); i++) {
240                PluginJAR jar = (PluginJAR) availablePlugins.get(i);
241    
242                if (!jar.getLoaded()) {
243                    try {
244                        jar.loadPlugin();
245                    } catch (Exception e) {
246                        com.valhalla.Logger.logException(e);
247                    }
248                } else {
249                    com.valhalla.Logger.debug("Plugin is already loaded.");
250                }
251            }
252        }
253    
254        /**
255         * Reads in all the available plugins and the information about them
256         */
257        public void findPlugins(String d) {
258            this.pluginDir = d;
259            String files[] = null;
260            File pluginDir = null;
261    
262            installedPlugins.clear();
263            ArrayList newAvailable = new ArrayList();
264            invalidPlugins.clear();
265            incompatiblePlugins.clear();
266    
267            try {
268                // open up the plugin directory
269                pluginDir = new File(this.pluginDir);
270                if (!pluginDir.isDirectory())
271                    return;
272                files = pluginDir.list();
273            } catch (Exception exception) {
274                return;
275            }
276    
277            boolean ignoreIncompatible =
278                Settings.getInstance().getBoolean("ignoreIncompatiblePlugins");
279    
280            for (int i = 0; i < files.length; i++) {
281                // if it's a jar file, it might be a plugin
282                if (files[i].endsWith(".jar")) {
283                    try {
284                        PluginJAR jar = null;
285                        String loc = pluginDir.getPath() + File.separatorChar
286                                + files[i];
287                        jar = getPluginFromLocation(loc);
288    
289                        if (jar == null || !jar.getLoaded()) {
290                            jar = new PluginJAR(pluginDir.getPath()
291                                    + File.separatorChar + files[i]);
292                        }
293    
294                        Properties props = jar.getProperties();
295                        String name = props.getProperty("name");
296    
297                        if (props == null || props.getProperty("mainClass") == null
298                                || name == null)
299                            invalidPlugins.add(files[i]);
300                        else if (ignoreIncompatible && !checkPlatform (props)) {
301                            incompatiblePlugins.add(name);
302                            com.valhalla.Logger.debug("Incompatible plugin found: " +
303                                                      name + " (" + files[i] + ")");
304                        } else {
305                            int version = Integer.parseInt(props
306                                    .getProperty("APIVersion"));
307                            if (version != pluginAPIVersion) {
308                                invalidPlugins.add(name);
309                            } else {
310                                newAvailable.add(jar);
311                            }
312    
313                            installedPlugins.add(props);
314                        }
315                    } catch (Exception ex) {
316                        com.valhalla.Logger.debug("Invalid plugin found: "
317                                + files[i]);
318                    }
319                }
320            }
321    
322            availablePlugins = newAvailable;
323        }
324    
325        /**
326         * Gets a resource as a stream
327         *
328         * @param resource
329         *            the resource to get
330         * @return the stream
331         */
332        public InputStream getResourceAsStream(String resource) {
333            InputStream stream = null;
334            stream = this.getClass().getClassLoader().getResourceAsStream(resource);
335            if (stream != null)
336                return stream;
337    
338            PluginJAR jar = getPluginThatContains(resource);
339    
340            if (jar != null) {
341                // find the entry in the Jar file
342                JarEntry entry = jar.getJarEntry(resource);
343    
344                stream = jar.getInputStream(entry);
345            } else {
346                com.valhalla.Logger.debug("Stream was null for " + resource);
347            }
348    
349            return stream;
350        }
351    
352        protected Hashtable getLoadedClasses() {
353            return loadedClasses;
354        }
355    
356        /**
357         * Loads the Class out of the plugin file
358         *
359         * @param className
360         *            the class name to load
361         * @param resolveIt
362         *            true to resolve the class (load dependancies)
363         * @return the Class if it was found
364         * @throws <code>ClassNotFoundException</code> if the class could not be
365         *             found in the system or any of the plugin files
366         */
367        public synchronized Class loadClass(String className, boolean resolveIt)
368                throws ClassNotFoundException {
369            Class result = null;
370    
371            String origName = className;
372    
373            // if the Class was already loaded, return it
374            result = (Class) loadedClasses.get(origName);
375            if (result != null)
376                return result;
377    
378            // check to see if it's a system class
379            try {
380                result = this.getClass().getClassLoader().loadClass(className);
381                //result = super.findSystemClass( className );
382    
383                return result;
384            } catch (ClassNotFoundException exception) {
385            }
386    
387            className = className.replaceAll("\\.", "/");
388            className += ".class";
389    
390            // if the class name starts with "java", we will not load it
391            // because of the security risks involved. A class in the package
392            // java. or javax. will be able to access protected members of system
393            // Classes, and this isn't good.
394            
395    /*        *REMOVED* as official JBother plugins are checked by devel team 
396     *        anyways...
397     *        if (className.startsWith("java"))
398                throw new ClassNotFoundException();
399    */
400            PluginJAR jar = getPluginThatContains(className);
401    
402            if (jar == null)
403                throw new ClassNotFoundException();
404    
405            try {
406                // find the entry in the Jar file
407                JarEntry entry = jar.getJarEntry(className);
408                InputStream stream = jar.getInputStream(entry);
409    
410                // read it into a class
411                int len = (int) entry.getSize();
412                int offset = 0;
413                int success = 0;
414                byte[] contents = new byte[len];
415    
416                while (success < len) {
417                    len -= success;
418                    offset += success;
419                    success = stream.read(contents, offset, len);
420                    if (success == -1)
421                        throw new ClassNotFoundException();
422                }
423    
424                stream.read(contents, 0, (int) entry.getSize());
425    
426                // actually define the class
427                result = defineClass(origName, contents, 0, contents.length);
428    
429                if (resolveIt)
430                    resolveClass(result);
431    
432                // mark this class as already loaded and put it in the cache
433                loadedClasses.put(origName, result);
434    
435                return result;
436            } catch (Exception e) {
437                throw new ClassNotFoundException(className);
438            }
439        }
440    
441        /**
442         *  Checks whether the current platform (OS name and architecture) is
443         *  compatible with the given plugin's platform or not.
444         *
445         *  @param pluginProps  plugin properties
446         *  @return true if the plugin is compatible and false otherwise
447         */
448        static boolean checkPlatform (Properties pluginProps)
449        {
450            // @todo it may be worth to introduce 'os.match' and 'arch.match'
451            // plugin properties to perform more flexible regexp match
452    
453            String os = pluginProps.getProperty("os");
454            String arch = pluginProps.getProperty("arch");
455    
456            boolean ok = os == null || os.equals("all") ||
457                         System.getProperty("os.name").startsWith(os);
458    
459            if (ok) {
460                ok = arch == null || arch.equals("all") ||
461                System.getProperty("os.arch").equals(arch);
462            }
463    
464            return ok;
465        }
466    }