/*
 * Copyright  2000-2004 The Apache Software Foundation
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.tools.ant.taskdefs.optional.ejb;

import java.io.File;
import java.io.IOException;
import java.io.FileInputStream;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.ArrayList;

import javax.xml.parsers.SAXParser;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.Execute;
import org.apache.tools.ant.taskdefs.ExecuteStreamHandler;
import org.apache.tools.ant.taskdefs.Javac;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.Path;
import org.xml.sax.SAXException;
import org.xml.sax.InputSource;
import org.xml.sax.AttributeList;

/**
 * <p>Title: BorlandCMPDeploymentTool.java</p>
 * <p>Description: Ant task to generate _PM classes for Borland Enterprise Server</p>
 *
 * BorlandCMPDeploymentTool extends the BorlandDeploymentTool task and adds support for
 * the following additional attribute (required):
 *
 * <ul>
 * <li>cmpdir (String)     : directory for generated _PM java and class files
 * </ul>
 *
 *<PRE>
 *
 *      &lt;ejbjar srcdir=&quot;${build.classes}&quot;
 *               basejarname=&quot;vsmp&quot;
 *               descriptordir=&quot;${rsc.dir}/hrmanager&quot;&gt;
 *        &lt;borlandcmp destdir=&quot;tstlib&quot; cmpdir=&quot;tmpdir&quot;&gt;
 *          &lt;classpath refid=&quot;classpath&quot; /&gt;
 *        &lt;/borlandcmp&gt;
 *        &lt;include name=&quot;**\ejb-jar.xml&quot;/&gt;
 *        &lt;support dir=&quot;${build.classes}&quot;&gt;
 *          &lt;include name=&quot;demo\smp\*.class&quot;/&gt;
 *          &lt;include name=&quot;demo\helper\*.class&quot;/&gt;
 *         &lt;/support&gt;
 *     &lt;/ejbjar&gt;
 *</PRE>
 *
 * The generated classes will be added to the ejb jar.
 *
 * @author David LeRoy
 *
 */
