org.ikasan.connector.ftp.net.FileTransferProtocolClient.java Source code

Java tutorial

Introduction

Here is the source code for org.ikasan.connector.ftp.net.FileTransferProtocolClient.java

Source

/*
 * $Id$
 * $URL$
 * 
 * ====================================================================
 * Ikasan Enterprise Integration Platform
 * 
 * Distributed under the Modified BSD License.
 * Copyright notice: The copyright for this software and a full listing 
 * of individual contributors are as shown in the packaged copyright.txt 
 * file. 
 * 
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 *
 *  - Redistributions of source code must retain the above copyright notice, 
 *    this list of conditions and the following disclaimer.
 *
 *  - Redistributions in binary form must reproduce the above copyright notice, 
 *    this list of conditions and the following disclaimer in the documentation 
 *    and/or other materials provided with the distribution.
 *
 *  - Neither the name of the ORGANIZATION nor the names of its contributors may
 *    be used to endorse or promote products derived from this software without 
 *    specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 
 * USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 * ====================================================================
 */
package org.ikasan.connector.ftp.net;

import java.io.*;
import java.net.BindException;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.StringTokenizer;

import javax.resource.ResourceException;

import org.apache.commons.net.ftp.*;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.ikasan.connector.basefiletransfer.net.BaseFileTransferMappedRecord;
import org.ikasan.connector.basefiletransfer.net.BaseFileTransferUtils;
import org.ikasan.connector.basefiletransfer.net.ClientCommandCdException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandGetException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandLsException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandMkdirException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandPutException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandPwdException;
import org.ikasan.connector.basefiletransfer.net.ClientCommandRenameException;
import org.ikasan.connector.basefiletransfer.net.ClientConnectionException;
import org.ikasan.connector.basefiletransfer.net.ClientException;
import org.ikasan.connector.basefiletransfer.net.ClientInitialisationException;
import org.ikasan.connector.basefiletransfer.net.ClientListEntry;
import org.ikasan.connector.basefiletransfer.net.ClientPolarisedFilter;

/**
 * <p>
 * This class provides the basic functionality of an FTP client based on
 * Apache's commons-net library. Some of the basic functionality is wrapped to
 * provide <code>FileTransferProtocolClient</code> specific exceptions, or to
 * provide the functionality required by a framework component.
 * </p>
 * 
 * TODO This has a good deal of similar code to SFTP client, might be able to
 * get some common code out of that
 * 
 * @author Ikasan Development Team
 */
public class FileTransferProtocolClient implements FileTransferProtocol {

    /** Initialising the logger */
    private static Logger logger = Logger.getLogger(FileTransferProtocolClient.class);

    /** Whether we should transmit in Active mode (passive by default) */
    private boolean active;

    /** Remote Hostname */
    private String remoteHostname;

    /** Local Hostname */
    private String localHostname = null;

    /** Maximum no of times to retry connection */
    private Integer maxRetryAttempts;

    /** Password */
    private String password;

    /** Remote Port Number */
    private Integer remotePort;

    /** The default maximum local port number in Hex */
    private static final Integer DEFAULT_MAXIMUM_LOCAL_PORT = 0xFFFF;

    /** User name */
    private String username;

    /**
     * FTP Client systemKey, if set, expected to be in the
     * FTPClientConfig.SYST_* range
     */
    private String systemKey;

    /** Timeout in milliseconds to use when opening a socket. Defaults to 60000 ms (1 min) */
    private Integer connectionTimeout = 60000;

    /** Timeout in milliseconds to use when reading from the data connection. Defaults to 300000 ms (5 min)*/
    private Integer dataTimeout = 300000;

    /** Timeout in milliseconds of a currently open connection. Defaults to 300000 ms (5 min) */
    private Integer socketTimeout = 300000;

    /** Third party library that implements much of FTP for us */
    private FTPClient ftpClient;

    /**
     * Constructor
     * 
     * @param active Whether we are active or passive mode
     * @param remoteHostname The remote hostname
     * @param localHost the local host
     * @param maxRetryAttempts The maximum amount of retries
     * @param password The password to connect with
     * @param remotePort The remote port to connect to
     * @param username The username
     * @param systemKey The system key
     * @param connectionTimeout connection timeout
     * @param soTimeout Socket timeout in ms after getting a connection
     * @param dataTimeout Data connection timeout in ms after opening a socket
     */
    public FileTransferProtocolClient(boolean active, String remoteHostname, String localHost,
            Integer maxRetryAttempts, String password, Integer remotePort, String username, String systemKey,
            Integer connectionTimeout, Integer soTimeout, Integer dataTimeout) {
        super();
        this.active = active;
        this.remoteHostname = remoteHostname;
        this.maxRetryAttempts = maxRetryAttempts;
        this.password = password;
        this.remotePort = remotePort;
        if (localHost != null && localHost.length() > 0) {
            this.localHostname = localHost;
        }
        if (connectionTimeout != null) {
            this.connectionTimeout = connectionTimeout;
        }
        if (soTimeout != null) {
            this.socketTimeout = soTimeout;
        }
        if (dataTimeout != null) {
            this.dataTimeout = dataTimeout;
        }
        this.username = username;
        this.systemKey = systemKey;
    }

