/**
 * 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$
 * --------------------------------------------------------------------------
 */

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

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

import javax.xml.ws.Endpoint;
import javax.xml.ws.WebServiceFeature;
import javax.xml.ws.soap.MTOMFeature;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
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.JOnASJaxWsImplementorInfo;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansEndpoint;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansJaxWsServiceFactoryBean;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansJaxWsServerFactoryBean;
import org.ow2.jonas.ws.cxf.jaxws.CXFWSEndpoint;
import org.ow2.jonas.ws.cxf.jaxws.WebservicesContainer;
import org.ow2.jonas.ws.cxf.jaxws.WebservicesModule;
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.base.JAXWSWebservicesContainer;
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);

    /**
     * The WS deployment manager.
     */
    private IWebServiceDeploymentManager manager;

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

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

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

    /**
     * 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();

        WebservicesModule 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();
                    String contextRoot = extractContextRoot(info.getArchive().getName());
                    pmd.setContextRoot(contextRoot);

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

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

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

                    // Get a Container for all services deployed using the same WSDL
                    WebservicesContainer container = (WebservicesContainer)module.findContainer(jaxWsImplementorInfo.getWsdlLocation());
                    if (container == null) {
                        // Create a new Container
                        container = new WebservicesContainer(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
                    EasyBeansEndpoint easybeansEndpoint = createEasyBeansEndpoint(factory, container.getBus(),
                            jaxWsImplementorInfo);

                    // Wraps the EasyBeansEndpoint into our own IWSEndpoint implementation
                    CXFWSEndpoint endpoint = new CXFWSEndpoint(easybeansEndpoint,
                                                               IWebServiceEndpoint.EndpointType.EJB,
                                                               pmd,
                                                               container);

                    // 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();

            for (JAXWSWebservicesContainer container : module.getContainers()) {

                // Register the endpoints
                for (IWebServiceEndpoint endpoint : container.getEndpoints()) {
                    // OK, We have a endpoint, in some way, we have to register it to the web container
                    // So here we should have an IWSDeploymentManager instance that we can use
                    try {

                        // Register our endpoint in the webcontainer
                        manager.registerWSEndpoint(endpoint);

                        // Print infos
                        endpoint.displayInfos();
                    } catch (WSException e) {
                        // Log an error
                        logger.error("Unable to register endpoint ''{0}''", endpoint, e);

                        // Try to unregister previous stuff
                        try {
                            manager.unregisterWSEndpoint(endpoint);
                        } catch (WSException e1) {
                            logger.error("Unable to unregister endpoint ''{0}'' after a failed deployment", endpoint, e);
                        }
                    }
                }

                // Attempt to publish this service's WSDL
                publishWSDL((WebservicesContainer)container);

            }
        }

    }

    /**
     * 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.
     * @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 EasyBeansEndpoint 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
        EasyBeansEndpoint endpoint = new EasyBeansEndpoint(factory, bus, factoryBean);
        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);
        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();
        WebservicesModule 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, WebservicesModule> entry : modules.entrySet()) {

            WebservicesModule 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 WebservicesModule module) {

        // Need to unregister the endpoints
        for (JAXWSWebservicesContainer container : module.getContainers()) {
            for (IWebServiceEndpoint endpoint : container.getEndpoints()) {
                try {
                    manager.unregisterWSEndpoint(endpoint);
                } catch (WSException wse) {
                    // Do not rethrow the Exception directly: try to
                    // undeploy the other endpoints before
                    logger.warn("Cannot unregister endpoint ''{0}'', continue processing", endpoint, wse);
                }
            }
        }

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

    /**
     * @param manager the manager to set
     */
    public void setManager(final IWebServiceDeploymentManager manager) {
        this.manager = manager;
    }

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