/* $Id: ModuleLoader2.java 17920 2010-01-24 15:03:19Z linus $ ***************************************************************************** * Copyright (c) 2009 Contributors - see below * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * tfmorris ***************************************************************************** * * Some portions of this file was previously release using the BSD License: */ // Copyright (c) 2004-2008 The Regents of the University of California. All // Rights Reserved. Permission to use, copy, modify, and distribute this // software and its documentation without fee, and without a written // agreement is hereby granted, provided that the above copyright notice // and this paragraph appear in all copies. This software program and // documentation are copyrighted by The Regents of the University of // California. The software program and documentation are supplied "AS // IS", without any accompanying services from The Regents. The Regents // does not warrant that the operation of the program will be // uninterrupted or error-free. The end-user understands that the program // was developed for research purposes and is advised not to rely // exclusively on the program for any reason. IN NO EVENT SHALL THE // UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, // SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, // ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF // THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF // SUCH DAMAGE. THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY // WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE // PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND THE UNIVERSITY OF // CALIFORNIA HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, // UPDATES, ENHANCEMENTS, OR MODIFICATIONS. package org.argouml.moduleloader; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Modifier; import java.net.URL; import java.net.URLClassLoader; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; import org.apache.log4j.Logger; import org.argouml.application.api.AbstractArgoJPanel; import org.argouml.application.api.Argo; import org.argouml.i18n.Translator; /** * This is the module loader that loads modules implementing the * ModuleInterface.<p> * * This is a singleton. There are convenience functions that are static * to access the module.<p> * * @stereotype singleton * @author Linus Tolke * @since 0.15.4 */ public final class ModuleLoader2 { /** * Logger. */ private static final Logger LOG = Logger.getLogger(ModuleLoader2.class); /** * This map contains the module loader information about the module.<p> * * The keys is the list of available modules. */ private Map<ModuleInterface, ModuleStatus> moduleStatus; /** * List of locations that we've searched and/or loaded modules * from. This is for information purposes only to allow it to * be displayed in the settings Environment tab. */ private List<String> extensionLocations = new ArrayList<String>(); /** * The module loader object. */ private static final ModuleLoader2 INSTANCE = new ModuleLoader2(); /** * The prefix in URL:s that are files. */ private static final String FILE_PREFIX = "file:"; /** * The prefix in URL:s that are jars. */ private static final String JAR_PREFIX = "jar:"; /** * Class file suffix. */ public static final String CLASS_SUFFIX = ".class"; /** * Constructor for this object. */ private ModuleLoader2() { moduleStatus = new HashMap<ModuleInterface, ModuleStatus>(); computeExtensionLocations(); } /** * Get hold of the instance of this object. * * @return the instance */ public static ModuleLoader2 getInstance() { return INSTANCE; } /** * @return a list of Details panel tabs */ List<AbstractArgoJPanel> getDetailsTabs() { List<AbstractArgoJPanel> result = new ArrayList<AbstractArgoJPanel>(); for (ModuleInterface module : getInstance().availableModules()) { ModuleStatus status = moduleStatus.get(module); if (status == null) { continue; } if (status.isEnabled()) { if (module instanceof DetailsTabProvider) { result.addAll( ((DetailsTabProvider) module).getDetailsTabs()); } } } return result; } /** * Return a collection of all available modules. * * @return A Collection of all available modules. */ private Collection<ModuleInterface> availableModules() { return Collections.unmodifiableCollection(moduleStatus.keySet()); } // Access methods for program infrastructure. /** * Enables all selected modules and disabling all modules not selected.<p> * * In short this attempts to make the modules obey their selection.<p> * * @param failingAllowed is <code>true</code> if enabling or disabling of * some of the modules is allowed to fail. */ public static void doLoad(boolean failingAllowed) { getInstance().doInternal(failingAllowed); } // Access methods for modules that need to query about the status of // other modules. /** * Gets the loaded status for some other module. * * @return true if the module exists and is enabled. * @param name is the module name of the queried module */ public static boolean isEnabled(String name) { return getInstance().isEnabledInternal(name); } // Access methods for the GUI that the user uses to enable and disable // modules. /** * Get a Collection with all the names. * * @return all the names. */ public static Collection<String> allModules() { Collection<String> coll = new HashSet<String>(); for (ModuleInterface mf : getInstance().availableModules()) { coll.add(mf.getName()); } return coll; } /** * Get the selected. * * @param name The name of the module. * @return <code>true</code> if the module is selected. */ public static boolean isSelected(String name) { return getInstance().isSelectedInternal(name); } /** * Get the selected. * * @see #isSelected(String) * @param name The name of the module. * @return <code>true</code> if the module is selected. */ private boolean isSelectedInternal(String name) { Map.Entry<ModuleInterface, ModuleStatus> entry = findModule(name); if (entry != null) { ModuleStatus status = entry.getValue(); if (status == null) { return false; } return status.isSelected(); } return false; } /** * Set the selected value. * * @param name The name of the module. * @param value Selected or not. */ public static void setSelected(String name, boolean value) { getInstance().setSelectedInternal(name, value); } /** * Set the selected value. * * @see #setSelected(String, boolean) * @param name The name of the module. * @param value Selected or not. */ private void setSelectedInternal(String name, boolean value) { Map.Entry<ModuleInterface, ModuleStatus> entry = findModule(name); if (entry != null) { ModuleStatus status = entry.getValue(); status.setSelected(value); } } /** * Create a description of the module based on the information provided * by the module itself. * * @param name The name of the module. * @return The description. */ public static String getDescription(String name) { return getInstance().getDescriptionInternal(name); } /** * Create a description of the module based on the information provided * by the module itself. * * @see #getDescription(String) * @param name The name of the module. * @return The description. */ private String getDescriptionInternal(String name) { Map.Entry<ModuleInterface, ModuleStatus> entry = findModule(name); if (entry == null) { throw new IllegalArgumentException("Module does not exist."); } ModuleInterface module = entry.getKey(); StringBuffer sb = new StringBuffer(); String desc = module.getInfo(ModuleInterface.DESCRIPTION); if (desc != null) { sb.append(desc); sb.append("\n\n"); } String author = module.getInfo(ModuleInterface.AUTHOR); if (author != null) { sb.append("Author: ").append(author); sb.append("\n"); } String version = module.getInfo(ModuleInterface.VERSION); if (version != null) { sb.append("Version: ").append(version); sb.append("\n"); } return sb.toString(); } // Access methods for the program infrastructure /** * Enables all selected modules. * * @param failingAllowed is true if this is not the last attempt at * turning on. */ private void doInternal(boolean failingAllowed) { huntForModules(); boolean someModuleSucceeded; do { someModuleSucceeded = false; for (ModuleInterface module : getInstance().availableModules()) { ModuleStatus status = moduleStatus.get(module); if (status == null) { continue; } if (!status.isEnabled() && status.isSelected()) { try { if (module.enable()) { someModuleSucceeded = true; status.setEnabled(); } } // Catch all exceptions and errors, however severe catch (Throwable e) { LOG.error("Exception or error while trying to " + "enable module " + module.getName(), e); } } else if (status.isEnabled() && !status.isSelected()) { try { if (module.disable()) { someModuleSucceeded = true; status.setDisabled(); } } // Catch all exceptions and errors, however severe catch (Throwable e) { LOG.error("Exception or error while trying to " + "disable module " + module.getName(), e); } } } } while (someModuleSucceeded); if (!failingAllowed) { // Notify the user that the modules in the list that are selected // but not enabled were not possible to enable and that are not // selected that we cannot disable. // // Currently we just log this. // // TODO: We could eventually pop up some warning window. // for (ModuleInterface module : getInstance().availableModules()) { ModuleStatus status = moduleStatus.get(module); if (status == null) { continue; } if (status.isEnabled() && status.isSelected()) { continue; } if (!status.isEnabled() && !status.isSelected()) { continue; } if (status.isSelected()) { LOG.warn("ModuleLoader was not able to enable module " + module.getName()); } else { LOG.warn("ModuleLoader was not able to disable module " + module.getName()); } } } } /** * Gets the loaded status for some other module. * * @return true if the module exists and is enabled. * @param name is the module name of the queried module */ private boolean isEnabledInternal(String name) { Map.Entry<ModuleInterface, ModuleStatus> entry = findModule(name); if (entry != null) { ModuleStatus status = entry.getValue(); if (status == null) { return false; } return status.isEnabled(); } return false; } /** * Return the ModuleInterface, ModuleStatus pair for the module * with the given name or <code>null</code> if there isn't any. * * @param name The given name. * @return A pair (Map.Entry). */ private Map.Entry<ModuleInterface, ModuleStatus> findModule(String name) { for (Map.Entry<ModuleInterface, ModuleStatus> entry : moduleStatus .entrySet()) { ModuleInterface module = entry.getKey(); if (name.equalsIgnoreCase(module.getName())) { return entry; } } return null; } /** * Tries to find as many available modules as possible.<p> * * As the modules are found they are appended to {@link #moduleStatus}.<p> */ private void huntForModules() { huntForModulesFromExtensionDir(); // TODO: huntForModulesFromJavaWebStart(); // Load modules specified by a System property. // Modules specified by a system property is for // running modules from within Eclipse and running // from Java Web Start. String listOfClasses = System.getProperty("argouml.modules"); if (listOfClasses != null) { StringTokenizer si = new StringTokenizer(listOfClasses, ";"); while (si.hasMoreTokens()) { String className = si.nextToken(); try { addClass(className); } catch (ClassNotFoundException e) { LOG.error("Could not load module from class " + className); } } } } /** * Find and enable modules from our "ext" directory and from the * directory specified in "argo.ext.dir".<p> */ private void huntForModulesFromExtensionDir() { for (String location : extensionLocations) { huntModulesFromNamedDirectory(location); } } /** * This does a calculation of where our "ext" directory is. * TODO: We should eventually make sure that this calculation is * only present in one place in the code and not several. */ private void computeExtensionLocations() { // Use a little trick to find out where Argo is being loaded from. // TODO: Use a different resource here. ARGOINI is unused and deprecated String extForm = getClass().getResource(Argo.ARGOINI).toExternalForm(); String argoRoot = extForm.substring(0, extForm.length() - Argo.ARGOINI.length()); // If it's a jar, clean it up and make it look like a file url if (argoRoot.startsWith(JAR_PREFIX)) { argoRoot = argoRoot.substring(JAR_PREFIX.length()); if (argoRoot.endsWith("!")) { argoRoot = argoRoot.substring(0, argoRoot.length() - 1); } } String argoHome = null; if (argoRoot != null) { LOG.info("argoRoot is " + argoRoot); if (argoRoot.startsWith(FILE_PREFIX)) { argoHome = new File(argoRoot.substring(FILE_PREFIX.length())) .getAbsoluteFile().getParent(); } else { argoHome = new File(argoRoot).getAbsoluteFile().getParent(); } try { argoHome = java.net.URLDecoder.decode(argoHome, Argo.getEncoding()); } catch (UnsupportedEncodingException e) { LOG.warn("Encoding " + Argo.getEncoding() + " is unknown."); } LOG.info("argoHome is " + argoHome); } if (argoHome != null) { String extdir; if (argoHome.startsWith(FILE_PREFIX)) { extdir = argoHome.substring(FILE_PREFIX.length()) + File.separator + "ext"; } else { extdir = argoHome + File.separator + "ext"; } extensionLocations.add(extdir); } String extdir = System.getProperty("argo.ext.dir"); if (extdir != null) { extensionLocations.add(extdir); } } /** * Get the list of locations that we've loaded extension modules from. * @return A list of the paths we've loaded from. */ public List<String> getExtensionLocations() { return Collections.unmodifiableList(extensionLocations); } /** * Find and enable a module from a given directory. * * @param dirname The name of the directory. */ private void huntModulesFromNamedDirectory(String dirname) { File extensionDir = new File(dirname); if (extensionDir.isDirectory()) { File[] files = extensionDir.listFiles(new JarFileFilter()); for (File file : files) { JarFile jarfile = null; // Try-catch only the JarFile instantiation so we // don't accidentally mask anything in ArgoJarClassLoader // or processJarFile. try { jarfile = new JarFile(file); if (jarfile != null) { ClassLoader classloader = new URLClassLoader(new URL[] { file.toURI().toURL(), }, getClass().getClassLoader()); try { processJarFile(classloader, file); } catch (ClassNotFoundException e) { LOG.error("The class is not found.", e); return; } } } catch (IOException ioe) { LOG.error("Cannot open Jar file " + file, ioe); } } } } /** * Check a jar file for an ArgoUML extension/module.<p> * * If there isn't a manifest or it isn't readable, we fall back to using * the raw JAR entries. * * @param classloader The classloader to use. * @param file The file to process. * @throws ClassNotFoundException if the manifest file contains a class * that doesn't exist. */ private void processJarFile(ClassLoader classloader, File file) throws ClassNotFoundException { LOG.info("Opening jar file " + file); JarFile jarfile; try { jarfile = new JarFile(file); } catch (IOException e) { LOG.error("Unable to open " + file, e); return; } Manifest manifest; try { manifest = jarfile.getManifest(); if (manifest == null) { // We expect all extensions to have a manifest even though we // can operate without one if necessary. LOG.warn(file + " does not have a manifest"); } } catch (IOException e) { LOG.error("Unable to read manifest of " + file, e); return; } // TODO: It is a performance drain to load all classes at startup time. // They should be lazy loaded when needed. Instead of scanning all // classes for ones which implement our loadable module interface, we // should use a manifest entry or a special name/name pattern that we // look for to find the single main module class to load here. - tfm boolean loadedClass = false; if (manifest == null) { Enumeration<JarEntry> jarEntries = jarfile.entries(); while (jarEntries.hasMoreElements()) { JarEntry entry = jarEntries.nextElement(); loadedClass = loadedClass | processEntry(classloader, entry.getName()); } } else { Map<String, Attributes> entries = manifest.getEntries(); for (String key : entries.keySet()) { // Look for our specification loadedClass = loadedClass | processEntry(classloader, key); } } // Add this to search list for I18N properties // (Done for both modules & localized property file sets) Translator.addClassLoader(classloader); // If it didn't have a loadable module class and it doesn't look like // a localized property set, warn the user that something funny is in // their extension directory if (!loadedClass && !file.getName().contains("argouml-i18n-")) { LOG.error("Failed to find any loadable ArgoUML modules in jar " + file); } } /** * Process a JAR file entry, attempting to load anything that looks like a * Java class. * * @param classloader * the classloader to use when loading the class * @param cname * the class name * @throws ClassNotFoundException * @return true if class was a module class and loaded successfully */ private boolean processEntry(ClassLoader classloader, String cname) throws ClassNotFoundException { if (cname.endsWith(CLASS_SUFFIX)) { int classNamelen = cname.length() - CLASS_SUFFIX.length(); String className = cname.substring(0, classNamelen); className = className.replace('/', '.'); return addClass(classloader, className); } return false; } /** * Add a class from the current class loader. * * @param classname The name of the class (including package). * @throws ClassNotFoundException if the class classname is not found. */ public static void addClass(String classname) throws ClassNotFoundException { getInstance().addClass(ModuleLoader2.class.getClassLoader(), classname); } /** * Try to load a module from the given ClassLoader.<p> * * Only add it as a module if it is a module (i.e. it implements the * {@link ModuleInterface} interface. * * @param classLoader The ClassLoader to load from. * @param classname The name. * @throws ClassNotFoundException if the class classname is not found. */ private boolean addClass(ClassLoader classLoader, String classname) throws ClassNotFoundException { LOG.info("Loading module " + classname); Class moduleClass; try { moduleClass = classLoader.loadClass(classname); } catch (UnsupportedClassVersionError e) { LOG.error("Unsupported Java class version for " + classname); return false; } catch (NoClassDefFoundError e) { LOG.error("Unable to find required class while loading " + classname + " - may indicate an obsolete" + " extension module or an unresolved dependency", e); return false; } catch (Throwable e) { if (e instanceof ClassNotFoundException) { throw (ClassNotFoundException) e; } LOG.error("Unexpected error while loading " + classname, e); return false; } if (!ModuleInterface.class.isAssignableFrom(moduleClass)) { LOG.debug("The class " + classname + " is not a module."); return false; } Constructor defaultConstructor; try { defaultConstructor = moduleClass.getDeclaredConstructor(new Class[] {}); } catch (SecurityException e) { LOG.error("The default constructor for class " + classname + " is not accessable.", e); return false; } catch (NoSuchMethodException e) { LOG.error("The default constructor for class " + classname + " is not found.", e); return false; } catch (NoClassDefFoundError e) { LOG.error("Unable to find required class while loading " + classname + " - may indicate an obsolete" + " extension module or an unresolved dependency", e); return false; } catch (Throwable e) { LOG.error("Unexpected error while loading " + classname, e); return false; } if (!Modifier.isPublic(defaultConstructor.getModifiers())) { LOG.error("The default constructor for class " + classname + " is not public. Not loaded."); return false; } Object moduleInstance; try { moduleInstance = defaultConstructor.newInstance(new Object[]{}); } catch (IllegalArgumentException e) { LOG.error("The constructor for class " + classname + " is called with incorrect argument.", e); return false; } catch (InstantiationException e) { LOG.error("The constructor for class " + classname + " threw an exception.", e); return false; } catch (IllegalAccessException e) { LOG.error("The constructor for class " + classname + " is not accessible.", e); return false; } catch (InvocationTargetException e) { LOG.error("The constructor for class " + classname + " cannot be called.", e); return false; } catch (NoClassDefFoundError e) { LOG.error("Unable to find required class while instantiating " + classname + " - may indicate an obsolete" + " extension module or an unresolved dependency", e); return false; } catch (Throwable e) { LOG.error("Unexpected error while instantiating " + classname, e); return false; } // The following check should have been satisfied before we // instantiated the module, but double check again if (!(moduleInstance instanceof ModuleInterface)) { LOG.error("The class " + classname + " is not a module."); return false; } ModuleInterface mf = (ModuleInterface) moduleInstance; addModule(mf); LOG.info("Succesfully loaded module " + classname); return true; } /** * Add a newly found module to {@link #moduleStatus}. If we already * know about it, don't add it. * * @param mf The module to add. */ private void addModule(ModuleInterface mf) { // Since there is no way to compare the objects as equal, // we have to search through the list at this point. for (ModuleInterface foundMf : moduleStatus.keySet()) { if (foundMf.getName().equals(mf.getName())) { return; } } // We havn't found it. Add it. ModuleStatus ms = new ModuleStatus(); // Enable it. // TODO: This by default selects all modules that are found. // Eventually we would rather obey a default either from the // modules themselves, from how they are found, and also // have information on what modules are selected from the // configuration. ms.setSelected(); moduleStatus.put(mf, ms); } /** * The file filter that selects Jar files. */ static class JarFileFilter implements FileFilter { /* * @see java.io.FileFilter#accept(java.io.File) */ public boolean accept(File pathname) { return (pathname.canRead() && pathname.isFile() && pathname.getPath().toLowerCase().endsWith(".jar")); } } } /** * Status for each of the available modules. This is created in one copy per * available module. */ class ModuleStatus { /** * If the module is enabled. */ private boolean enabled; /** * If the module is selected. */ private boolean selected; /** * Tells if the module is enabled or not. * * @return true if the module is enabled. */ public boolean isEnabled() { return enabled; } /** * Setter for enabled. */ public void setEnabled() { enabled = true; } /** * Setter for enabled. */ public void setDisabled() { enabled = false; } /** * Tells if the module is selected by the user or not. * * @return true if it is selected. */ public boolean isSelected() { return selected; } /** * Setter for selected. */ public void setSelected() { selected = true; } /** * Setter for selected. */ public void setUnselect() { selected = false; } /** * Setter for selected. * * @param value The value to set. */ public void setSelected(boolean value) { if (value) { setSelected(); } else { setUnselect(); } } }