/////////////////////////////////////////////////////////////////////////
//
// © University of Southampton IT Innovation Centre, 2004
//
// Copyright in this library belongs to the IT Innovation Centre of
// 2 Venture Road, Chilworth Science Park, Southampton, SO16 7NP, UK.
//
// This software may not be used, sold, licensed, transferred, copied
// or reproduced in whole or in part in any manner or form or in or
// on any media by any person other than in accordance with the terms
// of the Licence Agreement supplied with the software, or otherwise
// without the prior written consent of the copyright owners.
//
// This software is distributed WITHOUT ANY WARRANTY, without even the
// implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE, except where stated in the Licence Agreement supplied with
// the software.
//
//      Created by:             Stuart E. Middleton
//      Created date:           2004/04/30
//      Created for project:    GEMSS
//
/////////////////////////////////////////////////////////////////////////
//
//      Dependencies: None
//
/////////////////////////////////////////////////////////////////////////
//
//      Last commit info:       $Author: $
//                              $Date: $
//                              $Revision: $
//
/////////////////////////////////////////////////////////////////////////

package uk.ac.soton.itinnovation.gemss.proxies;

import eu.gemss.*;
import eu.gemss.components.transport.*;
import eu.gemss.components.transport.servicedescription.*;
import eu.gemss.components.transport.servicedescription.policytypes.PolicyTypeDescriptor;
import eu.gemss.components.transport.payload.*;
import eu.gemss.components.proxies.*;
import eu.gemss.components.*;

import uk.ac.soton.itinnovation.gemss.state.*;
import uk.ac.soton.itinnovation.gemss.xmlobject.*;
import uk.ac.soton.itinnovation.gemss.transportmessaging.*;
import uk.ac.soton.itinnovation.gemss.transportmessaging.servicedescription.*;
import uk.ac.soton.itinnovation.gemss.transportmessaging.proxies.gemss.*;

import java.lang.*;
import java.util.*;
import java.io.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import uk.ac.soton.itinnovation.gemss.transportmessaging.servicedescription.policytypes.NoPolicyImp;

/**
 * Utility component to provide a non-blocking interface to the transport and messaging component.
 * It sopports handles and serialization, so invocations can be interrupted, serialized and
 * resumed at a later date (invoker will re-establish the connection).
 */
public class WebServiceInvokerImp extends Object implements Serializable, Terminable, WebServiceInvoker {

	// member variables
	private Hashtable mhashStates;
	private Hashtable mhashThreads;
	private int mnNextProxyHandle = 1;

	// private flags for use with in-line thread
	private final static String mstaticInvocationStartedFlag = "InvocationStarted";
	private final static String mstaticInvocationFinishedFlag = "InvocationFinished";
	private final static String mstaticInvocationFailedFlag = "InvocationFailed";
	private final static String mstaticServiceDescLabel = "ServiceDesc";
	private final static String mstaticPayloadInLabel = "PayloadIn";
	private final static String mstaticPayloadOutLabel = "PayloadOut";
	private final static String mstaticIDLabel = "CallerID";
	private final static String mstaticNullIDLabel = "No_ID_Set";
	private final static String mstaticFilenameMap = "MapFilenames";

	// logger variable etc
	private Logger mlogger = Logger.getLogger("uk.ac.soton.itinnovation.gemss.proxies.webserviceinvokerimp");
	private final static String mstaticTMComponentName = "eu.gemss.components.transport.GEMSSTransportAndMessaging";

	/**
	 * constructor
	 */
	public WebServiceInvokerImp()
	{
		super();

		// init vectors
		mhashStates = new Hashtable();
		mhashThreads = new Hashtable();

		// all done
		return;
	}

	/**
	 * destructor
	 */
	public void finalize() throws Throwable
	{
		// clear the contents of the hash tables
		mhashStates.clear();
		mhashThreads.clear();

		// all done
		return;
	}

