/*
 * @(#)GroboReZipTask.java
 *
 * Copyright (C) 2004 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in
 *  all copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.codecoverage.v2.ant;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Enumeration;
import java.util.Stack;
import java.util.Vector;
import java.util.zip.CRC32;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.PatternSet;
//import org.apache.tools.ant.types.Resource;
import org.apache.tools.ant.util.FileUtils;

// We have to use the Ant ZipScanner due to the Reference type.
// But it's easy to avoid this...
//import org.apache.tools.ant.types.ZipScanner;

// use our own re-packaging of the Apache zip utility
import net.sourceforge.groboutils.codecoverage.v2.ant.zip.ZipEntry;
import net.sourceforge.groboutils.codecoverage.v2.ant.zip.ZipOutputStream;
import net.sourceforge.groboutils.codecoverage.v2.ant.zip.ZipFile;
import net.sourceforge.groboutils.codecoverage.v2.ant.zip.ZipFileSet;


/**
 * Similar to the Zip task, but its purpose is to modify not only
 * one zip file, but possibly many containing zip files as well.
 * Also, should have meta-inf support for modifying the classpath to
 * add in the runtime jar file.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/07/08 05:29:19 $
 * @since     March 23, 2004
 */
public class GroboReZipTask extends Task
{
    private boolean failOnError = true;
    private Vector zips = new Vector();
    
    private static final int BUFFER_SIZE = 8 * 1024;
    private static final int DELETE_RETRY_SLEEP_MILLIS = 10;
    
    private static final FileUtils FILEUTILS = FileUtils.newFileUtils();
    private static final long EMPTY_CRC = new CRC32 ().getValue ();
    
    
    public static class ReZipFileSet extends ZipFileSet
    {
        private boolean replaceOnly = false;
        
        public ReZipFileSet()
        {
            super();
        }
        
        public ReZipFileSet( FileSet fs )
        {
            super( fs );
        }
        
        public ReZipFileSet( ZipFileSet zfs )
        {
            super( zfs );
        }
        
        public ReZipFileSet( ReZipFileSet rzfs )
        {
            super( (ZipFileSet)rzfs );
            this.replaceOnly = rzfs.replaceOnly;
        }
        
        /**
         * If this is set to true, then only the files in this fileset
         * that already exist in the owning zip file will be added, all
         * others in this fileset will be ignored.
         */
        public void setReplaceOnly( boolean replace )
        {
            this.replaceOnly = replace;
        }
        
        public boolean getReplaceOnly( Project p )
        {
            if (isReference())
            {
                return ((ReZipFileSet)getRef( p )).getReplaceOnly( p );
            }
            return this.replaceOnly;
        }
    }
    
    
    /** the base type for altering a zip file. */
    public static class AlterZip
    {
        private Vector subZip = new Vector();
        private Vector fileSets = new Vector();
        private Vector alter = new Vector();
        private String srcFile;
        private String destFile;
        String encoding;
        boolean doCompress = true;
        
        public void setSrc( String f )
        {
            this.srcFile = f;
        }
        
        public void setDest( String f )
        {
            this.destFile = f;
        }
        
        public void addFileSet( ReZipFileSet zfs )
        {
            if (zfs != null)
            {
                this.fileSets.addElement( zfs );
            }
        }
        
        public void addZipFileSet( ReZipFileSet zfs )
        {
            if (zfs != null)
            {
                this.fileSets.addElement( zfs );
            }
        }
        
        public void addAlterZip( AlterZip z )
        {
            if (z != null)
            {
                this.subZip.addElement( z );
            }
        }
        
        public void addAlterWar( AlterWar z )
        {
            if (z != null)
            {
                this.subZip.addElement( z );
            }
        }
        
        public void addAlterEar( AlterEar z )
        {
            if (z != null)
            {
                this.subZip.addElement( z );
            }
        }
        
        public void addAlterJar( AlterJar z )
        {
            if (z != null)
            {
                this.subZip.addElement( z );
            }
        }
        
