001    /*
002     * This file is part of muCommander, http://www.mucommander.com
003     * Copyright (C) 2002-2008 Maxence Bernard
004     *
005     * muCommander is free software; you can redistribute it and/or modify
006     * it under the terms of the GNU General Public License as published by
007     * the Free Software Foundation; either version 3 of the License, or
008     * (at your option) any later version.
009     *
010     * muCommander is distributed in the hope that it will be useful,
011     * but WITHOUT ANY WARRANTY; without even the implied warranty of
012     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
013     * GNU General Public License for more details.
014     *
015     * You should have received a copy of the GNU General Public License
016     * along with this program.  If not, see <http://www.gnu.org/licenses/>.
017     */
018    
019    package com.mucommander.extension;
020    
021    import com.mucommander.PlatformManager;
022    import com.mucommander.file.AbstractFile;
023    import com.mucommander.file.AbstractFileClassLoader;
024    import com.mucommander.file.FileFactory;
025    import com.mucommander.file.filter.ExtensionFilenameFilter;
026    
027    import java.io.File;
028    import java.io.IOException;
029    import java.util.StringTokenizer;
030    
031    /**
032     * Manages muCommander's extensions.
033     * <p>
034     * Extensions must be stored in {@link #getExtensionsFolder()} in order for this class to be aware of them.
035     * Moreover, the method {@link #addExtensionsToClasspath()} must have been called before extensions can be used.
036     * </p>
037     * <p>
038     * Extensions are loaded through a custom <code>ClassLoader</code>. The optimal situation is for that <code>ClassLoader</code>
039     * to be the system one, which can only be achieved through setting the <code>java.system.class.loader</code> system property
040     * to <code>com.mucommander.file.AbstractFileClassLoader</code> at boot time.<br>
041     * However, if for some reason such is not the case, we'll use a separate instance of that class. This will work in most cases, but
042     * might cause conflicts under rare circumstances. Extension writers are advised to load resources through the <code>ClassLoader</code>
043     * returned by {@link #getClassLoader()}, as not doing so might result in using the bootstrap classloader which doesn't have access to
044     * resources found in {@link #getExtensionsFolder()}.
045     * </p>
046     * <p>
047     * This class can also be used to load Swing look and feel from JAR files that aren't in the system's classpath. In order to achieve this,
048     * application writers must:
049     * <ul>
050     *   <li>
051     *     Call <code>UIManager.getDefaults().put("ClassLoader", ExtensionManager.getClassLoader());</code> when initialising their application.
052     *     This will force Swing to use our custom classloader when loading Look&Feels.
053     *   </li>
054     *   <li>
055     *     Call <code>UIManager.setLookAndFeel((LookAndFeel)Class.forName(lnfName, true, ExtensionManager.getClassLoader()).newInstance());</code>
056     *     to set a new look and feel. This will ensure that all classes and resources are available when initialising the Look&Feel.
057     *   </li>
058     * </ul>
059     * Unfortunately, this is not always sufficient. Some Look&Feels suffer from a peculiar behaviour in Swing that might cause resources to be loaded
060     * through the system class loader rather than the one specified at initialisation time. This happens with Look&Feels that extend system ones, such
061     * as <code>Quaqua</code>. The only way to get these to load properly is to make sure the system classloader is an instance of
062     * {@link com.mucommander.file.AbstractFileClassLoader}.
063     * </p>
064     * @author Nicolas Rinaudo
065     */
066    public class ExtensionManager {
067        // - Class fields -----------------------------------------------------------
068        // --------------------------------------------------------------------------
069        /** ClassLoader used to load all extensions. */
070        private static AbstractFileClassLoader loader;
071    
072    
073    
074        // - Extensions folder ------------------------------------------------------
075        // --------------------------------------------------------------------------
076        /** Path to the extensions folder. */
077        private static       AbstractFile extensionsFolder;
078        /** Default name of the extensions folder. */
079        public  static final String       DEFAULT_EXTENSIONS_FOLDER_NAME = "extensions";
080    
081    
082    
083        // - Initialisation ---------------------------------------------------------
084        // --------------------------------------------------------------------------
085        static {
086            ClassLoader temp;
087    
088            // Initialises the extension class loader.
089            // If the system classloader is an instance of AbstractFileClassLoader, use it.
090            if((temp = ClassLoader.getSystemClassLoader()) instanceof AbstractFileClassLoader)
091                loader = (AbstractFileClassLoader)temp;
092    
093            // Otherwise, use a new instance of AbstractFileClassLoader.
094            else
095                loader = new AbstractFileClassLoader();
096        }
097    
098        /**
099         * Prevents instanciations of this class.
100         */
101        private ExtensionManager() {}
102    
103    
104    
105        // - Extension folder access ------------------------------------------------
106        // --------------------------------------------------------------------------
107        /**
108         * Sets the path to the folder in which all extensions are stored.
109         * <p>
110         * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent
111         * will be used instead.
112         * </p>
113         * @param  folder      path to the folder in which extensions are stored.
114         * @throws IOException if the specified folder or the specified file's parent couldn't be accessed.
115         * @see                #setExtensionsFolder(AbstractFile)
116         * @see                #setExtensionsFolder(String)
117         * @see                #getExtensionsFolder()
118         */
119        public static void setExtensionsFolder(File folder) throws IOException {setExtensionsFolder(FileFactory.getFile(folder.getAbsolutePath()));}
120    
121        /**
122         * Sets the path to the folder in which all extensions are stored.
123         * <p>
124         * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent
125         * will be used instead.
126         * </p>
127         * @param  folder      path to the folder in which extensions are stored.
128         * @throws IOException if the specified folder or the specified file's parent couldn't be accessed.
129         * @see                #setExtensionsFolder(File)
130         * @see                #setExtensionsFolder(String)
131         * @see                #getExtensionsFolder()
132         */
133        public static void setExtensionsFolder(AbstractFile folder) throws IOException {
134            // If the folder doesn't exist, create it.
135            if(!folder.exists())
136                folder.mkdir();
137    
138            // If it's not a browsable file, use its parent.
139            else if(!folder.isBrowsable())
140                folder = folder.getParent();
141    
142            extensionsFolder = folder;
143        }
144    
145        /**
146         * Sets the path to the folder in which all extensions are stored.
147         * <p>
148         * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent
149         * will be used instead.
150         * </p>
151         * @param  path        path to the folder in which extensions are stored.
152         * @throws IOException if the specified folder or the specified file's parent couldn't be accessed.
153         * @see                #setExtensionsFolder(File)
154         * @see                #setExtensionsFolder(String)
155         * @see                #getExtensionsFolder()
156         */
157        public static void setExtensionsFolder(String path) throws IOException {
158            AbstractFile folder;
159    
160            if((folder = FileFactory.getFile(path)) == null)
161                setExtensionsFolder(new File(path));
162            else
163                setExtensionsFolder(folder);
164        }
165    
166        /**
167         * Returns the path to the default extensions folder.
168         * <p>
169         * The default path is:
170         * <pre>
171         * {@link PlatformManager#getPreferencesFolder()}.{@link AbstractFile#getChild(String) getChild}({@link #DEFAULT_EXTENSIONS_FOLDER_NAME});
172         * </pre>
173         * </p>
174         * @return             the path to the default extensions folder.
175         * @throws IOException if there was an error retrieving the default extensions folder.
176         */
177        private static AbstractFile getDefaultExtensionsFolder() throws IOException {
178            AbstractFile folder;
179    
180            folder = PlatformManager.getPreferencesFolder().getChild(DEFAULT_EXTENSIONS_FOLDER_NAME);
181    
182            // Makes sure the folder exists.
183            if(!folder.exists())
184                folder.mkdir();
185    
186            return folder;
187        }
188    
189        /**
190         * Returns the folder in which all extensions are stored.
191         * @return             the folder in which all extensions are stored.
192         * @throws IOException if an error occured while locating the default extensions folder.
193         * @see                #setExtensionsFolder(AbstractFile)
194         */
195        public static AbstractFile getExtensionsFolder() throws IOException {
196            // If the extensions folder has been set, use it.
197            if(extensionsFolder != null)
198                return extensionsFolder;
199    
200            return getDefaultExtensionsFolder();
201        }
202    
203        /**
204         * Returns an <code>AbstractFile</code> to the extension file with the specified filename and located in the
205         * {@link #getExtensionsFolder() extensions folder}. The returned file may or may not exist.
206         * @param  filename    the extension's filename 
207         * @return             an AbstractFile to the extension file with the specified filename and located in the
208         * extensions folder.
209         * @throws IOException if the file could not be instanciated.
210         */
211        public static AbstractFile getExtensionsFile(String filename) throws IOException {
212            return getExtensionsFolder().getDirectChild(filename);
213        }
214    
215    
216        // - Classpath querying -----------------------------------------------------
217        // --------------------------------------------------------------------------
218        /**
219         * Returns <code>true</code> if the specified file is in the extension's classloader path.
220         * @param  file file whose presence in the extensions path will be checked.
221         * @return      <code>true</code> if the specified file is in the extension's classloader path, <code>false</code> otherwise.
222         */
223        public static boolean isInExtensionsPath(AbstractFile file) {return loader.contains(file);}
224    
225        /**
226         * Returns <code>true</code> if the specified file is in the system classpath.
227         * @param  file file whose presence in the system classpath will be checked.
228         * @return      <code>true</code> if the specified file is in the system classpath, <code>false</code> otherwise.
229         */
230        public static boolean isInClasspath(AbstractFile file) {
231            StringTokenizer parser;
232            String          path;
233    
234            path   = file.getAbsolutePath();
235            parser = new StringTokenizer(System.getProperty("java.class.path"), System.getProperty("path.separator"));
236            while(parser.hasMoreTokens())
237                if(parser.nextToken().equals(path))
238                    return true;
239            return false;
240        }
241    
242        /**
243         * Returns <code>true</code> if the specified file is either in the extension or system classpath.
244         * <p>
245         * This is a convenience method and is equivalent to calling:
246         * <code>{@link #isInClasspath(AbstractFile) isInClasspath}(file) || {@link #isInExtensionsPath(AbstractFile) isInExtensionsPath}(file)</code>.
247         * </p>
248         * @param file file whose availability will be checked.
249         * @return <code>true</code> if the specified file is either in the extension or system classpath, <code>false</code> otherwise.
250         */
251        public static boolean isAvailable(AbstractFile file) {return isInClasspath(file) || isInExtensionsPath(file);}
252    
253    
254    
255        // - Classpath extension ----------------------------------------------------
256        // --------------------------------------------------------------------------
257        /**
258         * Imports the specified file in muCommander's libraries.
259         * @param file  path to the library to import.
260         * @param  force       wether to overwrite eventual existing libraries of the same name.
261         * @return             <code>true</code> if the operation was a success,
262         *                     <code>false</code> if a library of the same name already exists and
263         *                     <code>force</code> is set to <code>false</code>.
264         * @throws IOException if an I/O error occurs.
265         */
266        public static boolean importLibrary(AbstractFile file, boolean force) throws IOException {
267            AbstractFile dest;
268    
269            // If the file is already in the extensions or classpath,
270            // there's nothing to do.
271            if(isAvailable(file))
272                return true;
273    
274            // If the destination file already exists, either delete it
275            // if force is set to true or just return false.
276            dest = getExtensionsFile(file.getName());
277            if(dest.exists()) {
278                if(!force)
279                    return false;
280                dest.delete();
281            }
282    
283            // Copies the library and adds it to the extensions classpath.
284            file.copyTo(dest);
285            addToClassPath(dest);
286            return true;
287        }
288    
289        /**
290         * Adds the specified file to the extension's classpath.
291         * @param file file to add to the classpath.
292         */
293        public static void addToClassPath(AbstractFile file) {loader.addFile(file);}
294    
295        /**
296         * Adds all known extensions to the current classpath.
297         * <p>
298         * This method will create the following new classpath entries:
299         * <ul>
300         *   <li>{@link #getExtensionsFolder()}</li>.
301         *   <li>All <code>JAR</code> files in {@link #getExtensionsFolder()}.</li>
302         * </ul>
303         * </p>
304         * @throws IOException if the extensions folder is not accessible.
305         */
306        public static void addExtensionsToClasspath() throws IOException {
307            AbstractFile[] files;
308    
309            // Adds the extensions folder to the classpath.
310            addToClassPath(getExtensionsFolder());
311    
312            // Adds all JAR files contained by the extensions folder to the classpath.
313            files = getExtensionsFolder().ls(new ExtensionFilenameFilter(".jar"));
314            for(int i = 0; i < files.length; i++)
315                addToClassPath(files[i]);
316        }
317    
318        /**
319         * Returns the <code>ClassLoader</code> used to load all extensions.
320         * @return the <code>ClassLoader</code> used to load all extensions.
321         */
322        public static ClassLoader getClassLoader() {return loader;}
323    }