/*
 * @(#)ModifiedClass.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.compiler;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.sourceforge.groboutils.codecoverage.v2.datastore.AnalysisModuleSet;
import net.sourceforge.groboutils.codecoverage.v2.datastore.ClassRecord;
import net.sourceforge.groboutils.codecoverage.v2.util.ChecksumUtil;
import net.sourceforge.groboutils.codecoverage.v2.util.ClassSignatureUtil;

import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.SourceFile;
import org.apache.bcel.generic.ConstantPoolGen;
import org.apache.bcel.generic.MethodGen;

/**
 * Refers to a class file that has been modified with additional logging
 * statements.
 *
 * @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 17, 2002
 */
public class ModifiedClass
{
    private static final org.apache.log4j.Logger LOG =
        org.apache.log4j.Logger.getLogger( ModifiedClass.class );
    
    // split these two strings apart so that this class itself can
    // be post-compiled.
    private static final String ALREADY_POST_COMPILED_FLAG_1 =
        "--..AlreadyPostCompiled";
    private static final String ALREADY_POST_COMPILED_FLAG_2 =
        // had to switch this from a char 0 due to parsing errors in
        // java - it doesn't like finding a '0' in a class file string.
        "..--" + (char)1;
    private static final String ALREADY_POST_COMPILED_FLAG;
    static {
        // don't let a compiler optimization create this string in this
        // class, so that this class can be instrumented.
        StringBuffer sb = new StringBuffer( ALREADY_POST_COMPILED_FLAG_1 );
        sb.append( ALREADY_POST_COMPILED_FLAG_2 );
        ALREADY_POST_COMPILED_FLAG = new String( sb );
    }
        
    private String className;
    private JavaClass origClass;
    //private ClassGen modClass;
    private ConstantPoolGen constantPool;
    private ModifiedMethod[] modMethods;
    private byte[] finalModifiedClass;
    
    /**
     * constant pool index for the name of the class's signature.
     */
    private int classSigPoolIndex;
    
    /**
     * constant pool index for the method-ref for invoking the logger.
     */
    private int staticMethodPoolIndex;
    
    /**
     * the original class file's checksum.
     */
    private long checksum;
    
