/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 1999-2005 Bull S.A.
 * Contact: jonas-team@objectweb.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.objectweb.jonas_ws.deployment.api;

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.objectweb.jonas_ejb.lib.BeanNaming;
import org.objectweb.jonas_lib.I18n;
import org.objectweb.jonas_lib.deployment.api.DeploymentDescException;
import org.objectweb.jonas_lib.deployment.api.HandlerDesc;
import org.objectweb.jonas_lib.deployment.xml.Handler;
import org.objectweb.jonas_lib.deployment.xml.JonasInitParam;
import org.objectweb.jonas_lib.deployment.xml.JonasPortComponentRef;
import org.objectweb.jonas_lib.deployment.xml.JonasServiceRef;
import org.objectweb.jonas_lib.deployment.xml.PortComponentRef;
import org.objectweb.jonas_lib.deployment.xml.ServiceRef;
import org.objectweb.jonas_ws.deployment.lib.MappingFileManager;
import org.objectweb.jonas_ws.deployment.lib.wrapper.MappingFileManagerWrapper;
import org.objectweb.jonas.common.Log;
import org.objectweb.jonas.server.Server;
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 {

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

    /**
     * logger
     */
    private static Logger logger = Log.getLogger(Log.JONAS_WS_PREFIX);

    /**
     * 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(ClassLoader classLoader, ServiceRef sref, 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() {

        if (Server.isStarted()) {
            // we're in jonas runtime
            // but if we're doing deploy stuff, return false
            Throwable t = new Exception();
            StackTraceElement[] elem = t.getStackTrace();
            for (int i = 0; i < elem.length; i++) {
                if ("org.objectweb.jonas_ws.wsgen.wrapper.WsGenWrapper".equals(elem[i].getClassName())) {
                    return false;
                }
                if ("org.objectweb.jonas_lib.genclientstub.wrapper.ClientGenStubWrapper".equals(elem[i].getClassName())) {
                    return false;
                }
                if ("org.objectweb.jonas_ejb.genic.wrapper.GenicServiceWrapper".equals(elem[i].getClassName())) {
                    return false;
                }
            }
            return true;

        } else {
            // offline mode or client container
            ClassLoader cl = this.getClass().getClassLoader();
            try {
                cl.loadClass("org.objectweb.jonas.server.Bootstrap");
            } catch (ClassNotFoundException e) {
                // we're in client container
                // => jonas runtime
                return true;
            }
            // loading successful => we're not in client container
            return false;
        }
    }

    /**
     * @param sref ServiceRef instance
     * @param jsref linked JonasServiceRef instance
     * @return Returns a map associating the PortComponentRef.sei with the JonasPortComponentRef
     */
    private Map linkPCR2JPCR(ServiceRef sref, 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.WARN, err);
                }
            }
        }
        return res;
    }

    /**
     * Return the list of PortComponentRef.
     * @return the list of PortComponentRef
     */
    public List getPortComponentRefs() {
        return pcRefs;
    }

    /**
     * Return the list of Handler.
     * @return the list of Handler
     */
    public List getHandlerRefs() {
        return hRefs;
    }

    /**
     * Return the name used for Service interface lookup.
     * @return the service-ref-name value
     */
    public String getServiceRefName() {
        return name;
    }

    /**
     * Return the Class object representing the service-interface.
     * @return the Class object representing the service-interface.
     */
    public Class getServiceInterface() {
        return serviceInterface;
    }

    /**
     * Return the WSDLFile object describing the WebService.
     * @return the WSDLFile object describing the WebService.
     */
    public WSDLFile getWSDLFile() {
        // if alt-wsdl is not set, use standard wsdl-file
        WSDLFile file = altWsdl;
        if (file == null) {
            file = wsdl;
        }
        return file;
    }

    /**
     * Return the MappingFile object.
     * @return the MappingFile object.
     */
    public MappingFile getMappingFile() {
        return mapping;
    }

    /**
     * Return all the params of the ServiceRefDesc as an Hashtable.
     * @return all the params of the ServiceRefDesc as an Hashtable.
     */
    public Hashtable getParams() {
        return params;
    }

    /**
     * Return the value of the specified parameter
     * @param name the parameter to retrieve
     * @return the value of the specified parameter
     */
    public String getParam(String name) {
        return (String) params.get(name);
    }

    /**
     * Return the name of WSDL inside of the module.
     * @return the name of WSDL inside of the module
     */
    public String getWsdlFileName() {
        return wsdlFileName;
    }

    /**
     * Return the QName identifying the service in the WSDL. can return null if
     * WSDL not defined.
     * @return Return the QName identifying the service in the WSDL (can be
     *         null).
     */
    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(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(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;
    }

    /**
     * @return Returns the alternate WSDL URL (may be null).
     */
    public URL getAlternateWsdlURL() {
        return alternateWSDL;
    }

    /**
     * @return Returns the URL where the uptodate WSDL can be found.
     */
    public URL getLocalWSDLURL() {
        return localWSDLURL;
    }

    /**
     * @return Returns the URL where the mapping file can be found.
     */
    public URL getMappingFileURL() {
        return mappingFileURL;
    }
}