/*
 * @(#)CoveragePostCompilerTask.java
 *
 * Copyright (C) 2002-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.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Enumeration;
import java.util.Properties;
import java.util.Vector;

import net.sourceforge.groboutils.codecoverage.v2.IAnalysisModule;
import net.sourceforge.groboutils.codecoverage.v2.compiler.AlreadyPostCompiledException;
import net.sourceforge.groboutils.codecoverage.v2.compiler.PostCompileClass;
import net.sourceforge.groboutils.codecoverage.v2.datastore.DirMetaDataWriter;
import net.sourceforge.groboutils.util.io.v1.ReadByteStream;

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.util.FileUtils;



/**
 * Ant task the compiles the class files into coverage files.  Now, also
 * generates the properties file for you.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @version   $Date: 2004/04/15 05:48:25 $
 * @since     December 18, 2002
 * @deprecated Please use the GroboInstrumentTask instead of this one.
 */
public class CoveragePostCompilerTask extends Task
{
    private static final FileUtils FILEUTILS = FileUtils.newFileUtils();
    
    private static final String CLASSNAME_EXT = ".class";
    
    private Vector filesets = new Vector();
    private File datadir = null;
    private File outfiledir = null;
    private Vector analysisModules = new Vector();
    private Configuration config = null;

    

    /**
     * Add a new fileset instance to this compilation. Whatever the fileset is,
     * only filename that are <tt>.class</tt> will be considered as
     * 'candidates'.  Currently, jar files are not read; you'll have to
     * uncompress them to a directory before running this step.
     *
     * @param     fs the new fileset containing the rules to get the testcases.
     */
    public void addFileSet( FileSet fs )
    {
        this.filesets.addElement(fs);
    }
    
    
    /**
     * Sets the directory in which all the data accumulated from the
     * post compilation step will be placed.  This should be a directory
     * dedicated just to the output data.
     */
    public void setDataDir( File f )
    {
        this.datadir = f;
    }
    
    
    /**
     * Sets the directory in which all the recompiled class files will be
     * placed.  This directory should never be confused with the original
     * class file location.
     */
    public void setOutClassDir( File f )
    {
        this.outfiledir = f;
    }
    
    
    /**
     * Creates a new analysis module.
     */
    public AnalysisModuleType createAnalysisModule()
    {
        AnalysisModuleType amt = new AnalysisModuleType();
        this.analysisModules.addElement( amt );
        return amt;
    }
    
    
    /**
     * Create the property file settings.
     */
    public Configuration createLogSettings()
            throws BuildException
    {
        if (this.config != null)
        {
            throw new BuildException(
                "Only one logsetting tag can be in this target." );
        }
        this.config = new Configuration();
        return this.config;
    }
    
    
    
    /**
     * Perform the task
     */
    public void execute()
            throws BuildException
    {
        log( "Use of the CoveragePostCompiler task is deprecated; please "+
            "use the 'grobo-instrument' task instead.  You may need to "+
            "change the taskdef to reference the new resource file, located "+
            "at [ant-grobocoverage.properties], instead of "+
            "[net/sourceforge/groboutils/codecoverage/grobocoverage.properties"+
            "].", Project.MSG_WARN );
        
        // pre-check
        if (this.datadir == null)
        {
            throw new BuildException( "Attribute 'datadir' was never set." );
        }
        if (this.outfiledir == null)
        {
            throw new BuildException(
                "Attribute 'outfiledir' was never set." );
        }
        
        // bug 906316: ensure the directories exist...
        if (!this.datadir.exists())
        {
            this.datadir.mkdirs();
        }
        if (!this.outfiledir.exists())
        {
            this.outfiledir.mkdirs();
        }
        
        ClassFile classFiles[] = getFilenames();
        try
        {
            log( "Writing meta-data to directory '"+this.datadir+"'.",
                Project.MSG_VERBOSE );
            DirMetaDataWriter dmdw = new DirMetaDataWriter( this.datadir );
            try
            {
                PostCompileClass pcc = new PostCompileClass( dmdw,
                    getAnalysisModules() );
                for (int i = 0; i < classFiles.length; ++i)
                {
                    File infile = classFiles[i].srcFile;
                    String filename = classFiles[i].filename;
                    
                    // create the output class file, and ensure that
                    // its directory structure exists before creating it
                    File outfile = new File( this.outfiledir, filename );
                    log( "Recompiling class '"+infile+"' to file '"+
                        outfile+"'.", Project.MSG_VERBOSE );
                    File parent = outfile.getParentFile();
                    if (!parent.exists())
                    {
                        parent.mkdirs();
                    }
                    
                    // need some code handle the situation where the
                    // outfile may be the same as the infile.  This will
                    // also allow us to correctly handle the situation of
                    // an exception not properly creating the instrumented
                    // class.  See bug 929332.
                    File tmpout = FILEUTILS.createTempFile( outfile.getName(),
                        ".tmp", parent );
                    FileOutputStream fos = new FileOutputStream( tmpout );
                    
                    try
                    {
                        pcc.postCompile( filename, readFile( infile ), fos );
                        fos.close();
                        fos = null;
                        FILEUTILS.copyFile( tmpout, outfile );
                    }
                    catch (AlreadyPostCompiledException apce)
                    {
                        // see bug 903837
                        log( "Ignoring '"+infile+"': it has already been "+
                            "post-compiled.", Project.MSG_INFO );
                    }
                    finally
                    {
                        if (fos != null)
                        {
                            fos.close();
                        }
                        if (tmpout.exists())
                        {
                            tmpout.delete();
                        }
                    }
                }
            }
            finally
            {
                dmdw.close();
            }
        }
        catch (IOException ioe)
        {
            throw new BuildException( "I/O exception during execution.",
                ioe, getLocation() );
        }
        
        // generate the property file
        if (this.config == null)
        {
            // use defaults if it wasn't set.
            this.config = new Configuration();
        }
        try
        {
            this.config.generatePropertyFile( this.outfiledir,
                this.analysisModules.size() );
        }
        catch (IOException ioe)
        {
            throw new BuildException( "I/O exception during execution.",
                ioe, getLocation() );
        }
    }
    
    
    
