/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2007 Bull S.A.S.
 * 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.deployment.ws;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Vector;

import javax.xml.namespace.QName;

import org.ow2.jonas.deployment.api.IServiceRefDesc;
import org.ow2.jonas.deployment.common.DeploymentDescException;
import org.ow2.jonas.deployment.common.HandlerDesc;
import org.ow2.jonas.deployment.common.xml.Handler;
import org.ow2.jonas.deployment.common.xml.JonasInitParam;
import org.ow2.jonas.deployment.common.xml.JonasPortComponentRef;
import org.ow2.jonas.deployment.common.xml.JonasServiceRef;
import org.ow2.jonas.deployment.common.xml.PortComponentRef;
import org.ow2.jonas.deployment.common.xml.ServiceRef;
import org.ow2.jonas.deployment.ws.lib.MappingFileManager;
import org.ow2.jonas.deployment.ws.wrapper.MappingFileManagerWrapper;
import org.ow2.jonas.lib.util.BeanNaming;
import org.ow2.jonas.lib.util.I18n;
import org.ow2.jonas.lib.util.Log;

import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Logger;

/**
 * A ServiceRefDesc describe a dependancy from a J2EE component onto a
 * WebService (it is a Web Service Client). The component will have the hability
 * to lookup the Service Implementation and use it.
 *
 * @author Guillaume Sauthier
 * @author Xavier Delplanque
 */
public class ServiceRefDesc implements IServiceRefDesc {

    /**
     * javax.xml.rpc.Service classname.
     */
    private static final String JAVAX_XML_RPC_SERVICE = "javax.xml.rpc.Service";

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

    /**
     * Internationalization.
     */
    private static I18n i18n = I18n.getInstance(ServiceRefDesc.class);

    /** All the params of the ServiceRefDesc. */
    private Hashtable params = new Hashtable();

    /** The list of PortComponentRefs. */
    private Vector pcRefs = new Vector();

    /** The list of Handlers. */
    private Vector hRefs = new Vector();

    /** The WSDLFile of the ServiceRefDesc. */
    private WSDLFile wsdl;

    /** The alternate WSDLFile of the ServiceRefDesc. */
    private WSDLFile altWsdl;

    /** The MappingFile of the ServiceRefDesc. */
    private MappingFile mapping;

    /** The name of the ServiceRefDesc (used in lookup). */
    private String name;

    /** The service interface. */
    private Class serviceInterface;

    /** The Service QName. */
    private QName serviceQName = null;

    /** The Module file (directory or jar). */
    private File moduleFile = null;

    /** WSDL FileName. */
    private String wsdlFileName = null;

    /** jaxrpc mapping file name. */
    private String mappingFileName = null;

    /** alt-wsdl URL. */
    private URL alternateWSDL = null;

    /**  local URL of the uptodate WSDL. */
    private URL localWSDLURL = null;

    /** local URL os the mapping file. */
    private URL mappingFileURL = null;

