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

// $Id: JAXWSWebServiceMetaDataBuilder.java 14115 2011-04-15 03:21:26Z bmaxwell $

import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.io.Writer;
import java.net.URL;

import javax.jws.HandlerChain;
import javax.jws.WebService;
import javax.jws.soap.SOAPMessageHandlers;
import javax.management.ObjectName;
import javax.xml.namespace.QName;

import org.jboss.ws.Constants;
import org.jboss.ws.WSException;
import org.jboss.ws.extensions.policy.annotation.PolicyAttachment;
import org.jboss.ws.extensions.policy.metadata.PolicyMetaDataBuilder;
import org.jboss.ws.metadata.builder.MetaDataBuilder;
import org.jboss.ws.metadata.umdm.EndpointMetaData;
import org.jboss.ws.metadata.umdm.HandlerMetaDataJAXWS;
import org.jboss.ws.metadata.umdm.ServerEndpointMetaData;
import org.jboss.ws.metadata.umdm.ServiceMetaData;
import org.jboss.ws.metadata.umdm.UnifiedMetaData;
import org.jboss.ws.metadata.wsdl.WSDLDefinitions;
import org.jboss.ws.metadata.wsdl.WSDLUtils;
import org.jboss.ws.metadata.wsdl.xmlschema.JBossXSModel;
import org.jboss.ws.metadata.wsse.WSSecurityConfigFactory;
import org.jboss.ws.metadata.wsse.WSSecurityConfiguration;
import org.jboss.ws.metadata.wsse.WSSecurityOMFactory;
import org.jboss.ws.tools.ToolsUtils;
import org.jboss.ws.tools.wsdl.JAXBWSDLGenerator;
import org.jboss.ws.tools.wsdl.WSDLDefinitionsFactory;
import org.jboss.ws.tools.wsdl.WSDLGenerator;
import org.jboss.ws.tools.wsdl.WSDLWriter;
import org.jboss.ws.tools.wsdl.WSDLWriterResolver;
import org.jboss.wsf.common.IOUtils;
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.metadata.j2ee.serviceref.UnifiedHandlerChainMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerChainsMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData;
import org.jboss.wsf.spi.metadata.j2ee.serviceref.UnifiedHandlerMetaData.HandlerType;
import org.jboss.wsf.spi.metadata.webservices.PortComponentMetaData;
import org.jboss.wsf.spi.metadata.webservices.WebserviceDescriptionMetaData;
import org.jboss.wsf.spi.metadata.webservices.WebservicesFactory;
import org.jboss.wsf.spi.metadata.webservices.WebservicesMetaData;

/**
 * An abstract annotation meta data builder.
 *
 * @author Thomas.Diesler@jboss.org
 * @author <a href="mailto:jason.greene@jboss.com">Jason T. Greene</a>
 * @author Heiko.Braun@jboss.org
 *
 * @since 15-Oct-2005
 */
@SuppressWarnings("deprecation")
public class JAXWSWebServiceMetaDataBuilder extends JAXWSServerMetaDataBuilder
{
   private boolean generateWsdl = true;
   private boolean extension;
   private boolean toolMode = false;
   private File wsdlDirectory = null;
   private PrintStream messageStream = null;

   private static class EndpointResult
   {
      private Class<?> epClass;
      private ServerEndpointMetaData sepMetaData;
      private ServiceMetaData serviceMetaData;
      private URL wsdlLocation;
      private URL policyLocation;
   }

   public void setGenerateWsdl(boolean generateWsdl)
   {
      this.generateWsdl = generateWsdl;
   }
   
   public void setExtension(boolean extension)
   {
      this.extension = extension;
   }