	/**
	 * Signal to kill on-going threads but do not wait for them to finish. If state is required to be saved
	 * then the serialization functions should have been called prior to this method.
	 */
	synchronized public void terminate()
	{
		Enumeration enumKeys;
		WSInvocationThread thread;

		try
		{
			// kill any on-going threads
			enumKeys = mhashThreads.keys();
			while( enumKeys.hasMoreElements() ) {
				// get thread and set the stop flag
				thread = (WSInvocationThread) mhashThreads.get( enumKeys.nextElement() );
				thread.flagStop();
			}

			// all done
			return;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during terminate",ex);
			return;
		}
	}

	/**
	 * Invoke a service, returning a proxy handle to the invocation. A return of null indicates a failure.
	 * This method will support payloads up to a few KByte using the SOAP invokeService methods provided
	 * by the transport component.
	 * @param serviceDescription description of the web service we want to invoke
	 * @param payload payload containing the operation and arguments for this invocation
	 * @param strID caller supplied ID so that web service invocations can be identified later if serialized
	 * then de-serialized. The handles are dynamic and will change for each serialized web service proxy object,
	 * so the supplied ID is the only persistent identifier for a servive invocation call.
	 * @return handle to the thread dealign with this invocation
	 */
	synchronized public String invokeService( ServiceDescription serviceDescription, Payload payload, String strID )
	{
		String strProxyHandle;
		State invocationState;
		WSInvocationThread invocationThread;

		try
		{
			// make a new invocation state object
			invocationState = new State();
			strProxyHandle = getNextProxyHandle();
			mhashStates.put( strProxyHandle,invocationState );

			// create a thread to manage the web service invocation and start it
			invocationThread = new WSInvocationThread();
			invocationThread.init( invocationState, this );
			mhashThreads.put( strProxyHandle,invocationThread );

			// init the state
			invocationState.addObject( mstaticServiceDescLabel, serviceDescription );
			invocationState.addObject( mstaticPayloadInLabel, payload );
			if( strID == null )
				invocationState.addObject( mstaticIDLabel, mstaticNullIDLabel );
			else
				invocationState.addObject( mstaticIDLabel, strID );

			// start thread
			invocationThread.start();
			invocationState.addFlag( mstaticInvocationStartedFlag );

			// return a handle to identify this invocation
			return strProxyHandle;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during invokeService",ex);
			return null;
		}
	}

	/**
	 * Invoke a service, returning a proxy handle to the invocation. A return of null indicates a failure.
	 * This method will support payloads up to a few KByte AND up to 2GByte files using the SOAP attachment
	 * invokeService methods provided by the transport component.
	 * @param serviceDescription description of the web service we want to invoke
	 * @param payload payload containing the operation and arguments for this invocation
	 * @param strID caller supplied ID so that web service invocations can be identified later if serialized
	 * then de-serialized. The handles are dynamic and will change for each serialized web service proxy object,
	 * so the supplied ID is the only persistent identifier for a servive invocation call.
	 * @param mapFilenames Map of the filenames required by this operation invocation. This map will hold a number of
	 * <tag> <filename> entries with the appropriate operation's parameter tag's all filled out. If these are wrong then
	 * the invocation will throw an exeception. Note: the map instance MUST be seializable like the HashMap.
         * @return handle to the thread dealign with this invocation
	 */
	synchronized public String invokeService( ServiceDescription serviceDescription, Payload payload, String strID, Map mapFilenames )
	{
		String strProxyHandle;
		State invocationState;
		WSInvocationThread invocationThread;

		try
		{
			// make a new invocation state object
			invocationState = new State();
			strProxyHandle = getNextProxyHandle();
			mhashStates.put( strProxyHandle,invocationState );

			// create a thread to manage the web service invocation and start it
			invocationThread = new WSInvocationThread();
			invocationThread.init( invocationState, this );
			mhashThreads.put( strProxyHandle,invocationThread );

			// init the state
			invocationState.addObject( mstaticServiceDescLabel, serviceDescription );
			invocationState.addObject( mstaticPayloadInLabel, payload );
			invocationState.addObject( mstaticFilenameMap, mapFilenames );
			if( strID == null )
				invocationState.addObject( mstaticIDLabel, mstaticNullIDLabel );
			else
				invocationState.addObject( mstaticIDLabel, strID );

			// start thread
			invocationThread.start();
			invocationState.addFlag( mstaticInvocationStartedFlag );

			// return a handle to identify this invocation
			return strProxyHandle;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during invokeService",ex);
			return null;
		}
	}

