/*
 * JBoss, Home of Professional Open Source.
 * Copyright 2006, 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.soa.bpel.runtime.ws;

import java.net.URL;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import javax.wsdl.Definition;
import javax.wsdl.Fault;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.SOAPFault;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ode.bpel.iapi.Message;
import org.apache.ode.bpel.iapi.MessageExchange;
import org.apache.ode.bpel.iapi.PartnerRoleMessageExchange;
import org.apache.ode.bpel.iapi.ProcessConf;
import org.apache.ode.bpel.iapi.Scheduler;
import org.apache.ode.utils.DOMUtils;
import org.jboss.soa.bpel.runtime.engine.EndpointReference;
import org.jboss.soa.bpel.runtime.engine.PartnerChannel;
import org.jboss.soa.bpel.runtime.engine.ode.BPELEngineImpl;
import org.jboss.soa.bpel.runtime.engine.ode.ExecutionEnvironment;
import org.jboss.soa.bpel.runtime.engine.ode.UDDIClientFactory;
import org.jboss.soa.bpel.runtime.engine.ode.UDDIRegistration;
import org.jboss.soa.bpel.runtime.integration.ServerConfigFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/**
 * Represents a WS invocation.
 * Delegates to a JAX-WS {@link javax.xml.ws.Dispatch} implementation.
 *
 * @author Heiko.Braun <heiko.braun@jboss.com>
 */
public class WebServiceClient implements PartnerChannel
{
  private static final String JAXWS_CLIENT_INITIALIZER = "jaxws.client.initializer.impl";
//we should consider having our own OdeConfigProperties class if we need more properties
  public final static String BPEL_UDDI_LOOKUP        = "uddi.lookup";
  public final static String BPEL_WEBSERVICE_SECURE  = "webservice.secure";
  public final static String BPEL_WEBSERVICE_BASEURL = "webservice.baseurl";

  protected final Log log = LogFactory.getLog(getClass());
  private boolean isInitialized=false;
  private BPELEngineImpl engine;
  private UDDIRegistration uddiRegistration = null;
  private URL wsdlUrl;

  private String id;

  private Dispatch<SOAPMessage> dispatcher = null;
  private EndpointMetaData metaData = null;
  private Definition wsdlDefinition;

  private final QName serviceName;
  private final QName port;

  private SOAPMessageAdapter messageAdapter;

  private ExecutionEnvironment executionEnvironment;
  private java.net.URI baseURI;
  
  public WebServiceClient(EndpointMetaData metaData, ExecutionEnvironment env,
		  BPELEngineImpl engine, ProcessConf pconf)
  {
	this.engine = engine;
    this.executionEnvironment = env;
    this.metaData = metaData;

    this.id = metaData.getEndpointId();
    this.serviceName = metaData.getServiceName();
    this.port = new QName(serviceName.getNamespaceURI(), metaData.getPortName());
    
    this.baseURI = pconf.getBaseURI();
    
    log.debug("Web Service Client: Base URI="+pconf.getBaseURI());
  }
  
  private synchronized void initialize() {
	  if (isUDDILookup()) {
	    	try {
	    		String webServiceHost = ServerConfigFactory.getServerConfig().getWebServiceHost();
	    		int webServicePort = ServerConfigFactory.getServerConfig().getWebServicePort();
	    		URL url = new URL("http://" + webServiceHost + ":" + webServicePort);
	    		if (isWebserviceSecure()) {
	    			int secureWebServicePort = ServerConfigFactory.getServerConfig().getWebServicePort();
	    			url = new URL("https://" + webServiceHost + ":" + secureWebServicePort);
	    		}
	    		//give the user the option to override
	    		if (engine.getOdeConfig().getProperty(BPEL_WEBSERVICE_BASEURL)==null) {
	    			engine.getOdeConfig().getProperties().setProperty(BPEL_WEBSERVICE_BASEURL, url.toExternalForm());
	    		}
	    		uddiRegistration = UDDIClientFactory.newInstance(engine.getOdeConfig().getProperties());
	    	} catch (Exception e) {
	    		log.error(e.getMessage());
	    		log.error("Continuing without UDDI integration.");
	    	}
	    }
	    if (isUDDILookup()) {
	        WSDLReference wsdlReference = uddiRegistration.lookupWSDL(
	        		metaData.getServiceName(),
	        		metaData.getPortName());
	        if (wsdlReference!=null) {
	        	wsdlDefinition = wsdlReference.getDefinition();
	        	wsdlUrl = wsdlReference.getWsdlURL();
	        }
		}
		if (isUDDILookup()==false || wsdlUrl==null) {
		    WSDLReference wsdlReference = new WSDLHelper().createWSDLReference(engine, 
		    		metaData.getProcessId(), metaData.getServiceName(), metaData.getPortName());
		    wsdlDefinition = wsdlReference.getDefinition();
		    wsdlUrl = wsdlReference.getWsdlURL();
		}
		
	    this.messageAdapter = new SOAPMessageAdapter(this.wsdlDefinition, serviceName, port.getLocalPart());		
		isInitialized=true;
  }

