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

// $Id: AxisService.java,v 1.35.4.14 2005/04/13 11:58:14 tdiesler Exp $
package org.jboss.webservice;

// $Id: AxisService.java,v 1.35.4.14 2005/04/13 11:58:14 tdiesler Exp $

import org.jboss.axis.MessageContext;
import org.jboss.axis.server.AxisServer;
import org.jboss.axis.utils.Admin;
import org.jboss.deployment.DeploymentInfo;
import org.jboss.logging.Logger;
import org.jboss.metadata.WebMetaData;
import org.jboss.system.ServiceMBeanSupport;
import org.jboss.system.server.ServerConfig;
import org.jboss.webservice.deployment.MetaDataRegistry;
import org.jboss.webservice.deployment.ServiceDescription;
import org.jboss.webservice.deployment.TypeMappingDescription;
import org.jboss.webservice.deployment.WSDDGenerator;
import org.jboss.webservice.metadata.PortComponentMetaData;
import org.jboss.webservice.metadata.WebserviceDescriptionMetaData;
import org.jboss.webservice.metadata.jaxrpcmapping.JavaWsdlMapping;
import org.jboss.webservice.server.InvokerProviderJMX;
import org.jboss.webservice.server.JMXInvokerEndpoint;
import org.jboss.webservice.server.ServerEngine;
import org.w3c.dom.Document;

import javax.management.ObjectName;
import javax.wsdl.Definition;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

/**
 * A service that keeps track of the Axis server and client engine
 * and can deploy/undeploy services to/from Axis using the Axis wsdd format.
 * <p/>
 * Internally it keeps a registry of deployed {@link PortComponentInfo} objects.
 * The key into that registry is [deploymentName]#[port-component-name]
 *
 * @author Thomas.Diesler@jboss.org
 * @jmx.mbean name="jboss.ws4ee:service=AxisService"
 * description="Axis Service"
 * extends="org.jboss.system.ServiceMBean"
 * @since 15-April-2004
 */
