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

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.naming.NamingException;
import javax.naming.Reference;
import javax.servlet.ServletContext;
import javax.xml.ws.Endpoint;
import javax.xml.ws.WebServiceFeature;
import javax.xml.ws.soap.MTOMFeature;
import javax.xml.ws.soap.SOAPBinding;
import javax.xml.ws.spi.Provider;

import org.apache.cxf.Bus;
import org.apache.cxf.BusFactory;
import org.apache.cxf.jaxws.EndpointImpl;
import org.apache.cxf.jaxws.spi.ProviderImpl;
import org.apache.cxf.jaxws.support.JaxWsServiceFactoryBean;
import org.apache.cxf.resource.ResourceManager;
import org.apache.cxf.transport.servlet.ServletContextResourceResolver;
import org.apache.cxf.version.Version;
import org.objectweb.util.monolog.api.BasicLevel;
import org.objectweb.util.monolog.api.Level;
import org.ow2.easybeans.server.Embedded;
import org.ow2.jonas.service.ServiceException;
import org.ow2.jonas.ws.cxf.client.CXFWebServiceRefBuilder;
import org.ow2.jonas.ws.cxf.easybeans.ext.WebServiceContextInjectionHandler;
import org.ow2.jonas.ws.cxf.easybeans.naming.WebServiceRefExtensionListener;
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.IJAXWSService;
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.JAXWSService;
import org.ow2.jonas.ws.jaxws.base.JAXWSWebservicesContainer;
import org.ow2.jonas.ws.jaxws.handler.builder.DescriptorHandlerChainBuilder;
import org.ow2.jonas.ws.publish.WSDLPublisherException;
import org.ow2.jonas.ws.publish.WSDLPublisherManager;
import org.ow2.util.annotation.processor.DefaultAnnotationProcessor;
import org.ow2.util.annotation.processor.IAnnotationProcessor;
import org.ow2.util.annotation.processor.ProcessorException;
import org.ow2.util.ee.builder.webserviceref.WebServiceRefBuilder;
import org.ow2.util.ee.metadata.common.api.struct.IJaxwsWebServiceRef;
import org.ow2.util.ee.metadata.common.api.xml.struct.IHandlerChains;
import org.ow2.util.ee.metadata.war.api.IWarClassMetadata;
import org.ow2.util.ee.metadata.ws.api.struct.IWebServiceMarker;
import org.ow2.util.event.api.IEventService;
import org.ow2.util.log.Log;
import org.ow2.util.log.LogFactory;
import static org.ow2.jonas.lib.util.Log.setComponentLogLevel;

/**
 * The CXFService class is used to declare CXF as a module in JOnAS.
 *
 * @author Guillaume Sauthier
 */
public class CXFService extends JAXWSService implements IJAXWSService {

    /**
     * This Bus is shared by all clients inside this JVM.
     * This is the default CXF Bus.
     */
    private Bus clientBus;

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

    /**
     * The event service used to register listeners.
     */
    private IEventService eventService;

    /**
     * The naming extension listener for EasyBean's java:comp/env building.
     */
    private WebServiceRefExtensionListener listener;

    /**
     * Each WebApp may be associated with one WSModule.
     */
    Map<String, WebservicesModule> webservicesModules = new HashMap<String, WebservicesModule>();

    /**
     * WSDL Publisher.
     */
    private WSDLPublisherManager publisherManager;

