package com.sapient.tfenne.ant;

// Standard java imports
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.File;
import java.io.IOException;
import java.util.jar.*;
import java.util.zip.*;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.Iterator;

// XML and Apache/Ant imports
import org.xml.sax.Parser;
import org.xml.sax.Locator;
import org.xml.sax.InputSource;
import org.xml.sax.AttributeList;
import org.xml.sax.DocumentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.ParserFactory;

// Apache/Ant imports
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.taskdefs.Java;
/**
 * <p>Provides automated ejb jar file creation for ant.  Extends the MatchingTask
 * class provided in the default ant distribution and also implements the
 * DocumentHandler interface for communicating with a SAX XML parser.</p>
 *
 * <p>The jar file is constructed by examining the EJB 1.1 compliant deployment
 * descriptor provided for the classnames of all classes involved in the
 * deployment.  These class names are translated into file names and paths (with
 * a little extra help).  All of the classes are then collected into a jar file
 * and then the deployment descriptor(s) is/are added in the appropriate
 * location in the jar file.</p>
 *
 * <p>Functionality is currently provided for standard EJB1.1 jars and Weblogic
 * 5.1 jars. Task works by scanning for the standard deployment descriptors (see
 * MatchingTask documentation for scanning semantics) and then constructing a jar
 * for each deployment descriptor found. Weblogic deployment descriptors are
 * found taking the remote interface name of the bean and appending '-' followed
 * by the regular name of the descriptor. As such, this functionality will only
 * work if deployment descriptors and beans match one to one.
 * </p>
 *
 * @author <a href="mailto:tfenne@sapient.com">Tim Fennell</a>
 */
public class EjbScanner extends MatchingTask implements DocumentHandler {

    /**
     * Bunch of constants used for storing entries in a hashtable, and for
     * constructing the filenames of various parts of the ejb jar.
     */
    private static final String HOME_INTERFACE   = "home";
    private static final String REMOTE_INTERFACE = "remote";
    private static final String BEAN_CLASS       = "ejb-class";
    private static final String PK_CLASS         = "prim-key-class";

    private static final String META_DIR  = "META-INF/";
    private static final String EJB_DD    = "ejb-jar.xml";
    private static final String WL_DD     = "weblogic-ejb-jar.xml";
    private static final String WL_CMP_DD = "weblogic-cmp-rdbms-jar.xml";

    /** Stores a handle to the directory under which to search for files */
    private File srcdir = null;

    /** Stores a handle to the directory to put the Jar files in */
    private File destdir = null;

    /** Instance variable that determines whether to generate weblogic jars. */
    private boolean generateweblogic = true;

    /** Instance variable that determines whether generic ejb jars are kept. */
    private boolean keepgeneric = true;

    /** Instance variable that stores the suffix for the generated jarfile. */
    private String genericjarsuffix = "-generic.jar";

    /** Instance variable that stores the suffix for the weblogic jarfile. */
    private String weblogicjarsuffix = "-wl.jar";

    /**
     * Instance variable used to store the name of the current attribute being
     * processed by the SAX parser.  Accessed by the SAX parser call-back methods
     * startElement() and endElement().
     */
    private String currentAttribute = null;

    /**
     * Instance variable used to store the name of the remote interface with
     * the leading path information, because this is the base of all of our
     * filenames.
     */
    private String beanName = null;
    

    /**
     * Instance variable that stores the names of the files as they will be put
     * into the jar file, mapped to File objects  Accessed by the SAX parser
     * call-back method characters().
     */
    private Hashtable ejbFiles = null;
    

    /*******************************************************
     * Start of SAX parser call-back methods
     *******************************************************/

    /**
     * SAX parser call-back method that does nothing in this implementation.
     */
    public void setDocumentLocator(Locator locator) {
        // do nothing
    }

    /**
     * SAX parser call-back method that is used to initialize the values of some
     * instance variables to ensure safe operation.
     */
    public void startDocument() throws SAXException {
        this.ejbFiles         = new Hashtable(10, 1);
        this.currentAttribute = null;
        this.beanName         = null;
    }

