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

import java.io.IOException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.jboss.remoting.loading.ClassBytes;
import org.jboss.remoting.loading.ClassRequestedMethodInvocationResult;
import org.jboss.remoting.loading.ClassRequiredMethodInvocation;
import org.jboss.remoting.loading.ClassUtil;

/**
 * ClientInvoker is an abstract client part handler that implements the bulk of the heavy
 * lefting to process a remote method and dispatch it to a remote ServerInvoker and handle the result. <P>
 *
 * The ClientInvoker in particular handles the loading of remote classes necessary for execution of invocations,
 * as well as the remote loading of remote classes necessary to unmarshal the results or parameters of a result
 * returned from a ServerInvoker. <P>
 *
 * Specialized Client/Server Invokers might add additional functionality as part of the invocation - such as
 * delivering queued notifcations from a remote server by adding the notification objects during each invocation
 * to the invocation result payload and then having the client re-dispatch the notifications locally upon
 * receiving the return invocation result.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @version $Revision: 1.10.2.1 $
 */
public abstract class ClientInvoker extends AbstractInvoker
{
    protected boolean connected=false;
    protected InvokerLocator localServerLocator;

    public ClientInvoker (ClassLoader cl, InvokerLocator locator)
    {
        super (locator);

        localServerLocator = InvokerRegistry.getSuitableServerLocatorForRemote(locator);
    }

    /**
     * This should be set when want to override the default behavior of automatically
     * getting s suitable locator.  This should be used want want to control what type
     * of callbacks to receive (pull or push).  Set to null to poll for callback messages.
     * This can also be used to receive callbacks using another transport and subsystem,
     * if desired.
     * @param locator
     */
    public void setClientLocator(InvokerLocator locator)
    {
        localServerLocator = locator;
    }

    /**
     * Gets the client locator.  This locator will be used by the server side
     * to make callbacks to the handler for this locator.
     * @return
     */
    public InvokerLocator getClientLocator()
    {
        return localServerLocator;
    }

