/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt 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.ws.metadata.builder;

// $Id: $

import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.management.ObjectName;
import javax.wsdl.Definition;
import javax.wsdl.Import;
import javax.wsdl.Port;
import javax.wsdl.extensions.soap.SOAPAddress;
import javax.wsdl.extensions.soap12.SOAP12Address;
import javax.xml.namespace.QName;
import javax.xml.ws.addressing.AddressingProperties;

import org.jboss.logging.Logger;
import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.core.soap.Use;
import org.jboss.ws.extensions.addressing.AddressingPropertiesImpl;
import org.jboss.ws.extensions.addressing.metadata.AddressingOpMetaExt;
import org.jboss.ws.extensions.eventing.EventingConstants;
import org.jboss.ws.extensions.eventing.EventingUtils;
import org.jboss.ws.extensions.eventing.metadata.EventingEpMetaExt;
import org.jboss.ws.metadata.umdm.ClientEndpointMetaData;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.OperationMetaData;
import org.jboss.ws.metadata.umdm.ServerEndpointMetaData;
import org.jboss.ws.metadata.wsdl.WSDLBinding;
import org.jboss.ws.metadata.wsdl.WSDLBindingOperation;
import org.jboss.ws.metadata.wsdl.WSDLDefinitions;
import org.jboss.ws.metadata.wsdl.WSDLEndpoint;
import org.jboss.ws.metadata.wsdl.WSDLInterface;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperation;
import org.jboss.ws.metadata.wsdl.WSDLInterfaceOperationOutput;
import org.jboss.ws.metadata.wsdl.WSDLProperty;
import org.jboss.ws.metadata.wsdl.WSDLService;
import org.jboss.ws.metadata.wsdl.WSDLUtils;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSModel;
import org.jboss.wsf.common.ObjectNameFactory;
import org.jboss.wsf.spi.SPIProvider;
import org.jboss.wsf.spi.SPIProviderResolver;
import org.jboss.wsf.spi.deployment.ArchiveDeployment;
import org.jboss.wsf.spi.deployment.Deployment;
import org.jboss.wsf.spi.deployment.Endpoint;
import org.jboss.wsf.spi.management.ServerConfig;
import org.jboss.wsf.spi.management.ServerConfigFactory;
import org.jboss.wsf.spi.metadata.j2ee.EJBArchiveMetaData;
import org.jboss.wsf.spi.metadata.j2ee.EJBMetaData;
import org.jboss.wsf.spi.metadata.j2ee.MDBMetaData;
import org.jboss.wsf.spi.metadata.j2ee.JSEArchiveMetaData;
import org.jboss.wsf.spi.metadata.j2ee.JSESecurityMetaData;
import org.jboss.wsf.spi.metadata.j2ee.JSESecurityMetaData.JSEResourceCollection;

/** An abstract meta data builder.
 *
 * @author Thomas.Diesler@jboss.org
 * @since 19-May-2005
 */
public abstract class MetaDataBuilder
{
   // provide logging
   private final static Logger log = Logger.getLogger(MetaDataBuilder.class);

   /** Inititialize the endpoint binding */
   protected void initEndpointBinding(WSDLEndpoint wsdlEndpoint, EndpointMetaData epMetaData)
   {
      WSDLDefinitions wsdlDefinitions = wsdlEndpoint.getWsdlService().getWsdlDefinitions();
      WSDLInterface wsdlInterface = wsdlEndpoint.getInterface();
      WSDLBinding wsdlBinding = wsdlDefinitions.getBindingByInterfaceName(wsdlInterface.getName());
      String bindingType = wsdlBinding.getType();
      if (Constants.NS_SOAP11.equals(bindingType))
         epMetaData.setBindingId(Constants.SOAP11HTTP_BINDING);
      else if (Constants.NS_SOAP12.equals(bindingType))
         epMetaData.setBindingId(Constants.SOAP12HTTP_BINDING);
   }

