/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008-2009 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: LifeCycleCallback.java 21003 2011-03-16 12:34:08Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.ws.cxf.easybeans.ext;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import javax.xml.ws.Endpoint;
//import javax.xml.ws.WebServiceFeature;
//import javax.xml.ws.soap.MTOMFeature;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.jaxws.support.JaxWsImplementorInfo;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.resource.ClassLoaderResolver;
import org.apache.cxf.resource.ResourceManager;
import org.ow2.easybeans.api.EZBContainerCallbackInfo;
import org.ow2.easybeans.api.Factory;
import org.ow2.easybeans.api.bean.info.IBeanInfo;
import org.ow2.easybeans.api.bean.info.IWebServiceInfo;
import org.ow2.easybeans.container.EmptyLifeCycleCallBack;
import org.ow2.easybeans.container.session.stateless.StatelessSessionFactory;
import org.ow2.jonas.ws.cxf.BusCreator;
import org.ow2.jonas.ws.cxf.JOnASJaxWsImplementorInfo;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansJaxWsServiceFactoryBean;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansJaxWsServerFactoryBean;
import org.ow2.jonas.ws.cxf.easybeans.CXFEJBWebserviceEndpoint;
import org.ow2.jonas.ws.cxf.jaxws.WebservicesContainer;
import org.ow2.jonas.ws.jaxws.IWebServiceDeploymentManager;
import org.ow2.jonas.ws.jaxws.IWebServiceEndpoint;
import org.ow2.jonas.ws.jaxws.PortMetaData;
import org.ow2.jonas.ws.jaxws.WSException;
import org.ow2.jonas.ws.jaxws.IWebservicesModule;
import org.ow2.jonas.ws.jaxws.ejb.IWebDeployer;
import org.ow2.jonas.ws.jaxws.base.JAXWSWebservicesModule;
import org.ow2.jonas.ws.jaxws.util.JAXWSClassUtils;
import org.ow2.jonas.ws.publish.WSDLPublisherException;
import org.ow2.jonas.ws.publish.WSDLPublisherManager;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;


/**
 * Callback for CXF.
 * @author Guillaume Sauthier
 * @author Florent Benoit
 */
public class LifeCycleCallback extends EmptyLifeCycleCallBack {

    /**
     * Logger.
     */
    private static Log logger = LogFactory.getLog(LifeCycleCallback.class);

    /**
     * WSDL Publication tool.
     */
    private WSDLPublisherManager publisherManager;

    /**
     * A Module that contains WSContainer and Endpoints.
     */
    private Map<String, IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>>> modules;

    /**
     * The Web Deployer used to configure Web contexts for the endpoints.
     */
    private IWebDeployer webDeployer;

    /**
     * BusFactory service.
     */
    private BusFactory factory;

    /**
     * Construct a new Callback.
     */
    public LifeCycleCallback() {
        modules = new HashMap<String, IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>>>();
    }