    /**
     * Creates a new ServiceRefDesc object.
     * @param classLoader web class loader
     * @param sref generated object containing service-ref informations.
     * @param jsref JOnAS specific service-ref informations.
     * @param filename the name of the archive where retrieve WSDL and mapping
     *        files
     * @throws WSDeploymentDescException if the wsdl file is undefined or if
     *         informations in wsdl and handler doesn't match.
     */
    public ServiceRefDesc(final ClassLoader classLoader,
                          final ServiceRef sref,
                          final JonasServiceRef jsref,
                          String filename)
            throws WSDeploymentDescException {

        if (filename != null) {
            moduleFile = new File(filename);
        }

        // setup the classloader
        ClassLoader loader = classLoader;

        // set ServiceRefDesc name
        name = sref.getServiceRefName();

        // set Service QName (if exists)
        if (sref.getServiceQname() != null) {
            serviceQName = sref.getServiceQname().getQName();
        }

        // set Service Interface
        String si = sref.getServiceInterface().trim();

        try {
            serviceInterface = loader.loadClass(si);
        } catch (ClassNotFoundException cnf) {
            throw new WSDeploymentDescException(getI18n().getMessage("ServiceRefDesc.serviceIntfNotFound", si), cnf); //$NON-NLS-1$
        }

        // this interface must implements javax.xml.rpc.Service
        if (!javax.xml.rpc.Service.class.isAssignableFrom(serviceInterface)) {
            String err = getI18n().getMessage("ServiceRefDesc.mustExtService", serviceInterface.getName()); //$NON-NLS-1$
            throw new WSDeploymentDescException(err);
        }

        // ServiceRefDesc init parameters : jonas specific deployment desc
        if (jsref != null) {
            List jipl = jsref.getJonasInitParamList();

            for (int i = 0; i < jipl.size(); i++) {
                // add in params table each init parameter name and associated value
                JonasInitParam p = (JonasInitParam) jipl.get(i);
                params.put(p.getParamName().trim(), p.getParamValue().trim());
            }
        }

        // fill portComponentRef list
        Map links = linkPCR2JPCR(sref, jsref);
        List pcrl = sref.getPortComponentRefList();

        for (int i = 0; i < pcrl.size(); i++) {
            // for each reference, build and add a PortComponentRef Object
            PortComponentRef ref = (PortComponentRef) pcrl.get(i);
            JonasPortComponentRef jref = (JonasPortComponentRef) links.get(ref.getServiceEndpointInterface());
            pcRefs.add(new PortComponentRefDesc(loader, ref, jref));
        }

        // fill handlers list
        List hl = sref.getHandlerList();
        Handler h = null;

        for (int i = 0; i < hl.size(); i++) {
            // for each reference, build and add a HandlerRef Object
            // get the standard dd handler
            h = (Handler) hl.get(i);
            // build and add a new handler
            try {
                hRefs.add(new HandlerDesc(loader, h));
            } catch (DeploymentDescException dde) {
                throw new WSDeploymentDescException(dde);
            }
        }

        // if alt-wsdl defined AND running online
        if (jsref != null && jsref.getAltWsdl() != null && isJonasRuntime()) {
            if (logger.isLoggable(BasicLevel.DEBUG)) {
                logger.log(BasicLevel.DEBUG, "loading alt-wsdl : " + jsref.getAltWsdl());
            }
            try {
                alternateWSDL = new URL(jsref.getAltWsdl());
                altWsdl = new WSDLFile(alternateWSDL, jsref.getAltWsdl());
            } catch (IOException e) {
                String err = "Cannot load alternate WSDL Stream from " + jsref.getAltWsdl();
                throw new WSDeploymentDescException(err);
            }
        }

        // get ServiceRefDesc WSDLFile name
        String wf = sref.getWsdlFile();
        if (wf != null) {
            this.wsdlFileName = wf.trim();
            wsdl = new WSDLFile(loader, wsdlFileName);

            localWSDLURL = loader.getResource(wsdlFileName);
        }

        if (getWSDLFile() != null) {
            if ((getWSDLFile().getNbServices() > 1) && (serviceQName == null)) {
                String err = getI18n().getMessage("ServiceRefDesc.serviceQnameNotDef", wsdlFileName, name); //$NON-NLS-1$
                throw new WSDeploymentDescException(err);
            } else if ((getWSDLFile().getNbServices() == 1) && (serviceQName == null)) {
                // When the WSDL defines only 1 service, we can retrieve
                // automatically the service QName for service-ref
                serviceQName = getWSDLFile().getServiceQname();
            } else {
                // serviceQName not null
                // check service QName existence in WSDL
                if (isJonasRuntime()) {
                    if (!getWSDLFile().hasService(serviceQName)) {
                        throw new WSDeploymentDescException(getI18n().getMessage(
                                "ServiceRefDesc.serviceNotFoundInWSDL", serviceQName, wsdlFileName)); //$NON-NLS-1$
                    }
                }
            }

        }

        // get ServiceRefDesc MappingFile name
        String mf = sref.getJaxrpcMappingFile();

        if (mf != null) {
            this.mappingFileName = mf.trim();
            this.mappingFileURL = loader.getResource(mappingFileName);
            setMappingFile(loader);
        }

        // mapping is required when generated service interface is specified
        if (!serviceInterface.getName().equals(JAVAX_XML_RPC_SERVICE) && (mapping == null)) {
            throw new WSDeploymentDescException(getI18n().getMessage(
                    "ServiceRefDesc.mappingRequired", serviceInterface.getName())); //$NON-NLS-1$
        }

        validate();
    }

    /**
     * @return Returns true if called in a JOnAS runtime (in a container, ...).
     */
    private boolean isJonasRuntime() {

        // Deployment related packages
        final String[] TESTED_PACKAGE = new String[] {
                "org.ow2.jonas.generators.wsgen",
                "org.ow2.jonas.generators.genclientstub",
                "org.ow2.jonas.generators.genic"
        };

        // Create a Throwable to obtain the "execution context" of this method.
        Throwable t = new Exception();
        StackTraceElement[] elem = t.getStackTrace();
        for (int i = 0; i < elem.length; i++) {
            // Test each packages
            for (int j = 0; j < TESTED_PACKAGE.length; j++) {
                if (elem[i].getClassName().startsWith(TESTED_PACKAGE[j])) {
                    // We're running in deployment related code, that's enough to return false
                    return false;
                }
            }
        }
        return true;

    }