   /** Initialize the endpoint encoding style from the binding operations
    */
   protected void initEndpointEncodingStyle(EndpointMetaData epMetaData)
   {
      WSDLDefinitions wsdlDefinitions = epMetaData.getServiceMetaData().getWsdlDefinitions();
      for (WSDLService wsdlService : wsdlDefinitions.getServices())
      {
         for (WSDLEndpoint wsdlEndpoint : wsdlService.getEndpoints())
         {
            if (epMetaData.getPortName().equals(wsdlEndpoint.getName()))
            {
               QName bindQName = wsdlEndpoint.getBinding();
               WSDLBinding wsdlBinding = wsdlDefinitions.getBinding(bindQName);
               if (wsdlBinding == null)
                  throw new WSException("Cannot obtain binding: " + bindQName);

               for (WSDLBindingOperation wsdlBindingOperation : wsdlBinding.getOperations())
               {
                  String encStyle = wsdlBindingOperation.getEncodingStyle();
                  epMetaData.setEncodingStyle(Use.valueOf(encStyle));
               }
            }
         }
      }
   }

   protected void initEndpointAddress(Deployment dep, ServerEndpointMetaData sepMetaData)
   {
      String contextRoot = dep.getService().getContextRoot();
      String urlPattern = null;

      // Get the URL pattern from the endpoint
      String linkName = sepMetaData.getLinkName();
      if (linkName != null)
      {
         Endpoint endpoint = dep.getService().getEndpointByName(linkName);
         if (endpoint != null)
            urlPattern = endpoint.getURLPattern();
      }

      // If not, derive the context root from the deployment
      if (contextRoot == null)
      {
         String simpleName = dep.getSimpleName();
         contextRoot = simpleName.substring(0, simpleName.indexOf('.'));
         if (dep instanceof ArchiveDeployment)
         {
            if (((ArchiveDeployment)dep).getParent() != null)
            {
               simpleName = ((ArchiveDeployment)dep).getParent().getSimpleName();
               simpleName = simpleName.substring(0, simpleName.indexOf('.'));
               contextRoot = simpleName + "-" + contextRoot;
            }
         }
      }

      // Default to "/*" 
      if (urlPattern == null)
         urlPattern = "/*";

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

      sepMetaData.setContextRoot(contextRoot);
      sepMetaData.setURLPattern(urlPattern);

      String servicePath = contextRoot + urlPattern;
      sepMetaData.setEndpointAddress(getServiceEndpointAddress(null, servicePath));
   }

   public static ObjectName createServiceEndpointID(Deployment dep, ServerEndpointMetaData sepMetaData)
   {
      String linkName = sepMetaData.getLinkName();
      String context = sepMetaData.getContextRoot();
      if (context.startsWith("/"))
         context = context.substring(1);

      StringBuilder idstr = new StringBuilder(ServerEndpointMetaData.SEPID_DOMAIN + ":");
      idstr.append(ServerEndpointMetaData.SEPID_PROPERTY_CONTEXT + "=" + context);
      idstr.append("," + ServerEndpointMetaData.SEPID_PROPERTY_ENDPOINT + "=" + linkName);

      // Add JMS destination JNDI name for MDB endpoints
      EJBArchiveMetaData apMetaData = dep.getAttachment(EJBArchiveMetaData.class);
      if (apMetaData != null)
      {
         String ejbName = sepMetaData.getLinkName();
         if (ejbName == null)
            throw new WSException("Cannot obtain ejb-link from port component");

         EJBMetaData beanMetaData = (EJBMetaData)apMetaData.getBeanByEjbName(ejbName);
         if (beanMetaData == null)
            throw new WSException("Cannot obtain ejb meta data for: " + ejbName);

         if (beanMetaData instanceof MDBMetaData)
         {
            MDBMetaData mdMetaData = (MDBMetaData)beanMetaData;
            String jndiName = mdMetaData.getDestinationJndiName();
            idstr.append(",jms=" + jndiName);
         }
      }

      return ObjectNameFactory.create(idstr.toString());
   }

