//==============================================================================
// FTPClient.java
//==============================================================================

package tribble.net.ftp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;

import java.lang.Exception;
import java.lang.Integer;
import java.lang.NullPointerException;
import java.lang.String;
import java.lang.System;

import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;

import java.util.ArrayList;


/*******************************************************************************
* Simple FTP client.
* Allows clients to establish FTP connections, send and receive files, get
* remote directory listings, etc.
*
* <p>
* This is a simple, bare-bones, no-nonsense implementation, providing only the
* most basic FTP capabilities, and performing only minimal error checking and
* recovery.  If your FTP server is well behaved, though, this implementation
* should meet the basic needs of simple FTP applications.
*
* <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
* <p>
* <b>Usage</b>
*
* <p>
* This is a simple program that connects to an FTP server, uploads a file,
* downloads a file, then closes the connection to the server:
*
* <pre>
*    import tribble.net.ftp.*;
*
*    public class <b>MyFtpClient</b>
*    {
*        public static void <b>main</b>(String[] args)
*            throws Exception
*        {
*            {@link FTPClientI}  ftp = null;
*
*            try
*            {
*                // Connect and login to the FTP server
*                ftp = new {@link #FTPClient FTPClient}();
*                ftp.{@link #setHost setHost}("ftp.domain.net");
*                ftp.{@link #connect connect}();
*                ftp.{@link #login(String, String) login}("userid", "password");
*
*                // Upload (put) a text file
*                ftp.{@link #setTextMode setTextMode}(true);
*                ftp.{@link #setRemoteDir setRemoteDir}("incoming");
*                ftp.{@link #putFile(String, String) putFile}("file.txt", "file.txt");
*
*                // Download (get) a binary file
*                ftp.{@link #setRemoteDirUp setRemoteDirUp}();
*                ftp.{@link #setRemoteDir setRemoteDir}("outgoing");
*                ftp.{@link #setTextMode setTextMode}(false);
*                ftp.{@link #getFile(String, String) getFile}("file.dat", "file.dat");
*            }
*            catch ({@link FTPException} ex)
*            {
*                // An error occurred
*                System.out.println(ex.getMessage());
*                throw ex;
*            }
*            finally
*            {
*                // Close the FTP connection
*                if (ftp != null)
*                    ftp.{@link #disconnect disconnect}();
*            }
*        }
*    }</pre>
*
* <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
* <p>
* <b>References</b>
*
* <p>
* IETF RFC 959 - File Transfer Protocol (FTP) <br/>
* <a href=
*  "http://www.ietf.org/rfc/rfc0959.txt">www.ietf.org/rfc/rfc0959.txt</a>.
*
* <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
* <dl>
* <dt> <b>Source code:</b> </dt>
*  <dd> Available at:
*   <a href="http://david.tribble.com/src/java/tribble/net/ftp/FTPClient.java"
*    >http://david.tribble.com/src/java/tribble/net/ftp/FTPClient.java</a>
*  </dd>
* <dt> <b>Documentation:</b> </dt>
*  <dd> Available at:
*   <a href="http://david.tribble.com/docs/tribble/net/ftp/FTPClient.html"
*    >http://david.tribble.com/docs/tribble/net/ftp/FTPClient.html</a>
*  </dd>
* </dl>
*
* <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
* @version	API 2.0 $Revision: 1.29 $ $Date: 2010/07/12 21:18:03 $
* @since	API 1.0, 2001-04-14
* @author	David R. Tribble (david&#64;tribble.com).
*	<p>
*	Copyright ©2001-2010 by David R. Tribble, all rights reserved.<br/>
*	Permission is granted to any person or entity except those designated
*	by the United States Department of State as a terrorist, or terrorist
*	government or agency, to use and distribute this source code provided
*	that the original copyright notice remains present and unaltered.
*/