  public EndpointReference getEndpointReference()
  {
    return new ClientEndpointReference(this.id);
  }


  public void invoke(final PartnerRoleMessageExchange mex)
  {
	  if (!isInitialized) initialize();
      log.debug("Invoking dispatcher "+this.id);
    
    
    boolean isTwoWay = mex.getMessageExchangePattern()
        == org.apache.ode.bpel.iapi.MessageExchange.MessageExchangePattern.REQUEST_RESPONSE;
    
    final Callable executionCallable;
    
    if(isTwoWay)
    {
      // Defer the invoke until the transaction commits.
      Scheduler scheduler = executionEnvironment.getScheduler();
      executionCallable = new TwoWayCallable(mex.getMessageExchangeId());
      scheduler.registerSynchronizer(new Scheduler.Synchronizer() {

            public void afterCompletion(boolean success)
            {
              // If the TX is rolled back, then we don't send the request.
              if (!success) return;

              // The invocation must happen in a separate thread, holding on the afterCompletion
              // blocks other operations that could have been listed there as well.
              ExecutorService executorService = executionEnvironment.getExecutorService();
              executorService.submit(executionCallable);
            }
            
            public void beforeCompletion(){}
          }
      );

      mex.replyAsync();
    }
    else
    {
      // one-way invocation
      ExecutorService executorService = executionEnvironment.getExecutorService();
      executionCallable = new OneWayCallable(mex.getMessageExchangeId());
      executorService.submit(executionCallable);

      mex.replyOneWayOk();
    }
  }
  
  private class TwoWayCallable implements Callable<Object> {
	
	private String mexId;  
	  
    public TwoWayCallable(String mexId) {
    	this.mexId = mexId;
    }  
	  
	public Object call() throws Exception {
		
		return executionEnvironment.getScheduler().execTransaction(new Callable<Object>(){

			public Object call() throws Exception {
				//We need to get a new mex from persistence layer.
				PartnerRoleMessageExchange mex = (PartnerRoleMessageExchange) engine.getBpelServer().getEngine().getMessageExchange(mexId);
		        try
		        {
		          // Create SOAPMessage
		          SOAPMessage soapRequestMessage = MessageFactory.newInstance().createMessage();
		          if(log.isDebugEnabled())
		            log.debug( "ODE outbound message: \n" +DOMWriter.printNode(mex.getRequest().getMessage(), true) );
		          
		          messageAdapter.createSoapRequest(soapRequestMessage, mex.getRequest(), mex.getOperation());
		          
		          if (log.isDebugEnabled()) {
		          	log.debug("Riftsaw soap request message: \n" + JavaUtils.getSoapMessageASString(soapRequestMessage));
		          }
		          
		          // The actual WS invocation
		          Dispatch proxy = getDispatcher(port);
		          
		          try {
		              SOAPMessage soapResponseMessage = (SOAPMessage)proxy.invoke(soapRequestMessage);

		              // Create ODE response
		              Message odeResponse = mex.createMessage(mex.getOperation().getOutput().getMessage().getQName());
		              if(soapResponseMessage.getSOAPBody().hasFault())
		              {
		                // fault handling
		                Document odeMsg = DOMUtils.newDocument();
		                Element odeMsgEl = odeMsg.createElementNS(null, "message");
		                odeMsg.appendChild(odeMsgEl);
		                
		                Fault fault = messageAdapter.parseSoapFault(odeMsgEl, soapResponseMessage, mex.getOperation());

		                handleFault(mex, fault, soapResponseMessage.getSOAPBody().getFault(), odeMsgEl);
		                
		              }
		              else
		              {
		                messageAdapter.parseSoapResponse(odeResponse,soapResponseMessage,mex.getOperation());
		                mex.reply(odeResponse);
		              }

		              if(log.isDebugEnabled())
		                log.debug( "ODE inbound message: \n" +DOMWriter.printNode(odeResponse.getMessage(), true) );
		              
		          } catch(javax.xml.ws.soap.SOAPFaultException fe) {
		          	
		                // fault handling
		                Document odeMsg = DOMUtils.newDocument();
		                Element odeMsgEl = odeMsg.createElementNS(null, "message");
		                odeMsg.appendChild(odeMsgEl);
		                
		                Fault fault=messageAdapter.parseSoapFault(odeMsgEl, fe.getFault(),
		              		  				mex.getOperation());

		                handleFault(mex, fault, fe.getFault(), odeMsgEl);
		          }
		        }
		        catch (Throwable e)
		        {
		          log.error("WS invocation failed", e);
		          mex.replyWithFailure(MessageExchange.FailureType.COMMUNICATION_ERROR, e.getMessage(), null);
		        }
		        return null;
			}
						
		});
      }
	}
  
  
  private class OneWayCallable implements Callable<Object> {
	