public class BorlandCMPDeploymentTool extends BorlandDeploymentTool
                                      implements ExecuteStreamHandler
{
    /** Vbj executable **/
    protected static final String VBJ = "vbj";

    // EntityBeanCodeGen class
    protected static final String EBCG = "com.borland.ejb.pm.EntityBeanCodeGen";

    // _PM classes generated by this tool
    private Hashtable cmpClasses = new Hashtable();

    // ArrayList of Object[2]: { beanClass, homeInterface }
    private static ArrayList cmpTargets = new ArrayList();

    /** Instance variable that stores the directory where generated files are placed*/
    private File cmpDir = null;

    /**
     * set the directory where generated _PM CMP files should go: required
     * @param cmpDir the destination directory.
     */
    public void setCmpdir(File cmpDir)
    {
        this.cmpDir = cmpDir;
    }

    /**
     * Called to validate that the tool parameters have been configured.
     *
     * @throws org.apache.tools.ant.BuildException If the Deployment Tool's configuration isn't
     *                        valid
     */
    public void validateConfigured() throws BuildException
    {
        super.validateConfigured();
        if ((cmpDir == null) || (!cmpDir.isDirectory())) {
            String msg = "A valid directory for generated files must be specified "
            + "using the \"gendir\" attribute.";
            throw new BuildException(msg, getLocation());
        }
    }

    public void processDescriptor(String descriptorFileName, SAXParser saxParser)
    {
        checkConfiguration(descriptorFileName, saxParser);
        try {
            DescriptorHandler handler =  new CMPDescriptorHandler(getTask(), getConfig().srcDir);
            for (Iterator i = getConfig().dtdLocations.iterator(); i.hasNext();) {
                EjbJar.DTDLocation dtdLocation = (EjbJar.DTDLocation) i.next();
                handler.registerDTD(dtdLocation.getPublicId(), dtdLocation.getLocation());
            }
            FileInputStream descriptorStream = null;
            try {
                File descriptor = new File(getConfig().descriptorDir, descriptorFileName);
                descriptorStream = new FileInputStream(descriptor);
                InputSource inputSource = new InputSource(descriptorStream);
                cmpTargets.clear();     // will be populated during parsing
                saxParser.parse(inputSource, handler);
            }
            finally {
                if (descriptorStream != null) {
                    try {
                        descriptorStream.close();
                    }
                    catch (IOException closeException) {
                        // ignore
                    }
                }
            }

        }
        catch (SAXException se) {
            String msg = "SAXException while parsing '"
            + descriptorFileName.toString()
            + "'. This probably indicates badly-formed XML."
            + "  Details: "
            + se.getMessage();
            throw new BuildException(msg, se);
        }
        catch (IOException ioe) {
            String msg = "IOException while parsing'"
            + descriptorFileName.toString()
            + "'.  This probably indicates that the descriptor"
            + " doesn't exist. Details: "
            + ioe.getMessage();
            throw new BuildException(msg, ioe);
        }
        super.processDescriptor(descriptorFileName, saxParser);
    }

    /**
     * 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.
     */
    protected void writeJar(String baseName, File jarFile, Hashtable files, String publicId)
        throws BuildException
    {
        // destination directory
        String destPath = cmpDir.getAbsolutePath() + File.separator + baseName;
        generateCMPClasses(destPath);
        //add the generated files to the collection
        files.putAll(cmpClasses);

        super.writeJar(baseName, jarFile, files, publicId);
    }

    /**
     * convert a file name : A/B/C/toto.java
     * into    a class name: A/B/C/toto.class
     */
    private String toClassFile(String filename)
    {
        //remove the .class
        String classfile = filename.substring(0, filename.lastIndexOf(".java"));
        classfile = classfile + ".class";
        return classfile;
    }

    private void generateCMPClasses(String destPath)
    {
        Iterator iterator = cmpTargets.iterator();
        while (iterator.hasNext()) {
            Object[] targets = (Object[]) iterator.next();
            Execute execTask = new Execute(this);
            Project project = getTask().getProject();
            execTask.setAntRun(project);
            execTask.setWorkingDirectory(project.getBaseDir());
            Commandline commandline = new Commandline();
            commandline.setExecutable(VBJ);
            //set the classpath
            commandline.createArgument().setValue("-VBJclasspath");
            commandline.createArgument().setPath(getCombinedClasspath());
            // com.borland.ejb.pm.EntityBeanCodeGen
            commandline.createArgument().setValue(EBCG);
            // destination directory
            commandline.createArgument().setValue(destPath);
            // Bean class
            commandline.createArgument().setValue(targets[0].toString());
            // Home class
            commandline.createArgument().setValue(targets[1].toString());
            try {
                log("Calling vbj", Project.MSG_VERBOSE);
                log(commandline.describeCommand(), Project.MSG_VERBOSE);
                execTask.setCommandline(commandline.getCommandline());
                int result = execTask.execute();
                if (Execute.isFailure(result)) {
                    String msg = "Failed executing vbj (ret code is "
                    + result + ")";
                    throw new BuildException(msg, getTask().getLocation());
                }
            }
            catch (java.io.IOException e) {
                log("vbj exception :" + e.getMessage(), Project.MSG_ERR);
                throw new BuildException(e, getTask().getLocation());
            }
        }
        Path path = new Path(getTask().getProject(), destPath);
        File dir = new File(destPath);
        dir.mkdir();
        Javac javac = null;
        javac = (Javac) getTask().getProject().createTask("javac");
        javac.setClasspath(getCombinedClasspath());
        javac.setDestdir(dir);
        javac.setSrcdir(path);
        javac.execute();
        File[] files = javac.getFileList();
        cmpClasses.clear();
        for (int i = 0; i < files.length; i++) {
            String classFile = toClassFile(files[i].getAbsolutePath());
            String key = classFile.substring(destPath.length() + 1);
            log("compiled " + classFile, Project.MSG_VERBOSE);
            log("added class file under key: " + key, Project.MSG_VERBOSE);
            cmpClasses.put(key, new File(classFile));
        }
    }

    /**
     * Inner class to re-parse ejb-jar.xml, looking for all CMP 2.x entity beans.
     * This handler will add an element to the cmpTargets ArrayList consisting
     * of the the bean class name and the home interface name for each CMP 2.x
     * entity bean that is found in the deployment descriptor.
     */
    private class CMPDescriptorHandler extends DescriptorHandler
    {
        private int parseState = SCANNING;      // state of parser
        private boolean inEJBRef = false;       // inside ejb-ref or ejb-local-ref?
        private boolean isCMP    = false;

        private static final int SCANNING  = 0; // outside <ejb-jar> ... </ejb-jar>
        private static final int IN_EJBJAR = 1; // inside <ejb-jar> ... </ejb-jar>
        private static final int IN_BEANS  = 2; // inside <enterprise-beans> ... </enterprise-beans>
        private static final int IN_ENTITY = 3; // inside <entity> ... </entity>

        private static final String EJB_JAR              = "ejb-jar";
        private static final String ENTERPRISE_BEANS     = "enterprise-beans";
        private static final String ENTITY_BEAN          = "entity";
        private static final String HOME_INTERFACE       = "home";
        private static final String LOCAL_HOME_INTERFACE = "local-home";
        private static final String BEAN_CLASS           = "ejb-class";
        private static final String PERSISTENCE_TYPE     = "persistence-type";
        private static final String CONTAINER            = "Container";
        private static final String CMP_VERSION          = "cmp-version";
        private static final String CMP_2_X              = "2.x";
        private static final String EJB_REF              = "ejb-ref";
        private static final String EJB_LOCAL_REF        = "ejb-local-ref";

        private String beanClass = "";          // name of entity bean component class
        private String homeInterface = "";      // name of entity bean home interface
        private String cmpVersion = "";         // CMP version: only 2.x supported

        /**
         * 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 name The name of the element being entered.
         */
        public void startElement(String name, AttributeList attrList) throws SAXException
        {
            currentText = "";
            switch (parseState) {
            case SCANNING:
                if (name.equals(EJB_JAR)) {
                    parseState = IN_EJBJAR;
                }
                break;

            case IN_EJBJAR:
                if (name.equals(ENTERPRISE_BEANS)) {
                    parseState = IN_BEANS;
                }
                break;

            case IN_BEANS:
                if (name.equals(ENTITY_BEAN)) {
                    beanClass = null;
                    homeInterface = null;
                    cmpVersion = CMP_2_X;         // unless stated otherwise
                    parseState = IN_ENTITY;
                    inEJBRef = false;
                    isCMP = false;
                }
                break;

            case IN_ENTITY:
                if (name.equals(EJB_REF) || name.equals(EJB_LOCAL_REF)) {
                    inEJBRef = true;
                }
                break;
            }
        }

        /**
         * 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 name The name of the attribute being exited.
         */
        public void endElement(String name) throws SAXException
        {
            switch (parseState) {
            case IN_ENTITY:
                if (!inEJBRef) {
                    if (name.equals(LOCAL_HOME_INTERFACE)) {
                        homeInterface = currentText;
                    } else if (name.equals(HOME_INTERFACE)) {
                        if (homeInterface == null) {
                            homeInterface = currentText;
                        }
                    } else if (name.equals(BEAN_CLASS)) {
                        beanClass = currentText;
                    } else if (name.equals(PERSISTENCE_TYPE)) {
                        if (currentText != null
                        &&  currentText.equalsIgnoreCase(CONTAINER)) {
                            isCMP = true;
                        }
                    } else if (name.equals(CMP_VERSION)) {
                        cmpVersion = currentText;
                    } else if (name.equals(ENTITY_BEAN)) {
                        if (isCMP
                        &&  cmpVersion.equals(CMP_2_X)
                        &&  beanClass != null
                        &&  homeInterface != null) {
                            Object[] classes = { beanClass, homeInterface };
                            cmpTargets.add(classes);
                        }
                        parseState = IN_BEANS;
                    }
                } else {
                    if (name.equals(EJB_REF) || name.equals(EJB_LOCAL_REF)) {
                        inEJBRef = false;
                    }
                }
                break;

            case IN_BEANS:
                if (name.equals(ENTERPRISE_BEANS)) {
                        parseState = IN_EJBJAR;
                }
                break;

            case IN_EJBJAR:
                if (name.equals(EJB_JAR)) {
                    parseState = SCANNING;
                }
                break;
            }
            currentText = "";
        }

        public CMPDescriptorHandler(Task task, File srcDir)
        {
            super(task, srcDir);
        }
    }
}