public class FTPClient
    extends FTPClientAdapter
    implements FTPClientI
{
    /** Revision information. */
    static final String		REV =
        "@(#)tribble/net/ftp/FTPClient.java API 2.0 $Revision: 1.29 $ $Date: 2010/07/12 21:18:03 $\n";


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Constants

    /** FTP command line terminator (newline). */
    static final String		CMD_EOLN =
        "\r\n";

    /** Local native JVM newline character sequence. */
    static final byte[]		LOCAL_NEWLINE =
        System.getProperty("line.separator").getBytes();

    /** Remote newline character sequence (CR/LF pair). */
    static final byte[]		REMOTE_NEWLINE =
        { '\r', '\n' };

    //--------------------------------------
    // FTP commands
    static final String		CMD_ABORT =		"ABOR";
    static final String		CMD_ACCOUNT =		"ACCT";
    static final String		CMD_ALLOCATE =		"ALLO";
    static final String		CMD_APPEND =		"APPE";
    static final String		CMD_CHDIR =		"CWD";
    static final String		CMD_DELETE =		"DELE";
    static final String		CMD_GET =		"RETR";
    static final String		CMD_GETDIR =		"PWD";
    static final String		CMD_HELP =		"HELP";
    static final String		CMD_LISTFILES =		"LIST";
    static final String		CMD_LISTNAMES =		"NLST";
    static final String		CMD_LOGIN =		"USER";
    static final String		CMD_LOGOUT =		"QUIT";
    static final String		CMD_MKDIR =		"MKD";
    static final String		CMD_MODE =		"MODE";
    static final String		CMD_MODE_STREAM =	"MODE S";
    static final String		CMD_MODE_BLOCK =	"MODE B";
    static final String		CMD_MODE_COMPR =	"MODE C";
    static final String		CMD_MOUNT =		"SMNT";
    static final String		CMD_NOP =		"NOOP";
    static final String		CMD_PARAMS =		"SITE";
    static final String		CMD_PASSIVE  =		"PASV";
    static final String		CMD_PASSWORD =		"PASS";
    static final String		CMD_PORT =		"PORT";
    static final String		CMD_PUT =		"STOR";
    static final String		CMD_PUT_UNIQ =		"STOU";
    static final String		CMD_REINIT =		"REIN";
    static final String		CMD_RENAME_FROM =	"RNFR";
    static final String		CMD_RENAME_TO =		"RNTO";
    static final String		CMD_RESTART =		"REST";
    static final String		CMD_RMDIR =		"RMD";
    static final String		CMD_STATUS =		"STAT";
    static final String		CMD_STRUCT =		"STRU";
    static final String		CMD_STRUCT_FILE =	"STRU F";
    static final String		CMD_STRUCT_REC =	"STRU R";
    static final String		CMD_STRUCT_PAGE =	"STRU P";
    static final String		CMD_SYSTEM =		"SYST";
    static final String		CMD_TYPE =		"TYPE";
    static final String		CMD_TYPE_ASC =		"TYPE A N";
    static final String		CMD_TYPE_BIN =		"TYPE I";
    static final String		CMD_UPDIR =		"CDUP";

    //--------------------------------------
    // FTP response code groups (first digit, code/100)
    static final short		RG_OKAY_INC =		1;
    static final short		RG_OKAY_COMPL =		2;
    static final short		RG_OKAY_PEND =		3;
    static final short		RG_FAIL_RETRY =		4;
    static final short		RG_FAIL =		5;


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Static methods

    /***************************************************************************
    * Test driver.
    *
    * <p>
    * <b> Usage </b>
    * <p>
    * <tt>
    * java tribble.net.ftp.FTPClient <i>host</i> <i>port</i>|-
    *    <i>user</i>|- <i>password</i> [-<i>action</i>...]
    * </tt>
    *
    * <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    * @param	args
    * Command line arguments.
    *
    * @see	FTPClientRun
    *
    * @since	1.1, 2001-04-14
    */
    public static void main(String[] args)
        throws Exception
    {
        FTPClientRun.run(new FTPClient(), args);
    }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Variables

    /** FTP command input stream. */
    InputStream		m_cmdIn;

    /** FTP command output stream. */
    OutputStream	m_cmdOut;

    /** FTP data input stream. */
    InputStream		m_dataIn;

    /** FTP data output stream. */
    OutputStream	m_dataOut;

    /** FTP command stream socket. */
    Socket		m_cmdSock;

    /** FTP data stream socket. */
    Socket		m_dataSock;

    /** FTP local connection host address. */
    InetAddress		m_localHost;

    /** Command I/O buffer. */
    byte[]		m_cbuf =	new byte[2*1024];

    /** Data I/O buffer. */
    byte[]		m_dbuf =	new byte[4*1024];


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Constructors

    /***************************************************************************
    * Default constructor.
    *
    * @since	1.1, 2001-04-14
    */
    public FTPClient()
    { }


// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// Methods

    /***************************************************************************
    * Connect to the remote FTP system.
    *
    * @throws	IOException
    * Thrown if unable to connect to the remote FTP system.
    *
    * @see	#disconnect disconnect()
    *
    * @since	1.1, 2001-04-14
    */
    @Override
    public void connect()
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (m_hostName == null  ||  m_hostName.length() == 0)
            throw new FTPException("FTP host name not set");

        if (m_debugOut != null)
            m_debugOut.println("$ connect: " + m_hostName
                + ":" + m_cmdPort + "/" + m_dataPort);

        // Close any previous connection
        if (m_isConnected)
            disconnect();
        m_stop = false;

        // Establish a new FTP connection
        m_cmdSock = new Socket(m_hostName, m_cmdPort);
        m_cmdSock.setSoTimeout(m_timeOut*1000);

        m_localHost = m_cmdSock.getLocalAddress();
        m_cmdIn =  m_cmdSock.getInputStream();
        m_cmdOut = m_cmdSock.getOutputStream();
        m_isConnected = true;

        if (m_debugOut != null)
            m_debugOut.println("$ localHost: " + m_localHost.toString()
                + ":" + m_cmdPort);

        // Read connection response from the remote FTP system
        resp = getResponse();
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't connect to FTP system (" + resp.m_code + "): "
                + m_hostName + ":" + m_cmdPort);
    }


    /***************************************************************************
    * Disconnect from the remote FTP system.
    * Note that this method does not throw {@link IOException}.
    *
    * @see	#connect connect()
    *
    * @since	1.1, 2001-04-14
    */
    @Override
    public void disconnect()
    {
        FTPResponse	resp;

        // Sanity checks
        if (!m_isConnected)
            return;

        // Close the data port I/O streams
        try
        {
            if (m_dataSock != null)
                closeDataPort();
        }
        catch (IOException ex)
        { }

        // Close the FTP connection
        try
        {
            resp = doCommand(CMD_LOGOUT);
        }
        catch (IOException ex)
        { }

        // Close the FTP connection
        try
        {
            // Shut down the FTP I/O streams
            m_cmdOut.close();
            m_cmdIn.close();
        }
        catch (IOException ex)
        { }

        try
        {
            // Shut down the FTP I/O streams
            m_cmdSock.close();
        }
        catch (IOException ex)
        { }

        // Clean up
        m_cmdOut =  null;
        m_cmdIn =   null;
        m_cmdSock = null;
        m_isLoggedOn =  false;
        m_isConnected = false;
        m_stop = false;

        if (m_debugOut != null)
            m_debugOut.println("$ disconnected: " + m_hostName);
    }


    /***************************************************************************
    * Log on to the remote FTP system.
    *
    * @param	user
    * FTP user-ID for the remote system.
    *
    * @param	pwd
    * User password.  This can be empty (<tt>""</tt>).
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @see	#login() login()
    * @see	#connect connect()
    *
    * @since	1.2, 2006-03-15
    */
    @Override
    public void login(String user, String pwd)
        throws IOException
    {
        FTPResponse	resp;
        int		rc;
        int		i, j;

        // Sanity checks
        if (user == null  ||  user.length() == 0)
            throw new FTPException("Null or empty FTP user-ID");
        if (pwd == null)
            throw new FTPException("Null FTP password");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        if (m_debugOut != null)
            m_debugOut.println("$ login: user='" + user + "'");

        // Log into the remote FTP system
        m_userID = user;
        resp = doCommand(CMD_LOGIN, user);

        rc = resp.m_code;
        if (rc/100 == RG_OKAY_PEND)
        {
            // Provide a required password
            m_password = pwd;
            resp = doCommand(CMD_PASSWORD, pwd);

            rc = resp.m_code;
            if (rc/100 == RG_FAIL)
                throw new FTPException(rc,
                    "Bad FTP user or password (" + rc + "): \"" + user + "\"");
        }

        if (rc/100 != RG_OKAY_COMPL  &&  rc/100 != RG_OKAY_PEND)
            throw new FTPException(rc,
                "FTP login failed (" + rc + "): \"" + user + "\"");

        m_isLoggedOn = true;
    }


    /***************************************************************************
    * Set the transfer mode to text (ASCII) or binary.
    * In text (ASCII) mode, files are transferred as text files, so that newline
    * sequences (CR, LF, or CR/LF) are converted into the local native newline
    * sequence (which is determined by the
    * <tt>System.getProperty("line.separator")</tt> setting).
    *
    * @param	flag
    * If true, the transfer mode is set to text (ASCII), otherwise it is set
    * to binary.
    *
    * @return
    * The previous mode setting.
    *
    * @since	1.26, 2007-07-26
    */
    @Override
    public boolean setTextMode(boolean flag)
    {
        boolean		prev;

        prev = m_textMode;

        try
        {
            FTPResponse	resp;

            // Set the transfer mode on the remote side
            resp = doCommand(flag ? CMD_TYPE_ASC : CMD_TYPE_BIN);
            if (resp.m_code/100 == RG_OKAY_COMPL)
                m_textMode = flag;
        }
        catch (IOException ex)
        {
            // Mode not set, leave m_textMode unchanged
        }

        return prev;
    }


    /***************************************************************************
    * Set the transfer mode to text (ASCII) or binary.
    *
    * @deprecated	(since 1.26, 2007-07-26)
    * Use {@link #setTextMode setTextMode()} instead.
    *
    * @param	flag
    * If true, the transfer mode is set to text (ASCII), otherwise it is set
    * to binary.
    *
    * @return
    * The previous mode setting.
    *
    * @since	1.3, 2006-03-15
    */
    public boolean setAsciiMode(boolean flag)
    {
        return setTextMode(flag);
    }


    /***************************************************************************
    * Ping the remote FTP system.
    * This sends a "NOOP" FTP command to the remote system and receives its
    * reply.
    *
    * @throws	IOException
    * Thrown if an error occurs, e.g., the FTP connection is broken.
    *
    * @since	1.4, 2006-03-16
    */
    @Override
    public void ping()
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        // Send a no-op command to the remote FTP system
        resp = doCommand(CMD_NOP);
    }


    /***************************************************************************
    * Retrieve the identity information of the remote FTP system.
    *
    * @param	out
    * Output stream to which the identification information is to be written.
    *
    * @throws	IOException
    * Thrown if an I/O error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * (write) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    @Override
    public void getSystemInfo(OutputStream out)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (out == null)
            return;

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        // Get the identification of the remote FTP system
        resp = doCommand(CMD_SYSTEM);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't get remote system info (" + resp.m_code + ")");

        transferCmd(resp, true, out);
    }


    /***************************************************************************
    * Retrieve the current status of the remote FTP system.
    *
    * @param	out
    * Output stream to which the status is to be written.
    *
    * @throws	IOException
    * Thrown if an I/O error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * (write) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    @Override
    public void getStatus(OutputStream out)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (out == null)
            return;

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        // Get the current status of the remote FTP system
        resp = doCommand(CMD_STATUS);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't get remote system status (" + resp.m_code + ")");

        transferCmd(resp, false, out);
    }


    /***************************************************************************
    * Get (receive) a help listing of supported FTP commands from the remote FTP
    * system.
    *
    * @param	out
    * Output stream to which the help listing is to be written.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * (write) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    @Override
    public void getHelp(OutputStream out)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (out == null)
            return;

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        // Get help listing
        resp = doCommand(CMD_HELP);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't get remote system help (" + resp.m_code + ")");

        transferCmd(resp, false, out);
    }


    /***************************************************************************
    * Set the working directory on the remote FTP system.
    *
    * @param	dir
    * Remote directory name.
    *
    * @return
    * The new current remote directory name.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @see	#setRemoteDirUp setRemoteDirUp()
    * @see	#getRemoteDir getRemoteDir()
    * @see	#setLocalDir setLocalDir()
    *
    * @since	1.1, 2001-04-14
    */
    @Override
    public String setRemoteDir(String dir)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (dir == null)
            throw new NullPointerException("Null FTP directory name");
        if (dir.length() == 0)
            throw new FTPException("Empty FTP directory name");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Change the remote working directory
        resp = doCommand(CMD_CHDIR, dir);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't change remote directory (" + resp.m_code + "): \""
                + dir + "\"");
        m_remoteDir = dir;

        // Retrieve the new current remote working directory
        dir = getCurrentDir(resp);
        if (dir == null)
        {
            resp = doCommand(CMD_GETDIR);
            dir = getCurrentDir(resp);
        }
        if (dir != null)
            m_remoteDir = dir;
        return m_remoteDir;
    }


    /***************************************************************************
    * Set the working directory on the remote FTP system to the parent directory
    * of the current working directory.
    * In other words, change the directory to be one level up from the current
    * setting.
    *
    * @return
    * The new current remote directory name.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @see	#getRemoteDir getRemoteDir()
    * @see	#setRemoteDir setRemoteDir()
    * @see	#setLocalDir setLocalDir()
    *
    * @since	1.9, 2006-04-01
    */
    @Override
    public String setRemoteDirUp()
        throws IOException
    {
        FTPResponse	resp;
        String		dir;

        // Sanity checks
        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Change the remote working directory
        resp = doCommand(CMD_UPDIR);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't change remote directory up (" + resp.m_code + ")");

        // Retrieve the new current remote working directory
        dir = getCurrentDir(resp);
        if (dir == null)
        {
            resp = doCommand(CMD_GETDIR);
            dir = getCurrentDir(resp);
        }
        if (dir != null)
            m_remoteDir = dir;
        return m_remoteDir;
    }


    /***************************************************************************
    * Retrieve the current working directory of the remote FTP system.
    *
    * @return
    * Remote directory name.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @see	#setRemoteDir setRemoteDir()
    * @see	#setRemoteDirUp setRemoteDirUp()
    * @see	#getLocalDir getLocalDir()
    *
    * @since	1.1, 2001-04-15
    */
    @Override
    public String getRemoteDir()
        throws IOException
    {
        FTPResponse	resp;
        String		dir;

        // Sanity checks
        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Retrieve the current remote working directory
        resp = doCommand(CMD_GETDIR);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't get remote directory (" + resp.m_code + ")");

        dir = getCurrentDir(resp);
        if (dir != null)
            m_remoteDir = dir;
        return m_remoteDir;
    }


    /***************************************************************************
    * Retrieve the current working directory of the remote FTP system from the
    * results of the previous FTP command.
    *
    * @return
    * Current remote directory name, or null if it cannot be retrieved.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @see	#setRemoteDir setRemoteDir()
    * @see	#setRemoteDirUp setRemoteDirUp()
    * @see	#getRemoteDir getRemoteDir()
    *
    * @since	1.9, 2006-04-01
    */
    String getCurrentDir(FTPResponse resp)
        throws IOException
    {
        // Retrieve the new current remote working directory
        //  from the results of a previous 'CMD_GETDIR' command
        if (resp.m_code/100 == RG_OKAY_COMPL)
        {
            byte[]	line;
            int		i, j, k, o;

            line = resp.line(0);
            for (i = 0;  i < line.length  &&  line[i] != '"';  i++)
                continue;
            for (j = line.length-1;  j > i  &&  line[j] != '"';  j--)
                continue;
            for (k = i+1, o = i+1;  o < j;  k++, o++)
            {
                if (line[o] == '"'  &&  line[o+1] == '"')
                    o++;
                line[k] = line[o];
            }
            if (i+1 < k)
                return (new String(line, i+1, k-(i+1)));
        }

        // Failed
        return null;
    }


    /***************************************************************************
    * Get (receive) a file from the remote FTP system to the local system.
    *
    * @param	src
    * Remote source filename.  If this does not contain a directory prefix, the
    * current remote working directory is assumed.
    *
    * @param	out
    * Output stream to write the contents of the file retrieved from the remote
    * FTP system to.  Note that this stream is flushed but is <i>not</i> closed
    * after the contents have been transmitted.
    *
    * @throws	IOException
    * Thrown if the file could not be transmitted or if any other error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * operation prematurely.
    *
    * @see	#getFile(String, File) getFile()
    * @see	#getFile(String, String) getFile()
    * @see	#putFile(InputStream, String) putFile()
    *
    * @since	1.12, 2006-04-10
    */
    @Override
    public void getFile(String src, OutputStream out)
        throws IOException
    {
        FTPResponse	resp;
        InputStream	in =	null;
        OutputStream	outp;
        IOException	exc =	null;

        // Sanity checks
        if (src == null)
            throw new NullPointerException("Null FTP source file");
        if (out == null)
            throw new NullPointerException("Null FTP output stream");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Open the target file as an output stream
        if (out instanceof BufferedOutputStream)
            outp = out;
        else
            outp = new BufferedOutputStream(out, m_bufSize);

        // Retrieve (get) the file from the remote FTP system
        resp = openDataPort(CMD_GET, src);
        if (resp.m_code/100 >= RG_FAIL_RETRY)
            throw new FTPException(resp.m_code,
                "Can't get remote FTP file (" + resp.m_code + "): " + src);

        in = new BufferedInputStream(m_dataIn, m_bufSize);
        transferFile(in, outp, LOCAL_NEWLINE);

        // Clean up
        try
        {
            in.close();
            outp.flush();
        }
        catch (IOException ex)
        {
            exc = ex;
        }

        // Clean up
        closeDataPort();
        resp = getResponse();

        if (exc != null)
            throw (exc);
    }


    /***************************************************************************
    * Put (send) a file from the local system to the remote FTP system.
    *
    * @param	src
    * Local source filename.  If this does not contain a directory prefix, the
    * current local working directory is assumed.
    *
    * @param	dst
    * Remote target filename.  If this does not contain a directory prefix, the
    * current remote working directory is assumed.  This may be null, in which
    * case the base filename of <tt>src</tt> (without the directory prefix) is
    * used.
    *
    * @throws	IOException
    * Thrown if the file could not be transmitted or if any other error occurs.
    *
    * @see	#putFile(String, String) putFile()
    * @see	#putFile(InputStream, String) putFile()
    * @see	#getFile(String, File) getFile()
    *
    * @since	1.1, 2001-04-14
    */
    @Override
    public void putFile(File src, String dst)
        throws IOException
    {
        InputStream	in;
        long		len;

        // Sanity checks
        if (src == null)
            throw new NullPointerException("Null FTP source file");
        if (dst == null)
            throw new NullPointerException("Null FTP target file");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        if (!src.exists())
            throw new FTPException("FTP source file does not exist: \""
                + src.getPath() + "\"");

        // Open the source file as an input stream
        in = new FileInputStream(src);

        // Preallocate the target file on the remote system
        len = src.length();
        if (len > 1024)
        {
            int		obytes;
            FTPResponse	resp;

            obytes = (len <= Integer.MAX_VALUE ? (int) len : Integer.MAX_VALUE);
            resp = doCommand(CMD_ALLOCATE, obytes + "");
        }

        try
        {
            // Store (put) the file to the remote FTP system
            putFile(in, dst);
        }
        finally
        {
            in.close();
        }
    }


    /***************************************************************************
    * Put (send) a file from the local system to the remote FTP system.
    *
    * @param	in
    * Input stream containing the contents of the file to send to the remote FTP
    * system.  Note that this stream is <i>not</i> closed after the contents
    * have been transmitted.
    *
    * @param	dst
    * Remote target filename.  If this does not contain a directory prefix, the
    * current remote working directory is assumed.  This may be null, in which
    * case the base filename of <tt>src</tt> (without the directory prefix) is
    * used.
    *
    * @throws	IOException
    * Thrown if the file could not be transmitted or if any other error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * operation prematurely.
    *
    * @see	#putFile(File, String) putFile()
    * @see	#putFile(String, String) putFile()
    * @see	#appendFile(File, String) appendFile()
    * @see	#getFile(String, OutputStream) getFile()
    *
    * @since	1.12, 2006-04-10
    */
    @Override
    public void putFile(InputStream in, String dst)
        throws IOException
    {
        FTPResponse	resp;
        InputStream	inp;
        IOException	exc =	null;

        // Sanity checks
        if (in == null)
            throw new NullPointerException("Null FTP input stream");
        if (dst == null)
            throw new NullPointerException("Null FTP target file");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Open the source file as an input stream
        if (in instanceof BufferedInputStream)
            inp = in;
        else
            inp = new BufferedInputStream(in, m_bufSize);

        // Store (put) the file to the remote FTP system
        resp = openDataPort(CMD_PUT, dst);
        if (resp.m_code/100 >= RG_FAIL_RETRY)
            throw new FTPException(resp.m_code,
                "Can't put file to remote FTP (" + resp.m_code + "): " + dst);

        try
        {
            OutputStream	out;

            // Store (put) the file to the remote FTP system
            out = new BufferedOutputStream(m_dataOut, m_bufSize);
            transferFile(inp, out, REMOTE_NEWLINE);
            out.flush();
            out.close();
        }
        catch (IOException ex)
        {
            exc = ex;
        }

        // Clean up
        closeDataPort();
        resp = getResponse();

        if (exc != null)
            throw (exc);
    }


    /***************************************************************************
    * Append (send) a file from the local system to a file on the remote FTP
    * system.
    *
    * @param	in
    * Input stream containing the contents of the file to send to the remote FTP
    * system.  Note that this stream is <i>not</i> closed after the contents
    * have been transmitted.
    *
    * @param	dst
    * Remote target filename.  If this does not contain a directory prefix, the
    * current remote working directory is assumed.  This may be null, in which
    * case the base filename of <tt>src</tt> (without the directory prefix) is
    * used.
    *
    * @throws	IOException
    * Thrown if the file could not be transmitted or if any other error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * operation prematurely.
    *
    * @see	#appendFile(File, String) appendFile()
    * @see	#appendFile(String, String) appendFile()
    * @see	#putFile(InputStream, String) putFile()
    *
    * @since	1.25, 2007-06-30
    */
    @Override
    public void appendFile(InputStream in, String dst)
        throws IOException
    {
        FTPResponse	resp;
        InputStream	inp;
        IOException	exc =	null;

        // Sanity checks
        if (in == null)
            throw new NullPointerException("Null FTP input stream");
        if (dst == null)
            throw new NullPointerException("Null FTP target file");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Open the source file as an input stream
        if (in instanceof BufferedInputStream)
            inp = in;
        else
            inp = new BufferedInputStream(in, m_bufSize);

        // Append (put) the file to the remote FTP system
        resp = openDataPort(CMD_APPEND, dst);
        if (resp.m_code/100 >= RG_FAIL_RETRY)
            throw new FTPException(resp.m_code,
                "Can't append file to remote FTP (" + resp.m_code + "): "
                + dst);

        try
        {
            OutputStream	out;

            // Append (put) the file to the remote FTP system
            out = new BufferedOutputStream(m_dataOut, m_bufSize);
            transferFile(inp, out, REMOTE_NEWLINE);
            out.flush();
            out.close();
        }
        catch (IOException ex)
        {
            exc = ex;
        }

        // Clean up
        closeDataPort();
        resp = getResponse();

        if (exc != null)
            throw (exc);
    }


    /***************************************************************************
    * Transfer (send or receive) a file from one system (local or remote) to the
    * other system (remote or local).
    *
    * <p>
    * When transferring text-mode files (i.e., {@link #m_textMode} is true),
    * this method handles the translation of newlines, translating CR, LF, and
    * CR/LF input sequences into CR/LF output sequences.
    *
    * @param	in
    * Input source stream.  This may be for a local file or a remote file.
    *
    * @param	out
    * Output target stream.  This may be for a remote file or a local file.
    *
    * @param	nl
    * Newline sequence for the target system.  This only applies to text (ASCII)
    * mode file transfers.
    *
    * @return
    * Number of bytes written to the output stream.
    *
    * @throws	IOException
    * Thrown if the file could not be transmitted or if any other error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * operation prematurely.
    *
    * @see	#getFile(String, File) getFile()
    * @see	#putFile(File, String) putFile()
    *
    * @since	1.5, 2006-03-18
    */
    long transferFile(InputStream in, OutputStream out, byte[] nl)
        throws IOException
    {
        long	nlines = 0;

        // Copy the source file to the target file
        try
        {
            m_inBytes =  0;
            m_outBytes = 0;

            if (m_textMode)
            {
                int	ungetCh =	-1;

                // Text (ASCII) data transfer
                for (;;)
                {
                    int		ch;

                    // Check for an interrupt signal
                    if (m_stop)
                        throw new FTPStoppedException("FTP transfer stopped");

                    // Read the next byte from the local/remote source file
                    if (ungetCh < 0)
                        ch = in.read();
                    else
                        ch = ungetCh;
                    ungetCh = -1;

                    if (ch < 0)
                        break;
                    m_inBytes++;

                    // Write the data byte to the remote/local target file
                    if (ch == '\r')
                    {
                        // Translate a CR or CR/LF newline sequence
                        ch = in.read();
                        m_inBytes++;
                        if (ch != '\n')
                        {
                            ungetCh = ch;
                            m_inBytes--;
                        }

                        ch = '\n';
                    }

                    if (ch == '\n')
                    {
                        // Translate a LF, CR, or CR/LF into a CR/LF
                        for (int i = 0;  i < nl.length;  i++)
                        {
                            out.write(nl[i]);
                            m_outBytes++;
                        }
                        nlines++;
                    }
                    else
                    {
                        out.write(ch);
                        m_outBytes++;
                    }
                }

                return m_outBytes;
            }
            else
            {
                // Binary data transfer
                for (;;)
                {
                    int		ch;

                    // Check for an interrupt signal
                    if (m_stop)
                        throw new FTPStoppedException("FTP transfer stopped");

                    // Read the next byte from the local/remote source file
                    ch = in.read();
                    if (ch < 0)
                        break;
                    m_inBytes++;

                    // Write the data byte to the remote/local target file
                    out.write(ch);
                    m_outBytes++;
                }

                return m_outBytes;
            }
        }
        finally
        {
            if (m_debugOut != null)
                m_debugOut.println("$ bytes read:" + m_inBytes
                    + ", wrote:" + m_outBytes + ", lines:" + nlines);
        }
    }


    /***************************************************************************
    * Transfer the contents of the command stream to a local output stream.
    * Leading spaces are stripped from the command response lines.
    *
    * @param	resp
    * Command response received from the remote FTP system.
    *
    * @param	all
    * If true, all response lines including the last one are written, otherwise
    * all but the last line are written.
    *
    * @param	out
    * Local output stream.
    *
    * @throws	IOException
    * Thrown if an I/O error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the transfer
    * (write) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    void transferCmd(FTPResponse resp, boolean all, OutputStream out)
        throws IOException
    {
        int	len;

        // Write the command response to the output stream
        len = resp.m_lines.size();
        len = (all ? len : len-1);

        for (int i = 0;  i < len;  i++)
        {
            byte[]	line;
            int		j;

            // Check for an interrupt signal
            if (m_stop)
                throw new FTPStoppedException("FTP transfer stopped");

            // Write the next response line
            line = resp.line(i);
            j = (i == 0 ? 4 : 0);
            for ( ;  j < line.length  &&  line[j] == ' ';  j++)
                continue;
            for ( ;  j < line.length;  j++)
                out.write((char) (line[j] & 0xFF));
            for (j = 0;  j < LOCAL_NEWLINE.length;  j++)
                out.write((char) (LOCAL_NEWLINE[j] & 0xFF));
        }
        out.flush();
    }


    /***************************************************************************
    * Get (receive) a directory listing from the remote FTP system.
    *
    * @param	path
    * The remote directory or filename to list.  If this is empty
    * (<tt>""</tt>), the current remote working directory is assumed.
    *
    * @param	max
    * Maximum number of filenames (output lines) to list.  A value of zero (0)
    * specifies that there is no maximum.
    *
    * @param	out
    * Output stream to which the directory listing is to be written.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the listing
    * (output) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    @Override
    public void getDirectoryList(String path, int max, OutputStream out)
        throws IOException
    {
        // Get the directory listing of the remote directory
        doDirectoryCmd(path, true, max, out);
    }


    /***************************************************************************
    * Get (receive) a list of filenames in a directory on the remote FTP system.
    *
    * @param	path
    * The remote directory or filename to list.  If this is empty
    * (<tt>""</tt>), the current remote working directory is assumed.
    *
    * @param	max
    * Maximum number of filenames to get.  A value of zero (0) specifies that
    * there is no maximum.
    *
    * @param	out
    * Output stream to which the directory listing is to be written.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the listing
    * (output) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    @Override
    public void getDirectoryNames(String path, int max, OutputStream out)
        throws IOException
    {
        // Get the directory listing of the remote directory
        doDirectoryCmd(path, false, max, out);
    }


    /***************************************************************************
    * Get (receive) a directory listing for the current working directory on the
    * remote FTP system.
    *
    * @param	path
    * The remote directory or filename to list.  If this is empty
    * (<tt>""</tt>), the current remote working directory is assumed.
    *
    * @param	full
    * If true, a full directory listing is produced, otherwise only filenames
    * are received.
    *
    * @param	max
    * Maximum number of filenames (or output lines) to get.  A value of zero (0)
    * specifies that there is no maximum.
    *
    * @param	out
    * Output stream to which the directory listing is to be written.  The stream
    * is flushed but not closed after the filenames are written to it.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the listing
    * (output) operation prematurely.
    *
    * @since	API 2.0, 1.29, 2010-07-12
    */
    void doDirectoryCmd(String path, boolean full, int max, OutputStream out)
        throws IOException
    {
        FTPResponse	resp;
        int		n;

        // Sanity checks
        if (out == null)
            return;

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Get the directory listing of the remote directory
        if (path == null)
            path = "";
        resp = openDataPort((full ? CMD_LISTFILES : CMD_LISTNAMES), path);
        if (resp.m_code/100 != RG_OKAY_INC  &&  resp.m_code/100 != RG_OKAY_PEND)
        {
            closeDataPort();

            if (resp.m_code/100 == RG_FAIL_RETRY)
            {
                // Assume that the remote directory is empty
                out.flush();
                return;
            }

            throw new FTPException(resp.m_code,
                "Can't get remote directory list (" + resp.m_code + "): \""
                + path + "\"");
        }

        // Read the directory listing results
        n = 0;
        for (;;)
        {
            int		len;

            // Check for an interrupt signal
            if (m_stop)
                throw new FTPStoppedException("FTP listing stopped");

            // Read the next line from the directory listing
            len = readDataLine();
            if (len < 0)
                break;

            // Write the directory entry to the output stream
            if (len > 0)
            {
                for (int j = 0;  j < len;  j++)
                    out.write((char) (m_dbuf[j] & 0xFF));
                for (int j = 0;  j < LOCAL_NEWLINE.length;  j++)
                    out.write((char) (LOCAL_NEWLINE[j] & 0xFF));

                n++;
                if (n >= max  &&  max > 0)
                    break;
            }
        }
        out.flush();

        // Get the transfer completion response
        closeDataPort();
        resp = getResponse();
    }


    /***************************************************************************
    * Get (receive) a list of filenames in a directory on the remote FTP system.
    *
    * @param	path
    * The remote directory or filename to list.  If this is empty
    * (<tt>""</tt>), the current remote working directory is assumed.
    *
    * @param	filt
    * Filer to apply to the filenames.  Only filenames that are accepted by the
    * filter will appear in the returned vector.  If this is null, no filtering
    * is applied to the filenames.  This object's <tt>accept()</tt> method is
    * called for each filename in the directory, being passed a null directory
    * (first argument) and the found filename (second argument).
    *
    * @param	max
    * Maximum number of filenames to list.  A value of zero (0) specifies that
    * there is no maximum.
    *
    * @return
    * A vector of <tt>String</tt>s containing the filenames in the remote
    * directory.  Note that this may contain zero entries.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @throws	FTPStoppedException
    * Thrown if {@link #stop stop()} is called, which terminates the listing
    * operation prematurely.
    *
    * @since	1.22, 2007-04-15
    */
    //@Override
    public ArrayList<String> getDirectoryNames(String path,
        FilenameFilter filt, int max)
        throws IOException
    {
        ArrayList<String>	list;
        FTPResponse		resp;

        // Sanity checks
        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Get the directory listing of the remote directory
        if (path == null)
            path = "";
        resp = openDataPort(CMD_LISTNAMES, path);
        if (resp.m_code/100 != RG_OKAY_INC  &&  resp.m_code/100 != RG_OKAY_PEND)
        {
            closeDataPort();

            if (resp.m_code/100 == RG_FAIL_RETRY)
            {
                // Assume that the remote directory is empty
                return (new ArrayList<String>(1));
            }

            throw new FTPException(resp.m_code,
                "Can't get remote directory names (" + resp.m_code + "): \""
                + path + "\"");
        }

        // Read the directory listing results
        list = new ArrayList<String>();
        for (;;)
        {
            int		len;

            // Check for an interrupt signal
            if (m_stop)
                throw new FTPStoppedException("FTP listing stopped");

            // Read the next line from the directory listing
            len = readDataLine();
            if (len < 0)
                break;

            // Found a directory entry
            if (len > 0)
            {
                String	fname;

                fname = new String(m_dbuf, 0, len);
                if (filt == null  ||  filt.accept(null, fname))
                {
                    // Add the filename to the list
                    list.add(fname);
                    if (max > 0  &&  list.size() >= max)
                        break;
                }
            }
        }

        // Get the transfer completion response
        closeDataPort();
        resp = getResponse();
        return list;
    }


    /***************************************************************************
    * Rename a file or directory on the remote FTP system.
    *
    * @param	from
    * Old (existing) remote file or directory name to rename.
    *
    * @param	to
    * New name to rename the remote file or directory to.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @since	1.4, 2006-03-17
    */
    @Override
    public void rename(String from, String to)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (from == null)
            throw new NullPointerException("Null FTP from-file");
        if (to == null)
            throw new NullPointerException("Null FTP to-file");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Rename an existing remote file or directory
        resp = doCommand(CMD_RENAME_FROM, from);
        if (resp.m_code/100 != RG_OKAY_PEND)
            throw new FTPException(resp.m_code,
                "Can't rename remote file (" + resp.m_code + "): from \""
                + from + "\"");

        resp = doCommand(CMD_RENAME_TO, to);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't rename remote file (" + resp.m_code + "): to \""
                + to + "\"");
    }


    /***************************************************************************
    * Remove a file on the remote FTP system.
    *
    * @param	file
    * Name of the file to delete.  If this specifies a relative filename, the
    * file is assumed to be located in the current remote working directory.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @since	1.4, 2006-03-17
    */
    @Override
    public void removeFile(String file)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (file == null)
            throw new NullPointerException("Null FTP filename");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Remove an existing remote file or directory
        resp = doCommand(CMD_DELETE, file);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't delete remote file (" + resp.m_code + "): \""
                + file + "\"");
    }


    /***************************************************************************
    * Create a directory on the remote FTP system.
    *
    * @param	dir
    * Name of the directory to create.  If this specifies a relative directory
    * name, the directory is created in the current remote working directory.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @since	1.4, 2006-03-17
    */
    @Override
    public void createDirectory(String dir)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (dir == null)
            throw new NullPointerException("Null FTP directory name");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Create a new remote directory
        resp = doCommand(CMD_MKDIR, dir);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't create remote directory (" + resp.m_code + "): \""
                + dir + "\"");
    }


    /***************************************************************************
    * Remove a directory on the remote FTP system.
    *
    * @param	dir
    * Name of the directory to delete.  If this specifies a relative directory
    * name, the directory is assumed to be located in the current remote working
    * directory.
    *
    * @throws	IOException
    * Thrown if an error occurs.
    *
    * @since	1.4, 2006-03-17
    */
    @Override
    public void removeDirectory(String dir)
        throws IOException
    {
        FTPResponse	resp;

        // Sanity checks
        if (dir == null)
            throw new NullPointerException("Null FTP directory name");

        if (!m_isConnected)
            throw new FTPException("FTP session not connected");
        if (!m_isLoggedOn)
            throw new FTPException("Not logged into FTP session");

        // Remove an existing remote directory
        resp = doCommand(CMD_RMDIR, dir);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't remove remote directory (" + resp.m_code + "): \""
                + dir + "\"");
    }


    /***************************************************************************
    * Send a command to the remote FTP system and receive its response.
    *
    * @param	cmd
    * FTP command.
    *
    * @param	args
    * Command arguments.  This can be null.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if the command could not be transmitted or if any other error
    * occurs.
    *
    * @since	1.1, 2001-04-14
    */
    FTPResponse doCommand(String cmd, String args)
        throws IOException
    {
        // Sanity checks
        if (!m_isConnected)
            throw new FTPException("FTP session not connected");

        if (m_debugOut != null)
            m_debugOut.println("$> " + cmd
                + (args != null ?
                    " " + (cmd.equals(CMD_PASSWORD) ? "***" : args) :
                    ""));

        // Send the command to the remote FTP system
        writeCmd(cmd);
        if (args != null  &&  args.length() > 0)
        {
            writeCmd(" ");
            writeCmd(args);
        }
        writeCmd(CMD_EOLN);

        // Receive the response from the FTP system
        return getResponse();
    }


    /***************************************************************************
    * Send a command to the remote FTP system and receive its response.
    *
    * @param	cmd
    * FTP command to execute.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if the command could not be transmitted or if any other error
    * occurs.
    *
    * @since	1.1, 2001-04-14
    */
    FTPResponse doCommand(String cmd)
        throws IOException
    {
        return doCommand(cmd, null);
    }


    /***************************************************************************
    * Receive the response to a command sent to the remote FTP system.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if the command response could not be received or if any other error
    * occurs.
    *
    * @since	1.1, 2001-04-14
    */
    FTPResponse getResponse()
        throws IOException
    {
        FTPResponse		resp;
        ArrayList<byte[]>	lines;

        // Set up
        resp = new FTPResponse();
        lines = new ArrayList<byte[]>(2);
        resp.m_lines = lines;
        resp.m_code = -1;

        // Receive the command response line(s)
    readLines:
        for (;;)
        {
            int		len;
            byte[]	line;

            // Receive the next response line
            len = readCmdLine();
            if (len < 0)
                break readLines;

            // Skip blank lines
            if (len == 0)
                continue readLines;

            // Add the line to the list of response lines
            line = new byte[len];
            System.arraycopy(m_cbuf, 0, line, 0, len);
            lines.add(line);

            if (m_debugOut != null)
                m_debugOut.println("$< [" + new String(line, 0, len) + "]");

            if (line.length > 3  &&
                line[0] >= '0'  &&  line[0] <= '9'  &&
                line[1] >= '0'  &&  line[1] <= '9'  &&
                line[2] >= '0'  &&  line[2] <= '9'  &&
                (line[3] == ' '  ||  line[3] == '-'))
            {
                int	code;

                // Parse the response code
                code = ((line[0]-'0')*10 + line[1]-'0')*10 + line[2]-'0';

                // Check for the final command line of the group
                if (resp.m_code < 0)
                    resp.m_code = code;
                if (code == resp.m_code  &&  line[3] == ' ')
                    break readLines;
            }
        }

        // Done
        return resp;
    }


    /***************************************************************************
    * Write a string to the remote FTP command port.
    *
    * @param	s
    * String to write.
    *
    * @throws	IOException
    * Thrown if the command could not be transmitted or if any other error
    * occurs.
    *
    * @since	1.1, 2001-04-14
    */
    void writeCmd(String s)
        throws IOException
    {
        int	len;

        // Write data to the command port of the remote FTP system
        len = s.length();
        for (int i = 0;  i < len;  i++)
            m_cmdOut.write(s.charAt(i) & 0xFF);
    }


    /***************************************************************************
    * Read a response text line from the remote FTP command port.
    *
    * @return
    * Length of the text line read from the FTP system (which is stored in
    * {@link #m_cbuf}.
    *
    * @throws	IOException
    * Thrown if the line could not be received or if any other error occurs.
    *
    * @since	1.1, 2001-04-14
    */
    int readCmdLine()
        throws IOException
    {
        return readLine(m_cmdIn, m_cbuf);
    }


    /***************************************************************************
    * Read a text line from the remote FTP data port.
    *
    * @return
    * Length of the text line read from the FTP system (which is stored in
    * {@link #m_dbuf}.
    *
    * @throws	IOException
    * Thrown if the line could not be received or if any other error occurs.
    *
    * @since	1.4, 2006-03-16
    */
    int readDataLine()
        throws IOException
    {
        return readLine(m_dataIn, m_dbuf);
    }


    /***************************************************************************
    * Read a text line from the remote FTP command or data port.
    *
    * @param	in
    * Command or data port connected to the remote FTP system.
    *
    * @param	buf
    * Buffer into which the received text line is written.
    *
    * @return
    * Length of the text line read from the FTP system.
    *
    * @throws	IOException
    * Thrown if the line could not be received or if any other FTP error occurs.
    *
    * @since	1.4, 2006-03-17
    */
    int readLine(InputStream in, byte[] buf)
        throws IOException
    {
        int	len;
        int	ch;

        // Read a text line from the FTP command port
        ch = in.read();
        if (ch < 0)
            return -1;
        m_inBytes++;

        len = 0;
        for (;;)
        {
            // Handle end of line
            if (ch == '\r')
            {
                // Look for a CR/LF newline sequence
                while (ch == '\r')
                {
                    ch = in.read();
                    m_inBytes++;
                }

                if (ch == '\n')
                    break;

                if (len < buf.length)
                    buf[len++] = '\n';
            }
            else if (ch == '\n')
                break;

            // Add the next character to the command line
            if (len < buf.length)
                buf[len++] = (byte) ch;

            // Read the next command character
            ch = in.read();
            if (ch < 0)
                break;
            m_inBytes++;
        }

        return (len <= buf.length ? len : buf.length);
    }


    /***************************************************************************
    * Open and connect a data port socket to the remote FTP system.
    *
    * @param	cmd
    * FTP command to execute which results in a stream being opened on the data
    * port.
    *
    * @param	args
    * Command arguments.  This can be null.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if a socket error occurs.
    *
    * @since	1.7, 2006-03-22
    */
    FTPResponse openDataPort(String cmd, String args)
        throws IOException
    {
        if (m_passiveMode)
            return openRemoteDataPort(cmd, args);
        else
            return openLocalDataPort(cmd, args);
    }


    /***************************************************************************
    * Open and connect to a passive data port socket originating on the remote
    * FTP system.
    *
    * @param	cmd
    * FTP command to execute which results in a stream being opened on the data
    * port.
    *
    * @param	args
    * Command arguments.  This can be null.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if a socket error occurs.
    *
    * @since	1.7, 2006-03-22
    */
    FTPResponse openRemoteDataPort(String cmd, String args)
        throws IOException
    {
        FTPResponse	resp;
        byte[]		line;
        int[]		parms;
        Socket		sock;
        int		dport;
        int		i, j;

        // Initiate a passive socket port on the remote FTP system
        resp = doCommand(CMD_PASSIVE);
        if (resp.m_code/100 != RG_OKAY_COMPL)
            throw new FTPException(resp.m_code,
                "Can't open passive FTP data port (" + resp.m_code + "): "
                + m_hostName);

        // Extract the remote passive data port
        // Response is "3xx ...h4,h3,h2,h1,p2,p1..."
        parms = new int[4+2];
        line = resp.line(0);

        for (j = line.length-1;
            j > 3  &&  (line[j] < '0' || line[j] > '9');
            j--)
            continue;
        for (i = j;
            i > 3  &&  ((line[i] >= '0' && line[i] <= '9')  ||  line[i] == ',');
            i--)
            continue;
        for (int n = 0;  n < 6;  n++)
        {
            int		d =	0;

            for (i++;  i <= j  &&  (line[i] >= '0'  &&  line[i] <= '9');  i++)
                d = d*10 + line[i]-'0';
            parms[n] = d;
        }
        dport = (parms[4]<<8) + parms[5];

        if (m_debugOut != null)
            m_debugOut.println("$ remoteHost: "
                + parms[0] + "." + parms[1] + "." + parms[2] + "." + parms[3]
                + ":" + dport);

        // Open the remote data port socket
        m_dataSock = new Socket(m_hostName, dport);
        m_dataSock.setSoTimeout(m_timeOut*1000);

        m_dataIn =  m_dataSock.getInputStream();
        m_dataOut = m_dataSock.getOutputStream();

        // Send the command to the remote FTP system
        resp = doCommand(cmd, args);
        return resp;
    }


    /***************************************************************************
    * Open a non-passive data port socket originating on the local system, and
    * wait for a connection to it from the remote FTP system.
    *
    * @param	cmd
    * FTP command to execute which results in a stream being opened on the data
    * port.
    *
    * @param	args
    * Command arguments.  This can be null.
    *
    * @return
    * Command response.
    *
    * @throws	IOException
    * Thrown if a socket error occurs.
    *
    * @since	1.7, 2006-03-22
    */
    FTPResponse openLocalDataPort(String cmd, String args)
        throws IOException
    {
        ServerSocket	sock =	null;

        try
        {
            FTPResponse		resp;
            byte[]		haddr;
            int			p;
            String		port;

            // Create a local socket to listen on
            sock = new ServerSocket(m_dataPort);
            sock.setSoTimeout(m_timeOut*1000);

            // Notify the remote FTP system of the port
            haddr = m_localHost.getAddress();
            p = sock.getLocalPort();
            port = "";
            for (int i = 0;  i < haddr.length;  i++)
                port += (haddr[i] & 0xFF) + ",";
            port += ((p >>> 8) & 0xFF) + "," + (p & 0xFF);

            if (m_debugOut != null)
                m_debugOut.println("$ localHost: " + m_localHost.toString()
                    + ":" + p);

            resp = doCommand(CMD_PORT, port);
            if (resp.m_code/100 != RG_OKAY_COMPL)
                throw new FTPException(resp.m_code,
                    "Can't set FTP data port (" + resp.m_code + "): " + p);

            // Send the command to the remote FTP system
            resp = doCommand(cmd, args);
            if (resp.m_code/100 != RG_OKAY_INC)
                throw new FTPException(resp.m_code,
                    "Can't execute FTP command (" + resp.m_code + "): " + cmd);

            // Listen (wait) for a connection from the remote FTP system
            m_dataSock = sock.accept();
            m_dataSock.setSoTimeout(m_timeOut*1000);

            m_dataIn =  m_dataSock.getInputStream();
            m_dataOut = m_dataSock.getOutputStream();

            return resp;
        }
        finally
        {
            // Clean up
            try
            {
                if (sock != null)
                    sock.close();
            }
            catch (IOException ex)
            { }
        }
    }


    /***************************************************************************
    * Close the data port socket connected to the remote FTP system.
    *
    * @throws	IOException
    * Thrown if a socket error occurs.
    *
    * @since	1.4, 2006-03-16
    */
    void closeDataPort()
        throws IOException
    {
        // Shut down the data socket connection
        if (m_dataSock != null)
        {
            try
            {
                if (m_dataIn != null)
                    m_dataIn.close();
                if (m_dataOut != null)
                    m_dataOut.close();

                m_dataSock.close();
            }
            finally
            {
                m_dataIn =   null;
                m_dataOut =  null;
                m_dataSock = null;
            }
        }
    }
}

// End FTPClient.java
