/* 
 */
/**
 * File:   FTPConnection.java
 * Author: Bret Taylor 
 * URL: http://www.stanford.edu/~bstaylor/cs/ftpconnection/FTPConnection.java.shtml
 * ---------------------------------------------
 * $Id$
 * ---------------------------------------------
 * Parts of this code were adopted from a variety of other FTP classes the
 * author has encountered that he was not completely satisfied with.  If you
 * think more thanks are due to any particular author than is given, please
 * let him know.  With that caveat, this class can be freely distributed and
 * modified as long as Bret Taylor is given credit in the source code comments.
 * 
 * 
 * Modified by Julian Robichaux -- http://www.nsftools.com 
 * Added constructors, logout(), listFiles(), listSubdirectories(), getAndParseDirList(),
 * processFileListCommand(), and overloaded getFullServerReply(). 
 * Also added StringBuffer parameter options to transferData() and executeDataCommand()
 * and did a few other little things.
 */

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A wrapper for the network and command protocols needed for the most common
 * FTP commands.  Standard usage looks something like this:
 *  FTPConnection connection = new FTPConnection();
 * try {
 *     if (connection.connect(host)) {
 *         if (connection.login(username, password)) {
 *             connection.downloadFile(serverFileName);
 *             connection.uploadFile(localFileName);
 *         }
 *         connection.disconnect();
 *     }
 * } catch (UnknownHostException e) {
 *     // handle unknown host
 * } catch (IOException e) {
 *     // handle I/O exception
 * }
 * Most FTP commands are wrapped by easy-to-use methods, but in case clients
 * need more flexibility, you can execute commands directly using the methods
 * executeCommand and
 * executeDataCommand,
 * the latter of which is used for commands that require an open data port.
 * 
 * @author Bret Taylor
 * @author Julian Robichaux
 * @version 1.01
 */
public class FTPConnection extends Object {

    /**
     * If this flag is on, we print out debugging information to stdout during
     * execution.  Useful for debugging the FTP class and seeing the server's
     * responses directly.
     */
    private static boolean PRINT_DEBUG_INFO = false;
    
    /**
     * The socket through which we are connected to the FTP server.
     */
    private Socket connectionSocket = null;
    /**
     * The socket output stream.
     */
    private PrintStream outputStream = null;
    /**
     * The socket input stream.
     */
    private BufferedReader inputStream = null;

    /**
     * The offset at which we resume a file transfer.
     */
    private long restartPoint = 0L;

    /**
     * Added by Julian: If this flag is on, we're currently logged in to something.
     */
    private boolean loggedIn = false;
    
    /**
     * Added by Julian: This is the line terminator to use for multi-line responses.
     */
    public String lineTerm = "\n";
    
    /**
     * Added by Julian: This is the size of the data blocks we use for transferring
     * files.
     */
    private static int BLOCK_SIZE = 4096;
    

    /**
     * Added by Julian: After you create an FTPConnection object, you will call the
     * connect() and login() methods to access your server. Please don't forget to
     * logout() and disconnect() when you're done (it's only polite...).
     */
    public FTPConnection ()
    {
    	// default constructor (obviously) -- this is just good to have...
    }
    

    /**
     * Added by Julian: Allows you to specify if you want to send debug output to
     * the console (true if you do, false if you don't).
     */
    public FTPConnection (boolean debugOut)
    {
    	PRINT_DEBUG_INFO = debugOut;
    }
    

    /**
     * Prints debugging information to stdout if the private flag
     * PRINT_DEBUG_INFO is turned on.
     */
    private void debugPrint(String message) {
        if (PRINT_DEBUG_INFO) System.err.println(message);
    }


    /**
     * Connects to the given FTP host on port 21, the default FTP port.
     */
    public boolean connect(String host)
        throws UnknownHostException, IOException
    {
        return connect(host, 21);
    }