    /**
     * SAX parser call-back method that does nothing in this implementation.
     */
    public void endDocument() throws SAXException {
        // do nothing
    }

    /**
     * SAX parser call-back method that is invoked when a new element is entered
     * into.  Used to store the context (attribute name) in the currentAttribute
     * instance variable.
     * @param <code>name</code> The name of the element being entered.
     * @param <code>attrs</code> Attributes associated to the element.
     */
    public void startElement(String name, AttributeList attrs) 
        throws SAXException {
        this.currentAttribute = name;
    }

    /**
     * SAX parser call-back method that is invoked when an element is exited.
     * Used to blank out (set to the empty string, not nullify) the name of
     * the currentAttribute.  A better method would be to use a stack as an
     * instance variable, however since we are only interested in leaf-node
     * data this is a simpler and workable solution.
     * @param <code>name</code> The name of the attribute being exited. Ignored
     *        in this implementation.
     */
    public void endElement(String name) throws SAXException {
        this.currentAttribute = "";
    }

    /**
     * SAX parser call-back method invoked whenever characters are located within
     * an element.  currentAttribute (modified by startElement & endElement)
     * tells us whether we are in an interesting element (one of the up to four
     * classes of an EJB).  If so then converts the classname from the format
     * org.apache.tools.ant.Parser to the convention for storing such a class,
     * org/apache/tools/ant/Parser.class.  This is then resolved into a file
     * object under the srcdir which is stored in the
     * @param <code>ch</code> A character array containing all the characters in
     *        the element, and maybe others that should be ignored.
     * @param <code>start</code> An integer marking the position in the char
     *        array to start reading from.
     * @param <code>length</code> An integer representing an offset into the
     *        char array where the current data terminates.
     */
    public void characters(char[] ch, int start, int length) 
        throws SAXException {
        if (currentAttribute.equals(EjbScanner.HOME_INTERFACE)   ||
            currentAttribute.equals(EjbScanner.REMOTE_INTERFACE) ||
            currentAttribute.equals(EjbScanner.BEAN_CLASS)       ||
            currentAttribute.equals(EjbScanner.PK_CLASS)) {
            
            // dunk the filename into a vector...
            File classFile = null;
            String className = new String(ch, start, length);
            className = className.replace('.', File.separator.charAt(0));
            
            // If it's the remote interface we have then store it's name in
            // this.beanName because we'll use it to construct other filenames.
            if ( currentAttribute.equals(EjbScanner.REMOTE_INTERFACE) ) {
                this.beanName = className;
            }
            
            // Add the .class to the filename, and put it into the Hashtable.
            className += ".class";
            classFile = new File(this.srcdir, className);
            ejbFiles.put(className, classFile);
        }
    }

    /**
     * SAX parser call-back method that does nothing in this implementation.
     */
    public void ignorableWhitespace(char[] ch, int start, int length) 
        throws SAXException {
        // do nothing
    }

    /**
     * SAX parser call-back method that does nothing in this implementation.
     */
    public void processingInstruction(String target, String data) 
        throws SAXException {
        // do nothing
    }


    /*******************************************************
     * Start of Ant Task Methods
     *******************************************************/

    /**
     * Setter used to store the value of srcdir prior to execute() being called.
     * @param <code>inDir</code> The string indicating the source directory.
     */
    public void setSrcdir(String inDir) {
        this.srcdir = this.project.resolveFile(inDir);
    }

    /**
     * Setter used to store the value of destination directory prior to execute()
     * being called.
     * @param <code>inFile</code> The string indicating the source directory.
     */
    public void setDestdir(String inDir) {
        this.destdir = this.project.resolveFile(inDir);
    }

    /**
     * Setter used to store the suffix for the generated jar file.
     * @param inString the string to use as the suffix.
     */
    public void setGenericjarsuffix(String inString) {
        this.genericjarsuffix = inString;
    }