    /**
     * Called when container is starting.
     * @param info some information on the container which is starting.
     */
    @Override
    public void start(final EZBContainerCallbackInfo info) {

        // Archive identifier
        String name = info.getArchive().getName();

        IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>> module = null;

        // Generic part ----------

        // Iterates over the factories
        Map<String, Factory<?, ?>> factories = info.getFactories();
        for (Factory<?, ?> f : factories.values()) {

            // Only for @Stateless
            if (f instanceof StatelessSessionFactory) {

                StatelessSessionFactory factory = (StatelessSessionFactory) f;

                Class<?> klass = factory.getBeanClass();

                // An EJB exposed as a JAX-WS web services HAS TO be annotated
                // with @WebService or @WebServiceProvider
                if (JAXWSClassUtils.isWebService(klass)) {

                    // Extract merged webservices deploy time informations
                    IBeanInfo beanInfo = factory.getBeanInfo();
                    IWebServiceInfo webServiceInfo = beanInfo.getWebServiceInfo();

                    // Creates PortMetaData to hold deployment/runtime information
                    // about the web service
                    PortMetaData pmd = new PortMetaData();

                    // End of Generic Part -----------

                    JOnASJaxWsImplementorInfo jaxWsImplementorInfo;
                    if (webServiceInfo != null) {
                        jaxWsImplementorInfo = new JOnASJaxWsImplementorInfo(klass, webServiceInfo);

                        // Also try to read context-root
                        pmd.setContextRoot(webServiceInfo.getContextRoot());
                    } else {
                        jaxWsImplementorInfo = new JOnASJaxWsImplementorInfo(klass);
                    }

                    // Get the WSModule for this archive if not already set
                    module = modules.get(name);
                    if (module == null) {
                        module = createWebservicesModule(name);
                        modules.put(name, module);
                    }

                    // Get a Container for all services deployed using the same WSDL
                    WebservicesContainer<CXFEJBWebserviceEndpoint> container = module.findContainer(jaxWsImplementorInfo.getWsdlLocation());
                    if (container == null) {
                        // Create a new Container
                        container = createWebservicesContainer(jaxWsImplementorInfo.getWsdlLocation());
                        module.addContainer(container);

                        // The Bus can resolve file against the module Classloader
                        ResourceManager manager = container.getBus().getExtension(ResourceManager.class);
                        manager.addResourceResolver(new ClassLoaderResolver(klass.getClassLoader()));
                    }

                    // find the URL pattern to use
                    String pattern = getUrlPattern(klass, webServiceInfo, jaxWsImplementorInfo);
                    pmd.setUrlPattern(pattern);

                    // Creates our Endpoint
                    EndpointImpl easybeansEndpoint = createEasyBeansEndpoint(factory, container.getBus(),
                            jaxWsImplementorInfo);

                    // Wraps the Endpoint into our own IWSEndpoint implementation
                    CXFEJBWebserviceEndpoint endpoint = new CXFEJBWebserviceEndpoint(
                            easybeansEndpoint,
                            pmd,
                            container,
                            info.getContainer(),
                            beanInfo);

                    // Add the endpoint in the list
                    container.addEndpoint(endpoint);

                    // If one of the endpoint set a wsdl-publication-directory, use it
                    if (webServiceInfo != null) {
                        String directory = webServiceInfo.getWsdlPublicationDirectory();
                        if (directory != null) {
                            container.setWsdlPublicationDirectory(directory);
                        }
                    }

                }
            }
        }

        // Start the module if some services where found
        if (module != null) {
            module.start();

            try {
                // Deploy the web context of the endpoints
                webDeployer.deploy(module);

                // WSDL publication
                for (WebservicesContainer<CXFEJBWebserviceEndpoint> container : module.getContainers()) {

                    // Attempt to publish the service's WSDLs
                    publishWSDL(container);

                    // Display the endpoint's URLs
                    for (CXFEJBWebserviceEndpoint endpoint : container.getEndpoints()) {
                        endpoint.displayInfos();
                    }
                }
            } catch (WSException e) {

                // Display a warning
                logger.warn("Webservices endpoints of the EjbJar ''{0}'' cannot be deployed because of some error: {1}",
                            info.getContainer().getName(),
                            e.getMessage(),
                            e);

                // An exception occured, cleanup
                webDeployer.undeploy(module);
                stopWebservicesModule(module);
                modules.remove(name);

            }

        }

    }

    protected WebservicesContainer<CXFEJBWebserviceEndpoint> createWebservicesContainer(String name) {
        return new WebservicesContainer<CXFEJBWebserviceEndpoint>(name, factory);
    }

    protected JAXWSWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>> createWebservicesModule(String name) {
        return new JAXWSWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>>(name);
    }

    /**
     * Enable MTOM feature on CXF service factory if it is enabled.
     * @param jaxWsServiceFactoryBean the CXF service factory bean
     * @param info the annotation merged info
     */
    protected void checkEnableMtom(final JaxWsServiceFactoryBean jaxWsServiceFactoryBean, final JOnASJaxWsImplementorInfo info) {
        if (info == null) {
            return;
        }
        // MTOM is enabled
        if (info.isMTOMEnabled()) {
            String protocolBinding = info.getBindingType();
            // if the protocol binding is correct, enable the MTOM feature
            if (protocolBinding != null && (SOAPBinding.SOAP11HTTP_BINDING.equals(protocolBinding) || SOAPBinding.SOAP12HTTP_BINDING.equals(protocolBinding))) {
                    //List<WebServiceFeature> wsFeatures = jaxWsServiceFactoryBean.getWsFeatures();
                    // Create if not present
                    //if (wsFeatures == null) {
                    //    wsFeatures = new ArrayList<WebServiceFeature>();
                    //    jaxWsServiceFactoryBean.setWsFeatures(wsFeatures);
                    //}
                    //wsFeatures.add(new MTOMFeature(true));
            }
        }
    }

    /**
     * Get a valid URL pattern from available deployment info.
     * TODO Should be moved inside the IWebDeployer (another strategy ?)
     * @param klass endpoint class
     * @param webServiceInfo easybeans.xml infos
     * @param jaxWsImplementorInfo merged deployment info
     * @return a valid url pattern
     */
    private String getUrlPattern(final Class<?> klass,
                                 final IWebServiceInfo webServiceInfo,
                                 final JaxWsImplementorInfo jaxWsImplementorInfo) {

        String pattern;

        // If user has specified an endpoint address, use it
        if ((webServiceInfo != null) && (webServiceInfo.getEndpointAddress() != null)) {

            // User defined address
            pattern = webServiceInfo.getEndpointAddress();
            if (!pattern.startsWith("/")) {
                // Pattern needs to start with a '/'
                pattern = "/" + pattern;
            }
        } else {

            // Default naming strategy
            String serviceName = jaxWsImplementorInfo.getServiceName().getLocalPart();
            if (!"".equals(serviceName)) {
                // default pattern : /{service-name}
                pattern = "/" + serviceName;
            } else {
                // else /{class-name}Service
                pattern = "/" + klass.getSimpleName() + "Service";
            }
        }
        return pattern;
    }