    /**
     * Connects to the given FTP host on the given port.
     */
    public boolean connect(String host, int port)
        throws UnknownHostException, IOException
    {
        connectionSocket = new Socket(host, port);
        outputStream = new PrintStream(connectionSocket.getOutputStream());
        inputStream = new BufferedReader(new
                       InputStreamReader(connectionSocket.getInputStream()));

        if (!isPositiveCompleteResponse(getServerReply())){
            disconnect();
            return false;
        }

        return true;
    }


    /**
     * Disconnects from the host to which we are currently connected.
     */
    public void disconnect()
    {
        if (outputStream != null) {
            try {
        		if (loggedIn) { logout(); };
                outputStream.close();
                inputStream.close();
                connectionSocket.close();
            } catch (IOException e) {}

            outputStream = null;
            inputStream = null;
            connectionSocket = null;
        }
    }


    /**
     * Wrapper for the commands user [username] and pass
     * [password].
     */
    public boolean login(String username, String password)
        throws IOException
    {
        int response = executeCommand("user " + username);
        if (!isPositiveIntermediateResponse(response)) return false;
        response = executeCommand("pass " + password);
        loggedIn = isPositiveCompleteResponse(response);
        return loggedIn;
    }


    /**
     * Added by Julian: Logout before you disconnect (this is good form).
     */
    public boolean logout()
        throws IOException
    {
        int response = executeCommand("quit");
        loggedIn = !isPositiveCompleteResponse(response);
        return !loggedIn;
    }


    /**
     * Wrapper for the command cwd [directory].
     */
    public boolean changeDirectory(String directory)
        throws IOException
    {
        int response = executeCommand("cwd " + directory);
        return isPositiveCompleteResponse(response);
    }


    /**
     * Wrapper for the commands rnfr [oldName] and rnto
     * [newName].
     */
    public boolean renameFile(String oldName, String newName)
        throws IOException
    {
        int response = executeCommand("rnfr " + oldName);
        if (!isPositiveIntermediateResponse(response)) return false;
        response = executeCommand("rnto " + newName);
        return isPositiveCompleteResponse(response);
    }
 

    /**
     * Wrapper for the command mkd [directory].
     */
    public boolean makeDirectory(String directory)
        throws IOException
    {
        int response = executeCommand("mkd " + directory);
        return isPositiveCompleteResponse(response);
    }
 

    /**
     * Wrapper for the command rmd [directory].
     */
    public boolean removeDirectory(String directory)
        throws IOException
    {
        int response = executeCommand("rmd " + directory);
        return isPositiveCompleteResponse(response);
    }
 

    /**
     * Wrapper for the command cdup.
     */
    public boolean parentDirectory()
        throws IOException
    {
        int response = executeCommand("cdup");
        return isPositiveCompleteResponse(response);
    }
 

    /**
     * Wrapper for the command dele [fileName].
     */
    public boolean deleteFile(String fileName)
        throws IOException
    {
        int response = executeCommand("dele " + fileName);
        return isPositiveCompleteResponse(response);
    }
 

    /**
     * Wrapper for the command pwd.
     */
    public String getCurrentDirectory()
        throws IOException
    {
        String response = getExecutionResponse("pwd");
        StringTokenizer strtok = new StringTokenizer(response);

        // Get rid of the first token, which is the return code
        if (strtok.countTokens() < 2) return null;
        strtok.nextToken();
        String directoryName = strtok.nextToken();

        // Most servers surround the directory name with quotation marks
        int strlen = directoryName.length();
        if (strlen == 0) return null;
        if (directoryName.charAt(0) == '\"') {
            directoryName = directoryName.substring(1);
            strlen--;
        }
        if (directoryName.charAt(strlen - 1) == '\"')
            return directoryName.substring(0, strlen - 1);
        return directoryName;
    }
 

    /**
     * Wrapper for the command syst.
     */
    public String getSystemType()
        throws IOException
    {
        return excludeCode(getExecutionResponse("syst"));
    }
 