    /**
     * Abstract start-up method to be implemented by sub-classes.
     *
     * @throws org.ow2.jonas.service.ServiceException
     *          service start-up failed
     */
    @Override
    protected void doStart() throws ServiceException {

        // CXF Bus init is extra verbose, level up the default logging level
        Level old = setComponentLogLevel("org.apache.cxf", BasicLevel.LEVEL_ERROR);
        Level old2 = setComponentLogLevel("org.springframework", BasicLevel.LEVEL_ERROR);

        // TODO: Remove these lines when issue will be closed
        // Avoid to get e.printStackTrace from CatalogManager class by catching error stream
        PrintStream previousErrorStream = System.err;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        PrintStream tempStream = new PrintStream(baos);
        System.setErr(tempStream);
        try {
            // Force initialization of the singleton Bus shared by all clients.
            clientBus = BusFactory.getDefaultBus();
        } finally {
            System.setErr(previousErrorStream);
            // Check if there was an UnknownHostException
            String errorMessage = baos.toString();
            if (errorMessage != null && errorMessage.contains("java.net.UnknownHostException")) {
                // debug level
                logger.debug("Error while initializing the Bus : ''{0}'' ", errorMessage);
            } else if (!"".equals(errorMessage)) {
                // error level
                logger.error("Error while initializing the Bus : ''{0}'' ", errorMessage);
            }

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

        // Force usage of the CXF JAX-WS Provider
        System.setProperty(Provider.JAXWSPROVIDER_PROPERTY, ProviderImpl.class.getName());

        super.doStart();

        logger.info("{0} started", Version.getCompleteVersionString());
    }

    /**
     * Abstract method for service stopping to be implemented by sub-classes.
     *
     * @throws org.ow2.jonas.service.ServiceException
     *          service stopping failed
     */
    @Override
    protected void doStop() throws ServiceException {

        Level old = setComponentLogLevel("org.apache.cxf", BasicLevel.LEVEL_ERROR);
        Level old2 = setComponentLogLevel("org.springframework", BasicLevel.LEVEL_ERROR);

        // Stop the client bus
        // true means 'wait for complete stop'
        clientBus.shutdown(true);
        clientBus = null;
        BusFactory.setDefaultBus(null);

        setComponentLogLevel("org.apache.cxf", old);
        setComponentLogLevel("org.springframework", old2);

        super.doStop();

        logger.info("CXF service stopped");
    }

    /**
     * Construct a Reference dedicated to the given @WebServiceRef metadata.
     *
     * @param serviceRef the reference described by the user.
     * @return a JNDI Naming {@link javax.naming.Reference} that can be bound in a JNDI Context.
     */
    public Reference createNamingReference(final IJaxwsWebServiceRef serviceRef) throws NamingException {

        // This could be subclassed for any CXF related specific needs
        WebServiceRefBuilder builder = new CXFWebServiceRefBuilder();

        return builder.build(serviceRef);
    }

    /**
     * Creates a new POJO Web service endpoint from classes metadata.
     *
     * @param metadata the class own metadata (annotation + XML)
     * @param loader the ClassLoader to be used to load classes from the metadata
     * @return A POJO typed IWebServiceEndpoint based on metadata
     */
    public IWebServiceEndpoint createPOJOWebServiceEndpoint(final IWarClassMetadata metadata,
                                                            final ClassLoader loader,
                                                            final ServletContext servletContext)
             throws WSException {

        // Find a unique name for the WebModule
        String name = findUniqueContextName(servletContext);

        // The module linked to this archive
        WebservicesModule module = webservicesModules.get(name);
        if (module == null) {
            module = new WebservicesModule(name);
            webservicesModules.put(name, module);
        }

        // Load the class
        String classname = metadata.getJClass().getName().replace('/', '.');
        Class<?> klass = null;
        try {
            klass = loader.loadClass(classname);
        } catch (ClassNotFoundException e) {
            throw new WSException("Impossible to load the WebService class", e);
        }

        IWebServiceMarker marker = metadata.getWebServiceMarker();

        JOnASJaxWsImplementorInfo jaxWsImplementorInfo = new JOnASJaxWsImplementorInfo(klass, marker);

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

        }

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

        EndpointImpl ep = createEndpointImpl(container.getBus(),
                                             klass,
                                             jaxWsImplementorInfo,
                                             marker);

        // JaxWsImplementorInfo contains merged/overrided values
        String serviceName = jaxWsImplementorInfo.getServiceName().getLocalPart();

        // find the URL pattern to use
        String pattern = null;
        if (!"".equals(serviceName)) {
            // default pattern : /{service-name}
            pattern = "/" + serviceName;
        } else {
            // else /{class.name}Service
            pattern = "/" + klass.getSimpleName() + "Service";
        }

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

        logger.debug("PortMetaData [pattern:{0}]", pmd.getUrlPattern());

        // Wraps the Endpoint into our own IWSEndpoint implementation
        CXFWSEndpoint endpoint = new CXFWSEndpoint(ep,
                                                   IWebServiceEndpoint.EndpointType.POJO,
                                                   pmd,
                                                   container);

        // Link the endpoint
        container.addEndpoint(endpoint);

        return endpoint;
    }