    /**
     * We can't allow coverage of every method: some shouldn't be allowed,
     * such as javac-generated methods like "class$".
     */
    private static final String[] IGNORE_METHODS = {
            "class$(Ljava/lang/String;)Ljava/lang/Class;",
        };
    
    
    /**
     * Uses the default settings for the <tt>ParseCoverageLogger</tt>
     * during the creation of the modified class.
     *
     * @param filename the name of the file the class file was pulled from.
     * @throws IllegalStateException if the class file has already been
     *         modified (identified by a class name field).
     */
    public ModifiedClass( String filename, byte[] originalClassFile )
            throws IOException
    {
        this( new ParseCoverageLogger(), filename, originalClassFile );
    }
    
    
    /**
     * @param pcl the definition for the logger to create method references
     *         for.  This allows for easier testing and customizations of the
     *         logger to compile for.
     * @param filename the name of the file the class file was pulled from.
     * @throws IllegalStateException if the class file has already been
     *         modified (identified by a class name field).
     */
    public ModifiedClass( ParseCoverageLogger pcl, String filename,
            byte[] originalClassFile )
            throws IOException
    {
        if (originalClassFile == null || filename == null || pcl == null)
        {
            throw new IllegalArgumentException( "No null args." );
        }
        
        updateChecksum( originalClassFile );
        updateClassGen( pcl, originalClassFile, filename );
    }
    
    
    /**
     * 
     */
    public String getClassName()
    {
        return this.className;
    }
    
    
    /**
     * 
     */
    public long getClassCRC()
    {
        return this.checksum;
    }
    
    
    /**
     * 
     */
    public String getClassSignature()
    {
        return ClassSignatureUtil.getInstance().
            createClassSignature( getClassName(), getClassCRC() );
    }
    
    
    /**
     * 
     */
    public String getSourceFileName()
    {
        String ret = "";
        Attribute attr[] = this.origClass.getAttributes();
        for (int i = 0; i < attr.length; ++i)
        {
            if (attr[i] instanceof SourceFile)
            {
                ret = ((SourceFile)attr[i]).getSourceFileName();
                break;
            }
        }
        return ret;
    }
    
    
    /**
     * 
     */
    public ClassRecord createClassRecord( AnalysisModuleSet ams )
    {
        ModifiedMethod mmL[] = getMethods();
        String methodSigs[] = new String[ mmL.length ];
        for (int i = 0; i < mmL.length; ++i)
        {
            methodSigs[ mmL[i].getMethodIndex() ] = mmL[i].getMethodName();
        }
        return new ClassRecord( getClassName(), getClassCRC(),
            getSourceFileName(), methodSigs, ams );
    }
    
    
    /**
     * Return all modifiable methods owned by the class.  This will not return
     * methods deemed non-modifiable, such as abstract or native methods, or
     * javac-generated methods.
     */
    public ModifiedMethod[] getMethods()
    {
        if (this.modMethods == null)
        {
            checkClose();
            
            Method mL[] = this.origClass.getMethods();
            List methods = new ArrayList();
            short methodIndex = 0;
            for (int i = 0; i < mL.length; ++i)
            {
                if (allowModification( mL[i] ))
                {
                    ModifiedMethod mm = new ModifiedMethod(
                        methodIndex,
                        this.classSigPoolIndex,
                        this.staticMethodPoolIndex,
                        this.origClass,
                        mL[i],
                        new MethodGen( mL[i], this.className, this.constantPool )
                    );
                    methods.add( mm );
                    ++methodIndex;
                }
            }
            this.modMethods = (ModifiedMethod[])methods.toArray(
                new ModifiedMethod[ methods.size() ] );
        }
        return this.modMethods;
    }
    
    
    /**
     * Returns the class in its current state, and closes off the modified
     * class from further modifications.
     */
    public byte[] getModifiedClass()
    {
        if (this.finalModifiedClass == null)
        {
            checkClose();
            
            // commit the current changes to the generated class
            updateClass();
            
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try
            {
                this.origClass.dump( baos );
            }
            catch (IOException e)
            {
                throw new IllegalStateException("This should never be reached.");
            }
            
            // close off the modified class from further modification
            this.origClass = null;
            
            this.finalModifiedClass = baos.toByteArray();
        }
        return this.finalModifiedClass;
    }
    
    
    /**
     * String that gets placed within class files to flag that they've
     * been post-compiled.
     */
    public static final String getPostCompiledSignature()
    {
        return ALREADY_POST_COMPILED_FLAG;
    }
    
    
    //-------------------------------------------------------------------------
    // private members
    
    
    /**
     * This method commits any changes to the class file, and closes the
     * class off to any more changes.  It can be called multiple times
     * without error.
     */
    private void updateClass()
    {
        // update the methods
        ModifiedMethod mL[] = this.modMethods;
        if (mL == null)
        {
            // we've already committed the methods, or no modifications
            // were done to the methods.
            return;
        }
        
        // we're committing the methods, so close off our list
        this.modMethods = null;
        
        // create a map of all our modified methods.
        Map mLmap = new HashMap();
        for (int i = 0; i < mL.length; ++i)
        {
            // commit the methods and remove them from our list.
            ModifiedMethod m = mL[i];
            mL[i] = null;
            m.close();
            
            mLmap.put( m.getMethodName(), m.getNewMethod() );
        }
        
        // Replace the original methods with the modified methods.
        Method origMethods[] = this.origClass.getMethods();
        for (int i = 0; i < origMethods.length; ++i)
        {
            Method m = (Method)mLmap.get( createSignature( origMethods[i] ) );
            if (m != null)
            {
                LOG.debug( "replacing method ["+origMethods[i]+"]." );
                origMethods[i] = m;
            }
        }
        this.origClass.setMethods( origMethods );
        
        this.origClass.setConstantPool(
            this.constantPool.getFinalConstantPool() );
    }
    
    
    /**
     * 
     */
    private void updateClassGen( ParseCoverageLogger pcl, byte[] code,
            String filename )
            throws IOException
    {
        ByteArrayInputStream bais = new ByteArrayInputStream( code );
        ClassParser cp = new ClassParser( bais, filename );
        this.origClass = cp.parse();
        this.className = this.origClass.getClassName();
        //this.modClass = new ClassGen( this.origClass );
        //this.modClass.setMajor( this.origClass.getMajor() );
        //this.modClass.setMinor( this.origClass.getMinor() );
        this.constantPool = new ConstantPoolGen(
            this.origClass.getConstantPool() );
        
        addClassSignature( this.constantPool );
        addMethodRef( pcl, this.constantPool );
    }
    
    
    