   public ServerEndpointMetaData buildWebServiceMetaData(Deployment dep, UnifiedMetaData wsMetaData, Class<?> sepClass, String linkName)
   {
      try
      {
         EndpointResult result = processWebService(dep, wsMetaData, sepClass, linkName);

         // Clear the java types, etc.
         ClassLoader runtimeClassLoader = dep.getRuntimeClassLoader();
         if(null == runtimeClassLoader)
            throw new IllegalArgumentException("Runtime loader cannot be null");
         resetMetaDataBuilder(runtimeClassLoader);

         ServerEndpointMetaData sepMetaData = result.sepMetaData;
         ServiceMetaData serviceMetaData = result.serviceMetaData;
         serviceMetaData.setWsdlLocation(result.wsdlLocation);
         Class<?> seiClass = result.epClass;

         sepMetaData.setLinkName(linkName);
         sepMetaData.setServiceEndpointImplName(sepClass.getName());
         sepMetaData.setServiceEndpointInterfaceName(seiClass.getName());

         // Assign the WS-Security configuration,
         WSSecurityConfigFactory wsseConfFactory = WSSecurityConfigFactory.newInstance();
         WSSecurityConfiguration securityConfiguration = wsseConfFactory.createConfiguration(wsMetaData.getRootFile(), WSSecurityOMFactory.SERVER_RESOURCE_NAME);
         serviceMetaData.setSecurityConfiguration(securityConfiguration);

         // Process an optional @SOAPBinding annotation
         processSOAPBinding(sepMetaData, seiClass);

         // Process an optional @BindingType annotation
         processBindingType(sepMetaData, sepClass);

         // process config
         processEndpointConfig(sepClass, linkName, sepMetaData);

         // Process web methods
         processWebMethods(sepMetaData, seiClass);

         // Init the transport guarantee
         initTransportGuaranteeJSE(dep, sepMetaData, linkName);

         // Initialize types
         createJAXBContext(sepMetaData);
         populateXmlTypes(sepMetaData);

         //Process an optional @PolicyAttachment annotation
         if (sepClass.isAnnotationPresent(PolicyAttachment.class))
         {
            PolicyMetaDataBuilder policyBuilder = PolicyMetaDataBuilder.getServerSidePolicyMetaDataBuilder(toolMode);
            policyBuilder.processPolicyAnnotations(sepMetaData, sepClass);
         }

         // The server must always generate WSDL
         if (generateWsdl || !toolMode)
            processOrGenerateWSDL(seiClass, serviceMetaData, result.wsdlLocation, sepMetaData);

         // No need to process endpoint items if we are in tool mode
         if (toolMode)
            return sepMetaData;

         // Sanity check: read the generated WSDL and initialize the schema model
         // Note, this should no longer be needed, look into removing it
         WSDLDefinitions wsdlDefinitions = serviceMetaData.getWsdlDefinitions();
         JBossXSModel schemaModel = WSDLUtils.getSchemaModel(wsdlDefinitions.getWsdlTypes());
         serviceMetaData.getTypesMetaData().setSchemaModel(schemaModel);

         // Note, that @WebContext needs to be defined on the endpoint not the SEI
         processWebContext(dep, sepClass, linkName, sepMetaData);

         // setup handler chain from config
         sepMetaData.initEndpointConfig();

         // Process an optional @HandlerChain annotation
         if (sepClass.isAnnotationPresent(HandlerChain.class))
            processHandlerChain(sepMetaData, sepClass);
         else if (seiClass.isAnnotationPresent(HandlerChain.class))
            processHandlerChain(sepMetaData, seiClass);

         // process webservices.xml contributions
         processWSDDContribution(sepMetaData);

         // Init the endpoint address
         initEndpointAddress(dep, sepMetaData);

         // Process an optional @SOAPMessageHandlers annotation
         if (sepClass.isAnnotationPresent(SOAPMessageHandlers.class) || seiClass.isAnnotationPresent(SOAPMessageHandlers.class))
            log.warn("@SOAPMessageHandlers is deprecated as of JAX-WS 2.0 with no replacement.");

         MetaDataBuilder.replaceAddressLocation(sepMetaData);
         processEndpointMetaDataExtensions(sepMetaData, wsdlDefinitions);

         // init service endpoint id
         ObjectName sepID = MetaDataBuilder.createServiceEndpointID(dep, sepMetaData);
         sepMetaData.setServiceEndpointID(sepID);

         return sepMetaData;
      }
      catch (RuntimeException rte)
      {
         throw rte;
      }
      catch (Exception ex)
      {
         throw new WSException("Cannot build meta data: " + ex.getMessage(), ex);
      }
   }