    /**
     * 
     */
    private IAnalysisModule[] getAnalysisModules()
            throws BuildException
    {
        final Vector v = new Vector();
        final Enumeration enum = this.analysisModules.elements();
        while (enum.hasMoreElements())
        {
            AnalysisModuleType amt = (AnalysisModuleType)enum.nextElement();
            IAnalysisModule am = amt.getAnalysisModule();
            v.addElement( am );
        }
        final IAnalysisModule[] amL = new IAnalysisModule[ v.size() ];
        v.copyInto( amL );
        return amL;
    }
    
    
    
    
    /**
     * Iterate over all filesets and return the filename of all files
     * that end with <tt>.class</tt> (case insensitive). This is to avoid
     * trying to parse a non-class file.
     *
     * @return an array of filenames to parse.
     */
    private ClassFile[] getFilenames()
    {
        Vector v = new Vector();
        final int size = this.filesets.size();
        for (int j = 0; j < size; j++) 
        {
            FileSet fs = (FileSet)filesets.elementAt( j );
            DirectoryScanner ds = fs.getDirectoryScanner( getProject() );
            File baseDir = ds.getBasedir();
            ds.scan();
            String[] f = ds.getIncludedFiles();
            for (int k = 0; k < f.length; k++) 
            {
                String pathname = f[k];
                if (pathname.toLowerCase().endsWith( CLASSNAME_EXT )) 
                {
                    // this isn't right
                    v.addElement( new ClassFile( baseDir, pathname ) );
                }
            }
        }

        ClassFile[] files = new ClassFile[v.size()];
        v.copyInto(files);
        return files;
    }
    
    
    private static final class ClassFile
    {
        public File srcFile;
        public String filename;
        
        public ClassFile( File baseDir, String filename )
        {
            if (baseDir == null || filename == null)
            {
                throw new IllegalArgumentException("no null args.");
            }
            this.filename = filename;
            this.srcFile = new File( baseDir, filename );
        }
    }
    
    
    public static final class Configuration
    {
        public String factory =
            "net.sourceforge.groboutils.codecoverage.v2.logger.DirectoryChannelLoggerFactory";
        public File logDir;
        public int cacheSize = -1;
        
        public void setFactory( String name )
        {
            if (name.indexOf(".") < 0)
            {
                this.factory =
                    "net.sourceforge.groboutils.codecoverage.v2.logger."+
                    name;
            }
            else
            {
                this.factory = name;
            }
        }
        
        
        public void setLogdir( File dirname )
        {
            this.logDir = dirname;
        }
        
        
        public void setCacheSize( int size )
        {
            this.cacheSize = size;
        }
        
        
        public void generatePropertyFile( File outfiledir, int moduleCount )
                throws IOException
        {
            Properties props = new Properties();
            if (this.factory != null)
            {
                props.setProperty( "factory", this.factory );
            }
            if (this.logDir != null)
            {
                props.setProperty( "logger.dir",
                    this.logDir.getAbsolutePath() );
            }
            if (this.cacheSize > 0)
            {
                props.setProperty( "logger.cache-size",
                    Integer.toString( this.cacheSize ) );
            }
            props.setProperty( "channel-count",
                Integer.toString( moduleCount ) );
            
            FileOutputStream fos = new FileOutputStream(
                new File( outfiledir, "grobocoverage.properties" ) );
            try
            {
                props.store( fos, "CodeCoverage setup file" );
            }
            finally
            {
                fos.close();
            }
        }
    }
    
    
    /**
     * 
     */
    private byte[] readFile( File file )
            throws IOException
    {
        FileInputStream fis = new FileInputStream( file );
        try
        {
            byte[] outfile = ReadByteStream.readByteStream( fis );
            return outfile;
        }
        finally
        {
            fis.close();
        }
    }
}