    /**
     * Wrapper for the command mdtm [fileName].  If the file does
     * not exist, we return -1;
     */
    public long getModificationTime(String fileName)
        throws IOException
    {
        String response = excludeCode(getExecutionResponse("mdtm " + fileName));
        try {
            return Long.parseLong(response);
        } catch (Exception e) {
            return -1L;
        }
    }
 

    /**
     * Wrapper for the command size [fileName].  If the file does
     * not exist, we return -1;
     */
    public long getFileSize(String fileName)
        throws IOException
    {
        String response = excludeCode(getExecutionResponse("size " + fileName));
        try {
            return Long.parseLong(response);
        } catch (Exception e) {
            return -1L;
        }
    }


    /**
     * Wrapper for the command retr [fileName].
     */
    public boolean downloadFile(String fileName)
        throws IOException
    {
        return readDataToFile("retr " + fileName, fileName);
    }


    /**
     * Wrapper for the command retr [serverPath]. The local file
     * path to which we will write is given by localPath. 
     */
    public boolean downloadFile(String serverPath, String localPath)
        throws IOException
    {
        return readDataToFile("retr " + serverPath, localPath);
    }


    /**
     * Wrapper for the command stor [fileName].
     */
    public boolean uploadFile(String fileName)
        throws IOException
    {
        return writeDataFromFile("stor " + fileName, fileName);
    }


    /**
     * Wrapper for the command stor [localPath]. The server file
     * path to which we will write is given by serverPath. 
     */
    public boolean uploadFile(String serverPath, String localPath)
        throws IOException
    {
        return writeDataFromFile("stor " + serverPath, localPath);
    }


    /**
     * Set the restart point for the next download or upload operation.  This
     * lets clients resume interrupted uploads or downloads.
     */
    public void setRestartPoint(int point)
    {
        restartPoint = point;
        debugPrint("Restart noted");
    }


    /** 
     * Gets server reply code from the control port after an ftp command has
     * been executed.  It knows the last line of the response because it begins
     * with a 3 digit number and a space, (a dash instead of a space would be a
     * continuation).
     */
    private int getServerReply()
        throws IOException
    {
        return Integer.parseInt(getFullServerReply().substring(0, 3));
    }


    /** 
     * Gets server reply string from the control port after an ftp command has
     * been executed.  This consists only of the last line of the response,
     * and only the part after the response code.
     */
    private String getFullServerReply()
        throws IOException
    {
        String reply;

        do {
            reply = inputStream.readLine();
            debugPrint(reply);
        } while(!(Character.isDigit(reply.charAt(0)) && 
                  Character.isDigit(reply.charAt(1)) &&
                  Character.isDigit(reply.charAt(2)) &&
                  reply.charAt(3) == ' '));

        return reply;
    }
    

    /**
     * Added by Julian: Returns the last line of the server reply, but also
     * returns the full multi-line reply in a StringBuffer parameter.
     */
    private String getFullServerReply(StringBuffer fullReply)
    	throws IOException
    {
        String reply;
        fullReply.setLength(0);

        do {
            reply = inputStream.readLine();
            debugPrint(reply);
            fullReply.append(reply + lineTerm);
        } while(!(Character.isDigit(reply.charAt(0)) && 
                  Character.isDigit(reply.charAt(1)) &&
                  Character.isDigit(reply.charAt(2)) &&
                  reply.charAt(3) == ' '));
		
		// remove any trailing line terminators from the fullReply
		if (fullReply.length() > 0)  
		{  
			fullReply.setLength(fullReply.length() - lineTerm.length());
		}
		
        return reply;
    }


    /** 
     * Added by Julian: Gets a list of files in the current directory.
     */
	public String listFiles()
		throws IOException
	{
		return listFiles("");
	}
	

