/*
 * JBoss, the OpenSource J2EE webOS
 *
 * Distributable under LGPL license.
 * See terms of license at gnu.org.
 */

// $Id: ServiceDeployer.java,v 1.18.2.13 2005/04/06 03:07:58 nihility Exp $
package org.jboss.webservice;

import org.dom4j.Document;
import org.dom4j.Element;
import org.jboss.deployment.DeploymentException;
import org.jboss.deployment.DeploymentInfo;
import org.jboss.deployment.MainDeployerMBean;
import org.jboss.deployment.SubDeployer;
import org.jboss.logging.Logger;
import org.jboss.metadata.ApplicationMetaData;
import org.jboss.metadata.BeanMetaData;
import org.jboss.metadata.EjbPortComponentMetaData;
import org.jboss.metadata.WebMetaData;
import org.jboss.mx.util.MBeanProxy;
import org.jboss.mx.util.MBeanProxyCreationException;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.webservice.metadata.PortComponentMetaData;
import org.jboss.webservice.metadata.WebserviceDescriptionMetaData;
import org.jboss.webservice.metadata.WebservicesFactory;
import org.jboss.webservice.metadata.WebservicesMetaData;
import org.jboss.xml.binding.ObjectModelFactory;
import org.jboss.xml.binding.Unmarshaller;

import javax.management.InstanceNotFoundException;
import javax.management.Notification;
import javax.management.NotificationFilterSupport;
import javax.management.NotificationListener;
import javax.management.ObjectName;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A deployer service that manages WS4EE compliant Web-Services within JMX
 * by translating/delegating to an axis deployer.
 * <p/>
 * This service receives deployment notifications from the EJBDeployer and
 * AbstractWebContainer and deploys the webservices using the {@link AxisService}
 *
 * @author Thomas.Diesler@jboss.org
 * @author Scott.Stark@jboss.org
 * @version $Revision: 1.18.2.13 $
 * @jmx.mbean description="Abstract Webservice deployer"
 * extends="org.jboss.system.ServiceMBean"
 * @since 15-April-2004
 */