    /**
     * Adds the class signature to the constant pool of the class.
     *
     * @throws IllegalStateException if the class file has already been
     *         modified (identified by a class name field).
     */
    private void addClassSignature( ConstantPoolGen pool )
    {
        if (pool == null)
        {
            throw new IllegalArgumentException( "No null args." );
        }
        
        // check if the class signature has already been set
        // see bug 903837
        if (pool.lookupUtf8( getPostCompiledSignature() ) != -1)
        {
            throw new AlreadyPostCompiledException(
                "Class '"+this.className+"' has already been modified." );
        }
        
        // add constant pool name reference
        this.classSigPoolIndex = pool.addString( getClassSignature() );
        LOG.debug( "Added pool index "+this.classSigPoolIndex+" pointing to '"+
            getClassSignature() );
        
        // and add the signature that the class has been post-compiled
        pool.addString( getPostCompiledSignature() );
    }
    
    
    
    /**
     * 
     */
    private void addMethodRef( ParseCoverageLogger pcl, ConstantPoolGen pool )
    {
        if (pool == null || pcl == null)
        {
            throw new IllegalArgumentException( "No null args." );
        }
        
        LOG.debug( "Adding methodref to pool for method: "+
            pcl.getClassName()+" # "+pcl.getMethodName() + ": "+
            pcl.getMethodSignature() );
        this.staticMethodPoolIndex = pool.addMethodref(
            pcl.getClassName(),
            pcl.getMethodName(),
            pcl.getMethodSignature() );
        LOG.debug( "Methodref pool index = "+this.staticMethodPoolIndex );
    }
    
    
    /**
     * 
     */
    private void updateChecksum( byte[] code )
    {
        if (code == null)
        {
            throw new IllegalArgumentException("no null args");
        }
        this.checksum = ChecksumUtil.getInstance().checksum( code );
    }
    
    
    /**
     * 
     */
    private void checkClose()
    {
        if (this.origClass == null)
        {
            throw new IllegalStateException(
                "Class has been closed from further modifications." );
        }
    }
    
    
    /**
     * Returns <tt>true</tt> if the method can be modified, otherwise returns
     * <tt>false</tt>.  A method can be modified only if it is not native,
     * not abstract, it has a code attribute, and is not one of the "ignored
     * methods" defined at the top of this class.
     */
    private boolean allowModification( Method m )
    {
        if (!isMarkable( m ))
        {
            return false;
        }
        
        String sig = createSignature( m );
        for (int i = 0; i < IGNORE_METHODS.length; ++i)
        {
            if (IGNORE_METHODS[i].equals( sig ))
            {
                return false;
            }
        }
        return true;
    }
    
    
    /**
     * Checks if the given method is markable only from the context of the
     * method contents, not by looking at the signature.
     */
    static boolean isMarkable( Method m )
    {
        return (!m.isNative() && !m.isAbstract() && m.getCode() != null);
    }
    
    
    /**
     * 
     */
    private String createSignature( Method m )
    {
        String sig = m.getName() + m.getSignature();
        return sig;
    }
}

