/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 2000-2003 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "Ant" and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package org.ow2.jonas.antmodular.ejb2;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Hashtable;

import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * Inner class used by EjbJar to facilitate the parsing of deployment
 * descriptors and the capture of appropriate information. Extends
 * HandlerBase so it only implements the methods needed. During parsing
 * creates a hashtable consisting of entries mapping the name it should be
 * inserted into an EJB jar as to a File representing the file on disk. This
 * list can then be accessed through the getFiles() method.
 */
public class DescriptorHandler extends org.xml.sax.helpers.DefaultHandler {
    private static final int STATE_LOOKING_EJBJAR = 1;
    private static final int STATE_IN_EJBJAR = 2;
    private static final int STATE_IN_BEANS = 3;
    private static final int STATE_IN_SESSION = 4;
    private static final int STATE_IN_ENTITY = 5;
    private static final int STATE_IN_MESSAGE = 6;

    private static final int STATE_IN_SERVICE_REF = 7;
    private static final int STATE_IN_PORT_COMPONENT_REF = 8;
    private static final int STATE_IN_HANDLER = 9;


    private Task owningTask;

    private String publicId = null;

    /**
     * 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 EJB_REF               = "ejb-ref";
    private static final String EJB_LOCAL_REF         = "ejb-local-ref";
    private static final String HOME_INTERFACE        = "home";
    private static final String REMOTE_INTERFACE      = "remote";
    private static final String LOCAL_HOME_INTERFACE  = "local-home";
    private static final String LOCAL_INTERFACE       = "local";
    private static final String ENDPOINT_INTERFACE    = "service-endpoint";
    private static final String BEAN_CLASS            = "ejb-class";
    private static final String PK_CLASS              = "prim-key-class";
    private static final String EJB_NAME              = "ejb-name";
    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 SESSION_BEAN          = "session";
    private static final String MESSAGE_BEAN          = "message-driven";

    /**
     * Add support for service-ref elements
     */
    private static final String SERVICE_REF                = "service-ref";
    private static final String SERVICE_INTERFACE          = "service-interface";
    private static final String WSDL_FILE                  = "wsdl-file";
    private static final String JAXRPC_MAPPING_FILE        = "jaxrpc-mapping-file";
    private static final String PORT_COMPONENT_REF         = "port-component-ref";
    private static final String SERVICE_ENDPOINT_INTERFACE = "service-endpoint-interface";
    private static final String HANDLER                    = "handler";
    private static final String HANDLER_CLASS              = "handler-class";

    /**
     * The state of the parsing
     */
    private int parseState = STATE_LOOKING_EJBJAR;

    /**
     * The old state of the parsing (used only for service-ref)
     */
    private int oldParseState;

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

    /**
     * The text of the current element
     */
    protected String currentText = 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().
     */
    protected Hashtable ejbFiles = null;

    /**
     * Instance variable that stores the value found in the &lt;ejb-name&gt; element
     */
    protected String ejbName = null;

    private Hashtable fileDTDs = new Hashtable();

    private Hashtable resourceDTDs = new Hashtable();

    private boolean inEJBRef = false;

    private Hashtable urlDTDs = new Hashtable();

    /**
     * The directory containing the bean classes and interfaces. This is
     * used for performing dependency file lookups.
     */
    private File srcDir;

    public DescriptorHandler(Task task, File srcDir) {
        this.owningTask = task;
        this.srcDir = srcDir;
    }

    public void registerDTD(String publicId, String location) {
        if (location == null) {
            return;
        }

        File fileDTD = new File(location);
        if (!fileDTD.exists()) {
            // resolve relative to project basedir
            fileDTD = owningTask.getProject().resolveFile(location);
        }

        if (fileDTD.exists()) {
            if (publicId != null) {
                fileDTDs.put(publicId, fileDTD);
                owningTask.log("Mapped publicId " + publicId + " to file "
                    + fileDTD, Project.MSG_VERBOSE);
            }
            return;
        }

        if (getClass().getResource(location) != null) {
            if (publicId != null) {
                resourceDTDs.put(publicId, location);
                owningTask.log("Mapped publicId " + publicId + " to resource "
                    + location, Project.MSG_VERBOSE);
            }
        }

        try {
            if (publicId != null) {
                URL urldtd = new URL(location);
                urlDTDs.put(publicId, urldtd);
            }
        } catch (java.net.MalformedURLException e) {
            //ignored
        }

    }

