Author: skitching Date: Wed Jun 1 21:09:16 2005 New Revision: 179500 URL: http://svn.apache.org/viewcvs?rev=179500&view=rev Log: Change to discovery process: testing whether various logging libraries are available is now done by trying to actually create an instance.
Modified: jakarta/commons/proper/logging/trunk/src/java/org/apache/commons/logging/impl/LogFactoryImpl.java Modified: jakarta/commons/proper/logging/trunk/src/java/org/apache/commons/logging/impl/LogFactoryImpl.java URL: http://svn.apache.org/viewcvs/jakarta/commons/proper/logging/trunk/src/java/org/apache/commons/logging/impl/LogFactoryImpl.java?rev=179500&r1=179499&r2=179500&view=diff ============================================================================== --- jakarta/commons/proper/logging/trunk/src/java/org/apache/commons/logging/impl/LogFactoryImpl.java (original) +++ jakarta/commons/proper/logging/trunk/src/java/org/apache/commons/logging/impl/LogFactoryImpl.java Wed Jun 1 21:09:16 2005 @@ -57,12 +57,12 @@ * * <p>This factory will remember previously created <code>Log</code> instances * for the same name, and will return them on repeated requests to the - * <code>getInstance()</code> method. This implementation ignores any - * configured attributes.</p> + * <code>getInstance()</code> method. * * @author Rod Waldhoff * @author Craig R. McClanahan * @author Richard A. Sitze + * @author Brian Stansberry * @version $Revision$ $Date$ */ @@ -115,6 +115,7 @@ */ private String diagnosticPrefix; + /** * Configuration attributes. */ @@ -164,9 +165,9 @@ protected Class logMethodSignature[] = { LogFactory.class }; - // --------------------------------------------------------- Public Methods + /** * Return the configuration attribute with the specified name (if any), * or <code>null</code> if there is no such attribute. @@ -309,14 +310,16 @@ return LogFactory.getContextClassLoader(); } + /** * Workaround for bug in Java1.2; in theory this method is not needed. - * See LogFactory.isInternalLoggingEnabled. + * See LogFactory.isDiagnosticsEnabled. */ protected static boolean isDiagnosticsEnabled() { return LogFactory.isDiagnosticsEnabled(); } + /** * Workaround for bug in Java1.2; in theory this method is not needed. * See LogFactory.getClassLoader. @@ -325,6 +328,7 @@ return LogFactory.getClassLoader(clazz); } + // ------------------------------------------------------ Protected Methods /** @@ -352,6 +356,7 @@ diagnosticPrefix = clazz.getName() + "@" + classLoader.toString() + ":"; } + /** * Output a diagnostic message to a user-specified destination (if the * user has enabled diagnostic logging). @@ -366,75 +371,18 @@ /** * Return the fully qualified Java classname of the [EMAIL PROTECTED] Log} - * implementation we will be using. - * <p> - * This method looks in the following places: - * <ul> - * <li>Looks for an attribute LOG_PROPERTY or LOG_PROPERTY_OLD in the - * "attributes" associated with this class, as set earlier by method - * setAttribute. - * <li>Looks for a property LOG_PROPERTY or LOG_PROPERTY_OLD in the - * system properties. - * <li>Looks for log4j, jdk logging and jdk13lumberjack classes in - * the classpath. - * </ul> + * implementation we will be using. + * + * @deprecated Never invoked by this class; subclasses should not assume + * it will be. */ protected String getLogClassName() { - // Return the previously identified class name (if any) - if (logClassName != null) { - return logClassName; - } - - logDiagnostic("Determining the name for the Log implementation."); - - logDiagnostic("Trying to get log class from attribute " + LOG_PROPERTY); - logClassName = (String) getAttribute(LOG_PROPERTY); - - if (logClassName == null) { // @deprecated - logDiagnostic("Trying to get log class from attribute " + LOG_PROPERTY_OLD); - logClassName = (String) getAttribute(LOG_PROPERTY_OLD); - } - if (logClassName == null) { - try { - logDiagnostic("Trying to get log class from system property " + LOG_PROPERTY); - logClassName = System.getProperty(LOG_PROPERTY); - } catch (SecurityException e) { - ; - } - } - - if (logClassName == null) { // @deprecated - try { - logDiagnostic("Trying to get log class from system property " + LOG_PROPERTY_OLD); - logClassName = System.getProperty(LOG_PROPERTY_OLD); - } catch (SecurityException e) { - ; - } - } - - // no need for internalLog calls below; they are done inside the - // various isXXXAvailable methods. - if ((logClassName == null) && isLog4JAvailable()) { - logClassName = "org.apache.commons.logging.impl.Log4JLogger"; - } - - if ((logClassName == null) && isJdk14Available()) { - logClassName = "org.apache.commons.logging.impl.Jdk14Logger"; - } - - if ((logClassName == null) && isJdk13LumberjackAvailable()) { - logClassName = "org.apache.commons.logging.impl.Jdk13LumberjackLogger"; + discoverLogImplementation(getClass().getName()); } - - if (logClassName == null) { - logClassName = "org.apache.commons.logging.impl.SimpleLog"; - } - - logDiagnostic("Using log class " + logClassName); - return (logClassName); - + + return logClassName; } @@ -448,139 +396,24 @@ * in all circumstances.</p> * * @exception LogConfigurationException if a suitable constructor - * cannot be returned + * cannot be returned + * + * @deprecated Never invoked by this class; subclasses should not assume + * it will be. */ protected Constructor getLogConstructor() throws LogConfigurationException { // Return the previously identified Constructor (if any) - if (logConstructor != null) { - return logConstructor; - } - - String logClassName = getLogClassName(); - - // Attempt to load the Log implementation class - // - // Question: why is the loginterface being loaded dynamically? - // Isn't the code below exactly the same as this? - // Class logInterface = Log.class; - - Class logClass = null; - Class logInterface = null; - try { - ClassLoader cl = getClassLoader(this.getClass()); - if (cl == null) { - // we are probably in Java 1.1, but may also be running in - // some sort of embedded system.. - logInterface = loadClass(LOG_INTERFACE); - } else { - // normal situation - logInterface = cl.loadClass(LOG_INTERFACE); - } - - logClass = loadClass(logClassName); - if (logClass == null) { - logDiagnostic( - "Unable to find any class named [" + logClassName + "]" - + " in either the context classloader" - + " or the classloader that loaded this class."); - - throw new LogConfigurationException - ("No suitable Log implementation for " + logClassName); - } - - if (!logInterface.isAssignableFrom(logClass)) { - // oops, we need to cast this logClass we have loaded into - // a Log object in order to return it. But we won't be - // able to. See method reportInvalidLogAdapter for more - // information. - LogConfigurationException ex = - reportInvalidLogAdapter(logInterface, logClass); - throw ex; - } - } catch (Throwable t) { - logDiagnostic( - "An unexpected problem occurred while loading the" - + " log adapter class: " + t.getMessage()); - throw new LogConfigurationException(t); - } - - // Identify the <code>setLogFactory</code> method (if there is one) - try { - logMethod = logClass.getMethod("setLogFactory", - logMethodSignature); - } catch (Throwable t) { - logMethod = null; + if (logConstructor == null) { + discoverLogImplementation(getClass().getName()); } - // Identify the corresponding constructor to be used - try { - logConstructor = logClass.getConstructor(logConstructorSignature); - return (logConstructor); - } catch (Throwable t) { - throw new LogConfigurationException - ("No suitable Log constructor " + - logConstructorSignature+ " for " + logClassName, t); - } + return logConstructor; } + /** - * Report a problem loading the log adapter, then <i>always</i> throw - * a LogConfigurationException. - * <p> - * There are two possible reasons why we successfully loaded the - * specified log adapter class then failed to cast it to a Log object: - * <ol> - * <li>the specific class just doesn't implement the Log interface - * (user screwed up), or - * <li> the specified class has bound to a Log class loaded by some other - * classloader; [EMAIL PROTECTED] cannot be cast to [EMAIL PROTECTED] - * </ol> - * <p> - * Here we try to figure out which case has occurred so we can give the - * user some reasonable feedback. - * - * @param logInterface is the class that this LogFactoryImpl class needs - * to return the adapter as. - * @param logClass is the adapter class we successfully loaded (but which - * could not be cast to type logInterface). - */ - private LogConfigurationException reportInvalidLogAdapter( - Class logInterface, Class logClass) { - - Class interfaces[] = logClass.getInterfaces(); - for (int i = 0; i < interfaces.length; i++) { - if (LOG_INTERFACE.equals(interfaces[i].getName())) { - - if (isDiagnosticsEnabled()) { - ClassLoader logInterfaceClassLoader = getClassLoader(logInterface); - ClassLoader logAdapterClassLoader = getClassLoader(logClass); - Class logAdapterInterface = interfaces[i]; - ClassLoader logAdapterInterfaceClassLoader = getClassLoader(logAdapterInterface); - logDiagnostic( - "Class " + logClassName + " was found in classloader " - + objectId(logAdapterClassLoader) - + " but it implements the Log interface as loaded" - + " from classloader " + objectId(logAdapterInterfaceClassLoader) - + " not the one loaded by this class's classloader " - + objectId(logInterfaceClassLoader)); - } - - throw new LogConfigurationException - ("Invalid class loader hierarchy. " + - "You have more than one version of '" + - LOG_INTERFACE + "' visible, which is " + - "not allowed."); - } - } - - return new LogConfigurationException - ("Class " + logClassName + " does not implement '" + - LOG_INTERFACE + "'."); - } - - /** * MUST KEEP THIS METHOD PRIVATE. * * <p>Exposing this method outside of @@ -590,10 +423,14 @@ * </p> * * Load a class, try first the thread class loader, and - * if it fails use the loader that loaded this class. Actually, as - * the thread (context) classloader should always be the same as or a - * child of the classloader that loaded this class, the fallback should - * never be used. + * if it fails use the loader that loaded this class. + * + * @param name fully qualified class name of the class to load + * + * @throws LinkageError if the linkage fails + * @throws ExceptionInInitializerError if the initialization provoked + * by this method fails + * @throws ClassNotFoundException if the class cannot be located */ private static Class loadClass( final String name ) throws ClassNotFoundException @@ -619,29 +456,32 @@ if (result instanceof Class) return (Class)result; - + throw (ClassNotFoundException)result; } /** - * Is <em>JDK 1.3 with Lumberjack</em> logging available? + * Is <em>JDK 1.3 with Lumberjack</em> logging available? + * + * @deprecated Never invoked by this class; subclasses should not assume + * it will be. */ protected boolean isJdk13LumberjackAvailable() { - - // note: the algorithm here is different from isLog4JAvailable. - // I think isLog4JAvailable is correct....see bugzilla#31597 + logDiagnostic("Checking for Jdk13Lumberjack."); try { - loadClass("java.util.logging.Logger"); - loadClass("org.apache.commons.logging.impl.Jdk13LumberjackLogger"); + createLogFromClass("org.apache.commons.logging.impl.Jdk13LumberjackLogger", + getClass().getName(), + false); + // No exception means success logDiagnostic("Found Jdk13Lumberjack."); return true; } catch (Throwable t) { logDiagnostic("Did not find Jdk13Lumberjack."); return false; } - + } @@ -649,20 +489,19 @@ * <p>Return <code>true</code> if <em>JDK 1.4 or later</em> logging * is available. Also checks that the <code>Throwable</code> class * supports <code>getStackTrace()</code>, which is required by - * Jdk14Logger.</p> + * Jdk14Logger.</p> + * + * @deprecated Never invoked by this class; subclasses should not assume + * it will be. */ protected boolean isJdk14Available() { - // note: the algorithm here is different from isLog4JAvailable. - // I think isLog4JAvailable is correct.... logDiagnostic("Checking for Jdk14."); try { - loadClass("java.util.logging.Logger"); - loadClass("org.apache.commons.logging.impl.Jdk14Logger"); - Class throwable = loadClass("java.lang.Throwable"); - if (throwable.getDeclaredMethod("getStackTrace", (Class[]) null) == null) { - return (false); - } + createLogFromClass("org.apache.commons.logging.impl.Jdk14Logger", + getClass().getName(), + false); + // No exception means success logDiagnostic("Found Jdk14."); return true; } catch (Throwable t) { @@ -673,16 +512,20 @@ /** - * Is a <em>Log4J</em> implementation available? + * Is a <em>Log4J</em> implementation available? + * + * @deprecated Never invoked by this class; subclasses should not assume + * it will be. */ protected boolean isLog4JAvailable() { logDiagnostic("Checking for Log4J"); try { - Class adapterClass = loadClass("org.apache.commons.logging.impl.Log4JLogger"); - ClassLoader cl = getClassLoader(adapterClass); - Class loggerClass = cl.loadClass("org.apache.log4j.Logger" ); - logDiagnostic("Found Log4J"); + createLogFromClass("org.apache.commons.logging.impl.Log4JLogger", + getClass().getName(), + false); + // No exception means success + logDiagnostic("Found Log4J."); return true; } catch (Throwable t) { logDiagnostic("Did not find Log4J"); @@ -704,15 +547,31 @@ Log instance = null; try { - Object params[] = new Object[1]; - params[0] = name; - instance = (Log) getLogConstructor().newInstance(params); + if (logConstructor == null) { + instance = discoverLogImplementation(name); + } + else { + Object params[] = { name }; + instance = (Log) logConstructor.newInstance(params); + } + if (logMethod != null) { - params[0] = this; + Object params[] = { this }; logMethod.invoke(instance, params); } + return (instance); + + } catch (LogConfigurationException lce) { + + // this type of exception means there was a problem in discovery + // and we've already output diagnostics about the issue, etc.; + // just pass it on + throw (LogConfigurationException) lce; + } catch (InvocationTargetException e) { + // A problem occurred invoking the Constructor or Method + // previously discovered Throwable c = e.getTargetException(); if (c != null) { throw new LogConfigurationException(c); @@ -720,10 +579,393 @@ throw new LogConfigurationException(e); } } catch (Throwable t) { + // A problem occurred invoking the Constructor or Method + // previously discovered throw new LogConfigurationException(t); } + } + + + // ------------------------------------------------------ Private Methods + + /** + * Attempts to create a Log instance for the given category name. + * Follows the discovery process described in the class javadoc. + * + * @param logCategory the name of the log category + * + * @throws LogConfigurationException if an error in discovery occurs, + * or if no adapter at all can be + * instantiated + */ + private Log discoverLogImplementation(String logCategory) + { + logDiagnostic("Attempting to discover a Log implementation."); + + Log result = null; + + // See if the user specified the Log implementation to use + String specifiedLogClassName = findUserSpecifiedLogClassName(); + + if (specifiedLogClassName != null) { + try { + // note: createLogFromClass never returns null.. + result = createLogFromClass(specifiedLogClassName, + logCategory, + true); + return result; + } catch (LogConfigurationException ex) { + // this type of exception means we've already output + // diagnostics about this issue, etc.; just pass it on + throw ex; + } catch (Throwable t) { + // log problem, and throw a LogConfigurationException + // wrapping the Throwable + handleFlawedDiscovery(specifiedLogClassName, null, t); + + // handleFlawedDiscovery should have thrown an LCE, but + // in case it didn't we'll throw one. Inability to + // instantiate a user specified class is a fatal error + throw new LogConfigurationException("Unable to instantiate " + + specifiedLogClassName, + t); + } + + // this if-statement never exits! + } + + // No user specified log; try to discover what's on the classpath + + // Try Log4j + try { + result = createLogFromClass("org.apache.commons.logging.impl.Log4JLogger", + logCategory, + true); + } catch (LogConfigurationException lce) { + + // LCE means we had a flawed discovery and already + // output diagnostics; just pass it on + throw (LogConfigurationException) lce; + + } catch (Throwable t) { + // Other throwables just mean couldn't load the adapter + // or log4j; continue with discovery + } + + if (result == null) { + // Try JDK 1.4 Logging + try { + result = createLogFromClass("org.apache.commons.logging.impl.Jdk14Logger", + logCategory, + true); + } catch (LogConfigurationException lce) { + + // LCE means we had a flawed discovery and already + // output diagnostics; just pass it on + throw (LogConfigurationException) lce; + + } catch (Throwable t) { + // Other throwables just mean couldn't load the adapter + // or j.u.l; continue with discovery + } + } + if (result == null) { + // Try Lumberjack + try { + result = createLogFromClass("org.apache.commons.logging.impl.Jdk13LumberjackLogger", + logCategory, + true); + } catch (LogConfigurationException lce) { + + // LCE means we had a flawed discovery and already + // output diagnostics; just pass it on + throw (LogConfigurationException) lce; + + } catch (Throwable t) { + // Other throwables just mean couldn't load the adapter + // or j.u.l; continue with discovery + } + } + + if (result == null) { + // Try SimpleLog + try { + result = createLogFromClass("org.apache.commons.logging.impl.SimpleLog", + logCategory, + true); + } catch (LogConfigurationException lce) { + + // LCE means we had a flawed discovery and already + // output diagnostics; just pass it up + throw (LogConfigurationException) lce; + + } catch (Throwable t) { + // Other throwables just mean couldn't load the adapter + } + } + + if (result == null) { + throw new LogConfigurationException + ("No suitable Log implementation"); + } + + return result; } + + + /** + * Checks system properties and the attribute map for + * a Log implementation specified by the user under the + * property names [EMAIL PROTECTED] #LOG_PROPERTY} or [EMAIL PROTECTED] #LOG_PROPERTY_OLD}. + * + * @return classname specified by the user, or <code>null</code> + */ + private String findUserSpecifiedLogClassName() + { + logDiagnostic("Trying to get log class from attribute " + LOG_PROPERTY); + String specifiedClass = (String) getAttribute(LOG_PROPERTY); + + if (specifiedClass == null) { // @deprecated + logDiagnostic("Trying to get log class from attribute " + + LOG_PROPERTY_OLD); + specifiedClass = (String) getAttribute(LOG_PROPERTY_OLD); + } + + if (specifiedClass == null) { + logDiagnostic("Trying to get log class from system property " + + LOG_PROPERTY); + try { + specifiedClass = System.getProperty(LOG_PROPERTY); + } catch (SecurityException e) { + ; + } + } + + if (specifiedClass == null) { // @deprecated + logDiagnostic("Trying to get log class from system property " + + LOG_PROPERTY_OLD); + try { + specifiedClass = System.getProperty(LOG_PROPERTY_OLD); + } catch (SecurityException e) { + ; + } + } + + return specifiedClass; + + } + + + /** + * Attempts to load the given class, find a suitable constructor, + * and instantiate an instance of Log. + * + * @param logAdapterClass classname of the Log implementation + * @param logCategory argument to pass to the Log implementation's + * constructor + * @param affectState <code>true</code> if this object's state should + * be affected by this method call, <code>false</code> + * otherwise. + * + * @return an instance of the given class. Will not return + * <code>null</code>. + * + * @throws LinkageError if any linkage provoked by this method fails + * @throws ExceptionInInitializerError if any initialization provoked + * by this method fails + * @throws ClassNotFoundException if the class cannot be located + * @throws NoClassDefFoundError if <code>logImplClass</code> could be + * loaded but the logging implementation it + * relies on could not be located + * @throws LogConfigurationException if the class was loaded but no suitable + * logger could be created and this object + * is configured to fail in such a + * situation + */ + private Log createLogFromClass(String logAdapterClass, + String logCategory, + boolean affectState) + throws Throwable { + logDiagnostic("Attempting to instantiate " + logAdapterClass); + + Class logClass = loadClass(logAdapterClass); + + Object[] params = { logCategory }; + Log result = null; + Constructor constructor = null; + try { + constructor = logClass.getConstructor(logConstructorSignature); + result = (Log) constructor.newInstance(params); + } catch (NoClassDefFoundError e) { + // We were able to load the adapter but its underlying + // logger library could not be found. This is normal and not + // a "flawed discovery", so just throw the error on + logDiagnostic("Unable to load logging library used by " + + logAdapterClass); + throw e; + } catch (Throwable t) { + // ExceptionInInitializerError + // NoSuchMethodException + // InvocationTargetException + // ClassCastException + // All mean the adapter and underlying logger library were found + // but there was a problem creating an instance. + // This is a "flawed discovery" + + handleFlawedDiscovery(logAdapterClass, logClass, t); + // handleFlawedDiscovery should have thrown an LCE, but + // in case it didn't we'll throw one + throw new LogConfigurationException(t); + } + + if (affectState) { + // We've succeeded, so set instance fields + this.logClassName = logClass.getName(); + this.logConstructor = constructor; + + // Identify the <code>setLogFactory</code> method (if there is one) + try { + this.logMethod = logClass.getMethod("setLogFactory", + logMethodSignature); + logDiagnostic("Found method setLogFactory(LogFactory) in " + + logClassName); + } catch (Throwable t) { + this.logMethod = null; + logDiagnostic(logAdapterClass + " does not declare method " + + "setLogFactory(LogFactory)"); + } + } + + return result; + + } + + + /** + * Generates an internal diagnostic logging of the discovery failure and + * then throws a <code>LogConfigurationException</code> that wraps + * the passed <code>Throwable</code>. + * + * @param logClassName the class name of the Log implementation + * that could not be instantiated. Cannot be + * <code>null</code>. + * @param adapterClass <code>Code</code> whose name is + * <code>logClassName</code>, or <code>null</code> if + * discovery was unable to load the class. + * @param discoveryFlaw Throwable thrown during discovery. + * + * @throws LogConfigurationException ALWAYS + */ + private void handleFlawedDiscovery(String logClassName, + Class adapterClass, + Throwable discoveryFlaw) { + + // Output diagnostics + + // For ClassCastException use the more complex diagnostic + // that analyzes the classloader hierarchy + if ( discoveryFlaw instanceof ClassCastException + && adapterClass != null) { + // reportInvalidAdapter returns a LogConfigurationException + // that wraps the ClassCastException; replace variable + // 'discoveryFlaw' with that so we can rethrow the LCE + discoveryFlaw = reportInvalidLogAdapter(adapterClass, + discoveryFlaw); + } + else { + logDiagnostic("Could not instantiate Log " + + logClassName + " -- " + + discoveryFlaw.getLocalizedMessage()); + } + + + if (discoveryFlaw instanceof LogConfigurationException) { + throw (LogConfigurationException) discoveryFlaw; + } + else { + throw new LogConfigurationException(discoveryFlaw); + } + + } + + + /** + * Report a problem loading the log adapter, then return + * a LogConfigurationException. + * <p> + * There are two possible reasons why we successfully loaded the + * specified log adapter class then failed to cast it to a Log object: + * <ol> + * <li>the specific class just doesn't implement the Log interface + * (user screwed up), or + * <li> the specified class has bound to a Log class loaded by some other + * classloader; [EMAIL PROTECTED] cannot be cast to [EMAIL PROTECTED] + * </ol> + * <p> + * Here we try to figure out which case has occurred so we can give the + * user some reasonable feedback. + * + * @param logClass is the adapter class we successfully loaded (but which + * could not be cast to type logInterface). Cannot be <code>null</code>. + * @param cause is the <code>Throwable</code> to wrap. + * + * @return <code>LogConfigurationException</code> that wraps + * <code>cause</code> and includes a diagnostic message. + */ + private LogConfigurationException reportInvalidLogAdapter(Class logClass, + Throwable cause) { + + Class interfaces[] = logClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + if (LOG_INTERFACE.equals(interfaces[i].getName())) { + + if (isDiagnosticsEnabled()) { + + try { + // Need to load the log interface so we know its + // classloader for diagnostics + Class logInterface = null; + ClassLoader cl = getClassLoader(this.getClass()); + if (cl == null) { + // we are probably in Java 1.1, but may also be + // running in some sort of embedded system.. + logInterface = loadClass(LOG_INTERFACE); + } else { + // normal situation + logInterface = cl.loadClass(LOG_INTERFACE); + } + + ClassLoader logInterfaceClassLoader = getClassLoader(logInterface); + ClassLoader logAdapterClassLoader = getClassLoader(logClass); + Class logAdapterInterface = interfaces[i]; + ClassLoader logAdapterInterfaceClassLoader = getClassLoader(logAdapterInterface); + logDiagnostic( + "Class " + logClass.getName() + + " was found in classloader " + + objectId(logAdapterClassLoader) + + " but it implements the Log interface as loaded" + + " from classloader " + + objectId(logAdapterInterfaceClassLoader) + + " not the one loaded by this class's classloader " + + objectId(logInterfaceClassLoader)); + } catch (Throwable t) { + ; + } + } + + return new LogConfigurationException + ("Invalid class loader hierarchy. " + + "You have more than one version of '" + + LOG_INTERFACE + "' visible, which is " + + "not allowed.", cause); + } + } + + return new LogConfigurationException + ("Class " + logClassName + " does not implement '" + + LOG_INTERFACE + "'.", cause); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [EMAIL PROTECTED] For additional commands, e-mail: [EMAIL PROTECTED]