    /**
     * All constructor arguments are essential, so use this method to validate
     * them before attempting connection
     * 
     * @throws ClientInitialisationException if any of the required parameters
     *             are invalid
     */
    public void validateConstructorArgs() throws ClientInitialisationException {
        // If all values seem OK, log the info and return
        if (this.remoteHostname != null && this.password != null && this.username != null) {
            return;
        }
        // Otherwise, trace through, log the erroneous parameters and throw an
        // exception
        StringBuilder sb = new StringBuilder("The following arguments are erroneous or missing:\n"); //$NON-NLS-1$
        if (this.remoteHostname == null || this.remoteHostname.length() == 0) {
            sb.append("Invalid hostname! [");
            sb.append(this.remoteHostname);
            sb.append("]");
        }
        if (this.password == null || this.password.length() == 0) {
            sb.append("Invalid password! ["); //$NON-NLS-1$
            sb.append(this.password);
            sb.append("]\n"); //$NON-NLS-1$
        }
        if (this.username == null || this.username.length() == 0) {
            sb.append("Invalid username! [");
            sb.append(this.username);
            sb.append("]\n");
        }
        throw new ClientInitialisationException(sb.toString());
    }

    /**
     * Connect to the FTP server
     * 
     * @throws ClientConnectionException Exception thrown when we can't connect
     */
    public void connect() throws ClientConnectionException {
        echoConfig(Level.DEBUG);
        // Checking the connection state,
        String msg = new String("Checking connection status... "); //$NON-NLS-1$
        logger.debug(msg + "[" + (isConnected() ? "connected" : "disconnected") + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        // and acting accordingly.
        if (isConnected()) {
            logger.debug("Already connected!"); //$NON-NLS-1$
            return;
        }
        // try to gain a connection, up to the max no of retries - if one exists
        int maxAttemptConnection = 1;
        if (maxRetryAttempts != null) {
            maxAttemptConnection = maxRetryAttempts.intValue();
        }
        logger.debug("about to attempt up to [" + maxAttemptConnection //$NON-NLS-1$
                + "] times to establish a connection"); //$NON-NLS-1$
        ClientConnectionException connectionException = null;
        for (int retryCount = 0; retryCount < maxAttemptConnection; retryCount++) {
            try {
                doConnect();
                if (isConnected()) {
                    logger.debug("Connection established on attempt [" //$NON-NLS-1$
                            + (retryCount + 1) + "]"); //$NON-NLS-1$
                    break;
                }
                // Default else
                logger.warn("Attempt [" + (retryCount + 1) //$NON-NLS-1$
                        + "] failed to connect, without exception!"); //$NON-NLS-1$
            } catch (ClientConnectionException e) {
                connectionException = e;
                logger.warn("Attempt [" + (retryCount + 1) + "] failed to connect due to : ", e); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
        }
        // if we are still not connected after retrying, we need to throw the
        // exception from the last failure
        if (!isConnected()) {
            if (connectionException != null) {
                logger.error("Failed to connect after ]" + maxAttemptConnection //$NON-NLS-1$
                        + "] retries."); //$NON-NLS-1$
                throw connectionException;
            }
            // Default else
            logger.error("Failed to connect after [" + maxAttemptConnection //$NON-NLS-1$
                    + "] retries, but ClientConnectionException was not thrown!!"); //$NON-NLS-1$
        }
        logger.debug("Connected!"); //$NON-NLS-1$
    }

    /**
     * login to the FTP server
     * 
     * @throws ClientConnectionException Exception thrown when we can't connect
     */
    public void login() throws ClientConnectionException {
        echoConfig(Level.DEBUG);
        // Checking the connection state,
        String msg = new String("Checking connection status... "); //$NON-NLS-1$
        logger.debug(msg + "[" + (isConnected() ? "connected" : "disconnected") + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
        if (isConnected()) {
            try {
                if (ftpClient.login(username, password)) {
                    /*
                     * Event though the FTP spec says that we should default to
                     * ASCII mode, as we do not yet support mode changes, we
                     * will default into BINARY mode, as that guarantees that
                     * the file will be transfered explicitly and not have any
                     * UNIX <-> Windows character substitutions made.
                     */
                    ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
                    logger.info("Successfully logged in to [" + remoteHostname + "] with user [" + username + "]");
                } else {
                    throw new ClientConnectionException("Login was refused."); //$NON-NLS-1$
                }
            } catch (IOException e) {
                throw new ClientConnectionException("IOException caught trying to login."); //$NON-NLS-1$
            }
        } else {
            throw new ClientConnectionException("Tried to login with disconnected client!"); //$NON-NLS-1$
        }
    }

    /**
     * Method that handles the creation and connection for FTP.
     * 
     * @throws ClientConnectionException if connection attempt fails
     */
    private void doConnect() throws ClientConnectionException {

        this.ftpClient = new FTPClient();

        String msg = new String("Attempting connection to [" + remoteHostname + "]."); //$NON-NLS-1$ //$NON-NLS-2$
        logger.debug(msg);
        try {
            /*
             * Summer (Nov 26th 2008): Rather than relying on the FTP client to
             * figure out the system it is connecting to (and hence what parsers
             * it should use) we pass this configuration to the client and force
             * it to use it.
             */
            if ((systemKey != null) && (!"".equals(systemKey))) {
                ftpClient.configure(new FTPClientConfig(systemKey));
            }
            // leave local port unspecified
            int localPort = 0;
            this.ftpClient.setDefaultTimeout(this.connectionTimeout);
            // Keep trying to connect, until successful
            for (int i = 0; i < DEFAULT_MAXIMUM_LOCAL_PORT; i++) {
                try {
                    logger.debug("Connecting to remote host [" + this.remoteHostname + ":" + this.remotePort
                            + "] from local host [" + this.localHostname + ":" + localPort + "].");
                    // Had to update the  ftpClient.connect method as the localhost was not resolving correctly
                    //ftpClient.connect(InetAddress.getByName(this.remoteHostname), this.remotePort, InetAddress.getByName(this.localHostname), localPort);
                    ftpClient.connect(InetAddress.getByName(this.remoteHostname), this.remotePort);

                    int reply = ftpClient.getReplyCode();
                    if (!FTPReply.isPositiveCompletion(reply)) {
                        throw new SocketException("Connection attempt failed with replyCode [" + reply + "]");
                    }

                    if (active) {
                        this.ftpClient.enterLocalActiveMode();
                    } else {
                        this.ftpClient.enterLocalPassiveMode();
                    }

                    this.ftpClient.setSoTimeout(this.socketTimeout);
                    this.ftpClient.setDataTimeout(this.dataTimeout);
                    break;
                } catch (BindException be) {
                    logger.info("Address is already in use.. will try again.", be);
                }
            }
        } catch (SocketException se) {
            msg = new String(msg + " [Failed]"); //$NON-NLS-1$
            logger.info(msg, se);
            // Clean up after ourselves just in case
            try {
                if (this.ftpClient != null && this.ftpClient.isConnected()) {
                    this.ftpClient.disconnect();
                }
            } catch (IOException disconnectException) {
                logger.warn("Could not cleanup after a failed connect, this may leave behind open sockets.",
                        disconnectException);
            }
            throw new ClientConnectionException(msg, se);
        } catch (IOException ie) {
            msg = new String(msg + " [Failed]"); //$NON-NLS-1$
            logger.info(msg, ie);
            // Clean up after ourselves
            try {
                if (this.ftpClient != null && this.ftpClient.isConnected()) {
                    this.ftpClient.disconnect();
                }
            } catch (IOException disconnectException) {
                logger.warn("Could not cleanup after a failed connect, this may leave behind open sockets.",
                        disconnectException);
            }
            throw new ClientConnectionException(msg, ie);
        }

        logger.info(
                "Connected to host [" + remoteHostname + "]. " + "Mode [" + (active ? "active" : "passive") + "].");
    }

    /**
     * Method that tests if the underlying library is valid and connected.
     * 
     * @return <code>true</code> if fully connected, <code>false</code>
     *         otherwise
     */
    public boolean isConnected() {
        if (this.ftpClient != null) {
            return this.ftpClient.isConnected();
        }
        return false;
    }

    /**
     * Disconnect
     */
    public void disconnect() {
        logger.debug("Disconnecting..."); //$NON-NLS-1$
        try {
            if (this.ftpClient.logout()) {
                logger.debug("logout... [OK]");
            } else {
                logger.info("logout... [FAILED], continuing with disconnect...");
            }

            this.ftpClient.disconnect();
            logger.debug("Disconnected... [OK]"); //$NON-NLS-1$
        } catch (IOException e) {
            logger.info("Disconnecting... [Failed] due to: ", e); //$NON-NLS-1$
        }
    }

    /**
     * Method used to log the configuration information used to initialise the
     * client.
     * 
     * @param logLevel The log level at which to log the information
     */
    public void echoConfig(Level logLevel) {
        StringBuilder sb = new StringBuilder(256);
        sb.append("FTP configuration information:"); //$NON-NLS-1$
        sb.append("\nRemote Hostname  = [");
        sb.append(remoteHostname);
        sb.append("]\nLocal Hostname   = [");
        sb.append(localHostname);
        sb.append("]\nPassword         = [");
        // sb.append(password);
        // We do not want to log the password
        sb.append("********");
        sb.append("]\nRemote Port      = [");
        sb.append(remotePort);
        sb.append("]\nUsername         = [");
        sb.append(username);
        sb.append("]"); //$NON-NLS-1$
        logger.log(logLevel, sb.toString());
    }

    public void ensureConnection() throws ResourceException {
        if (!isConnected()) {
            try {
                connect();
                login();
            } catch (ClientConnectionException e1) {
                throw new ResourceException(
                        "Failed to ensure that the underlying ftp connection is still open. Likely this was previously open, closed prematurely, and now cannot be reestablished", //$NON-NLS-1$
                        e1);
            }
        }
    }

    public void cd(String targetPath) throws ClientCommandCdException {
        // This code is not quite ideal as printWorkingDirectory() could fail
        // but the net result is that the developer will be informed of the
        // fault regardless
        String currentDirectory = null;
        try {
            currentDirectory = this.ftpClient.printWorkingDirectory();
            if (!this.ftpClient.changeWorkingDirectory(targetPath)) {
                throw new ClientCommandCdException("Failed to call directory [" + targetPath //$NON-NLS-1$
                        + "] from [" + currentDirectory + "]"); //$NON-NLS-1$//$NON-NLS-2$
            }
            logger.debug("CD from [" + currentDirectory + "] to [" + this.ftpClient.printWorkingDirectory() + "].");
        } catch (IOException e) {
            throw new ClientCommandCdException("Failed to call directory [" + targetPath //$NON-NLS-1$
                    + "] from [" + currentDirectory + "]", e); //$NON-NLS-1$//$NON-NLS-2$
        }
    }

    public void deleteRemoteDirectory(String directoryPath, boolean recurse)
            throws ClientException, ClientCommandLsException {
        try {
            if (recurse) {
                try {
                    List<ClientListEntry> entryList = ls(directoryPath);
                    for (ClientListEntry entry : entryList) {
                        String filePath = entry.getUri().getPath();
                        if (entry.isDirectory()) {
                            if (!filePath.endsWith(".")) {
                                deleteRemoteDirectory(filePath, recurse);
                            }
                        } else {
                            deleteRemoteFile(filePath);
                        }
                    }
                } catch (URISyntaxException e) {
                    throw new ClientCommandLsException(e);
                }
            }
            if (!this.ftpClient.removeDirectory(directoryPath)) {
                throw new ClientException("Exception while deleting directory [" + directoryPath + "]");
            }
        } catch (IOException e) {
            throw new ClientException("Exception while deleting directory [" + directoryPath + "]", e);
        }
    }

    public void deleteRemoteFile(String filename) throws ClientException {
        try {
            if (!this.ftpClient.deleteFile(filename)) {
                throw new ClientException("Exception while deleting file [" + filename + "]"); //$NON-NLS-1$//$NON-NLS-2$                
            }
        } catch (IOException e) {
            throw new ClientException("Exception while deleting file [" + filename + "]", e); //$NON-NLS-1$//$NON-NLS-2$
        }
    }

    public BaseFileTransferMappedRecord get(ClientListEntry clientListEntry) throws ClientCommandGetException {
        // Construct file path and get the file into an
        // BaseFileTransferMappedRecord
        URI uri = clientListEntry.getUri();
        File srcFile = new File((uri).getPath());
        logger.debug("Getting file [" + srcFile.getPath() + "] into an BaseFileTransferMappedRecord"); //$NON-NLS-1$ //$NON-NLS-2$
        // Getting the file content
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        String fileName = srcFile.getName();
        BaseFileTransferMappedRecord record = null;
        try {
            if (!this.ftpClient.retrieveFile(fileName, output)) {
                throw new ClientCommandGetException("Failed to get file [" + fileName //$NON-NLS-1$
                        + "] from directory [" + uri.getPath()); //$NON-NLS-1$
            }
            record = BaseFileTransferUtils.createBaseFileTransferMappedRecord(uri, output);
            output.close();
        } catch (IOException e) {
            throw new ClientCommandGetException("Failed to get file [" + fileName //$NON-NLS-1$
                    + "] from directory [" + uri.getPath(), e); //$NON-NLS-1$
        }
        return record;
    }

    public BaseFileTransferMappedRecord get(String filePath) throws ClientCommandGetException {
        URI uri;
        BaseFileTransferMappedRecord record = null;
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        try {
            uri = new URI(filePath);
            try {
                logger.debug("getting file from filepath: [" + filePath + "]"); //$NON-NLS-1$ //$NON-NLS-2$
                if (!this.ftpClient.retrieveFile(filePath, output)) {
                    throw new ClientCommandGetException("Failed to get file from [" + filePath + "]"); //$NON-NLS-1$ //$NON-NLS-2$                    
                }
                record = BaseFileTransferUtils.createBaseFileTransferMappedRecord(uri, output);
                output.close();
            } catch (IOException e) {
                throw new ClientCommandGetException("Failed to get file from [" + filePath + "]", e); //$NON-NLS-1$ //$NON-NLS-2$
            }
        } catch (URISyntaxException e) {
            throw new ClientCommandGetException("could not create URI from filePath", e); //$NON-NLS-1$
        }
        return record;
    }

    /**
     * OutputStream is closed by the caller
     * 
     * @param filePath The path to the file that we're getting
     * @param outputStream The stream we're getting the file with
     * @throws ClientCommandGetException Exception if we could not get the file
     */
    public void get(String filePath, OutputStream outputStream) throws ClientCommandGetException {
        logger.debug("get called with filePath [" + filePath + "] and outputStream [" + outputStream + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
        try {
            if (!ftpClient.retrieveFile(filePath, outputStream)) {
                throw new ClientCommandGetException();
            }
        } catch (IOException e) {
            throw new ClientCommandGetException(e);
        }
    }

    public void get(String filePath, OutputStream outputStream, int resume, long offset)
            throws ClientCommandGetException {
        throw new ClientCommandGetException("Not yet implemented"); //$NON-NLS-1$
    }

    public InputStream getAsInputStream(String filePath) throws ClientCommandGetException {
        InputStream input = null;
        try {
            input = ftpClient.retrieveFileStream(filePath);
            if (input == null) {
                throw new ClientCommandGetException("InputStream for [" + filePath + "] was null.");
            }
            // Apache recommends using completePendingCommand() after
            // retrieveFileStream()
            if (!ftpClient.completePendingCommand()) {
                throw new ClientCommandGetException("Error trying to complete pending command.");
            }
            return input;
        } catch (IOException e) {
            throw new ClientCommandGetException(e);
        }
    }

    public InputStream getContentAsStream(ClientListEntry entry) throws ClientCommandGetException {
        // Construct file path and get the file into an OutputStream
        File srcFile = new File((entry.getUri()).getPath());
        logger.debug("Trying to get data from file [" + srcFile.getPath() + "] into an InputStream"); //$NON-NLS-1$ //$NON-NLS-2$
        InputStream input = null;
        // Getting the file content
        String currentDir = null;
        try {
            currentDir = this.ftpClient.printWorkingDirectory();
            input = this.ftpClient.retrieveFileStream(srcFile.getName());
            if (input == null) {
                throw new ClientCommandGetException("Failed to get file [" + srcFile.getName() //$NON-NLS-1$
                        + "] from directory [" + currentDir + "]"); //$NON-NLS-1$//$NON-NLS-2$
            }
            // Apache recommends using completePendingCommand() after
            // retrieveFileStream()
            if (!ftpClient.completePendingCommand()) {
                throw new ClientCommandGetException("Failed to complete the get command."); //$NON-NLS-1$
            }
        } catch (IOException e) {
            throw new ClientCommandGetException("Failed to get file [" + srcFile.getName() //$NON-NLS-1$
                    + "] from directory [" + currentDir + "]", e); //$NON-NLS-1$//$NON-NLS-2$
        }
        return input;
    }

    /**
     * Method used to get a list of all files and directories in the target path
     * including the "." and ".." directories.
     * 
     * @param path The (directory) path to list.
     * @return A <code>ClientListEntry</code> typed <code>List</code>
     * @throws URISyntaxException If a malformed <code>URI</code> is created
     * @throws ClientCommandLsException If the directory listing fails (i.e.
     *             path not a directory or path not found)
     */
    public List<ClientListEntry> ls(String path) throws ClientCommandLsException, URISyntaxException {
        try {
            return doList(path, null);
        } catch (ClientException e) {
            throw new ClientCommandLsException(e);
        }
    }

    /**
     * Method used to get the listing of a remote directory path. When used
     * without any <code>SFTPClientFilter</code>s, this method will return all
     * files and directories in the target path including the "." and ".."
     * directories.
     * 
     * Note: Currently there are no rules defining the order of the
     * <code>List</code> returned.
     * 
     * @param path The (directory) path to list.
     * @param filters The <code>SFTPClientListEntry</code>
     * @return A List of<code>SFTPClientFilter</code>s to apply
     * @throws URISyntaxException If a malformed <code>URI</code> is created
     * @throws ClientException If the directory listing fails (i.e. path not a
     *             directory or path not found)
     */
    private List<ClientListEntry> doList(String path, List<ClientPolarisedFilter> filters)
            throws ClientException, URISyntaxException {
        List<ClientListEntry> list = null;
        List<ClientListEntry> filteredList = null;
        try {
            String startDir = this.ftpClient.printWorkingDirectory();
            logger.debug("Start directory [" + startDir + "].");
            if (!this.ftpClient.changeWorkingDirectory(path)) {
                // is FILE
                int fs = path.lastIndexOf('/');
                String dir = null;// path.substring(0, fs);
                //if (path.startsWith(System.getProperty("file.separator"))) //$NON-NLS-1$
                if (path.startsWith("/")) //$NON-NLS-1$
                {
                    dir = path.substring(0, fs);
                } else {
                    // assume relative to whatever path we currently are
                    dir = startDir + path.substring(0, fs);
                }
                String file = path.substring(fs + 1);
                if (!this.ftpClient.changeWorkingDirectory(dir)) {
                    throw new ClientException("Unable to change dir to: [" + path + "]");
                }
                FTPFile[] ftpFiles = this.ftpClient.listFiles(file);
                String currentDir = this.ftpClient.printWorkingDirectory();
                list = convertFTPFiles(currentDir, ftpFiles);

            } else {
                // is Directory
                String currentDir = this.ftpClient.printWorkingDirectory();
                logger.debug("Listing directory [" + currentDir + "]");
                // Get a list the files, if it's empty, return null
                FTPFile[] ftpFiles = this.ftpClient.listFiles(".");
                // initialise an array of ClientListEntries
                list = convertFTPFiles(currentDir, ftpFiles);

            }
            // Return to the calling directory
            if (!this.ftpClient.changeWorkingDirectory(startDir)) {
                throw new ClientException("Unable to change dir back to: [" + startDir + "]");
            }
            // Filter the list
            if (filters != null && filters.size() > 0) {
                filteredList = BaseFileTransferUtils.filterList(list, filters);
            }
        } catch (IOException e) {
            StringBuilder sb = new StringBuilder(384);
            sb.append("Failed to get listing for directory! ["); //$NON-NLS-1$
            sb.append(path);
            sb.append(']');
            throw new ClientException(sb.toString(), e);
        }
        return (filteredList != null) ? filteredList : list;
    }

    private List<ClientListEntry> convertFTPFiles(String currentDir, FTPFile[] ftpFiles) throws URISyntaxException {
        List<ClientListEntry> list = new ArrayList<ClientListEntry>(ftpFiles.length);

        if (ftpFiles == null) {
            logger.debug("Directory was empty.");
            return list;
        }

        for (FTPFile ftpFile : ftpFiles) {
            // Apache net library can return null elements in list for
            // unparsed items
            if (ftpFile != null) {
                URI fileUri = this.getURI(currentDir, ftpFile.getName());
                ClientListEntry entry = convertFTPFileToClientListEntry(ftpFile, fileUri, currentDir);
                list.add(entry);
            } else {
                logger.warn("One of the ftp file listings could not be parsed.");
            }
        }
        return list;

    }

    public void put(String name, byte[] content) throws ClientCommandPutException {
        InputStream ins = new ByteArrayInputStream(content);
        try {
            if (!this.ftpClient.storeFile(name, ins)) {
                throw new ClientCommandPutException("Failed to write input stream to file! [" + name + "]"); //$NON-NLS-1$ //$NON-NLS-2$                
            }
            // Close the InputStream once we're finished with it
            ins.close();
        } catch (IOException e) {
            throw new ClientCommandPutException("Failed to write input stream to file! [" + name + "]", e); //$NON-NLS-1$ //$NON-NLS-2$
        }
    }

    /**
     * FTP Put using an output stream form an input stream.
     * 
     * @param fileName Name of the file we're putting
     * @param inputStream Stream we're putting the file with
     * @throws ClientCommandPutException Exception if we can't put
     * @throws ClientCommandLsException Exception if we can't lista directory
     * @throws ClientCommandMkdirException Exception if we can't make a
     *             directory
     * 
     */
    public void putWithOutputStream(String fileName, InputStream inputStream)
            throws ClientCommandPutException, ClientCommandLsException, ClientCommandMkdirException {
        try {
            ensureParentsExist(fileName);
            OutputStream outputStream = ftpClient.storeFileStream(fileName);
            if (outputStream == null) {
                throw new ClientCommandPutException("OutputStream for [" + fileName + "] was null");
            }
            BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream);
            int result = 0;
            long writeCounter = 0;
            while (result != -1) {
                result = inputStream.read();
                if (result != -1) {
                    bufferedOutputStream.write(result);
                    writeCounter++;
                }
            }
            bufferedOutputStream.flush();
            bufferedOutputStream.close();
            // Apache recommends using completePendingCommand() after
            // storeFileStream()
            if (!ftpClient.completePendingCommand()) {
                throw new ClientCommandPutException(
                        "Error on completePendingCommand within 'putWithOutputStream'.");
            }
        } catch (IOException e) {
            throw new ClientCommandPutException(e);
        }
    }

    public String pwd() throws ClientCommandPwdException {
        String currentDir = null;
        try {
            currentDir = this.ftpClient.printWorkingDirectory();
        } catch (IOException e) {
            throw new ClientCommandPwdException("Failed to get working directory!", e); //$NON-NLS-1$
        }
        return currentDir;
    }

    public void rename(String currentPath, String newPath) throws ClientCommandRenameException {
        logger.debug("rename called with currentPath [" + currentPath + "], newPath [" + newPath + "]"); //$NON-NLS-1$ //$NON-NLS-2$//$NON-NLS-3$
        try {
            String dirBefore = this.ftpClient.printWorkingDirectory();
            logger.debug("Working directory before rename = [" + dirBefore + "]"); //$NON-NLS-1$ //$NON-NLS-2$
            logger.debug("Current Path = [" + currentPath + "]"); //$NON-NLS-1$ //$NON-NLS-2$
            logger.debug("New Path = [" + newPath + "]"); //$NON-NLS-1$ //$NON-NLS-2$
            if (!this.ftpClient.rename(currentPath, newPath)) {
                throw new ClientCommandRenameException(
                        "Failed to rename [" + currentPath + "] to [" + newPath + "]"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
            }
            logger.debug("Successfully renamed [" + currentPath + "] to [" //$NON-NLS-1$ //$NON-NLS-2$
                    + newPath + "]"); //$NON-NLS-1$
            String dirAfter = this.ftpClient.printWorkingDirectory();
            logger.debug("Working directory after rename = [" + dirAfter + "]"); //$NON-NLS-1$ //$NON-NLS-2$
            if (!dirBefore.equals(dirAfter)) {
                this.ftpClient.changeWorkingDirectory(dirBefore);
                logger.debug("Returning to previous working = [" + dirBefore + "]"); //$NON-NLS-1$//$NON-NLS-2$
            }
        } catch (IOException e) {
            throw new ClientCommandRenameException("Failed to rename [" + currentPath + "] to [" + newPath + "]", //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
                    e);
        }
    }

    /**
     * Constructing a <code>ClientListEntry</code> object from an
     * <code>FTPFile</code> object. This is a direct map with some formatting
     * changes.
     * 
     * @param ftpFile The <code>FTPFile</code> to map to a
     *            <code>ClientListEntry</code>
     * @param fileUri The URI of the underlying file for the particular
     *            <code>FTPFile</code>
     * @return ClientListEntry
     */
    private ClientListEntry convertFTPFileToClientListEntry(FTPFile ftpFile, URI fileUri, String currentDir) {
        ClientListEntry clientListEntry = new ClientListEntry();
        clientListEntry.setUri(fileUri);
        clientListEntry.setName(ftpFile.getName());
        clientListEntry.setFullPath(currentDir + System.getProperty("file.separator") + ftpFile.getName());

        clientListEntry.setClientId(null);
        // Can't distinguish between Last Accessed and Last Modified
        clientListEntry.setDtLastAccessed(ftpFile.getTimestamp().getTime());
        clientListEntry.setDtLastModified(ftpFile.getTimestamp().getTime());
        clientListEntry.setSize(ftpFile.getSize());
        clientListEntry.isDirectory(ftpFile.isDirectory());
        clientListEntry.isLink(ftpFile.isSymbolicLink());
        clientListEntry.setLongFilename(ftpFile.getRawListing());
        clientListEntry.setAtime(ftpFile.getTimestamp().getTime().getTime());
        clientListEntry.setMtime(ftpFile.getTimestamp().getTime().getTime());
        clientListEntry.setAtimeString(ftpFile.getTimestamp().toString());
        clientListEntry.setMtimeString(ftpFile.getTimestamp().toString());
        // clientListEntry.setFlags();
        clientListEntry.setGid(ftpFile.getGroup());
        clientListEntry.setUid(ftpFile.getUser());
        // TODO might be able to ask which permissions it has and build an int
        // and String from there
        // clientListEntry.setPermissions();
        // clientListEntry.setPermissionsString();
        // No extended information
        clientListEntry.setExtended(null);
        return clientListEntry;
    }

    /**
     * Method used to create a <code>URI</code> object from an absolute path to
     * a target. The absolute path needs be passed in as two parameters: One
     * indicating the absolute path of the parent directory and one indicating
     * the actual file name.
     * 
     * Note: Avoid having any encoding/decoding of the path in here
     * 
     * TODO: Could add check to see if this is root path or not!
     * 
     * @param absDir The absolute directory
     * @param filename The file name
     * @return URI URI of the file
     * @throws URISyntaxException Exception if the URI is not valid
     */
    private URI getURI(String absDir, String filename) throws URISyntaxException {
        StringBuilder absolutePath = new StringBuilder(absDir.length() + 1 + filename.length());
        absolutePath.append(absDir);
        absolutePath.append('/');
        absolutePath.append(filename);
        // absolutePath.trimToSize();
        StringBuilder userInfo = new StringBuilder(this.username.length());
        userInfo.append(this.username);
        // userInfo.trimToSize();
        return new URI("ftp", userInfo.toString(), this.remoteHostname, this.remotePort, absolutePath.toString(),
                null, null);
    }

    /**
     * Creates, if necessary all the parents in given file path
     * 
     * @param filePath The path to the file
     * @throws ClientCommandLsException Exception if we cannot list files
     * @throws ClientCommandMkdirException Exception if we cannot make a
     *             directory
     */
    private void ensureParentsExist(String filePath) throws ClientCommandLsException, ClientCommandMkdirException {
        logger.debug("ensureParentsExist called with [" + filePath + "]"); //$NON-NLS-1$//$NON-NLS-2$
        File file = new File(filePath);
        List<File> parents = new ArrayList<File>();
        BaseFileTransferUtils.findParents(file, parents);
        Collections.reverse(parents);
        for (File directory : parents) {
            if (!dirExists(directory)) {
                logger.debug("creating new parent dir [" + directory.getPath() + "]"); //$NON-NLS-1$ //$NON-NLS-2$
                mkdir(directory.getPath());
            }
        }
    }

    /**
     * Determines if a remote directory exists
     * 
     * @param directory Directory to check
     * @return true if this file represents an existent remote directory
     * @throws ClientCommandLsException Exception if we can't list the directory
     */
    private boolean dirExists(File directory) throws ClientCommandLsException {
        boolean dirFound = false;
        String directoryParentPath = null;
        try {
            directoryParentPath = ftpClient.printWorkingDirectory();
        } catch (IOException e) {
            throw new ClientCommandLsException("Could not get working directory.", e);
        }
        if (directoryParentPath == null) {
            throw new ClientCommandLsException("Could not get working directory.");
        }
        if (directory.getParent() != null) {
            directoryParentPath = directory.getParentFile().getPath();
        }
        List<ClientListEntry> entries;
        try {
            entries = ls(directoryParentPath);
            // does the ls on the directory's parent contain the directory?
            Iterator<ClientListEntry> iterator = entries.iterator();
            while (iterator.hasNext()) {
                File entryFile = new File(iterator.next().getLongFilename());
                if (entryFile.getName().equals(directory.getName())) {
                    dirFound = true;
                }
            }
        } catch (URISyntaxException e) {
            throw new ClientCommandLsException(e);
        }
        return dirFound;
    }

    /**
     * Utility method for <code>mkdir(String newDirPath, boolean force)</code>
     * that defaults the <code>force</code> parameter to <code>true</code>.
     * 
     * @param newPath The new path to create
     * @throws ClientCommandMkdirException Exception if we can't make a
     *             directory
     */
    public void mkdir(String newPath) throws ClientCommandMkdirException {
        try {
            createRemotePath(newPath, true);
        } catch (ClientException e) {
            throw new ClientCommandMkdirException(e);
        }
    }

    /**
     * Utility method used to create a path on the remote host. Any path element
     * that does not exist along the path will be created if the
     * <code>force</code> parameter is set to true. Currently used by the ...
     * method.
     * 
     * @param newPath The path to create on the remote host. This can be either
     *            absolute or relative to current working directory. Absolute
     *            paths must start with a '/' character.
     * @param force If true all missing elements along the <code>newPath</code>
     *            will be created.
     * 
     * @throws ClientException If the operation fails (i.e. because of
     *             permission issues). It is left up the caller method to throw
     *             the appropriate specific exception as this in only used
     *             internally.
     */
    private void createRemotePath(String newPath, boolean force) throws ClientException {
        StringBuilder file = new StringBuilder(256);
        // Make a note of the current working directory
        String cwd = null;
        try {
            cwd = this.ftpClient.printWorkingDirectory();
        } catch (IOException e) {
            throw new ClientException("Could not get working directory.", e);
        }
        if (cwd == null) {
            throw new ClientException("Could not get working directory.");
        }
        // If the newpath starts with a separator, assume a root directory;
        if (newPath.startsWith("/")) {
            file.append(newPath);
        }
        // otherwise, assume subdirectory of current working directory
        else {
            file.append(cwd);
            file.append('/');
            file.append(newPath);
        }
        // Hence by this point we deal with an absolute path on the remote host
        // Create an arraylist holding all the relative path elements for this
        // absolute path. Note: the root needs to be added explicitly
        StringTokenizer st = new StringTokenizer(file.toString(), "/");
        ArrayList<String> pathElements = new ArrayList<String>();
        pathElements.add("/");
        while (st.hasMoreTokens())
            pathElements.add(st.nextToken());
        // Therefore we can go to the root and start creating the directories
        // one by one without worrying over the platform's filesystem separator
        boolean createFlag = false;
        for (String pathElement : pathElements) {
            try {
                if (!this.ftpClient.changeWorkingDirectory(pathElement)) {
                    if (force) {
                        createFlag = true;
                        logger.debug("Will try to create working directory to [" + pathElement + "]");
                    } else {
                        throw new ClientException("Could not change working directory to [" + pathElement + "]");
                    }
                }
            } catch (IOException e) {
                if (force) {
                    createFlag = true;
                    logger.debug("Will try to create working directory to [" + pathElement + "]");
                } else {
                    throw new ClientException("Could not change working directory to [" + pathElement + "]", e);
                }
            }
            // By this point we have either cd'ed into the existing directory,
            // or the directory does not exists and the createFlag is set to
            // true, hence try mkdir
            if (createFlag) {
                try {
                    if (!this.ftpClient.makeDirectory(pathElement)) {
                        throw new ClientException(
                                "Failed to create path element [" + pathElement + "] of path [" + newPath + "]");
                    }
                    if (!this.ftpClient.changeWorkingDirectory(pathElement)) {
                        throw new ClientException("Failed to navigate to path element [" + pathElement
                                + "] of path [" + newPath + "]");
                    }
                    logger.debug("Created path element [" + pathElement + "] of path [" + newPath + "]");
                } catch (IOException e) {
                    throw new ClientException("Failed to create & navigate to path element [" + pathElement
                            + "] of path [" + newPath + "]", e);
                }
            }
            // Otherwise, reset createFlag and go for next path element
            createFlag = false;
        }
        // Reset the working directory to where we have started from
        try {
            if (!this.ftpClient.changeWorkingDirectory(cwd)) {
                throw new ClientException("Failed to reset working directory to [" + cwd + "]");
            }
        } catch (IOException e) {
            throw new ClientException("Failed to reset working directory to [" + cwd + "]", e);
        }
    }
}