    public InputSource resolveEntity(String publicId, String systemId)
        throws SAXException {
        this.publicId = publicId;

        File dtdFile = (File) fileDTDs.get(publicId);
        if (dtdFile != null) {
            try {
                owningTask.log("Resolved " + publicId + " to local file "
                    + dtdFile, Project.MSG_VERBOSE);
                return new InputSource(new FileInputStream(dtdFile));
            } catch (FileNotFoundException ex) {
                // ignore
            }
        }

        String dtdResourceName = (String) resourceDTDs.get(publicId);
        if (dtdResourceName != null) {
            InputStream is = this.getClass().getResourceAsStream(dtdResourceName);
            if (is != null) {
                owningTask.log("Resolved " + publicId + " to local resource "
                    + dtdResourceName, Project.MSG_VERBOSE);
                return new InputSource(is);
            }
        }

        URL dtdUrl = (URL) urlDTDs.get(publicId);
        if (dtdUrl != null) {
            try {
                InputStream is = dtdUrl.openStream();
                owningTask.log("Resolved " + publicId + " to url "
                    + dtdUrl, Project.MSG_VERBOSE);
                return new InputSource(is);
            } catch (IOException ioe) {
                //ignore
            }
        }

        owningTask.log("Could not resolve ( publicId: " + publicId
            + ", systemId: " + systemId + ") to a local entity", Project.MSG_INFO);

        return null;
    }

    /**
     * Getter method that returns the set of files to include in the EJB jar.
     */
    public Hashtable getFiles() {
        return (ejbFiles == null) ? new Hashtable() : ejbFiles;
    }

    /**
     * Get the publicId of the DTD
     */
    public String getPublicId() {
        return publicId;
    }

     /**
     * Getter method that returns the value of the &lt;ejb-name&gt; element.
     */
    public String getEjbName() {
        return ejbName;
    }

    /**
     * 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.currentElement = null;
        inEJBRef = false;
    }


    /**
     * 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 uri The namespace of the element being entered.
     * @param localname The name of the element being entered.
     * @param qname The qname of the element being entered.
     * @param attrs Attributes associated to the element.
     */
    public void startElement(String uri, String localname, String qname, Attributes attrs)
        throws SAXException {

        String name = qname;

        this.currentElement = name;
        currentText = "";
        if (name.equals(EJB_REF) || name.equals(EJB_LOCAL_REF)) {
            inEJBRef = true;
        } else if (parseState == STATE_LOOKING_EJBJAR && name.equals(EJB_JAR)) {
            parseState = STATE_IN_EJBJAR;
        } else if (parseState == STATE_IN_EJBJAR && name.equals(ENTERPRISE_BEANS)) {
            parseState = STATE_IN_BEANS;
        } else if (parseState == STATE_IN_BEANS && name.equals(SESSION_BEAN)) {
            parseState = STATE_IN_SESSION;
        } else if (parseState == STATE_IN_BEANS && name.equals(ENTITY_BEAN)) {
            parseState = STATE_IN_ENTITY;
        } else if (parseState == STATE_IN_BEANS && name.equals(MESSAGE_BEAN)) {
            parseState = STATE_IN_MESSAGE;
        } else if ( ((parseState == STATE_IN_SESSION)
                     || (parseState == STATE_IN_ENTITY)
                     || (parseState == STATE_IN_MESSAGE))
                    && (name.equals(SERVICE_REF))) {
            oldParseState = parseState;
            parseState = STATE_IN_SERVICE_REF;
        } else if ((parseState == STATE_IN_SERVICE_REF)
                   && (name.equals(PORT_COMPONENT_REF))) {
            parseState = STATE_IN_PORT_COMPONENT_REF;
        } else if ((parseState == STATE_IN_SERVICE_REF)
                   && (name.equals(HANDLER))) {
            parseState = STATE_IN_HANDLER;
        }
    }