	/**
	 * Get the current status of an invocation. A null return indicates an invalid handle.
	 * @param strProxyHandle Handle identifying a particular invocation
	 * @return vector of flag strings describing the current status of the invocation
	 */
	synchronized public Vector getInvocationFlags( String strProxyHandle )
	{
		Vector vectorStatus;
		State state;
		WSInvocationThread thread;

		try
		{
			// get the state of the invocation
			state = (State) mhashStates.get( strProxyHandle );
			if( state == null )
				return null;

			// create a status report (started, finished or failed)
			vectorStatus = new Vector();

			// if statements are in order since finished flag is more progressive than started flag
			if( state.checkFlag( mstaticInvocationFailedFlag ) ) {
				// failed
				vectorStatus.addElement( WebServiceInvoker.mstaticStatusValueFailed );	// failed
			}
			else if( state.checkFlag( mstaticInvocationFinishedFlag ) ) {
				// finished pending result download
				vectorStatus.addElement( WebServiceInvoker.mstaticStatusValueFinished );// finished
			}
			else if( state.checkFlag( mstaticInvocationStartedFlag ) ) {
				// started and thus on-going
				vectorStatus.addElement( WebServiceInvoker.mstaticStatusValueStarted );	// started
			}
			else {
				// error
				mlogger.log( Level.WARNING,"Unknown invocation state, defaulting to failed state");
				vectorStatus.addElement( WebServiceInvoker.mstaticStatusValueFailed );	// no flags at all
			}

			// return the status report
			return vectorStatus;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during invokeInvocationStatus",ex);
			return null;
		}
	}

	/**
	 * Return the payload, throw a GridException or return null if the invocation is still running. This method
	 * should be called once the caller knows the invocation has finished or failed. This method will return the
	 * payload or exception and then removes all trace of the invocation, including its handle, from memory.
	 * The result objects must thus be kept and used by the caller or lost forever. The payload can be of zero length
	 * depending on the service return value.
	 * If no GridException was generated when the invocation failed, then null will be returned instead. If the handle
	 * is invalid null will also be returned.
	 * @param strProxyHandle Handle identifying a particular invocation
	 * @return Payload object containing the result of the invocation
	 */
	synchronized public Payload getInvocationResult( String strProxyHandle ) throws GridException
	{
		State invocationState;
		Payload payload;
		WSInvocationThread thread;
		GridException gridException;

		try
		{
			// get the state of the invocation
			invocationState = (State) mhashStates.get( strProxyHandle );
			thread = (WSInvocationThread) mhashThreads.get( strProxyHandle );

			// return null if we have a duff handle
			if( invocationState == null || thread == null ) return null;

			// get invocation result
			if( invocationState.checkFlag( mstaticInvocationFinishedFlag ) ) {
				// invocation finished, return the payload
				payload = (Payload) invocationState.getObject( mstaticPayloadOutLabel );
				gridException = null;
			}
			else if( invocationState.checkFlag( mstaticInvocationFailedFlag ) ) {
				// invocation failed, return the exception (if any)
				payload = null;
				gridException = thread.getGridException();
			}
			else {
				// invocation no yet finished so return null and let it all continue
				return null;
			}

			// stop thread and remove it (if still active) as well as the state object
			if( thread.isAlive() ) {
				thread.flagStop();
			}
			mhashThreads.remove( strProxyHandle );
			mhashStates.remove( strProxyHandle );
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during invokeInvocationResult",ex);
			return null;
		}

		// outside main exception handlier to allow the throwing of a GridException
		// throw the GridException if it failed
		if( gridException != null ) throw gridException;

		// Return payload (or null if there is no exception instance to throw).
		// The caller should know if the invocation worked by prior looking at the
		// getInvocationFlags return vector
		return payload;
	}

