package com.kirona.axis2.service.client;

import java.io.File;

import java.lang.reflect.InvocationTargetException;

import java.util.Iterator;
import java.util.Set;

import javax.servlet.ServletContext;

import org.apache.axis2.AxisFault;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.Stub;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.ConfigurationContextFactory;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.OperationContext;
import org.apache.axis2.transport.http.CommonsTransportHeaders;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.commons.pool.PoolableObjectFactory;
import org.apache.commons.pool.impl.GenericObjectPool;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

/**
 * <p>
 * All client web services based on AXIS2 that create a client stub using the
 * WSDL2java tool must use this class.
 * The client stub created by the AXIS2 WSDL2java tool is not thread safe
 * and so this class will create a pool of client stub objects ready for use
 * maintaining thread safety.
 * </p>
 * To use this class you must do the following:
 * <ul>
 * <li>First create an AXIS2 web service client stub using the wsdl2java tool using
 * a supplied wsdl</li>
 * <li>Instantiate this class and set the target endpoint URL, the AXIS2 web service client stub class created above,
 * and optionally the number of concurrent web service calls you want to support (the default is 10)</li>
 * <li>Then you must call initialize() where a pool of client stubs are created for you. You must retain
 * this instance somewhere in your application and re-use for all subsequent calls since the initial
 * setup of axis2 is very expensive</li>
 * <li>To use the class perform code similar to the following...</br>
 * <pre>
 *   myWebService = new Axis2ClientWebService();
 *   myWebService.setTargetEndPointURL("http://your.endpoint.url/...");
 *   myWebService.setWebServiceClientStub(YourStub.class);
 *   myWebService.setClientStubPoolSize(20);
 *   myWebService.intialize();
 *
 *   try
 *   {
 *     yourStub = (YourStub)myWebService.borrowWebServiceClientStub();
 *     // make the web service call
 *     webServiceResponse = yourStub.aMethodCall(webServiceParam1,webServiceParam2, etc ...);
 *   }
 *   catch(RemoteException e)
 *   {
 *     logger.error("Web service call failed",e);
 *     .....
 *   }
 *   finally
 *   {
 *     myWebService.returnWebServiceClientStub(yourStub);
 *   }
 * </pre></li>
 * </ul>
 */
public class Axis2ClientWebService1
{
  public static final int DEFAULT_CLIENT_STUB_POOL_SIZE = 10;

  protected final Logger logger = LoggerFactory.getLogger(Axis2ClientWebService1.class);
  
  // the endpoint we are targetting
  private String targetEndPointURL;
  private int clientStubPoolSize = DEFAULT_CLIENT_STUB_POOL_SIZE;
  // we only create one AXIS2 configuration context whcih are shared amongst all the client stubs
  private ConfigurationContext configurationContext;
  private GenericObjectPool webServiceClientStubPool;
  // the web service client stub created using the wsdl2java tool
  private Class webServiceClientStub;
  
  private int webServiceCallTimeOutMillis = 60000;
  
  public int getWebServiceCallTimeOutMillis()
	{
		return webServiceCallTimeOutMillis;
	}

	public void setWebServiceCallTimeOutMillis(int webServiceCallTimeOutMillis)
	{
		this.webServiceCallTimeOutMillis = webServiceCallTimeOutMillis;
	}

  /**
   * The web service client stub pool
   */
  private class WebServiceStubObjectFactory implements PoolableObjectFactory
  {
    
    public Object makeObject()
    {
      Stub clientStub;
      try
      {
        clientStub = makeClientWebServiceStub();
        return clientStub;
      }
      catch (AxisFault e)
      {
        logger.error("Failed to instantiate the web service client stub",e);
        throw new RuntimeException("Failed to instantiate the web service client stub",e);
      }
    }

    public void destroyObject(Object object)
    {
    }

    public boolean validateObject(Object object)
    {
      return true;
    }

    public void activateObject(Object object)
    {
    }

    public void passivateObject(Object object)
    {
    }
  }

  /**
   * Required constructor if you want to install modules into the AXIS2 client stub
   * in a standalone environment (useful for testing your web service outside a web container).
   * This constructor should only be used if this client web service is running in a
   * standalone environment (i.e. non-web) and you need the ability to add on AXIS2
   * modules.
   * For simplicity we assume the axis2.xml file resides in the root of the repository.
   * @param absolutePathToRepository
   */
  public Axis2ClientWebService1(String absolutePathToRepository)
  {
    logger.info("Creating AXIS2 configuration from the axis2.xml configuration file in location "+absolutePathToRepository);
    try
    {
      if (!absolutePathToRepository.endsWith("/") && !absolutePathToRepository.endsWith("\\"))
      {
        absolutePathToRepository = absolutePathToRepository+File.separator;
      }
      configurationContext = ConfigurationContextFactory.createConfigurationContextFromFileSystem(absolutePathToRepository, absolutePathToRepository+"axis2.xml");
    }
    catch (Exception e)
    {
      throw new RuntimeException("Failed to create the AXIS2 configuration context",e);
    }
  }

