diff --git a/proposal/myrmidon/build.xml b/proposal/myrmidon/build.xml index d95be41e3..8aafc6d8f 100644 --- a/proposal/myrmidon/build.xml +++ b/proposal/myrmidon/build.xml @@ -67,6 +67,7 @@ Legal: + @@ -544,6 +545,9 @@ Legal: + + + + @@ -602,6 +607,56 @@ Legal: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + fork="${test.fork}" failureProperty="test.failed"> diff --git a/proposal/myrmidon/docs/todo.html b/proposal/myrmidon/docs/todo.html index 14a09c053..9fc326c81 100644 --- a/proposal/myrmidon/docs/todo.html +++ b/proposal/myrmidon/docs/todo.html @@ -837,23 +837,12 @@ public class MyrmidonSecurityManager <socket> conditions to an antlib. Need to resolve how these will be passed a logger. -
  • Make the - <uptodate> task a condition, and move to - an antlib. -
  • -
  • Split up - <is-set> condition into is-set and is-true conditions. -
  • Allow the <if> task to take any condition implementation.
  • Add an else block to the <if> task.
  • -
  • Split the - <available> condition into separate conditions - that test for the availability of a class, or a resource. -
  • Move crimson.jar to bin/lib in the distribution, @@ -863,6 +852,19 @@ public class MyrmidonSecurityManager
  • Add a --type command-line option, to allow the project builder to be manually selected.
  • +
  • Change ProjectBuilder + and Embeddor to throw something more + specialised than Exception. +
  • +
  • Change DefaultClassLoaderManager to handle + directories as part of a library classpath. +
  • +
  • <condition> should set the property + value to false when the condition is false.
  • +
  • Split the <uptodate> condition into + a condition that checks against a single target file, + and one which checks using a destdir/mapper.
  • +
  • Add a task to unset a property.
  • Unit tests.
  • diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/DefaultClassLoaderManager.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/DefaultClassLoaderManager.java index e72328a0c..338884471 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/DefaultClassLoaderManager.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/DefaultClassLoaderManager.java @@ -8,19 +8,17 @@ package org.apache.myrmidon.components.classloader; import java.io.File; -import java.net.JarURLConnection; import java.net.MalformedURLException; import java.net.URL; -import java.net.URLClassLoader; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.List; +import java.util.Set; +import java.util.HashSet; import java.util.jar.Manifest; +import java.util.jar.JarFile; import org.apache.avalon.excalibur.extension.Extension; import org.apache.avalon.excalibur.extension.OptionalPackage; -import org.apache.avalon.excalibur.extension.PackageManager; import org.apache.avalon.excalibur.i18n.ResourceManager; import org.apache.avalon.excalibur.i18n.Resources; import org.apache.avalon.framework.activity.Initializable; @@ -28,8 +26,8 @@ import org.apache.avalon.framework.logger.AbstractLogEnabled; import org.apache.avalon.framework.service.ServiceException; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; -import org.apache.myrmidon.interfaces.classloader.ClassLoaderManager; import org.apache.myrmidon.interfaces.classloader.ClassLoaderException; +import org.apache.myrmidon.interfaces.classloader.ClassLoaderManager; import org.apache.myrmidon.interfaces.deployer.DeploymentException; import org.apache.myrmidon.interfaces.extensions.ExtensionManager; import org.apache.tools.todo.types.PathUtil; @@ -38,6 +36,7 @@ import org.apache.tools.todo.types.PathUtil; * A default implementation of a ClassLoader manager. * * @author Adam Murdoch + * @version $Revision$ $Date$ */ public class DefaultClassLoaderManager extends AbstractLogEnabled @@ -47,13 +46,22 @@ public class DefaultClassLoaderManager ResourceManager.getPackageResources( DefaultClassLoaderManager.class ); /** - * Map from File to the ClassLoader for that file. + * Map from File/ArrayList to the ClassLoader for that file/files. */ - private final Map m_fileDeployers = new HashMap(); + private final Map m_classLoaders = new HashMap(); - private PackageManager m_packageManager; + private ExtensionManager m_extensionManager; private ClassLoader m_commonClassLoader; + public DefaultClassLoaderManager() + { + } + + public DefaultClassLoaderManager( final ClassLoader commonClassLoader ) + { + m_commonClassLoader = commonClassLoader; + } + public void initialize() throws Exception { if( null == m_commonClassLoader ) @@ -68,18 +76,7 @@ public class DefaultClassLoaderManager public void service( final ServiceManager serviceManager ) throws ServiceException { - final ExtensionManager extensionManager = - (ExtensionManager)serviceManager.lookup( ExtensionManager.ROLE ); - m_packageManager = new PackageManager( extensionManager ); - } - - /** - * Sets the ClassLoader to use as the parent for all classloaders - * created by this ClassLoader manager. - */ - public void setCommonClassLoader( final ClassLoader classLoader ) - { - m_commonClassLoader = classLoader; + m_extensionManager = (ExtensionManager)serviceManager.lookup( ExtensionManager.ROLE ); } /** @@ -94,9 +91,27 @@ public class DefaultClassLoaderManager /** * Creates a class loader for a Jar file. */ - public ClassLoader createClassLoader( final File file ) throws ClassLoaderException + public ClassLoader getClassLoader( final File file ) throws ClassLoaderException { - return createClassLoader( new File[] { file } ); + try + { + final File canonFile = file.getCanonicalFile(); + + // Check for cached classloader, creating it if required + ClassLoader loader = (ClassLoader)m_classLoaders.get( canonFile ); + if( loader == null ) + { + checkFile( canonFile ); + final OptionalPackage optionalPackage = toOptionalPackage( canonFile ); + loader = buildClassLoader( optionalPackage, new HashSet() ); + } + return loader; + } + catch( final Exception e ) + { + final String message = REZ.getString( "create-classloader-for-file.error", file ); + throw new ClassLoaderException( message, e ); + } } /** @@ -106,113 +121,165 @@ public class DefaultClassLoaderManager { try { - // Build a list of canonical file names - final ArrayList canonFiles = new ArrayList( files.length ); - for( int i = 0; i < files.length; i++ ) + if( files == null || files.length == 0 ) { - canonFiles.add( files[ i ].getCanonicalFile() ); + return m_commonClassLoader; } - // Locate cached classloader, creating it if necessary - ClassLoader classLoader = (ClassLoader)m_fileDeployers.get( canonFiles ); - if( classLoader == null ) + // Build a list of optional packages for the files + final OptionalPackage[] packages = new OptionalPackage[ files.length ]; + for( int i = 0; i < files.length; i++ ) { - classLoader = buildClassLoader( canonFiles ); - m_fileDeployers.put( canonFiles, classLoader ); + final File canonFile = files[ i ].getCanonicalFile(); + checkFile( canonFile ); + packages[ i ] = toOptionalPackage( canonFile ); } - return classLoader; + + // Build the classloaders for the required extensions + final ClassLoader[] parentClassLoaders = buildParentClassLoaders( packages, new HashSet() ); + + // Build the classloader + final URL[] urls = buildClasspath( files ); + return new MultiParentURLClassLoader( urls, parentClassLoaders ); } catch( final Exception e ) { final String fileNames = PathUtil.formatPath( files ); - final String message = REZ.getString( "create-classloader-for-file.error", fileNames ); + final String message = REZ.getString( "create-classloader-for-files.error", fileNames ); throw new ClassLoaderException( message, e ); } } /** - * Builds the classloader for a set of files. + * Builds the classloader for an optional package. */ - private ClassLoader buildClassLoader( final ArrayList files ) + private ClassLoader buildClassLoader( final OptionalPackage pkg, + final Set pending ) throws Exception { - final ArrayList allFiles = new ArrayList( files ); - final int count = files.size(); - for( int i = 0; i < count; i++ ) + final File jarFile = pkg.getFile(); + + // Check for cached classloader + ClassLoader classLoader = (ClassLoader)m_classLoaders.get( jarFile ); + if( classLoader != null ) { - final File file = (File)files.get(i ); - checkFile( file ); - getOptionalPackagesFor( file, allFiles ); + return classLoader; } - final URL[] urls = buildClasspath( allFiles ); - return new URLClassLoader( urls, m_commonClassLoader ); + // Check for cyclic dependency + if( pending.contains( jarFile ) ) + { + final String message = REZ.getString( "dependency-cycle.error", jarFile ); + throw new Exception( message ); + } + pending.add( jarFile ); + + // Build the classloaders for the extensions required by this optional + // package + final ClassLoader[] parentClassLoaders = + buildParentClassLoaders( new OptionalPackage[] { pkg }, pending ); + + // Create and cache the classloader + final URL[] urls = { jarFile.toURL() }; + classLoader = new MultiParentURLClassLoader( urls, parentClassLoaders ); + m_classLoaders.put( jarFile, classLoader ); + pending.remove( jarFile ); + return classLoader; + } + + /** + * Builds the parent classloaders for a set of optional packages. That is, + * the classloaders for all of the extensions required by the given set + * of optional packages. + */ + private ClassLoader[] buildParentClassLoaders( final OptionalPackage[] packages, + final Set pending ) + throws Exception + { + final ArrayList classLoaders = new ArrayList(); + + // Include the common class loader + classLoaders.add( m_commonClassLoader ); + + // Build the classloader for each optional package, filtering out duplicates + for( int i = 0; i < packages.length; i++ ) + { + final OptionalPackage optionalPackage = packages[ i ]; + + // Locate the dependencies for this jar file + final OptionalPackage[] requiredPackages = getOptionalPackagesFor( optionalPackage ); + + // Build the classloader for the package + for( int j = 0; j < requiredPackages.length; j++ ) + { + final OptionalPackage requiredPackage = requiredPackages[j ]; + final ClassLoader classLoader = buildClassLoader( requiredPackage, pending ); + if( ! classLoaders.contains( classLoader ) ) + { + classLoaders.add( classLoader ); + } + } + } + + return (ClassLoader[])classLoaders.toArray( new ClassLoader[classLoaders.size() ] ); } /** * Assembles a set of files into a URL classpath. */ - private URL[] buildClasspath( final ArrayList files ) + private URL[] buildClasspath( final File[] files ) throws MalformedURLException { - final URL[] urls = new URL[ files.size() ]; - final int count = files.size(); - for( int i = 0; i < count; i++ ) + final URL[] urls = new URL[ files.length ]; + for( int i = 0; i < files.length; i++ ) { - final File file = (File)files.get( i ); - urls[ i ] = file.toURL(); + urls[ i ] = files[i ].toURL(); } return urls; } /** - * Retrieve the files for the optional packages required by - * the specified typeLibrary jar. + * Builds an OptionalPackage for a Jar file. * - * @param jarFile the typeLibrary - * @param packages used to return the files that need to be added to ClassLoader. + * @param file the jar. */ - private void getOptionalPackagesFor( final File jarFile, final List packages ) + private OptionalPackage toOptionalPackage( final File file ) throws Exception { - final URL url = new URL( "jar:" + jarFile.getCanonicalFile().toURL() + "!/" ); - final JarURLConnection connection = (JarURLConnection)url.openConnection(); - final Manifest manifest = connection.getManifest(); - final Extension[] available = Extension.getAvailable( manifest ); + // Determine the extensions required by this file + final JarFile jarFile = new JarFile( file ); + final Manifest manifest = jarFile.getManifest(); final Extension[] required = Extension.getRequired( manifest ); + return new OptionalPackage( file, new Extension[0], required ); + } - if( getLogger().isDebugEnabled() ) - { - final String message1 = - REZ.getString( "available-extensions.notice", Arrays.asList( available ) ); - getLogger().debug( message1 ); - final String message2 = - REZ.getString( "required-extensions.notice", Arrays.asList( required ) ); - getLogger().debug( message2 ); - } - - final ArrayList dependencies = new ArrayList(); - final ArrayList unsatisfied = new ArrayList(); - - m_packageManager.scanDependencies( required, - available, - dependencies, - unsatisfied ); - - if( 0 != unsatisfied.size() ) + /** + * Locates the optional packages required by an optional package. + */ + private OptionalPackage[] getOptionalPackagesFor( final OptionalPackage pkg ) + throws Exception + { + // Locate the optional packages that provide the required extesions + final Extension[] required = pkg.getRequiredExtensions(); + final ArrayList packages = new ArrayList(); + for( int i = 0; i < required.length; i++ ) { - final String message = - REZ.getString( "unsatisfied.extensions.error", new Integer( unsatisfied.size() ) ); - throw new Exception( message ); + final Extension extension = required[i ]; + final OptionalPackage optionalPackage = m_extensionManager.getOptionalPackage( extension ); + if( optionalPackage == null ) + { + final String message = + REZ.getString( "unsatisfied.extension.error", + pkg.getFile(), + extension.getExtensionName(), + extension.getSpecificationVersion() ); + throw new Exception( message ); + } + packages.add( optionalPackage ); } - final int count = dependencies.size(); - for( int i = 0; i < count; i++ ) - { - final OptionalPackage optionalPackage = (OptionalPackage)dependencies.get(i ); - packages.add( optionalPackage.getFile() ); - } + return (OptionalPackage[])packages.toArray( new OptionalPackage[packages.size() ] ); } /** diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/MultiParentURLClassLoader.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/MultiParentURLClassLoader.java new file mode 100644 index 000000000..35444946e --- /dev/null +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/MultiParentURLClassLoader.java @@ -0,0 +1,141 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.classloader; + +import java.io.IOException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.Set; +import java.util.HashSet; + +/** + * A URLClassLoader with more than one parent. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class MultiParentURLClassLoader + extends URLClassLoader +{ + private final ClassLoader[] m_parents; + + /** + * Constructs a new URLClassLoader for the given URLs. + * + * @param urls the URLs from which to load classes and resources + * @param parents the parent class loaderer for delegation + */ + public MultiParentURLClassLoader( final URL[] urls, final ClassLoader[] parents ) + { + super( urls ); + m_parents = parents; + } + + /** + * Finds a class. + * + * @param name the name of the class + * @return the resulting class + * @exception ClassNotFoundException if the class could not be found + */ + protected Class findClass( final String name ) + throws ClassNotFoundException + { + // Try the parent classloaders first + for( int i = 0; i < m_parents.length; i++ ) + { + try + { + final ClassLoader parent = m_parents[ i ]; + return parent.loadClass( name ); + } + catch( ClassNotFoundException e ) + { + // Ignore - continue to the next ClassLoader + } + } + + // Now this classloader + return super.findClass( name ); + } + + /** + * Finds a resource. + * + * @param name the name of the resource + * @return a URL for the resource, or null + * if the resource could not be found. + */ + public URL findResource( final String name ) + { + // Try the parent classloaders first + for( int i = 0; i < m_parents.length; i++ ) + { + final ClassLoader parent = m_parents[ i ]; + final URL resource = parent.getResource( name ); + if( resource != null ) + { + return resource; + } + } + + // Now this classloader + return super.findResource( name ); + } + + /** + * Returns an Enumeration of URLs representing all of the resources + * having the specified name. + * + * @param name the resource name + * @throws IOException if an I/O exception occurs + * @return an Enumeration of URLs + */ + public Enumeration findResources( final String name ) throws IOException + { + // Need to filter out duplicate resources + final ArrayList urls = new ArrayList(); + final Set urlSet = new HashSet(); + + // Gather the resources from the parent classloaders + for( int i = 0; i < m_parents.length; i++ ) + { + final ClassLoader parent = m_parents[ i ]; + final Enumeration enum = parent.getResources( name ); + addUrls( enum, urls, urlSet ); + } + + // Gather the resources from this classloader + addUrls( super.findResources( name ), urls, urlSet ); + + return Collections.enumeration( urls ); + } + + /** + * Adds those URLs not already present. + */ + private void addUrls( final Enumeration enum, + final List urls, + final Set urlSet ) + { + while( enum.hasMoreElements() ) + { + final URL url = (URL)enum.nextElement(); + final String urlStr = url.toExternalForm(); + if( !urlSet.contains( urlStr ) ) + { + urls.add( url ); + urlSet.add( urlStr ); + } + } + } +} diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/Resources.properties index dfefa825e..6cb819340 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/classloader/Resources.properties @@ -1,6 +1,5 @@ -create-classloader-for-file.error=Could not create ClassLoader for files: {0}. -available-extensions.notice=The list of available extensions for type library includes; {0} -required-extensions.notice=The list of required extensions for type library includes; {0} -unsatisfied.extensions.error=Missing {0} extensions for type library. -no-file.error=Could not find type library "{0}". -file-is-dir.error=Type library "{0}" is a directory. +create-classloader-for-file.error=Could not create a ClassLoader for "{0}". +create-classloader-for-files.error=Could not create a ClassLoader for {0}. +unsatisfied.extension.error=Library "{0}" requires unknown extension "{1}" ( version {2}). +no-file.error=Could not find library "{0}". +file-is-dir.error=Library "{0}" is a directory. diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/deployer/DefaultDeployer.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/deployer/DefaultDeployer.java index 2a0fcd4b5..f41e008b3 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/deployer/DefaultDeployer.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/deployer/DefaultDeployer.java @@ -108,7 +108,7 @@ public class DefaultDeployer { try { - final ClassLoader classLoader = m_classLoaderManager.createClassLoader( file ); + final ClassLoader classLoader = m_classLoaderManager.getClassLoader( file ); return createDeployment( classLoader, file.toURL() ); } catch( final Exception e ) diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/DefaultEmbeddor.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/DefaultEmbeddor.java index 2e89bd5a5..583312d2b 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/DefaultEmbeddor.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/DefaultEmbeddor.java @@ -71,10 +71,7 @@ public class DefaultEmbeddor private List m_components = new ArrayList(); private DefaultServiceManager m_serviceManager = new DefaultServiceManager(); private Parameters m_parameters; - private Parameters m_defaults; - private File m_homeDir; - private File m_taskLibDir; private static final String MYRMIDON_HOME = "myrmidon.home"; /** @@ -163,9 +160,6 @@ public class DefaultEmbeddor public void initialize() throws Exception { - // setup default properties - m_defaults = createDefaultParameters(); - // setup the root components setupComponents(); @@ -183,9 +177,6 @@ public class DefaultEmbeddor m_workspaceServiceManager = new MultiSourceServiceManager(); m_workspaceServiceManager.add( projServiceManager ); m_workspaceServiceManager.add( m_serviceManager ); - - // setup - setupFiles(); } public void start() @@ -198,7 +189,8 @@ public class DefaultEmbeddor // Deploy all type libraries in the lib directory final ExtensionFileFilter filter = new ExtensionFileFilter( ".atl" ); - deployFromDirectory( m_deployer, m_taskLibDir, filter ); + final File taskLibDir = new File( m_parameters.getParameter( "myrmidon.lib.path" ) ); + deployFromDirectory( m_deployer, taskLibDir, filter ); } /** @@ -230,26 +222,6 @@ public class DefaultEmbeddor m_deployer = null; m_serviceManager = null; m_parameters = null; - m_defaults = null; - m_homeDir = null; - m_taskLibDir = null; - } - - /** - * Create default properties which includes default names of all components. - * Overide this in sub-classes to change values. - * - * @return the Parameters - */ - private Parameters createDefaultParameters() - { - final Parameters defaults = new Parameters(); - - //create all the default properties for files/directories - defaults.setParameter( "myrmidon.bin.path", "bin" ); - defaults.setParameter( "myrmidon.lib.path", "lib" ); - - return defaults; } /** @@ -296,80 +268,6 @@ public class DefaultEmbeddor return component; } - /** - * Setup all the files attributes. - */ - private void setupFiles() - throws Exception - { - String filepath = null; - - filepath = getParameter( MYRMIDON_HOME ); - m_homeDir = ( new File( filepath ) ).getAbsoluteFile(); - checkDirectory( m_homeDir, "home-dir.name" ); - - filepath = getParameter( "myrmidon.lib.path" ); - m_taskLibDir = resolveDirectory( filepath, "task-lib-dir.name" ); - } - - /** - * Retrieve value of named property. - * First access passed in properties and then the default properties. - * - * @param name the name of property - * @return the value of property or null - */ - private String getParameter( final String name ) - { - String value = m_parameters.getParameter( name, null ); - - if( null == value ) - { - value = m_defaults.getParameter( name, null ); - } - - return value; - } - - /** - * Resolve a directory relative to another base directory. - * - * @param dir the base directory - * @param name the relative directory - * @return the created File - * @exception Exception if an error occurs - */ - private File resolveDirectory( final String dir, final String name ) - throws Exception - { - final File file = FileUtil.resolveFile( m_homeDir, dir ); - checkDirectory( file, name ); - return file; - } - - /** - * Verify file is a directory else throw an exception. - * - * @param file the File - * @param name the name of file type (used in error messages) - */ - private void checkDirectory( final File file, final String name ) - throws Exception - { - if( !file.exists() ) - { - final String nameStr = REZ.getString( name ); - final String message = REZ.getString( "file-no-exist.error", nameStr, file ); - throw new Exception( message ); - } - else if( !file.isDirectory() ) - { - final String nameStr = REZ.getString( name ); - final String message = REZ.getString( "file-not-dir.error", nameStr, file ); - throw new Exception( message ); - } - } - /** * Create a component that implements an interface. * diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/Resources.properties index ad5d554af..31ea37a88 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/embeddor/Resources.properties @@ -1,10 +1,6 @@ -file-no-exist.error={0} ({1}) does not exist. -file-not-dir.error={0} ({1}) is not a directory. bad-type.error=Object {0} is not an instance of {1}. bad-ctor.error=Non-public constructor for {0} {1}. no-instantiate.error=Error instantiating class for {0} {1}. no-class.error=Could not find the class for {0} ({1}). bad-filename.error=Unable to retrieve filename for file {0}. -home-dir.name=Myrmidon home directory -task-lib-dir.name=Task library directory create-project.error=Could not load the project definition from {0}. \ No newline at end of file diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/components/extensions/DefaultExtensionManager.java b/proposal/myrmidon/src/java/org/apache/myrmidon/components/extensions/DefaultExtensionManager.java index c2ca09cb1..11e1a598f 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/components/extensions/DefaultExtensionManager.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/components/extensions/DefaultExtensionManager.java @@ -50,7 +50,6 @@ public class DefaultExtensionManager File.separator + "lib" + File.separator + "tools.jar"; private Logger m_logger; - private String m_path; public DefaultExtensionManager() @@ -58,6 +57,11 @@ public class DefaultExtensionManager super( new File[ 0 ] ); } + public DefaultExtensionManager( final File[] path ) + { + super( path ); + } + public void enableLogging( final Logger logger ) { m_logger = logger; @@ -66,16 +70,13 @@ public class DefaultExtensionManager public void parameterize( final Parameters parameters ) throws ParameterException { - final String phoenixHome = parameters.getParameter( "myrmidon.home" ); - final String defaultExtPath = phoenixHome + File.separator + "ext"; - m_path = parameters.getParameter( "myrmidon.ext.path", defaultExtPath ); + m_path = parameters.getParameter( "myrmidon.ext.path" ); } public void initialize() throws Exception { - final String[] pathElements = StringUtil.split( m_path, "|" ); - + final String[] pathElements = StringUtil.split( m_path, File.pathSeparator ); final File[] dirs = new File[ pathElements.length ]; for( int i = 0; i < dirs.length; i++ ) { @@ -86,6 +87,7 @@ public class DefaultExtensionManager scanPath(); + // Add the JVM's tools.jar as an extension final Extension extension = createToolsExtension(); final File jar = getToolsJar(); final Extension[] available = new Extension[]{extension}; @@ -99,6 +101,23 @@ public class DefaultExtensionManager clearCache(); } + /** + * Locates the optional package which best matches a required extension. + * + * @param extension the extension to locate an optional package + * @return the optional package, or null if not found. + */ + public OptionalPackage getOptionalPackage( final Extension extension ) + { + final OptionalPackage[] packages = getOptionalPackages( extension ); + + if( null == packages || 0 == packages.length ) return null; + + //TODO: Use heurisitic to find which is best package + + return packages[ 0 ]; + } + protected void debug( final String message ) { m_logger.debug( message ); diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/CLIMain.java b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/CLIMain.java index 764123bec..f50b9fc55 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/CLIMain.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/CLIMain.java @@ -277,10 +277,10 @@ public class CLIMain break; case LISTENER_OPT: - m_embedded.setListener( option.getArgument() ); + m_embedded.setProjectListener( option.getArgument() ); break; case NO_PREFIX_OPT: - m_embedded.setListener( "noprefix" ); + m_embedded.setProjectListener( "noprefix" ); break; case DEFINE_OPT: @@ -310,7 +310,7 @@ public class CLIMain try { // Set system properties set up by launcher - m_embedded.setEmbeddorProperty( "myrmidon.home", properties.get( "myrmidon.home" ) ); + m_embedded.setHomeDirectory( (File)properties.get( "myrmidon.home" ) ); // Command line if( !parseCommandLineOptions( args ) ) diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/EmbeddedAnt.java b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/EmbeddedAnt.java index 895905db1..01d2ba06c 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/EmbeddedAnt.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/EmbeddedAnt.java @@ -8,8 +8,10 @@ package org.apache.myrmidon.frontends; import java.io.File; +import java.util.ArrayList; import org.apache.avalon.excalibur.i18n.ResourceManager; import org.apache.avalon.excalibur.i18n.Resources; +import org.apache.avalon.excalibur.io.FileUtil; import org.apache.avalon.framework.activity.Disposable; import org.apache.avalon.framework.activity.Initializable; import org.apache.avalon.framework.activity.Startable; @@ -47,12 +49,25 @@ public class EmbeddedAnt private String m_projectFile = "build.ant"; private Project m_project; private String m_listenerName = "default"; - private ProjectListener m_listener; + private ArrayList m_listeners = new ArrayList(); private Parameters m_workspaceProps = new Parameters(); private Parameters m_builderProps = new Parameters(); private Parameters m_embeddorParameters = new Parameters(); private ClassLoader m_sharedClassLoader; private Embeddor m_embeddor; + private File m_homeDir; + + /** + * Sets the Myrmidon home directory. Default is to use the current + * directory. + * + * @todo Autodetect myrmidon home, rather than using current directory + * as the default (which is a dud default). + */ + public void setHomeDirectory( final File homeDir ) + { + m_homeDir = homeDir.getAbsoluteFile(); + } /** * Sets the project file to execute. Default is 'build.ant'. @@ -75,21 +90,20 @@ public class EmbeddedAnt } /** - * Sets the name of the project listener to use. + * Sets the name of the project listener to use. Set to null to disable + * the project listener. */ - public void setListener( final String listener ) + public void setProjectListener( final String listener ) { m_listenerName = listener; - m_listener = null; } /** - * Sets the project listener to use. + * Adds a project listener. */ - public void setListener( final ProjectListener listener ) + public void addProjectListener( final ProjectListener listener ) { - m_listenerName = null; - m_listener = listener; + m_listeners.add( listener ); } /** @@ -99,7 +113,7 @@ public class EmbeddedAnt public void setWorkspaceProperty( final String name, final Object value ) { // TODO - Make properties Objects, not Strings - m_workspaceProps.setParameter( name, (String)value ); + m_workspaceProps.setParameter( name, value.toString() ); } /** @@ -109,7 +123,7 @@ public class EmbeddedAnt public void setBuilderProperty( final String name, final Object value ) { // TODO - Make properties Objects, not Strings - m_builderProps.setParameter( name, (String)value ); + m_builderProps.setParameter( name, value.toString() ); } /** @@ -145,14 +159,13 @@ public class EmbeddedAnt checkHomeDir(); - // Prepare the embeddor, project listener and project model + // Prepare the embeddor, and project model final Embeddor embeddor = prepareEmbeddor(); - final ProjectListener listener = prepareListener( embeddor ); final Project project = prepareProjectModel( embeddor ); // Create a new workspace final Workspace workspace = embeddor.createWorkspace( m_workspaceProps ); - workspace.addProjectListener( listener ); + prepareListeners( embeddor, workspace ); //execute the project executeTargets( workspace, project, targets ); @@ -181,7 +194,7 @@ public class EmbeddedAnt { m_embeddor = null; m_project = null; - m_listener = null; + m_listeners.clear(); } } @@ -213,19 +226,26 @@ public class EmbeddedAnt */ private void checkHomeDir() throws Exception { - final String home = m_embeddorParameters.getParameter( "myrmidon.home" ); - final File homeDir = ( new File( home ) ).getAbsoluteFile(); - if( !homeDir.isDirectory() ) + if( m_homeDir == null ) { - final String message = REZ.getString( "home-not-dir.error", homeDir ); - throw new Exception( message ); + m_homeDir = new File( "." ).getAbsoluteFile(); } + checkDirectory( m_homeDir, "home-dir.name" ); + m_embeddorParameters.setParameter( "myrmidon.home", m_homeDir.getAbsolutePath() ); if( getLogger().isInfoEnabled() ) { - final String message = REZ.getString( "homedir.notice", homeDir ); + final String message = REZ.getString( "homedir.notice", m_homeDir ); getLogger().info( message ); } + + String path = m_embeddorParameters.getParameter( "myrmidon.lib.path", "lib" ); + File dir = resolveDirectory( m_homeDir, path, "task-lib-dir.name" ); + m_embeddorParameters.setParameter( "myrmidon.lib.path", dir.getAbsolutePath() ); + + path = m_embeddorParameters.getParameter( "myrmidon.ext.path", "ext" ); + dir = resolveDirectory( m_homeDir, path, "ext-dir.name" ); + m_embeddorParameters.setParameter( "myrmidon.ext.path", dir.getAbsolutePath() ); } /** @@ -267,14 +287,21 @@ public class EmbeddedAnt /** * Prepares and returns the project listener to use. */ - private ProjectListener prepareListener( final Embeddor embeddor ) + private void prepareListeners( final Embeddor embeddor, + final Workspace workspace ) throws Exception { - if( m_listener == null ) + if( m_listenerName != null ) + { + final ProjectListener listener = embeddor.createListener( m_listenerName ); + workspace.addProjectListener( listener ); + } + final int count = m_listeners.size(); + for( int i = 0; i < count; i++ ) { - m_listener = embeddor.createListener( m_listenerName ); + final ProjectListener listener = (ProjectListener)m_listeners.get(i ); + workspace.addProjectListener( listener ); } - return m_listener; } /** @@ -310,4 +337,36 @@ public class EmbeddedAnt return projectFile; } + + /** + * Resolve a directory relative to another base directory. + */ + private File resolveDirectory( final File baseDir, final String dir, final String name ) + throws Exception + { + final File file = FileUtil.resolveFile( baseDir, dir ); + checkDirectory( file, name ); + return file; + } + + /** + * Verify file is a directory else throw an exception. + */ + private void checkDirectory( final File file, final String name ) + throws Exception + { + if( !file.exists() ) + { + final String nameStr = REZ.getString( name ); + final String message = REZ.getString( "file-no-exist.error", nameStr, file ); + throw new Exception( message ); + } + else if( !file.isDirectory() ) + { + final String nameStr = REZ.getString( name ); + final String message = REZ.getString( "file-not-dir.error", nameStr, file ); + throw new Exception( message ); + } + } + } diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/Resources.properties b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/Resources.properties index eca021877..553969b9a 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/Resources.properties +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/frontends/Resources.properties @@ -16,7 +16,8 @@ define.opt=Define a property (ie -Dfoo=var). build.opt=Define a builder parameter (ie -Bfoo=var). dry-run.opt=Do not execute tasks - just print them out. -home-not-dir.error=myrmidon-home ({0}) is not a directory. +file-no-exist.error={0} {1} does not exist. +file-not-dir.error={0} {1} is not a directory. bad-file.error=File {0} is not a file or doesn't exist. bad-loglevel.error=Unknown log level - {0}. build-failed.error=BUILD FAILED. @@ -26,3 +27,6 @@ repeat.notice=Continue ? (Enter no to stop) homedir.notice=Ant Home Directory: {0} buildfile.notice=Ant Build File: {0} +home-dir.name=Ant home directory +task-lib-dir.name=Task library directory +ext-dir.name=Extension library directory diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/classloader/ClassLoaderManager.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/classloader/ClassLoaderManager.java index ca0c26154..2631b913b 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/classloader/ClassLoaderManager.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/classloader/ClassLoaderManager.java @@ -13,6 +13,7 @@ import java.io.File; * Manages a classloader hierarchy. * * @author Adam Murdoch + * @version $Revision$ $Date$ */ public interface ClassLoaderManager { @@ -20,15 +21,22 @@ public interface ClassLoaderManager String ROLE = ClassLoaderManager.class.getName(); /** - * Builds the ClassLoader for a Jar file, resolving dependencies. + * Returns the ClassLoader for a Jar file. The ClassLoader is created, + * if necessary. The ClassLoader's parent will include the common + * ClassLoader, along with any extensions required by the Jar file. + * It is guaranteed that each extension will appear at most once in the + * ClassLoader hierarchy, so that classes from the extension can be + * shared across the ClassLoaders returned by this method. + * * @param jar the jar file containing the classes to load - * @return the created classloader + * @return the classloader * @throws ClassLoaderException on error */ - ClassLoader createClassLoader( File jar ) throws ClassLoaderException; + ClassLoader getClassLoader( File jar ) throws ClassLoaderException; /** - * Builds the ClassLoader for a set of files, resolving dependencies. + * Creates a ClassLoader for a set of files. See {@link #getClassLoader} + * for details. * * @param jars The Jar/zip files to create the classloader for. Use null * or an empty array to use the common classloader. @@ -38,9 +46,10 @@ public interface ClassLoaderManager ClassLoader createClassLoader( File[] jars ) throws ClassLoaderException; /** - * Provides the common ClassLoader, which is the parent of all classloaders + * Returns the common ClassLoader, which is the parent of all classloaders * built by this ClassLoaderManager. - * @return the common ClassLoader + * + * @return the common ClassLoader. */ ClassLoader getCommonClassLoader(); } diff --git a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/extensions/ExtensionManager.java b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/extensions/ExtensionManager.java index 7f616d944..5545da4be 100644 --- a/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/extensions/ExtensionManager.java +++ b/proposal/myrmidon/src/java/org/apache/myrmidon/interfaces/extensions/ExtensionManager.java @@ -7,17 +7,25 @@ */ package org.apache.myrmidon.interfaces.extensions; -import org.apache.avalon.excalibur.extension.PackageRepository; +import org.apache.avalon.excalibur.extension.Extension; +import org.apache.avalon.excalibur.extension.OptionalPackage; /** - * PackageRepository + * Maintains a set of optional packages. * * @author Peter Donald * @version $Revision$ $Date$ */ public interface ExtensionManager - extends PackageRepository { /** Role name for this interface. */ String ROLE = ExtensionManager.class.getName(); + + /** + * Locates the optional package which best matches a required extension. + * + * @param extension the extension to locate an optional package + * @return the optional package, or null if not found. + */ + public OptionalPackage getOptionalPackage( Extension extension ); } diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractMyrmidonTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractMyrmidonTest.java index ce0fb3738..490d8b253 100644 --- a/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractMyrmidonTest.java +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractMyrmidonTest.java @@ -29,28 +29,21 @@ public abstract class AbstractMyrmidonTest private final File m_baseDir; private Logger m_logger; - protected static final Resources getResourcesForTested( final Class clazz ) + public AbstractMyrmidonTest( final String name ) { - final Package pkg = clazz.getPackage(); - - String baseName; - if( null == pkg ) - { - final String name = clazz.getName(); - if( -1 == name.lastIndexOf( "." ) ) - { - baseName = ""; - } - else - { - baseName = name.substring( 0, name.lastIndexOf( "." ) ); - } - } - else - { - baseName = pkg.getName(); - } + super( name ); + final String baseDirProp = System.getProperty( "test.basedir" ); + m_baseDir = getCanonicalFile( new File( baseDirProp ) ); + final String packagePath = getPackageName( getClass() ).replace( '.', File.separatorChar ); + m_testBaseDir = getCanonicalFile( new File( m_baseDir, packagePath ) ); + } + /** + * Locates the error message resources for a class. + */ + protected static final Resources getResourcesForTested( final Class clazz ) + { + String baseName = getPackageName( clazz ); if( baseName.endsWith( ".test" ) ) { baseName = baseName.substring( 0, baseName.length() - 5 ); @@ -59,16 +52,29 @@ public abstract class AbstractMyrmidonTest return ResourceManager.getBaseResources( baseName + ".Resources", AbstractMyrmidonTest.class.getClassLoader() ); } - public AbstractMyrmidonTest( String name ) + /** + * Returns the name of the package containing a class. + * + * @return The . delimited package name, or an empty string if the class + * is in the default package. + */ + protected static String getPackageName( final Class clazz ) { - super( name ); - final String baseDirProp = System.getProperty( "test.basedir" ); - m_baseDir = getCanonicalFile( new File( baseDirProp ) ); - String packagePath = getClass().getName(); - int idx = packagePath.lastIndexOf( '.' ); - packagePath = packagePath.substring( 0, idx ); - packagePath = packagePath.replace( '.', File.separatorChar ); - m_testBaseDir = getCanonicalFile( new File( m_baseDir, packagePath ) ); + final Package pkg = clazz.getPackage(); + if( null != pkg ) + { + return pkg.getName(); + } + + final String name = clazz.getName(); + if( -1 == name.lastIndexOf( "." ) ) + { + return ""; + } + else + { + return name.substring( 0, name.lastIndexOf( "." ) ); + } } /** diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractProjectTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractProjectTest.java index e23c282a4..2eed07523 100644 --- a/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractProjectTest.java +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/AbstractProjectTest.java @@ -8,12 +8,7 @@ package org.apache.myrmidon; import java.io.File; -import org.apache.avalon.framework.logger.Logger; -import org.apache.avalon.framework.parameters.Parameters; -import org.apache.myrmidon.components.embeddor.DefaultEmbeddor; -import org.apache.myrmidon.interfaces.embeddor.Embeddor; -import org.apache.myrmidon.interfaces.model.Project; -import org.apache.myrmidon.interfaces.workspace.Workspace; +import org.apache.myrmidon.frontends.EmbeddedAnt; import org.apache.myrmidon.listeners.ProjectListener; /** @@ -25,50 +20,11 @@ import org.apache.myrmidon.listeners.ProjectListener; public class AbstractProjectTest extends AbstractMyrmidonTest { - private DefaultEmbeddor m_embeddor; - public AbstractProjectTest( final String name ) { super( name ); } - /** - * Tear-down the test. - */ - protected void tearDown() throws Exception - { - if( m_embeddor != null ) - { - m_embeddor.dispose(); - m_embeddor = null; - } - } - - /** - * Returns an embeddor which can be used to build and execute projects. - */ - protected Embeddor getEmbeddor() throws Exception - { - if( m_embeddor == null ) - { - // Need to set the context classloader - The default embeddor uses it - Thread.currentThread().setContextClassLoader( getClass().getClassLoader() ); - - final Logger logger = getLogger(); - m_embeddor = new DefaultEmbeddor(); - m_embeddor.enableLogging( logger ); - - final Parameters params = new Parameters(); - final File instDir = getInstallDirectory(); - params.setParameter( "myrmidon.home", instDir.getAbsolutePath() ); - m_embeddor.parameterize( params ); - m_embeddor.initialize(); - m_embeddor.start(); - } - - return m_embeddor; - } - /** * Executes a target in a project, and asserts that it fails with the * given error message. @@ -117,22 +73,27 @@ public class AbstractProjectTest throws Exception { // Create the project and workspace - final Embeddor embeddor = getEmbeddor(); - final Project project = embeddor.createProject( projectFile.getAbsolutePath(), null, null ); - final Workspace workspace = embeddor.createWorkspace( new Parameters() ); + final EmbeddedAnt embeddor = new EmbeddedAnt(); + embeddor.setHomeDirectory( getInstallDirectory() ); + embeddor.enableLogging( getLogger() ); + embeddor.setSharedClassLoader( getClass().getClassLoader() ); + embeddor.setProjectFile( projectFile.getAbsolutePath() ); + embeddor.setProjectListener( null ); // Add a listener to make sure all is good final TrackingProjectListener tracker = new TrackingProjectListener(); - workspace.addProjectListener( tracker ); + embeddor.addProjectListener( tracker ); // Add supplied listener if( listener != null ) { - workspace.addProjectListener( listener ); + embeddor.addProjectListener( listener ); } // Now execute the target - workspace.executeProject( project, targetName ); + embeddor.executeTargets( new String[] { targetName } ); + + embeddor.stop(); // Make sure all expected events were delivered tracker.assertComplete(); diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/AbstractComponentTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/AbstractComponentTest.java index 45ea88298..231f1a76a 100644 --- a/proposal/myrmidon/src/test/org/apache/myrmidon/components/AbstractComponentTest.java +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/AbstractComponentTest.java @@ -10,12 +10,16 @@ package org.apache.myrmidon.components; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.io.File; import org.apache.aut.converter.Converter; import org.apache.avalon.framework.logger.LogEnabled; import org.apache.avalon.framework.logger.Logger; import org.apache.avalon.framework.service.DefaultServiceManager; import org.apache.avalon.framework.service.ServiceManager; import org.apache.avalon.framework.service.Serviceable; +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.framework.parameters.Parameterizable; +import org.apache.avalon.framework.activity.Initializable; import org.apache.myrmidon.AbstractMyrmidonTest; import org.apache.myrmidon.components.classloader.DefaultClassLoaderManager; import org.apache.myrmidon.components.configurer.DefaultConfigurer; @@ -93,10 +97,9 @@ public abstract class AbstractComponentTest m_serviceManager.put( Executor.ROLE, component ); components.add( component ); - final DefaultClassLoaderManager classLoaderMgr = new DefaultClassLoaderManager(); - classLoaderMgr.setCommonClassLoader( getClass().getClassLoader() ); - m_serviceManager.put( ClassLoaderManager.ROLE, classLoaderMgr ); - components.add( classLoaderMgr ); + component = createComponent( ClassLoaderManager.ROLE, DefaultClassLoaderManager.class ); + m_serviceManager.put( ClassLoaderManager.ROLE, component ); + components.add( component ); component = createComponent( ExtensionManager.ROLE, DefaultExtensionManager.class ); m_serviceManager.put( ExtensionManager.ROLE, component ); @@ -132,6 +135,29 @@ public abstract class AbstractComponentTest } } + // Parameterise the components + final Parameters parameters = getParameters(); + for( Iterator iterator = components.iterator(); iterator.hasNext(); ) + { + Object obj = iterator.next(); + if( obj instanceof Parameterizable ) + { + final Parameterizable parameterizable = (Parameterizable)obj; + parameterizable.parameterize( parameters ); + } + } + + // Initialise the components + for( Iterator iterator = components.iterator(); iterator.hasNext(); ) + { + Object obj = iterator.next(); + if( obj instanceof Initializable ) + { + final Initializable initializable = (Initializable)obj; + initializable.initialize(); + } + } + // Register some standard roles // Add some core roles final RoleManager roleManager = (RoleManager)getServiceManager().lookup( RoleManager.ROLE ); @@ -144,12 +170,29 @@ public abstract class AbstractComponentTest } /** - * Creates an instance of a component. Sub-classes can override this + * Creates the parameters for the test. Sub-classes can override this + * method to set-up the parameters. + */ + protected Parameters getParameters() + { + final Parameters parameters = new Parameters(); + final String homeDir = getInstallDirectory().getAbsolutePath(); + parameters.setParameter( "myrmidon.home", homeDir ); + parameters.setParameter( "myrmidon.ext.path", homeDir + File.separatorChar + "ext" ); + return parameters; + } + + /** + * Creates an instance of a test component. Sub-classes can override this * method to add a particular implementation to the set of test components. */ protected Object createComponent( final String role, final Class defaultImpl ) throws Exception { + if( role.equals( ClassLoaderManager.ROLE ) ) + { + return new DefaultClassLoaderManager( getClass().getClassLoader() ); + } return defaultImpl.newInstance(); } diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/DefaultClassLoaderManagerTestCase.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/DefaultClassLoaderManagerTestCase.java new file mode 100644 index 000000000..8ac769f02 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/DefaultClassLoaderManagerTestCase.java @@ -0,0 +1,364 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.classloader.test; + +import java.io.File; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.Enumeration; +import org.apache.myrmidon.components.AbstractComponentTest; +import org.apache.myrmidon.components.classloader.DefaultClassLoaderManager; +import org.apache.myrmidon.interfaces.classloader.ClassLoaderManager; +import org.apache.myrmidon.interfaces.classloader.ClassLoaderException; +import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.excalibur.i18n.Resources; + +/** + * Test cases for the DefaultClassLoaderManager. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class DefaultClassLoaderManagerTestCase + extends AbstractComponentTest +{ + private static final String UNSHARED_PKG_NAME = + getPackageName( DefaultClassLoaderManagerTestCase.class ) + ".libs.unshared"; + private static final String UNSHARED_RES_NAME = getResourceName( UNSHARED_PKG_NAME, "unshared.txt" ); + private static final String UNSHARED_CLASS_NAME = UNSHARED_PKG_NAME + ".UnsharedClass"; + + private static final String SHARED_PKG_NAME = + getPackageName( DefaultClassLoaderManagerTestCase.class ) + ".libs.shared"; + private static final String SHARED_RES_NAME = getResourceName( SHARED_PKG_NAME, "shared.txt" ); + private static final String SHARED_CLASS_NAME = SHARED_PKG_NAME + ".SharedClass"; + + private static final String EXTN_PKG_NAME = + getPackageName( DefaultClassLoaderManagerTestCase.class ) + ".libs.extn"; + private static final String EXTN_RES_NAME = getResourceName( EXTN_PKG_NAME, "extn.txt" ); + private static final String EXTN_CLASS_NAME = EXTN_PKG_NAME + ".ExtnClass"; + + private File m_commonJar; + private ClassLoader m_commonClassLoader; + private ClassLoaderManager m_loaderManager; + + public DefaultClassLoaderManagerTestCase( final String name ) + { + super( name ); + } + + /** + * Sets up the test. + */ + protected void setUp() throws Exception + { + m_commonJar = getTestResource( "common.jar" ); + final URL commonJarUrl = m_commonJar.toURL(); + m_commonClassLoader = new URLClassLoader( new URL[]{commonJarUrl} ); + + assertClassFound( m_commonClassLoader, SHARED_CLASS_NAME ); + assertResourcesFound( m_commonClassLoader, SHARED_RES_NAME, m_commonJar ); + + // Create the classloader mgr + m_loaderManager = (ClassLoaderManager)getServiceManager().lookup( ClassLoaderManager.ROLE ); + } + + /** + * Creates an instance of a test component. + */ + protected Object createComponent( final String role, final Class defaultImpl ) + throws Exception + { + if( role.equals( ClassLoaderManager.ROLE ) ) + { + return new DefaultClassLoaderManager( m_commonClassLoader ); + } + else + { + return super.createComponent( role, defaultImpl ); + } + } + + /** + * Creates the parameters for the test. Sub-classes can override this + * method to set-up the parameters. + */ + protected Parameters getParameters() + { + final Parameters parameters = super.getParameters(); + parameters.setParameter( "myrmidon.ext.path", getTestDirectory( "ext" ).getAbsolutePath() ); + return parameters; + } + + /** + * Returns the name of a resource in a package. + */ + private static String getResourceName( final String pkgName, + final String resname ) + { + return pkgName.replace( '.', '/' ) + '/' + resname; + } + + /** + * Asserts that a class is not available in a classloader. + */ + private void assertClassNotFound( final ClassLoader classLoader, + final String className ) + { + try + { + classLoader.loadClass( className ); + fail( "Class " + className + " should not be available." ); + } + catch( ClassNotFoundException e ) + { + } + } + + /** + * Asserts that a class is available in a classloader. + */ + private void assertClassFound( final ClassLoader classLoader, + final String className ) + throws Exception + { + assertClassFound( classLoader, className, classLoader ); + } + + /** + * Asserts that a class is available in a classloader. + */ + private void assertClassFound( final ClassLoader classLoader, + final String className, + final ClassLoader expectedClassLoader ) + throws Exception + { + try + { + final Class cls = classLoader.loadClass( className ); + assertSame( expectedClassLoader, cls.getClassLoader() ); + if( classLoader != expectedClassLoader ) + { + final Class expectedCls = expectedClassLoader.loadClass( className ); + assertSame( expectedCls, cls ); + } + } + catch( ClassNotFoundException e ) + { + fail( "Class " + className + " not found." ); + } + + } + + /** + * Asserts that a resouce is not available in a classloader. + */ + private void assertResourceNotFound( final ClassLoader classLoader, + final String resName ) + throws Exception + { + assertNull( classLoader.getResource( resName ) ); + assertNull( classLoader.getResourceAsStream( resName ) ); + final Enumeration enum = classLoader.getResources( resName ); + assertTrue( !enum.hasMoreElements() ); + } + + /** + * Asserts that a resource is available in a classloader. + */ + private void assertResourcesFound( final ClassLoader classLoader, + final String resName, + final File expectedJar ) + throws Exception + { + assertResourcesFound( classLoader, resName, new File[]{expectedJar} ); + } + + /** + * Asserts that a resource is available in a classloader. + */ + private void assertResourcesFound( final ClassLoader classLoader, + final String resName, + final File[] expectedJars ) + throws Exception + { + final String[] expectedLocations = new String[ expectedJars.length ]; + for( int i = 0; i < expectedJars.length; i++ ) + { + final File jar = expectedJars[ i ]; + expectedLocations[ i ] = "jar:" + jar.toURL() + "!/" + resName; + } + + assertResourcesFound( classLoader, resName, expectedLocations ); + } + + /** + * Asserts that a resource is available in a classloader. + */ + private void assertResourcesFound( final ClassLoader classLoader, + final String resName, + final String[] expectedLocations ) + throws Exception + { + // Use the first in the list of expected locations as the location + // of the resource returned by getResource() + final URL resUrl = classLoader.getResource( resName ); + assertNotNull( resUrl ); + assertEquals( expectedLocations[ 0 ], resUrl.toString() ); + + // Now check all of the resources returned by getResources() + final Enumeration resources = classLoader.getResources( resName ); + for( int i = 0; i < expectedLocations.length; i++ ) + { + final String expectedLocation = expectedLocations[ i ]; + assertTrue( resources.hasMoreElements() ); + final URL location = (URL)resources.nextElement(); + assertEquals( expectedLocation, location.toString() ); + } + assertTrue( !resources.hasMoreElements() ); + } + + /** + * Tests for a Jar with no required extensions. + */ + public void testNoDependencies() throws Exception + { + // Make some assumptions about the common classloader + assertClassNotFound( m_commonClassLoader, UNSHARED_CLASS_NAME ); + assertResourceNotFound( m_commonClassLoader, UNSHARED_RES_NAME ); + + // Build the classloader + final File jarFile = getTestResource( "no-dependencies.jar" ); + final ClassLoader classLoader = m_loaderManager.getClassLoader( jarFile ); + + // Check shared classes/resources + assertClassFound( classLoader, SHARED_CLASS_NAME, m_commonClassLoader ); + assertResourcesFound( classLoader, SHARED_RES_NAME, new File[]{m_commonJar, jarFile} ); + + // Check unshared classes/resources + assertClassFound( classLoader, UNSHARED_CLASS_NAME ); + assertResourcesFound( classLoader, UNSHARED_RES_NAME, jarFile ); + } + + /** + * Tests ClassLoader caching. + */ + public void testClassLoaderReuse() throws Exception + { + final File jarFile = getTestResource( "no-dependencies.jar" ); + final ClassLoader classLoader1 = m_loaderManager.getClassLoader( jarFile ); + final ClassLoader classLoader2 = m_loaderManager.getClassLoader( jarFile ); + assertSame( classLoader1, classLoader2 ); + } + + /** + * Tests for a Jar with a single required extension. + */ + public void testOneDependency() throws Exception + { + // Make some assumptions about the common classloader + assertClassNotFound( m_commonClassLoader, UNSHARED_CLASS_NAME ); + assertResourceNotFound( m_commonClassLoader, UNSHARED_RES_NAME ); + assertClassNotFound( m_commonClassLoader, EXTN_CLASS_NAME ); + assertResourceNotFound( m_commonClassLoader, EXTN_RES_NAME ); + + // Build the extension classloader + final File extnJarFile = getTestResource( "ext/simple-extension.jar" ); + final ClassLoader extnClassLoader = m_loaderManager.getClassLoader( extnJarFile ); + + // Build the Jar classloader + final File jarFile = getTestResource( "one-dependency.jar" ); + final ClassLoader classLoader = m_loaderManager.getClassLoader( jarFile ); + + // Check shared classes/resources + assertClassFound( classLoader, SHARED_CLASS_NAME, m_commonClassLoader ); + assertResourcesFound( classLoader, SHARED_RES_NAME, new File[]{m_commonJar, extnJarFile, jarFile} ); + + // Check extension classes/resources + assertClassFound( classLoader, EXTN_CLASS_NAME, extnClassLoader ); + assertResourcesFound( classLoader, EXTN_RES_NAME, extnJarFile ); + + // Check unshared classes/resources + assertClassFound( classLoader, UNSHARED_CLASS_NAME ); + assertResourcesFound( classLoader, UNSHARED_RES_NAME, jarFile ); + } + + /** + * Tests that classes from extensions can be shared across classloaders. + */ + public void testShareClasses() throws Exception + { + // Build the extension classloader + final File extnJarFile = getTestResource( "ext/simple-extension.jar" ); + final ClassLoader extnClassLoader = m_loaderManager.getClassLoader( extnJarFile ); + + // Build the Jar classloaders + final File jarFile1 = getTestResource( "one-dependency.jar" ); + final ClassLoader classLoader1 = m_loaderManager.getClassLoader( jarFile1 ); + final File jarFile2 = getTestResource( "one-dependency-2.jar" ); + final ClassLoader classLoader2 = m_loaderManager.getClassLoader( jarFile2 ); + + // Check extension classes/resources + assertClassFound( classLoader1, EXTN_CLASS_NAME, extnClassLoader ); + assertResourcesFound( classLoader1, EXTN_RES_NAME, extnJarFile ); + assertClassFound( classLoader2, EXTN_CLASS_NAME, extnClassLoader ); + assertResourcesFound( classLoader2, EXTN_RES_NAME, extnJarFile ); + } + + /** + * Tests detection of dependency cycles in extensions. + */ + public void testCycle() throws Exception + { + final File jarFile = getTestResource( "ext/cycle-extension-1.jar" ); + try + { + m_loaderManager.getClassLoader( jarFile ); + fail(); + } + catch( final ClassLoaderException e ) + { + final Resources rez = getResourcesForTested( DefaultClassLoaderManager.class ); + final String[] messages = { + rez.getString( "create-classloader-for-file.error", jarFile ), + rez.getString( "dependency-cycle.error", jarFile ) + }; + assertSameMessage( messages, e ); + } + } + + /** + * add some classes to common loader only. + * + * unknown extension + * multiple versions of extensions + * extn with requirement on itself + * + * jar with 1 and 2 extns: + * class/resources in parent + * class/resources in jar + * class/resources in extn + * class/resources in all + * + * jar with transitive extn + * class/resources in 2nd extn + * + * jar with transitive extn + explicit extn on same jar + * class/resources in 2nd extn + * + * Same classes: + * get extn explicitly and implicitly, and check classes are the same + * extn shared by 2 jars, using same extn and different extns + * classes in common classloader, shared by 2 jars + * + * multiple files: + * fetch classloader twice + * different path ordering + * + * tools.jar + */ +} diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-1.mf b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-1.mf new file mode 100644 index 000000000..5380f28ed --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-1.mf @@ -0,0 +1,10 @@ +Extension-Name: test.cycle1 +Specification-Title: Test Extension +Specification-Version: 1.0.0 +Specification-Vendor: Jakarta Apache +Implementation-Vendor-Id: org.apache.myrmidon +Implementation-Vendor: Apache Myrmidon Project +Implementation-Version: 3.0 +Extension-List: cycle2 +cycle2-Extension-Name: test.cycle2 +cycle2-Specification-Version: 1.0 diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-2.mf b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-2.mf new file mode 100644 index 000000000..799efc5de --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/cycle-extension-2.mf @@ -0,0 +1,10 @@ +Extension-Name: test.cycle2 +Specification-Title: Test Extension +Specification-Version: 1.0.0 +Specification-Vendor: Jakarta Apache +Implementation-Vendor-Id: org.apache.myrmidon +Implementation-Vendor: Apache Myrmidon Project +Implementation-Version: 1.709.2 +Extension-List: cycle1 +cycle1-Extension-Name: test.cycle1 +cycle1-Specification-Version: 1.0 diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/ExtnClass.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/ExtnClass.java new file mode 100644 index 000000000..6300e4049 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/ExtnClass.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.classloader.test.libs.extn; + +import org.apache.myrmidon.components.classloader.test.libs.shared.SharedClass; + +/** + * A test class loaded from an extension. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class ExtnClass +{ + public SharedClass m_test = new SharedClass(); +} diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/extn.txt b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/extn.txt new file mode 100644 index 000000000..f7953b475 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/extn/extn.txt @@ -0,0 +1 @@ +A test resource loaded from an extension. \ No newline at end of file diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/one-dependency.mf b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/one-dependency.mf new file mode 100644 index 000000000..5ffbb34e4 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/one-dependency.mf @@ -0,0 +1,3 @@ +Extension-List: extension1 +extension1-Extension-Name: test.simple +extension1-Specification-Version: 1.0 diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/SharedClass.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/SharedClass.java new file mode 100644 index 000000000..dfb8fc5d0 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/SharedClass.java @@ -0,0 +1,18 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.classloader.test.libs.shared; + +/** + * A test class. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class SharedClass +{ +} diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/shared.txt b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/shared.txt new file mode 100644 index 000000000..31897ec6d --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/shared/shared.txt @@ -0,0 +1 @@ +A shared resource. \ No newline at end of file diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/simple-extension.mf b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/simple-extension.mf new file mode 100644 index 000000000..54c47a9cf --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/simple-extension.mf @@ -0,0 +1,7 @@ +Extension-Name: test.simple +Specification-Title: Test Simple Extension +Specification-Version: 1.0.0 +Specification-Vendor: Jakarta Apache +Implementation-Vendor-Id: org.apache.myrmidon +Implementation-Vendor: Apache Myrmidon Project +Implementation-Version: 1.0.2 diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/UnsharedClass.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/UnsharedClass.java new file mode 100644 index 000000000..6f250eab9 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/UnsharedClass.java @@ -0,0 +1,21 @@ +/* + * Copyright (C) The Apache Software Foundation. All rights reserved. + * + * This software is published under the terms of the Apache Software License + * version 1.1, a copy of which has been included with this distribution in + * the LICENSE.txt file. + */ +package org.apache.myrmidon.components.classloader.test.libs.unshared; + +import org.apache.myrmidon.components.classloader.test.libs.shared.SharedClass; + +/** + * A test class. + * + * @author Adam Murdoch + * @version $Revision$ $Date$ + */ +public class UnsharedClass +{ + public SharedClass m_test = new SharedClass(); +} diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/unshared.txt b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/unshared.txt new file mode 100644 index 000000000..082b78cb1 --- /dev/null +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/classloader/test/libs/unshared/unshared.txt @@ -0,0 +1 @@ +An unshared resource. \ No newline at end of file diff --git a/proposal/myrmidon/src/test/org/apache/myrmidon/components/embeddor/test/DefaultEmbeddorTest.java b/proposal/myrmidon/src/test/org/apache/myrmidon/components/embeddor/test/DefaultEmbeddorTest.java index 20d971643..d96fad6d0 100644 --- a/proposal/myrmidon/src/test/org/apache/myrmidon/components/embeddor/test/DefaultEmbeddorTest.java +++ b/proposal/myrmidon/src/test/org/apache/myrmidon/components/embeddor/test/DefaultEmbeddorTest.java @@ -9,8 +9,10 @@ package org.apache.myrmidon.components.embeddor.test; import java.io.File; import org.apache.avalon.framework.parameters.Parameters; +import org.apache.avalon.framework.logger.Logger; import org.apache.myrmidon.AbstractProjectTest; import org.apache.myrmidon.LogMessageTracker; +import org.apache.myrmidon.components.embeddor.DefaultEmbeddor; import org.apache.myrmidon.interfaces.embeddor.Embeddor; import org.apache.myrmidon.interfaces.model.Project; import org.apache.myrmidon.interfaces.model.Target; @@ -26,11 +28,50 @@ import org.apache.myrmidon.listeners.ProjectListener; public class DefaultEmbeddorTest extends AbstractProjectTest { + private DefaultEmbeddor m_embeddor; + public DefaultEmbeddorTest( String name ) { super( name ); } + /** + * Tear-down the test. + */ + protected void tearDown() throws Exception + { + if( m_embeddor != null ) + { + m_embeddor.dispose(); + m_embeddor = null; + } + } + + /** + * Returns an embeddor which can be used to build and execute projects. + */ + protected Embeddor getEmbeddor() throws Exception + { + if( m_embeddor == null ) + { + // Need to set the context classloader - The default embeddor uses it + Thread.currentThread().setContextClassLoader( getClass().getClassLoader() ); + + final Logger logger = getLogger(); + m_embeddor = new DefaultEmbeddor(); + m_embeddor.enableLogging( logger ); + + final Parameters params = new Parameters(); + final File instDir = getInstallDirectory(); + params.setParameter( "myrmidon.home", instDir.getAbsolutePath() ); + m_embeddor.parameterize( params ); + m_embeddor.initialize(); + m_embeddor.start(); + } + + return m_embeddor; + } + /** * Tests that a project is successfully built from a file. */ @@ -63,6 +104,7 @@ public class DefaultEmbeddorTest public void testCreateListener() throws Exception { final ProjectListener listener = getEmbeddor().createListener( "default" ); + assertNotNull( listener ); } /** diff --git a/proposal/myrmidon/src/xdocs/todo.xml b/proposal/myrmidon/src/xdocs/todo.xml index fc7827c19..074747d07 100644 --- a/proposal/myrmidon/src/xdocs/todo.xml +++ b/proposal/myrmidon/src/xdocs/todo.xml @@ -524,23 +524,12 @@ public class MyrmidonSecurityManager <socket> conditions to an antlib. Need to resolve how these will be passed a logger. -
  • Make the - <uptodate> task a condition, and move to - an antlib. -
  • -
  • Split up - <is-set> condition into is-set and is-true conditions. -
  • Allow the <if> task to take any condition implementation.
  • Add an else block to the <if> task.
  • -
  • Split the - <available> condition into separate conditions - that test for the availability of a class, or a resource. -
  • Move crimson.jar to bin/lib in the distribution, @@ -550,6 +539,21 @@ public class MyrmidonSecurityManager
  • Add a --type command-line option, to allow the project builder to be manually selected.
  • +
  • Change ProjectBuilder + and Embeddor to throw something more + specialised than Exception. +
  • +
  • Change DefaultClassLoaderManager to handle + directories as part of a library classpath. +
  • +
  • <condition> should set the property + value to false when the condition is false.
  • +
  • Split the <uptodate> condition into + a condition that checks against a single target file, + and one which checks using a destdir/mapper.
  • +
  • Add a task to unset a property.
  • +
  • Change the various def and import task to allow a classpath + to be provided.
  • Unit tests.