    /**
     * @param sref ServiceRef instance
     * @param jsref linked JonasServiceRef instance
     * @return Returns a map associating the PortComponentRef.sei with the JonasPortComponentRef
     */
    private Map linkPCR2JPCR(final ServiceRef sref, final JonasServiceRef jsref) {
        Map res = new HashMap();
        // for each port-component-ref
        for (Iterator i = sref.getPortComponentRefList().iterator(); i.hasNext();) {
            PortComponentRef pcr = (PortComponentRef) i.next();
            res.put(pcr.getServiceEndpointInterface(), null);
        }
        // jonas-port-component-ref(s)
        if (jsref != null) {

            // get all jonas-port-component.sei
            Set keys = res.keySet();

            // for each jonas-port-component
            for (Iterator i = jsref.getJonasPortComponentRefList().iterator(); i.hasNext();) {
                JonasPortComponentRef jpcr = (JonasPortComponentRef) i.next();
                String sei = jpcr.getServiceEndpointInterface();

                if ((sei != null) && (keys.contains(sei))) {
                    // jonas-port-component-ref linked to port-component-ref
                    res.put(sei, jpcr);
                } else {
                    String err = "jonas-port-component-ref '" + sei + "' is not linked to any port-component-ref. It will be ignored.";
                    logger.log(BasicLevel.DEBUG, err);
                }
            }
        }
        return res;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getPortComponentRefs()
     */
    public List getPortComponentRefs() {
        return pcRefs;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getHandlerRefs()
     */
    public List getHandlerRefs() {
        return hRefs;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getServiceRefName()
     */
    public String getServiceRefName() {
        return name;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getServiceInterface()
     */
    public Class getServiceInterface() {
        return serviceInterface;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getWSDLFile()
     */
    public WSDLFile getWSDLFile() {
        // if alt-wsdl is not set, use standard wsdl-file
        WSDLFile file = altWsdl;
        if (file == null) {
            file = wsdl;
        }
        return file;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getMappingFile()
     */
    public MappingFile getMappingFile() {
        return mapping;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getParams()
     */
    public Hashtable getParams() {
        return params;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getParam(java.lang.String)
     */
    public String getParam(final String name) {
        return (String) params.get(name);
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getWsdlFileName()
     */
    public String getWsdlFileName() {
        return wsdlFileName;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getServiceQName()
     */
    public QName getServiceQName() {
        return serviceQName;
    }



    /**
     * @see java.lang.String#hashCode()
     */
    public int hashCode() {
        return this.name.hashCode();
    }
    /**
     * Return <code>true</code> if the parameter is a ServiceRefDesc and if it
     * equals this object. Return <code>false</code> else.
     * @param other the object to compare
     * @return true if objects are equals
     */
    public boolean equals(final Object other) {
        if (other == null) {
            return false;
        }

        if (!(other instanceof ServiceRefDesc)) {
            return false;
        }

        ServiceRefDesc sr = (ServiceRefDesc) other;

        if (!mapping.equals(sr.getMappingFile())) {
            return false;
        }

        if (!wsdl.equals(sr.getWSDLFile())) {
            return false;
        }

        if (!params.equals(sr.getParams())) {
            return false;
        }

        for (Iterator i = sr.getPortComponentRefs().iterator(); i.hasNext();) {
            PortComponentRefDesc pcr = (PortComponentRefDesc) i.next();

            if (!pcRefs.contains(pcr)) {
                return false;
            }
        }

        for (Iterator i = sr.getHandlerRefs().iterator(); i.hasNext();) {
            HandlerDesc hr = (HandlerDesc) i.next();

            if (!hRefs.contains(hr)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Create the MappingFile.
     * @param loader ClassLoader containing mapping file
     * @throws WSDeploymentDescException When MappingFile creation fails.
     */
    private void setMappingFile(final ClassLoader loader) throws WSDeploymentDescException {

        // build the MappingFile
        // Build Mapping file
        if (moduleFile != null) {
            if (isRunningInClientContainer()) {
                mapping = MappingFileManager.getInstance(moduleFile, mappingFileName);
            } else {
                mapping = MappingFileManagerWrapper.getMappingFile(moduleFile, mappingFileName);
            }
        } else {
            // Try to get the mapping from the ClassLoader
            InputStream is = null;
            if (loader != null) {
                is = loader.getResourceAsStream(mappingFileName);
                if (is != null) {
                    // build the MappingFile
                    if (isRunningInClientContainer()) {
                        mapping = MappingFileManager.getInstance(is, mappingFileName);
                    } else {
                        mapping = MappingFileManagerWrapper.getMappingFile(is, mappingFileName);
                    }
                } else {
                    throw new WSDeploymentDescException(getI18n().getMessage(
                            "ServiceRefDesc.mappingFileNotFoundInLoader", mappingFileName)); //$NON-NLS-1$
                }
            } else {
                throw new WSDeploymentDescException(getI18n().getMessage("ServiceRefDesc.mappingFileNotFound")); //$NON-NLS-1$
            }
        }
    }

    /**
     * @return Returns true if current execution takes place inside a
     *         ClientContainer
     */
    private boolean isRunningInClientContainer() {
        return (System.getProperty("jonas.base") == null);
    }

    /**
     * Validate the ServiceRefDesc.
     * @throws WSDeploymentDescException DOCUMENT ME!
     */
    private void validate() throws WSDeploymentDescException {
        // validate informations :
        // TODO pcLinkExist

        if ((wsdlFileName != null) && ((mappingFileName == null) || ("".equals(mappingFileName)))) {
            // Must have a mapping file along with WSDL
            throw new WSDeploymentDescException(getI18n().getMessage(
                    "ServiceRefDesc.missingMappingFile", this.name)); //$NON-NLS-1$
        }

        // Port validity between handlers and wsdl
        for (int i = 0; i < hRefs.size(); i++) {
            HandlerDesc myHRef = (HandlerDesc) hRefs.get(i);
            List pnl = myHRef.getPortNames();

            for (int j = 0; j < pnl.size(); j++) {
                if (!getWSDLFile().hasPort((String) pnl.get(j))) {
                    // the handler use a port undefined in the wsdl
                    throw new WSDeploymentDescException(getI18n().getMessage(
                            "ServiceRefDesc.undefinedPort", myHRef.getName(), pnl.get(j), wsdlFileName)); //$NON-NLS-1$
                }
            }
        }

        // Check service-interface
        // if service interface is not generic, Full WSDL Knowledge is required
        if (!serviceInterface.equals(javax.xml.rpc.Service.class)) {
            if ((getWSDLFile() == null) || ((getWSDLFile() != null) && (getWSDLFile().getDefinition().getServices().values().size() == 0))) {
                throw new WSDeploymentDescException(getI18n().getMessage(
                        "ServiceRefDesc.wsdlMissingInformation", serviceInterface.getName(), name)); //$NON-NLS-1$
            }

            // Check mapping for GeneratedServiceInterface
            String namespaceURI = serviceQName.getNamespaceURI();
            String realPackage = BeanNaming.getPackageName(serviceInterface.getName());

            // real != mapping provided package
            if (!realPackage.equals(mapping.getMapping(namespaceURI))) {
                throw new WSDeploymentDescException(getI18n().getMessage(
                        "ServiceRefDesc.needPackageMapping", mappingFileName, realPackage)); //$NON-NLS-1$
            }

            /*
             * TODO : Uncomment this part will require substancial modification
             * to number of tests (integ + unit) // -> We need to create
             * generated clients interfaces for each endpoint ... // keep
             * service QName namespaceURI (targetNamespace) // check mapping for
             * ServiceEndpointInterface when having a generated
             * service-interface for (Iterator p = pcRefs.iterator() ;
             * p.hasNext() ; ) { PortComponentRef pcr = (PortComponentRef)
             * p.next(); String sei = pcr.getSei().getName(); realPackage =
             * BeanNaming.getPackageName(sei); if
             * (!realPackage.equals(mapping.getMapping(namespaceURI))) throw new
             * WSDeploymentDescException("jaxrpc-mapping-file '" +
             * mappingFileName + "' need a mapping for package '" + realPackage +
             * "'"); }
             */
        }
    }

    /**
     * @return Returns the i18n.
     */
    protected static I18n getI18n() {
        return i18n;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getAlternateWsdlURL()
     */
    public URL getAlternateWsdlURL() {
        return alternateWSDL;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getLocalWSDLURL()
     */
    public URL getLocalWSDLURL() {
        return localWSDLURL;
    }

    /* (non-Javadoc)
     * @see org.ow2.jonas.deployment.api.IServiceRefDesc#getMappingFileURL()
     */
    public URL getMappingFileURL() {
        return mappingFileURL;
    }
}