    /**
     * Setter used to store the suffix for the generated weblogic jar file.
     * @param inString the string to use as the suffix.
     */
    public void setWeblogicjarsuffix(String inString) {
        this.weblogicjarsuffix = inString;
    }

    /**
     * Setter used to store the value of generateweblogic.
     * @param <code>inValue</code> a string, either 'true' or 'false'.
     */
    public void setGenerateweblogic(String inValue) {
        this.generateweblogic = Boolean.valueOf(inValue).booleanValue();
    }

    /**
     * Setter used to store the value of keepgeneric
     * @param <code>inValue</code> a string, either 'true' or 'false'.
     */
    public void setKeepgeneric(String inValue) {
        this.keepgeneric = Boolean.valueOf(inValue).booleanValue();
    }

    /**
     * Utility method that encapsulates the logic of adding a file entry to
     * a .jar file.  Used by execute() to add entries to the jar file as it is
     * constructed.
     * @param <code>jStream</code> A JarOutputStream into which to write the
     *        jar entry.
     * @param <code>iStream</code> A FileInputStream from which to read the
     *        contents the file being added.
     * @param <code>filename</code> A String representing the name, including
     *        all relevant path information, that should be stored for the entry
     *        being added.
     */
    protected void addFileToJar(JarOutputStream jStream,
                                FileInputStream iStream,
                                String          filename)
        throws BuildException {
        try {
            // Create the zip entry and add it to the jar file
            ZipEntry zipEntry = new ZipEntry(filename);
            jStream.putNextEntry(zipEntry);
            
            // Create the file input stream, and buffer everything over
            // to the jar output stream
            byte[] byteBuffer = new byte[2 * 1024];
            int count = 0;
            do {
                jStream.write(byteBuffer, 0, count);
                count = iStream.read(byteBuffer, 0, byteBuffer.length);
            } while (count != -1);
            
            // Close up the file input stream for the class file
            iStream.close();
        }
        catch (IOException ioe) {
            String msg = "IOException while adding entry "
                         + filename + "to jarfile."
                         + ioe.getMessage();
            throw new BuildException(msg, ioe);
        }
    }

    /**
     * Method used to encapsulate the writing of the JAR file. Iterates over the
     * filenames/java.io.Files in the Hashtable stored on the instance variable
     * ejbFiles.
     */
    public void writeJar(File jarfile) throws BuildException{
        JarOutputStream jarStream = null;
        Iterator entryIterator = null;
        String entryName = null;
        File entryFile = null;

        try {
            /* If the jarfile already exists then whack it and recreate it.
             * Should probably think of a more elegant way to handle this
             * so that in case of errors we don't leave people worse off
             * than when we started =)
             */
            if (jarfile.exists()) jarfile.delete();
            jarfile.getParentFile().mkdirs();
            jarfile.createNewFile();
            
            // Create the streams necessary to write the jarfile
            jarStream = new JarOutputStream(new FileOutputStream(jarfile));
            jarStream.setMethod(JarOutputStream.DEFLATED);
            
            // Loop through all the class files found and add them to the jar
            entryIterator = this.ejbFiles.keySet().iterator();
            while (entryIterator.hasNext()) {
                entryName = (String) entryIterator.next();
                entryFile = (File) this.ejbFiles.get(entryName);
                
                this.project.log("[ejbgen] adding file '"
                                 + entryName
                                 + "'",
                                 Project.MSG_VERBOSE);

                addFileToJar(jarStream,
                             new FileInputStream(entryFile),
                             entryName);
            }
            // All done.  Close the jar stream.
            jarStream.close();
        }
        catch(IOException ioe) {
            String msg = "IOException while processing ejb-jar file '"
                + jarfile.toString()
                + "'. Details: "
                + ioe.getMessage();
            throw new BuildException(msg, ioe);
        }
    } // end of writeJar
    
