/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2015, Red Hat Middleware LLC, and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This 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 (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.wsf.stack.cxf.configuration;

import java.lang.reflect.Constructor;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.xml.stream.XMLStreamReader;
import javax.xml.ws.handler.Handler;
import javax.xml.ws.soap.SOAPBinding;

import org.apache.cxf.Bus;
import org.apache.cxf.annotations.UseAsyncMethod;
import org.apache.cxf.buslifecycle.BusLifeCycleListener;
import org.apache.cxf.buslifecycle.BusLifeCycleManager;
import org.apache.cxf.configuration.Configurer;
import org.apache.cxf.endpoint.ServerLifeCycleManager;
import org.apache.cxf.interceptor.OneWayProcessorInterceptor;
import org.apache.cxf.management.InstrumentationManager;
import org.apache.cxf.management.counters.CounterRepository;
import org.apache.cxf.management.interceptor.ResponseTimeMessageInInterceptor;
import org.apache.cxf.management.interceptor.ResponseTimeMessageInvokerInterceptor;
import org.apache.cxf.management.interceptor.ResponseTimeMessageOutInterceptor;
import org.apache.cxf.resource.ResourceManager;
import org.apache.cxf.resource.ResourceResolver;
import org.apache.cxf.service.factory.FactoryBeanListener;
import org.apache.cxf.service.factory.FactoryBeanListenerManager;
import org.apache.cxf.service.invoker.Invoker;
import org.apache.cxf.staxutils.XMLStreamReaderWrapper;
import org.apache.cxf.transport.http.HttpDestinationFactory;
import org.apache.cxf.transport.servlet.ServletDestinationFactory;
import org.apache.cxf.workqueue.AutomaticWorkQueue;
import org.apache.cxf.workqueue.AutomaticWorkQueueImpl;
import org.apache.cxf.workqueue.WorkQueueManager;
import org.apache.cxf.ws.addressing.WSAddressingFeature;
import org.apache.cxf.ws.discovery.listeners.WSDiscoveryServerListener;
import org.apache.cxf.ws.policy.AlternativeSelector;
import org.apache.cxf.ws.policy.PolicyEngine;
import org.apache.cxf.ws.policy.selector.MaximalAlternativeSelector;
import org.apache.cxf.ws.rm.RMManager;
import org.apache.cxf.wsdl.WSDLManager;
import org.apache.cxf.wsdl11.WSDLManagerImpl;
import org.jboss.ws.api.annotation.PolicySets;
import org.jboss.ws.api.binding.BindingCustomization;
import org.jboss.wsf.spi.SPIProvider;
import org.jboss.wsf.spi.WSFException;
import org.jboss.wsf.spi.classloading.ClassLoaderProvider;
import org.jboss.wsf.spi.deployment.AnnotationsInfo;
import org.jboss.wsf.spi.deployment.ArchiveDeployment;
import org.jboss.wsf.spi.deployment.Deployment;
import org.jboss.wsf.spi.metadata.config.SOAPAddressRewriteMetadata;
import org.jboss.wsf.spi.metadata.webservices.JBossWebservicesMetaData;
import org.jboss.wsf.spi.security.JASPIAuthenticationProvider;
import org.jboss.wsf.stack.cxf.JBossWSInvoker;
import org.jboss.wsf.stack.cxf.Loggers;
import org.jboss.wsf.stack.cxf.Messages;
import org.jboss.wsf.stack.cxf.addressRewrite.SoapAddressRewriteHelper;
import org.jboss.wsf.stack.cxf.client.Constants;
import org.jboss.wsf.stack.cxf.client.configuration.FeatureUtils;
import org.jboss.wsf.stack.cxf.client.configuration.InterceptorUtils;
import org.jboss.wsf.stack.cxf.client.configuration.JBossWSBusFactory;
import org.jboss.wsf.stack.cxf.client.configuration.JBossWSConfigurerImpl;
import org.jboss.wsf.stack.cxf.deployment.EndpointImpl;
import org.jboss.wsf.stack.cxf.deployment.WSDLFilePublisher;
import org.jboss.wsf.stack.cxf.extensions.policy.PolicySetsAnnotationListener;
import org.jboss.wsf.stack.cxf.interceptor.EndpointAssociationInterceptor;
import org.jboss.wsf.stack.cxf.interceptor.GracefulShutdownInterceptor;
import org.jboss.wsf.stack.cxf.interceptor.HandlerAuthInterceptor;
import org.jboss.wsf.stack.cxf.interceptor.NsCtxSelectorStoreInterceptor;
import org.jboss.wsf.stack.cxf.interceptor.WSDLSoapAddressRewriteInterceptor;
import org.jboss.wsf.stack.cxf.management.InstrumentationManagerExtImpl;
import org.jboss.wsf.stack.cxf.metadata.services.DDBeans;
import org.jboss.wsf.stack.cxf.metadata.services.DDEndpoint;
import org.jboss.wsf.stack.cxf.security.authentication.AuthenticationMgrSubjectCreatingInterceptor;

/**
 * A wrapper of the Bus for performing most of the configurations required on it by JBossWS
 * 
 * @author [email protected]
 * @since 25-Mar-2010
 *
 */
public class BusHolder
{
   private boolean configured = false;

   protected DDBeans metadata;
   protected List<EndpointImpl> endpoints = new LinkedList<EndpointImpl>();
   
   protected Bus bus;
   protected BusHolderLifeCycleListener busHolderListener;
   protected FactoryBeanListener policySetsListener;
   
   public BusHolder()
   {
      
   }
   
   public BusHolder(Bus bus)
   {
      setBus(bus);
   }

   public BusHolder(DDBeans metadata)
   {
      super();
      this.metadata = metadata;
      bus = new JBossWSBusFactory().createBus();
      //Force servlet transport to prevent CXF from using Jetty / http server or other transports
      bus.setExtension(new ServletDestinationFactory(), HttpDestinationFactory.class);
   }

   /**
    * Update the Bus held by the this instance using the provided parameters.
    * This basically prepares the bus for being used with JBossWS.
    * 
    * @param resolver               The ResourceResolver to configure, if any
    * @param configurer             The JBossWSCXFConfigurer to install in the bus, if any
    * @param wsmd                   The current JBossWebservicesMetaData, if any
    * @param dep                    The current deployment
    */
   public void configure(ResourceResolver resolver, Configurer configurer, JBossWebservicesMetaData wsmd, Deployment dep)
   {
      if (configured)
      {
         throw Messages.MESSAGES.busAlreadyConfigured(bus);
      }
      
      bus.setProperty(org.jboss.wsf.stack.cxf.client.Constants.DEPLOYMENT_BUS, true);
      busHolderListener = new BusHolderLifeCycleListener();
      bus.getExtension(BusLifeCycleManager.class).registerLifeCycleListener(busHolderListener);
      setWSDLManagerStreamWrapper(bus);
      
      if (configurer != null)
      {
         bus.setExtension(configurer, Configurer.class);
      }
      Map<String, String> props = getProperties(wsmd);
      
      setInterceptors(bus, dep, props);
      dep.addAttachment(Bus.class, bus);

      try
      {
         final JASPIAuthenticationProvider jaspiProvider = SPIProvider.getInstance().getSPI(
               JASPIAuthenticationProvider.class,
               ClassLoaderProvider.getDefaultProvider().getServerIntegrationClassLoader());
         
         if (jaspiProvider != null && jaspiProvider.enableServerAuthentication(dep, wsmd))
         {
            bus.getInInterceptors().add(new AuthenticationMgrSubjectCreatingInterceptor());
         }
      }
      catch (WSFException e)
      {
         Loggers.DEPLOYMENT_LOGGER.cannotFindJaspiClasses();
      }
      
      setResourceResolver(bus, resolver);
      
      if (bus.getExtension(PolicyEngine.class) != null) 
      {
         bus.getExtension(PolicyEngine.class).setAlternativeSelector(getAlternativeSelector(props));
      }     
      setCXFManagement(bus, props); //*first* enabled cxf management if required, *then* add anything else which could be manageable (e.g. work queues)
      setAdditionalWorkQueues(bus, props); 
      setWSDiscovery(bus, props);
      
      AnnotationsInfo ai = dep.getAttachment(AnnotationsInfo.class);
      if (ai == null || ai.hasAnnotatedClasses(PolicySets.class.getName())) {
         policySetsListener = new PolicySetsAnnotationListener(dep.getClassLoader());
         bus.getExtension(FactoryBeanListenerManager.class).addListener(policySetsListener);
      }
      
      //default to USE_ORIGINAL_THREAD = true; this can be overridden by simply setting the property in the endpoint or in the message using an interceptor
      //this forces one way operation to use original thread, which is required for ejb webserivce endpoints to avoid authorization failures from ejb container
      //and is a performance improvement in general when running in-container, as CXF needs to cache the message to free the thread, which is expensive
      //(moreover the user can tune the web container thread pool instead of expecting cxf to fork new threads)
      bus.setProperty(OneWayProcessorInterceptor.USE_ORIGINAL_THREAD, true);
      
      //[JBWS-3135] enable decoupled faultTo. This is an optional feature in cxf and we need this to be default to make it same behavior with native stack
      bus.setProperty("org.apache.cxf.ws.addressing.decoupled_fault_support", true);
      
      FeatureUtils.addFeatures(bus, bus, props);

      for (DDEndpoint dde : metadata.getEndpoints())
      {
         EndpointImpl endpoint = new EndpointImpl(bus, newInstance(dde.getImplementor()));
         if (dde.getInvoker() != null)
            endpoint.setInvoker(newInvokerInstance(dde.getInvoker(), dep));
         endpoint.setAddress(dde.getAddress());
         endpoint.setEndpointName(dde.getPortName());
         endpoint.setServiceName(dde.getServiceName());
         endpoint.setWsdlLocation(dde.getWsdlLocation());
         setHandlers(endpoint, dde);
         if (dde.getProperties() != null)
         {
            Map<String, Object> p = new HashMap<String, Object>();
            p.putAll(dde.getProperties());
            endpoint.setProperties(p);
         }
         if (dde.isAddressingEnabled()) 
         {
            WSAddressingFeature addressingFeature = new WSAddressingFeature();
            addressingFeature.setAddressingRequired(dde.isAddressingRequired());
            addressingFeature.setResponses(dde.getAddressingResponses());
            endpoint.getFeatures().add(addressingFeature);
         }
         endpoint.setPublishedEndpointUrl(dde.getPublishedEndpointUrl());
         endpoint.setSOAPAddressRewriteMetadata(dep.getAttachment(SOAPAddressRewriteMetadata.class));
         endpoint.publish();
         endpoints.add(endpoint);
         if (dde.isMtomEnabled())
         {
            SOAPBinding binding = (SOAPBinding) endpoint.getBinding();
            binding.setMTOMEnabled(true);
         }
      }
      configured = true;
   }
   
   @SuppressWarnings("rawtypes")
   private static void setHandlers(EndpointImpl endpoint, DDEndpoint dde)
   {
      List<String> handlers = dde.getHandlers();
      if (handlers != null && !handlers.isEmpty())
      {
         List<Handler> handlerInstances = new LinkedList<Handler>();
         for (String handler : handlers)
         {
            handlerInstances.add((Handler) newInstance(handler));
         }
         endpoint.setHandlers(handlerInstances);
      }
   }
   
   public void close()
   {
      //Move this stuff to the bus (our own impl)?
      RMManager rmManager = bus.getExtension(RMManager.class);
      if (rmManager != null)
      {
         rmManager.shutdown();
      }
      
      for (EndpointImpl endpoint : endpoints)
      {
         if (endpoint.isPublished())
         {
            endpoint.stop();
         }
      }
      endpoints.clear();
      
      //call bus shutdown unless the listener tells us shutdown has already been asked
      if (busHolderListener == null || !busHolderListener.isPreShutdown())
      {
         bus.shutdown(true);
      }
      busHolderListener = null;
      bus.getExtension(FactoryBeanListenerManager.class).removeListener(policySetsListener);
      policySetsListener = null;
   }

   private static Invoker newInvokerInstance(String className, Deployment dep)
   {
      final ClassLoader tccl = SecurityActions.getContextClassLoader();
      try
      {
         SecurityActions.setContextClassLoader(null);
         @SuppressWarnings("unchecked")
         Class<Invoker> clazz = (Class<Invoker>)tccl.loadClass(className);
         final AnnotationsInfo ai = dep.getAttachment(AnnotationsInfo.class);
         if (ai != null && clazz.isAssignableFrom(JBossWSInvoker.class)) {
            Constructor<Invoker> constr = clazz.getConstructor(boolean.class);
            return constr.newInstance(ai.hasAnnotatedClasses(UseAsyncMethod.class.getName()));
         } else {
            return clazz.newInstance();
         }
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
      finally
      {
         SecurityActions.setContextClassLoader(tccl);
      }
   }
   
   private static Object newInstance(String className)
   {
      final ClassLoader tccl = SecurityActions.getContextClassLoader();
      try
      {
         SecurityActions.setContextClassLoader(null);
         Class<?> clazz = tccl.loadClass(className);
         return clazz.newInstance();
      }
      catch (Exception e)
      {
         throw new RuntimeException(e);
      }
      finally
      {
         SecurityActions.setContextClassLoader(tccl);
      }
   }

   /**
    * A convenient method for getting a jbossws cxf server configurer
    * 
    * @param customization    The binding customization to set in the configurer, if any
    * @param wsdlPublisher    The wsdl file publisher to set in the configurer, if any
    * @param dep     The deployment
    * @return                 The new jbossws cxf configurer
    */
   public Configurer createServerConfigurer(BindingCustomization customization, WSDLFilePublisher wsdlPublisher, ArchiveDeployment dep)
   {
      ServerBeanCustomizer customizer = new ServerBeanCustomizer();
      customizer.setBindingCustomization(customization);
      customizer.setWsdlPublisher(wsdlPublisher);
      customizer.setDeployment(dep);
      return new JBossWSConfigurerImpl(customizer);
   }
   
   private static Map<String, String> getProperties(JBossWebservicesMetaData wsmd) {
      Map<String, String> props;
      if (wsmd != null) {
         props = wsmd.getProperties();
      } else {
         props = Collections.emptyMap();
      }
      return props;
   }
   
   protected void setInterceptors(Bus bus, Deployment dep, Map<String, String> props)
   {
      //Install the EndpointAssociationInterceptor for linking every message exchange
      //with the proper spi Endpoint retrieved in CXFServletExt
      bus.getInInterceptors().add(new EndpointAssociationInterceptor());
      bus.getInInterceptors().add(new NsCtxSelectorStoreInterceptor());
      bus.getInInterceptors().add(new GracefulShutdownInterceptor());
      
      final String p = (props != null) ? props.get(Constants.JBWS_CXF_DISABLE_HANDLER_AUTH_CHECKS) : null;
      if ((p == null || (!"true".equalsIgnoreCase(p) && !"1".equalsIgnoreCase(p))) && !Boolean.getBoolean(Constants.JBWS_CXF_DISABLE_HANDLER_AUTH_CHECKS)) {
         bus.getInInterceptors().add(new HandlerAuthInterceptor());
      }
      
      final SOAPAddressRewriteMetadata sarm = dep.getAttachment(SOAPAddressRewriteMetadata.class);
      if (SoapAddressRewriteHelper.isPathRewriteRequired(sarm) || SoapAddressRewriteHelper.isSchemeRewriteRequired(sarm)) {
         bus.getInInterceptors().add(new WSDLSoapAddressRewriteInterceptor(sarm));
      }
      
      InterceptorUtils.addInterceptors(bus, props);
   }
   
   protected static void setResourceResolver(Bus bus, ResourceResolver resourceResolver)
   {
      if (resourceResolver != null)
      {
         bus.getExtension(ResourceManager.class).addResourceResolver(resourceResolver);
      }
   }
   
   /**
    * Adds work queues parsing simple values of properties in jboss-webservices.xml:
    *   cxf.queue.<queue-name>.<parameter> = value
    * e.g.
    *   cxf.queue.default.maxQueueSize = 500
    * 
    * See constants in {@link org.jboss.wsf.stack.cxf.client.Constants}.
    * 
    * @param bus
    * @param wsmd
    */
   protected static void setAdditionalWorkQueues(Bus bus, Map<String, String> props)
   {
      if (props != null && !props.isEmpty()) {
         Map<String, Map<String, String>> queuesMap = new HashMap<String, Map<String,String>>();
         for (Entry<String, String> e : props.entrySet()) {
            String k = e.getKey();
            if (k.startsWith(Constants.CXF_QUEUE_PREFIX)) {
               String sk = k.substring(Constants.CXF_QUEUE_PREFIX.length());
               int i = sk.indexOf(".");
               if (i > 0) {
                  String queueName = sk.substring(0, i);
                  String queueProp = sk.substring(i+1);
                  Map<String, String> m = queuesMap.get(queueName);
                  if (m == null) {
                     m = new HashMap<String, String>();
                     queuesMap.put(queueName, m);
                  }
                  m.put(queueProp, e.getValue());
               }
            }
         }
         WorkQueueManager mgr = bus.getExtension(WorkQueueManager.class);
         for (Entry<String, Map<String, String>> e : queuesMap.entrySet()) {
            final String queueName = e.getKey();
            AutomaticWorkQueue q = createWorkQueue(queueName, e.getValue());
            mgr.addNamedWorkQueue(queueName, q);
         }
      }
   }
   
   protected static void setCXFManagement(Bus bus, Map<String, String> props) {
      if (props != null && !props.isEmpty()) {
         final String p = props.get(Constants.CXF_MANAGEMENT_ENABLED);
         if ("true".equalsIgnoreCase(p) || "1".equalsIgnoreCase(p)) {
            InstrumentationManagerExtImpl instrumentationManagerImpl = new InstrumentationManagerExtImpl();
            instrumentationManagerImpl.setBus(bus);
            instrumentationManagerImpl.setEnabled(true);
            instrumentationManagerImpl.init();
            instrumentationManagerImpl.initMBeanServer();            
            bus.setExtension(instrumentationManagerImpl, InstrumentationManager.class);
            CounterRepository couterRepository = new CounterRepository();
            couterRepository.setBus(bus);
            final String installRespTimeInterceptors = props.get(Constants.CXF_MANAGEMENT_INSTALL_RESPONSE_TIME_INTERCEPTORS);
            if (installRespTimeInterceptors == null ||
                  "true".equalsIgnoreCase(installRespTimeInterceptors) ||
                  "1".equalsIgnoreCase(installRespTimeInterceptors)) {
               ResponseTimeMessageInInterceptor in = new ResponseTimeMessageInInterceptor();
               ResponseTimeMessageInvokerInterceptor invoker = new ResponseTimeMessageInvokerInterceptor();
               ResponseTimeMessageOutInterceptor out = new ResponseTimeMessageOutInterceptor();
               bus.getInInterceptors().add(in);
               bus.getInInterceptors().add(invoker);
               bus.getOutInterceptors().add(out);
            }
            bus.setExtension(couterRepository, CounterRepository.class);
         }
      }
   }
   
   protected static void setWSDiscovery(Bus bus, Map<String, String> props) {
      if (props != null && !props.isEmpty()) {
         final String p = props.get(Constants.CXF_WS_DISCOVERY_ENABLED);
         if ("true".equalsIgnoreCase(p) || "1".equalsIgnoreCase(p)) {
            bus.getExtension(ServerLifeCycleManager.class).registerListener(new WSDiscoveryServerListener(bus));
         }
      }
   }
   
   private static void setWSDLManagerStreamWrapper(Bus bus)
   {
      ((WSDLManagerImpl) bus.getExtension(WSDLManager.class)).setXMLStreamReaderWrapper(new XMLStreamReaderWrapper()
      {
         @Override
         public XMLStreamReader wrap(XMLStreamReader reader)
         {
            return new SysPropExpandingStreamReader(reader);
         }
      });
   }
   
   private static AlternativeSelector getAlternativeSelector(Map<String, String> props) {
      //default to MaximalAlternativeSelector on server side [JBWS-3149]
      AlternativeSelector selector = new MaximalAlternativeSelector();
      if (props != null && !props.isEmpty()) {
         String className = props.get(Constants.CXF_POLICY_ALTERNATIVE_SELECTOR_PROP);
         if (className != null) {
            try {
               Class<?> clazz = Class.forName(className);
               selector = (AlternativeSelector)clazz.newInstance();
            } catch (Exception e) {
               
            }
         }
      }
      return selector;
   }
   
   
   
   private static AutomaticWorkQueue createWorkQueue(String name, Map<String, String> props) {
      int mqs = parseInt(props.get(Constants.CXF_QUEUE_MAX_QUEUE_SIZE_PROP), 256);
      int initialThreads = parseInt(props.get(Constants.CXF_QUEUE_INITIAL_THREADS_PROP), 0);
      int highWaterMark = parseInt(props.get(Constants.CXF_QUEUE_HIGH_WATER_MARK_PROP), 25);
      int lowWaterMark = parseInt(props.get(Constants.CXF_QUEUE_LOW_WATER_MARK_PROP), 5);
      long dequeueTimeout = parseLong(props.get(Constants.CXF_QUEUE_DEQUEUE_TIMEOUT_PROP), 2 * 60 * 1000L);
      return new AutomaticWorkQueueImpl(mqs, initialThreads, highWaterMark, lowWaterMark, dequeueTimeout, name);
   }

   private static int parseInt(String prop, int defaultValue) {
      return prop != null ? Integer.parseInt(prop) : defaultValue;
   }
   
   private static long parseLong(String prop, long defaultValue) {
      return prop != null ? Long.parseLong(prop) : defaultValue;
   }
   
   /**
    * Return the hold bus
    * 
    * @return
    */
   public Bus getBus()
   {
      return bus;
   }
   
   protected void setBus(Bus bus)
   {
      this.bus = bus;
   }
   
   private static class BusHolderLifeCycleListener implements BusLifeCycleListener
   {
      private volatile boolean preShutdown = false;

      public boolean isPreShutdown()
      {
         return preShutdown;
      }

      @Override
      public void initComplete()
      {
         //NOOP
      }

      @Override
      public void preShutdown()
      {
         preShutdown = true;
      }

      @Override
      public void postShutdown()
      {
         //NOOP
      }
   }
}