    /** 
     * Added by Julian: Gets a list of files in either the current
     * directory, or one specified as a parameter. The 'params' parameter
     * can be either a directory name, a file mask, or both (such as
     * '/DirName/*.txt').
     */
	public String listFiles(String params)
		throws IOException
	{
		StringBuffer files = new StringBuffer();
		StringBuffer dirs = new StringBuffer();
		if (!getAndParseDirList(params, files, dirs))
		{
			debugPrint("Error getting file list");
		}
		
		return files.toString();
	}
	

    /** 
     * Added by Julian: Gets a list of subdirectories in the current directory.
     */
	public String listSubdirectories()
		throws IOException
	{
		return listSubdirectories("");
	}
	

    /** 
     * Added by Julian: Gets a list of subdirectories in either the current
     * directory, or one specified as a parameter. The 'params' parameter
     * can be either a directory name, a name mask, or both (such as
     * '/DirName/Sub*').
     */
	public String listSubdirectories(String params)
		throws IOException
	{
		StringBuffer files = new StringBuffer();
		StringBuffer dirs = new StringBuffer();
		if (!getAndParseDirList(params, files, dirs))
		{
			debugPrint("Error getting dir list");
		}
		
		return dirs.toString();
	}
	

    /** 
     * Added by Julian: Sends and gets the results of a file list command,
     * like LIST or NLST.
     */
    private String processFileListCommand(String command)
        throws IOException
    {
        StringBuffer reply = new StringBuffer();
        String replyString;
        
        // file listings require you to issue a PORT command, 
        // like a file transfer
		boolean success = executeDataCommand(command, reply);
		if (!success)
		{
			return "";
		}
		
        replyString = reply.toString();
        // strip the trailing line terminator from the reply
        if (reply.length() > 0)
        {
        	return replyString.substring(0, reply.length() - 1);
        }  else  {
        	return replyString;
        }
    }


	/**
	 * Added by Julian: Gets a directory list from the server and parses
	 * the elements into a list of files and a list of subdirectories.
	 */
	private boolean getAndParseDirList(String params, StringBuffer files, StringBuffer dirs)
		throws IOException
	{
		// reset the return variables (we're using StringBuffers instead of
		// Strings because you can't change a String value and pass it back
		// to the calling routine -- changing a String creates a new object)
		files.setLength(0);
		dirs.setLength(0);
		
		// get the NLST and the LIST -- don't worry if the commands
		// don't work, because we'll just end up sending nothing back
		// if that's the case
		String shortList = processFileListCommand("nlst " + params);
		String longList = processFileListCommand("list " + params);
		
		// tokenize the lists we got, using a newline as a separator
		StringTokenizer sList = new StringTokenizer(shortList, "\n");
		StringTokenizer lList = new StringTokenizer(longList, "\n");
		
		// other variables we'll need
		String sString;
		String lString;
		
		// assume that both lists have the same number of elements
		while ((sList.hasMoreTokens()) && (lList.hasMoreTokens())) {
			sString = sList.nextToken();
			lString = lList.nextToken();
			
			if (lString.length() > 0)
			{
				if (lString.startsWith("d"))
				{
					dirs.append(sString.trim() + lineTerm);
					debugPrint("Dir: " + sString);
				}  else if (lString.startsWith("-"))  {
					files.append(sString.trim() + lineTerm);
					debugPrint("File: " + sString);
				}  else  {
					// actually, symbolic links will start with an "l"
					// (lowercase L), but we're not going to mess with
					// those
					debugPrint("Unknown: " + lString);
				}
			}
		}
		
		// strip off any trailing line terminators and return the values
		if (files.length() > 0)  {  files.setLength(files.length() - lineTerm.length());  }
		if (dirs.length() > 0)  {  dirs.setLength(dirs.length() - lineTerm.length());  }
		
		return true;
	}
	

    /**
     * Executes the given FTP command on our current connection, returning the
     * three digit response code from the server.  This method only works for
     * commands that do not require an additional data port.
     */
    public int executeCommand(String command)
        throws IOException
    {
        outputStream.println(command);
        return getServerReply();
    }