  /**
   * Required constructor if you DO NOT need to install modules into the AXIS2 client stub
   * in a standalone environment (useful for testing your web service outside a web container without any AXIS2 modules).
   * This constructor should only be used if this client web service is running in a
   * standalone environment (i.e. non-web)
   */
  public Axis2ClientWebService1()
  {
    logger.info("Creating default AXIS2 configuration from the axis2_default.xml configuration file contained in the axis2.jar");
    try
    {
      configurationContext = ConfigurationContextFactory.createDefaultConfigurationContext();
    }
    catch (Exception e)
    {
      throw new RuntimeException("Failed to create the AXIS2 configuration context");
    }
  }

  /**
   * We assume the repository is located relative to the root of the web application
   * given by the relativePath parameter. The axis2.xml file must exist at
   * <p>
   * [web root directory]/relativePath/axis2.xml
   * </p>
   * Hence modules expect to be installed at [web root directory]/relativePath/modules.
   * 
   * @param servletContext - required to determine the root directory of the web application
   * @param relativePath - the relativePath from the web root directory (must have a leading slash)
   */
  public Axis2ClientWebService1(ServletContext servletContext, String relativePath)
  {
    if (!relativePath.endsWith("/") && !relativePath.endsWith("\\"))
    {
      relativePath = relativePath+File.separator;
    }
    String absolutePathToRepository = servletContext.getRealPath("/")+relativePath;
    logger.info("Creating AXIS2 configuration from the "+absolutePathToRepository+"axis2.xml configuration file");
    File axis2xml = new File(absolutePathToRepository+"axis2.xml");

    if (axis2xml.isFile())
    {
      logger.error("Could not found the axis2.xml at "+absolutePathToRepository);
      throw new RuntimeException("Could not found the axis2.xml at "+absolutePathToRepository);
    }
    try
    {
      configurationContext = ConfigurationContextFactory.createConfigurationContextFromFileSystem(absolutePathToRepository, absolutePathToRepository+"axis2.xml");  
    }
    catch (Exception e)
    {
      throw new RuntimeException("Failed to create the AXIS2 configuration context");
    }
  }

  /**
   * intialize the web service client
   */
  public void intialize()
  {
    if (webServiceClientStub == null)
    {
      throw new RuntimeException("The webServiceClientStub class has not been set");
    }
    if (targetEndPointURL == null)
    {
      throw new RuntimeException("The targetEndPointURL has not been set");
    }
    logger.info("Creating web service client stub object pool for client stub "+webServiceClientStub.getName());
    webServiceClientStubPool = new GenericObjectPool(new WebServiceStubObjectFactory());
    webServiceClientStubPool.setMaxActive(clientStubPoolSize);
    webServiceClientStubPool.setMaxIdle(clientStubPoolSize);
    webServiceClientStubPool.setMinIdle(0);
  }

  /**
   * For each call will create a new web service client stub as specified by {@link #setWebServiceClientStub}
   * @return
   * @throws AxisFault
   */
  private Stub makeClientWebServiceStub() throws AxisFault
  {
    try
    {
      Stub stub = (Stub)webServiceClientStub.getConstructor(new Class[] {ConfigurationContext.class, String.class})
             .newInstance(new Object[] {configurationContext, targetEndPointURL});

      Options options = stub._getServiceClient().getOptions();
      options.setTimeOutInMilliSeconds(webServiceCallTimeOutMillis);
      stub._getServiceClient().setOptions(options);
      
      return stub;
    }
    catch (NoSuchMethodException e)
    {
      throw new RuntimeException("Could not find the constructor (configurationContext, targetEndPointURL) on the web service client stub",e);
    }
    catch (IllegalAccessException e)
    {
      throw new RuntimeException("Failed to access the web service client stub",e);
    }
    catch (InstantiationException e)
    {
      throw new RuntimeException("Could not instantiate the web service client stub",e);
    }
    catch (InvocationTargetException e)
    {
      throw new RuntimeException("The web service client stub instantiation threw an exception",e);
    }
  }

  /**
   * Obtain a client stub from the pool. If the pool is exhausted then the thread will be blocked
   * until one is available
   * @return
   */
  public Stub borrowWebServiceClientStub()
  {
    logger.info("Borrowing client stub from pool");
    Stub stub = null;
    try
    {
      stub = (Stub)webServiceClientStubPool.borrowObject();
      return stub;
    }
    catch (Exception e)
    {
      logger.error("Failed to obtain a web service client stub from the web service client object pool",e);
      // return stub so we do not leak objects if an unexpected error occurs
      if (stub != null)
      {
        try {webServiceClientStubPool.returnObject(stub);} catch(Exception ignore){}
      }
      throw new RuntimeException("Failed to obtain a web service client stub from the web service client object pool",e);
    }
  }
  
  /**
   * Return the client stub to the object pool
   * @param stub
   */
  public void returnWebServiceClientStub(Stub stub)
  {
    logger.info("Returning client stub to pool");
    try {webServiceClientStubPool.returnObject(stub);} catch(Exception ignore){}
  }

  public void setTargetEndPointURL(String targetEndPointURL)
  {
    this.targetEndPointURL = targetEndPointURL;
  }

  public void setClientStubPoolSize(int clientStubPoolSize)
  {
    this.clientStubPoolSize = clientStubPoolSize;
  }

  public void setWebServiceClientStub(Class webServiceClientStub)
  {
    this.webServiceClientStub = webServiceClientStub;
  }
    
}
