/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.extension; import com.mucommander.PlatformManager; import com.mucommander.commons.file.AbstractFile; import com.mucommander.commons.file.AbstractFileClassLoader; import com.mucommander.commons.file.FileFactory; import com.mucommander.commons.file.filter.ExtensionFilenameFilter; import java.io.File; import java.io.IOException; import java.util.StringTokenizer; /** * Manages muCommander's extensions. * <p> * Extensions must be stored in {@link #getExtensionsFolder()} in order for this class to be aware of them. * Moreover, the method {@link #addExtensionsToClasspath()} must have been called before extensions can be used. * </p> * <p> * Extensions are loaded through a custom <code>ClassLoader</code>. The optimal situation is for that <code>ClassLoader</code> * to be the system one, which can only be achieved through setting the <code>java.system.class.loader</code> system property * to <code>com.mucommander.commons.file.AbstractFileClassLoader</code> at boot time.<br> * 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 * might cause conflicts under rare circumstances. Extension writers are advised to load resources through the <code>ClassLoader</code> * returned by {@link #getClassLoader()}, as not doing so might result in using the bootstrap classloader which doesn't have access to * resources found in {@link #getExtensionsFolder()}. * </p> * <p> * 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, * application writers must: * <ul> * <li> * Call <code>UIManager.getDefaults().put("ClassLoader", ExtensionManager.getClassLoader());</code> when initialising their application. * This will force Swing to use our custom classloader when loading Look&Feels. * </li> * <li> * Call <code>UIManager.setLookAndFeel((LookAndFeel)Class.forName(lnfName, true, ExtensionManager.getClassLoader()).newInstance());</code> * to set a new look and feel. This will ensure that all classes and resources are available when initialising the Look&Feel. * </li> * </ul> * Unfortunately, this is not always sufficient. Some Look&Feels suffer from a peculiar behaviour in Swing that might cause resources to be loaded * through the system class loader rather than the one specified at initialisation time. This happens with Look&Feels that extend system ones, such * as <code>Quaqua</code>. The only way to get these to load properly is to make sure the system classloader is an instance of * {@link com.mucommander.commons.file.AbstractFileClassLoader}. * </p> * @author Nicolas Rinaudo */ public class ExtensionManager { // - Class fields ----------------------------------------------------------- // -------------------------------------------------------------------------- /** ClassLoader used to load all extensions. */ private static AbstractFileClassLoader loader; // - Extensions folder ------------------------------------------------------ // -------------------------------------------------------------------------- /** Path to the extensions folder. */ private static AbstractFile extensionsFolder; /** Default name of the extensions folder. */ public static final String DEFAULT_EXTENSIONS_FOLDER_NAME = "extensions"; // - Initialisation --------------------------------------------------------- // -------------------------------------------------------------------------- static { ClassLoader temp; // Initialises the extension class loader. // If the system classloader is an instance of AbstractFileClassLoader, use it. if((temp = ClassLoader.getSystemClassLoader()) instanceof AbstractFileClassLoader) loader = (AbstractFileClassLoader)temp; // Otherwise, use a new instance of AbstractFileClassLoader. else loader = new AbstractFileClassLoader(); } /** * Prevents instanciations of this class. */ private ExtensionManager() {} // - Extension folder access ------------------------------------------------ // -------------------------------------------------------------------------- /** * Sets the path to the folder in which all extensions are stored. * <p> * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * </p> * @param folder path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(AbstractFile) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ public static void setExtensionsFolder(File folder) throws IOException {setExtensionsFolder(FileFactory.getFile(folder.getAbsolutePath()));} /** * Sets the path to the folder in which all extensions are stored. * <p> * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * </p> * @param folder path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(File) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ public static void setExtensionsFolder(AbstractFile folder) throws IOException { // If the folder doesn't exist, create it. if(!folder.exists()) folder.mkdir(); // If it's not a browsable file, use its parent. else if(!folder.isBrowsable()) folder = folder.getParent(); extensionsFolder = folder; } /** * Sets the path to the folder in which all extensions are stored. * <p> * If the specified path is not browsable (i.e. a folder or any file that muCommander can treat as such), its parent * will be used instead. * </p> * @param path path to the folder in which extensions are stored. * @throws IOException if the specified folder or the specified file's parent couldn't be accessed. * @see #setExtensionsFolder(File) * @see #setExtensionsFolder(String) * @see #getExtensionsFolder() */ public static void setExtensionsFolder(String path) throws IOException { AbstractFile folder; if((folder = FileFactory.getFile(path)) == null) setExtensionsFolder(new File(path)); else setExtensionsFolder(folder); } /** * Returns the path to the default extensions folder. * <p> * The default path is: * <pre> * {@link PlatformManager#getPreferencesFolder()}.{@link AbstractFile#getChild(String) getChild}({@link #DEFAULT_EXTENSIONS_FOLDER_NAME}); * </pre> * </p> * @return the path to the default extensions folder. * @throws IOException if there was an error retrieving the default extensions folder. */ private static AbstractFile getDefaultExtensionsFolder() throws IOException { AbstractFile folder; folder = PlatformManager.getPreferencesFolder().getChild(DEFAULT_EXTENSIONS_FOLDER_NAME); // Makes sure the folder exists. if(!folder.exists()) folder.mkdir(); return folder; } /** * Returns the folder in which all extensions are stored. * @return the folder in which all extensions are stored. * @throws IOException if an error occurred while locating the default extensions folder. * @see #setExtensionsFolder(AbstractFile) */ public static AbstractFile getExtensionsFolder() throws IOException { // If the extensions folder has been set, use it. if(extensionsFolder != null) return extensionsFolder; return getDefaultExtensionsFolder(); } /** * Returns an <code>AbstractFile</code> to the extension file with the specified filename and located in the * {@link #getExtensionsFolder() extensions folder}. The returned file may or may not exist. * @param filename the extension's filename * @return an AbstractFile to the extension file with the specified filename and located in the * extensions folder. * @throws IOException if the file could not be instantiated. */ public static AbstractFile getExtensionsFile(String filename) throws IOException { return getExtensionsFolder().getDirectChild(filename); } // - Classpath querying ----------------------------------------------------- // -------------------------------------------------------------------------- /** * Returns <code>true</code> if the specified file is in the extension's classloader path. * @param file file whose presence in the extensions path will be checked. * @return <code>true</code> if the specified file is in the extension's classloader path, <code>false</code> otherwise. */ public static boolean isInExtensionsPath(AbstractFile file) {return loader.contains(file);} /** * Returns <code>true</code> if the specified file is in the system classpath. * @param file file whose presence in the system classpath will be checked. * @return <code>true</code> if the specified file is in the system classpath, <code>false</code> otherwise. */ public static boolean isInClasspath(AbstractFile file) { StringTokenizer parser; String path; path = file.getAbsolutePath(); parser = new StringTokenizer(System.getProperty("java.class.path"), System.getProperty("path.separator")); while(parser.hasMoreTokens()) if(parser.nextToken().equals(path)) return true; return false; } /** * Returns <code>true</code> if the specified file is either in the extension or system classpath. * <p> * This is a convenience method and is equivalent to calling: * <code>{@link #isInClasspath(AbstractFile) isInClasspath}(file) || {@link #isInExtensionsPath(AbstractFile) isInExtensionsPath}(file)</code>. * </p> * @param file file whose availability will be checked. * @return <code>true</code> if the specified file is either in the extension or system classpath, <code>false</code> otherwise. */ public static boolean isAvailable(AbstractFile file) {return isInClasspath(file) || isInExtensionsPath(file);} // - Classpath extension ---------------------------------------------------- // -------------------------------------------------------------------------- /** * Imports the specified file in muCommander's libraries. * @param file path to the library to import. * @param force wether to overwrite eventual existing libraries of the same name. * @return <code>true</code> if the operation was a success, * <code>false</code> if a library of the same name already exists and * <code>force</code> is set to <code>false</code>. * @throws IOException if an I/O error occurs. */ public static boolean importLibrary(AbstractFile file, boolean force) throws IOException { AbstractFile dest; // If the file is already in the extensions or classpath, // there's nothing to do. if(isAvailable(file)) return true; // If the destination file already exists, either delete it // if force is set to true or just return false. dest = getExtensionsFile(file.getName()); if(dest.exists()) { if(!force) return false; dest.delete(); } // Copies the library and adds it to the extensions classpath. file.copyTo(dest); addToClassPath(dest); return true; } /** * Adds the specified file to the extension's classpath. * @param file file to add to the classpath. */ public static void addToClassPath(AbstractFile file) {loader.addFile(file);} /** * Adds all known extensions to the current classpath. * <p> * This method will create the following new classpath entries: * <ul> * <li>{@link #getExtensionsFolder()}</li>. * <li>All <code>JAR</code> files in {@link #getExtensionsFolder()}.</li> * </ul> * </p> * @throws IOException if the extensions folder is not accessible. */ public static void addExtensionsToClasspath() throws IOException { AbstractFile[] files; // Adds the extensions folder to the classpath. addToClassPath(getExtensionsFolder()); // Adds all JAR files contained by the extensions folder to the classpath. files = getExtensionsFolder().ls(new ExtensionFilenameFilter(".jar")); for (AbstractFile file : files) addToClassPath(file); } /** * Returns the <code>ClassLoader</code> used to load all extensions. * @return the <code>ClassLoader</code> used to load all extensions. */ public static ClassLoader getClassLoader() {return loader;} }