        protected void _addModifyEntry( ModifyEntry me )
        {
            if (me != null)
            {
                this.alter.addElement( me );
            }
        }
        
        public File getAbsoluteSrc( Project p )
        {
            return p.resolveFile( this.srcFile );
        }
        
        public File getAbsoluteDest( Project p )
        {
            return p.resolveFile( this.destFile );
        }
        
        public String getSrc()
        {
            return this.srcFile;
        }
        
        public String getDest()
        {
            return this.destFile;
        }
        
        public boolean hasDest()
        {
            return (this.destFile != null);
        }
        
        public AlterZip[] getSubZips()
        {
            AlterZip az[] = new AlterZip[ this.subZip.size() ];
            this.subZip.copyInto( az );
            return az;
        }
        
        public FileSet[] getFileSets()
        {
            FileSet fs[] = new FileSet[ this.fileSets.size() ];
            this.fileSets.copyInto( fs );
            return fs;
        }
        
        public ModifyEntry[] getModifyEntries()
        {
            Vector entries = new Vector();
            Enumeration e = this.alter.elements();
            while (e.hasMoreElements())
            {
                entries.addElement( e.nextElement() );
            }
            e = this.subZip.elements();
            while (e.hasMoreElements())
            {
                AlterZip az = (AlterZip)e.nextElement();
                entries.addElement( new AlterZipModifyEntry( az ) );
            }
            ModifyEntry me[] = new ModifyEntry[ entries.size() ];
            entries.copyInto( me );
            return me;
        }
    
    
        public ModifyEntry shouldModify( String r )
        {
            ModifyEntry[] me = getModifyEntries();
            for (int i = 0; i < me.length; ++i)
            {
                if (me[i].shouldModify( r ))
                {
                    return me[i];
                }
            }
            return null;
        }
    }
    
    
    /**
     * Tells the processing system that a particular file needs to
     * be modified.
     */
    public static abstract class ModifyEntry
    {
        private String filename;
        public ModifyEntry( String src )
        {
            this.filename = src;
        }
        
        public boolean shouldModify( String r )
        {
            return (this.filename.equals( r ) );
        }
        
        public abstract void modify( GroboReZipTask grze,
                InputStream in, File outfile )
                throws IOException;
    }
    
    
    /**
     * Recursively alter the inner zip file, by writing its contents
     * to temporary files.
     */
    public static class AlterZipModifyEntry extends ModifyEntry
    {
        private AlterZip az;
        public AlterZipModifyEntry( AlterZip az )
        {
            super( az.srcFile );
            this.az = az;
        }
        
        public void modify( GroboReZipTask grze,
                InputStream in, File outfile )
                throws IOException
        {
            File src = FILEUTILS.createTempFile( "z", ".zip", null );
            src.deleteOnExit();
            FileOutputStream fos = new FileOutputStream( src );
            byte buff[] = new byte[ BUFFER_SIZE ];
            try
            {
                int size = in.read( buff, 0, BUFFER_SIZE );
                while (size > 0)
                {
                    fos.write( buff, 0, size );
                    size = in.read( buff, 0, BUFFER_SIZE );
                }
            }
            finally
            {
                fos.close();
            }
            
            try
            {
                grze.processZip( this.az, src, outfile );
            }
            finally
            {
                delete( src );
            }
        }
    }
    
    
    public static class AlterEar extends AlterZip
    {
        // currently, this is exactly the same.
        // we may want to alter the application.xml file
        // in the future.
    }
    
    
    public static class AlterJar extends AlterZip
    {
        // currently, this is exactly the same.
        // we may want to alter the MANIFEST.MF file
        // in the future, to possibly append the classpath entry.
        // This should be based on a 'addLib' like syntax.
    }
    
    
    public static class AlterWar extends AlterZip
    {
        public void addClasses( ReZipFileSet zfs )
        {
            zfs.setPrefix( "WEB-INF/classes/" );
            zfs.setReplaceOnly( true );
            addZipFileSet( zfs );
        }
        
