diff --git a/proposal/myrmidon/src/java/org/apache/antlib/file/CopyTask.java b/proposal/myrmidon/src/java/org/apache/antlib/file/CopyTask.java
new file mode 100644
index 000000000..b3c1628ea
--- /dev/null
+++ b/proposal/myrmidon/src/java/org/apache/antlib/file/CopyTask.java
@@ -0,0 +1,466 @@
+/*
+ * 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.antlib.file;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import org.apache.avalon.excalibur.i18n.ResourceManager;
+import org.apache.avalon.excalibur.i18n.Resources;
+import org.apache.avalon.excalibur.io.FileUtil;
+import org.apache.myrmidon.api.AbstractTask;
+import org.apache.myrmidon.api.TaskException;
+import org.apache.tools.ant.types.DirectoryScanner;
+import org.apache.tools.ant.types.FileSet;
+import org.apache.tools.ant.types.ScannerUtil;
+import org.apache.tools.ant.types.SourceFileScanner;
+import org.apache.tools.ant.util.mappers.FileNameMapper;
+import org.apache.tools.ant.util.mappers.FlatFileNameMapper;
+import org.apache.tools.ant.util.mappers.IdentityMapper;
+import org.apache.tools.ant.util.mappers.Mapper;
+
+/**
+ * This is a task used to copy files.
+ *
+ * @ant:task name="copy"
+ * @author Peter Donald
+ * @author Glenn McAllister
+ * @author Stefan Bodewig
+ * @author Michael McCallum
+ * @author Magesh Umasankar
+ * @version $Revision$ $Date$
+ */
+public class CopyTask
+ extends AbstractTask
+{
+ private final static Resources REZ =
+ ResourceManager.getPackageResources( CopyTask.class );
+
+ private File m_file;
+ private ArrayList m_filesets = new ArrayList();
+ private File m_destFile;
+ private File m_destDir;
+ private boolean m_preserveLastModified;
+ private boolean m_overwrite;
+ private boolean m_flatten;
+ private boolean m_includeEmpty = true;
+ private Mapper m_mapper;
+
+ private HashMap m_fileMap = new HashMap();
+ private HashMap m_dirMap = new HashMap();
+
+ /**
+ * Sets a single source file to copy.
+ */
+ public void setFile( final File file )
+ {
+ m_file = file;
+ }
+
+ public void addFileset( final FileSet set )
+ {
+ m_filesets.add( set );
+ }
+
+ public void setDestFile( final File destFile )
+ {
+ m_destFile = destFile;
+ }
+
+ public void setDestDir( final File destDir )
+ {
+ m_destDir = destDir;
+ }
+
+ public void setPreserveLastModified( boolean preserveLastModified )
+ {
+ m_preserveLastModified = preserveLastModified;
+ }
+
+ /**
+ * Overwrite any existing destination file(s).
+ */
+ public void setOverwrite( boolean overwrite )
+ {
+ m_overwrite = overwrite;
+ }
+
+ /**
+ * When copying directory trees, the files can be "flattened" into a single
+ * directory. If there are multiple files with the same name in the source
+ * directory tree, only the first file will be copied into the "flattened"
+ * directory, unless the forceoverwrite attribute is true.
+ *
+ * @param flatten The new Flatten value
+ */
+ public void setFlatten( final boolean flatten )
+ {
+ m_flatten = flatten;
+ }
+
+ /**
+ * Defines the FileNameMapper to use (nested mapper element).
+ */
+ public void addMapper( final Mapper mapper )
+ throws TaskException
+ {
+ if( null != m_mapper )
+ {
+ final String message = "Cannot define more than one mapper";
+ throw new TaskException( message );
+ }
+ m_mapper = mapper;
+ }
+
+ public void execute()
+ throws TaskException
+ {
+ validate();
+
+ // deal with the single file
+ if( m_file != null )
+ {
+ if( null == m_destFile )
+ {
+ m_destFile = new File( m_destDir, m_file.getName() );
+ }
+
+ if( m_overwrite ||
+ ( m_file.lastModified() > m_destFile.lastModified() ) )
+ {
+ m_fileMap.put( m_file.getAbsolutePath(), m_destFile.getAbsolutePath() );
+ }
+ else
+ {
+ final String message =
+ REZ.getString( "copy.omit-uptodate.notice", m_file, m_destFile );
+ getLogger().debug( message );
+ }
+ }
+
+ // deal with the filesets
+ final int size = m_filesets.size();
+ for( int i = 0; i < size; i++ )
+ {
+ final FileSet fileSet = (FileSet)m_filesets.get( i );
+ final DirectoryScanner scanner = ScannerUtil.getDirectoryScanner( fileSet );
+ final File fromDir = fileSet.getDir();
+
+ final String[] srcFiles = scanner.getIncludedFiles();
+ final String[] srcDirs = scanner.getIncludedDirectories();
+
+ scan( fromDir, m_destDir, srcFiles, srcDirs );
+ }
+
+ // do all the copy operations now...
+ doFileOperations( m_fileMap, m_dirMap );
+ }
+
+ protected void validate()
+ throws TaskException
+ {
+ final int fileSetSize = m_filesets.size();
+
+ if( null == m_file && 0 == fileSetSize )
+ {
+ final String message = REZ.getString( "copy.missing-src.error" );
+ throw new TaskException( message );
+ }
+
+ if( null != m_destFile && null != m_destDir )
+ {
+ final String message = REZ.getString( "copy.one-dest-only.error" );
+ throw new TaskException( message );
+ }
+
+ if( null != m_file && m_file.exists() && m_file.isDirectory() )
+ {
+ final String message = REZ.getString( "copy.fileset-for-dirs.error" );
+ throw new TaskException( message );
+ }
+
+ if( null != m_destFile && fileSetSize > 0 )
+ {
+ if( fileSetSize > 1 )
+ {
+ final String message = REZ.getString( "copy.need-destdir.error" );
+ throw new TaskException( message );
+ }
+ else
+ {
+ final FileSet fileSet = (FileSet)m_filesets.get( 0 );
+ final DirectoryScanner scanner = ScannerUtil.getDirectoryScanner( fileSet );
+ final String[] srcFiles = scanner.getIncludedFiles();
+
+ if( srcFiles.length > 0 )
+ {
+ if( m_file == null )
+ {
+ m_file = new File( srcFiles[ 0 ] );
+ m_filesets.remove( 0 );
+ }
+ else
+ {
+ final String message = REZ.getString( "copy.bad-mapping.error" );
+ throw new TaskException( message );
+ }
+ }
+ else
+ {
+ final String message = REZ.getString( "copy.bad-operation.error" );
+ throw new TaskException( message );
+ }
+ }
+ }
+
+ if( null != m_file && !m_file.exists() )
+ {
+ final String message =
+ REZ.getString( "copy.missing-file.error", m_file.getAbsolutePath() );
+ throw new TaskException( message );
+ }
+
+ if( null != m_destFile )
+ {
+ m_destDir = m_destFile.getParentFile();
+ }
+ }
+
+ /**
+ * Compares source files to destination files to see if they should be
+ * copied.
+ */
+ private void scan( final File sourceDir,
+ final File destDir,
+ final String[] files,
+ final String[] dirs )
+ throws TaskException
+ {
+ final FileNameMapper mapper = getFilenameMapper();
+
+ buildMap( sourceDir, destDir, files, mapper, m_fileMap );
+
+ if( m_includeEmpty )
+ {
+ buildMap( sourceDir, destDir, dirs, mapper, m_dirMap );
+ }
+ }
+
+ private void buildMap( final File sourceDir,
+ final File destDir,
+ final String[] files,
+ final FileNameMapper mapper,
+ final Map map )
+ throws TaskException
+ {
+ final String[] toCopy = buildFilenameList( files, mapper, sourceDir, destDir );
+ for( int i = 0; i < toCopy.length; i++ )
+ {
+ final String destFilename = mapper.mapFileName( toCopy[ i ] )[ 0 ];
+ final File source = new File( sourceDir, toCopy[ i ] );
+ final File destination = new File( destDir, destFilename );
+ map.put( source.getAbsolutePath(), destination.getAbsolutePath() );
+ }
+ }
+
+ /**
+ * Utility method to build up a list of files needed between both
+ * but only getting the files that need updating (unless overwrite is true).
+ */
+ private String[] buildFilenameList( final String[] names,
+ final FileNameMapper mapper,
+ final File fromDir,
+ final File toDir )
+ throws TaskException
+ {
+ if( m_overwrite )
+ {
+ final ArrayList list = new ArrayList( names.length );
+ for( int i = 0; i < names.length; i++ )
+ {
+ final String name = names[ i ];
+ if( null != mapper.mapFileName( name ) )
+ {
+ list.add( name );
+ }
+ }
+
+ return (String[])list.toArray( new String[ list.size() ] );
+ }
+ else
+ {
+ final SourceFileScanner scanner = new SourceFileScanner();
+ setupLogger( scanner );
+ return scanner.restrict( names, fromDir, toDir, mapper );
+ }
+ }
+
+ /**
+ * Perform the oepration on all the files (and possibly empty directorys).
+ */
+ private void doFileOperations( final Map fileCopyMap, final Map dirCopyMap )
+ throws TaskException
+ {
+ final int fileCount = fileCopyMap.size();
+ if( fileCount > 0 )
+ {
+ doOperationOnFiles( fileCopyMap );
+ }
+
+ if( m_includeEmpty )
+ {
+ doOperationOnDirs( dirCopyMap );
+ }
+ }
+
+ /**
+ * perform operation on files.
+ */
+ private void doOperationOnFiles( final Map fileMap )
+ throws TaskException
+ {
+ final int fileCount = fileMap.size();
+ displayFilecountNotice( fileCount );
+
+ final Iterator names = fileMap.keySet().iterator();
+ while( names.hasNext() )
+ {
+ final String source = (String)names.next();
+ final String destination = (String)fileMap.get( source );
+
+ if( source.equals( destination ) )
+ {
+ final String message =
+ REZ.getString( "copy.selfcopy-ignored.notice", source );
+ getLogger().info( message );
+ continue;
+ }
+
+ try
+ {
+ final String message =
+ REZ.getString( "copy.filecopy.notice", source, destination );
+ getLogger().info( message );
+
+ doOperation( source, destination );
+ }
+ catch( final IOException ioe )
+ {
+ final String message =
+ REZ.getString( "copy.filecopy.error", source, destination, ioe );
+ throw new TaskException( message, ioe );
+ }
+ }
+ }
+
+ /**
+ * perform operation on directories.
+ */
+ private void doOperationOnDirs( final Map dirMap )
+ {
+ final Iterator dirs = dirMap.values().iterator();
+ int count = 0;
+ while( dirs.hasNext() )
+ {
+ final String pathname = (String)dirs.next();
+ final File dir = new File( pathname );
+ if( !dir.exists() )
+ {
+ if( !dir.mkdirs() )
+ {
+ final String message =
+ REZ.getString( "copy.dircopy.error", dir.getAbsolutePath() );
+ getLogger().error( message );
+ }
+ else
+ {
+ count++;
+ }
+ }
+ }
+
+ if( count > 0 )
+ {
+ displayDirCopyNotice( count );
+ }
+ }
+
+ /**
+ * Utility method to determine and retrieve FilenameMapper.
+ */
+ private FileNameMapper getFilenameMapper()
+ throws TaskException
+ {
+ if( null != m_mapper )
+ {
+ return m_mapper.getImplementation();
+ }
+ else if( m_flatten )
+ {
+ return new FlatFileNameMapper();
+ }
+ else
+ {
+ return new IdentityMapper();
+ }
+ }
+
+ /**
+ * Utility method to perform operation to transform a single source file
+ * to a destination.
+ */
+ private void doOperation( final String sourceFilename,
+ final String destinationFilename )
+ throws IOException
+ {
+ final File source = new File( sourceFilename );
+ final File destination = new File( destinationFilename );
+
+ if( m_overwrite )
+ {
+ FileUtil.forceDelete( destination );
+ }
+
+ FileUtil.copyFile( source, destination );
+
+ if( m_preserveLastModified )
+ {
+ destination.setLastModified( source.lastModified() );
+ }
+ }
+
+ /**
+ * Utility method to display notice about how many dirs copied.
+ */
+ private void displayDirCopyNotice( final int count )
+ {
+ final String message =
+ REZ.getString( "copy.dir-count.notice",
+ new Integer( count ),
+ m_destDir.getAbsolutePath() );
+ getLogger().info( message );
+ }
+
+ /**
+ * Utility method to display notice about how many files copied.
+ */
+ private void displayFilecountNotice( final int count )
+ {
+ if( getLogger().isInfoEnabled() )
+ {
+ final String message =
+ REZ.getString( "copy.file-count.notice",
+ new Integer( count ),
+ m_destDir.getAbsolutePath() );
+ getLogger().info( message );
+ }
+ }
+}
diff --git a/proposal/myrmidon/src/java/org/apache/antlib/file/Resources.properties b/proposal/myrmidon/src/java/org/apache/antlib/file/Resources.properties
index 29552e401..e0910e0e2 100644
--- a/proposal/myrmidon/src/java/org/apache/antlib/file/Resources.properties
+++ b/proposal/myrmidon/src/java/org/apache/antlib/file/Resources.properties
@@ -19,3 +19,18 @@ delete.delete-file.notice=Deleting {0}.
delete.delete-file.error=Unable to delete file {0}.
delete.delete-file.error=Deleting {0} files from {1}.
delete.summary.notice=Deleted {0,choice,0#zero directories|1#1 directory|2<{0} directories} from {1}.
+
+copy.omit-uptodate.notice={0} omitted as {1} is up to date.
+copy.missing-src.error=No source file or fileset specified.
+copy.one-dest-only.error=Only one of destFile or destDir may be set.
+copy.fileset-for-dirs.error=Use a fileset to copy directories.
+copy.need-destdir.error=Cannot copy multiple files into a single file.
+copy.bad-mapping.error=Cannot concatenate multiple files into a single file.
+copy.bad-operation.error=Cannot perform operation from directory to file.
+copy.missing-file.error=Could not find file {0} to copy.
+copy.dir-count.notice=Copied {0} empty director{0,choice,1#y|2