/*
 * $Id: BeanGenerator.java,v 1.3 2003/09/02 04:10:45 rago2483 Exp $
 */
package com.diginsite.services.presentation.cocoon.generation;

import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.Component;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.configuration.Configurable;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.configuration.ConfigurationException;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.cocoon.ProcessingException;
import org.apache.cocoon.environment.SourceResolver;
import org.apache.cocoon.generation.AbstractGenerator;
import org.apache.commons.betwixt.io.SAXBeanWriter;
import org.apache.commons.betwixt.strategy.CapitalizeNameMapper;
import org.xml.sax.SAXException;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * Generates an XML representation of a bean using Betwixt.
 * <p>
 * <b>Configuration:</b>
 * <p>
 * &lt;className&gt;name of class that creates the bean&lt;/className&gt; (required)<br>
 * &lt;getMethod&gt;name of method in className that creates the bean&lt;/getMethod&gt; (required)<br>
 * &lt;constructorParameters&gt; (optional)<br>
 * &nbsp&nbsp&lt;parameter name="parm name" type="java type" [role="role name"]&gt;[value]&lt;/parameter&gt;<br>
 * &lt;/constructorParameters&gt;<br>
 * &lt;parameters&gt; (optional)<br>
 * &nbsp&nbsp&lt;parameter name="parm name" type="java type" [role="role name"]&gt;[value]&lt;/parameter&gt;<br>
 * &lt;/parameters&gt;<br>
 * <p>
 * These configuration options are supported only at declaration time. Roles must
 * reference components that provide a get method that include the name of the type and
 * return that type (i.e. java.util.Properties getProperties()). Either a role or a value
 * must be specified for constructor parameters.
 * <p>
 * When the BeanGenerator is used in a pipeline, parameters to the get method may be
 * specified. The parameter name specified must match a parameter name in the
 * parameters definition. Get method parameters specified in the pipeline will override
 * values specified in the configuration.
 *
 * @version $Revision: 1.3 $
 * @created August 2003
 */
public class BeanGenerator extends AbstractGenerator implements Configurable, Composable
{
    /**
     * Configure this Generator for the Bean it will be processing. The configured
     * class will be created. Called by Cocoon during initialization
     *
     * @param config BeanGenerator configuration
     */
    public void configure(Configuration config)
            throws ConfigurationException
    {
        try
        {
            this.className = config.getChild("className").getValue();
            this.methodName = config.getChild("getMethod").getValue();
        }
        catch (ConfigurationException e)
        {
            getLogger().error("Invalid configuration", e);
            throw e;
        }

        ConfigParameter constructorParms[] = null;

        Configuration constructorConfig = config.getChild("constructorParameters", false);

        if (constructorConfig != null)
        {
            constructorParms = getParameters(constructorConfig, true);
        }

        this.methodParms = null;
        Configuration parameterConfig = config.getChild("parameters", false);

        if (parameterConfig != null)
        {
            this.methodParms = getParameters(parameterConfig, false);
        }

        try
        {
            this.factoryClass = Class.forName(this.className);

            this.objectFactory = null;

            if (constructorConfig == null)
            {
                this.objectFactory = factoryClass.newInstance();
            }
            else
            {
                Class parmTypes[] = new Class[constructorParms.length];
                Object parms[] = new Object[constructorParms.length];

                for (int i = 0; i < constructorParms.length; ++i)
                {
                    parms[i] = constructorParms[i].getValue();
                    parmTypes[i] = Class.forName(constructorParms[i].getType());
                }

                Constructor constructor = factoryClass.getConstructor(parmTypes);

                this.objectFactory = constructor.newInstance(parms);
            }
        }
        catch (Exception e)
        {
            getLogger().error("configure caught: ", e);
            ConfigurationException exception = new ConfigurationException(e.getMessage());
            throw exception;
        }
    }

    /**
     * Compose this Composable object. We need the manager to look up the role. Called
     * by Cocoon during initialization
     * @param cm the ComponentManager
     */
    public void compose(ComponentManager cm)
    {
        this.manager = cm;
    }

    /**
     * Sets up the BeanGenerator. Parameters will be read and the bean will be
     * generated by calling the get method of the configured class.
     *
     * @param resolver the source resolver
     * @param objectModel the Cocoon object model
     * @param src ignored by the BeanGenerator
     * @param par parameters to the get method
     * @throws ProcessingException
     * @throws SAXException
     * @throws IOException
     */
    public void setup(SourceResolver resolver, Map objectModel, String src,
                      Parameters par)
            throws ProcessingException, SAXException, IOException
    {
        super.setup(resolver, objectModel, src, par);

        Class parmTypes[] = new Class[this.methodParms.length];
        Object parms[] = new Object[this.methodParms.length];

        try
        {
            for (int i = 0; i < this.methodParms.length; ++i)
            {
                parms[i] = par.getParameter(methodParms[i].getName());
                parmTypes[i] = Class.forName(methodParms[i].getType());
            }

            Method method = this.factoryClass.getMethod(this.methodName, parmTypes);

            bean = method.invoke(this.objectFactory, parms);
        }
        catch (Exception e)
        {
            getLogger().error("setup caught: ", e);
            ProcessingException exception = new ProcessingException(e.getMessage());
            throw exception;
        }

        if (bean == null)
        {
            String msg = methodName + " in class " + className +
                    "did not return an object";
            ProcessingException processingException =
                    new ProcessingException(msg);
            getLogger().error(msg);
            throw processingException;
        }
    }