   /** Get the web service address for a given path
    */
   public static String getServiceEndpointAddress(String uriScheme, String servicePath)
   {
      if (servicePath == null || servicePath.length() == 0)
         throw new WSException("Service path cannot be null");

      if (servicePath.endsWith("/*"))
         servicePath = servicePath.substring(0, servicePath.length() - 2);

      if (uriScheme == null)
         uriScheme = "http";

      SPIProvider spiProvider = SPIProviderResolver.getInstance().getProvider();
      ServerConfig config = spiProvider.getSPI(ServerConfigFactory.class).getServerConfig();

      String host = config.getWebServiceHost();
      
      int port;
      if ("https".equals(uriScheme))
      {
         port = config.getWebServiceSecurePort();
      }
      else
      {
         port = config.getWebServicePort();
      }      
      
      // Reset port if using the default for the scheme.
      if (("http".equals(uriScheme) && port == 80) || ("https".equals(uriScheme) && port == 443))
      {
         port = -1;
      }

      URL url = null;
      try
      {
         if (port > -1)
         {
            url = new URL(uriScheme, host, port, servicePath);
         }
         else
         {
            url = new URL(uriScheme, host, servicePath);
         }
         return url.toExternalForm();
      }
      catch (MalformedURLException e)
      {
         throw new WSException("Malformed URL: uriScheme={" + uriScheme + "} host={" + host + "} port={" + port + "} servicePath={" + servicePath + "}", e);
      }
   }

   /**
    * Read the transport guarantee from web.xml
    */
   protected void initTransportGuaranteeJSE(Deployment dep, ServerEndpointMetaData sepMetaData, String servletLink) throws IOException
   {
      String transportGuarantee = null;
      JSEArchiveMetaData webMetaData = dep.getAttachment(JSEArchiveMetaData.class);
      if (webMetaData != null)
      {
         Map<String, String> servletMappings = webMetaData.getServletMappings();
         String urlPattern = servletMappings.get(servletLink);

         if (urlPattern == null)
            throw new WSException("Cannot find <url-pattern> for servlet-name: " + servletLink);

         List<JSESecurityMetaData> securityList = webMetaData.getSecurityMetaData();
         for (JSESecurityMetaData currentSecurity : securityList)
         {
            if (currentSecurity.getTransportGuarantee() != null && currentSecurity.getTransportGuarantee().length() > 0)
            {
               for (JSEResourceCollection currentCollection : currentSecurity.getWebResources())
               {
                  for (String currentUrlPattern : currentCollection.getUrlPatterns())
                  {
                     if (urlPattern.equals(currentUrlPattern) || "/*".equals(currentUrlPattern))
                     {
                        transportGuarantee = currentSecurity.getTransportGuarantee();
                     }
                  }
               }
            }
         }
      }
      sepMetaData.setTransportGuarantee(transportGuarantee);
   }

   /** Replace the address locations for a given port component.
    */
   public static void replaceAddressLocation(ServerEndpointMetaData sepMetaData)
   {
      WSDLDefinitions wsdlDefinitions = sepMetaData.getServiceMetaData().getWsdlDefinitions();
      QName portName = sepMetaData.getPortName();

      boolean endpointFound = false;
      for (WSDLService wsdlService : wsdlDefinitions.getServices())
      {
         for (WSDLEndpoint wsdlEndpoint : wsdlService.getEndpoints())
         {
            QName wsdlPortName = wsdlEndpoint.getName();
            if (wsdlPortName.equals(portName))
            {
               endpointFound = true;

               String orgAddress = wsdlEndpoint.getAddress();
               String uriScheme = getUriScheme(orgAddress);

               String transportGuarantee = sepMetaData.getTransportGuarantee();
               if ("CONFIDENTIAL".equals(transportGuarantee))
                  uriScheme = "https";

               String servicePath = sepMetaData.getContextRoot() + sepMetaData.getURLPattern();
               String serviceEndpointURL = getServiceEndpointAddress(uriScheme, servicePath);

               SPIProvider spiProvider = SPIProviderResolver.getInstance().getProvider();
               ServerConfig config = spiProvider.getSPI(ServerConfigFactory.class).getServerConfig();
               boolean alwaysModify = config.isModifySOAPAddress();

               if (alwaysModify || uriScheme == null || orgAddress.indexOf("REPLACE_WITH_ACTUAL_URL") >= 0)
               {
                  log.debug("Replace service endpoint address '" + orgAddress + "' with '" + serviceEndpointURL + "'");
                  wsdlEndpoint.setAddress(serviceEndpointURL);
                  sepMetaData.setEndpointAddress(serviceEndpointURL);

                  // modify the wsdl-1.1 definition
                  if (wsdlDefinitions.getWsdlOneOneDefinition() != null)
                     replaceWSDL11SOAPAddress(wsdlDefinitions, portName, serviceEndpointURL);
               }
               else
               {
                  log.debug("Don't replace service endpoint address '" + orgAddress + "'");
                  try
                  {
                     sepMetaData.setEndpointAddress(new URL(orgAddress).toExternalForm());
                  }
                  catch (MalformedURLException e)
                  {
                     throw new WSException("Malformed URL: " + orgAddress);
                  }
               }
            }
         }
      }

      if (endpointFound == false)
         throw new WSException("Cannot find port in wsdl: " + portName);
   }

