/*
 * $Id: FTPConnection.java,v 1.4 2004/06/08 19:05:27 dog Exp $
 * Copyright (C) 2003 The Free Software Foundation
 * 
 * This file is part of GNU inetlib, a library.
 * 
 * GNU inetlib is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 * 
 * GNU inetlib 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 General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 * 
 * As a special exception, if you link this library with other files to
 * produce an executable, this library does not by itself cause the
 * resulting executable to be covered by the GNU General Public License.
 * This exception does not however invalidate any other reasons why the
 * executable file might be covered by the GNU General Public License.
 */

package gnu.inet.ftp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.BindException;
import java.net.InetAddress;
import java.net.ProtocolException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;

import gnu.inet.util.CRLFInputStream;
import gnu.inet.util.LineInputStream;

/**
 * An FTP client connection, or PI.
 * This implements RFC 959, with the following exceptions:
 * <ul>
 * <li>STAT, HELP, SITE, SMNT, and ACCT commands are not supported.</li>
 * <li>the TYPE command does not allow alternatives to the default bytesize
 * (Non-print), and local bytesize is not supported.</li>
 * </ul>
 *
 * @author <a href='mailto:dog@gnu.org'>Chris Burdess</a>
 * @version $Revision: 1.4 $ $Date: 2004/06/08 19:05:27 $
 */
public class FTPConnection
{

  /**
   * The default FTP transmission control port.
   */
  public static final int FTP_PORT = 21;

  /**
   * The FTP data port.
   */
  public static final int FTP_DATA_PORT = 20;

  // -- FTP vocabulary --
  protected static final String USER = "USER";
  protected static final String PASS = "PASS";
  protected static final String ACCT = "ACCT";
  protected static final String CWD = "CWD";
  protected static final String CDUP = "CDUP";
  protected static final String SMNT = "SMNT";
  protected static final String REIN = "REIN";
  protected static final String QUIT = "QUIT";

  protected static final String PORT = "PORT";
  protected static final String PASV = "PASV";
  protected static final String TYPE = "TYPE";
  protected static final String STRU = "STRU";
  protected static final String MODE = "MODE";

  protected static final String RETR = "RETR";
  protected static final String STOR = "STOR";
  protected static final String STOU = "STOU";
  protected static final String APPE = "APPE";
  protected static final String ALLO = "ALLO";
  protected static final String REST = "REST";
  protected static final String RNFR = "RNFR";
  protected static final String RNTO = "RNTO";
  protected static final String ABOR = "ABOR";
  protected static final String DELE = "DELE";
  protected static final String RMD = "RMD";
  protected static final String MKD = "MKD";
  protected static final String PWD = "PWD";
  protected static final String LIST = "LIST";
  protected static final String NLST = "NLST";
  protected static final String SITE = "SITE";
  protected static final String SYST = "SYST";
  protected static final String STAT = "STAT";
  protected static final String HELP = "HELP";
  protected static final String NOOP = "NOOP";

  public static final int TYPE_ASCII = 1;
  public static final int TYPE_EBCDIC = 2;
  public static final int TYPE_BINARY = 3;

  public static final int STRUCTURE_FILE = 1;
  public static final int STRUCTURE_RECORD = 2;
  public static final int STRUCTURE_PAGE = 3;

  public static final int MODE_STREAM = 1;
  public static final int MODE_BLOCK = 2;
  public static final int MODE_COMPRESSED = 3;

  // -- Telnet constants --
  private static final String US_ASCII = "US-ASCII";
  private static final byte[] CRLF = { 0x0d, 0x0a };

  /**
   * The socket used to communicate with the server.
   */
  protected Socket socket;

  /**
   * The socket input stream.
   */
  protected LineInputStream in;

  /**
   * The socket output stream.
   */
  protected OutputStream out;

  /**
   * If true, print debugging information.
   */
  protected boolean debug;

  /**
   * The current data transfer process in use by this connection.
   */
  protected DTP dtp;

  /**
   * The current representation type.
   */
  protected int representationType = TYPE_ASCII;