   /**
    * With JAX-WS the use of webservices.xml is optional since the annotations can be used
    * to specify most of the information specified in this deployment descriptor file.
    * The deployment descriptors are only used to override or augment the annotation member attributes.
    * @param sepMetaData
    */
   private void processWSDDContribution(ServerEndpointMetaData sepMetaData)
   {
      WebservicesMetaData webservices = WebservicesFactory.loadFromVFSRoot(sepMetaData.getRootFile());
      if (webservices != null)
      {
         for (WebserviceDescriptionMetaData wsDesc : webservices.getWebserviceDescriptions())
         {
            for (PortComponentMetaData portComp : wsDesc.getPortComponents())
            {
               // We match portComp's by SEI first and portQName second
               // In the first case the portComp may override the portQName that derives from the annotation
               String portCompSEI = portComp.getServiceEndpointInterface();
               boolean doesMatch = portCompSEI != null ? portCompSEI.equals(sepMetaData.getServiceEndpointInterfaceName()) : false;
               if (!doesMatch)
               {
                  doesMatch = portComp.getWsdlPort().equals(sepMetaData.getPortName());
               }

               if (doesMatch)
               {

                  log.debug("Processing 'webservices.xml' contributions on EndpointMetaData");

                  // PortQName overrides
                  if (portComp.getWsdlPort() != null)
                  {
                     log.debug("Override EndpointMetaData portName " + sepMetaData.getPortName() + " with " + portComp.getWsdlPort());
                     sepMetaData.setPortName(portComp.getWsdlPort());
                  }

                  // HandlerChain contributions
                  UnifiedHandlerChainsMetaData chainWrapper = portComp.getHandlerChains();
                  if (chainWrapper != null)
                  {
                     for (UnifiedHandlerChainMetaData handlerChain : chainWrapper.getHandlerChains())
                     {
                        for (UnifiedHandlerMetaData uhmd : handlerChain.getHandlers())
                        {
                           log.debug("Contribute handler from webservices.xml: " + uhmd.getHandlerName());
                           HandlerMetaDataJAXWS hmd = HandlerMetaDataJAXWS.newInstance(uhmd, HandlerType.ENDPOINT);
                           sepMetaData.addHandler(hmd);
                        }
                     }
                  }

                  // MTOM settings
                  if (portComp.isEnableMtom())
                  {
                     log.debug("Enabling MTOM");

                     String bindingId = sepMetaData.getBindingId();
                     if (bindingId.equals(Constants.SOAP11HTTP_BINDING))
                        sepMetaData.setBindingId(Constants.SOAP11HTTP_MTOM_BINDING);
                     else if (bindingId.equals(Constants.SOAP12HTTP_BINDING))
                        sepMetaData.setBindingId(Constants.SOAP12HTTP_MTOM_BINDING);

                  }

               }
            }
         }

      }
   }

   private EndpointResult processWebService(Deployment dep, UnifiedMetaData wsMetaData, Class<?> sepClass, String linkName) throws ClassNotFoundException, IOException
   {
      WebService anWebService = sepClass.getAnnotation(WebService.class);
      if (anWebService == null)
         throw new WSException("Cannot obtain @WebService annotation from: " + sepClass.getName());

      Endpoint ep = dep.getService().getEndpointByName(linkName);
      
      Class<?> seiClass = null;
      String seiName;
      WSDLUtils wsdlUtils = WSDLUtils.getInstance();

      String name = anWebService.name();
      if (name.length() == 0)
         name = WSDLUtils.getJustClassName(sepClass);

      String serviceName = anWebService.serviceName();
      if (serviceName.length() == 0)
         serviceName = WSDLUtils.getJustClassName(sepClass) + "Service";

      String serviceNS = anWebService.targetNamespace();
      if (serviceNS.length() == 0)
         serviceNS = wsdlUtils.getTypeNamespace(sepClass);

      String portName = anWebService.portName();
      if (portName.length() == 0)
         portName = name + "Port";

      String wsdlLocation = anWebService.wsdlLocation();
      String interfaceNS = serviceNS; // the default, but a SEI annotation may override this

      if (anWebService.endpointInterface().length() > 0)
      {
         seiName = anWebService.endpointInterface();
         ClassLoader runtimeClassLoader = dep.getRuntimeClassLoader();
         if(null == runtimeClassLoader)
            throw new IllegalArgumentException("Runtime loader cannot be null");
         seiClass = runtimeClassLoader.loadClass(seiName);
         WebService seiAnnotation = seiClass.getAnnotation(WebService.class);

         if (seiAnnotation == null)
            throw new WSException("Interface does not have a @WebService annotation: " + seiName);

         if (seiAnnotation.portName().length() > 0 || seiAnnotation.serviceName().length() > 0 || seiAnnotation.endpointInterface().length() > 0)
            throw new WSException("@WebService[portName,serviceName,endpointInterface] MUST NOT be defined on: " + seiName);

         // Redefine the interface or "PortType" name
         name = seiAnnotation.name();
         if (name.length() == 0)
            name = WSDLUtils.getJustClassName(seiClass);

         interfaceNS = seiAnnotation.targetNamespace();
         if (interfaceNS.length() == 0)
            interfaceNS = wsdlUtils.getTypeNamespace(seiClass);

         // The spec states that WSDL location should be allowed on an SEI, although it
         // makes far more sense on the implementation bean, so we ALWAYS override the SEI
         // when wsdlLocation is defined on the bean

         if (wsdlLocation.length() == 0)
            wsdlLocation = seiAnnotation.wsdlLocation();
      }

      // Setup the ServerEndpointMetaData
      QName portQName = new QName(serviceNS, portName);
      QName portTypeQName = new QName(interfaceNS, name);

      EndpointResult result = new EndpointResult();
      result.serviceMetaData = new ServiceMetaData(wsMetaData, new QName(serviceNS, serviceName));
      result.sepMetaData = new ServerEndpointMetaData(result.serviceMetaData, ep, portQName, portTypeQName, EndpointMetaData.Type.JAXWS);
      result.epClass = (seiClass != null ? seiClass : sepClass);
      result.serviceMetaData.addEndpoint(result.sepMetaData);
      wsMetaData.addService(result.serviceMetaData);

      if (dep instanceof ArchiveDeployment)
         result.wsdlLocation = ((ArchiveDeployment)dep).getMetaDataFileURL(wsdlLocation);

      return result;
   }