	private String mexId;  
	  
    public OneWayCallable(String mex) {
    	this.mexId = mexId;
    }    
	  
	public Object call() throws Exception {
		return executionEnvironment.getScheduler().execTransaction(new Callable<Object>(){

			public Object call() throws Exception {
				//We need to get a new mex from persistence layer.
				PartnerRoleMessageExchange mex = (PartnerRoleMessageExchange) engine.getBpelServer().getEngine().getMessageExchange(mexId);
	              try
	              {
	                // Create SOAPMessage
	                SOAPMessage soapRequestMessage = MessageFactory.newInstance().createMessage();
	                if(log.isDebugEnabled())
	                  log.debug( "ODE outbound message: \n" +DOMWriter.printNode(mex.getRequest().getMessage(), true) );
	                
	                messageAdapter.createSoapRequest(soapRequestMessage, mex.getRequest(), mex.getOperation());

	                if (log.isDebugEnabled()) {
	                	log.debug("Riftsaw soap request message: \n" + JavaUtils.getSoapMessageASString(soapRequestMessage));
	                }
	                
	                // The actual WS invocation
	                Dispatch proxy = getDispatcher(port);                
	                proxy.invokeOneWay(soapRequestMessage);

	              }
	              catch (Throwable e)
	              {
	                log.error("WS invocation failed", e);
	                mex.replyWithFailure(MessageExchange.FailureType.COMMUNICATION_ERROR, e.getMessage(), null);
	              }

	              return null;
			}			
			
		});
	}
	  
  }

  
  private void handleFault(PartnerRoleMessageExchange mex, Fault fault, SOAPFault soapFault,
		  							Element odeMsgEl) {
      if (fault != null)
      {
        if (log.isWarnEnabled())
          log.warn("Fault response: faultName="
              + fault.getName() + " faultType=" + fault.getMessage().getQName()
              + "\n" + DOMWriter.printNode(odeMsgEl, true));

        QName faultType = fault.getMessage().getQName();
        QName faultName = new QName(wsdlDefinition.getTargetNamespace(), fault.getName());
        Message response = mex.createMessage(faultType);
        response.setMessage(odeMsgEl);

        mex.replyWithFault(faultName, response);
      }
      else
      {
        if (log.isWarnEnabled())
          log.warn("Fault response: faultType=(unknown)");

        mex.replyWithFailure(MessageExchange.FailureType.OTHER, "Unspecified", soapFault.getDetail());
      }
  }

  private UserTransaction getUserTransaction()
      throws NamingException
  {
    InitialContext ctx = new InitialContext();
    UserTransaction tx = (UserTransaction)ctx.lookup("UserTransaction");
    return tx;
  }

  public Element invoke(String operation, Element mesg) throws Exception
  {
    throw new RuntimeException("Not implemented. Should be removed form interface");
  }

  private synchronized Dispatch getDispatcher(QName portName)
  {
	  
	 if(null==dispatcher) {
    	
      log.debug("Creating Dispatcher ("+this.id+") on " + wsdlUrl + ": "+serviceName);
      JAXWSInitializer initializer=initializeStack(portName);

      Service service = Service.create(this.wsdlUrl, serviceName);
      dispatcher = service.createDispatch(
          portName,
          SOAPMessage.class,
          Service.Mode.MESSAGE
      );
      
      if (initializer != null) {
    	  initializer.serviceCreated();
      }
    }

    return dispatcher;
  }
  
  private synchronized JAXWSInitializer initializeStack(QName portName) {
	  JAXWSInitializer initializer=null;
	  String impl=this.executionEnvironment.getOdeConfig().getProperty(JAXWS_CLIENT_INITIALIZER);

	  if (impl != null && impl.trim().length() > 0) {
		  
		  try {
			  Class<?> cls=Class.forName(impl);
			  
			  initializer = (JAXWSInitializer)cls.newInstance();
			  
		  } catch(Exception e) {
			  log.error("Unable to obtain JAXWS Initializer class '"+impl+"'", e);
		  }
		  
		  if (initializer != null) {
			  initializer.initializeStack(this.serviceName, portName, this.baseURI);
		  }
	  } else {
		  log.debug("JAXWS Client Initializer not defined");
	  }
	  
	  return(initializer);
  }

  public String toString()
  {
    return "WebServiceClient {service="+serviceName+",port="+port+"}";
  }
  
  public boolean isUDDILookup() {
      return Boolean.valueOf(engine.getOdeConfig().getProperty(BPEL_UDDI_LOOKUP, "false"));
  }
  
  public boolean isWebserviceSecure() {
      return Boolean.valueOf(engine.getOdeConfig().getProperty(BPEL_WEBSERVICE_SECURE, "false"));
  }
  
 


}
