/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2009 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id$
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.ws.axis;

import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;

import javax.naming.BinaryRefAddr;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.wsdl.Definition;
import javax.wsdl.Port;
import javax.wsdl.Service;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.xml.namespace.QName;

import org.apache.axis.EngineConfiguration;
import org.apache.axis.client.AxisClient;
import org.apache.axis.configuration.XMLStringProvider;
import org.apache.axis.deployment.wsdd.WSDDConstants;
import org.apache.axis.deployment.wsdd.WSDDProvider;
import org.apache.axis.utils.XMLUtils;
import org.apache.axis.wsdl.toJava.Utils;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;
import org.ow2.jonas.deployment.api.IServiceRefDesc;
import org.ow2.jonas.deployment.ws.MappingFile;
import org.ow2.jonas.deployment.ws.PortComponentDesc;
import org.ow2.jonas.deployment.ws.PortComponentRefDesc;
import org.ow2.jonas.deployment.ws.ServiceRefDesc;
import org.ow2.jonas.lib.util.I18n;
import org.ow2.jonas.lib.util.JNDIUtils;
import org.ow2.jonas.lib.util.Log;
import org.ow2.jonas.lib.util.XMLSerializer;
import org.ow2.jonas.ws.jaxrpc.WSException;
import org.ow2.jonas.ws.jaxrpc.factory.JServiceFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

/**
 * Axis specific JServiceFactory. Create an Axis Service Reference. Used to
 * recreate Axis Service bound in JNDI (ObjectFactory)
 * @author Guillaume Sauthier
 * @author Xavier Delplanque
 */
public class JAxisServiceFactory implements JServiceFactory {


    /** logger */
    private static Logger logger = Log.getLogger("org.ow2.jonas.ws");

    /** i18n instance */
    private static I18n i18n = I18n.getInstance(JAxisServiceFactory.class);

    /** The jonas-init-param name for client configuration file */
    private static final String AXIS_CLIENT_CONFIG_PARAM = "axis.clientConfigFile";

    /** default client configuration file to be filled */
    private static final String CLIENT_CONFIG_WSDD = "org/ow2/jonas/ws/axis/client-config.wsdd";

    /** Service Implementation in JOnAS */
    private static final String JONAS_SERVICE_CLASSNAME = "org.ow2.jonas.ws.axis.JService";

    // ======================================================================

    /**
     * Ref name for client-config wsdd
     */
    public static final String REF_CLIENT_CONFIG = "client.config.wsdd";

    /**
     * Ref name for the service WSDL URL
     */
    public static final String REF_SERVICE_WSDL = "service.wsdl.url";

    /**
     * Ref name for the service QName
     */
    public static final String REF_SERVICE_QNAME = "service.qname";

    /**
     * Ref name for a Map linking service-endpoint-interface (from port-component) to wsdl:port
     */
    public static final String REF_SERVICE_PORT2WSDL = "port.2.wsdl.map";

    /**
     * Ref name for the service call properties
     */
    public static final String REF_SERVICE_CALL_PROPS = "service.call.properties";

    /**
     * Ref name for the service call properties
     */
    public static final String REF_SERVICE_STUB_PROPS = "service.stub.properties";

    /**
     * Ref name for the service port list
     */
    public static final String REF_SERVICE_WSDL_PORT_LIST = "service.port.list";

    /**
     * setEndpointAddress method signature
     */
    private static final Class[] SETENDPOINTADDRESS_SIG = new Class[] {java.lang.String.class, java.lang.String.class };

    // ======================================================================

    /**
     * Construct a new JAxisServiceFactory.
     */
    public JAxisServiceFactory() {
        QName javaURI = new QName(WSDDConstants.URI_WSDD_JAVA, WSDDNoopProvider.PROVIDER_NAME);
        WSDDProvider.registerProvider(javaURI, new WSDDNoopProvider());
    }