    private String findUniqueContextName(final ServletContext context) {
        return context.getRealPath("/");
    }

    /**
     * Configure CXF internals for the new JAXWS {@link javax.xml.ws.Endpoint}.
     * @param bus the Bus that will be used to support the new endpoint
     * @param klass POJO's class
     * @param info Annotation merged values
     * @param serviceMarker
     * @return a configured CXF {@link org.apache.cxf.jaxws.EndpointImpl} instance.
     * @throws WSException if it was impossible to create a new instance from the klass parameter
     */
    private EndpointImpl createEndpointImpl(final Bus bus,
                                            final Class<?> klass,
                                            final JOnASJaxWsImplementorInfo info,
                                            final IWebServiceMarker serviceMarker)
               throws WSException {

        JaxWsServiceFactoryBean jaxWsServiceFactoryBean = new JaxWsServiceFactoryBean(info);
        jaxWsServiceFactoryBean.setBus(bus);

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

        // Create a service with the correct classloader
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        Object implementor = null;
        try {
            Thread.currentThread().setContextClassLoader(klass.getClassLoader());

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

            // Init
            jaxWsServiceFactoryBean.create();

            // Create the "service" instance
            implementor = klass.newInstance();
        } catch (IllegalAccessException e) {
            throw new WSException("Cannot access '" + klass.getName() + "'", e);
        } catch (InstantiationException e) {
            throw new WSException("Cannot instantiate '" + klass.getName() + "'", e);
        } finally {
            // reset
            Thread.currentThread().setContextClassLoader(old);
        }

        // Build the ServerFactory
        JOnASJaxWsServerFactoryBean factoryBean = new JOnASJaxWsServerFactoryBean(jaxWsServiceFactoryBean);

        // Check if the marker provides a HandlerChains
        IHandlerChains handlerChains = serviceMarker.getHandlerChains();
        if (handlerChains != null) {
            factoryBean.setHandlerChainBuilder(new DescriptorHandlerChainBuilder(handlerChains));
        }

        // Inject the implementor with WebServiceContext
        injectImplementorResources(implementor);

        // Build the endpoint
        EndpointImpl endpoint = new EndpointImpl(bus, implementor, factoryBean);
        endpoint.setServiceFactory(jaxWsServiceFactoryBean);

        // Set some endpoints properties (avoid some NPE later)
        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;
    }

    /**
     * Inject the given Object with requested @Resource
     * @param implementor instance to be injected
     * @throws WSException if injection failed
     */
    private void injectImplementorResources(final Object implementor) throws WSException {
        IAnnotationProcessor processor = new DefaultAnnotationProcessor();
        // Inject @Resource WebServiceContext
        processor.addAnnotationHandler(new WebServiceContextInjectionHandler());

        try {
            processor.process(implementor);
        } catch (ProcessorException e) {
            throw new WSException("Cannot inject instance " + implementor, e);
        }
    }


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

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


    /**
     * Finalize the deployment of POJO endpoints contained in the given ServletContext.
     *
     * @param context ServletContext.
     */
    public void finalizePOJODeployment(final ServletContext context) {

        String name = findUniqueContextName(context);

        WebservicesModule module = webservicesModules.get(name);
        if (module != null) {
            module.start();

            for (JAXWSWebservicesContainer container : module.getContainers()) {
                publishWSDL((WebservicesContainer)container);
            }

        }
    }

    /**
     * Stop and undeploy the POJO endpoints contained in the given ServletContext.
     *
     * @param context ServletContext.
     */
    public void undeployPOJOEndpoints(final ServletContext context) {

        String name = findUniqueContextName(context);

        WebservicesModule module = webservicesModules.get(name);
        if (module != null) {
            module.stop();
            // Remove the module from the list
            webservicesModules.remove(name);
        }
    }

    /**
     * Callback invoked when an EventService becomes available.
     * @param eventService the event service
     */
    public void bindEventService(final IEventService eventService) {
        this.eventService = eventService;
        listener = new WebServiceRefExtensionListener();
        eventService.registerListener(listener, Embedded.NAMING_EXTENSION_POINT);
    }

    /**
     * Callback invoked when the used EventService becomes unavalable.
     * @param eventService disposed service
     */
    public void unbindEventService(final IEventService eventService) {
        this.eventService.unregisterListener(listener);
        this.eventService = null;
    }

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