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 }