    /**
     * Executes the given FTP command on our current connection, returning the
     * last line of the server's response.  Useful for commands that return
     * one line of information.
     */
    public String getExecutionResponse(String command)
        throws IOException
    {
        outputStream.println(command);
        return getFullServerReply();
    }


    /**
     * Executes the given ftpd command on the server and writes the results
     * returned on the data port to the file with the given name, returning true
     * if the server indicates that the operation was successful.
     */
    public boolean readDataToFile(String command, String fileName)
        throws IOException
    {
        // Open the local file
        RandomAccessFile outfile = new RandomAccessFile(fileName, "rw");

        // Do restart if desired
        if (restartPoint != 0) {
            debugPrint("Seeking to " + restartPoint);
            outfile.seek(restartPoint);
        }

        // Convert the RandomAccessFile to an OutputStream
        FileOutputStream fileStream = new FileOutputStream(outfile.getFD());
        boolean success = executeDataCommand(command, fileStream);

        outfile.close();

        return success;
    }


    /**
     * Executes the given ftpd command on the server and writes the contents
     * of the given file to the server on an opened data port, returning true
     * if the server indicates that the operation was successful.
     */
    public boolean writeDataFromFile(String command, String fileName)
        throws IOException
    {
        // Open the local file
        RandomAccessFile infile = new RandomAccessFile(fileName, "r");

        // Do restart if desired
        if (restartPoint != 0) {
            debugPrint("Seeking to " + restartPoint);
            infile.seek(restartPoint);
        }

        // Convert the RandomAccessFile to an InputStream
        FileInputStream fileStream = new FileInputStream(infile.getFD());
        boolean success = executeDataCommand(command, fileStream);

        infile.close();

        return success;
    }


    /**
     * Executes the given ftpd command on the server and writes the results
     * returned on the data port to the given OutputStream, returning true
     * if the server indicates that the operation was successful.
     */
    public boolean executeDataCommand(String command, OutputStream out)
        throws IOException
    {
        // Open a data socket on this computer
        ServerSocket serverSocket = new ServerSocket(0);
        if (!setupDataPort(command, serverSocket)) return false;
        Socket clientSocket = serverSocket.accept();

        // Transfer the data
        InputStream in = clientSocket.getInputStream();
        transferData(in, out);

        // Clean up the data structures
        in.close();
        clientSocket.close();
        serverSocket.close();

        return isPositiveCompleteResponse(getServerReply());    
    }


    /**
     * Executes the given ftpd command on the server and writes the contents
     * of the given InputStream to the server on an opened data port, returning
     * true if the server indicates that the operation was successful.
     */
    public boolean executeDataCommand(String command, InputStream in)
        throws IOException
    {
        // Open a data socket on this computer
        ServerSocket serverSocket = new ServerSocket(0);
        if (!setupDataPort(command, serverSocket)) return false;
        Socket clientSocket = serverSocket.accept();

        // Transfer the data
        OutputStream out = clientSocket.getOutputStream();
        transferData(in, out);

        // Clean up the data structures
        out.close();
        clientSocket.close();
        serverSocket.close();

        return isPositiveCompleteResponse(getServerReply());    
    }


    /**
     * Added by Julian: Executes the given ftpd command on the server 
     * and writes the results returned on the data port to the given 
     * StringBuffer, returning true if the server indicates that the 
     * operation was successful.
     */
    public boolean executeDataCommand(String command, StringBuffer sb)
        throws IOException
    {
        // Open a data socket on this computer
        ServerSocket serverSocket = new ServerSocket(0);
        if (!setupDataPort(command, serverSocket)) return false;
        Socket clientSocket = serverSocket.accept();

        // Transfer the data
        InputStream in = clientSocket.getInputStream();
        transferData(in, sb);

        // Clean up the data structures
        in.close();
        clientSocket.close();
        serverSocket.close();

        return isPositiveCompleteResponse(getServerReply());    
    }