   private static void replaceWSDL11SOAPAddress(WSDLDefinitions wsdlDefinitions, QName portQName, String serviceEndpointURL)
   {
      Definition wsdlOneOneDefinition = wsdlDefinitions.getWsdlOneOneDefinition();
      String tnsURI = wsdlOneOneDefinition.getTargetNamespace();

      // search for matching portElement and replace the address URI
      Port wsdlOneOnePort = modifySOAPAddress(tnsURI, portQName, serviceEndpointURL, wsdlOneOneDefinition.getServices());

      // recursivly process imports if none can be found
      if (wsdlOneOnePort == null && !wsdlOneOneDefinition.getImports().isEmpty())
      {

         Iterator imports = wsdlOneOneDefinition.getImports().values().iterator();
         while (imports.hasNext())
         {
            List l = (List)imports.next();
            Iterator importsByNS = l.iterator();
            while (importsByNS.hasNext())
            {
               Import anImport = (Import)importsByNS.next();
               wsdlOneOnePort = modifySOAPAddress(anImport.getNamespaceURI(), portQName, serviceEndpointURL, anImport.getDefinition().getServices());
            }
         }
      }

      // if it still doesn't exist something is wrong
      if (wsdlOneOnePort == null)
         throw new IllegalArgumentException("Cannot find port with name '" + portQName + "' in wsdl document");
   }

   private static Port modifySOAPAddress(String tnsURI, QName portQName, String serviceEndpointURL, Map services)
   {
      Port wsdlOneOnePort = null;
      Iterator itServices = services.values().iterator();
      while (itServices.hasNext())
      {
         javax.wsdl.Service wsdlOneOneService = (javax.wsdl.Service)itServices.next();
         Map wsdlOneOnePorts = wsdlOneOneService.getPorts();
         Iterator itPorts = wsdlOneOnePorts.keySet().iterator();
         while (itPorts.hasNext())
         {
            String portLocalName = (String)itPorts.next();
            if (portQName.equals(new QName(tnsURI, portLocalName)))
            {
               wsdlOneOnePort = (Port)wsdlOneOnePorts.get(portLocalName);
               List extElements = wsdlOneOnePort.getExtensibilityElements();
               for (Object extElement : extElements)
               {
                  if (extElement instanceof SOAPAddress)
                  {
                     SOAPAddress address = (SOAPAddress)extElement;
                     address.setLocationURI(serviceEndpointURL);
                  }
                  if (extElement instanceof SOAP12Address)
                  {
                     SOAP12Address address = (SOAP12Address)extElement;
                     address.setLocationURI(serviceEndpointURL);
                  }
               }
            }
         }
      }

      return wsdlOneOnePort;
   }

   private static String getUriScheme(String addrStr)
   {
      try
      {
         URI addrURI = new URI(addrStr);
         String scheme = addrURI.getScheme();
         return scheme;
      }
      catch (URISyntaxException e)
      {
         return null;
      }
   }

