/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2008 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 org.apache.cxf.Bus;
import org.apache.cxf.BusException;
import org.apache.cxf.BusFactory;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.jaxws.JaxWsServerFactoryBean;
import org.apache.cxf.jaxws.support.JaxWsImplementorInfo;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.transport.DestinationFactory;
import org.apache.cxf.transport.DestinationFactoryManager;
import org.ow2.easybeans.api.EZBContainerCallbackInfo;
import org.ow2.easybeans.api.Factory;
import org.ow2.easybeans.container.EmptyLifeCycleCallBack;
import org.ow2.easybeans.container.session.stateless.StatelessSessionFactory;
import org.ow2.jonas.ws.cxf.PortComponent;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansEndpoint;
import org.ow2.jonas.ws.cxf.easybeans.EasyBeansJaxWsServerFactoryBean;
import org.ow2.jonas.ws.cxf.http.JOnASHTTPTransportFactory;
import org.ow2.jonas.ws.cxf.jaxws.CXFWSEndpoint;
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 static org.ow2.jonas.lib.util.Log.setComponentLogLevel;
import org.ow2.util.execution.ExecutionResult;
import org.ow2.util.execution.IExecution;
import org.ow2.util.execution.helper.RunnableHelper;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import org.objectweb.util.monolog.api.Level;
import org.objectweb.util.monolog.api.BasicLevel;

