/*
 * @(#)Ftp.java
 *
 * Copyright (c) 1999-2007 JSCAPE LLC
 *
 * This software is the confidential and proprietary information of
 * JSCAPE. ("Confidential Information").  You shall not disclose such
 * Confidential Information and shall use it only in accordance with
 * the terms of the license agreement you entered into with JSCAPE.
 */
package com.jscape.inet.ftp;

import com.jscape.util.*;
import com.jscape.util.regex.JDKMatcherFactory;
import com.jscape.util.regex.Matcher;
import com.jscape.util.regex.MatcherFactory;

import java.io.*;
import java.nio.file.Files;
import java.util.*;


/**
 * Implements the basic functionality of a FTP client.<P>
 * <p/>
 * Example Usage:<P>
 * <p/>
 * <pre>
 * // create new Ftp instance
 * Ftp ftp = new Ftp("ftp.myserver.com","anonymous","user@myserver.com");
 * try {
 * // connect to FTP server
 * ftp.connect();
 *
 * // print out directory listing
 * System.out.println(ftp.getDirListingAsString());
 *
 * // disconnect from FTP server
 * ftp.disconnect();
 * } catch(Exception e) {
 * System.out.println(e);
 * }
 * </pre>
 */
@SuppressWarnings("unchecked")
public class Ftp
        implements AutoCloseable {

    /**
     * Flag to indicate that automatic transfer mode detection is used.
     *
     * @see #getMode
     */
    public static final int AUTO = 0;

    /**
     * Flag to indicate that ASCII transfer mode is used.
     *
     * @see #getMode
     */
    public static final int ASCII = 1;

    /**
     * Flag to indicate that BINARY transfer mode is used.
     *
     * @see #getMode
     */
    public static final int BINARY = 2;

    /**
     * Represents the current directory.
     * Value of "."
     */
    public static final String CURRENT_DIR = ".";

    /**
     * Represents the parent directory of current directory.
     * Value of ".."
     */
    public static final String UP_DIR = "..";

    private static final MatcherFactory REGEX_FACTORY = new JDKMatcherFactory();

    private File localDir;
    private FtpFileParser fileParser;
    private FtpImplementation impl;


    /**
     * Creates Ftp instance for passed implementation.
     *
     * @param impl desired implementation.
     */
    public Ftp(FtpImplementation impl) {
        
        this.impl = impl;
        this.impl.setFtp(this);
    }

    /**
     * Creates a new Ftp instance.
     */
    public Ftp() {
        this(new FtpBaseImplementation());
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host the hostname of FTP server
     * @param user the username of FTP user
     * @param pwd  the password of FTP user
     * @param impl desired implementation.
     */
    public Ftp(String host, String user, String pwd, FtpImplementation impl) {
        this(impl);

        this.impl.setHost(host);
        this.impl.setUser(user);
        this.impl.setPassword(pwd);
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host the hostname of FTP server
     * @param user the username of FTP user
     * @param pwd  the password of FTP user
     * @param port the port of FTP server
     * @param impl desired implementation.
     */
    public Ftp(String host, String user, String pwd, int port,
               FtpImplementation impl) {
        this(host, user, pwd, impl);

        this.impl.setPort(port);
    }

    /**
     * Creates new Ftp instance.
     *
     * @param host           the hostname of FTP server
     * @param user           the username of FTP user
     * @param pwd            the password of FTP user
     * @param localDirectory the local directory to be used for uploading
     *                       / downloading files
     * @param impl           desired implementation
     */
    public Ftp(String host, String user, String pwd, File localDirectory, FtpImplementation impl) {
        this(host, user, pwd, impl);

        this.localDir = localDirectory;
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host the hostname of FTP server
     * @param user the username of FTP user
     * @param pwd  the password of FTP user
     */
    public Ftp(String host, String user, String pwd) {
        this(host, user, pwd, new FtpBaseImplementation());
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host    the hostname of FTP server
     * @param user    the username of FTP user
     * @param pwd     the password of FTP user
     * @param account the optional account for FTP user, or <code>null</code> if not needed
     */
    public Ftp(String host, String user, String pwd, String account) {
        this(host, user, pwd, new FtpBaseImplementation());
        this.setAccount(account);
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host the hostname of FTP server
     * @param user the username of FTP user
     * @param pwd  the password of FTP user
     * @param port the port of FTP server
     */
    public Ftp(String host, String user, String pwd, int port) {
        this(host, user, pwd, port, new FtpBaseImplementation());
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host           the hostname of FTP server
     * @param user           the username of FTP user
     * @param pwd            the password of FTP user
     * @param localDirectory the local directory to be used for uploading and downloading files
     */
    public Ftp(String host, String user, String pwd, File localDirectory) {
        this(host, user, pwd, localDirectory, new FtpBaseImplementation());
    }


    /**
     * Set the timezone. This is used for file timestamps.
     *
     * @param timeZone
     */
    public synchronized void setTimezone(TimeZone timeZone) {
        this.impl.setTimezone(timeZone);
    }

    /**
     * Get the associated timezone. This is used for file timestamps. Default is GMT
     *
     * @return timezone
     */
    public synchronized TimeZone getTimeZone() {
        return this.impl.getTimeZone();
    }

    /**
     * Set the preserve download timestamp value.
     *
     * @param value
     */
    public synchronized void setPreserveDownloadTimestamp(boolean value) {
        this.impl.setPreserveDownloadTimestamp(value);
    }

    /**
     * Get preserve download timestamp.
     *
     * @return boolean value
     */
    public synchronized boolean getPreserveDownloadTimestamp() {
        return this.impl.getPreserveDownloadTimestamp();
    }

    /**
     * Get upload modification time.
     *
     * @return - boolean value
     */
    public synchronized boolean getPreserveUploadTimestamp() {
        return this.impl.getPreserveUploadTimestamp();
    }

    /**
     * Set upload modification time, replaces the last modified timestamp with that of the local file after upload is complete.
     *
     * @param value - boolean value
     */
    public synchronized void setPreserveUploadTimestamp(boolean value) {
        this.impl.setPreserveUploadTimestamp(value);
    }

    /**
     * Set auto detect Ipv6 detection mode.
     *
     * @param value
     */
    public synchronized void setAutoDetectIpv6(boolean value) {
        this.impl.setAutoDetectIpv6(value);
    }

    /**
     * Get auto detect Ipv6 detection mode.
     *
     * @return
     */
    public synchronized boolean getAutoDetectIpv6() {
        return this.impl.getAutoDetectIpv6();
    }

    /**
     * Sets the proxy authentication username and password to use with
     * proxy server.
     * To clear these settings invoke the #clearProxySettings method.
     *
     * @param proxyUsername the proxy username
     * @param proxyPassword the proxy password
     *
     * @see #clearProxySettings
     */
    public void setProxyAuthentication(String proxyUsername,
                                       String proxyPassword) {
        this.impl.setProxyAuthentication(proxyUsername, proxyPassword);
    }

    /**
     * Sets the proxy hostname and port for this FTP connection.
     * To clear these settings invoke the #clearProxySettings method.
     *
     * @param proxyHostname the hostname or ip address of the proxy server
     * @param proxyPort     the port of the proxy server to connect to
     *
     * @see #clearProxySettings
     */
    public void setProxyHost(String proxyHostname, int proxyPort) {
        this.impl.setProxyHost(proxyHostname, proxyPort);
    }

    /**
     * Sets the proxy type will be used for this FTP connection.
     *
     * @param proxyType The proxy type. Values allowed: HTTP, SOCKS5
     */
    public void setProxyType(String proxyType) {
        this.impl.setProxyType(proxyType);
    }

    /**
     * Clears proxy server values.
     */
    public void clearProxySettings() {
        this.impl.clearProxySettings();
    }

    /**
     * Sets data transfer mode to ASCII. You must be connected to the FTP
     * server prior to invoking this method.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setAscii()
            throws FtpException {
        this.impl.setTransferMode(ASCII);
    }

    /**
     * Sets data transfer mode to be automatically detected.
     * Any files with a base Content-Type value of "text" will be transferred
     * using ASCII, all other files will be transfered using binary.
     * <p/>
     * Example: file.txt, Content-Type = text/plain, uploaded using ASCII
     * Example: image.jpg Content-Type = image/jpg, uploaded using binary
     *
     * @param auto <code>true</code> to turn automatic detect on <code>false</code> to turn off
     */
    public synchronized void setAuto(boolean auto) {
        this.impl.setAuto(auto);
    }

    /**
     * Sets data transfer mode to binary. You must be connected to the FTP
     * server prior to invoking this method.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setBinary()
            throws FtpException {
        this.impl.setTransferMode(BINARY);
    }

    /**
     * Sets block size for use in transferring files.
     * Default blocksize is 4096 bytes
     *
     * @param blocksize the blocksize in bytes
     */
    public synchronized void setBlockTransferSize(int blocksize) {
        this.impl.setBlockTransferSize(blocksize);
    }

    /**
     * Gets blocksize for use in transferring files.
     * Default blocksize is 4096 bytes
     *
     * @return blocksize in bytes
     */
    public synchronized int getBlockTransferSize() {
        return this.impl.getBlockTransferSize();
    }

    /**
     * Sets character set used when issuing commands and transferring files.
     * This is helpful in instances where the FTP server uses a different
     * character encoding than that used by the FTP client.
     *
     * @param encoding the character encoding to be used
     *
     * @deprecated use the setWireEncoding method
     */
    public synchronized void setCharacterEncoding(String encoding) {
        this.impl.setEncoding(encoding);
    }

    /**
     * Gets character encoding used when issuing commands and transferring files.
     *
     * @return the character encoding to be used
     *
     * @deprecated use the getWireEncoding method
     */
    public synchronized String getCharacterEncoding() {
        return this.impl.getEncoding();
    }

    /**
     * Sets whether passive socket connection should be established before or after sending
     * command to read data from passive connection.  Default is <code>true</code>.
     *
     * @param value <code>true</code> if connection should be established before command, <code>false</code> otherwise
     */
    public synchronized void setConnectBeforeCommand(boolean value) {
        this.impl.setConnectBeforeCommand(value);
    }

    /**
     * Gets whether passive socket connection should be established before or after sending
     * command to read data from passive connection.
     * Default is <code>true</code>.
     *
     * @return <code>true</code> if connection is established before command, <code>false</code> otherwise
     */
    public synchronized boolean getConnectBeforeCommand() {
        return this.impl.getConnectBeforeCommand();
    }

    /**
     * Checks if client is connected to FTP server.
     * To check if FTP client is connected to FTP server a NOOP (No Operation)
     * command is sent to the FTP server.  If connection has been lost then
     * a value of <code>false</code> will be returned and an <code>FtpConnectionLostEvent</code>
     * will be fired to any subscribed <code>FtpListener</code>.
     *
     * @return <code>true</code> if connected <code>false</code> otherwise
     *
     * @see FtpConnectionLostEvent
     */
    public synchronized boolean isConnected() {
        return impl.isConnected();
    }

    /**
     * Sets data port for incoming active PORT connections.
     * Default value is 0, assigning a random open port
     *
     * @param dataPort active port
     *
     * @see #setDataPortRange
     * @deprecated Replaced by setDataPortRange
     */
    public void setDataPort(int dataPort) {
        try {
            this.impl.setDataPortStart(dataPort);
            this.impl.setDataPortEnd(dataPort);
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
    }

    /**
     * Returns current port for incoming active PORT data connections.
     * Default value is 0, assigning a random open port.
     *
     * @return active port
     *
     * @see #getDataPortStart
     * @see #getDataPortEnd
     * @deprecated replaced by getDataPortStart and getDataPortEnd methods.
     */
    public int getDataPort() {
        return this.impl.getDataPort();
    }

    /**
     * Gets the data port end range for incoming non-passive (active) data connections.
     *
     * @return end data port range for incoming data connections
     */
    public synchronized int getDataPortEnd() {
        return this.impl.getDataPortEnd();
    }

    /**
     * Sets the data port range for incoming non-passive (active) connections.
     *
     * @param dataPortStart start data port range for incoming connections
     * @param dataPortEnd   end data port range for incoming connections
     */
    public synchronized void setDataPortRange(int dataPortStart, int dataPortEnd) {
        this.impl.setDataPortEnd(dataPortEnd);
        this.impl.setDataPortStart(dataPortStart);
    }

    /**
     * Gets the data port start range for incoming non-passive (active) data connections.
     *
     * @return start data port range for incoming data connections
     */
    public synchronized int getDataPortStart() {
        return this.impl.getDataPortStart();
    }

    /**
     * Sets debugging state.
     * All debug statements are reported to debug stream.
     * Default debug stream is System.out
     *
     * @param debug <code>true</code> to turn debugging on
     *
     * @see #setDebugStream
     */
    public synchronized void setDebug(boolean debug) {
        this.impl.setDebug(debug);
    }

    /**
     * Gets debugging state.
     *
     * @return <code>true</code> if debugging is on
     */
    public synchronized boolean getDebug() {
        return this.impl.getDebug();
    }

    /**
     * Sets PrintStream used in reporting debug statements.
     * Default PrintStream is System.out
     *
     * @param debugStream the PrintStream to send debug statements to
     *
     * @see java.io.PrintStream
     */
    public synchronized void setDebugStream(PrintStream debugStream) {
        this.impl.setDebugStream(debugStream);
    }

    /**
     * Gets PrintStream used in reporting debug statements.
     * Default PrintStream is System.out
     *
     * @return the PrintStream used in reporting debug statements
     *
     * @see java.io.PrintStream
     */
    public synchronized PrintStream getDebugStream() {
        return this.impl.getDebugStream();
    }

    /**
     * Sets current directory on FTP server.
     *
     * @param remoteDirectory the remote directory
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setDir(String remoteDirectory)
            throws FtpException {
        this.impl.setDir(remoteDirectory);
    }

    /**
     * Gets current working directory of FTP server.
     *
     * @return current directory
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized String getDir()
            throws FtpException {
        return this.impl.getDir();
    }

    /**
     * Gets file listing of files matching filter criteria.
     * When using this method filter is passed to server and server is responsible for parsing filter.
     * Servers may vary in how they handle these filters.  For a client side filter use the <code>getDirListingRegex</code>
     * method which uses regular expressions to parse results of a directory listing.
     * <p/>
     * Parsers are included to handle UNIX, NT and VMS directory style listings.
     * If you are connecting to a FTP server that does not use any of these listing formats
     * then you can optionally develop your own parser by extending the <code>FtpFileParser</code> class,
     * or use some of the alternative methods for obtaining a directory listing such
     * as the <code>getNameListing</code> or <code>getDirListingAsString</code> methods.
     *
     * @param filter the filename filter to be handled by server
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code> objects
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see FtpFile
     * @see #getDirListingAsString(String)
     * @see #getDirListingRegex(String)
     * @see #getNameListing
     * @see FtpFileParser
     * @see UnixParser
     * @see NTParser
     * @see VMSParser
     */
    public synchronized Enumeration getDirListing(String filter)
            throws FtpException {
        return this.impl.getDirListing(filter).elements();
    }

    /**
     * Gets directory listing from FTP server.
     * <p/>
     * Parsers are included to handle UNIX, NT and VMS directory style listings.
     * If you are connecting to a FTP server that does not use any of these listing formats
     * then you can optionally develop your own parser by extending the <code>FtpFileParser</code> class,
     * or use some of the alternative methods for obtaining a directory listing such
     * as the <code>getNameListing</code> or <code>getDirListingAsString</code> methods.
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code> objects
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see FtpFile
     * @see #getDirListingAsString(String)
     * @see #getNameListing
     * @see FtpFileParser
     * @see UnixParser
     * @see NTParser
     * @see VMSParser
     */
    public synchronized Enumeration getDirListing()
            throws FtpException {
        return getDirListing("");
    }

    /**
     * Gets raw directory listing of files matching filter criteria.
     * When using this method filter is passed to server and server is responsible for parsing filter.
     * Servers may vary in how they handle these filters.  For a client side filter use the <code>getDirListingRegex</code>
     * method which uses regular expressions to parse results of a directory listing.
     *
     * @param filter the filename filter to be handled by server
     *
     * @return an <code>String</code> containing raw directory listing contents
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #getDirListingRegex(String)
     */
    public synchronized String getDirListingAsString(String filter)
            throws FtpException {
        return this.impl.getDirListingAsString(filter);
    }

    /**
     * Gets raw directory listing of files on FTP server.
     *
     * @return raw directory listing as <code>String</code>
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized String getDirListingAsString()
            throws FtpException {
        return getDirListingAsString("");
    }

    /**
     * Gets directory listing from FTP server returning only file and directory names that match
     * the regular expression.
     * <p/>
     * For example, to list all files with extension .java or .class the regex
     * value should be ".+\\.(?:java|class)".
     *
     * @param filter regular expression used to obtain the file list
     *
     * @return file list which matches the regular expression
     *
     * @throws FtpException if an I/O or protocol error occurs.
     */
    public synchronized Enumeration getDirListingRegex(String filter)
            throws FtpException {
        Matcher regex = createRegex(filter);
        Vector ret = new Vector();

        for (Enumeration e = this.impl.getDirListing("").elements(); e.hasMoreElements(); ) {
            FtpFile file = (FtpFile) e.nextElement();

            if (regex.matches(file.getFilename())) {
                ret.add(file);
            }
        }

        return ret.elements();
    }

    /**
     * Gets directory listing from FTP server returning only file and directory names that match
     * the regular expression.
     * <p/>
     * For example, to list all files with extension .java or .class the regex
     * value should be ".+\\.(?:java|class)".
     *
     * @param filter regular expression used to obtain the file list
     *
     * @return raw directory listing as <code>String</code>
     *
     * @throws FtpException if an I/O or protocol error occurs.
     */
    public synchronized String getDirListingRegexAsString(String filter)
            throws FtpException {
        StringBuilder buffer = new StringBuilder();

        for (Enumeration e = getDirListingRegex(filter); e.hasMoreElements(); ) {
            FtpFile file = (FtpFile) e.nextElement();
            buffer.append(file.toString()).append("\r\n");
        }

        return buffer.toString();
    }

    /**
     * Sets current directory on FTP server to parent directory.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setDirUp()
            throws FtpException {
        this.impl.dirUp();
    }

    /**
     * Sets whether an exception should be thrown when downloading a file and server does not support SIZE command.
     * The SIZE command is used to obtain the length of remote file for use in firing progress events.
     * Default is <code>false</code>.
     *
     * @param error <code>true</code> if exception should be thrown, <code>false</code> otherwise
     */
    public void setErrorOnSizeCommand(boolean error) {
        this.impl.setErrorOnSizeCommand(error);
    }

    /**
     * Gets whether an exception should be thrown when downloading a file and server does not support SIZE command.
     * The SIZE command is used to obtain the length of remote file for use in firing progress events.
     * Default is <code>false</code>.
     *
     * @return <code>true</code> if exception is thrown when SIZE command is not supported
     */
    public boolean getErrorOnSizeCommand() {
        return this.impl.getErrorOnSizeCommand();
    }

    /**
     * Looks for the command argument into the server features list to validate if it is supported.
     *
     * @param command the feature command to search for
     *
     * @return <code>true</code> if the command is found into the server feature list;
     *         <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public boolean isFeatureSupported(String command)
            throws FtpException {
        return this.impl.isFeatureSupported(command);
    }

    /**
     * Returns supported server features using FEAT command.
     *
     * @return an <code>Enumeration</code> of features <code>Strings</code>
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public Enumeration getFeatures()
            throws FtpException {
        return this.impl.getFeatures().elements();
    }

    /**
     * Sets date/timestamp of remote file on FTP server.
     * This feature is not supported by RFC-959 so may not work on all FTP servers.
     *
     * @param remoteFile the remote file
     * @param date   the date/timestamp of remote file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setFileTimestamp(String remoteFile, Date date)
            throws FtpException {
        this.impl.setFileModificationTime(remoteFile, date);
    }

    /**
     * Gets date/timestamp of remote file from FTP server.
     * This feature is not supported by RFC-959 so may not work on all FTP servers.
     *
     * @param remoteFile the remote file
     *
     * @return date the date/timestamp of remote file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized Date getFileTimestamp(String remoteFile)
            throws FtpException {
        return this.impl.getFileTimeStamp(remoteFile);
    }

    /**
     * Sets file creation time with a new value.
     * This command is not specified in RFC 959 and it is not support by many Ftp servers.
     *
     * @param remoteName the remote file name
     * @param date       the new creation time
     *
     * @throws FtpException If an error occurs.
     */
    public synchronized void setFileCreationTime(String remoteName, Date date)
            throws FtpException {
        this.impl.setFileCreationTime(remoteName, date);
    }

    /**
     * Sets the file modification time with a new value.
     * This command is not specified in RFC 959 and it is not support by many Ftp servers.
     *
     * @param remoteName the remote file
     * @param date       the new modification time
     *
     * @throws FtpException If an error occurs
     */
    public synchronized void setFileModificationTime(String remoteName, Date date)
            throws FtpException {
        this.impl.setFileModificationTime(remoteName, date);
    }

    /**
     * Gets filesize of remote file from FTP server as represented in ASCII.
     * This feature is not supported by RFC-959 so may not work on all FTP servers.
     * For larger files it may take some time to return as server will often
     * convert file to ASCII before returning a filesize.
     *
     * @param remoteFile the remote file
     *
     * @return filesize in bytes
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized long getFilesize(String remoteFile)
            throws FtpException {
        return this.impl.getFileSize(remoteFile);
    }

    /**
     * Sets the <code>FtpFileParser</code> used to parse directory listing returned when
     * invoking <code>Ftp#getDirListing</code> method. If <code>FtpFileParser</code> is <code>null</code> upon
     * establishing connection the <code>Ftp</code> class will attempt to determine the proper
     * <code>FtpFileParser</code> based on information retrieved from the FTP server.
     * If the <code>FtpFileParser</code> is <code>null</code> and cannot be determined otherwise then
     * by default an instance of <code>UnixParser</code> will be used.
     * Upon disconnecting from FTP server the <code>FtpFileParser</code> will be reset to <code>null</code>.
     *
     * @param parser a FtpFileParser instance
     *
     * @see FtpFileParser
     * @see UnixParser
     * @see NTParser
     * @see VMSParser
     */
    public synchronized void setFtpFileParser(FtpFileParser parser) {
        this.fileParser = parser;
    }

    /**
     * Gets the <code>FtpFileParser</code> used to parse directory listing returned when
     * invoking <code>Ftp#getDirListing</code> method. If <code>FtpFileParser</code> is <code>null</code> upon
     * establishing connection the <code>Ftp</code> class will attempt to determine the proper
     * <code>FtpFileParser</code> based on information retrieved from the FTP server.
     * If the <code>FtpFileParser</code> is <code>null</code> and cannot be determined otherwise then
     * by default an instance of <code>UnixParser</code> will be used.
     * Upon disconnecting from FTP server the <code>FtpFileParser</code> will be reset to <code>null</code>.
     *
     * @return a FtpFileParser or null if no FtpFileParser has been defined.
     *
     * @see FtpFileParser
     * @see UnixParser
     * @see NTParser
     * @see VMSParser
     */
    public FtpFileParser getFtpFileParser() {
        return this.fileParser;
    }

    /**
     * Sets hostname of FTP server.
     *
     * @param host the hostname or IP address of FTP server
     */
    public synchronized void setHostname(String host) {
        this.impl.setHost(host);
    }

    /**
     * Sets optional account of FTP server.
     * This should not be confused with username.
     *
     * @param account the account
     *
     * @see #setUsername(String)
     */
    public synchronized void setAccount(String account) {
        this.impl.setAccount(account);
    }

    /**
     * Gets optional account of FTP server.
     * This should not be confused with username.
     *
     * @return the account
     *
     * @see #getUsername()
     */
    public synchronized String getAccount() {
        return this.impl.getAccount();
    }

    /**
     * Gets hostname of FTP server.
     *
     * @return hostname
     */
    public synchronized String getHostname() {
        return this.impl.getHost();
    }

    /**
     * Gets an <code>InputStream</code> from FTP server for the purposes of downloading a file.
     *
     * @param remoteFile remote file name to download
     * @param pos        file offset
     *
     * @return an InputStream
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized InputStream getInputStream(String remoteFile, long pos)
            throws FtpException {
        return this.impl.getInputStream(remoteFile, pos);
    }

    /**
     * Sets the linger time for closing data sockets.
     *
     * @param linger the linger value in milliseconds
     */
    public synchronized void setLinger(int linger) {
        this.impl.setLinger(linger);
    }

    /**
     * Gets the linger time for closing data sockets.
     *
     * @return the linger time in milliseconds
     */
    public synchronized int getLinger() {
        return this.impl.getLinger();
    }

    /**
     * Sets local directory to be used in FTP session.
     * Downloads will be stored relative to local directory.
     * Uploads are performed relative to current local directory.
     *
     * @param localDirectory the local directory
     */
    public synchronized void setLocalDir(File localDirectory) {
        if (!localDirectory.exists() || !localDirectory.isDirectory()) {
            throw new IllegalArgumentException(
                    "local directory does not exist: " + localDirectory.toString());
        }

        this.localDir = localDirectory;
    }

    /**
     * Gets local directory to be used in FTP session.
     * Downloads will be stored relative to local directory.
     * Uploads are performed relative to current local directory.
     *
     * @return the local directory
     */
    public synchronized File getLocalDir() {
        if (this.localDir == null) {
            initLocalDir();
        }

        return this.localDir;
    }

    /**
     * Gets local directory listing.
     *
     * @return an <code>Enumeration</code> of <code>File</code>
     */
    public synchronized Enumeration getLocalDirListing() {
        return new DiskIterator(getLocalDir().toString(), "*", false, true,
                                false);
    }

    /**
     * Gets file listing of specified directory using MLSD command.
     * This command is not supported by all FTP servers.
     *
     * @param dirname directory name or empty <code>String</code> for current directory
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code>
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see FtpFile
     */
    public Enumeration getMachineDirListing(String dirname)
            throws FtpException {
        return this.impl.getMachineDirListing(dirname).elements();
    }

    /**
     * Gets file listing of specified directory using MLSD command.
     * This command is not supported by all FTP servers.
     *
     * @param dirname directory name or empty <code>String</code> for current directory
     * @param regex   a regular expression e.g. "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>" for all files ending in .txt extension
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code>
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public Enumeration getMachineDirListing(String dirname, String regex)
            throws FtpException {
        return this.impl.getMachineDirListing(dirname, regex).elements();
    }

    /**
     * Gets the file description for the specified filename using MLST command.
     * This command is not supported by all FTP servers.
     *
     * @param filename target filename or empty for current directory
     *
     * @return an <code>FtpFile</code>
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see FtpFile
     */
    public FtpFile getMachineFileListing(String filename)
            throws FtpException {
        return this.impl.getMachineFileListing(filename);
    }

    /**
     * Gets data transfer mode for current session.
     * Default mode is <code>Ftp.AUTO</code> where 0 = <code>Ftp.AUTO</code>, 1 = <code>Ftp.ASCII</code>, 2 = <code>Ftp.BINARY</code>
     *
     * @return the data transfer mode
     *
     * @see #AUTO
     * @see #BINARY
     * @see #ASCII
     */
    public synchronized int getMode() {
        return this.impl.getTransferMode();
    }

    /**
     * Gets directory listing of files on FTP server matching filter criteria
     * and using NLST command.  Server is responsible for parsing filter.
     *
     * @param filter the filename filter
     *
     * @return an <code>Enumeration</code> of <code>String</code> where each item represents a file or directory name
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized Enumeration getNameListing(String filter)
            throws FtpException {
        Vector files = this.impl.getNameListing(filter);

        return files.elements();
    }

    /**
     * Gets directory listing of files on FTP server.
     *
     * @return an <code>Enumeration</code> of <code>String</code> where each item represents a file or directory name
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized Enumeration getNameListing()
            throws FtpException {
        return getNameListing("");
    }

    /**
     * Gets an <code>OutputStream</code> from FTP server for the purposes of uploading a file.
     *
     * @param fileName the remote filename to store file as
     * @param off      file offset
     * @param append   <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @return an OutputStream
     *
     * @throws FtpException if an FTP error occurs.
     */
    public synchronized OutputStream getOutputStream(String fileName, long off,
                                                     boolean append)
            throws FtpException {
        return this.impl.getOutputStream(fileName, append, off);
    }

    /**
     * Sets whether the data will be compressed during the transfer.
     *
     * @param compression Compression indicator.
     */
    public synchronized void setCompression(boolean compression) {
        this.impl.setCompression(compression);
    }

    /**
     * Gets whether the data is compressed during the transfering. The data will be compressed
     * if the Ftp server support this feature (MODE Z command)
     *
     * @return <code>true</code> data will be compressed, <code>false</code> otherwise.
     */
    public synchronized boolean getCompression() {
        return this.impl.getCompression();
    }

    /**
     * Sets a NAT address which will be used for PASV file transfers.
     * For active (non-passive) file transfer see the <code>setPortAddress</code> and <code>setDataPortRange</code> methods.
     *
     * @param ipAddress The NAT IP address.
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see #setDataPortRange(int, int)
     * @see #setPortAddress(String)
     */
    public void setNATAddress(String ipAddress)
            throws FtpException {
        this.impl.setNATAddress(ipAddress);
    }

    /**
     * Get the NAT address.
     *
     * @return address
     */
    public String getNATAddress() {
        return this.impl.getNATAddress();
    }

    /**
     * Sets whether passive mode should be used.
     * Default is <code>true</code>.
     *
     * @param passive <code>true</code> to use passive transfer, <code>false</code> otherwise
     */
    public synchronized void setPassive(boolean passive) {
        this.impl.setPassive(passive);
    }

    /**
     * Gets whether passive mode should be used.
     * Default is <code>true</code>.
     *
     * @return <code>true</code> if passive mode is enabled, <code>false</code> otherwise
     */
    public synchronized boolean getPassive() {
        return this.impl.isPassive();
    }

    /**
     * Sets password of FTP user.
     *
     * @param password the password of FTP user
     */
    public synchronized void setPassword(String password) {
        this.impl.setPassword(password);
    }

    /**
     * Returns current FTP user password.
     *
     * @return current FTP password.
     */
    public synchronized String getPassword() {
        return this.impl.getPassword();
    }

    /**
     * Sets port of FTP server.
     * Default value is 21.
     *
     * @param port the port of FTP server
     */
    public synchronized void setPort(int port) {
        this.impl.setPort(port);
    }

    /**
     * Gets port of FTP server.
     * Default value is 21.
     *
     * @return port
     */
    public synchronized int getPort() {
        return this.impl.getPort();
    }

    /**
     * Sets the external IP address to use when listing for active connections using the PORT command.
     *
     * @param string an IP address
     */
    public void setPortAddress(String string) {
        this.impl.setPortAddress(string);
    }

    /**
     * Gets the external IP address to use when listing for active connections using the PORT command.
     *
     * @return the external IP address
     */
    public String getPortAddress() {
        return this.impl.getPortAddress();
    }

    /**
     * Gets response code from last FTP command.
     *
     * @return response code or -1 if code cannot be determined
     */
    public synchronized int getResponseCode() {
        return this.impl.getResponseCode();
    }

    /**
     * Gets the system type of remote FTP server.
     *
     * @return system type
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized String getSystemType()
            throws FtpException {
        return this.impl.getSystemType();
    }

    /**
     * Sets the send buffer size option of the ftp connection.
     *
     * @param sendBufferSize Send buffer size.
     */
    public synchronized void setSendBufferSize(int sendBufferSize) {
        this.impl.setSendBufferSize(sendBufferSize);
    }

    /**
     * Sets the receive buffer size option of the ftp connection.
     *
     * @param receiveBufferSize Receive buffer size.
     */
    public synchronized void setReceiveBufferSize(int receiveBufferSize) {
        this.impl.setReceiveBufferSize(receiveBufferSize);
    }

    /**
     * Sets the timeout for opening connection to FTP server.
     *
     * @param timeout the timeout in milliseconds
     */
    public synchronized void setTimeout(int timeout) {
        this.impl.setTimeout(timeout);
    }

    /**
     * Gets the timeout for opening connection to FTP server.
     *
     * @return timeout in milliseconds
     */
    public synchronized int getTimeout() {
        return this.impl.getTimeout();
    }

    /**
     * Set socket keep alive.
     *
     * @param keepAlive
     */
    public synchronized void setKeepAlive(boolean keepAlive) {
        this.impl.setKeepAlive(keepAlive);
    }

    /**
     * Get socket keep alive.
     *
     * @return keep alive
     */
    public synchronized boolean getKeepAlive() {
        return this.impl.getKeepAlive();
    }
    
    public boolean getTcpNoDelay() {
        return this.impl.getTcpNoDelay();
    }
    
    public void setTcpNoDelay(boolean value) {
        this.impl.setTcpNoDelay(value);
    }
    
    /**
     * Sets username of FTP user.
     *
     * @param user the username of FTP user
     */
    public synchronized void setUsername(String user) {
        this.impl.setUser(user);
    }

    /**
     * Sets whether to use EPSV or PASV command for data tranfers.
     * Default is to use PASV command.
     *
     * @param useEPSV <code>true</code> to use EPSV command, <code>false</code> to use PASV command.
     */
    public synchronized void setUseEPSV(boolean useEPSV) {
        this.impl.setUseEPSV(useEPSV);
    }

    /**
     * Gets whether to use EPRT or PORT command for data tranfers.
     *
     * @return <code>true</code> to use EPRT command, <code>false</code> to use PORT command.
     */
    public synchronized boolean getUseEPRT() {
        return this.impl.getUseEPRT();
    }

    /**
     * Sets whether to use EPRT or PASV command for data tranfers.
     * Default is to use PASV command.
     *
     * @param value <code>true</code> to use EPRT command, <code>false</code> to use PORT command.
     */
    public synchronized void setUseEPRT(boolean value) {
        this.impl.setUseEPRT(value);
    }

    /**
     * Gets whether to use EPSV or PASV command for data tranfers.
     *
     * @return <code>true</code> to use EPSV command, <code>false</code> to use PASV command.
     */
    public synchronized boolean getUseEPSV() {
        return this.impl.getUseEPSV();
    }

    /**
     * Gets username of FTP user.
     *
     * @return username
     */
    public synchronized String getUsername() {
        return this.impl.getUser();
    }

    /**
     * Sets encoding set used when issuing commands and transferring files.
     * Default is UTF8.
     *
     * @param encoding the character encoding to be used
     */
    public synchronized void setWireEncoding(String encoding) {
        this.impl.setWireEncoding(encoding);
    }

    /**
     * Gets encoding set used when issuing commands and transferring files.
     * Default is UTF8.
     *
     * @return the character encoding being used
     */
    public synchronized String getWireEncoding() {
        return this.impl.getWireEncoding();
    }

    /**
     * Adds FTP event listener.
     *
     * @param listener the listener to add
     *
     * @see FtpListener
     */
    public synchronized void addFtpListener(FtpListener listener) {
        this.impl.addListener(listener);
    }

    /**
     * Changes password for current account.
     * <p/>
     * This feature is not supported by RFC-959 so may not work on all FTP servers.
     *
     * @param oldPassword The old password.
     * @param newPassword The new password.
     *
     * @throws FtpException FtpException if underlayed I/O or FTP error occurs.
     */
    public synchronized void changePassword(String oldPassword,
                                            String newPassword)
            throws FtpException {
        this.impl.changePassword(oldPassword, newPassword);
    }

    /**
     * Compares checksum of the local and remote files.
     *
     * @param localfile  local file
     * @param remoteFile remote file
     *
     * @return <code>true</code> if checksums are equal, <code>false</code> otherwise
     *
     * @throws FtpException if an error occurs
     */
    public synchronized boolean checksum(File localfile, String remoteFile)
            throws FtpException {
        try (InputStream in = Files.newInputStream(localfile.toPath())) {
            return sameChecksum(in, remoteFile);
        } catch (Exception e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * Compares checksum of the local data and remote file.
     *
     * @param data       data
     * @param remoteFile remote file
     *
     * @return <code>true</code> if checksum are equal, <code>false</code> otherwise
     *
     * @throws FtpException if an error occurs
     */
    public synchronized boolean checksum(byte[] data, String remoteFile)
            throws FtpException {
        return sameChecksum(new ByteArrayInputStream(data), remoteFile);
    }

    /**
     * Connects to FTP server and performs login using specified username, password and optional account.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setUsername
     * @see #setPassword
     * @see #setAccount
     */
    public synchronized Ftp connect()
            throws FtpException {
        this.impl.connect();
        return this;
    }

    /**
     * Connects to FTP server and optionally performs login.
     *
     * @param login <code>true</code> to login <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized Ftp connect(boolean login)
            throws FtpException {
        this.impl.connect(login);
        return this;
    }

    /**
     * Deletes directory from FTP server.
     * If the directory cannot be deleted because it is not empty the <code>recurse</code>
     * parameter will be used to determine if files and subdirectories within
     * directory should be removed.
     *
     * @param remoteDirectory the directory to remove
     * @param recurse         <code>true</code> if it should delete files and subdirectories within directory
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void deleteDir(String remoteDirectory, boolean recurse)
            throws FtpException {
        this.impl.deleteDir(remoteDirectory, recurse);
    }

    /**
     * Deletes directory from FTP server.
     *
     * @param remoteDirectory the directory to remove
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void deleteDir(String remoteDirectory)
            throws FtpException {
        deleteDir(remoteDirectory, false);
    }

    /**
     * Deletes remote file from FTP server.
     *
     * @param remoteFile name of file to delete
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void deleteFile(String remoteFile)
            throws FtpException {
        this.impl.deleteFile(remoteFile);
    }

    /**
     * Quits FTP session and disconnects from FTP server.
     */
    public synchronized void disconnect() {
        this.impl.disconnect();
    }

    @Override
    public void close() {
        disconnect();
    }

    /**
     * Downloads file from FTP server and writes contents to provided <code>OutputStream</code>.
     * Closure of <code>OutputStream</code> is responsibility of programmer.
     *
     * @param out        the <code>OutputStream</code> to write download to
     * @param remoteFile remote filename
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized void download(OutputStream out, String remoteFile)
            throws FtpException {
        this.impl.setTargetFile(remoteFile);

        try {
            this.impl.download(out, remoteFile, 0);
        } finally {
            this.impl.setTargetFile(null);
        }
    }

    /**
     * Downloads file from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param localFile  destination filename relative to current local directory, NOT an absolute path
     * @param remoteFile remote filename
     *
     * @return the file downloaded
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized File download(String localFile, String remoteFile)
            throws FtpException {
        File result = new File(getLocalDir(), localFile);

        // create parent structure if necessary
        createDirectoryStructure(result);

        OutputStream out = getOutputStream(result);
        this.impl.setTargetPath(result.getAbsolutePath());
        String remoteDirectory = currentRemoteDirectory();

        try {
            download(out, remoteFile);
            IOTools.close(out);

            if (this.impl.getPreserveDownloadTimestamp() && !this.impl.interrupted()) {
                if (!this.impl.isConnected()) {
                    this.impl.connect();
                    this.impl.reset();
                    this.impl.setDir(remoteDirectory);
                }

                boolean success = result.setLastModified(this.getFileTimestamp(remoteFile).getTime());

                if (success && this.impl.getDebug()) {
                    this.impl.getDebugStream().println("Timestamp of local file " + result.getAbsolutePath() + " changed successfully. [" + new Date(result.lastModified()) + "]");
                } else {
                    if (this.impl.getDebug()) {
                        this.impl.getDebugStream().println("Timestamp of local file " + result.getAbsolutePath() + " count not be changed.");
                    }
                }
            }

            return result;
        } catch (FtpException e) {
            if (result.length() == 0) {
                result.delete();
            }

            throw e;
        } finally {
            IOTools.close(out);
            this.impl.setTargetPath(null);
        }
    }

    private String currentRemoteDirectory() {
        try {
            return getDir();
        } catch (Exception e) {
            return ".";
        }
    }

    /**
     * Downloads file from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteFile the file to download
     *
     * @return the file downloaded
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized File download(String remoteFile)
            throws FtpException {
        return download(remoteFile, remoteFile);
    }

    /**
     * Downloads remote directory and contents recursively from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteDir the directory to download
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void downloadDir(String remoteDir)
            throws FtpException {
        String curRemoteDir = getDir();
        setDir(remoteDir);

        File curLocalDir = getLocalDir();

        try {
            String dirName = getDirectoryName(remoteDir);

            if (!dirName.equals("..")) {
                setLocalDir(makeLocalDir(dirName));
            }

            Enumeration files = getDirListing();

            while (files.hasMoreElements() && !this.impl.interrupted()) {
                FtpFile file = (FtpFile) files.nextElement();

                if (!file.isDirectory()) {
                    download(file.getFilename());
                } else if (!isCurrentOrParentDir(file)) {
                    downloadDir(file.getFilename());
                }
            }
        } finally {
            setDir(curRemoteDir);
            setLocalDir(curLocalDir);

            String filename = curLocalDir + File.separator + remoteDir;
            try {
                if (this.interrupted() == false && this.getPreserveDownloadTimestamp() == true) {
                    try {
                        if (this.isConnected() == false) {
                            this.connect();
                            this.reset();
                        }

                        File localFileToUpdate = new File(filename);
                        localFileToUpdate.setLastModified(this.getFileTimestamp(remoteDir).getTime());

                        if (this.impl.getDebug()) {
                            this.impl.getDebugStream().println("Timestamp for local directory " + localFileToUpdate.getAbsolutePath() + " changed successfully.");
                        }
                    } catch (FtpException e) {
                        if (this.impl.getDebug()) {
                            this.impl.getDebugStream().println("Timestamp for local directory " + filename + " could not be changed");
                        }
                    }
                }
            } catch (Exception e) {
                if (this.impl.getDebug()) {
                    this.impl.getDebugStream().println("Timestamp of local directory " + filename + " could not be changed");
                }
            }
        }
    }

    /**
     * Downloads remote directory and contents from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteDir       the directory to download
     * @param retryLimit      retry limit if an error occurs
     * @param performChecksum whether to perform checksum after each download, not supported by all FTP servers
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void downloadDir(String remoteDir, int retryLimit, boolean performChecksum)
            throws FtpException {
        this.downloadDir(remoteDir, retryLimit, 0, performChecksum);
    }

    /**
     * Download directory.
     *
     * @param remoteDir
     * @param retryLimit
     * @param performChecksum
     * @param threadCount
     *
     * @throws FtpException
     */
    public void downloadDir(String remoteDir, int retryLimit,
                            boolean performChecksum, int threadCount)
            throws FtpException {
        this.downloadDir(remoteDir, retryLimit, 0, performChecksum, threadCount);
    }

    /**
     * Downloads remote directory and contents from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteDir       the directory to download
     * @param retryLimit      retry limit if an error occurs
     * @param performChecksum whether to perform checksum after each download, not supported by all FTP servers
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void downloadDir(String remoteDir, int retryLimit, int retryInterval,
                                         boolean performChecksum, int threadCount)
            throws FtpException {

        if (threadCount == 0 || threadCount == 1) {
            downloadDir(remoteDir, retryLimit, retryInterval, performChecksum);
            return;
        } else if (threadCount < 0) {
            throw new FtpException("Illegal thread count value : " + threadCount + ". Must be greather than zero.");
        }

        String lastCurrentRemoteDir = getDir();
        setDir(remoteDir);

        File lastLocalDir = getLocalDir();
        boolean success = false;

        String dirName = getDirectoryName(remoteDir);

        if (!dirName.equals("..")) {
            setLocalDir(makeLocalDir(dirName));
        }

        Vector fileList = getRemoteFileList(remoteDir);

        Vector remoteEmptyDirs = getRemoteEmptyDirList(remoteDir);
        makeEmptyLocalDirs(remoteEmptyDirs, new File(remoteDir));

        String currentRemoteDir = getDir();
        File currentLocalDir = getLocalDir();

        for (int i = 0; (i <= retryLimit) && !this.impl.interrupted(); ++i) {
            try {
                downloadDir(currentRemoteDir, currentLocalDir, performChecksum,
                            fileList, threadCount);

                success = true;
            } catch (Exception e) {
                e.printStackTrace();
                this.abortDownloadThreads();

                //  throw exception if retry limit exceeded ... otherwise continue
                if (i >= retryLimit) {
                    throw new FtpException(e.getMessage());
                }


                // sleep some time before disconnecting and trying again 
                if (i < retryLimit) {
                    try {
                        Thread.sleep(retryInterval * 1000);
                    } catch (InterruptedException ie) {
                        // ignore
                    }
                }

                disconnect();
                connect();
            }

            if (success) {
                setDir(lastCurrentRemoteDir);
                setLocalDir(lastLocalDir);

                break;
            }
        }
    }

    /**
     * Downloads a directory.
     *
     * @param currentRemoteDir Remote directory
     * @param currentLocalDir  Local directory
     * @param performChecksum  Perform checksum indicator
     * @param fileList         File list will be downloaded
     * @param threadCount      The number of threads to use for download
     *
     * @throws IOException  If a IO error occurs
     * @throws FtpException If a FTP error occurs
     */
    private void downloadDir(String currentRemoteDir, File currentLocalDir,
                             boolean performChecksum, Vector fileList, int threadCount)
            throws IOException, FtpException {

        this.downloadItems.clear();
        this.downloadHashMap.clear();
        int totalFiles = fileList.size();
        int totalThreads = threadCount;

        // declare the thread map according to the number of threads available
        Vector threadMap = new Vector();
        for (int i = 0; i < totalThreads; i++) {
            threadMap.add(new Vector());
        }

        // initialize the thread map.
        int j = 0;
        for (int i = 0; i < totalFiles; i++) {
            if (j >= threadMap.size()) {
                j = 0;
            }
            Vector thread = (Vector) threadMap.get(j);
            thread.add(fileList.get(i));
            threadMap.setElementAt(thread, j++);
        }

        // add the upload threads to a Vector object.
        for (int i = 0; i < threadMap.size(); i++) {
            Vector files = (Vector) threadMap.get(i);
            if (files.size() == 0) {
                continue;
            }
            FtpDownloadItem item = new FtpDownloadItem(this, files, performChecksum, currentRemoteDir, localDir);
            downloadItems.add(item);

            for (int index = 0; index < files.size(); index++) {
                this.downloadHashMap.put(files.get(index).toString(), item);
            }
        }

        // start the threads.
        for (int i = 0; i < downloadItems.size(); i++) {
            ((FtpDownloadItem) downloadItems.get(i)).start();
        }

        boolean error = false;
        String errorMsg = null;

        // wait for upload threads to complete.
        for (int i = 0; i < downloadItems.size(); i++) {
            FtpDownloadItem item = (FtpDownloadItem) downloadItems.get(i);
            if (item.error()) {
                error = true;
                errorMsg = item.getErrorMessage();
            }
            if (this.interrupted() == true) {
                this.abortDownloadThreads();
                break;
            }

            if (item.getConnected() == true) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // ignore
                }
                --i;
            }
        }

        if (error) {
            throw new FtpException(errorMsg);
        }
    }


    /**
     * Vector to hold the SftpDownloadItem instances.
     */
    private Vector downloadItems = new Vector();
    /**
     * Hashmap to hold the upload threads, this is useful to keep track of which file is handled by which thread.
     */
    private HashMap<String, FtpDownloadItem> downloadHashMap = new HashMap<String, FtpDownloadItem>();

    /**
     * Cancel the given file transfer thread.
     *
     * @param file
     */
    public void abortDownloadThread(String file) {
        this.downloadHashMap.get(file).interruptTransfer();
    }

    /**
     * Abort all download threads.
     */
    public void abortDownloadThreads() {
        for (int i = 0; i < downloadItems.size(); i++) {
            ((FtpDownloadItem) downloadItems.get(i)).cancelTransfer();
        }
    }

    private class FtpDownloadItem
            extends Thread {
        private Ftp parent = null;
        private Ftp ftp = null;
        private Vector files = null;
        private boolean performChecksum = false;
        private String currentRemoteDir = null;
        private File currentLocalDir = null;
        private boolean connected = true;
        private boolean abortAll = false;
        private boolean error = false;
        private String errorMsg = null;

        private FtpDownloadItem(Ftp ftp, Vector files, boolean performChecksum, String currentRemoteDir, File currentLocalDir) {
            this.parent = ftp;
            this.files = files;
            this.performChecksum = performChecksum;
            this.currentRemoteDir = currentRemoteDir;
            this.currentLocalDir = currentLocalDir;
            this.ftp = new Ftp(ftp.getHostname(), ftp.getUsername(), ftp.getPassword());
            this.ftp.setTimezone(ftp.getTimeZone());
            this.ftp.setPreserveDownloadTimestamp(ftp.getPreserveDownloadTimestamp());
            this.ftp.setPreserveUploadTimestamp(ftp.getPreserveUploadTimestamp());
            this.ftp.setPassive(ftp.getPassive());
            this.ftp.setPort(ftp.getPort());
            this.ftp.setConnectBeforeCommand(ftp.getConnectBeforeCommand());
            this.ftp.setBlockTransferSize(ftp.getBlockTransferSize());
            this.ftp.setCompression(ftp.getCompression());
            this.ftp.setDataPortRange(ftp.getDataPortStart(), ftp.getDataPortEnd());
            this.ftp.setErrorOnSizeCommand(ftp.getErrorOnSizeCommand());
            this.ftp.setFtpFileParser(ftp.getFtpFileParser());
            this.ftp.setLocalDir(ftp.getLocalDir());
            this.ftp.setLinger(ftp.getLinger());
            this.ftp.setDebug(ftp.getDebug());
            this.ftp.setDebugStream(ftp.getDebugStream());

            try {
                this.ftp.setNATAddress(ftp.getNATAddress());
            } catch (Exception ne) {
                // ignore
            }

            this.ftp.setPortAddress(ftp.getPortAddress());
            this.ftp.setProxyAuthentication(ftp.impl.getProxyUsername(), ftp.impl.getProxyPassword());
            this.ftp.setProxyHost(ftp.impl.getProxyHostname(), ftp.impl.getProxyPort());
            this.ftp.setProxyType(ftp.impl.getProxyType());
            this.ftp.setTimeout(ftp.getTimeout());
            this.ftp.setUseEPRT(ftp.getUseEPRT());
            this.ftp.setUseEPSV(ftp.getUseEPSV());
            this.ftp.impl.setListeners(ftp.impl.getListeners());
        }

        /**
         * Get connected.
         *
         * @return connected
         */
        public boolean getConnected() {
            return this.connected;
        }

        /**
         * Interrupt the transfer thread.
         */
        public void interruptTransfer() {
            try {
                this.ftp.interrupt();
            } catch (Exception e) {
                // ignore
            }
        }

        /**
         * Cancel the upload thead
         */
        public void cancelTransfer() {
            try {
                this.abortAll = true;
                this.ftp.interrupt();
            } catch (Exception e) {
                // ignore
            } finally {
                this.connected = false;
            }
        }

        public boolean error() {
            return this.error;
        }

        public String getErrorMessage() {
            return this.errorMsg;
        }

        /**
         * Download the files.
         */
        public void run() {
            try {
                this.ftp.connect();
                this.connected = true;

                if (ftp.getMode() == Ftp.ASCII) {
                    this.ftp.setAscii();
                } else if (ftp.getMode() == Ftp.BINARY) {
                    this.ftp.setBinary();
                } else if (ftp.getMode() == Ftp.AUTO) {
                    this.ftp.setAuto(true);
                }

                Enumeration elements = files.elements();
                while (elements.hasMoreElements() && this.abortAll == false) {
                    ftp.reset();
                    ftp.setDir(this.currentRemoteDir);
                    ftp.setLocalDir(this.currentLocalDir);

                    String fileName = (String) elements.nextElement();
                    File tempFile = new File(fileName);

                    String relativePath = FileTools.getRelativePath(new File(this.currentRemoteDir), tempFile);

                    if ((relativePath != null) && !relativePath.equals("")) {
                        ftp.setDir(relativePath);
                        ftp.setLocalDir(ftp.makeLocalDir(relativePath));
                    }

                    try {
                        ftp.download(tempFile.getName());

                        File fileDownloaded = new File(ftp.getLocalDir() + "/" + tempFile.getName());

                        if ((fileDownloaded.length() > 0) && this.performChecksum && parent.isFeatureSupported("XCRC")) {
                            String fileDir = ftp.getDir();

                            if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                                fileDir = fileDir + "/";
                            }

                            if (!ftp.checksum(fileDownloaded, fileDir + tempFile.getName())) {
                                throw new FtpException("Checksum returned false");
                            }
                        }
                    } catch (FtpException fe) {
                        if (ftp.interrupted() == true) {
                            continue;
                        } else {
                            this.error = true;
                            this.errorMsg = fe.getMessage();

                            //fire FtpErrorEvent for FtpErrorListener(s)
                            for (Iterator i = impl.getListeners().iterator(); i.hasNext(); ) {
                                FtpListener listener = (FtpListener) i.next();

                                if (listener instanceof FtpErrorListener) {
                                    ((FtpErrorListener) listener).error(new FtpErrorEvent(this,
                                                                                          tempFile.getName(),
                                                                                          this.currentRemoteDir + "/" + relativePath,
                                                                                          this.currentLocalDir + File.separator + relativePath + File.separator + tempFile.getName(),
                                                                                          FtpProgressEvent.DOWNLOAD));
                                }
                            }

                            fe.printStackTrace();
                        }
                    }
                }
            } catch (Exception e) {
                // todo: ignore?
            } finally {
                this.ftp.disconnect();
                this.connected = false;
            }
        }
    }

    /**
     * Downloads remote directory and contents from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteDir       the directory to download
     * @param retryLimit      retry limit if an error occurs
     * @param performChecksum whether to perform checksum after each download, not supported by all FTP servers
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void downloadDir(String remoteDir, int retryLimit, int retryInterval,
                                         boolean performChecksum)
            throws FtpException {
        String lastCurrentRemoteDir = getDir();
        setDir(remoteDir);

        File lastLocalDir = getLocalDir();
        boolean success = false;

        String dirName = getDirectoryName(remoteDir);

        if (!dirName.equals("..")) {
            setLocalDir(makeLocalDir(dirName));
        }

        Vector fileList = getRemoteFileList(remoteDir);

        Vector remoteEmptyDirs = getRemoteEmptyDirList(remoteDir);
        makeEmptyLocalDirs(remoteEmptyDirs, new File(remoteDir));

        String currentRemoteDir = getDir();
        File currentLocalDir = getLocalDir();

        for (int i = 0; (i <= retryLimit) && !this.impl.interrupted(); ++i) {
            try {
                downloadDir(currentRemoteDir, currentLocalDir, performChecksum,
                            fileList);

                success = true;
            } catch (Exception e) {

                //  throw exception if retry limit exceeded ... otherwise continue
                if (i >= retryLimit) {
                    throw new FtpException(e.getMessage());
                }


                // sleep some time before disconnecting and trying again 
                if (i < retryLimit) {
                    try {
                        Thread.sleep(retryInterval * 1000);
                    } catch (InterruptedException ie) {
                        // ignore
                    }
                }

                disconnect();
                connect();
            }

            if (success) {
                setDir(lastCurrentRemoteDir);
                setLocalDir(lastLocalDir);

                break;
            }
        }
    }


    /**
     * Downloads a directory.
     *
     * @param currentRemoteDir Remote directory
     * @param currentLocalDir  Local directory
     * @param performChecksum  Perform checksum indicator
     * @param fileList         File list will be downloaded
     *
     * @throws IOException  If a IO error occurs
     * @throws FtpException If a FTP error occurs
     */
    private void downloadDir(String currentRemoteDir, File currentLocalDir,
                             boolean performChecksum, Vector fileList)
            throws IOException, FtpException {
        File parent = new File(currentRemoteDir);

        while ((fileList.size() > 0) && !this.impl.interrupted()) {
            setDir(currentRemoteDir);
            setLocalDir(currentLocalDir);

            String fileName = (String) fileList.elementAt(0);
            File tempFile = new File(fileName);

            String relativePath = FileTools.getRelativePath(parent, tempFile);

            if ((relativePath != null) && !relativePath.equals("")) {
                setDir(relativePath);
                setLocalDir(makeLocalDir(relativePath));
            }

            download(tempFile.getName());

            File fileDownloaded = new File(getLocalDir() + "/" + tempFile.getName());

            if ((fileDownloaded.length() > 0) && performChecksum && isFeatureSupported("XCRC")) {
                String fileDir = getDir();

                if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                    fileDir = fileDir + "/";
                }

                if (!checksum(fileDownloaded, fileDir + tempFile.getName())) {
                    throw new FtpException("Checksum returned false");
                }
            }

            fileList.remove(0);
        }
    }

    /**
     * Interrupts the current data transfer and sets interrupted flag to <code>true</code>.
     * After invoking this method you must reset the interrupted flag to <code>false</code>
     * before transferring any files.
     *
     * @see #reset
     */
    public void interrupt() {
        this.impl.interrupt();
    }

    /**
     * Checks if transfer process was interrupted.
     *
     * @return <code>true</code> if transfer process was interrupted.
     */
    public boolean interrupted() {
        return this.impl.interrupted();
    }

    /**
     * Issues command to FTP server.
     *
     * @param cmd the command to issue
     *
     * @return response code
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String issueCommand(String cmd)
            throws FtpException {
        return this.impl.issueCommand(cmd);
    }

    /**
     * Issues command to FTP server.
     *
     * @param cmd the command to issue
     *
     * @return response code
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String issueCommandCheck(String cmd)
            throws FtpException {
        return this.impl.issueCommandCheck(cmd);
    }

    /**
     * Logs into FTP server after establishing a connection.
     * This method should only be used if login is not performed automatically during <code>Ftp#connect</code>.
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized void login()
            throws FtpException {
        this.impl.login();
    }

    /**
     * Makes directory on FTP server.
     *
     * @param newDirectory name of directory to make
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void makeDir(String newDirectory)
            throws FtpException {
        this.impl.makeDir(newDirectory);
    }

    /**
     * Makes directory on FTP server, creating subdirectories if necessary.
     *
     * @param newDirectory full path of directory to make
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized void makeDirRecursive(String newDirectory)
            throws FtpException {
        String currentDir = getDir();

        if (newDirectory.indexOf(currentDir) == 0) {
            newDirectory = newDirectory.substring(currentDir.length());
        }

        StringTokenizer st = new StringTokenizer(newDirectory, "/");

        while (st.hasMoreElements()) {
            String path = (String) st.nextElement();

            try {
                this.setDir(path);
            } catch (FtpException sd) {
                try {
                    this.makeDir(path);
                    this.setDir(path);
                } catch (FtpException md) {
                    throw md;
                }
            }
        }

        setDir(currentDir);
    }

    /**
     * Creates local directory.
     * Directory is relative to current local directory.
     *
     * @param directoryName name of directory
     *
     * @return the directory
     *
     * @see #getLocalDir
     */
    public synchronized File makeLocalDir(String directoryName) {
        File dir = new File(getLocalDir(), directoryName);

        // prepares parent directories if they do not exist
        this.createDirectoryStructure(dir);

        if (!dir.exists()) {
            dir.mkdir();
        }

        return dir;
    }

    /**
     * Deletes multiple files matching regular expression in FTP server's current directory.
     * For example, to delete all files ending in .txt extension use regular expression "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>"
     *
     * @param filter the filter to use
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void mdelete(String filter)
            throws FtpException {
        Matcher regex = createRegex(filter);

        for (Enumeration e = getDirListing(); e.hasMoreElements() && !this.impl.interrupted(); ) {
            FtpFile file = (FtpFile) e.nextElement();
            String filename = file.getFilename();

            if (regex.matches(filename)) {
                if (file.isDirectory()) {
                    deleteDir(filename);
                } else {
                    deleteFile(filename);
                }
            }
        }
    }

    /**
     * Downloads specified files in current working remote directory from FTP
     * server to local directory. Do not use this method to download a directory,
     * see <code>downloadDir(String)</code> method.
     * Downloads are stored relative to current local directory.
     *
     * @param fileNames filenames of files to download
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     * @see #downloadDir(String)
     */
    public synchronized void mdownload(Enumeration fileNames)
            throws FtpException {
        while (fileNames.hasMoreElements() && !this.impl.interrupted()) {
            String file = fileNames.nextElement().toString();

            if (file != null) {
                download(file);
            }
        }
    }

    /**
     * Downloads multiple files matching regular expression from FTP server current
     * directory.
     * <p/>
     * Downloads are stored relative to current local directory.
     * For example to download all files ending with extension .txt use the regular
     * expression "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>". To download a directory use the <code>downloadDir</code> method.
     *
     * @param filter the regular expression filter to use
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see #setLocalDir
     * @see #downloadDir
     */
    public synchronized void mdownload(String filter)
            throws FtpException {
        Matcher regex = createRegex(filter);

        for (Enumeration fileNames = getDirListing(); fileNames.hasMoreElements() && !this.impl.interrupted(); ) {
            FtpFile file = (FtpFile) fileNames.nextElement();
            String filename = file.getFilename();

            if (!file.isDirectory() && regex.matches(filename)) {
                download(filename);
            }
        }
    }

    /**
     * Uploads multiple files to FTP server.
     * Uploads are performed relative to current local directory.
     *
     * @param fileNames an <code>Enumeration</code> of <code>String</code> filenames relative to current local directory to upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void mupload(Enumeration fileNames)
            throws FtpException {
        while (fileNames.hasMoreElements() && !this.impl.interrupted()) {
            String file = (String) fileNames.nextElement();

            if (file != null) {
                upload(file);
            }
        }
    }

    /**
     * Uploads multiple files matching regular expression to FTP server's current directory.
     * Uploads are performed relative to current local directory. For example, to upload files
     * ending in extension .txt use the regular expression "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>".
     *
     * @param filter the filter to use
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void mupload(String filter)
            throws FtpException {
        Matcher regex = createRegex(filter);
        File[] files = getLocalDir().listFiles();

        for (int index = 0; index < files.length && !this.impl.interrupted(); ++index) {
            if (regex.matches(files[index].getName())) {
                upload(files[index]);
            }
        }
    }

    /**
     * Sends a NOOP (No Operation) command to FTP server.
     * This is useful in helping to prevent a lost connection due to inactivity.
     *
     * @return the response to the NOOP command
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String noop()
            throws FtpException {
        return this.impl.noop();
    }

    /**
     * Reads server response.
     * Normally this method should not be invoked as this is done automatically.  This should
     * be used only in cases where manual reading of response is necessary.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    public synchronized void readResponse()
            throws FtpException {
        this.impl.readResponse();
    }

    /**
     * Removes specified FTP event listener.
     *
     * @param listener the listener to remove
     *
     * @see FtpListener
     */
    public synchronized void removeFtpListener(FtpListener listener) {
        this.impl.removeListener(listener);
    }

    /**
     * Renames remote file on FTP server.
     *
     * @param remoteFile name of file to rename
     * @param newFile    new name of file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void renameFile(String remoteFile, String newFile)
            throws FtpException {
        this.impl.renameFile(remoteFile, newFile);
    }

    /**
     * Resets the interrupted flag to <code>false</code>.
     * This method should be invoked if you have interrupted a transfer using the
     * <code>Ftp#interrupt</code> method and wish to resume transfering files.
     *
     * @see #interrupt
     */
    public void reset() {
        this.impl.reset();
    }

    /**
     * Resumes download of file from FTP server.
     *
     * @param remoteFile the file to download
     * @param off        file offset in bytes
     *
     * @return File the file downloaded
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized File resumeDownload(String remoteFile, long off)
            throws FtpException {
        return resumeDownload(remoteFile, remoteFile, off);
    }

    /**
     * Resumes download of file from FTP server.
     *
     * @param localFile  local file name
     * @param remoteFile the file to download
     * @param off        file offset in bytes
     *
     * @return File the file downloaded
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized File resumeDownload(String localFile, String remoteFile, long off)
            throws FtpException {
        if (off == 0) {
            return download(localFile, remoteFile);
        }

        File result = new File(getLocalDir(), localFile);

        // create temporary file to store results of download
        File tmp = new File(getLocalDir().getAbsolutePath() + File.separator + System.currentTimeMillis() + ".tmp");

        // create output stream to write to temporary file
        FileOutputStream out = null;

        // create random access file to write to destination file
        RandomAccessFile raf = null;

        // create input stream to read from temporary file
        BufferedInputStream bin = null;

        try {
            out = new FileOutputStream(tmp);
            this.impl.setTargetPath(result.getAbsolutePath());
            this.impl.setTargetFile(remoteFile);

            // download results to temporary file            

            try {
                this.impl.download(out, remoteFile, off);
            } catch (FtpException e) {
                // ignore
            }

            // write temporary file to result file
            raf = new RandomAccessFile(result, "rw");
            raf.seek(off);
            bin = new BufferedInputStream(new FileInputStream(tmp));

            byte[] buffer = new byte[1024 * 30];
            int b = -1;

            while ((b = bin.read(buffer)) != -1) {
                raf.write(buffer, 0, b);
            }
        } catch (IOException ioe) {
            throw new FtpException(ioe.getMessage(), ioe);
        } finally {
            this.impl.setTargetPath(null);
            this.impl.setTargetFile(null);
            IOTools.close(out);
            IOTools.close(bin);
            IOTools.close(raf);
            // delete temp file
            tmp.delete();
        }

        return result;
    }

    /**
     * Resumes upload of file to FTP server.
     *
     * @param localFile the local file to upload
     * @param off       file offset in bytes
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void resumeUpload(File localFile, long off)
            throws FtpException {
        resumeUpload(localFile, localFile.getName(), off);
    }

    /**
     * Resumes upload of file to FTP server.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination file on FTP server
     * @param off        local file offset in bytes
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized void resumeUpload(File localFile, String remoteFile, long off)
            throws FtpException {
        InputStream in = getInputStream(localFile);
        this.impl.setTargetPath(localFile.getAbsolutePath());
        this.impl.setTargetFile(remoteFile);
        boolean error = false;

        try {
            this.impl.upload(in, remoteFile, false, off, localFile.length(), false);
        } catch (FtpException e) {
            error = true;
            throw e;
        } finally {
            this.impl.setTargetPath(null);
            this.impl.setTargetFile(null);
            IOTools.close(in);

            try {
                if (error == false) {
                    if (this.getPreserveUploadTimestamp() && this.impl.interrupted() == false) {

                        if (this.impl.isConnected() == false) {
                            this.impl.connect();
                            this.impl.reset();
                        }


                        Date date = new Date(localFile.lastModified());
                        this.setFileTimestamp(remoteFile, date);

                        if (this.impl.getDebug()) {
                            this.impl.getDebugStream().println("Timestamp of remote file " + remoteFile + " changed successfully. [" + date.toString() + "]");
                        }
                    }
                }
            } catch (FtpException fe) {
                if (this.impl.getDebug()) {
                    this.impl.getDebugStream().println("Timestamp of remote file " + remoteFile + " could not be changed.");
                }
            }
        }
    }

    /**
     * Resumes upload of file to FTP server.
     *
     * @param localFile the local file to upload
     * @param off       file offset in bytes
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void resumeUpload(String localFile, long off)
            throws FtpException {
        resumeUpload(localFile, localFile, off);
    }

    /**
     * Resumes upload of file to FTP server.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination file on FTP server
     * @param off        the local file offset
     *
     * @throws FtpException if an I/O or FTP related error occurs.
     */
    public synchronized void resumeUpload(String localFile, String remoteFile,
                                          long off)
            throws FtpException {
        resumeUpload(new File(getLocalDir(), localFile), remoteFile, off);
    }

    /**
     * Uploads file to FTP server.
     * If <code>localFile</code> is a directory then directory and entire contents are uploaded.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination of upload
     * @param append     <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(File localFile, String remoteFile, boolean append)
            throws FtpException {
        boolean error = false;

        if (!localFile.isFile()) {
            uploadDir(localFile);
        } else {
            InputStream in = getInputStream(localFile);
            this.impl.setTargetPath(localFile.getAbsolutePath());
            this.impl.setTargetFile(remoteFile);
            String remoteDirectory = this.impl.getDir();
            try {
                this.impl.upload(in, remoteFile, append, 0, localFile.length(), false);
            } catch (FtpException e) {
                error = true;
                throw e;
            } finally {
                this.impl.setTargetPath(null);
                this.impl.setTargetFile(null);
                IOTools.close(in);

                try {
                    if (error == false) {
                        if (this.impl.getPreserveUploadTimestamp() && this.impl.interrupted() == false) {
                            if (this.impl.isConnected() == false) {
                                this.impl.connect();
                                this.impl.reset();
                                this.impl.setDir(remoteDirectory);
                            }

                            Date date = new Date(localFile.lastModified());
                            this.impl.setFileModificationTime(remoteFile, date);

                            if (this.impl.getDebug()) {
                                this.impl.getDebugStream().println("Timestamp of remote file " + remoteFile + " changed successfully. [" + date.toString() + "]");
                            }
                        }
                    }
                } catch (FtpException fe) {
                    try {

                        Date date = new Date(localFile.lastModified());
                        this.impl.setFileModificationTime(remoteFile, date);

                        if (this.impl.getDebug()) {
                            this.impl.getDebugStream().println("Timestamp of remote file " + remoteFile + " changed successfully. [" + date.toString() + "]");
                        }

                    } catch (FtpException ie) {

                        if (this.impl.getDebug()) {
                            this.impl.getDebugStream().println("Timestamp of remote file " + remoteFile + " could not be changed.");
                        }
                    }
                }

            }
        }
    }

    /**
     * Uploads file to FTP server.
     *
     * @param input      the <code>InputStream</code> to read from
     * @param remoteFile the destination filename of upload
     * @param append     true to append, false otherwise
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(InputStream input, String remoteFile,
                                    boolean append)
            throws FtpException {
        this.impl.setTargetPath(null);
        this.impl.setTargetFile(remoteFile);

        int available = -1;

        try {
            available = input.available();
        } catch (IOException ioe) {
            available = -1;
        }

        try {
            this.impl.upload(input, remoteFile, append, 0, available, false);
        } finally {
            this.impl.setTargetPath(null);
            this.impl.setTargetFile(null);
        }
    }

    /**
     * Uploads file to FTP server.
     *
     * @param input      the <code>InputStream</code> to read from
     * @param remoteFile the destination filename of upload
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(InputStream input, String remoteFile)
            throws FtpException {
        upload(input, remoteFile, false);
    }

    /**
     * Uploads file to FTP server.
     * If localFile is a directory then directory and entire contents are uploaded.
     *
     * @param localFile the local file to upload
     * @param append    <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(File localFile, boolean append)
            throws FtpException {
        upload(localFile, localFile.getName(), append);
    }

    /**
     * Uploads file to FTP server.
     * If <code>localFile</code> is a directory then directory and entire contents are uploaded.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination of upload
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(File localFile, String remoteFile)
            throws FtpException {
        upload(localFile, remoteFile, false);
    }

    /**
     * Uploads file to FTP server.
     * If <code>localFile</code> is a directory then directory and entire contents are uploaded.
     *
     * @param localFile the local file to upload
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(File localFile)
            throws FtpException {
        upload(localFile, false);
    }

    /**
     * Uploads file to FTP server and renames the remote file name.
     *
     * @param extension the temporary extension to append to file before renaming
     * @param localFile the local file to upload
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(String extension, File localFile)
            throws FtpException {
        if (StringTools.isValidString(extension)) {
            String remoteFileName;

            if (extension.startsWith(".")) {
                remoteFileName = localFile.getName() + extension;
            } else {
                remoteFileName = localFile.getName() + "." + extension;
            }

            String remoteDirectory = this.getDir();
            upload(localFile, remoteFileName);

            if (this.isConnected() == false) {
                this.connect();
                this.reset();
                this.setDir(remoteDirectory);
            }

            if (this.impl.interrupted() == false) {
                String newFileName = localFile.getName();

                try {
                    this.deleteFile(newFileName);
                } catch (FtpException e) { /* ignore */
                }

                try {
                    renameFile(remoteFileName, newFileName);
                } catch (FtpException e) { /*ignore*/
                }

            }
        } else {
            upload(localFile, false);
        }
    }

    /**
     * Uploads file to FTP server.
     * Uploads are performed relative to current local directory.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination filename of upload
     * @param append     <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void upload(String localFile, String remoteFile,
                                    boolean append)
            throws FtpException {
        upload(new File(getLocalDir(), localFile), remoteFile, append);
    }

    /**
     * Uploads file to FTP server.
     * Uploads are performed relative to current local directory.
     *
     * @param localFile  the local file to upload
     * @param remoteFile the destination filename of upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void upload(String localFile, String remoteFile)
            throws FtpException {
        upload(localFile, remoteFile, false);
    }

    /**
     * Uploads file to FTP server.
     * Uploads are performed relative to current local directory.
     *
     * @param localFile the local file to upload
     * @param append    <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void upload(String localFile, boolean append)
            throws FtpException {
        upload(localFile, localFile, append);
    }

    /**
     * Uploads file to FTP server.
     * Uploads are performed relative to current local directory.
     *
     * @param localFile the local file to upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #setLocalDir
     */
    public synchronized void upload(String localFile)
            throws FtpException {
        upload(localFile, false);
    }

    /**
     * Uploads file to FTP server where contents of file is not on disk but
     * in memory.
     *
     * @param data       the raw contents of the file to upload
     * @param remoteFile the filename to store file as on FTP server
     * @param append     <code>true</code> to append to remote file on FTP server, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(byte[] data, String remoteFile,
                                    boolean append)
            throws FtpException {
        ByteArrayInputStream in = new ByteArrayInputStream(data);
        this.impl.setTargetFile(remoteFile);

        try {
            this.impl.upload(in, remoteFile, append, 0, data.length, false);
        } finally {
            this.impl.setTargetFile(null);
            IOTools.close(in);
        }
    }

    /**
     * Uploads file to FTP server where contents of file is not on disk but
     * in memory.
     *
     * @param data       the raw contents of the file to upload
     * @param remoteFile the filename to store file as on FTP server
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void upload(byte[] data, String remoteFile)
            throws FtpException {
        upload(data, remoteFile, false);
    }

    /**
     * Uploads local directory and contents recursively to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir the directory to upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir)
            throws FtpException {
        uploadDir(localDir, null);
    }


    //  --------------------------------------------------------------------------

    /**
     * Uploads local directory and contents recursively to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir  the directory to upload
     * @param extension the temporary extension to use before renaming file or <code>null</code> to use default extension
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir, String extension)
            throws FtpException {
        FtpException error = null;
        String curRemoteDir = getDir();

        if (!localDir.exists()) {
            throw new FtpException("Local file or directory " + localDir.getAbsolutePath() + " does not exist.");
        }

        // directory already exists .. OK
        try {
            makeDir(localDir.getName());
        } catch (FtpException e) {
            // todo: ignore?
        }

        setDir(localDir.getName());

        File curLocalDir = getLocalDir();
        setLocalDir(localDir);

        try {
            Enumeration files = getLocalDirListing();

            while (files.hasMoreElements() && !this.impl.interrupted()) {
                File file = (File) files.nextElement();

                if (file.isFile()) {
                    upload(extension, file);
                } else if (!isCurrentOrParentDir(file)) {
                    uploadDir(file, extension);
                }
            }
        } catch (FtpException fe) {
            error = fe;
        } finally {
            setDir(curRemoteDir);
            setLocalDir(curLocalDir);

            try {
                if (this.getPreserveUploadTimestamp() && this.interrupted() == false) {
                    if (this.isConnected() == false) {
                        this.connect();
                        this.reset();
                    }

                    Date date = new Date(localDir.lastModified());
                    this.setFileTimestamp(localDir.getName(), date);

                    if (this.impl.getDebug()) {
                        this.impl.getDebugStream().println("Timestamp of remote directory " + localDir.getName() + " changed successfully. [" + date.toString() + "]");
                    }
                }
            } catch (Exception e) {
                if (this.impl.getDebug()) {
                    this.impl.getDebugStream().println("Timestamp of remote directory " + localDir.getName() + " could not be changed");
                }
            }

        }

        if (error != null) {
            throw error;
        }
    }

    /**
     * Uploads local directory and contents to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir        the directory to upload
     * @param retryLimit      the retry limit if upload fails
     * @param performChecksum whether to perform checksum after each file upload, not supported by all FTP servers
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir, int retryLimit,
                                       boolean performChecksum)
            throws FtpException {
        uploadDir(localDir, retryLimit, 0, performChecksum, null);
    }

    /**
     * Uploads local directory and contents to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir        the directory to upload
     * @param retryLimit      the retry limit if upload fails
     * @param performChecksum whether to perform checksum after each upload, not supported by all FTP servers
     * @param extension       the temporary extension to use before renaming files or <code>null</code> to use default extension
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir, int retryLimit, int retryInterval,
                                       boolean performChecksum, String extension)
            throws FtpException {
        String lastCurrentDir = getDir();

        if (!localDir.exists()) {
            throw new FtpException("Local file or directory " + localDir.getAbsolutePath() + " does not exist.");
        }

        boolean success = false;

        try {
            makeDir(localDir.getName());
        } catch (FtpException e) {
            // todo: ignore?
        }

        setDir(localDir.getName());

        File lastLocalDir = getLocalDir();
        setLocalDir(localDir);

        Vector fileList = FileTools.listFiles(localDir);
        String currentRemoteDir = getDir();

        Vector emptyDirList = FileTools.getEmptyDirList(localDir);
        makeEmptyDir(emptyDirList, localDir);

        for (int i = 0; (i <= retryLimit) && !this.impl.interrupted(); ++i) {
            try {
                uploadDir(currentRemoteDir, performChecksum, extension,
                          fileList, localDir);
                success = true;
            } catch (Exception e) {
                // throw exception if retry limit exceeded ... otherwise
                // continue
                if (i >= retryLimit) {
                    throw new FtpException(e.getMessage());
                }

                // sleep some time before trying again
                try {
                    Thread.sleep(retryInterval * 1000);
                } catch (InterruptedException ie) {
                    // ignore
                }

                disconnect();
                connect();
            }

            if (success) {
                setDir(lastCurrentDir);
                setLocalDir(lastLocalDir);

                break;
            }
        }
    }

    public synchronized void uploadDir(File localDir, int retryLimit,
                                       boolean performChecksum, String extension, int threadCount)
            throws FtpException {
        this.uploadDir(localDir, retryLimit, 0, performChecksum, extension, threadCount);
    }

    /**
     * Uploads local directory and contents to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir        the directory to upload
     * @param retryLimit      the retry limit if upload fails
     * @param performChecksum whether to perform checksum after each upload, not supported by all FTP servers
     * @param extension       the temporary extension to use before renaming files or <code>null</code> to use default extension
     * @param threadCount     the number of threads to use for transferring files
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir, int retryLimit, int retryInterval,
                                       boolean performChecksum, String extension, int threadCount)
            throws FtpException {

        if (threadCount == 0 || threadCount == 1) {
            uploadDir(localDir, retryLimit, retryInterval, performChecksum, extension);
            return;
        } else if (threadCount < 0) {
            throw new FtpException("Illegal thread count value : " + threadCount + ". Must be greather than zero.");
        }

        String lastCurrentDir = getDir();

        if (!localDir.exists()) {
            throw new FtpException("Local file or directory " + localDir.getAbsolutePath() + " does not exist.");
        }

        boolean success = false;

        try {
            makeDir(localDir.getName());
        } catch (FtpException e) {
            // todo: ignore?
        }

        setDir(localDir.getName());

        File lastLocalDir = getLocalDir();
        setLocalDir(localDir);

        Vector fileList = FileTools.listFiles(localDir);
        String currentRemoteDir = getDir();

        Vector emptyDirList = FileTools.getEmptyDirList(localDir);
        makeEmptyDir(emptyDirList, localDir);

        for (int i = 0; (i <= retryLimit) && !this.impl.interrupted(); ++i) {
            try {
                uploadDir(currentRemoteDir, performChecksum, extension, fileList, localDir, threadCount);
                success = true;
            } catch (Exception e) {
                e.printStackTrace();
                this.abortUploadThreads();

                // throw exception if retry limit exceeded ... otherwise
                // continue
                if (i >= retryLimit) {
                    throw new FtpException(e.getMessage());
                }

                // sleep some time before trying again
                try {
                    Thread.sleep(retryInterval * 1000);
                } catch (InterruptedException ie) {
                    // ignore
                }

                disconnect();
                connect();
            }

            if (success) {
                setDir(lastCurrentDir);
                setLocalDir(lastLocalDir);

                break;
            }
        }
    }

    /**
     * Uploads a directory.
     *
     * @param currentRemoteDir Remote directory
     * @param performChecksum  whether checksum should be performed, not supported by all FTP servers
     * @param extension        the temporary extension to use when uploading files or <code>null</code> to use default extension
     * @param fileList         File list will be uploaded
     * @param localDir         Local directory
     * @param threadCount      the number of threads to use to transfer files
     *
     * @throws FtpException if an FTP related error occurs
     * @throws IOException  if an I/O related error occurs
     */
    private void uploadDir(String currentRemoteDir, boolean performChecksum,
                           String extension, Vector fileList, File localDir, int threadCount)
            throws FtpException, IOException {

        this.uploadItems.clear();
        this.uploadHashMap.clear();

        int totalFiles = fileList.size();
        int totalThreads = threadCount;

        // declare the thread map according to the number of threads available
        Vector threadMap = new Vector();
        for (int i = 0; i < totalThreads; i++) {
            threadMap.add(new Vector());
        }

        // initialize the thread map.
        int j = 0;
        for (int i = 0; i < totalFiles; i++) {
            if (j >= threadMap.size()) {
                j = 0;
            }
            Vector thread = (Vector) threadMap.get(j);
            thread.add(fileList.get(i));
            threadMap.setElementAt(thread, j++);
        }

        // add the upload threads to a Vector object.
        for (int i = 0; i < threadMap.size(); i++) {
            Vector files = (Vector) threadMap.get(i);
            if (files.size() == 0) {
                continue;
            }
            FtpUploadItem item = new FtpUploadItem(this, extension, files, currentRemoteDir, localDir, performChecksum);
            uploadItems.add(item);

            for (int index = 0; index < files.size(); index++) {
                this.uploadHashMap.put(files.get(index).toString(), item);
            }
        }

        // start the threads.
        for (int i = 0; i < uploadItems.size(); i++) {
            ((FtpUploadItem) uploadItems.get(i)).start();
        }

        boolean error = false;
        String errorMsg = null;

        // wait for upload threads to complete.
        for (int i = 0; i < uploadItems.size(); i++) {
            FtpUploadItem item = (FtpUploadItem) uploadItems.get(i);
            if (item.error()) {
                error = true;
                errorMsg = item.getErrorMessage();
            }
            if (this.interrupted() == true) {
                this.abortUploadThreads();
                break;
            }
            if (item.getConnected() == true) {
                try {
                    Thread.sleep(100);
                } catch (Exception e) {
                    // ignore
                }
                --i;
            }
        }

        if (error) {
            throw new FtpException(errorMsg);
        }
    }

    /**
     * Vector to hold the upload threads.
     */
    private Vector uploadItems = new Vector();

    /**
     * Hashmap to hold the upload threads, this is useful to keep track of which file is handled by which thread.
     */
    private HashMap<String, FtpUploadItem> uploadHashMap = new HashMap<String, FtpUploadItem>();

    /**
     * Cancel the given file transfer thread.
     *
     * @param file
     */
    public void abortUploadThread(String file) {
        this.uploadHashMap.get(file).interruptTransfer();
    }

    /**
     * Cancel all upload threads.
     */
    public void abortUploadThreads() {
        for (int i = 0; i < uploadItems.size(); i++) {
            ((FtpUploadItem) uploadItems.get(i)).cancelTransfer();
        }
    }

    private class FtpUploadItem
            extends Thread {

        private Ftp parent = null;
        private Ftp ftp = null;
        private String extension = null;
        private Vector files = null;
        private String currentRemoteDir = null;
        private boolean connected = true;
        private File currentFile = null;
        private File localDir = null;
        private boolean performChecksum = false;
        private boolean abortAll = false;
        private boolean error = false;
        private String errorMsg = null;

        private FtpUploadItem(Ftp ftp, String extension, Vector files, String remoteDir, File localDir, boolean performChecksum) {
            this.parent = ftp;
            this.extension = extension;
            this.files = files;
            this.currentRemoteDir = remoteDir;
            this.localDir = localDir;
            this.performChecksum = performChecksum;
            this.ftp = new Ftp(ftp.getHostname(), ftp.getUsername(), ftp.getPassword());
            this.ftp.setTimezone(ftp.getTimeZone());
            this.ftp.setPreserveUploadTimestamp(ftp.getPreserveUploadTimestamp());
            this.ftp.setPreserveDownloadTimestamp(ftp.getPreserveDownloadTimestamp());
            this.ftp.setPassive(ftp.getPassive());
            this.ftp.setPort(ftp.getPort());
            this.ftp.setConnectBeforeCommand(ftp.getConnectBeforeCommand());
            this.ftp.setBlockTransferSize(ftp.getBlockTransferSize());
            this.ftp.setCompression(ftp.getCompression());
            this.ftp.setDataPortRange(ftp.getDataPortStart(), ftp.getDataPortEnd());
            this.ftp.setErrorOnSizeCommand(ftp.getErrorOnSizeCommand());
            this.ftp.setFtpFileParser(ftp.getFtpFileParser());
            this.ftp.setLocalDir(ftp.getLocalDir());
            this.ftp.setLinger(ftp.getLinger());

            try {
                this.ftp.setNATAddress(ftp.getNATAddress());
            } catch (Exception ne) {
                // ignore
            }

            this.ftp.setPortAddress(ftp.getPortAddress());
            this.ftp.setProxyAuthentication(ftp.impl.getProxyUsername(), ftp.impl.getProxyPassword());
            this.ftp.setProxyHost(ftp.impl.getProxyHostname(), ftp.impl.getProxyPort());
            this.ftp.setProxyType(ftp.impl.getProxyType());
            this.ftp.setTimeout(ftp.getTimeout());
            this.ftp.setUseEPRT(ftp.getUseEPRT());
            this.ftp.setUseEPSV(ftp.getUseEPSV());
            this.ftp.impl.setListeners(ftp.impl.getListeners());
        }

        /**
         * Get connected.
         *
         * @return connected
         */
        public boolean getConnected() {
            return this.connected;
        }

        /**
         * Interrupt the transfer thread.
         */
        public void interruptTransfer() {
            try {
                this.ftp.interrupt();
            } catch (Exception e) {
                // ignore
            }
        }

        /**
         * Cancel the upload thead
         */
        public void cancelTransfer() {
            try {
                this.abortAll = true;
                this.ftp.interrupt();
            } catch (Exception e) {
                // ignore
            }
        }

        public boolean error() {
            return this.error;
        }

        public String getErrorMessage() {
            return this.errorMsg;
        }

        public void run() {
            try {
                ftp.connect();
                this.connected = true;

                if (ftp.getMode() == Ftp.ASCII) {
                    this.ftp.setAscii();
                } else if (ftp.getMode() == Ftp.BINARY) {
                    this.ftp.setBinary();
                } else if (ftp.getMode() == Ftp.AUTO) {
                    this.ftp.setAuto(true);
                }

                Enumeration elements = this.files.elements();
                while (elements.hasMoreElements() && this.abortAll == false) {
                    ftp.reset();
                    ftp.setDir(this.currentRemoteDir);

                    String fileName = (String) elements.nextElement();
                    this.currentFile = new File(fileName);
                    String relativePath = "";
                    relativePath = FileTools.getRelativePath(this.localDir, this.currentFile);

                    if ((relativePath != null) && !relativePath.equals("")) {
                        ftp.makePath(relativePath);
                    } else {
                        relativePath = this.currentRemoteDir;
                    }

                    if (this.currentFile.isFile() == true) {

                        try {
                            ftp.upload(this.extension, this.currentFile);

                            if ((this.currentFile.length() > 0) && performChecksum && parent.isFeatureSupported("XCRC")) {
                                String fileDir = ftp.getDir();

                                if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                                    fileDir = fileDir + "/";
                                }

                                if (!ftp.checksum(this.currentFile, fileDir + this.currentFile.getName())) {
                                    throw new FtpException("Checksum returned false");
                                }
                            }
                        } catch (FtpException fe) {
                            if (ftp.interrupted()) {
                                continue;
                            } else {
                                this.error = true;
                                this.errorMsg = fe.getMessage();

                                for (Iterator i = impl.getListeners().iterator(); i.hasNext(); ) {
                                    FtpListener listener = (FtpListener) i.next();

                                    if (listener instanceof FtpErrorListener) {
                                        ((FtpErrorListener) listener).error(new FtpErrorEvent(this,
                                                                                              this.currentFile.getName(),
                                                                                              ftp.getDir(),
                                                                                              this.currentFile.getPath(),
                                                                                              FtpProgressEvent.UPLOAD));
                                    }
                                }

                                fe.printStackTrace();
                            }
                        }
                    }
                }
            } catch (Exception e) {
                // todo: ignore?
            } finally {
                this.ftp.disconnect();
                this.connected = false;
            }
        }
    }

    /**
     * Uploads a directory.
     *
     * @param currentRemoteDir Remote directory
     * @param performChecksum  whether checksum should be performed, not supported by all FTP servers
     * @param extension        the temporary extension to use when uploading files or <code>null</code> to use default extension
     * @param fileList         File list will be uploaded
     * @param localDir         Local directory
     *
     * @throws FtpException if an FTP related error occurs
     * @throws IOException  if an I/O related error occurs
     */
    private void uploadDir(String currentRemoteDir, boolean performChecksum,
                           String extension, Vector fileList, File localDir)
            throws FtpException, IOException {
        while ((fileList.size() > 0) && !this.impl.interrupted()) {
            setDir(currentRemoteDir);

            String fileName = (String) fileList.elementAt(0);
            File file = new File(fileName);
            String relativePath = "";
            relativePath = FileTools.getRelativePath(localDir, file);

            if ((relativePath != null) && !relativePath.equals("")) {
                makePath(relativePath);
            } else {
                relativePath = currentRemoteDir;
            }

            if (file.isFile()) {
                upload(extension, file);

                if ((file.length() > 0) && performChecksum && isFeatureSupported("XCRC")) {
                    String fileDir = getDir();

                    if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                        fileDir = fileDir + "/";
                    }

                    if (!checksum(file, fileDir + file.getName())) {
                        throw new FtpException("Checksum returned false");
                    }
                }
            }

            fileList.remove(fileName);
        }
    }

    /**
     * Uploads file to FTP server storing remotely using a unique name.
     *
     * @param fileName the filename to upload, filename will be used as seed for unique remote filename
     *
     * @return the unique filename
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized String uploadUnique(String fileName)
            throws FtpException {
        return uploadUnique(fileName, true);
    }

    /**
     * Uploads file to FTP server storing remotely using a unique name.
     *
     * @param fileName          the filename to upload, filename will be used as seed for unique remote filename
     * @param useFilenameAsSeed whether filename should be used as seed value for generating unique filename on server
     *
     * @return the unique filename
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized String uploadUnique(String fileName, boolean useFilenameAsSeed)
            throws FtpException {
        return uploadUnique(new File(fileName), useFilenameAsSeed);
    }


    /**
     * Uploads file to FTP server storing remotely using a unique name.
     *
     * @param file the file to upload, filename will be used as seed for unique remote filename
     *
     * @return the unique file name
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized String uploadUnique(File file)
            throws FtpException {
        return uploadUnique(file, true);
    }

    /**
     * Uploads file to FTP server storing remotely using a unique name.
     *
     * @param file              the file to upload, filename will be used as seed for unique remote filename
     * @param useFilenameAsSeed whether filename should be used as seed value for generating unique filename on server
     *
     * @return the unique file name
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized String uploadUnique(File file, boolean useFilenameAsSeed)
            throws FtpException {
        InputStream in = getInputStream(file);
        if (useFilenameAsSeed) {
            return uploadUnique(in, file.getName());
        } else {
            return uploadUnique(in, null);
        }
    }


    /**
     * Uploads file to FTP server storing remotely using a unique name.
     *
     * @param in       the data <code>InputStream</code> to upload from
     * @param fileName the filename to be used as seed for unique remote filename
     *
     * @return the unique file name
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized String uploadUnique(InputStream in, String fileName)
            throws FtpException {
        this.impl.setTargetFile(fileName);

        try {
            return this.impl.uploadUnique(in, fileName);
        } finally {
            IOTools.close(in);
            this.impl.setTargetFile(null);
        }
    }


    /**
     * Checks if file is current or parrent directory.
     *
     * @param file file to check.
     *
     * @return true if passed file is current or parent directory.
     */
    private static boolean isCurrentOrParentDir(FtpFile file) {
        String name = file.getFilename();

        return (name.equals(CURRENT_DIR) || name.equals(UP_DIR));
    }

    /**
     * Checks if file is current or parrent directory.
     *
     * @param file file to check.
     *
     * @return true if passed file is current or parent directory.
     */
    private static boolean isCurrentOrParentDir(File file) {
        String name = file.getName();

        return (name.equals(CURRENT_DIR) || name.equals(UP_DIR));
    }

    /**
     * Gets remote directory name removing any path information.
     *
     * @param path the remote directory path
     *
     * @return the remote directory name
     */
    private static String getDirectoryName(String path) {
        // trim any spaces
        path = path.trim();

        // check for root path
        if (path.equals("/") || path.equals("\\")) {
            return path;
        }

        // trim trailing delimeter if found
        if (path.endsWith("/") || path.endsWith("\\")) {
            path = path.substring(0, path.length() - 1);
        }

        if (path.indexOf("/") != -1) {
            return path.substring(path.lastIndexOf("/") + 1);
        } else if (path.indexOf("\\") != -1) {
            return path.substring(path.lastIndexOf("\\") + 1);
        } else {
            return path;
        }
    }

    /**
     * Creates new FileInputStream object.
     *
     * @param f source file
     *
     * @return created InputStream
     *
     * @throws FtpException if underlayed FileNotFoundException occurs.
     */
    private static InputStream getInputStream(File f)
            throws FtpException {
        try {
            return Files.newInputStream(f.toPath());
        } catch (Exception e) {
            throw new FtpException(e);
        }
    }

    /**
     * Determines if a directory is empty.
     *
     * @param file The directory which will be analized
     *
     * @return <code>true</code> if the directory is empty; <code>false</code> otherwise
     *
     * @throws FtpException If an error occurs
     */
    private boolean isEmptyDir(FtpFile file)
            throws FtpException {
        boolean empty = true;
        String lastDir = getDir();
        setDir(file.getFilename());

        Enumeration e = getDirListing();

        while (e.hasMoreElements()) {
            FtpFile current = (FtpFile) e.nextElement();

            if (!isCurrentOrParentDir(current)) {
                empty = false;

                break;
            }
        }

        setDir(lastDir);

        return empty;
    }

    /**
     * Creates new FileOuputStream object.
     *
     * @param f source file
     *
     * @return created OutputStream
     *
     * @throws FtpException if underlayed FileNotFoundException occurs.
     */
    private static OutputStream getOutputStream(File f)
            throws FtpException {
        try {
            return new FileOutputStream(f);
        } catch (IOException ex) {
            throw new FtpException(ex.getMessage(), ex);
        }
    }

    /**
     * Gets empty remote directory list.
     *
     * @param remoteDir The directory root which will be analized
     *
     * @return A vector which will contain empty directories
     *
     * @throws FtpException If an error occurs
     */
    private Vector getRemoteEmptyDirList(String remoteDir)
            throws FtpException {
        Vector vector = new Vector();

        Enumeration e = getDirListing();

        if (!remoteDir.endsWith("/") && !remoteDir.endsWith("\\")) {
            remoteDir = remoteDir + "/";
        }

        while (e.hasMoreElements()) {
            FtpFile file = (FtpFile) e.nextElement();

            if (file.isDirectory() && !isCurrentOrParentDir(file)) {
                if (isEmptyDir(file)) {
                    vector.add(remoteDir + file.getFilename());
                } else {
                    String lastDir = getDir();
                    setDir(file.getFilename());
                    vector.addAll(getRemoteEmptyDirList(remoteDir + file.getFilename()));
                    setDir(lastDir);
                }
            }
        }

        return vector;
    }

    /**
     * Get size of all files rescursively in a given remote directory.
     *
     * @param directory - the remote directory name
     *
     * @return size of files.
     */
    public synchronized int getRecursiveDirectoryFileCount(String directory) {
        int count = 0;
        try {
            String oldCurrentDir = this.getDir();
            this.setDir(directory);
            count = this.getRemoteFileList("").size();
            this.setDir(oldCurrentDir);
            return count;
        } catch (FtpException se) {
            return count;
        }
    }


    /**
     * Get size of all files rescursively in a given remote directory.
     *
     * @param directory - the remote directory name
     *
     * @return size of files.
     */
    public synchronized long getRecursiveDirectorySize(String directory) {
        long size = 0L;
        try {
            String oldCurrentDir = this.getDir();
            this.setDir(directory);
            Vector listing = this.getRemoteFileList("");
            for (int i = 0; i < listing.size(); i++) {
                size += this.getFilesize(listing.elementAt(i).toString());
            }
            this.setDir(oldCurrentDir);
            return size;
        } catch (FtpException se) {
            return size;
        }
    }

    /**
     * Gets a remote file list fron the current directory.
     *
     * @param remoteDir The current remote directory
     *
     * @return A collection of file paths.
     *
     * @throws FtpException If a Ftp error occurs
     */
    public synchronized Vector getRemoteFileList(String remoteDir)
            throws FtpException {
        Enumeration e = getDirListing();

        Vector vector = new Vector();

        remoteDir = getDir();

        if (!remoteDir.endsWith("/") && !remoteDir.endsWith("\\")) {
            remoteDir = remoteDir + "/";
        }

        while (e.hasMoreElements()) {
            FtpFile file = (FtpFile) e.nextElement();

            if (file.isDirectory()) {
                if (!isCurrentOrParentDir(file)) {
                    String lastDirectory = getDir();
                    setDir(file.getFilename());
                    vector.addAll(getRemoteFileList(remoteDir));
                    setDir(lastDirectory);
                }
            } else {
                vector.add(remoteDir + file.getFilename());
            }
        }

        return vector;
    }

    /**
     * Get remote file objects.
     *
     * @param remoteDir
     *
     * @return
     *
     * @throws FtpException
     */
    public synchronized Vector<FtpFile> getRemoteFileObjectList(String remoteDir)
            throws FtpException {
        Enumeration e = getDirListing();

        Vector vector = new Vector();

        remoteDir = getDir();

        if (!remoteDir.endsWith("/") && !remoteDir.endsWith("\\")) {
            remoteDir = remoteDir + "/";
        }

        while (e.hasMoreElements()) {
            FtpFile file = (FtpFile) e.nextElement();

            if (file.isDirectory()) {
                if (!isCurrentOrParentDir(file)) {
                    String lastDirectory = getDir();
                    setDir(file.getFilename());
                    vector.addAll(getRemoteFileObjectList(remoteDir));
                    setDir(lastDirectory);
                }
            } else {
                vector.add(file);
            }
        }

        return vector;
    }

    /**
     * Makes a list of remote empty directories.
     *
     * @param emptyDirList Empty directories will be created
     * @param home         The home directory
     *
     * @throws FtpException If an IO error occurs
     */
    private void makeEmptyDir(Vector emptyDirList, File home)
            throws FtpException {
        try {
            Enumeration e = emptyDirList.elements();
            String currentPath = getDir();

            if (!currentPath.endsWith("/") && !currentPath.endsWith("\\")) {
                currentPath = currentPath + "/";
            }

            while (e.hasMoreElements()) {
                String dirName = (String) e.nextElement();
                File file = new File(dirName);

                String relative = FileTools.getRelativePath(home, file, false);
                makePath(relative);
                setDir(currentPath);
            }
        } catch (IOException exception) {
            throw new FtpException(exception.getMessage());
        }
    }

    /**
     * Makes a list of local empty directories.
     *
     * @param emptyDirs Directories that will be created locally
     * @param localDir  The local dir home
     *
     * @throws FtpException If  IO error ocurrs
     */
    private void makeEmptyLocalDirs(Vector emptyDirs, File localDir)
            throws FtpException {
        Enumeration e = emptyDirs.elements();

        while (e.hasMoreElements()) {
            String dirName = (String) e.nextElement();

            File directory = new File(dirName);

            String relative = "";

            try {
                relative = FileTools.getRelativePath(localDir, directory, false);
            } catch (IOException ex) {
                throw new FtpException(ex.getMessage());
            }

            if ((relative != null) && !relative.equals("")) {
                makeLocalDir(relative);
            }
        }
    }

    /**
     * Creates a path of directories recursively.
     *
     * @param path The directory path will be created
     *
     * @throws FtpException If a IO exception occurs
     */
    private void makePath(String path)
            throws FtpException {
        File current = new File(path);

        if (current.getParent() != null) {
            makePath(current.getParent());
        }

        String name = current.getName();

        try {
            if ((name != null) && !name.equals("")) {
                makeDir(name);
            }
        } catch (FtpException exception) {
            // todo: ignore?
        }

        setDir(name);
    }

    /**
     * Returns local checksum.
     *
     * @param in input stream
     *
     * @return checksum
     *
     * @throws FtpException if an error occurs
     */
    private String getLocalChecksum(InputStream in)
            throws FtpException {
        try {
            byte[] hash = Checksum.CRC32.of(in, Long.MAX_VALUE);
            return HexFormat.formatData(hash, "");
        } catch (Exception e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * Returns local checksum.
     *
     * @param localFile the file to use in calculating checksum
     *
     * @return local file checksum
     *
     * @throws FtpException if an error occurs
     */
    public String getLocalChecksum(File localFile)
            throws FtpException {
        try (InputStream in = Files.newInputStream(localFile.toPath())) {
            return getLocalChecksum(in);
        } catch (IOException e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * Returns remote file checksum.
     *
     * @param remoteFile remote file
     *
     * @return remote file checksum.
     *
     * @throws FtpException if an error occurs
     */
    public String getRemoteFileChecksum(String remoteFile)
            throws FtpException {
        if (isFeatureSupported("XCRC")) {
            String command = "XCRC " + remoteFile;
            String response = "";

            try {
                response = issueCommandCheck(command);
            } catch (FtpException e) {
                int index = command.indexOf(" ");
                String fileName = command.substring(index + 1, command.length());

                if (fileName.indexOf(" ") != -1) {
                    command = command.substring(0, index + 1) + "\""
                            + command.substring(index + 1, command.length()) + "\"";

                    response = issueCommandCheck(command);
                } else {
                    throw e;
                }
            }

            StringTokenizer st = new StringTokenizer(response);
            st.nextToken();

            return st.nextToken();
        } else {
            throw new FtpException("Command XCRC is not supported");
        }
    }

    /**
     * Compares checksum of the local data and remote file.
     *
     * @param in         input stream
     * @param remoteFile remote file
     *
     * @return <code>true</code> if checksum are equal, <code>false</code> otherwise
     *
     * @throws FtpException if an error occurs
     */
    private boolean sameChecksum(InputStream in, String remoteFile)
            throws FtpException {
        try {
            String localSum = getLocalChecksum(in);
            String remotesSum = getRemoteFileChecksum(remoteFile);

            return localSum.equalsIgnoreCase(remotesSum);
        } catch (Exception e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * Creates necessary parent directory structure if file does not exist.
     *
     * @param file the file to create parent directory structure for
     */
    private void createDirectoryStructure(File file) {
        String parentPath = file.getParent();
        File parent = new File(parentPath);

        if (!parent.exists()) {
            createDirectoryStructure(parent);
            parent.mkdir();
        }
    }

    /**
     * Initializes local directory path.
     */
    private void initLocalDir() {
        synchronized (this) {
            File curDir = new File(CURRENT_DIR);
            String path = CURRENT_DIR;

            try {
                path = curDir.getCanonicalPath().replace('\\', '/');
            } catch (IOException ex) {
                // todo: ignore?
            }

            this.localDir = new File(path);
        }
    }

    private Matcher createRegex(String filter)
            throws FtpException {
        try {
            return StringTools.isValidString(filter)
                   ? REGEX_FACTORY.matcherFor(filter)
                   : Matcher.POSITIVE;
        } catch (Exception e) {
            throw new FtpException(e.getMessage(), e);
        }
    }
}