    public void buildWeblogicJar(File sourceJar, File destJar) {
        org.apache.tools.ant.taskdefs.Java javaTask = null;
        
        try {
            // Unfortunately, because weblogic.ejbc calls system.exit(), we
            // cannot do it 'in-process'. If they ever fix this, we should
            // change this code - it would be much quicker!
            String args = sourceJar + " " + destJar;
            
            javaTask = (Java) this.project.createTask("java");
            javaTask.setClassname("weblogic.ejbc");
            javaTask.setArgs(args);
            javaTask.setFork("true");

            this.project.log("[ejbgen] Calling weblogic.ejbc for "
                             + sourceJar.toString(),
                             Project.MSG_INFO);

            javaTask.execute();
        }
        catch (Exception e) {
            // Have to catch this because of the semantics of calling main()
            String msg = "Exception while calling ejbc. Details: " + e.toString();
            throw new BuildException(msg, e);
        }
    }

    /**
     * Invoked by Ant after the task is prepared, when it is ready to execute
     * this task.  Parses the XML deployment descriptor to acquire the list of
     * files, then constructs the destination jar file (first deleting it if it
     * already exists) from the list of classfiles encountered and the descriptor
     * itself.  File will be of the expected format with classes under full
     * package hierarchies and the descriptor in META-INF/ejb-jar.xml
     * @exception <code>BuildException</code> thrown whenever a problem is
     *            encountered that cannot be recovered from, to signal to ant
     *            that a major problem occurred within this task.
     */
    public void execute() throws BuildException {
        boolean          needBuild  = true;
        DirectoryScanner ds         = null;
        String[]         files      = null;
        int              index      = 0;
        File             weblogicDD = null;
        File             jarfile    = null;
        File             wlJarfile  = null;
        File             jarToCheck = null;

        // Lets do a little asserting to make sure we have all the
        // required attributes from the task processor
        StringBuffer sb = new StringBuffer();
        boolean die = false;
        sb.append("Processing ejbjar - the following attributes ");
        sb.append("must be specified: ");
        if (this.srcdir     == null) { sb.append("srcdir ");     die = true; }
        if (this.destdir    == null) { sb.append("destdir");     die = true; }
        if ( die ) throw new BuildException(sb.toString());

        try {
            // Create the parser using whatever parser the system dictates
            Parser saxParser = ParserFactory.makeParser();

            // Register ourselves as the document handler class!
            saxParser.setDocumentHandler(this);

            ds = this.getDirectoryScanner(this.srcdir);
            ds.scan();
            files = ds.getIncludedFiles();

            this.project.log("[ejbgen] " + files.length 
                             + " deployment descriptors located.",
                             Project.MSG_VERBOSE);

            // Loop through the files. Each file represents one deployment
            // descriptor, and hence one bean in our model.
            for (index=0; index < files.length; ++index) {

                // By default we assume we need to build.
                needBuild = true;

                /* Parse the ejb deployment descriptor.  While it may not
                 * look like much, passing 'this' in the above method allows
                 * the parser to call us back when it finds interesting things.
                 */
                saxParser.parse(new InputSource
                                (new FileInputStream
                                 (new File(this.srcdir, files[index]))));
        
                /* Now that we've parsed the deployment descriptor we have the
                 * bean name, so we can figure out all the .xml filenames and
                 * add them to the set of files for the jar.
                 */

                // First the regular deployment descriptor
                ejbFiles.put(EjbScanner.META_DIR + EjbScanner.EJB_DD,
                             new File(this.srcdir, files[index]));

                // Then the weblogic deployment descriptor
                weblogicDD = new File(this.srcdir,
                                      this.beanName + "-" + EjbScanner.WL_DD);

                if (weblogicDD.exists()) {
                    ejbFiles.put(EjbScanner.META_DIR + EjbScanner.WL_DD,
                                 weblogicDD);
                }

                // The the weblogic cmp deployment descriptor
                weblogicDD = new File(this.srcdir,
                                      this.beanName
                                      + "-" + EjbScanner.WL_CMP_DD);

                if (weblogicDD.exists()) {
                    ejbFiles.put(EjbScanner.META_DIR + EjbScanner.WL_CMP_DD,
                                 weblogicDD);
                }

                // Lastly for the jarfiles
                jarfile = new File(this.destdir,
                                   this.beanName
                                   + this.genericjarsuffix);
                
                wlJarfile = new File(this.destdir,
                                     this.beanName
                                     + this.weblogicjarsuffix);
                
                /* Check to see if the jar file is already up to date. 
                 * Unfortunately we have to parse the descriptor just to do
                 * that, but it's still a saving over re-constructing the jar
                 * file each time. Tertiary is used to determine which jarfile
                 * we should check times against...think about it.
                 */
                jarToCheck = this.generateweblogic ? wlJarfile : jarfile;
                
                if (jarToCheck.exists()) {
                    long    lastBuild = jarToCheck.lastModified();
                    Iterator fileIter = this.ejbFiles.values().iterator();
                    File currentFile  = null;
                    
                    // Set the need build to false until we find out otherwise.
                    needBuild = false;

                    // Loop through the files seeing if any has been touched
                    // more recently than the destination jar.
                    while( (needBuild == false) && (fileIter.hasNext()) ) {
                        currentFile = (File) fileIter.next();
                        needBuild = ( lastBuild < currentFile.lastModified() );
                    }
                }
                
                // Check to see if we need a build and start
                // doing the work!
                if (needBuild) {
                    // Log that we are going to build...
                    this.project.log("[ejbgen] building "
                                     + jarfile.getName()
                                     + " with "
                                     + String.valueOf(this.ejbFiles.size())
                                     + " total files",
                                     Project.MSG_INFO);

                    // Use helper method to write the jarfile
                    this.writeJar(jarfile);

                    // Generate weblogic jar if requested
                    if (this.generateweblogic) {
                        this.buildWeblogicJar(jarfile, wlJarfile);
                    }

                    // Delete the original jar if we weren't asked to keep it.
                    if (!this.keepgeneric) {
                        this.project.log("[ejbgen] deleting jar "
                                         + jarfile.toString());
                        jarfile.delete();
                    }
                }
                else {
                    // Log that the file is up to date...
                    this.project.log("[ejbgen] file "
                                     + jarfile.toString()
                                     + " is up to date.");
                }
            }
        }
        catch (SAXException se) {
            String msg = "SAXException while parsing '"
                + files[index].toString()
                + "'. This probably indicates badly-formed XML."
                + "  Details: "
                + se.getMessage();
            throw new BuildException(msg, se);
        }
        catch (ClassNotFoundException cnfe) {
            String msg = "ClassNotFoundException while parsing '"
                + files[index].toString()
                + "'.  This probably indicates that the parser: "
                + "com.sun.xml.parser.Parser could not be located."
                + "  Check that ant/lib/xml.jar is in your classpath."
                + "Details: "
                + cnfe.getMessage();
            throw new BuildException(msg, cnfe);
        }
        catch (IllegalAccessException iae) {
            String msg = "IllegalAccessException while parsing '"
                + files[index].toString()
                + "'.  This probably indicates that the parser: "
                + "com.sun.xml.parser.Parser could not be created."
                + "  This is bad!  It represents a lack of permission."
                + "  Are you programmaticailly calling Ant?"
                + "Details: "
                + iae.getMessage();
            throw new BuildException(msg, iae);
        }
        catch (InstantiationException ie) {
            String msg = "InstatiationException while parsing '"
                + files[index].toString()
                + "'.  This probably indicates that the parser: "
                + "com.sun.xml.parser.Parser could not be created."
                + "  This is bad!  Time to start hacking apart your"
                + " java environment my friend!"
                + "Details: "
                + ie.getMessage();
            throw new BuildException(msg, ie);
        }
        catch (IOException ioe) {
            String msg = "IOException while parsing'"
                + files[index].toString()
                + "'.  This probably indicates that the descriptor"
                + " doesn't exist. Details:"
                + ioe.getMessage();
            throw new BuildException(msg, ioe);
        }
    } // end of execute()
}