    /**
     * transport a request against a remote ServerInvoker
     *
     * @param in
     * @return
     * @throws Throwable
     */
   public Object invoke (InvocationRequest in)
            throws Throwable
    {
       Object invocation = new RemoteMethodInvocation(
          in.getSessionId(),
          in.getSubsystem(),
          in.getParameter(),
          in.getRequestPayload(),
          localServerLocator);
        RemoteMethodInvocation originalInvocation = (RemoteMethodInvocation) invocation;
        ClassBytes neededClass = null;
        ClassBytes resultBytes = null;
        Object value = null;
        Set classAttempts = null;
        Map receivedPayload = null;
        Object returnValue = null;
        int invokeCount = 0;

        try
        {
            // we want to continue invocation since the pending invocation may fail if the remote
            // side or the local side needs as a class to complete the invocation.  in this case,
            // a) if a remote end needs a class, the local side will find the class bytes and return
            // them to the remote end, allowing him to finish the invocation (or fail), or
            // b) if the local end needs a class, the local side will transport a special invocation to
            // tell the remote end to give us the needed class bytes, and, if found, will add the
            // classbytes local and attempt re-invocation.

            // this cycle may need to occur several times in the case that one class causes a dependency
            // resolution to another... in which case the cycle will continue until either:
            // 1) invocation completes successfully or fails normally, or;
            // 2) a class on either side cannot be found, in which case a ClassNotFoundException is raised

            while (true)
            {
                if (logExtra && log.isDebugEnabled())
                {
                    log.debug((++invokeCount)+") invoking =>"+invocation+" with parameter: "+in.getParameter());
                }
                // serialize invocation into buffer
                byte send[] = ClassUtil.serialize (invocation);

                try
                {
                    // send to remote end
                   byte receive[] = transport (in.getSessionId(), send);

                    // deserialize result buffer into invocation result
                    Object obj = ClassUtil.deserialize (receive, classbyteloader);

                    if (logExtra && log.isDebugEnabled())
                    {
                        log.debug("received result=>"+obj);
                    }

                    RemoteMethodInvocationResult result = null;

                    if (obj instanceof ClassRequiredMethodInvocation)
                    {
                        value = obj;
                    }
                    else
                    {
                        result = (RemoteMethodInvocationResult) obj;

                        // if we have return payload, process it
                        receivedPayload = result.getPayload ();
                        if (receivedPayload != null)
                        {
                            // process any payload data
                           preProcess (in.getSessionId(), in.getParameter(), in.getRequestPayload(), receivedPayload);
                        }

                        if (result instanceof ClassRequestedMethodInvocationResult)
                        {
                            // pull out the needed class, we'll use it again below in the re-unmarshalling of the
                            // previous invocation result
                            neededClass = ((ClassRequestedMethodInvocationResult) result).getRequestedClass ();
                        }
                        else
                        {
                            // get the result value
                            resultBytes = result.getResult ();
                            if (resultBytes == null)
                            {
                                // transport succeeded, and it returned null, return null
                                returnValue = null;
                                break;
                            }
                        }
                        // we either have a return value or we have an exception
                        // we need to first attempt deserialization

                        // this could be a re-unmarshalling in the case we failed the first (or subsequent)
                        // time and we requested remote class bytes - in which case the needed class will be passed
                        // as the second argument. in the first attempt, the needed class will be null
                        if (resultBytes==null)
                        {
                            classbyteloader.addClass(neededClass);
                            invocation = originalInvocation;
                            if (logExtra && log.isDebugEnabled())
                            {
                                log.debug("received needed class: "+neededClass+", reinvoking: "+invocation);
                            }
                            continue;
                        }
                        value = unmarshal (resultBytes, neededClass);
                    }
                    if (value instanceof ClassRequiredMethodInvocation)
                    {
                        // OK, so we sent an invocation to the server and it contains parameters that
                        // he can't deserialize because he doesn't have the classes locally, so we need to
                        // provide them back to him so he can added them and attempt the invocation again
                        // with the proper classes
                        ClassRequiredMethodInvocation crm = (ClassRequiredMethodInvocation) value;
                        String requiredClass = crm.getClassRequiredName ();
                        byte clazz[] = getClassBytes (requiredClass);
                        if (clazz == null)
                        {
                            // we don't have it
                           invocation = new RemoteMethodInvocationResult (in.getSessionId(), new ClassNotFoundException (requiredClass), true, null);
                            continue;
                        }
                        // found it
                        invocation = new ClassRequestedMethodInvocationResult (in.getSessionId(), new org.jboss.remoting.loading.ClassBytes (requiredClass, clazz), originalInvocation);
                        continue;
                    }
                    if (result.isException ())
                    {
                        throw (Throwable) value;
                    }
                    else
                    {
                        returnValue = value;
                        break;
                    }

                }
                catch (ClassNotFoundException cnf)
                {
                    // OK, so we're attempting to deserialize the result and the result
                    // is of a class that we don't have locally, so we need to ask him for it
                    // in which case will re-loop, get the class and then re-attempt unmarshalling
                    String classNeeded = cnf.getMessage ();
                    if (classAttempts == null)
                    {
                        // to make sure we don't get in an infinite loop, if we request and the
                        // remote side throughs an exception again for the same class, just fail
                        classAttempts = new HashSet ();
                    }
                    if (classAttempts.contains (classNeeded))
                    {
                        if (logExtra && log.isDebugEnabled())
                        {
                            log.debug("class not found (client side) ... "+classNeeded+" - however, we've already asked the server for it and still don't have it!");
                        }
                        // we've already attempted to load this class, in which case we need to fail
                        throw cnf;
                    }
                    // record the fact that in this invocation context, we've attempt to remotely load
                    // so we don't get into a recursive loop trying to reload the same class over and over
                    classAttempts.add (classNeeded);
                    if (logExtra && log.isDebugEnabled())
                    {
                        log.debug("class not found (client side) ... "+classNeeded+", asking server for it");
                    }
                    // we need to get the class from the remote side
                    invocation = new ClassRequiredMethodInvocation (in.getSessionId(), classNeeded,locator);
                    continue;
                }
            }
            return returnValue;
        }
        finally
        {
           postProcess (in.getSessionId(), in.getParameter(), in.getRequestPayload(), receivedPayload);
           /*
            if (receivedPayload != null && sendPayload != null)
            {
                // sendPayload is like an IN/OUT variable - we basically clear any values and return
                // all the values in the received payload into the send so the client invoker has access
                // to them
                sendPayload.clear ();
                sendPayload.putAll (receivedPayload);
            }
           */
           in.setReturnPayload(receivedPayload);
        }
    }

    /**
     * this method is called prior to making the remote invocation to allow the subclass the ability
     * to provide additional data or modify the invocation
     *
     * @param sessionId
     * @param param
     * @param sendPayload
     * @param receivedPayload
     */
    protected void preProcess (String sessionId, Object param, Map sendPayload, Map receivedPayload)
    {
    }

    /**
     * this method is called prior to returning the result for the invocation to allow the subclass the ability
     * to modify the result result
     *
     * @param sessionId
     * @param param
     * @param sendPayload
     * @param receivedPayload
     */
   protected void postProcess (String sessionId, Object param, Map sendPayload,
                                Map receivedPayload)
    {

    }