public abstract class ServiceDeployer extends ServiceMBeanSupport
        implements ServiceDeployerMBean, NotificationListener
{
   // provide logging
   private final Logger log = Logger.getLogger(ServiceDeployer.class);

   // The servlet init param in web.xml that is the service ID
   public static final String INIT_PARAM_WEBSERVICE_ID = "WebServiceID";
   // The servlet init param in web.xml that is the service endpoint class
   public static final String INIT_PARAM_SERVICE_ENDPOINT_IMPL = "ServiceEndpointImpl";
   // The key in the deployment info context for the webapp context root
   public static final String WEBSERVICE_CONTEXT_ROOT = "webservice-context-root";

   // Proxy to AxisService
   private AxisServiceMBean axisService;

   /**
    * Maps the deployment url the the WebservicesMetaData
    */
   protected Map webservicesMap = new HashMap();

   /** Get a proxy to AxisService
    */
   protected void startService() throws Exception
   {
      super.startService();
      axisService = (AxisServiceMBean)MBeanProxy.get(AxisServiceMBean.class, AxisServiceMBean.OBJECT_NAME, server);
   }

   /**
    * Callback method from the broadcaster MBean this listener implementation
    * is registered to.
    *
    * @param notification the notification object
    * @param handback     the handback object given to the broadcaster
    *                     upon listener registration
    */
   public void handleNotification(Notification notification, Object handback)
   {
      DeploymentInfo di = (DeploymentInfo)notification.getUserData();

      String moduleName = di.shortName;
      String type = notification.getType();
      log.debug("handleNotification: " + type + "," + moduleName);

      if (isWebservicesDeployment(di))
      {
         try
         {
            if (type.equals(SubDeployer.INIT_NOTIFICATION))
               initWebservice(di);
            else if (type.equals(SubDeployer.CREATE_NOTIFICATION))
               createWebservice(di);
            else if (type.equals(SubDeployer.START_NOTIFICATION))
               startWebservice(di);
         }
         catch (Throwable e)
         {
            handleStartupException(di, e);
         }

         try
         {
            if (type.equals(SubDeployer.STOP_NOTIFICATION))
               stopWebservice(di);
            else if (type.equals(SubDeployer.DESTROY_NOTIFICATION))
               destroyWebservice(di);
         }
         catch (Throwable e)
         {
            handleShutdownException(moduleName, e);
         }
      }
   }

   /**
    * Overwrite to initialize the webservice
    * Is called when the parent deployer sends the INIT_NOTIFICATION.
    * <p/>
    * This implementation does nothing
    */
   protected void initWebservice(DeploymentInfo di) throws DeploymentException
   {
      if (di.metaData instanceof ApplicationMetaData)
      {
         ApplicationMetaData applMetaData = (ApplicationMetaData)di.metaData;
         applMetaData.setWebServiceDeployment(true);
      }

      if (di.metaData instanceof WebMetaData)
      {
         WebMetaData webMetaData = (WebMetaData)di.metaData;
         webMetaData.setWebServiceDeployment(true);
      }
   }

   /**
    * Overwrite to create the webservice
    * Is called when the parent deployer sends the CREATE_NOTIFICATION.
    * <p/>
    * This implementation parses webservices.xml and puts it in the local registry.
    */
   protected void createWebservice(DeploymentInfo di) throws DeploymentException
   {
      URL url = getWebservicesDescriptor(di);
      if (url != null)
      {
         WebservicesMetaData wsMetaData = parseWebservicesXML(di, url);
         webservicesMap.put(di.url, wsMetaData);
      }
   }

   /** Get the resource name of the webservices.xml descriptor. */
   protected abstract URL getWebservicesDescriptor(DeploymentInfo di);

   /** Return true if this is a web service deployment */
   private boolean isWebservicesDeployment(DeploymentInfo di)
   {
      if (getWebservicesDescriptor(di) != null)
         return true;

      if (di.metaData instanceof ApplicationMetaData)
      {
         ApplicationMetaData applMetaData = (ApplicationMetaData)di.metaData;
         return applMetaData.isWebServiceDeployment();
      }

      if (di.metaData instanceof WebMetaData)
      {
         WebMetaData webMetaData = (WebMetaData)di.metaData;
         return webMetaData.isWebServiceDeployment();
      }

      return false;
   }

   /**
    * Overwrite to start the webservice
    * Is called when the parent deployer sends the START_NOTIFICATION.
    * <p/>
    * This implementation deployes the webservices to Axis.
    */
   protected void startWebservice(DeploymentInfo di) throws DeploymentException
   {
      WebservicesMetaData webservices = (WebservicesMetaData)webservicesMap.get(di.url);
      if (webservices != null)
      {
         // update the service address
         ServiceLocationResolver locationResolver = new ServiceLocationResolver(di);
         WebserviceDescriptionMetaData[] wsdArray = webservices.getWebserviceDescriptions();
         for (int i = 0; i < wsdArray.length; i++)
         {
            WebserviceDescriptionMetaData wsdMetaData = wsdArray[i];
            wsdMetaData.updateServiceAddress(locationResolver);
            String wsdName = wsdMetaData.getWebserviceDescriptionName();

            // copy the wsdl publish location from jboss.xml
            if (di.metaData instanceof ApplicationMetaData)
            {
               ApplicationMetaData applMetaData = (ApplicationMetaData)di.metaData;
               String wsdlPublishLocation = applMetaData.getWsdlPublishLocationByName(wsdName);
               wsdMetaData.setWsdlPublishLocation(wsdlPublishLocation);
            }

            // copy the wsdl publish location from jboss-web.xml
            if (di.metaData instanceof WebMetaData)
            {
               WebMetaData webMetaData = (WebMetaData)di.metaData;
               String wsdlPublishLocation = webMetaData.getWsdlPublishLocationByName(wsdName);
               wsdMetaData.setWsdlPublishLocation(wsdlPublishLocation);
            }
         }

         WSDLFilePublisher wsdlfp = new WSDLFilePublisher(di);
         wsdlfp.publishWsdlFile(webservices);
         deployWebservices(di, webservices);
      }
   }

   /**
    * Overwrite to stop the webservice
    * Is called when the parent deployer sends the STOP_NOTIFICATION.
    * <p/>
    * This implementation undeployes the webservices to Axis and
    * removes the webservices.xml from the local registry.
    */
   protected void stopWebservice(DeploymentInfo di)
   {
      WebservicesMetaData webservices = (WebservicesMetaData)webservicesMap.get(di.url);
      if (webservices != null)
      {
         undeployWebservices(di, webservices);
         WSDLFilePublisher wsdlfp = new WSDLFilePublisher(di);
         wsdlfp.unpublishWsdlFile();
      }
   }

   /**
    * Overwrite to destroy the webservice
    * This method is called when the parent deployer sends the DESTROY_NOTIFICATION.
    */
   protected void destroyWebservice(DeploymentInfo di)
   {
      webservicesMap.remove(di.url);
   }

   // PROTECTED  *******************************************************************************************************

   /**
    * Handle all webservice deployment exceptions.
    * <p/>
    * You can either simply logs the problem and keep the EJB/WAR module
    * alive or undeploy properly.
    */
   protected void handleStartupException(DeploymentInfo di, Throwable th)
   {
      log.error("Cannot startup webservice for: " + di.shortName, th);
      try
      {
         MainDeployerMBean mainDeployer = (MainDeployerMBean)MBeanProxy.get(MainDeployerMBean.class, MainDeployerMBean.OBJECT_NAME, server);
         mainDeployer.undeploy(di);
      }
      catch (MBeanProxyCreationException e)
      {
         e.printStackTrace();
      }
   }

   /**
    * Handle all webservice deployment exceptions.
    * <p/>
    * You can either simply logs the problem and keep the EJB/WAR module
    * alive or undeploy properly.
    */
   protected void handleShutdownException(String moduleName, Throwable th)
   {
      log.error("Cannot shutdown webservice for: " + moduleName, th);
   }

   /**
    * Register the notification listener
    */
   protected void registerNotificationListener(ObjectName serviceName)
           throws InstanceNotFoundException
   {
      NotificationFilterSupport filter = new NotificationFilterSupport();
      filter.enableType(SubDeployer.INIT_NOTIFICATION);
      filter.enableType(SubDeployer.CREATE_NOTIFICATION);
      filter.enableType(SubDeployer.START_NOTIFICATION);
      filter.enableType(SubDeployer.STOP_NOTIFICATION);
      filter.enableType(SubDeployer.DESTROY_NOTIFICATION);
      server.addNotificationListener(serviceName, this, filter, null);
   }

   /**
    * Unregister the notification listener
    */
   protected void unregisterNotificationListener(ObjectName serviceName)
   {
      try
      {
         server.removeNotificationListener(serviceName, this);
      }
      catch (Exception e)
      {
         log.error("Cannot remove notification listener: " + e.toString());
      }
   }

   /**
    * Unmarshal the webservices.xml
    */
   protected WebservicesMetaData parseWebservicesXML(DeploymentInfo di, URL webservicesURL) throws DeploymentException
   {
      WebservicesMetaData webservices = null;
      try
      {
         // let the object model factory to create an instance and populate it with data from XML
         InputStream is = webservicesURL.openStream();
         try
         {
            // setup the XML binding Unmarshaller
            Unmarshaller unmarshaller = new Unmarshaller();
            ObjectModelFactory factory = new WebservicesFactory(di.localCl);
            webservices = (WebservicesMetaData)unmarshaller.unmarshal(is, factory, null);
         }
         finally
         {
            is.close();
         }
      }
      catch (Exception e)
      {
         throw new DeploymentException("Cannot obtain webservices meta data", e);
      }
      return webservices;
   }

   /**
    * Deploy the webservices using the AxisService MBean
    */
   protected void deployWebservices(DeploymentInfo di, WebservicesMetaData webservices)
           throws DeploymentException
   {
      try
      {
         WebserviceDescriptionMetaData[] wsdArr = webservices.getWebserviceDescriptions();
         for (int i = 0; i < wsdArr.length; i++)
         {
            WebserviceDescriptionMetaData wsd = wsdArr[i];
            PortComponentMetaData[] pcArr = wsd.getPortComponents();
            for (int j = 0; j < pcArr.length; j++)
            {
               PortComponentMetaData pcMetaData = pcArr[j];
               PortComponentInfo pcInfo = new PortComponentInfo(di, pcMetaData);
               axisService.deployService(pcInfo);
            }
         }
      }
      catch (Exception e)
      {
         throw new DeploymentException("Cannot deploy webservice", e);
      }
   }

   /**
    * Undeploy the webservices using the AxisService MBean
    */
   protected void undeployWebservices(DeploymentInfo di, WebservicesMetaData webservices)
   {
      try
      {
         WebserviceDescriptionMetaData[] wsdarr = webservices.getWebserviceDescriptions();
         for (int i = 0; i < wsdarr.length; i++)
         {
            WebserviceDescriptionMetaData wsDescription = wsdarr[i];
            PortComponentMetaData[] pcarr = wsDescription.getPortComponents();
            for (int j = 0; j < pcarr.length; j++)
            {
               PortComponentMetaData pcMetaData = pcarr[j];
               PortComponentInfo pcInfo = new PortComponentInfo(di, pcMetaData);
               String wsID = pcInfo.getServiceID();
               axisService.undeployService(wsID);
            }
         }
      }
      catch (Exception ignore)
      {
         log.warn("Cannot undeploy webservice: " + ignore);
      }
   }

   /** Modify the servlet-class element
    */
   protected boolean modifyServletConfig(Document doc, String servletName, PortComponentInfo pcInfo) throws DeploymentException
   {
      Element servletElement = null;

      Iterator itServlet = doc.getRootElement().elements("servlet").iterator();
      while (itServlet.hasNext() && servletElement == null)
      {
         Element el = (Element)itServlet.next();
         String elName = el.elementTextTrim("servlet-name");
         if (servletName.equals(elName))
            servletElement = el;
      }
      if (servletElement == null)
         throw new DeploymentException("Cannot find <servlet> with servlet-name: " + servletName);

      // find the servlet-class
      Element classElement = servletElement.element("servlet-class");
      if (classElement == null)
         throw new DeploymentException("Cannot find <servlet-class> for servlet-name: " + servletName);

      // replace the class name
      String servletClass = classElement.getTextTrim();
      String serviceEndpointServletName = getServiceEndpointServletName();

      // Nothing to do if we have an <init-param> with the WebServiceID
      if (isAlreadyModified(servletElement) == false)
      {
         classElement.setText(serviceEndpointServletName);

         // build a list of detached elements that come after <servlet-class>
         boolean startDetach = false;
         ArrayList detachedElements = new ArrayList();
         itServlet = servletElement.elements().iterator();
         while (itServlet.hasNext())
         {
            Element el = (Element)itServlet.next();
            if (startDetach == true)
            {
               detachedElements.add(el);
               el.detach();
            }
            if (el.equals(classElement))
               startDetach = true;
         }

         // add additional init params
         Element paramElement = servletElement.addElement("init-param");
         paramElement.addElement("param-name").addText(INIT_PARAM_WEBSERVICE_ID);
         paramElement.addElement("param-value").addText(pcInfo.getServiceID());

         // In case of the generated web.xml for EJB endpoints
         // we don't have an endpoint implemenation pojo in <servlet-class>
         if (servletClass.equals(serviceEndpointServletName) == false)
         {
            paramElement = servletElement.addElement("init-param");
            paramElement.addElement("param-name").addText(INIT_PARAM_SERVICE_ENDPOINT_IMPL);
            paramElement.addElement("param-value").addText(servletClass);
         }

         PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();
         pcMetaData.setServiceEndpointBean(servletClass);

         // reattach the elements
         itServlet = detachedElements.iterator();
         while (itServlet.hasNext())
         {
            Element el = (Element)itServlet.next();
            servletElement.add(el);
         }

         return true;
      }
      else
      {
         Iterator it = servletElement.elementIterator("init-param");
         while (it.hasNext())
         {
            Element elParam = (Element)it.next();
            if (INIT_PARAM_SERVICE_ENDPOINT_IMPL.equals(elParam.elementText("param-name")))
            {
               String serviceEndpointImpl = elParam.elementText("param-value");
               PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();
               pcMetaData.setServiceEndpointBean(serviceEndpointImpl);
            }
         }

         return false;
      }
   }

   // Return true if the web.xml is already modified
   private boolean isAlreadyModified(Element servletElement)
   {
      String serviceID = null;

      Iterator it = servletElement.elementIterator("init-param");
      while (serviceID == null && it.hasNext())
      {
         Element elParam = (Element)it.next();
         if (INIT_PARAM_WEBSERVICE_ID.equals(elParam.elementText("param-name")))
            serviceID = elParam.elementText("param-value");
      }

      return serviceID != null;
   }

   /**
    * Override to return the name of the service endpoint servlet
    */
   protected abstract String getServiceEndpointServletName();

   /**
    * This guy resolves the service location, when ask to do so
    */
   public class ServiceLocationResolver
   {
      private DeploymentInfo di;

      public ServiceLocationResolver(DeploymentInfo di)
      {
         this.di = di;
      }

      public boolean alwaysResolve()
      {
         return axisService.isAlwaysModifySOAPAddress();
      }

      /**
       * Get the web service address for either an JSE or an EJB endpoint
       * <p/>
       * EJB: [schema][host]:[port]/[port-component-uri|deployment name/port-component-name]
       * JSE: [schema][host]:[port]/[path derived from servlet mapping in web.xml]
       *
       * @param schema The url schema, can be 'http://' or 'https://', or null
       * @param pcmd   The port component meta data
       * @return
       */
      public String getServiceLocation(String schema, PortComponentMetaData pcmd)
      {
         String ejbLink = pcmd.getEjbLink();
         String servletLink = pcmd.getServletLink();
         String serviceName = pcmd.getPortComponentName();

         String servicePath = null;

         // For a web based service endpoint it is derived from the servlet mapping
         if (servletLink != null)
         {
            WebMetaData metaData = (WebMetaData)di.metaData;
            Map servletMappings = metaData.getServletMappings();
            String urlPattern = (String)servletMappings.get(servletLink);
            if (urlPattern == null)
               throw new IllegalStateException("Cannot obtain servlet-mapping for: " + servletLink);

            if (urlPattern.startsWith("/") == false)
               urlPattern = "/" + urlPattern;

            servicePath = metaData.getContextRoot() + urlPattern;
         }
         else if (ejbLink != null)
         {
            ApplicationMetaData amd = (ApplicationMetaData)di.metaData;
            BeanMetaData bmd = (BeanMetaData)amd.getBeanByEjbName(ejbLink);
            if (bmd == null)
               throw new IllegalStateException("Cannot find ejb-name: " + ejbLink);

            // Use the webservice context root if we have one
            String contextRoot = amd.getWebServiceContextRoot();

            // If not, derive the context root from the deployment short name
            if (contextRoot == null)
            {
               String shortName = di.shortName;
               contextRoot = shortName.substring(0, shortName.indexOf('.'));

               if (di.parent != null)
               {
                  shortName = di.parent.shortName;
                  contextRoot = shortName.substring(0, shortName.indexOf('.')) + "/" + contextRoot;
               }

               contextRoot = "/" + contextRoot;
            }
            di.context.put(WEBSERVICE_CONTEXT_ROOT, contextRoot);

            EjbPortComponentMetaData ejbpcMetaData = bmd.getPortComponent();
            if (ejbpcMetaData != null && ejbpcMetaData.getPortComponentURI() != null)
            {
               servicePath = ejbpcMetaData.getPortComponentURI();
            }
            else
            {
               servicePath = contextRoot + "/" + serviceName;
            }
         }
         else
         {
            throw new IllegalStateException("Cannot find valid <servlet-link> nor <ejb-link> in port component meta data");
         }

         if (servicePath.endsWith("/*"))
            servicePath = servicePath.substring(0, servicePath.indexOf("/*"));

         if (schema == null)
            schema = "http://";

         int port = 0;
         String host = null;
         String serviceEndpointAddress = null;
         try
         {
            host = axisService.getWebServiceHost();
            port = axisService.getWebServicePort();
            if ("https://".equals(schema))
               port = axisService.getWebServiceSecurePort();

            serviceEndpointAddress = new URL(schema + host + ":" + port + servicePath).toExternalForm();
         }
         catch (Exception e)
         {
            log.error("Cannot obtain attribute from AxisService, cause: " + e.toString());
         }

         return serviceEndpointAddress;
      }
   }
}