    /**
     * Generate XML data.
     * @throws SAXException
     */
    public void generate()
            throws SAXException
    {
        try
        {
            SAXBeanWriter beanWriter = new SAXBeanWriter(this.contentHandler);
            beanWriter.setWriteEmptyElements(false);
            beanWriter.getXMLIntrospector().setElementNameMapper(new CapitalizeNameMapper());
            beanWriter.getXMLIntrospector().setAttributesForPrimitives(true);
            beanWriter.write(this.bean);
        }
        catch (Exception e)
        {
            getLogger().error("Cannot generate XML for " + bean, e);
            SAXException saxException = new SAXException(e.getMessage());
            throw saxException;
        }
    }

    /**
     * Get method parameter descriptions and values
     * @param config the configuration data
     * @param valueRequred true if parameter values are required in the configuration
     * @return an array of ConfigParameters
     * @throws ConfigurationException
     */
    private ConfigParameter[] getParameters(Configuration config, boolean valueRequired)
            throws ConfigurationException
    {
        Configuration parms[] = config.getChildren("parameter");
        ConfigParameter parameters[] = new ConfigParameter[parms.length];

        for (int i = 0; i < parms.length; ++i)
        {
            if (parms[i].getName().equals("parameter"))
            {
                String name = parms[i].getAttribute("name");
                String type = parms[i].getAttribute("type");
                String role = parms[i].getAttribute("role", "");
                Object value = parms[i].getValue("");
                if (valueRequired && role.equals("") && value.equals(""))
                {
                    String msg = "Parameter " + name + " must have either or role or a value";
                    getLogger().error(msg);
                    throw new ConfigurationException(msg);
                }

                if (!role.equals(""))
                {
                    Component comp = null;
                    try
                    {
                        comp = this.manager.lookup(role);

                        int index = type.lastIndexOf('.');
                        String roleMethodName = "get" + type.substring(index >= 0 ? index + 1: 0);
                        Method method = comp.getClass().getMethod(roleMethodName, null);
                        value = method.invoke(comp, null);
                    }
                    catch (ComponentException e)
                    {
                        String msg = "Unable to lookup role " + role + ": " + e;
                        getLogger().error(msg);
                        throw new ConfigurationException(msg);
                    }
                    catch (NoSuchMethodException e)
                    {
                        String msg = "Unable to locate method for role " + role + ": " + e;
                        getLogger().error(msg);
                        throw new ConfigurationException(msg);
                    }
                    catch (Exception e)
                    {
                        getLogger().error("getParameters caught: " + e.getMessage());
                        throw new ConfigurationException(e.getMessage());
                    }
                }
                parameters[i] = new ConfigParameter(name, type, value);
            }
            else
            {
                String msg = "Invalid element name " + parms[i].getName();

                ConfigurationException configurationException =
                        new ConfigurationException(msg);
                getLogger().error(msg);
                throw configurationException;
            }
        }
        return parameters;
    }

    private ConfigParameter methodParms[];

    private Class factoryClass;
    private Object objectFactory;
    private Object bean;
    private String className;
    private String methodName;

    protected ComponentManager manager;

    /**
     * Configuration Parameter
     */
    private class ConfigParameter
    {
        /**
         * Constructor
         * @param name the parameter name
         * @param type the parameter's data type
         */
        public ConfigParameter(String name, String type)
        {
            this(name, type, null);
        }

        /**
         * Constructur
         * @param name the parameter name
         * @param type the parameter's data type
         * @param value the parameter's value
         */
        public ConfigParameter(String name, String type, Object value)
        {
            this.parmName = name;
            this.parmType = type;
            this.parmValue = value;
        }

        /**
         * Return the parameter name
         * @return the parameter name
         */
        public final String getName()
        {
            return this.parmName;
        }

        /**
         * Return the parameter's data type
         * @return the parameter's data type
         */
        public final String getType()
        {
            return this.parmType;
        }

        /**
         * Return the parameter's value
         * @return the parameter's value
         */
        public final Object getValue()
        {
            return this.parmValue;
        }

        private String parmName;
        private String parmType;
        private Object parmValue;
    }
}