   protected void processEndpointMetaDataExtensions(EndpointMetaData epMetaData, WSDLDefinitions wsdlDefinitions)
   {
      for (WSDLInterface wsdlInterface : wsdlDefinitions.getInterfaces())
      {
         WSDLProperty eventSourceProp = wsdlInterface.getProperty(Constants.WSDL_PROPERTY_EVENTSOURCE);
         if (eventSourceProp != null && epMetaData instanceof ServerEndpointMetaData)
         {
            ServerEndpointMetaData sepMetaData = (ServerEndpointMetaData)epMetaData;
            String eventSourceNS = wsdlInterface.getName().getNamespaceURI() + "/" + wsdlInterface.getName().getLocalPart();

            // extract the schema model
            JBossXSModel schemaModel = WSDLUtils.getSchemaModel(wsdlDefinitions.getWsdlTypes());
            String[] notificationSchema = EventingUtils.extractNotificationSchema(schemaModel);

            // extract the root element NS
            String notificationRootElementNS = null;
            WSDLInterfaceOperation wsdlInterfaceOperation = wsdlInterface.getOperations()[0];
            if (wsdlInterfaceOperation.getOutputs().length > 0)
            {
               WSDLInterfaceOperationOutput wsdlInterfaceOperationOutput = wsdlInterfaceOperation.getOutputs()[0];
               notificationRootElementNS = wsdlInterfaceOperationOutput.getElement().getNamespaceURI();
            }
            else
            {
               // WSDL operation of an WSDL interface that is marked as an event source
               // requires to carry an output message.
               throw new WSException("Unable to resolve eventing root element NS. No operation output found at " + wsdlInterfaceOperation.getName());
            }

            EventingEpMetaExt ext = new EventingEpMetaExt(EventingConstants.NS_EVENTING);
            ext.setEventSourceNS(eventSourceNS);
            ext.setNotificationSchema(notificationSchema);
            ext.setNotificationRootElementNS(notificationRootElementNS);
            sepMetaData.addExtension(ext);
         }
      }
   }

   /** Process operation meta data extensions. */
   protected void processOpMetaExtensions(OperationMetaData opMetaData, WSDLInterfaceOperation wsdlOperation)
   {

      String tns = wsdlOperation.getName().getNamespaceURI();
      String portTypeName = wsdlOperation.getName().getLocalPart();

      AddressingProperties ADDR = new AddressingPropertiesImpl();
      AddressingOpMetaExt addrExt = new AddressingOpMetaExt(ADDR.getNamespaceURI());

      // inbound action
      WSDLProperty wsaInAction = wsdlOperation.getProperty(Constants.WSDL_PROPERTY_ACTION_IN);
      if (wsaInAction != null)
      {
         addrExt.setInboundAction(wsaInAction.getValue());
      }
      else
      {
         WSDLProperty messageName = wsdlOperation.getProperty(Constants.WSDL_PROPERTY_MESSAGE_NAME_IN);
         if (messageName != null)
         {
            addrExt.setInboundAction(tns + "/" + portTypeName + "/" + messageName.getValue());
         }
         else
         {
            addrExt.setInboundAction(tns + "/" + portTypeName + "/IN");
         }
      }

      // outbound action
      WSDLProperty wsaOutAction = wsdlOperation.getProperty(Constants.WSDL_PROPERTY_ACTION_OUT);
      if (wsaOutAction != null)
      {
         addrExt.setOutboundAction(wsaOutAction.getValue());
      }
      else
      {
         WSDLProperty messageName = wsdlOperation.getProperty(Constants.WSDL_PROPERTY_MESSAGE_NAME_OUT);
         if (messageName != null)
         {
            addrExt.setOutboundAction(tns + "/" + portTypeName + "/" + messageName.getValue());
         }
         else
         {
            addrExt.setOutboundAction(tns + "/" + portTypeName + "/OUT");
         }
      }

      opMetaData.addExtension(addrExt);
   }
}