    /**
     * Transfers the data from the given input stream to the given output
     * stream until we reach the end of the stream.
     */
    private void transferData(InputStream in, OutputStream out)
        throws IOException
    {
        byte b[] = new byte[BLOCK_SIZE];
        int amount;

        // Read the data into the file
        while ((amount = in.read(b)) > 0) {
            out.write(b, 0, amount);
        }
    }


    /**
     * Added by Julian: Transfers the data from the given input stream 
     * to the given StringBuffer until we reach the end of the stream.
     */
    private void transferData(InputStream in, StringBuffer sb)
        throws IOException
    {
        byte b[] = new byte[BLOCK_SIZE];
        int amount;

        // Read the data into the StringBuffer
        while ((amount = in.read(b)) > 0) {
            sb.append(new String(b, 0, amount));
        }
    }


    /**
     * Executes the given ftpd command on the server and writes the results
     * returned on the data port to the given FilterOutputStream, returning true
     * if the server indicates that the operation was successful.
     */
    private boolean setupDataPort(String command, ServerSocket serverSocket)
        throws IOException
    {
        // Send our local data port to the server
        if (!openPort(serverSocket)) return false;

        // Set binary type transfer
        outputStream.println("type i");
        if (!isPositiveCompleteResponse(getServerReply())) {
            debugPrint("Could not set transfer type");
            return false;
        }

        // If we have a restart point, send that information
        if (restartPoint != 0) {
            outputStream.println("rest " + restartPoint);
            restartPoint = 0;
            // TODO: Interpret server response here
            getServerReply();
        }

        // Send the command
        outputStream.println(command);

        return isPositivePreliminaryResponse(getServerReply());
    }


    /**
     * Get IP address and port number from serverSocket and send them via the
     * port command to the ftp server, returning true if we get a
     * valid response from the server, returning true if the server indicates
     * that the operation was successful.
     */
    private boolean openPort(ServerSocket serverSocket)
        throws IOException
    {                        
        int localport = serverSocket.getLocalPort();

        // get local ip address
        InetAddress inetaddress = serverSocket.getInetAddress();
        InetAddress localip;
        try {
            localip = inetaddress.getLocalHost();
        } catch(UnknownHostException e) {
            debugPrint("Can't get local host");
            return false;
        }

        // get ip address in high byte order
        byte[] addrbytes = localip.getAddress();

        // tell server what port we are listening on
        short addrshorts[] = new short[4];

        // problem:  bytes greater than 127 are printed as negative numbers
        for(int i = 0; i <= 3; i++) {
            addrshorts[i] = addrbytes[i];
            if (addrshorts[i] < 0)
                addrshorts[i] += 256;
        }

        outputStream.println("port " + addrshorts[0] + "," + addrshorts[1] +
                             "," + addrshorts[2] + "," + addrshorts[3] + "," +
                             ((localport & 0xff00) >> 8) + "," +
                             (localport & 0x00ff));

        return isPositiveCompleteResponse(getServerReply());
    }


    /**
     * True if the given response code is in the 100-199 range.
     */
    private boolean isPositivePreliminaryResponse(int response)
    {
        return (response >= 100 && response < 200);
    }


    /**
     * True if the given response code is in the 300-399 range.
     */
    private boolean isPositiveIntermediateResponse(int response)
    {
        return (response >= 300 && response < 400);
    }

    /**
     * True if the given response code is in the 200-299 range.
     */
    private boolean isPositiveCompleteResponse(int response)
    {
        return (response >= 200 && response < 300);
    }


    /**
     * True if the given response code is in the 400-499 range.
     */
    private boolean isTransientNegativeResponse(int response)
    {
        return (response >= 400 && response < 500);
    }


    /**
     * True if the given response code is in the 500-599 range.
     */
    private boolean isPermanentNegativeResponse(int response)
    {
        return (response >= 500 && response < 600);
    }


    /**
     * Eliminates the response code at the beginning of the response string.
     */
    private String excludeCode(String response)
    {
        if (response.length() < 5) return response;
        return response.substring(4);
    }

}