  /**
   * The current file structure type.
   */
  protected int fileStructure = STRUCTURE_FILE;

  /**
   * The current transfer mode.
   */
  protected int transferMode = MODE_STREAM;

  /**
   * If true, use passive mode.
   */
  protected boolean passive = false;

  /**
   * Creates a new connection to the server using the default port.
   * @param hostname the hostname of the server to connect to
   */
  public FTPConnection (String hostname)
    throws UnknownHostException, IOException
    {
      this (hostname, -1, -1, -1, false);
    }

  /**
   * Creates a new connection to the server.
   * @param hostname the hostname of the server to connect to
   * @param port the port to connect to (if &lt;=0, use default port)
   */
  public FTPConnection (String hostname, int port)
    throws UnknownHostException, IOException
    {
      this (hostname, port, -1, -1, false);
    }

  /**
   * Creates a new connection to the server.
   * @param hostname the hostname of the server to connect to
   * @param port the port to connect to (if &lt;=0, use default port)
   * @param connectionTimeout the connection timeout, in milliseconds
   * @param timeout the I/O timeout, in milliseconds
   * @param debug print debugging information
   */
  public FTPConnection (String hostname, int port,
                        int connectionTimeout, int timeout, boolean debug)
    throws UnknownHostException, IOException
    {
      this.debug = debug;
      if (port <= 0)
        {
          port = FTP_PORT;
        }

      // Set up socket
      // TODO connection timeout
      socket = new Socket (hostname, port);
      if (timeout > 0)
        {
          socket.setSoTimeout (timeout);
        }
      
      InputStream in = socket.getInputStream ();
      in = new BufferedInputStream (in);
      in = new CRLFInputStream (in);
      this.in = new LineInputStream (in);
      OutputStream out = socket.getOutputStream ();
      this.out = new BufferedOutputStream (out);

      // Read greeting
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 220:                  // hello
          break;
        default:
          throw new FTPException (response);
        }
    }
  
  /**
   * Authenticate using the specified username and password.
   * If the username suffices for the server, the password will not be used
   * and may be null.
   * @param username the username
   * @param password the optional password
   * @return true on success, false otherwise
   */
  public boolean authenticate (String username, String password)
    throws IOException
    {
      String cmd = USER + ' ' + username;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 230:                  // User logged in
          return true;
        case 331:                 // User name okay, need password
          break;
        case 332:                 // Need account for login
        case 530:                 // No such user
          return false;
        default:
          throw new FTPException (response);
        }
      cmd = PASS + ' ' + password;
      send (cmd);
      response = getResponse ();
      switch (response.getCode ())
        {
        case 230:                  // User logged in
        case 202:                  // Superfluous
          return true;
        case 332:                  // Need account for login
        case 530:                  // Bad password
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Changes directory to the specified path.
   * @param path an absolute or relative pathname
   * @return true on success, false if the specified path does not exist
   */
  public boolean changeWorkingDirectory (String path) throws IOException
    {
      String cmd = CWD + ' ' + path;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 250:
          return true;
        case 550:
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Changes directory to the parent of the current working directory.
   * @return true on success, false otherwise
   */
  public boolean changeToParentDirectory () throws IOException
    {
      send (CDUP);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 250:
          return true;
        case 550:
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Terminates an authenticated login.
   * If file transfer is in progress, it remains active for result response
   * only.
   */
  public void reinitialize () throws IOException
    {
      send (REIN);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 220:
          if (dtp != null)
            {
              dtp.complete ();
              dtp = null;
            }
          break;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Terminates the control connection.
   * The file transfer connection remains open for result response only.
   * This connection is invalid and no further commands may be issued.
   */
  public void logout () throws IOException
    {
      send (QUIT);
      try
        {
          getResponse ();            // not required
        }
      catch (IOException e)
        {
        }
      if (dtp != null)
        {
          dtp.complete ();
          dtp = null;
        }
      try
        {
          socket.close ();
        }
      catch (IOException e)
        {
        }
    }

  /**
   * Initialise the data transfer process.
   */
  protected void initialiseDTP () throws IOException
    {
      if (dtp != null)
        {
          dtp.complete ();
          dtp = null;
        }

      InetAddress localhost = socket.getLocalAddress ();
      if (passive)
        {
          send (PASV);
          FTPResponse response = getResponse ();
          switch (response.getCode ())
            {
            case 227:
              String message = response.getMessage ();
              try
                {
                  int start = message.indexOf (',');
                  char c = message.charAt (start - 1);
                  while (c > 0x30 && c < 0x39)
                    {
                      c = message.charAt ((--start) - 1);
                    }
                  int mid1 = start;
                  for (int i = 0; i < 4; i++)
                    {
                      mid1 = message.indexOf (',', mid1 + 1);
                    }
                  int mid2 = message.indexOf (',', mid1 + 1);
                  int end = mid2;
                  c = message.charAt (end + 1);
                  while (c > 0x30 && c < 0x39)
                    {
                      c = message.charAt((++end) + 1);
                    }

                  String address =
                    message.substring (start, mid1).replace (',', '.');
                  int port_hi =
                    Integer.parseInt (message.substring (mid1 + 1, mid2));
                  int port_lo =
                    Integer.parseInt (message.substring (mid2 + 1, end + 1));
                  int port = (port_hi << 8) | port_lo;

                  /*System.out.println ("Entering passive mode: " + address +
                                      ":" + port);*/
                  dtp = new PassiveModeDTP (address, port, localhost);
                  break;
                }
              catch (ArrayIndexOutOfBoundsException e)
                {
                  throw new ProtocolException (e.getMessage () + ": " +
                                               message);
                }
              catch (NumberFormatException e)
                {
                  throw new ProtocolException (e.getMessage () + ": " +
                                               message);
                }
            default:
              throw new FTPException (response);
            }
        }
      else
        {
          // Get the local port
          int port = socket.getLocalPort () + 1;
          int tries = 0;
          // Bind the active mode DTP
          while (dtp == null)
            {
              try
                {
                  dtp = new ActiveModeDTP (localhost, port);
                  /*System.out.println ("Listening on: " + port);*/
                }
              catch (BindException e)
                {
                  port++;
                  tries++;
                  if (tries > 9)
                    {
                      throw e;
                    }
                }
            }

          // Send PORT command
          StringBuffer buf = new StringBuffer (PORT);
          buf.append (' ');
          // Construct the address/port string form
          byte[] address = localhost.getAddress ();
          for (int i = 0; i < address.length; i++)
            {
              int a = (int) address[i];
              if (a < 0)
                {
                  a += 0x100;
                }
              buf.append (a);
              buf.append (',');
            }
          int port_hi = (port & 0xff00) >> 8;
          int port_lo = (port & 0x00ff);
          buf.append (port_hi);
          buf.append (',');
          buf.append (port_lo);
          send (buf.toString ());
          // Get response
          FTPResponse response = getResponse ();
          switch (response.getCode ())
            {
            case 200:                // OK
              break;
            default:
              dtp.abort ();
              dtp = null;
              throw new FTPException (response);
            }
        }
      dtp.setTransferMode (transferMode);
    }

  /**
   * Set passive mode.
   * @param flag true if we should use passive mode, false otherwise
   */
  public void setPassive (boolean flag) throws IOException
    {
      if (passive != flag)
        {
          passive = flag;
          initialiseDTP ();
        }
    }

  /**
   * Returns the current representation type of the transfer data.
   * @return TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY
   */
  public int getRepresentationType ()
    {
      return representationType;
    }

  /**
   * Sets the desired representation type of the transfer data.
   * @param type TYPE_ASCII, TYPE_EBCDIC, or TYPE_BINARY
   */
  public void setRepresentationType (int type) throws IOException
    {
      StringBuffer buf = new StringBuffer (TYPE);
      buf.append(' ');
      switch (type)
        {
        case TYPE_ASCII:
          buf.append ('A');
          break;
        case TYPE_EBCDIC:
          buf.append ('E');
          break;
        case TYPE_BINARY:
          buf.append ('I');
          break;
        default:
          throw new IllegalArgumentException (Integer.toString (type));
        }
      //buf.append (' ');
      //buf.append ('N');
      send (buf.toString ());
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 200:
          representationType = type;
          break;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns the current file structure type.
   * @return STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE
   */
  public int getFileStructure ()
    {
      return fileStructure;
    }

  /**
   * Sets the desired file structure type.
   * @param structure STRUCTURE_FILE, STRUCTURE_RECORD, or STRUCTURE_PAGE
   */
  public void setFileStructure (int structure) throws IOException
    {
      StringBuffer buf = new StringBuffer (STRU);
      buf.append (' ');
      switch (structure)
        {
        case STRUCTURE_FILE:
          buf.append ('F');
          break;
        case STRUCTURE_RECORD:
          buf.append ('R');
          break;
        case STRUCTURE_PAGE:
          buf.append ('P');
          break;
        default:
          throw new IllegalArgumentException (Integer.toString (structure));
        }
      send (buf.toString ());
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 200:
          fileStructure = structure;
          break;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns the current transfer mode.
   * @return MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED
   */
  public int getTransferMode ()
    {
      return transferMode;
    }

  /**
   * Sets the desired transfer mode.
   * @param mode MODE_STREAM, MODE_BLOCK, or MODE_COMPRESSED
   */
  public void setTransferMode (int mode) throws IOException
    {
      StringBuffer buf = new StringBuffer (MODE);
      buf.append (' ');
      switch (mode)
        {
        case MODE_STREAM:
          buf.append ('S');
          break;
        case MODE_BLOCK:
          buf.append ('B');
          break;
        case MODE_COMPRESSED:
          buf.append ('C');
          break;
        default:
          throw new IllegalArgumentException (Integer.toString (mode));
        }
      send (buf.toString ());
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 200:
          transferMode = mode;
          if (dtp != null)
            {
              dtp.setTransferMode (mode);
            }
          break;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Retrieves the specified file.
   * @param filename the filename of the file to retrieve
   * @return an InputStream containing the file content
   */
  public InputStream retrieve (String filename) throws IOException
    {
      if (dtp == null || transferMode == MODE_STREAM)
        {
          initialiseDTP ();
        }
      /*
         int size = -1;
         String cmd = SIZE + ' ' + filename;
         send (cmd);
         FTPResponse response = getResponse ();
         switch (response.getCode ())
         {
         case 213:
         size = Integer.parseInt (response.getMessage ());
         break;
         case 550: // File not found
         default:
         throw new FTPException (response);
         }
       */
      String cmd = RETR + ' ' + filename;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 125:                  // Data connection already open; transfer starting
        case 150:                  // File status okay; about to open data connection
          return dtp.getInputStream ();
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns a stream for uploading a file.
   * If a file with the same filename already exists on the server, it will
   * be overwritten.
   * @param filename the name of the file to save the content as
   * @return an OutputStream to write the file data to
   */
  public OutputStream store (String filename) throws IOException
    {
      if (dtp == null || transferMode == MODE_STREAM)
        {
          initialiseDTP ();
        }
      String cmd = STOR + ' ' + filename;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 125:                  // Data connection already open; transfer starting
        case 150:                  // File status okay; about to open data connection
          return dtp.getOutputStream ();
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns a stream for uploading a file.
   * If a file with the same filename already exists on the server, the
   * content specified will be appended to the existing file.
   * @param filename the name of the file to save the content as
   * @return an OutputStream to write the file data to
   */
  public OutputStream append (String filename) throws IOException
    {
      if (dtp == null || transferMode == MODE_STREAM)
        {
          initialiseDTP ();
        }
      String cmd = APPE + ' ' + filename;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 125:                  // Data connection already open; transfer starting
        case 150:                  // File status okay; about to open data connection
          return dtp.getOutputStream ();
        default:
          throw new FTPException (response);
        }
    }

  /**
   * This command may be required by some servers to reserve sufficient
   * storage to accommodate the new file to be transferred.
   * It should be immediately followed by a <code>store</code> or
   * <code>append</code>.
   * @param size the number of bytes of storage to allocate
   */
  public void allocate (long size) throws IOException
    {
      String cmd = ALLO + ' ' + size;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 200:                  // OK
        case 202:                  // Superfluous
          break;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Renames a file.
   * @param oldName the current name of the file
   * @param newName the new name
   * @return true if successful, false otherwise
   */
  public boolean rename (String oldName, String newName) throws IOException
    {
      String cmd = RNFR + ' ' + oldName;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 450:                  // File unavailable
        case 550:                  // File not found
          return false;
        case 350:                 // Pending
          break;
        default:
          throw new FTPException (response);
        }
      cmd = RNTO + ' ' + newName;
      send (cmd);
      response = getResponse ();
      switch (response.getCode ())
        {
        case 250:                  // OK
          return true;
        case 450:
        case 550:
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Aborts the transfer in progress.
   * @return true if a transfer was in progress, false otherwise
   */
  public boolean abort () throws IOException
    {
      send (ABOR);
      FTPResponse response = getResponse ();
      // Abort client DTP
      if (dtp != null)
        {
          dtp.abort ();
        }
      switch (response.getCode ())
        {
        case 226:                  // successful abort
          return false;
        case 426:                 // interrupted
          response = getResponse ();
          if (response.getCode () == 226)
            {
              return true;
            }
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Causes the file specified to be deleted at the server site.
   * @param filename the file to delete
   */
  public boolean delete (String filename) throws IOException
    {
      String cmd = DELE + ' ' + filename;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 250:                  // OK
          return true;
        case 450:                 // File unavailable
        case 550:                 // File not found
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Causes the directory specified to be deleted.
   * This may be an absolute or relative pathname.
   * @param pathname the directory to delete
   */
  public boolean removeDirectory (String pathname) throws IOException
    {
      String cmd = RMD + ' ' + pathname;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 250:                  // OK
          return true;
        case 550:                 // File not found
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Causes the directory specified to be created at the server site.
   * This may be an absolute or relative pathname.
   * @param pathname the directory to create
   */
  public boolean makeDirectory (String pathname) throws IOException
    {
      String cmd = MKD + ' ' + pathname;
      send (cmd);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 257:                  // Directory created
          return true;
        case 550:                 // File not found
          return false;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns the current working directory.
   */
  public String getWorkingDirectory () throws IOException
    {
      send (PWD);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 257:
          String message = response.getMessage ();
          if (message.charAt (0) == '"')
            {
              int end = message.indexOf ('"', 1);
              if (end == -1)
                {
                  throw new ProtocolException (message);
                }
              return message.substring (1, end);
            }
          else
            {
              int end = message.indexOf (' ');
              if (end == -1)
                {
                  return message;
                }
              else
                {
                  return message.substring (0, end);
                }
            }
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns a listing of information about the specified pathname.
   * If the pathname specifies a directory or other group of files, the
   * server should transfer a list of files in the specified directory.
   * If the pathname specifies a file then the server should send current
   * information on the file.  A null argument implies the user's
   * current working or default directory.
   * @param pathname the context pathname, or null
   */
  public InputStream list (String pathname) throws IOException
    {
      if (dtp == null || transferMode == MODE_STREAM)
        {
          initialiseDTP ();
        }
      if (pathname == null)
        {
          send (LIST);
        }
      else
        {
          String cmd = LIST + ' ' + pathname;
          send (cmd);
        }
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 125:                  // Data connection already open; transfer starting
        case 150:                  // File status okay; about to open data connection
          return dtp.getInputStream ();
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns a directory listing. The pathname should specify a
   * directory or other system-specific file group descriptor; a null
   * argument implies the user's current working or default directory.
   * @param pathname the directory pathname, or null
   * @return a list of filenames (strings)
   */
  public List nameList (String pathname) throws IOException
    {
      if (dtp == null || transferMode == MODE_STREAM)
        {
          initialiseDTP ();
        }
      if (pathname == null)
        {
          send (NLST);
        }
      else
        {
          String cmd = NLST + ' ' + pathname;
          send (cmd);
        }
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 125:                  // Data connection already open; transfer starting
        case 150:                  // File status okay; about to open data connection
          InputStream in = dtp.getInputStream ();
          in = new BufferedInputStream (in);
          in = new CRLFInputStream (in);     // TODO ensure that TYPE is correct
          LineInputStream li = new LineInputStream (in);
          List ret = new ArrayList ();
          for (String line = li.readLine ();
               line != null;
               line = li.readLine ())
            {
              ret.add (line);
            }
          li.close ();
          return ret;
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Returns the type of operating system at the server.
   */
  public String system () throws IOException
    {
      send (SYST);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 215:
          String message = response.getMessage ();
          int end = message.indexOf (' ');
          if (end == -1)
            {
              return message;
            }
          else
            {
              return message.substring (0, end);
            }
        default:
          throw new FTPException (response);
        }
    }

  /**
   * Does nothing.
   * This method can be used to ensure that the connection does not time
   * out.
   */
  public void noop () throws IOException
    {
      send (NOOP);
      FTPResponse response = getResponse ();
      switch (response.getCode ())
        {
        case 200:
          break;
        default:
          throw new FTPException (response);
        }
    }

  // -- I/O --

  /**
   * Sends the specified command line to the server.
   * The CRLF sequence is automatically appended.
   * @param cmd the command line to send
   */
  protected void send (String cmd) throws IOException
    {
      byte[] data = cmd.getBytes (US_ASCII);
      out.write (data);
      out.write (CRLF);
      out.flush ();
    }

  /**
   * Reads the next response from the server.
   * If the server sends the "transfer complete" code, this is handled here,
   * and the next response is passed to the caller.
   */
  protected FTPResponse getResponse () throws IOException
    {
      FTPResponse response = readResponse ();
      if (response.getCode () == 226)
        {
          if (dtp != null)
            {
              dtp.transferComplete ();
            }
          response = readResponse ();
        }
      return response;
    }

  /**
   * Reads and parses the next response from the server.
   */
  protected FTPResponse readResponse () throws IOException
    {
      String line = in.readLine ();
      if (line == null)
        {
          throw new ProtocolException( "EOF");
        }
      if (line.length () < 4)
        {
          throw new ProtocolException (line);
        }
      int code = parseCode (line);
      if (code == -1)
        {
          throw new ProtocolException (line);
        }
      char c = line.charAt (3);
      if (c == ' ')
        {
          return new FTPResponse (code, line.substring (4));
        }
      else if (c == '-')
        {
          StringBuffer buf = new StringBuffer (line.substring (4));
          buf.append ('\n');
          while (true)
            {
              line = in.readLine ();
              if (line == null)
                {
                  throw new ProtocolException ("EOF");
                }
              if (line.length () >= 4 &&
                  line.charAt (3) == ' ' &&
                  parseCode (line) == code)
                {
                  return new FTPResponse (code, line.substring (4),
                                          buf.toString ());
                }
              else
                {
                  buf.append (line);
                  buf.append ('\n');
                }
            }
        }
      else
        {
          throw new ProtocolException (line);
        }
    }

  /*
   * Parses the 3-digit numeric code at the beginning of the given line.
   * Returns -1 on failure.
   */
  static final int parseCode (String line)
    {
      char[] c = { line.charAt (0), line.charAt (1), line.charAt (2) };
      int ret = 0;
      for (int i = 0; i < 3; i++)
        {
          int digit = ((int) c[i]) - 0x30;
          if (digit < 0 || digit > 9)
            {
              return -1;
            }
          // Computing integer powers is way too expensive in Java!
          switch (i)
            {
            case 0:
              ret += (100 * digit);
              break;
            case 1:
              ret += (10 * digit);
              break;
            case 2:
              ret += digit;
              break;
            }
        }
      return ret;
    }

}