        public void addLib( ReZipFileSet zfs )
        {
            zfs.setPrefix( "WEB-INF/lib/" );
            addZipFileSet( zfs );
        }
    }
    
    
    
    //------------------------------------------------------------------
    
    public void addAlterZip( AlterZip z )
    {
        if (z != null)
        {
            this.zips.addElement( z );
        }
    }
    
    public void addAlterWar( AlterWar z )
    {
        if (z != null)
        {
            this.zips.addElement( z );
        }
    }
    
    public void addAlterEar( AlterEar z )
    {
        if (z != null)
        {
            this.zips.addElement( z );
        }
    }
    
    public void addAlterJar( AlterJar z )
    {
        if (z != null)
        {
            this.zips.addElement( z );
        }
    }
    
    
    //------------------------------------------------------------------
    
    public void execute()
            throws BuildException
    {
        // the top level zips act a bit differently than the rest -
        // their files are absolute.
        
        Enumeration e = this.zips.elements();
        while (e.hasMoreElements())
        {
            AlterZip az = (AlterZip)e.nextElement();
            File src = az.getAbsoluteSrc( getProject() );
            if (src == null)
            {
                String msg = "No source specified for zip.";
                if (this.failOnError)
                {
                    throw new BuildException( msg );
                }
                else
                {
                    log( msg, Project.MSG_WARN );
                }
            }
            File dest = null;
            boolean isTemp = false;
            if (az.hasDest())
            {
                dest = az.getAbsoluteDest( getProject() );
                if (dest.equals( src ))
                {
                    // replacing the original file...
                    dest = null;
                }
            }
            if (dest == null)
            {
                isTemp = true;
                dest = FILEUTILS.createTempFile( "z", ".zip", null );
                dest.deleteOnExit();
            }
            
            log( "Altering ["+src.getAbsolutePath()+"] into ["+
                dest.getAbsolutePath()+"]", Project.MSG_INFO );
            
            try
            {
                processZip( az, src, dest );
                
                if (isTemp)
                {
                    // replace the original file.
                    FILEUTILS.copyFile( dest, src );
                    System.gc();
                    System.runFinalization();
                    delete( dest );
                }
            }
            catch (SecurityException se)
            {
                throw new BuildException(
                    "Not allowed to rename temporary file to old file ("
                    + src.getAbsolutePath()
                    + ")" );
            }
            catch (IOException ioe)
            {
                String msg = "Problem processing zip file "+
                    src.getAbsolutePath()+": "+
                    ioe.getMessage();
                if (this.failOnError)
                {
                    throw new BuildException( msg, ioe );
                }
                else
                {
                    log( msg, Project.MSG_WARN );
                }
            }
            catch (BuildException be)
            {
                if (this.failOnError)
                {
                    throw be;
                }
                else
                {
                    log( be.getMessage(), Project.MSG_WARN );
                }
            }
        }
    }
    
    
    //------------------------------------------------------------------
    
    
    public void processZip( AlterZip az, File src, File dest )
            throws BuildException, IOException
    {
        ZipFile inzip = new ZipFile( src );
        az.encoding = inzip.getEncoding();
        Vector originalFiles = new Vector();
        Enumeration entries = inzip.getEntries();
        while (entries.hasMoreElements())
        {
            ZipEntry ze = (ZipEntry)entries.nextElement();
            originalFiles.addElement( ze.getName() );
        }
        entries = null;
        inzip.close();
        inzip = null;
        
        ZipOutputStream zOut = new ZipOutputStream(
            new FileOutputStream( dest ) );
        zOut.setEncoding( az.encoding );
        
        Vector addedFiles = new Vector();
        addFiles( zOut, az.getFileSets(), az, addedFiles, originalFiles );
        
        log( "Adding contents of original zip ["+src.getAbsolutePath()+"]",
            Project.MSG_VERBOSE );
        ReZipFileSet oldFiles = new ReZipFileSet();
        oldFiles.setProject( getProject() );
        oldFiles.setSrc( src );
        oldFiles.setReplaceOnly( false );
        /*
        Enumeration af = addedFiles.elements();
        while (af.hasMoreElements())
        {
            PatternSet.NameEntry ne = oldFiles.createExclude();
            ne.setName( (String)af.nextElement() );
        }
        */
        addFiles( zOut, oldFiles, az, addedFiles, originalFiles );
        
        finalizeZipOutputStream(zOut);
    }
    
    
    private void addFiles( ZipOutputStream zOut, FileSet[] filesets,
            AlterZip az, Vector addedFiles, Vector originalFiles )
            throws IOException, BuildException
    {
        for (int i = 0; i < filesets.length; ++i)
        {
            addFiles( zOut, filesets[i], az, addedFiles, originalFiles );
        }
    }
    
    
    private void addFiles( ZipOutputStream zOut, FileSet fileset,
            AlterZip az, Vector addedFiles, Vector originalFiles )
            throws IOException, BuildException
    {
        DirectoryScanner ds =
            fileset.getDirectoryScanner( getProject() );
        //if (ds instanceof ZipScanner)
        //{
        //    ((ZipScanner) ds).setEncoding( az.encoding );
        //}
        ds.scan();
        String[] f = ds.getIncludedFiles();
        if (f.length > 0)
        {
            addResources( az, fileset, f, false, zOut, addedFiles, originalFiles );
        }

        String[] d = ds.getIncludedDirectories();
        if (d.length > 0)
        {
            addResources( az, fileset, d, true, zOut, addedFiles, originalFiles );
        }
    }
    
    
    /**
     * Add the given resources.  Ripped from Ant's Zip task.
     *
     * @param fileset may give additional information like fullpath or
     * permissions.
     * @param resources the resources to add
     * @param zOut the stream to write to
     *
     * @since Ant 1.5.2
     */
    protected final void addResources( AlterZip az,
            FileSet fileset, String[] resources, boolean areDirs,
            ZipOutputStream zOut, Vector addedFiles, Vector originalFiles )
            throws IOException
    {
        String prefix = "";
        String fullpath = "";
        int dirMode = ZipFileSet.DEFAULT_DIR_MODE;
        int fileMode = ZipFileSet.DEFAULT_FILE_MODE;
        boolean replaceOnly = false;

        ZipFileSet zfs = null;
        if (fileset instanceof ZipFileSet)
        {
            zfs = (ZipFileSet) fileset;
            prefix = zfs.getPrefix(getProject());
            fullpath = zfs.getFullpath(getProject());
            dirMode = zfs.getDirMode(getProject());
            fileMode = zfs.getFileMode(getProject());
            if (fileset instanceof ReZipFileSet)
            {
                replaceOnly = ((ReZipFileSet)fileset).getReplaceOnly(getProject());
            }
            log( "Processing resources from ZipFileSet: prefix=["+
                prefix+"]; fullpath=["+fullpath+"]; dirMode=["+
                dirMode+"]; fileMode=["+fileMode+"]; replaceOnly=["+
                replaceOnly+"]", Project.MSG_DEBUG );
        }

        if (prefix.length() > 0 && fullpath.length() > 0)
        {
            throw new BuildException("Both prefix and fullpath attributes must"
                                     + " not be set on the same fileset.");
        }

        if (resources.length != 1 && fullpath.length() > 0)
        {
            throw new BuildException("fullpath attribute may only be specified"
                                     + " for filesets that specify a single"
                                     + " file.");
        }

        if (prefix.length() > 0)
        {
            if (!prefix.endsWith("/") && !prefix.endsWith("\\"))
            {
                prefix += "/";
            }
            addParentDirs( null, prefix, zOut, "", dirMode, addedFiles,
                replaceOnly );
        }

        ZipFile zf = null;
        try
        {
            boolean dealingWithFiles = false;
            File base = null;
            if (zfs == null || zfs.getSrc( getProject() ) == null)
            {
                dealingWithFiles = true;
                base = fileset.getDir( getProject() );
                log( "Dealing with files (base=["+base+"])",
                    Project.MSG_DEBUG );
            }
            else
            {
                File src = zfs.getSrc( getProject() );
                zf = new ZipFile( src, az.encoding );
                log( "Dealing with zipFile (src=["+src.getAbsolutePath()+"])",
                    Project.MSG_DEBUG );
            }
            
            for (int i = 0; i < resources.length; i++)
            {
                log("Processing resource ["+resources[i]+"]",
                    Project.MSG_DEBUG );
                String name = null;
                if (fullpath.length() > 0)
                {
                    name = fullpath;
                }
                else
                {
                    name = resources[i];
                }
                name = name.replace(File.separatorChar, '/');
                
                if ("".equals(name))
                {
                    log("Empty name - continuing.",
                        Project.MSG_DEBUG );
                    continue;
                }
                if (areDirs && !name.endsWith("/"))
                {
                    name = name + "/";
                }
                String prefixName = prefix + name;
                log("Output Zip entry name = ["+prefixName+"]",
                    Project.MSG_DEBUG );
                
                if (replaceOnly && !originalFiles.contains( prefixName ))
                {
                    // only add the file when the original Zip contains it.
                    log( prefixName + " not in original zip, so skip it.",
                        Project.MSG_VERBOSE );
                    continue;
                }
                
                if (!dealingWithFiles
                    && areDirs
                    && !zfs.hasDirModeBeenSet())
                {
                    log("Adding directory's path into zip", Project.MSG_DEBUG);
                    int nextToLastSlash = name.lastIndexOf( "/",
                        name.length() - 2 );
                    if (nextToLastSlash != -1)
                    {
                        addParentDirs( base, name.substring( 0,
                            nextToLastSlash + 1 ),
                            zOut, prefix, dirMode, addedFiles,
                            replaceOnly );
                    }
                    ZipEntry ze = zf.getEntry( resources[i] );
                    addParentDirs( base, name, zOut, prefix, ze.getUnixMode(),
                        addedFiles, replaceOnly );
                }
                else
                {
                    log("Adding file's path into zip", Project.MSG_DEBUG);
                    addParentDirs( base, name, zOut, prefix, dirMode,
                        addedFiles, replaceOnly );
                }
                
                if (!areDirs)
                {
                    if (dealingWithFiles)
                    {
                        File f = FILEUTILS.resolveFile(base,
                            resources[i]);
                        // only modify files that already exist in the
                        // zip file.
                        zipFile( az, f, zOut, prefixName, fileMode,
                            addedFiles );
                    }
                    else
                    {
                        ZipEntry ze = zf.getEntry( resources[i] );
                        
                        if (ze != null)
                        {
                            log("Inserting zip entry ["+resources[i]+
                                "]", Project.MSG_DEBUG);
                            boolean oldCompress = az.doCompress;
                            az.doCompress =
                                (ze.getMethod() == ZipEntry.DEFLATED);
                            ModifyEntry me = az.shouldModify( resources[i] );
                            try
                            {
                                zipFile( az, me,
                                    zf.getInputStream(ze), zOut, prefixName,
                                    ze.getTime(), zfs.getSrc( getProject() ),
                                    zfs.hasFileModeBeenSet() ? fileMode
                                    : ze.getUnixMode(), addedFiles );
                            }
                            finally
                            {
                                az.doCompress = oldCompress;
                            }
                        }
                    }
                }
            }
        }
        finally
        {
            if (zf != null)
            {
                zf.close();
            }
        }
    }

    