    /**
     * Publish this service's WSDL using the WSDL publisher manager.
     * @param container Group of endpoint sharing the same WSDL
     */
    private void publishWSDL(final WebservicesContainer container) {

        try {
            publisherManager.publish(container.getPublishableDefinitions());
        } catch (WSDLPublisherException e) {
            // Log an error
            logger.error("Unable to publish WSDL for the container ''{0}''", container.getName(), e);
        }

    }

    /**
     * Extract a context-root name from the archive filename.
     * @param filename archive filename
     * @return a context-root which is the last part of the filename minus the extension
     */
    private static String extractContextRoot(final String filename) {
        // Remove everything before the last '/'
        String context = filename.substring(filename.lastIndexOf(File.separator) + 1);
        int underscoreIndex = context.indexOf('_');
        if (underscoreIndex == -1) {
            // Cut before the last '.'
            context = context.substring(0, context.lastIndexOf('.'));
        } else {
            // Cut before the first '_' for unpacked archives with timestamp
            context = context.substring(0, underscoreIndex);
        }
        return context;
    }

    /**
     * Configure CXF internals for the new JAXWS {@link Endpoint}.
     * @param factory The EasyBeans EJB factory (used to get the instances)
     * @param bus The CXF Bus to use
     * @param info Info about the port being deployed
     * @return a configured CXF {@link EasyBeansEndpoint} subclass.
     */
    private EndpointImpl createEasyBeansEndpoint(final StatelessSessionFactory factory,
                                                 final Bus bus,
                                                 final JOnASJaxWsImplementorInfo info) {

        JaxWsServiceFactoryBean jaxWsServiceFactoryBean = new EasyBeansJaxWsServiceFactoryBean(info, factory);
        jaxWsServiceFactoryBean.setBus(bus);

        // Enable MTOM ?
        checkEnableMtom(jaxWsServiceFactoryBean, info);

        // We need to call create() here in order to initialize the ServiceFactory
        // otherwise, CXF try to guess some values without using our JaxWsImplementorInfo object.
        // We also need to set up the TCCL to a ClassLoader that can access
        // the JAXB2 implementation packages
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(Bus.class.getClassLoader());

            // Initialisation
            jaxWsServiceFactoryBean.setServiceName(info.getServiceName());
            jaxWsServiceFactoryBean.create();
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }

        // Build the ServerFactory
        JaxWsServerFactoryBean factoryBean = new EasyBeansJaxWsServerFactoryBean(jaxWsServiceFactoryBean,
                                                                                 factory);

        // Build the EasyBeans endpoint
        EndpointImpl endpoint = new EndpointImpl(bus, factory.getBeanClass(), factoryBean);
        endpoint.setImplementorClass(factory.getBeanClass());
        endpoint.setServiceFactory(jaxWsServiceFactoryBean);

        // Set some endpoint properties
        Map<String, Object> properties = new HashMap<String, Object>();
        if (info.getServiceName() != null) {
            properties.put(Endpoint.WSDL_SERVICE, info.getServiceName());
        }
        if (info.getEndpointName() != null) {
            properties.put(Endpoint.WSDL_PORT, info.getEndpointName());
        }
        endpoint.setProperties(properties);

        // Manage Audit handler
        Handler audit = (Handler) bus.getProperty(BusCreator.class.getName() + ".ENDPOINT");
        endpoint.getHandlers().add(audit);

        return endpoint;
    }

    /**
     * Called when container is stopping.
     * @param info some information on the container which is stopping.
    */
    @Override
    public void stop(final EZBContainerCallbackInfo info) {

        // Only stop the good module
        String name = info.getArchive().getName();
        IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>> module = modules.get(name);

        if (module != null) {

            stopWebservicesModule(module);
            modules.remove(name);
        }

    }

    /**
     * This stop method is not directly called by the EJB container. The lifecycle object
     * itself has decided to stop (maybe one of its dependencies is now unavailable)
     */
    public void stop() {

        logger.debug("Stop ''{0}''", this);

        // Stop all the modules
        for (Map.Entry<String, IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>>> entry : modules.entrySet()) {

            IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>> module = entry.getValue();

            // Stop the module
            stopWebservicesModule(module);
        }

        // Clear the map
        modules.clear();
    }

    /**
     * Stop the given module.
     * @param module module to be stopped
     */
    private void stopWebservicesModule(final IWebservicesModule<WebservicesContainer<CXFEJBWebserviceEndpoint>> module) {

        // Need to unregister the endpoints
        webDeployer.undeploy(module);

        // It will recursively stop containers and endpoints
        module.stop();
    }

    /**
     * Set the WSDLPublisherManager service.
     * @param publisherManager publication tool
     */
    public void setWSDLPublisherManager(final WSDLPublisherManager publisherManager) {
        this.publisherManager = publisherManager;
    }

    /**
     * Set the web deployer to be used to manage web contexts.
     * @param webDeployer deployer
     */
    public void setWebDeployer(final IWebDeployer webDeployer) {
        this.webDeployer = webDeployer;
    }

    /**
     * Set the BusFactory to be used
     * @param factory BusFactory
     */
    public void setBusFactory(BusFactory factory) {
        this.factory = factory;
    }
}