    /**
     * called to transport the marshalled invocation to the remote destination, and receive
     * back the marshalled invocation bytes back. this method allows the subclass to provide the
     * specific mechanism, protocol, etc. for delivering and returning remote invocations in a
     * transport-specific way.
     *
     * @param sessionId session id to pass along
     * @param buffer  buffer of the serialized method invocation to send to the server
     * @return result of the method invocation as serialized bytes
     * @throws IOException fatal exception should only be raised in the case the transport write fails (but is still a valid connection)
     * @throws ConnectionFailedException this exception is raised in the case the remote side is no longer available
     */
    protected abstract byte[] transport (String sessionId, byte buffer[])
            throws IOException, ConnectionFailedException;

    /**
     * subclasses must provide this method to return true if their remote connection is connected and
     * false if disconnected.  in some transports, such as SOAP, this method may always return true, since the
     * remote connectivity is done on demand and not kept persistent like other transports (such as socket-based
     * transport).
     *
     * @return boolean true if connected, false if not
     */
    public boolean isConnected ()
    {
        return connected;
    }


    /**
     * connect to the remote invoker
     *
     * @throws ConnectionFailedException
     */
    public synchronized void connect ()
            throws ConnectionFailedException
    {
        if (!connected)
        {
            if (log.isDebugEnabled())
            {
                log.debug("connect called for: "+this);
            }
            handleConnect();
            connected=true;
        }
    }

    /**
     * subclasses must implement this method to provide a hook to connect to the remote server, if this applies
     * to the specific transport. However, in some transport implementations, this may not make must difference since
     * the connection is not persistent among invocations, such as SOAP.  In these cases, the method should
     * silently return without any processing.
     *
     * @throws ConnectionFailedException
     */
    protected abstract void handleConnect ()
            throws ConnectionFailedException;

    /**
     * subclasses must implement this method to provide a hook to disconnect from the remote server, if this applies
     * to the specific transport. However, in some transport implementations, this may not make must difference since
     * the connection is not persistent among invocations, such as SOAP.  In these cases, the method should
     * silently return without any processing.
     */
    protected abstract void handleDisconnect ();

    /**
     * disconnect from the remote invokere
     */
    public synchronized void disconnect ()
    {
        if (connected)
        {
            if (log.isDebugEnabled())
            {
                log.debug("disconnect called for: "+this);
            }
            connected=false;
            handleDisconnect();
        }
    }

    /**
     * Called by the garbage collector on an object when garbage collection
     * determines that there are no more references to the object.
     * A subclass overrides the <code>finalize</code> method to dispose of
     * system resources or to perform other cleanup.
     * <p>
     * The general contract of <tt>finalize</tt> is that it is invoked
     * if and when the Java<font size="-2"><sup>TM</sup></font> virtual
     * machine has determined that there is no longer any
     * means by which this object can be accessed by any thread that has
     * not yet died, except as a result of an action taken by the
     * finalization of some other object or class which is ready to be
     * finalized. The <tt>finalize</tt> method may take any action, including
     * making this object available again to other threads; the usual purpose
     * of <tt>finalize</tt>, however, is to perform cleanup actions before
     * the object is irrevocably discarded. For example, the finalize method
     * for an object that represents an input/output connection might perform
     * explicit I/O transactions to break the connection before the object is
     * permanently discarded.
     * <p>
     * The <tt>finalize</tt> method of class <tt>Object</tt> performs no
     * special action; it simply returns normally. Subclasses of
     * <tt>Object</tt> may override this definition.
     * <p>
     * The Java programming language does not guarantee which thread will
     * transport the <tt>finalize</tt> method for any given object. It is
     * guaranteed, however, that the thread that invokes finalize will not
     * be holding any user-visible synchronization locks when finalize is
     * invoked. If an uncaught exception is thrown by the finalize method,
     * the exception is ignored and finalization of that object terminates.
     * <p>
     * After the <tt>finalize</tt> method has been invoked for an object, no
     * further action is taken until the Java virtual machine has again
     * determined that there is no longer any means by which this object can
     * be accessed by any thread that has not yet died, including possible
     * actions by other objects or classes which are ready to be finalized,
     * at which point the object may be discarded.
     * <p>
     * The <tt>finalize</tt> method is never invoked more than once by a Java
     * virtual machine for any given object.
     * <p>
     * Any exception thrown by the <code>finalize</code> method causes
     * the finalization of this object to be halted, but is otherwise
     * ignored.
     *
     * @throws Throwable the <code>Exception</code> raised by this method
     */
    protected void finalize () throws Throwable
    {
        disconnect();
        super.finalize ();
    }
}