    /**
     * Ripped from Ant's Zip task.
     *
     * Ensure all parent dirs of a given entry have been added.
     *
     * @since Ant 1.5.2
     */
    protected final void addParentDirs( File baseDir, String entry,
            ZipOutputStream zOut, String prefix, int dirMode,
            Vector addedFiles, boolean replaceOnly )
            throws IOException
    {
        // note: if replaceOnly == true, then any directories added would
        // mean that for them to be added, they must already exist in
        // the original zip file.  If that's the case, then the directory
        // will be added anyway.  Therefore we don't need to add the
        // directory if replaceOnly == true.
        if (replaceOnly)
        {
            return;
        }
        
        
        Stack directories = new Stack();
        int slashPos = entry.length();

        while ((slashPos = entry.lastIndexOf( '/', slashPos - 1 )) != -1)
        {
            String dir = entry.substring( 0, slashPos + 1 );
            if (addedFiles.contains( prefix + dir )) {
                break;
            }
            directories.push(dir);
        }

        while (!directories.isEmpty())
        {
            String dir = (String) directories.pop();
            File f = null;
            if (baseDir != null) {
                f = new File(baseDir, dir);
            } else {
                f = new File(dir);
            }
            
            zipDir( f, zOut, prefix + dir, dirMode, addedFiles );
        }
    }
    