import javax.jws.WebService;
import javax.xml.ws.Endpoint;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

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

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

    /**
     * Easybeans internal Bus.
     */
    private Bus bus = null;

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

    /**
     * List of deployed endpoints.
     */
    private List<IWebServiceEndpoint> endpoints = new ArrayList<IWebServiceEndpoint>();

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

    /**
     * Initialize Internal CXF Bus.
     * @param info Callback Info.
     */
    private void initBus(final EZBContainerCallbackInfo info) {

        // Create a new Bus
        // Creation mechanism use the TCCL to access default Bus configuration
        IExecution<Bus> createBusJob = new IExecution<Bus>() {

            public Bus execute() throws Exception {

                // Bus start/stop operations are extra verbose
                Level old = setComponentLogLevel("org.apache.cxf", BasicLevel.LEVEL_ERROR);
                Level old2 = setComponentLogLevel("org.springframework", BasicLevel.LEVEL_ERROR);

                Bus newBus =  BusFactory.newInstance().createBus();

                // Reset log levels
                setComponentLogLevel("org.apache.cxf", old);
                setComponentLogLevel("org.springframework", old2);

                return newBus;
            }

        };

        ExecutionResult<Bus> res = new RunnableHelper<Bus>().execute(Bus.class.getClassLoader(),
                                                                     createBusJob);

        if (res.hasException()) {
            logger.error("Cannot create Bus", res.getException());
        }

        this.bus = res.getResult();

        try {
            createDestinationFactory();
        } catch (BusException e) {
            logger.error("Cannot create destination factory", e);
        }

        logger.info("Internal EasyBeans Bus started ...");

    }

    /**
     * Create the EasyBeans destination factory and register the transports.
     * @throws BusException if the factory is not created.
     */
    private void createDestinationFactory() throws BusException {

        JOnASHTTPTransportFactory factory = new JOnASHTTPTransportFactory(this.bus);

        // transportIds done by Spring ?
        List<String> namespaces = new ArrayList<String>();
        namespaces.add("http://schemas.xmlsoap.org/wsdl/soap/http");
        namespaces.add("http://schemas.xmlsoap.org/soap/http");
        namespaces.add("http://www.w3.org/2003/05/soap/bindings/HTTP/");
        namespaces.add("http://schemas.xmlsoap.org/wsdl/http/");
        namespaces.add("http://cxf.apache.org/transports/http/configuration");
        namespaces.add("http://cxf.apache.org/bindings/xformat");
        factory.setTransportIds(namespaces);

        for (String ns : namespaces) {
            registerTransport(factory, ns);
        }

    }

    /**
     * Utility method to register {@link DestinationFactory} under a namespace.
     * @param namespace the key
     * @param factory the {@link DestinationFactory} instance
     * @throws BusException if registration fails.
     */
    private void registerTransport(final DestinationFactory factory,
                                   final String namespace) throws BusException {

        DestinationFactoryManager manager = this.bus.getExtension(DestinationFactoryManager.class);
        manager.registerDestinationFactory(namespace, factory);
    }

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

        // 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
                WebService ws = klass.getAnnotation(WebService.class);

                // only for @WebService
                if (ws != null) {

                    // Init CXF Bus if needed
                    initBus(info);

                    String wsdlName = ws.wsdlLocation();

                    // Load the WSDL if possible
                    URL url = null;
                    if (wsdlName != null) {
                        url = klass.getClassLoader().getResource(wsdlName);
                        if (url == null) {
                            try {
                                url = new URL(wsdlName);
                            } catch (MalformedURLException ex2) {
                                url = klass.getClassLoader().getResource("/" + wsdlName);
                            }
                        }
                    }

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

                    EasyBeansEndpoint easybeansEndpoint = createEasyBeansEndpoint(factory, klass, url);

                    // find the URL pattern to use

                    // TODO Fix the @PortComponent annotation, not currently working
                    String pattern = null;
                    PortComponent pc = klass.getAnnotation(PortComponent.class);

                    // But values could be overrided
                    // TODO use the merged metadata here !
                    String serviceName = ws.serviceName();
                    String portName = ws.portName();
                    String name = ws.name();

                    if (pc != null) {
                        pattern = pc.urlPattern();
                    } else {
                        if (!"".equals(portName)) {
                            // default pattern : service-name/port-name
                            pattern = "/" + serviceName + "/" + portName;
                        } else {
                            pattern = "/" + serviceName + "/" + name;
                        }
                    }

                    // Creates PortMetaData to hold deployment/runtime information
                    // about the web service
                    PortMetaData pmd = new PortMetaData();
                    String contextRoot = extractContextRoot(info.getArchive().getName());
                    pmd.setContextRoot(contextRoot);
                    pmd.setUrlPattern(pattern);

                    logger.info("PortMetaData [context:{0}, pattern:{1}]",
                                pmd.getContextRoot(),
                                pmd.getUrlPattern());

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

                    // 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 {
                        manager.registerWSEndpoint(endpoint);
                    } catch (WSException e) {
                        // Log an error
                        logger.error("Unable to register endpoint {0}", endpoint, e);
                        throw new RuntimeException("Endpoint not deployed completely", e);
                    }

                    endpoint.start();

                    // Add the endpoint in the list
                    endpoints.add(endpoint);

                    logger.info("Endpoint {0} inited", klass.getName());

                }
            }
        }
    }

    /**
     * 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) {
        String context = filename.substring(filename.lastIndexOf(File.separator) + 1);
        context = context.substring(0, context.lastIndexOf('.'));
        return context;
    }

    /**
     * Configure CXF internals for the new JAXWS {@link Endpoint}.
     * @param factory The EasyBeans EJB factory (used to get the instances)
     * @param klass Bean's class
     * @param wsdlUrl URL where the WSLD can be loaded
     * @return a configured CXF {@link EndpointImpl} subclass.
     */
    private EasyBeansEndpoint createEasyBeansEndpoint(final StatelessSessionFactory factory, final Class<?> klass, final URL wsdlUrl) {

        JaxWsImplementorInfo jaxWsImplementorInfo = new JaxWsImplementorInfo(klass);
        JaxWsServiceFactoryBean jaxWsServiceFactoryBean = new JaxWsServiceFactoryBean(jaxWsImplementorInfo);
        jaxWsServiceFactoryBean.setBus(this.bus);
        if (wsdlUrl != null) {
            jaxWsServiceFactoryBean.setWsdlURL(wsdlUrl);
        }

        // Create a service with the correct classloader
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(klass.getClassLoader());
            jaxWsServiceFactoryBean.create();
        } finally {
            // reset
            Thread.currentThread().setContextClassLoader(old);
        }

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

        return new EasyBeansEndpoint(factory, this.bus, factoryBean);
    }

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

        stop();
        logger.info("Bus stopped for " + info.getArchive());

    }

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

        List<WSException> exceptions = new ArrayList<WSException>();

        for (IWebServiceEndpoint endpoint : endpoints) {
            try {
                // Stop and unregister
                endpoint.stop();
                manager.unregisterWSEndpoint(endpoint);
            } catch (WSException e) {
                // Do not rethrow the Exception directly: try to
                // undeploy the other endpoints before
                exceptions.add(e);
            }
        }

        // Clear the list
        endpoints.clear();

        // Stop the Bus
        if (this.bus != null) {
            // Bus start/stop operations are extra verbose
            Level old = setComponentLogLevel("org.apache.cxf", BasicLevel.LEVEL_ERROR);
            Level old2 = setComponentLogLevel("org.springframework", BasicLevel.LEVEL_ERROR);

            this.bus.shutdown(true);
            this.bus = null;

            // Reset log levels
            setComponentLogLevel("org.apache.cxf", old);
            setComponentLogLevel("org.springframework", old2);
        }

        // Warn the user for each of the unregistration that have failed
        // TODO add more details (names, ...)
        for (WSException exception : exceptions) {
            logger.warn("Unable to stop/unregister endpoint", exception);
        }
    }

}