	/**
	 * Returns a hash table active proxy handles and states. Each object in the hash table is a
	 * currently active handle to a web service invocation. The key associated with the
	 * object is ID string (provided as a parameter to a invokeService call)
	 * @return Hash table where each key is an ID string and each object is an active invocation handle
	 */
	synchronized public Hashtable getActiveHandles()
	{
		Hashtable hash;
		Enumeration enum;
		String strHandle, strID;
		State state;

		try
		{
			// make result hash
			hash = new Hashtable();

			// loop on current state objects
			enum = mhashStates.keys();
			while( enum.hasMoreElements() ) {
				// get handle and state
				strHandle = (String) enum.nextElement();
				state = (State) mhashStates.get( strHandle );

				// get the ID (can be null if caller does not ever want to serialize)
				strID = (String) state.getObject( mstaticIDLabel );

				// add to result hash
				hash.put( strID,strHandle );
			} // next state object

			// return result hash
			return hash;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during getActiveHandles",ex);
			return null;
		}
	}

	/**
	 * Checks a handle to see if its still valid and has a thread handler running. A handle
	 * might become invalid if the results are downloaded or the invocation fails and the
	 * getStatus method returns the failure. Both these cases will remove the handle since
	 * the invocation has both ended and been reported back to the caller in some way.
	 * @param Handle to check for validity
	 * @return True if the handle is ok
	 */
	synchronized public boolean checkHandleIsValid( String strHandle )
	{
		if( strHandle == null || mhashStates == null )
	              return false;
		return mhashStates.containsKey( strHandle );
	}

	/**
	 * Returns a new and unique proxy handle
	 * @return Handle string
	 */
	synchronized private String getNextProxyHandle()
	{
		return String.valueOf( mnNextProxyHandle++ );
	}

	/**
	 * Write state and contents to a stream. Each state object will be written to the stream. The threads are
	 * not killed by this function, so should be terminated explicitly using a call to terminate().
	 * @param Output stream where writes should go
	 */
	synchronized private void writeObject(java.io.ObjectOutputStream out) throws IOException
	{
		int i;
		Enumeration enumKeys;

		try
		{
			// write the number of state objects
			out.writeInt( mhashStates.size() );

			// write the state objects to the stream
			enumKeys = mhashStates.keys();
			while( enumKeys.hasMoreElements() ) {
				out.writeObject( mhashStates.get( enumKeys.nextElement() ) );
			}

			// all done
			return;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during writeObject",ex);
			throw new IOException("Failed to serialize state object");
		}
	}

	/**
	 * read state and contents from a stream. Reads state objects and makes a thread for each of them.
	 * The proxy handle for each thread will not be the same as the old one. Apps should ask for the set
	 * of active proxies then get thier state to find out what they are doing. Apps should not save
	 * proxy handles and re-use them after serialization.
	 * Old invocation threads (if any) must be closed before this function is called, otherwise the
	 * handles to them will be lost.
	 * @param Input stream where to read from
	 */
	synchronized private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException
	{
		int i, nStates;
		WSInvocationThread thread;
		State state;
		String strKey;

		// init (no constructor)
		mlogger = Logger.getLogger("uk.ac.soton.itinnovation.gemss.webserviceproxyimp.webserviceproxyimp");

		try
		{
			// make the hash tables since constructor will not be called
			if( mhashStates == null )
				mhashStates = new Hashtable();
			mhashStates.clear();
			if( mhashThreads == null )
				mhashThreads = new Hashtable();
			mhashThreads.clear();

			// again, constractor not called so initialize the handle count
			mnNextProxyHandle = 1;

			// read the number of states
			nStates = in.readInt();

			// read all the states
			for( i=0;i<nStates;i++ ) {
				// get state
				state = (State) in.readObject();

				// record state into hash table
				strKey = getNextProxyHandle();
				mhashStates.put( strKey,state );

				// make a thread and start it from this state
				thread = new WSInvocationThread();
				thread.init( state,this );

				// add thread to hash table
				mhashThreads.put( strKey,thread );

				// start thread
				thread.start();
			}

			// all done
			return;
		}
		catch ( Exception ex )
		{
			mlogger.log( Level.SEVERE,"Exception during readObject",ex);
			throw new IOException("Failed to serialize state object");
		}
	}