   private void processOrGenerateWSDL(Class wsClass, ServiceMetaData serviceMetaData, URL wsdlLocation, EndpointMetaData epMetaData)
   {
      PolicyMetaDataBuilder policyBuilder = PolicyMetaDataBuilder.getServerSidePolicyMetaDataBuilder(toolMode);
      try
      {
         WSDLGenerator generator = new JAXBWSDLGenerator(jaxbCtx);
         generator.setExtension(extension);
         WSDLDefinitionsFactory factory = WSDLDefinitionsFactory.newInstance();
         if (wsdlLocation != null)
         {
            //we can no longer use the user provided wsdl without parsing it right now, since we
            //need to look for policies and eventually choose the supported policy alternatives
            WSDLDefinitions wsdlDefinitions = factory.parse(wsdlLocation);
            policyBuilder.processPolicyExtensions(epMetaData, wsdlDefinitions);
            //now we have the UMDM containing policy data; anyway we can't write a new wsdl file with
            //the supported alternatives and so on, since we need to publish the file the user provided
            serviceMetaData.setWsdlLocation(wsdlLocation);
         }
         else
         {
            WSDLDefinitions wsdlDefinitions = generator.generate(serviceMetaData);
            writeWsdl(serviceMetaData, wsdlDefinitions, epMetaData);
         }
      }
      catch (RuntimeException rte)
      {
         throw rte;
      }
      catch (IOException e)
      {
         throw new WSException("Cannot write generated wsdl", e);
      }
   }

   private void writeWsdl(ServiceMetaData serviceMetaData, WSDLDefinitions wsdlDefinitions, EndpointMetaData epMetaData) throws IOException
   {
      // The RI uses upper case, and the TCK expects it, so we just mimic this even though we don't really have to
      String wsdlName = ToolsUtils.firstLetterUpperCase(serviceMetaData.getServiceName().getLocalPart());
      // Ensure that types are only in the interface qname
      wsdlDefinitions.getWsdlTypes().setNamespace(epMetaData.getPortTypeName().getNamespaceURI());

      final File dir, wsdlFile;

      if (wsdlDirectory != null)
      {
         dir = wsdlDirectory;
         wsdlFile = new File(dir, wsdlName + ".wsdl");
      }
      else
      {
         dir = IOUtils.createTempDirectory();
         wsdlFile = File.createTempFile(wsdlName, ".wsdl", dir);
         wsdlFile.deleteOnExit();
      }

      message(wsdlFile.getName());
      Writer writer = IOUtils.getCharsetFileWriter(wsdlFile, Constants.DEFAULT_XML_CHARSET);
      new WSDLWriter(wsdlDefinitions).write(writer, Constants.DEFAULT_XML_CHARSET, new WSDLWriterResolver() {
         public WSDLWriterResolver resolve(String suggestedFile) throws IOException
         {
            File file;
            if (wsdlDirectory != null)
            {
               file = new File(dir, suggestedFile + ".wsdl");
            }
            else
            {
               file = File.createTempFile(suggestedFile, ".wsdl", dir);
               file.deleteOnExit();
            }
            actualFile = file.getName();
            message(actualFile);
            charset = Constants.DEFAULT_XML_CHARSET;
            writer = IOUtils.getCharsetFileWriter(file, Constants.DEFAULT_XML_CHARSET);
            return this;
         }
      });
      writer.close();

      serviceMetaData.setWsdlLocation(wsdlFile.toURL());
   }

   private void message(String msg)
   {
      if (messageStream != null)
         messageStream.println(msg);
   }

   public void setToolMode(boolean toolMode)
   {
      this.toolMode = toolMode;
   }

   public void setWsdlDirectory(File wsdlDirectory)
   {
      this.wsdlDirectory = wsdlDirectory;
   }

   public void setMessageStream(PrintStream messageStream)
   {
      this.messageStream = messageStream;
   }
}