public class AxisService extends ServiceMBeanSupport
        implements AxisServiceMBean
{
   // provide logging
   private static final Logger log = Logger.getLogger(AxisService.class);

   // maps serviceID to PortComponentInfo
   private Map wsRegistry = new HashMap();

   // The axis server engine that serves all deployed web services
   private ServerEngine axisServer;

   // A registry for meta data needed by the MetaDataBean[Serializer|Deserializer]Factory
   private MetaDataRegistry metaDataRegistry = new MetaDataRegistry();

   // The webservice host name that will be used when updating the wsdl
   private String webServiceHost;
   // The webservice port that will be used when updating the wsdl
   private int webServicePort;
   // The webservice port that will be used when updating the wsdl
   private int webServiceSecurePort;
   // Whether we should always modify the soap address to the deployed endpoing location
   private boolean alwaysModifySOAPAddress;
   // The default invoker provider for EJB endpoints
   private String invokerProviderEJB;
   // The default invoker provider for JSE endpoints
   private String invokerProviderJSE;

   /**
    * Sub-classes should override this method to provide
    * custum 'start' logic.
    * <p/>
    * <p>This method is empty, and is provided for convenience
    * when concrete service classes do not need to perform
    * anything specific for this state change.
    */
   protected void startService() throws Exception
   {
      super.startService();
   }

   /**
    * Sub-classes should override this method to provide
    * custum 'stop' logic.
    * <p/>
    * <p>This method is empty, and is provided for convenience
    * when concrete service classes do not need to perform
    * anything specific for this state change.
    */
   protected void stopService() throws Exception
   {
      super.stopService();
   }

   /**
    * @jmx.managed-attribute
    */
   public String getWebServiceHost()
   {
      return webServiceHost;
   }

   /**
    * @jmx.managed-attribute
    */
   public int getWebServicePort()
   {
      return webServicePort;
   }

   /**
    * @jmx.managed-attribute
    */
   public int getWebServiceSecurePort()
   {
      return webServiceSecurePort;
   }

   /**
    * @jmx.managed-attribute
    */
   public boolean isAlwaysModifySOAPAddress()
   {
      return alwaysModifySOAPAddress;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setWebServiceHost(String host)
   {
      if ("0.0.0.0".equals(host))
      {
         try
         {
            InetAddress localHost = InetAddress.getLocalHost();
            host = localHost.getHostName();
         }
         catch (UnknownHostException e)
         {
            log.error("Cannot map host: " + host, e);
         }
      }

      this.webServiceHost = host;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setWebServicePort(int port)
   {
      this.webServicePort = port;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setWebServiceSecurePort(int port)
   {
      this.webServiceSecurePort = port;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setAlwaysModifySOAPAddress(boolean modify)
   {
      this.alwaysModifySOAPAddress = modify;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getInvokerProviderEJB()
   {
      return invokerProviderEJB;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setInvokerProviderEJB(String invokerProviderEJB)
   {
      this.invokerProviderEJB = invokerProviderEJB;
   }

   /**
    * @jmx.managed-attribute
    */
   public String getInvokerProviderJSE()
   {
      return invokerProviderJSE;
   }

   /**
    * @jmx.managed-attribute
    */
   public void setInvokerProviderJSE(String invokerProviderJSE)
   {
      this.invokerProviderJSE = invokerProviderJSE;
   }

   /**
    * Get axis server singleton
    *
    * @jmx.managed-attribute
    */
   public AxisServer getAxisServer()
   {
      if (axisServer == null)
         axisServer = new ServerEngine(EngineConfigurationFinder.getServerEngineConfiguration());

      return axisServer;
   }

   /**
    * Get MetaDataBeanSerializer/Deserializer metaData registry
    *
    * @jmx.managed-attribute
    */
   public MetaDataRegistry getMetaDataRegistry()
   {
      return metaDataRegistry;
   }

   /** Get port component info for a given web service id
    *
    * The keys into the registry are:
    *
    *    [deploment.ear]/[deployment.?ar]#PortComponentName
    *
    * A client deployment may use a 'port-component-link' like:
    *
    *    [deployment.?ar]#PortComponentName
    *
    * In case we don't find a direct match we try matching by
    *
    *    key.endsWith(wsID)
    *
    * See CTS test: /com/sun/ts/tests/webservices/deploy/portcomplink
    *
    * @param wsID The web service identifier
    * @return The port component info, or null
    * @jmx.managed-operation
    */
   public PortComponentInfo getPortComponentInfo(String wsID)
   {
      PortComponentInfo pcInfo = (PortComponentInfo)wsRegistry.get(wsID);

      if (pcInfo == null)
      {
         log.debug("No PortComponentInfo found for serviceID: " + wsID);
         PortComponentInfo singleMatch = null;
         Iterator it = wsRegistry.keySet().iterator();
         while (it.hasNext())
         {
            String key = (String)it.next();
            if (key.endsWith(wsID) && singleMatch != null)
            {
               log.warn("Too many possible serviceID matches: " + key);
               return null;
            }
            if (key.endsWith(wsID) && singleMatch == null)
            {
               log.debug("Found possible match: " + key);
               singleMatch = (PortComponentInfo)wsRegistry.get(key);
            }
         }
         pcInfo = singleMatch;
      }

      return pcInfo;
   }

   /** Returns a the array of registered PortComponentInfo objects
    *
    * @jmx.managed-operation
    */
   public PortComponentInfo[] listServiceEndpointInfos()
   {
      PortComponentInfo[] arr = new PortComponentInfo[wsRegistry.size()];
      wsRegistry.values().toArray(arr);
      return arr;
   }

   /** List the registered webservices
    *
    * @jmx.managed-operation
    */
   public String listServiceEndpoints()
   {
      StringWriter sw = new StringWriter();
      PrintWriter pw = new PrintWriter(sw);

      pw.println("<table>");
      pw.println("<tr><th>ID</th><th>Address</th></tr>");

      PortComponentInfo[] pcInfoArr = listServiceEndpointInfos();
      for (int i = 0; i < pcInfoArr.length; i++)
      {
         PortComponentInfo pcInfo = pcInfoArr[i];
         String wsID = pcInfo.getServiceID();
         PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();
         pw.println("<tr><td>" + wsID + "</td><td>" + pcMetaData.getServiceEndpointURL() + "</td></tr>");
      }
      pw.println("</table>");
      pw.close();

      return sw.toString();
   }

   /**
    * Deploy a webservice from a Axis WSDD URL
    *
    * @jmx.managed-operation
    */
   public void deployService(PortComponentInfo pcInfo) throws Exception
   {
      DeploymentInfo di = pcInfo.getDeploymentInfo();
      PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();

      ObjectName oname = pcInfo.getObjectName();
      if (server.isRegistered(oname))
         throw new IllegalStateException("Service already registerd, maybe the port-component-name is not unique: " + oname);

      String serviceID = pcInfo.getServiceID();
      log.debug("deployService: " + serviceID);

      ServiceDescription serviceDesc = getServiceDescription(pcInfo);
      pcInfo.setServiceDesc(serviceDesc);

      String wsdd = generateDeploymentWSDD(pcInfo);
      Document genDoc = getDocumentBuilder().parse(new ByteArrayInputStream(wsdd.getBytes()));

      String deploymentName = di.getCanonicalName();
      String dataDir = System.getProperty(ServerConfig.SERVER_DATA_DIR);
      File wsddFile = new File(dataDir + "/wsdl/" + deploymentName + "/" + pcMetaData.getPortComponentName() + ".wsdd");
      wsddFile.getParentFile().mkdirs();

      FileWriter out = new FileWriter(wsddFile);
      out.write(wsdd);
      out.close();

      log.info("WSDD published to: " + wsddFile.getCanonicalPath());

      // Set the Context ClassLoader to the deployment ClassLoader, so that Axis
      // can find the user defined types
      ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
      try
      {
         ClassLoader deploymentCL = (di.metaData instanceof WebMetaData ? ((WebMetaData)di.metaData).getContextLoader() : di.ucl);
         Thread.currentThread().setContextClassLoader(deploymentCL);

         MessageContext msgContext = new MessageContext(getAxisServer());
         new Admin().process(msgContext, genDoc.getDocumentElement());

         registerBeanMetaData(serviceDesc);
      }
      finally
      {
         Thread.currentThread().setContextClassLoader(ctxClassLoader);
      }

      // Register the port component with the MBeanServer
      PortComponent pc = new PortComponent(pcInfo);
      server.registerMBean(pc, oname);

      wsRegistry.put(serviceID, pcInfo);

      String endpointURL = pcInfo.getServiceEndpointURL();
      log.info("Web Service deployed: " + endpointURL);
   }

   /** Register the meta data for the MetaDataSerializer/Deserializer
    */
   private void registerBeanMetaData(ServiceDescription serviceDesc)
   {
      Iterator it = serviceDesc.getTypMappings();
      while (it.hasNext())
      {
         TypeMappingDescription typeMapping = (TypeMappingDescription)it.next();
         if (typeMapping.getMetaData() != null)
            metaDataRegistry.registerTypeMappingMetaData(typeMapping);
      }
   }

   /**
    * Find optional type mapping meta data.
    *
    * @see {@link org.jboss.webservice.encoding.ser.MetaDataTypeDesc}
    *      <p/>
    *      It looks for "ws4ee-deployment.xml" in the WEB-INF/META-INF dir
    */
   private URL findTypeMappingMetaData(DeploymentInfo di)
   {
      URL resourceURL = null;

      // check to see if wsdd file already exists for this deployment
      String dataDir = System.getProperty(ServerConfig.SERVER_DATA_DIR);
      File resourceFile = new File(dataDir + "/wsdl/ws4ee-deployment.xml");
      if (resourceFile.exists())
      {
         try
         {
            resourceURL = resourceFile.toURL();
         }
         catch (MalformedURLException e)
         {
            log.warn("Could not get url to ws4ee-deployment.xml.", e);
         }
      }

      if (resourceURL == null)
      {
         String infDir = (di.shortName.endsWith(".war") ? "WEB-INF" : "META-INF");
         String resName = infDir + "/ws4ee-deployment.xml";
         resourceURL = di.localCl.findResource(resName);
      }

      return resourceURL;
   }

   /**
    * Undeploy a webservice for a given web service id
    *
    * @jmx.managed-operation
    */
   public void undeployService(String serviceID) throws Exception
   {
      PortComponentInfo pcInfo = getPortComponentInfo(serviceID);
      if (pcInfo == null)
         throw new IllegalStateException("Cannot find port component info for: " + serviceID);

      StringBuffer buffer = new StringBuffer();
      buffer.append("<undeployment");
      buffer.append("   xmlns='http://xml.apache.org/axis/wsdd/'");
      buffer.append("   xmlns:java='http://xml.apache.org/axis/wsdd/providers/java'");
      buffer.append("   xmlns:xsi='http://www.w3.org/2000/10/XMLSchema-instance'>");
      buffer.append("   <service name='" + serviceID + "'/>");
      buffer.append("</undeployment>");

      try
      {
         DocumentBuilder builder = getDocumentBuilder();
         Document doc = builder.parse(new ByteArrayInputStream(buffer.toString().getBytes()));

         MessageContext msgContext = new MessageContext(getAxisServer());
         new Admin().process(msgContext, doc.getDocumentElement());
      }
      catch (Exception e)
      {
         log.error("Cannot unregister Axis service: " + serviceID);
      }

      // Unregister the port component from the MBeanServer
      try
      {
         server.unregisterMBean(pcInfo.getObjectName());
      }
      catch (Exception e)
      {
         log.error("Cannot unregister port component MBean: " + serviceID);
      }

      unregisterBeanMetaData(pcInfo.getServiceDescription());

      wsRegistry.remove(serviceID);

      log.info("WebService undeployed: " + pcInfo.getServiceEndpointURL());
   }

   /** Unregister the meta data for the MetaDataSerializer/Deserializer
    */
   private void unregisterBeanMetaData(ServiceDescription serviceDesc)
   {
      Iterator it = serviceDesc.getTypMappings();
      while (it.hasNext())
      {
         TypeMappingDescription typeMapping = (TypeMappingDescription)it.next();
         if (typeMapping.getMetaData() != null)
            metaDataRegistry.unregisterTypeMappingMetaData(typeMapping.getTypeQName());
      }
   }

   /**
    * Write the generated axis wsdd to the given print writer
    */
   private ServiceDescription getServiceDescription(PortComponentInfo pcInfo) throws Exception
   {
      DeploymentInfo di = pcInfo.getDeploymentInfo();
      PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();
      WebserviceDescriptionMetaData wsdMetaData = pcMetaData.getWebserviceDescription();
      JavaWsdlMapping javaWsdlMapping = wsdMetaData.getJavaWsdlMapping();

      String portName = pcMetaData.getWsdlPort().getLocalPart();

      // build the ServiceDescr from wsdl + jaxrpc-mapping
      Definition wsdlDefinition = wsdMetaData.getWsdlDefinition();

      // Find the optional optional ws4ee-deployment.xml
      URL ws4eeMetaData = findTypeMappingMetaData(di);

      ServiceDescription serviceDesc = new ServiceDescription(wsdlDefinition, javaWsdlMapping, ws4eeMetaData, portName);
      return serviceDesc;
   }

   /**
    * Write the generated axis wsdd to the given print writer
    */
   private String generateDeploymentWSDD(PortComponentInfo pcInfo) throws Exception
   {
      StringWriter strWriter = new StringWriter(1024);
      PrintWriter out = new PrintWriter(strWriter);

      DeploymentInfo di = pcInfo.getDeploymentInfo();
      PortComponentMetaData pcMetaData = pcInfo.getPortComponentMetaData();
      ServiceDescription serviceDesc = pcInfo.getServiceDescription();
      String serviceID = pcInfo.getServiceID();

      WSDDGenerator wsddGenerator = new WSDDGenerator(serviceDesc);

      wsddGenerator.appendHeader(out);
      wsddGenerator.appendServiceElement(out, serviceID, "Handler");

      String wsID = pcInfo.getServiceID();
      out.println("  <parameter name='" + PortComponentMetaData.PARAMETER_WEBSERVICE_ID + "' value='" + wsID + "' />");

      String ejbLink = pcMetaData.getEjbLink();
      String servletLink = pcMetaData.getServletLink();
      if (ejbLink != null)
      {
         out.println("  <parameter name='handlerClass' value='" + invokerProviderEJB + "' />");
      }
      else if (servletLink != null)
      {
         if (servletLink.equals(JMXInvokerEndpoint.class.getName()))
            out.println("  <parameter name='handlerClass' value='" + InvokerProviderJMX.class.getName() + "' />");
         else
            out.println("  <parameter name='handlerClass' value='" + invokerProviderJSE + "' />");
      }
      else
      {
         throw new IllegalArgumentException("Cannot find <ejb-link> nor <servlet-link> in webservices.xml");
      }

      out.println();

      // Append the operations
      wsddGenerator.appendOperations(out);

      ClassLoader ctxClassLoader = Thread.currentThread().getContextClassLoader();
      try
      {
         ClassLoader deploymentCL = (di.metaData instanceof WebMetaData ? ((WebMetaData)di.metaData).getContextLoader() : di.ucl);
         Thread.currentThread().setContextClassLoader(deploymentCL);

         // Append the type mappings
         wsddGenerator.appendTypeMappings(out);
      }
      finally
      {
         Thread.currentThread().setContextClassLoader(ctxClassLoader);
      }

      wsddGenerator.appendFooter(out);
      out.close();

      return strWriter.toString();
   }

   /**
    * Get a namespace aware document builder
    */
   private DocumentBuilder getDocumentBuilder()
           throws ParserConfigurationException
   {
      DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
      factory.setNamespaceAware(true);
      DocumentBuilder builder = factory.newDocumentBuilder();
      return builder;
   }
}