    /**
     * Create a Reference on a Service from an Axis Service object.
     * @param sr the service-ref instance where informations are extracted
     * @param cl the classloader where config files can be loaded
     * @return a Reference on a Service from an Axis Service object.
     * @throws WSException if wsdd cannot be load or merged
     */
    public Reference getServiceReference(final IServiceRefDesc sr, final ClassLoader cl) throws WSException {

        // TODO remove this ugly cast
        String classname = createServiceClassname((ServiceRefDesc) sr);

        logger.log(BasicLevel.DEBUG, "Service classname: '" + classname + "'");

        // Create a reference on the Service class (generated or not)
        Reference ref = new Reference(classname, getClass().getName(), null);

        Document base = loadAxisDeployment(CLIENT_CONFIG_WSDD, cl);

        // get the WSDD configuration
        String clientConfig = sr.getParam(AXIS_CLIENT_CONFIG_PARAM);

        // If ServiceRef has a specified ConfigFile
        if (clientConfig != null) {
            // load wsdd
            Document doc = loadAxisDeployment(clientConfig, cl);

            // merge
            mergeAxisDeployment(base, doc);
        }

        // 1. client.wsdd
        String str = null;
        try {
            str = serializeDOM(base);
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "Client Descriptor file : \n" + str);
            }
        } catch (IOException ioe) {
            throw new WSException("Cannot serialize Document", ioe);
        }
        ref.add(new StringRefAddr(REF_CLIENT_CONFIG, str));

        //2. manage port-component-link
        for (Iterator i = sr.getPortComponentRefs().iterator(); i.hasNext();) {
            PortComponentRefDesc pcr = (PortComponentRefDesc) i.next();
            PortComponentDesc referencedPortComponent = pcr.getPortComponentDesc();

            // port has a link on a local port
            if (referencedPortComponent != null) {

                logger.log(BasicLevel.DEBUG, "Find a port-component-link in port-component-ref" + pcr.getSEI());
                // Get the Service WSDL URL !!!
                URL url = referencedPortComponent.getEndpointURL();
                if (url == null) {
                    // Component outside JOnAS server
                    // lookup endpoint URL value
                    try {
                        Context ic = new InitialContext();
                        url = (URL) ic.lookup(referencedPortComponent.getName());
                    } catch (NamingException ne) {
                        throw new WSException("Cannot find updated endpoint for port-component '"
                                + referencedPortComponent.getName() + "'", ne);
                    }
                }

                logger.log(BasicLevel.DEBUG, "Uptodate URL : '" + url + "?JWSDL' ");
                ref.add(new StringRefAddr(REF_SERVICE_WSDL, url.toExternalForm() + "?JWSDL"));
            }
        }

        // 3. service wsdl URL
        if (sr.getAlternateWsdlURL() != null) {
            logger.log(BasicLevel.DEBUG, "Using alternate WSDL URL : '" + sr.getAlternateWsdlURL() + "'");
            ref.add(new StringRefAddr(REF_SERVICE_WSDL, sr.getAlternateWsdlURL().toString()));
        } else if (sr.getLocalWSDLURL() != null) {
            logger.log(BasicLevel.DEBUG, "Using WSDL URL : '" + sr.getLocalWSDLURL() + "'");
            ref.add(new StringRefAddr(REF_SERVICE_WSDL, sr.getLocalWSDLURL().toExternalForm()));
        }

        // 4. Service QName
        if (sr.getServiceQName() != null) {
            ref.add(new BinaryRefAddr(REF_SERVICE_QNAME, JNDIUtils.getBytesFromObject(sr.getServiceQName())));
        }

        // 5. classname -> wsdl:port map
        List ports = sr.getPortComponentRefs();
        if (!ports.isEmpty()) {
            Map map = new Hashtable();
            for (Iterator i = ports.iterator(); i.hasNext();) {
                PortComponentRefDesc pcrd = (PortComponentRefDesc) i.next();
                QName wsdlPort = pcrd.getWsdlPort();
                if (wsdlPort != null) {
                    map.put(pcrd.getSEI().getName(), wsdlPort);
                }
            }
            if (!map.isEmpty()) {
                ref.add(new BinaryRefAddr(REF_SERVICE_PORT2WSDL, JNDIUtils.getBytesFromObject(map)));
            }
        }

        // 6. store call & stubs properties
        String portNames = null;
        for (Iterator i = sr.getPortComponentRefs().iterator(); i.hasNext();) {
            PortComponentRefDesc pcr = (PortComponentRefDesc) i.next();
            Properties cProps = pcr.getCallProperties();
            Properties sProps = pcr.getStubProperties();

            if (pcr.getWsdlPort() != null) {
                // use call & stub property only if there is a wsdl:port for applying to
                // TODO must be something to add into documentation for this
                String name = pcr.getWsdlPort().getLocalPart();
                if (!cProps.isEmpty()) {
                    ref.add(new BinaryRefAddr(REF_SERVICE_CALL_PROPS + "_" + name, JNDIUtils
                                    .getBytesFromObject(cProps)));
                }
                if (!sProps.isEmpty()) {
                    ref.add(new BinaryRefAddr(REF_SERVICE_STUB_PROPS + "_" + name, JNDIUtils
                                    .getBytesFromObject(sProps)));
                }
                if (!sProps.isEmpty() || !cProps.isEmpty()) {
                    if (portNames != null) {
                        // only the first addition does not need a comma
                        portNames += "," + name;
                    } else {
                        portNames = name;
                    }
                }
            }
        }
        if (portNames != null) {
            ref.add(new StringRefAddr(REF_SERVICE_WSDL_PORT_LIST, portNames));
        }

        return ref;
    }

    /**
     * @param base the Document to serialize
     * @return Returns a String representation of the given XML Document
     * @throws IOException when Serializer cannot proceed
     */
    private String serializeDOM(final Document base) throws IOException {
        StringWriter sw = new StringWriter();
        XMLSerializer ser = new XMLSerializer(base);
        ser.serialize(sw);
        return sw.getBuffer().toString();
    }

    /**
     * @param base father Document
     * @param doc Document that will be incorpored inside base Document
     */
    private void mergeAxisDeployment(final Document base, final Document doc) {
        Element importedDeploymentElement = (Element) base.importNode(doc.getDocumentElement(), true);
        NodeList list = importedDeploymentElement.getChildNodes();
        // append each Element childnodes
        for (int i = 0;  i < list.getLength(); i++) {
            if (list.item(i) instanceof Element) {
                base.getDocumentElement().appendChild(list.item(i));
            }
        }
    }

    /**
     * Create the Axis Service Implementation class name from the QName (if
     * generated interface found).
     * TODO Use jaxrpc mapping information if available !
     * @param sr the service-ref storing service informations
     * @return the fully qualified Service implementation classname.
     */
    private String createServiceClassname(final ServiceRefDesc sr) {

        String intfName = sr.getServiceInterface().getName();

        if (intfName.equals("javax.xml.rpc.Service")) {
            // no generated class
            return JONAS_SERVICE_CLASSNAME;
        } else {
            // generated class
            QName qn = sr.getServiceQName();
            MappingFile mf = sr.getMappingFile();

            String p = (String) mf.getMappings().get(qn.getNamespaceURI());

            String classname = "";
            if (p != null) {
                classname = p + "." + Utils.xmlNameToJavaClass(qn.getLocalPart()) + "Locator";
            } else {
                classname = Utils.xmlNameToJavaClass(qn.getLocalPart()) + "Locator";
            }

            return classname;
        }

    }

    /**
     * Load the Axis Deployment Descriptor from the ClassLoader.
     * @param filename the resource name to load
     * @param cl The ClassLoader used to load the WSDD
     * @return the Document Axis Deployment Descriptor from the ClassLoader.
     * @throws WSException if resource could not be found or if XML
     *         Document cannot be created
     */
    private Document loadAxisDeployment(final String filename, final ClassLoader cl) throws WSException {

        InputStream is = cl.getResourceAsStream(filename);

        //logger.log(BasicLevel.DEBUG, "classloader : " + cl);

        if (is == null) {
            // exception !
            String err = i18n.getMessage("JAxisServiceFactory.loadAxisDeployment.configNotFound", filename);
            logger.log(BasicLevel.ERROR, err);
            throw new WSException(err);
        }

        Document doc = null;
        try {
            doc = XMLUtils.newDocument(is);
        } catch (Exception e) {
            String err = i18n.getMessage("JAxisServiceFactory.loadAxisDeployment.docCreation", filename);
            logger.log(BasicLevel.DEBUG, err);

            throw new WSException(err, e);
        }

        return doc;
    }

    /**
     * Create a new instance of Axis Service (or sub classes) from a Reference
     * object.
     * @param refObject the Reference to create an instance from
     * @param name Object Name
     * @param nameCtx Context
     * @param env the environnment
     * @return A Service instance configured
     * @throws Exception when instanciation fail (check if this is a correct
     *         behavior !!!)
     */
    public Object getObjectInstance(final Object refObject, final Name name, final Context nameCtx, final Hashtable env) throws Exception {

        JService instance = null;
        Object proxy = null;

        if (refObject instanceof Reference) {
            Reference ref = (Reference) refObject;

            // get ClassLoader
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            logger.log(BasicLevel.DEBUG, "Context ClassLoader : " + cl);

            // get Class
            Class<?> serviceClass = cl.loadClass(ref.getClassName());

            // new Service
            BinaryRefAddr bRefQname = (BinaryRefAddr) ref.get(REF_SERVICE_QNAME);

            QName serviceQname = null;
            if (bRefQname != null) {
                serviceQname = (QName) JNDIUtils.getObjectFromBytes((byte[]) bRefQname.getContent());
            }

            RefAddr refServiceWSDL = ref.get(REF_SERVICE_WSDL);

            String serviceWsdl = null;
            if (refServiceWSDL != null) {
                serviceWsdl = (String) refServiceWSDL.getContent();
            }

            // Prefill as much as we can
            if (JONAS_SERVICE_CLASSNAME.equals(serviceClass.getName())) {

                // default service class
                logger.log(BasicLevel.DEBUG, "default service class");

                if ((serviceQname == null) && (serviceWsdl == null)) {
                    logger.log(BasicLevel.DEBUG, "Create a new Service instance without params");
                    // no param instance
                    instance = new JService();

                } else if ((serviceQname != null) && (serviceWsdl == null)) {
                    logger.log(BasicLevel.DEBUG, "Create a new Service instance with only a QName " + serviceQname);
                    // serviceQname only
                    instance = new JService(serviceQname);

                } else if ((serviceQname != null) && (serviceWsdl != null)) {
                    logger.log(BasicLevel.DEBUG, "Create a new Service instance with QName " + serviceQname
                            + "and WSDL " + serviceWsdl);
                    // serviceQname + wsdl
                    instance = new JService(serviceWsdl, serviceQname);

                } else {
                    logger.log(BasicLevel.DEBUG, "Create a new Service instance with WSDL " + serviceWsdl);
                    logger.log(BasicLevel.DEBUG, "Should not occurs !!!");
                    // wsdl
                    // serviceWsdl, updatedURLs, serviceQname);
                    instance = new JService();
                }

            } else {
                logger.log(BasicLevel.DEBUG, "Create a new Generated Service instance");
                logger.log(BasicLevel.DEBUG, "serviceWSDL:" + serviceWsdl + " serviceQName:" + serviceQname);

                // generated class
                Constructor<?> ctr = serviceClass.getConstructor(new Class[] {String.class, QName.class});
                instance = (JService) ctr.newInstance(new Object[] {serviceWsdl, serviceQname});

                // we have to update ports endpoints
                // we should have Service WSDL
                // otherwise, no Generated WSDL !
                if (serviceWsdl != null) {
                    WSDLFactory factory = WSDLFactory.newInstance();
                    WSDLReader reader = factory.newWSDLReader();
                    reader.setFeature("javax.wsdl.importDocuments", true);
                    Definition def = reader.readWSDL(serviceWsdl);

                    Service service = def.getService(serviceQname);
                    Map ports = service.getPorts();
                    java.lang.reflect.Method m = serviceClass.getMethod("setEndpointAddress", SETENDPOINTADDRESS_SIG);
                    for (Iterator i = ports.keySet().iterator(); i.hasNext();) {
                        String portName = (String) i.next();
                        Port port = service.getPort(portName);
                        String endpoint = getSOAPLocation(port);
                        m.invoke(instance, new Object[] {port.getName(), endpoint });
                    }
                }
            }

            // get port2wsdl map
            BinaryRefAddr bRefp2w = (BinaryRefAddr) ref.get(REF_SERVICE_PORT2WSDL);

            if (bRefp2w != null) {
                Map map = (Map) JNDIUtils.getObjectFromBytes((byte[]) bRefp2w.getContent());
                instance.assignSEIClassnameToWSDLPort(map);
            }

            // get WSDL_PORT_NAMES
            RefAddr portsRef = ref.get(REF_SERVICE_WSDL_PORT_LIST);
            String listPorts = null;
            if (portsRef != null) {
                listPorts = (String)  portsRef.getContent();

                if (listPorts != null) {
                    // get call properties
                    StringTokenizer  strPort = new StringTokenizer(listPorts, ",");
                    while (strPort.hasMoreTokens()) {
                        String port = strPort.nextToken();

                        // call properties
                        BinaryRefAddr bRefcp = (BinaryRefAddr) ref.get(REF_SERVICE_CALL_PROPS + "_" + port);

                        if (bRefcp != null) {
                            Properties callProperties = (Properties) JNDIUtils.getObjectFromBytes((byte[]) bRefcp.getContent());
                            instance.assignCallProperties(port, callProperties);
                        }

                        // stub properties
                        BinaryRefAddr bRefsp = (BinaryRefAddr) ref.get(REF_SERVICE_STUB_PROPS + "_" + port);

                        if (bRefsp != null) {
                            Properties stubProperties = (Properties) JNDIUtils.getObjectFromBytes((byte[]) bRefsp.getContent());
                            instance.assignStubProperties(port, stubProperties);
                        }
                    }
                }
            }
            EngineConfiguration ec = getConfiguration(ref);

            // configure the instance
            instance.setEngine(new AxisClient(ec));

            JServiceProxy handler = new JServiceProxy(instance);
            Class<?>[] serviceInterfaces = serviceClass.getInterfaces();
            Class<?>[] interfaces = new Class[serviceInterfaces.length + 1];
            for (int i = 0; i < serviceInterfaces.length; i++) {
                interfaces[i] = serviceInterfaces[i];
            }
            interfaces[serviceInterfaces.length] = javax.xml.rpc.Service.class;
            proxy = Proxy.newProxyInstance(cl, interfaces, handler);

        }

        return proxy;
    }

    /**
     * @param port analyzed port
     * @return Returns the endpoint URL of the given Port
     */
    private String getSOAPLocation(final Port port) {
        String endpoint = null;
        List extensions = port.getExtensibilityElements();
        for (Iterator i = extensions.iterator(); i.hasNext();) {
            ExtensibilityElement ext = (ExtensibilityElement) i.next();
            if (ext instanceof SOAPAddress) {
                SOAPAddress addr = (SOAPAddress) ext;
                endpoint = addr.getLocationURI();
            }
        }
        return endpoint;
    }

    /**
     * Return the EngineConfiguration object found in the Reference
     * @param ref The reference to an Axis Service instance
     * @return the EngineConfiguration object found in the Reference
     * @throws Exception when content of reference is not a valid WSDD
     */
    private EngineConfiguration getConfiguration(final Reference ref) throws Exception {
        // create a Configuration Object
        String conf = (String) ref.get(REF_CLIENT_CONFIG).getContent();
        logger.log(BasicLevel.DEBUG, "loaded configuration : " + conf);
        return new XMLStringProvider(conf);
    }

}