	/**
	 * In-line thread class to handle the running of an invocation using the transport and security
	 * module. This is where the action really happens.
	 */
	class WSInvocationThread extends Thread
	{
		private State mstate = null;
		private WebServiceInvoker minvoker = null;
		private boolean mbStopFlag = false;
		private GridException mgridException = null;

		/**
		 * constructor
		 */
		public WSInvocationThread()
		{
			super();

			// all done
			return;
		}

		/**
		 * Init the thread, noting the state and proxy instance calling it
		 * @param invocationState state object assigned to this thread
		 * @param invoker object that created this thread
		 */
		public void init( State invocationState, WebServiceInvokerImp invoker )
		{
			// setup member variables prior to the thread's run
			mstate = invocationState;
			minvoker = invoker;
		};


		/**
		 * Flag that the thread should stop
		 */
		public void flagStop()
		{
			// set the stop flag
			mbStopFlag = true;

			// info
			mlogger.log( Level.INFO,"WSInvocationThread stop flag set");

			// stop what it's currently doing so thread can end
			interrupt();
		};

		/**
		 * Return any GridException set during invocation (will be null if none
		 * was set
		 * @return GridException or null if none were thrown during invocation
		 */
		public GridException getGridException()
		{
			return mgridException;
		};

		/**
		 * Run's the thread that will process the invocation from start to finish
		 */
		public void run()
		{
			InvocationInput input;
			InvocationOutput output;
			Payload payload;
			ServiceDescriptionImp serviceDesc;
			ComponentManager componentManager;
			GEMSSTransportAndMessaging transportAndMessaging;
			GEMSSTransportMessagingProvider provider;
			Map mapFilenames;
			String strTest;
                        //fix for apache class-loading problem - DJM 16-3-04
                        this.setContextClassLoader(this.getClass().getClassLoader());

			try
			{
                        // Stuart has copied this in as a temp fix before CVS merge
                        // fix for apache class-loading problem - DJM 16-3-04
                        //this.setContextClassLoader(this.getClass().getClassLoader());
                        // End of copied text

				// test code
			/*
                          strTest = System.getProperty("gemss.test");
				if( strTest == null || strTest.equals("true") ) {
					provider = new GEMSSTransportMessagingProvider();
					transportAndMessaging = provider.createTransportMessaging();
					mlogger.log( Level.INFO,"Test variable SET, using transport provider directly");
				}
				else {
					// get transport component
					componentManager = GEMSS.getInstance();
					transportAndMessaging = (GEMSSTransportAndMessaging) componentManager.getInstance( mstaticTMComponentName );
					if( transportAndMessaging == null ) {
						throw new Exception("Null transport component" );
					}
				}
                          */

                          // get transport component
                                        componentManager = GEMSS.getInstance();
                                        transportAndMessaging = (GEMSSTransportAndMessaging) componentManager.getInstance( mstaticTMComponentName );
                                        if( transportAndMessaging == null ) {
                                                throw new Exception("Null transport component" );
					}
				// test flag
				mlogger.log( Level.INFO,"WSInvocationThread start");

				// check for early stop
				if( mbStopFlag ) {
					mlogger.log( Level.INFO,"WSInvocationThread finished early in response to stop flag");
					return;	// terminate flag set
				}

				// if not finished, start web service invocation
				if( !mstate.checkFlag( mstaticInvocationFinishedFlag ) ) {
					// get payload and service description from the state
					payload = (Payload) mstate.getObject( mstaticPayloadInLabel );
					serviceDesc = (ServiceDescriptionImp) mstate.getObject( mstaticServiceDescLabel );

					// get the file map if there is one (null if not)
					mapFilenames = (Map) mstate.getObject( mstaticFilenameMap );

					// make invocation input object
					input = new InvocationInputImp( payload );

                                        // invoke the service and get the output (this is a blocking call)
					PolicyTypeDescriptor noPolicy = (new NoPolicyImp()).getPolicyTypeDescriptor();
                                        if( mapFilenames == null ) {
                                          if(serviceDesc.getServicePolicy().getOrderedOutgoingPolicies()[0].getPolicyTypeDescriptor().isEqual(noPolicy)
                                              && serviceDesc.getServicePolicy().getOrderedIncomingPolicies()[0].getPolicyTypeDescriptor().isEqual(noPolicy))
						output = transportAndMessaging.invokeService( serviceDesc,input,true );
                                          else
                                            output = transportAndMessaging.invokeService( serviceDesc,input,false );
					}
					else {
                                           if(serviceDesc.getServicePolicy().getOrderedOutgoingPolicies()[0].getPolicyTypeDescriptor().isEqual(noPolicy)
                                               && serviceDesc.getServicePolicy().getOrderedIncomingPolicies()[0].getPolicyTypeDescriptor().isEqual(noPolicy))
						output = transportAndMessaging.invokeService( serviceDesc,input,mapFilenames,true );
                                          else
                                            output = transportAndMessaging.invokeService( serviceDesc,input,mapFilenames,false );
					}

					// check to see if we have terminated before blocking call returns
					// if so abort (objects probably already serialized etc.)
					if( mbStopFlag ) {
						mlogger.log( Level.INFO,"WSInvocationThread finished early in response to stop flag");
						return;	// terminate flag set
					}

					// output can be null for test  service descriptions objects
					// (errors in invokeService will generate an exception)
					// since it is ONLY appropriate to use dummy objects for testing the
					// behaviour is to wait 5 seconds for a test delay (useful for the test class)
					// if the output is null [Stuart Oct03]
					if( output != null ) {
						// get the payload from the output and add to the state object
						payload = output.getPayload();
						mstate.addObject( mstaticPayloadOutLabel, payload );
					}
					else { // see above - test code only here
						try { this.sleep(5000); } catch( InterruptedException ie ) {;}
						if( mbStopFlag ) {
							mlogger.log( Level.INFO,"WSInvocationThread finished early in response to stop flag");
							return;	// terminate flag set
						}
					}

					// flag that the service invocation has finished
					mstate.addFlag(mstaticInvocationFinishedFlag);

					// info
					mlogger.log( Level.INFO,"WSInvocationThread invokeService complete, result payload ready");

				} // service finished

				// test flag
				mlogger.log( Level.INFO,"WSInvocationThread finished");
			}
			catch ( GridException gridex ) {
				// for GridExceptions record the exception instance so it can be thrown back to the
				// caller
				mlogger.log( Level.SEVERE,"Exception WSInvocationThread.run",gridex);
				mgridException = gridex;
				// set failed flag
				mstate.addFlag(mstaticInvocationFailedFlag);
			}
			catch ( Exception ex )
			{
				mlogger.log( Level.SEVERE,"Exception WSInvocationThread.run",ex);
				// set failed flag
				mstate.addFlag(mstaticInvocationFailedFlag);
			}
                  catch ( Throwable th )
                  {
                        // should not need this but I am getting linkage errors and stuff like that :( stuart Apr04
                        mlogger.log( Level.SEVERE,"Throwable WSInvocationThread.run",th);
                        // set failed flag
                        mstate.addFlag(mstaticInvocationFailedFlag);
                  }
		};

	} // end of WSInvocationThread
} // end of WebServiceInvokerImp