    /**
     * Ripped from Ant's Zip task.
     *
     * @since Ant 1.5.2
     */
    protected void zipDir( File dir, ZipOutputStream zOut, String vPath,
            int mode, Vector addedFiles )
            throws IOException
    {
        if (addedFiles.contains(vPath))
        {
            // don't add directories we've already added.
            // no warning if we try, it is harmless in and of itself
            return;
        }

        log( "adding directory " + vPath, Project.MSG_VERBOSE );
        addedFiles.addElement( vPath );

        ZipEntry ze = new ZipEntry(vPath);
        if (dir != null && dir.exists()) {
            // ZIPs store time with a granularity of 2 seconds, round up
            ze.setTime(dir.lastModified() + 1999);
        } else {
            // ZIPs store time with a granularity of 2 seconds, round up
            ze.setTime(System.currentTimeMillis() + 1999);
        }
        ze.setSize( 0 );
        ze.setMethod( ZipEntry.STORED );
        // This is faintly ridiculous:
        ze.setCrc( EMPTY_CRC );
        ze.setUnixMode(mode);

        zOut.putNextEntry( ze );
    }
    
    
    /**
     * Ripped from Ant's Zip task.
     * Method that gets called when adding from java.io.File instances.
     *
     * <p>This implementation delegates to the six-arg version.</p>
     *
     * @param file the file to add to the archive
     * @param zOut the stream to write to
     * @param vPath the name this entry shall have in the archive
     * @param mode the Unix permissions to set.
     *
     * @since Ant 1.5.2
     */
    protected void zipFile( AlterZip az, File file,
            ZipOutputStream zOut, String vPath, int mode, Vector addedFiles )
            throws IOException
    {
        /*
        if (file.equals(zipFile))
        {
            throw new BuildException("A zip file cannot include itself",
                                     getLocation());
        }
        */
        log("zipFile( vPath = ["+vPath+"]; file = ["+file.getAbsolutePath()+"] )",
            Project.MSG_DEBUG );
        FileInputStream fIn = new FileInputStream(file);
        try
        {
            // ZIPs store time with a granularity of 2 seconds, round up
            zipFile( az, null, fIn, zOut, vPath, file.lastModified() + 1999,
                null, mode, addedFiles );
        }
        finally
        {
            fIn.close();
        }
    }
    
    
    /**
     * Ripped from Ant's Zip task.
     *
     * Adds a new entry to the archive, takes care of duplicates as well.
     *
     * @param in the stream to read data for the entry from.
     * @param zOut the stream to write to.
     * @param vPath the name this entry shall have in the archive.
     * @param lastModified last modification time for the entry.
     * @param fromArchive the original archive we are copying this
     * entry from, will be null if we are not copying from an archive.
     * @param mode the Unix permissions to set.
     *
     * @since Ant 1.5.2
     */
    protected void zipFile( AlterZip az, ModifyEntry me, InputStream in,
            ZipOutputStream zOut, String vPath,
            long lastModified, File fromArchive, int mode, Vector addedFiles )
            throws IOException
    {
        if (addedFiles.contains(vPath))
        {
            log( vPath + " already added, skipping", Project.MSG_VERBOSE );
            return;
            /*
            if (duplicate.equals("preserve")) {
                log(vPath + " already added, skipping", Project.MSG_INFO);
                return;
            } else if (duplicate.equals("fail")) {
                throw new BuildException("Duplicate file " + vPath
                                         + " was found and the duplicate "
                                         + "attribute is 'fail'.");
            } else {
                // duplicate equal to add, so we continue
                log("duplicate file " + vPath
                    + " found, adding.", Project.MSG_VERBOSE);
            }
            */
        }
        else
        {
            log( "adding entry " + vPath, Project.MSG_VERBOSE );
        }
        
        ZipEntry ze = new ZipEntry( vPath );
        ze.setTime( lastModified );
        ze.setMethod( az.doCompress ? ZipEntry.DEFLATED : ZipEntry.STORED );
        
        // should this zip file be modified?
        File tmpFile = null;
        try
        {
            if (me != null)
            {
                tmpFile = FILEUTILS.createTempFile( "z", ".tmp", null );
                tmpFile.deleteOnExit();
                
                me.modify( this, in, tmpFile );
                
                in = new FileInputStream( tmpFile );
            }
            
            /*
            * ZipOutputStream.putNextEntry expects the ZipEntry to
            * know its size and the CRC sum before you start writing
            * the data when using STORED mode - unless it is seekable.
            *
            * This forces us to process the data twice.
            */
            if (!zOut.isSeekable() && !az.doCompress)
            {
                long size = 0;
                CRC32 cal = new CRC32();
                if (!in.markSupported())
                {
                    // Store data into a byte[]
                    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    
                    byte[] buffer = new byte[8 * 1024];
                    int count = 0;
                    do {
                        size += count;
                        cal.update(buffer, 0, count);
                        bos.write(buffer, 0, count);
                        count = in.read(buffer, 0, buffer.length);
                    } while (count != -1);
                    in = new ByteArrayInputStream(bos.toByteArray());
                }
                else
                {
                    in.mark(Integer.MAX_VALUE);
                    byte[] buffer = new byte[8 * 1024];
                    int count = 0;
                    do
                    {
                        size += count;
                        cal.update(buffer, 0, count);
                        count = in.read(buffer, 0, buffer.length);
                    } while (count != -1);
                    in.reset();
                }
                ze.setSize(size);
                ze.setCrc(cal.getValue());
            }
            
            ze.setUnixMode(mode);
            zOut.putNextEntry(ze);
            
            byte[] buffer = new byte[BUFFER_SIZE];
            int count = 0;
            do
            {
                if (count != 0)
                {
                    zOut.write(buffer, 0, count);
                }
                count = in.read(buffer, 0, BUFFER_SIZE);
            } while (count != -1);
            
            addedFiles.addElement(vPath);
        }
        finally
        {
            // close the input stream before deleting the temp file.
            if (in != null)
            {
                in.close();
            }
            
            if (tmpFile != null)
            {
                delete( tmpFile );
            }
        }
    }
    
    
    
    /**
     * method for subclasses to override
     */
    protected void initZipOutputStream(ZipOutputStream zOut)
            throws IOException, BuildException
    {
    }

    /**
     * method for subclasses to override
     */
    protected void finalizeZipOutputStream(ZipOutputStream zOut)
            throws IOException, BuildException
    {
        zOut.finish();
    }
    
    /**
     * (pulled from the ant Delete task)
     * Attempt to fix possible race condition when deleting
     * files on WinXP. If the delete does not work,
     * wait a little and try again.
     */
    private static boolean delete(File f)
    {
        if (!f.delete())
        {
            try
            {
                Thread.sleep( DELETE_RETRY_SLEEP_MILLIS );
            }
            catch (InterruptedException ex)
            {
                // ignore
            }
            return f.delete();
        }
        return true;
    }
}

