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

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;

import org.jboss.remoting.ConnectionFailedException;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.RemoteClientInvoker;

/**
 * SocketClientInvoker uses Sockets to remotely connect to the a remote ServerInvoker, which
 * must be a SocketServerInvoker.
 *
 * @author <a href="mailto:jhaynie@vocalocity.net">Jeff Haynie</a>
 * @author <a href="mailto:telrod@e2technologies.net">Tom Elrod</a>
 * @version $Revision: 1.8.2.3 $
 */
public class SocketClientInvoker extends RemoteClientInvoker
{
    private static final boolean DEBUG = Boolean.getBoolean("jboss.remoting.socket.debug");
    private InetAddress addr;
    private int port;
    private SocketClientPool pool;

    public SocketClientInvoker (InvokerLocator locator)
    {
        super(locator);
        try
        {
            setup();
        }
        catch (Exception ex)
        {
            throw new RuntimeException(ex.getMessage());
        }
    }

    protected void setup ()
        throws Exception
    {
        this.addr = InetAddress.getByName(locator.getHost());
        this.port = locator.getPort();
    }

    public SocketClientPool getPool ()
    {
         return this.pool;
    }

    protected void finalize () throws Throwable
    {
        disconnect();
        super.finalize();
    }

    protected synchronized void handleConnect ()
            throws ConnectionFailedException
    {
        try
        {
            if (log.isDebugEnabled())
            {
                log.debug("Connecting to: "+addr+":"+port);
            }
            this.pool = new SocketClientPool(addr,port);
            this.pool.connect();
        }
        catch (IOException e)
        {
            this.connected = false;
            throw new ConnectionFailedException("Failed to connect to: "+this.addr+":"+this.port+". Exception message:" +e.getMessage());
        }
    }

    protected synchronized void handleDisconnect ()
    {
        if (this.pool!=null)
        {
            this.pool.disconnect();
            this.pool = null;
        }
    }

    private synchronized boolean connected ()
    {
        return connected;
    }

    /**
     * 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 byte[] transport (String sessionId, byte buffer[])
            throws IOException,ConnectionFailedException
    {
        if (connected()==false)
        {
            // attempt auto-connect
            connect();
        }
        Socket socket = null;
        try
        {
            ByteArrayOutputStream result=null;
            /**
             * Adding a retry count in the case that the socket has an unrecoverable
             * error, will retry with a new one, then if still has problem will throw
             * exception.
             */
            int retryCount = 1;
            while(retryCount >= 0 && connected())
            {
                --retryCount; // decrement for this try

                // get the next available socket client connection from the pool
                socket = pool.getNextAvailable();
                if (DEBUG)
                {
                    System.err.println(Thread.currentThread().getName()+" retrieved socket: "+socket);
                }
                int count = 0;
                synchronized(socket)
                {
                    OutputStream output = new BufferedOutputStream(socket.getOutputStream());
                    InputStream input = new BufferedInputStream(socket.getInputStream());

                    try
                    {
                       // first write the length of the read buffer
                       output.write(SocketUtil.pad(buffer.length,20));
                       // then write the buffer
                       output.write(buffer);
                       output.flush();
                    }
                    catch (SocketException se)
                    {
                       if(retryCount >= 0)
                       {
                           if (log.isDebugEnabled()) log.debug("SocketException when making call to server. Maybe its down, will retry", se);
                           if (socket!=null)
                           {
                               //discard the socket , don't put it back in the pool
                               try
                               {
                                   socket.close();
                               }
                               catch (Exception ex)
                               {
                                   if (log.isDebugEnabled()) log.debug("after failure of: "+se.getMessage()+", attempted to close socket:"+socket+", which raised: "+ex.getMessage());
                               }
                           }
                           socket=null;
                           continue;
                       }
                       else
                       {
                           // Throw a connect exception so that frameworks like clustering
                           // can retry on a separate node.
                           throw new ConnectionFailedException(se.getMessage());
                       }
                    }
                    // then read the response
                    byte lenbuf[]=new byte[20];
                    // read the response header, which contains a 20 length
                    // byte array of the length of the response buffer
                    input.read(lenbuf);
                    // calculate the length
                    int readlen = SocketUtil.len(lenbuf);
                    if (DEBUG)
                    {
                        System.err.println(Thread.currentThread().getName()+" expecting: "+readlen+" bytes");
                    }
                    byte readbuf[]=new byte[4096];
                    // create an array to write the result into
                    result=new ByteArrayOutputStream(readlen);
                    while (count<readlen)
                    {
                        // read the input
                        int c = input.read(readbuf);
                        if (DEBUG)
                        {
                            System.err.println(Thread.currentThread().getName()+" read: "+c+" bytes");
                        }
                        if (c<0)
                        {
                            if (log.isDebugEnabled())
                            {
                                log.debug("received a -1 on client invocation - raising a socket exception");
                            }
                            throw new SocketException("socket received -1 on read from remote server");
                        }
                        else
                        {
                            // write
                            result.write(readbuf,0,c);
                            count+=c;
                        }
                    }
                }
                if (DEBUG)
                {
                    System.err.println(Thread.currentThread().getName()+" read "+count+" total bytes");
                }
                result.flush();

                retryCount = -1; //have to set this so won't loop again
            }
            // return the result buffer
            return result.toByteArray();
        }
        catch (SocketException se)
        {
            if (se.getMessage().indexOf("Connection reset")>-1)
            {
                if (socket!=null)
                {
                    // close it and don't put it back in the pool
                    try
                    {
                        socket.close();
                    }
                    catch (Exception ig) { }
                    socket=null;
                }
                throw new ConnectionFailedException(se.getMessage());
            }
            throw se;
        }
        finally
        {
            if (socket!=null && this.pool!=null)
            {
                this.pool.makeAvailable(socket);
            }
        }
    }

    public String toString() {
        return "org.jboss.remoting.transport.socket.SocketClientInvoker{" +
                "addr=" + addr +
                ", port=" + port +
                ", pool=" + pool +
                "}";
    }
}