    /**
     * 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. Ignored
     *        in this implementation.
     */
    public void endElement(String uri, String localname, String qname) throws SAXException {
        String name = qname;

        processElement();
        currentText = "";
        this.currentElement = "";
        if (name.equals(EJB_REF) || name.equals(EJB_LOCAL_REF)) {
            inEJBRef = false;
        } else if (parseState == STATE_IN_HANDLER && name.equals(HANDLER)) {
            parseState = STATE_IN_SERVICE_REF;
        } else if (parseState == STATE_IN_PORT_COMPONENT_REF && name.equals(PORT_COMPONENT_REF)) {
            parseState = STATE_IN_SERVICE_REF;
        } else if (parseState == STATE_IN_SERVICE_REF && name.equals(SERVICE_REF)) {
            parseState = oldParseState;
        } else if (parseState == STATE_IN_ENTITY && name.equals(ENTITY_BEAN)) {
            parseState = STATE_IN_BEANS;
        } else if (parseState == STATE_IN_SESSION && name.equals(SESSION_BEAN)) {
            parseState = STATE_IN_BEANS;
        } else if (parseState == STATE_IN_MESSAGE && name.equals(MESSAGE_BEAN)) {
            parseState = STATE_IN_BEANS;
        } else if (parseState == STATE_IN_BEANS && name.equals(ENTERPRISE_BEANS)) {
            parseState = STATE_IN_EJBJAR;
        } else if (parseState == STATE_IN_EJBJAR && name.equals(EJB_JAR)) {
            parseState = STATE_LOOKING_EJBJAR;
        }
    }

    /**
     * SAX parser call-back method invoked whenever characters are located within
     * an element.  currentAttribute (modified by startElement and 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 a Hashtable.
     * @param ch A character array containing all the characters in
     *        the element, and maybe others that should be ignored.
     * @param start An integer marking the position in the char
     *        array to start reading from.
     * @param length 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 {

        currentText += new String(ch, start, length);
    }


    protected void processElement() {
        if (inEJBRef
            || (parseState != STATE_IN_ENTITY
                && parseState != STATE_IN_SESSION
                && parseState != STATE_IN_MESSAGE
                && parseState != STATE_IN_SERVICE_REF
                && parseState != STATE_IN_PORT_COMPONENT_REF
                && parseState != STATE_IN_HANDLER)) {
            return;
        }

        if (currentElement.equals(HOME_INTERFACE)
            || currentElement.equals(REMOTE_INTERFACE)
            || currentElement.equals(LOCAL_INTERFACE)
            || currentElement.equals(ENDPOINT_INTERFACE)
            || currentElement.equals(LOCAL_HOME_INTERFACE)
            || currentElement.equals(BEAN_CLASS)
            || currentElement.equals(PK_CLASS)
            || currentElement.equals(SERVICE_INTERFACE)
            || currentElement.equals(SERVICE_ENDPOINT_INTERFACE)
            || currentElement.equals(HANDLER_CLASS)) {

            // Get the filename into a String object
            File classFile = null;
            String className = currentText.trim();

            // If it's a primitive wrapper then we shouldn't try and put
            // it into the jar, so ignore it.
            if (!className.startsWith("java.")
                && !className.startsWith("javax.")) {
                // Translate periods into path separators, add .class to the
                // name, create the File object and add it to the Hashtable.
                className = className.replace('.', File.separatorChar);
                className += ".class";
                classFile = new File(srcDir, className);
                ejbFiles.put(className, classFile);
            }
        }

        // service-ref support for simple file
        if (currentElement.equals(WSDL_FILE)
            || currentElement.equals(JAXRPC_MAPPING_FILE)) {
            //owningTask.log("***** element name : " + currentElement, Project.MSG_VERBOSE);
            String filename = currentText.trim();
            File file = new File(srcDir, filename);
            ejbFiles.put(filename, file);
        }

        // Get the value of the <ejb-name> tag.  Only the first occurrence.
        if (currentElement.equals(EJB_NAME)) {
            if (ejbName == null) {
                ejbName = currentText.trim();
            }
        }
    }
}
