/*
 * @(#)Ftps.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.ftps;

import com.jscape.inet.ftp.*;
import com.jscape.inet.util.ConnectionParameters;
import com.jscape.inet.util.ServerSocketUtil;
import com.jscape.util.*;
import com.jscape.util.log.DebugStreamLogger;
import com.jscape.util.regex.JDKMatcherFactory;
import com.jscape.util.regex.Matcher;
import com.jscape.util.regex.MatcherFactory;

import javax.net.ssl.HandshakeCompletedListener;
import javax.net.ssl.SSLContext;
import java.io.*;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.security.KeyStore;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.Level;
import java.util.zip.DeflaterOutputStream;
import java.util.zip.InflaterInputStream;

/**
 * Implements the basic functionality of a FTPS (FTP over SSL) client.<P>
 * <p/>
 * Example Usage:<P>
 * <p/>
 * <pre>
 * // create new Ftps instance
 * Ftps ftps = new Ftps("ftp.myserver.com","jsmith","secret");
 * try {
 * // establish connection
 * ftps.connect();
 *
 * // print out directory listing
 * System.out.println(ftps.getDirListingAsString());
 *
 * // disconnect from FTPS server
 * ftps.disconnect();
 * } catch(Exception e) {
 * System.out.println(e);
 * }
 * </pre>
 */
@SuppressWarnings("unchecked")
public class Ftps
        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 = "..";

    /**
     * Connection type for implicit SSL session.
     */
    public static final ConnectionStrategy IMPLICIT_SSL = new ImplicitSslStrategy();

    /**
     * Connection type for AUTH TLS FTP session (default).
     */
    public static final ConnectionStrategy AUTH_TLS = new AuthTlsStrategy();

    /**
     * Connection type for AUTH SSL FTP session.
     */
    public static final ConnectionStrategy AUTH_SSL = new AuthSslStrategy();
    private static final int DEFAULT_BLOCK_SIZE = 8192;
    private static final FtpFileParser MACHINE_PARSER = new MLSDParser();

    private static final MatcherFactory REGEX_FACTORY = new JDKMatcherFactory();

    private static final int DEFAULT_PROXY_PORT = 1080;
    private static final String DEFAULT_PROXY_TYPE = "SOCKS5";

    private ContextFactory ctxFactory = new ContextFactory();
    private String encodingDisk = null;
    private String encodingWire = null;
    private String portAddress = null;
    private ConnectionStrategy connectionType = AUTH_TLS;
    private File localDir = null;
    private FtpFileParser fileParser = null;
    private FtpsClient client = null;
    private LinkedList listeners = new LinkedList();
    private FtpsCertificateVerifier certificateListener = null;
    private DebugStreamLogger logger = DebugStreamLogger.systemOutLogger(Level.OFF);
    private SSLContext context = null;
    private String encoding = StandardCharsets.US_ASCII.name();
    private String host = null;
    private String password = null;
    private String account = null;
    private String path;
    private String user = null;
    private TransferMode mode = new BinaryMode();
    private String[] enabledCiphers;
    private volatile boolean interrupted = false;
    private boolean passive = true;
    private boolean shutdownCCC = true;
    private int blockSize = DEFAULT_BLOCK_SIZE;
    private int dataPort = FtpsClient.DEFAULT_DATA_PORT;
    private ServerSocketUtil.PortRange dataPortRange = new ServerSocketUtil.PortRange(FtpsClient.DEFAULT_DATA_PORT, FtpsClient.DEFAULT_DATA_PORT);
    private int linger = 0;
    private int modeCode;
    private int port = FtpsClient.DEFAULT_PORT;
    private int timeout = 30000;
    private boolean tcpNoDelay = true;
    private boolean errorOnSizeCommand = false;

    private boolean compression = false;
    private boolean isModeZSupported = false;
    private DeflaterOutputStream deflaterOut = null;
    private InflaterInputStream inflaterIn = null;


    private String proxyUsername = null;
    private String proxyPassword = null;

    private String proxyHostname = null;
    private int proxyPort = DEFAULT_PROXY_PORT;
    private String proxyType = DEFAULT_PROXY_TYPE;
    private int sendBufferSize = -1;
    private int receiveBufferSize = -1;
    private Vector serverFeatures = null;
    private boolean serverFeaturesCommandSuccussful;
    private boolean useEPSV = false;
    private boolean useEPRT = false;
    private boolean useAutoDetectIpv6 = false;
    private boolean preserveUploadTimestamp = false;
    private boolean preserveDownloadTimestamp = false;
    private TimeZone timeZone = TimeZone.getTimeZone("GMT");  // default timezone 
    private File targetFile = null;
    private HandshakeCompletedListener sslHandshakeCompletedListener;

    // CONSTRUCTORS

    /**
     * Creates a new Ftps instance.
     */
    public Ftps() {
        this("", "", "");
    }

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

    /**
     * Creates a new Ftps 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 of FTP user or <code>null</code> if not needed
     */
    public Ftps(String host, String user, String pwd, String account) {
        this(host, user, pwd, FtpsClient.DEFAULT_PORT);
        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 Ftps(String host, String user, String pwd, int port) {
        this(host, port, user, pwd, new File(CURRENT_DIR));
    }

    /**
     * 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
     */
    public Ftps(String host, String user, String pwd, File localDirectory) {
        this(host, FtpsClient.DEFAULT_PORT, user, pwd, localDirectory);
    }

    /**
     * Creates a new Ftp instance.
     *
     * @param host           the hostname of FTP server
     * @param port           the port 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
     */
    private Ftps(String host, int port, String user, String pwd,
                 File localDirectory) {
        
        this.host = host;
        this.port = port;
        this.user = user;
        this.password = pwd;
        this.localDir = localDirectory;
    }

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

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

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

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

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

    /**
     * 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.preserveUploadTimestamp = value;
    }

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

    /**
     * Set auto detect Ipv6 detection mode.
     *
     * @param value
     */
    public synchronized void setAutoDetectIpv6(boolean value) {
        this.useAutoDetectIpv6 = value;
        if (this.client != null) {
            this.client.setAutoDetectIpv6(value);
        }
    }

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

    /**
     * Get proxy username.
     *
     * @return username
     */
    public synchronized String getProxyUsername() {
        return this.proxyUsername;
    }

    /**
     * Get proxy password.
     *
     * @return password
     */
    public synchronized String getProxyPassword() {
        return this.proxyPassword;
    }

    /**
     * Get proxy hostname.
     *
     * @return proxy hostname
     */
    public synchronized String getProxyHostname() {
        return this.proxyHostname;
    }

    /**
     * Get proxy port.
     *
     * @return port.
     */
    public synchronized int getProxyPort() {
        return this.proxyPort;
    }

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

    /**
     * Sets the proxy type will be used for this FTPS connection.
     *
     * @param proxyType The proxy type. Valid values: HTTP, SOCKS5
     */
    public synchronized void setProxyType(String proxyType) {
        this.proxyType = proxyType;
    }

    /**
     * Get proxy type.
     *
     * @return proxy
     */
    public synchronized String getProxyType() {
        return this.proxyType;
    }

    /**
     * Clears proxy server values.
     */
    public synchronized void clearProxySettings() {
        setProxyAuthentication(null, null);
        setProxyHost(null, DEFAULT_PROXY_PORT);
        setProxyType(DEFAULT_PROXY_TYPE);
    }


    /**
     * 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 {
        ensureConnected();
        setTransferMode(Ftps.ASCII);
        this.mode = new TextMode();
    }

    /**
     * Sets data transfer mode to automatic detect.
     * 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
     * <br>
     * 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
     *
     * @throws FtpException if an I/O or Ftp related error occurs
     */
    public synchronized void setAuto(boolean auto)
            throws FtpException {
        if (auto) {
            this.mode = new AutoMode();
        } else {
            setBinary();
        }
    }

    // TRANSFER MODE CONTROL

    /**
     * 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 {
        ensureConnected();
        setTransferMode(Ftps.BINARY);
        this.mode = new BinaryMode();
    }

    /**
     * Sets block size for use in transferring files.
     * Default block size is 8192 bytes.
     *
     * @param blockSize the block size in bytes
     */
    public synchronized void setBlockTransferSize(int blockSize) {
        Assert.greater(blockSize, 0, "bad block size: " + blockSize);
        this.blockSize = blockSize;
    }

    // TRANSFER BLOCK SIZE CONTROL

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

    /**
     * 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.encoding = encoding;
    }

    // ENCODING CONTROL

    /**
     * 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.encoding;
    }

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

    /**
     * Gets whether the data is compressed during the transfering. The data will be compressed
     * if the Ftps 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.compression;
    }

    /**
     * Sets optional client certificate to be used during authentication.
     *
     * @param keystore the JKS keystore containing certificates
     * @param password the password for opening JKS keystore
     *
     * @throws FtpException if an error occurs opening keystore
     */
    public void setClientCertificates(KeyStore keystore, String password)
            throws FtpException {
        this.ctxFactory.setKeys(keystore, password);
        this.context = null;
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the client certificate
     * @param password the keystore password
     *
     * @throws FtpException if an I/O error occurs
     */
    public void setClientCertificates(String filename, String password)
            throws FtpException {
        this.ctxFactory.setKeys(filename, password);
        this.context = null;
    }

    /**
     * Sets optional client certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the client certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     *
     * @throws FtpException if an I/O error occurs
     */
    public void setClientCertificates(String filename, String password,
                                      String storetype)
            throws FtpException {
        this.ctxFactory.setStoreType(storetype);
        setClientCertificates(filename, password);
    }

    /**
     * Gets whether connection will be established before sending data transfer command.
     *
     * @param value <code>true</code> if should connect before sending data transfer command, <code>false</code> otherwise
     *              send the command in passive mode
     */
    public synchronized void setConnectBeforeCommand(boolean value) {
        this.client.setConnectBeforeCommand(value);
    }

    /**
     * Gets whether connection will be established before sending data transfer command.
     *
     * @return <code>true</code> if should connect before sending data transfer command, <code>false</code> otherwise
     */
    public synchronized boolean getConnectBeforeCommand() {
        return this.client.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() {
        boolean open = ((this.client != null) && !this.client.isClosed());
        boolean available = false;

        if (open) {
            try {
                noop();
                available = true;
            } catch (FtpException e) {
                if (e.getMessage().startsWith("5")) {
                    available = true;
                }
            }
        }

        if (open && !available) {
            fireEvent(new FtpConnectionLostEvent(this, this.host));
        }

        return (open && available);
    }

    /**
     * Sets the connection type.
     * Default connection type is <code>Ftps.AUTH_TLS</code>.
     *
     * @param connectionType the current connection type
     */
    public synchronized void setConnectionType(ConnectionStrategy connectionType) {
        Assert.notNull(connectionType);
        this.connectionType = connectionType;
    }

    /**
     * Returns the current connection type.
     * Default connection type is <code>Ftps.AUTH_TLS</code>.
     *
     * @return the current connection type
     */
    public synchronized ConnectionStrategy getConnectionType() {
        return this.connectionType;
    }

    /**
     * Sets the current SSL context.
     *
     * @param context the new SSL context
     *
     * @see SSLContext
     */
    public synchronized void setContext(SSLContext context) {
        ensureNotConnected();
        this.context = context;
    }

    /**
     * Get listeners.
     *
     * @return list of listeners.
     */
    public synchronized LinkedList getListeners() {
        return this.listeners;
    }

    /**
     * Set the list of listeners.
     *
     * @param listeners
     */
    public synchronized void setListeners(LinkedList listeners) {
        this.listeners = listeners;
    }

    /**
     * Sets TLS/SSL protocol name.
     *
     * @param value a protocol name
     */
    public void setProtocol(String value) {
        this.ctxFactory.setProtocol(value);
    }

    /**
     * Sets provider used for keystore in default context.
     *
     * @param provider The provider
     */
    public synchronized void setContextKeystoreProvider(String provider) {
        this.ctxFactory.setKeystoreProvider(provider);
    }

    /**
     * Sets provider used for TLS protocol in default context.
     *
     * @param provider the provider
     */
    public synchronized void setContextProtocolProvider(String provider) {
        this.ctxFactory.setProtocolProvider(provider);
    }

    /**
     * Sets provider used for X509 algorithm in default context.
     *
     * @param provider the provider
     */
    public synchronized void setContextAlgorithmProvider(String provider) {
        this.ctxFactory.setAlgorithmProvider(provider);
    }

    /**
     * Gets provider used for X509 algorithm in default context.
     *
     * @return algorithm provider
     */
    public synchronized String getContextAlgorithmProvider() {
        return this.ctxFactory.getAlgorithmProvider();
    }

    /**
     * Gets keystore provider used for  default context.
     * Default is <code>null</code>.
     *
     * @return keystore provider
     */
    public synchronized String getContextKeystoreProvider() {
        return this.ctxFactory.getKeystoreProvider();
    }

    /**
     * Gets provider used for TLS protocol in default context.
     * Default is <code>null</code>.
     *
     * @return protocol provider
     */
    public synchronized String getContextProtocolProvider() {
        return this.ctxFactory.getProtocolProvider();
    }

    /**
     * Sets algorithm used for SSL communications in default context.
     * Default is SunX509.
     *
     * @param algorithm the algorithm
     */
    public synchronized void setContextAlgorithm(String algorithm) {
        this.ctxFactory.setAlgorithm(algorithm);
    }

    /**
     * Gets algorithm used for SSL communications in default context.
     * Default is SunX509.
     *
     * @return algorithm
     */
    public synchronized String getContextAlgorithm() {
        return this.ctxFactory.getAlgorithm();
    }

    /**
     * Sets protocol used for SSL communications in default context.
     * Default is TLS.
     *
     * @param protocol the protocol
     */
    public synchronized void setContextProtocol(String protocol) {
        this.ctxFactory.setProtocol(protocol);
    }

    /**
     * Gets protocol used for SSL communications in default context.
     * Default is TLS.
     *
     * @return protocol
     */
    public synchronized String getContextProtocol() {
        return this.ctxFactory.getProtocol();
    }

    /**
     * Obtains the current SSL context.
     *
     * @return the current SSL context
     *
     * @see SSLContext
     */
    public synchronized SSLContext getContext() {
        return this.context;
    }

    /**
     * Sets data port for incoming active PORT connections.
     *
     * @param dataPort active port
     *
     * @deprecated Replaced by setDataPortRange
     */
    public synchronized void setDataPort(int dataPort) {
        this.dataPort = dataPort;
        this.dataPortRange = new ServerSocketUtil.PortRange(dataPort, dataPort);
    }

    /**
     * Returns current port for incoming active PORT data connections.
     *
     * @return active port
     *
     * @deprecated Replaced by getDataPortStart and getDataPortEnd
     */
    public synchronized int getDataPort() {
        return this.dataPort;
    }

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

    /**
     * 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.setDataPortEnd(dataPortEnd);
        this.setDataPortStart(dataPortStart);
    }

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

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

    // SETUP DEBUG

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

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

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

    /**
     * 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 {
        Assert.notNull(remoteDirectory);
        ensureConnected();
        this.client.changeWorkingDirectory(remoteDirectory);
        this.path = getDir();
        fireEvent(new FtpChangeDirEvent(this, path));
    }

    // DIRECTORY CONTROL

    /**
     * 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 {
        ensureConnected();

        String path = this.client.printWorkingDirectory();

        try {
            return path.substring(path.indexOf("\"") + 1, path
                    .lastIndexOf("\""));
        } catch (Exception e) {
            throw new FtpException("Unable to retrieve remote directory path",
                                   e);
        }
    }

    /**
     * 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 regular expression filename filter
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code> objects
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see com.jscape.inet.ftp.FtpFile
     * @see #getDirListingAsString(String)
     * @see #getDirListingRegex(String)
     * @see #getNameListing
     * @see com.jscape.inet.ftp.FtpFileParser
     * @see com.jscape.inet.ftp.UnixParser
     * @see com.jscape.inet.ftp.NTParser
     * @see com.jscape.inet.ftp.VMSParser
     */
    public synchronized Enumeration getDirListing(String filter)
            throws FtpException {
        StringReader in = new StringReader(getDirListingAsString(filter));
        BufferedReader reader = new BufferedReader(in);
        Vector files = new Vector();

        try {
            for (Enumeration e = this.fileParser.parse(reader); e.hasMoreElements(); ) {
                files.addElement(e.nextElement());
            }

            fireEvent(new FtpListingEvent(this, files.elements()));

            return files.elements();
        } catch (IOException e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * 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 com.jscape.inet.ftp.FtpFileParser
     * @see com.jscape.inet.ftp.UnixParser
     * @see com.jscape.inet.ftp.NTParser
     * @see com.jscape.inet.ftp.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 a regular expression filename filter
     *
     * @return a directory listing
     *
     * @throws FtpException if an I/O or FTP error occurs.
     * @see #getDirListingRegex(String)
     */
    public synchronized String getDirListingAsString(String filter)
            throws FtpException {
        Assert.notNull(filter);
        ensureConnected();
        prepareDataConnection();

        Socket data = this.client.list(filter);
        Vector files = readFileList(data);
        StringBuffer buffer = new StringBuffer();

        for (int i = 0; i < files.size(); ++i) {
            buffer.append(files.elementAt(i)).append(FtpsClient.EOL);
        }

        return buffer.toString();
    }

    /**
     * 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 listing = new Vector();

        for (Enumeration e = getDirListing(); e.hasMoreElements(); ) {
            FtpFile file = (FtpFile) e.nextElement();

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

        return listing.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();
    }

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

    /**
     * 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 {
        ensureConnected();
        this.client.changeToParentDirectory();
        this.path = getDir();
        fireEvent(new FtpChangeDirEvent(this, path));
    }

    /**
     * Sets character set used when reading data from disk for ASCII file transfer.
     *
     * @param encoding the character encoding to be used
     */
    public void setDiskEncoding(String encoding) {
        this.encodingDisk = encoding;
    }

    /**
     * Gets character encoding used when reading data from disk for ASCII file transfer.
     *
     * @return the character encoding to be used
     */
    public String getDiskEncoding() {
        return this.encodingDisk;
    }

    /**
     * Sets user-defined enabled SSL cipher suites.
     *
     * @param enabledCiphers enabled SSL cipher suites or <code>null</code> for using
     *                       system-default cipher suites set
     */
    public void setEnabledCiphers(String[] enabledCiphers) {
        this.enabledCiphers = enabledCiphers;
    }

    /**
     * Returns user-defined enabled SSL cipher suites.
     *
     * @return user-defined enabled SSL cipher suites or <code>null</code>
     *         if system-default cipher suites are used
     */
    public String[] getEnabledCiphers() {
        return this.enabledCiphers;
    }

    /**
     * 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 synchronized void setErrorOnSizeCommand(boolean error) {
        this.errorOnSizeCommand = 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
     *         an exception when it caused an error, <code>false</code> the error will be ignored.
     */
    public synchronized boolean getErrorOnSizeCommand() {
        return this.errorOnSizeCommand;
    }

    /**
     * Looks for the command argument into the server features list to validate if it is supported
     *
     * @param command The feature will be searched
     *
     * @return <code>true</code> if the command is found into the server feature list;
     *         <code>false</code> otherwise
     */
    public boolean isFeatureSupported(String command) {
        command = command.trim();
        for (int i = 0; i < this.serverFeatures.size(); i++) {
            String currentFeature = this.serverFeatures.get(i).toString().trim();
            if (currentFeature.startsWith(command)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns supported server features.
     *
     * @return an <code>Enumeration</code> of features strings
     *
     * @throws FtpException if an error occurs
     */
    public Enumeration getFeatures()
            throws FtpException {
        FeatRequest request = new FeatRequest();
        request.execute(new FtpsProtocolInterpreter(this.client));

        return new Vector(java.util.Arrays.asList(request.getFeatures())).elements();
    }

    /**
     * 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 File will be modified its modification time.
     * @param date       The new modification time
     *
     * @throws FtpException If an error occurs
     */
    public synchronized void setFileModificationTime(String remoteName, Date date)
            throws FtpException {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        format.setTimeZone(getTimeZone());
        String dateString = format.format(date);
        issueCommandCheck("MFMT " + dateString + " " + remoteName);
    }

    /**
     * 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 File will be modified its creation time.
     * @param date       The new creation time
     *
     * @throws FtpException If an error occurs.
     */
    public synchronized void setFileCreationTime(String remoteName, Date date)
            throws FtpException {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        String dateString = format.format(date);
        issueCommandCheck("MFCT " + dateString + " " + remoteName);
    }

    /**
     * Sets 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 remoteName the remote file
     * @param dateFile   the date/timestamp of remote file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setFileTimestamp(String remoteName, Date dateFile)
            throws FtpException {
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        format.setTimeZone(this.getTimeZone());

        issueCommandCheck("MDTM " + format.format(dateFile) + " " + remoteName);
    }

    /**
     * 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 {
        Assert.notNull(remoteFile);
        ensureConnected();

        String response = issueCommandCheck("MDTM " + remoteFile);
        StringTokenizer st = new StringTokenizer(response);

        try {
            st.nextToken(); // skip

            SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
            format.setTimeZone(this.getTimeZone());

            return format.parse(st.nextToken());
        } catch (Exception e) {
            throw new FtpException(response);
        }
    }

    // FILE CONTROL

    /**
     * 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 {
        Assert.notNull(remoteFile);
        ensureConnected();

        String response = issueCommandCheck("SIZE " + remoteFile);
        StringTokenizer st = new StringTokenizer(response);

        try {
            st.nextToken(); // skip

            return Long.parseLong(st.nextToken());
        } catch (Exception e) {
            throw new FtpException(response);
        }
    }

    /**
     * Sets the <code>FtpFileParser</code> used to parse directory listing returned when
     * invoking <code>Ftps#getDirListing</code> method. If <code>FtpFileParser</code> is <code>null</code> upon
     * establishing connection the <code>Ftps</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 <code>FtpFileParser</code> instance
     *
     * @see com.jscape.inet.ftp.FtpFileParser
     * @see com.jscape.inet.ftp.UnixParser
     * @see com.jscape.inet.ftp.NTParser
     * @see com.jscape.inet.ftp.VMSParser
     */
    public synchronized void setFtpFileParser(FtpFileParser parser) {
        Assert.notNull(parser);
        this.fileParser = parser;
    }

    // FILE PARSER CONTROL

    /**
     * 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>Ftps</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 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 <code>FtpFileParser</code> or <code>null</code> if no <code>FtpFileParser</code> has been defined.
     *
     * @see com.jscape.inet.ftp.FtpFileParser
     * @see com.jscape.inet.ftp.UnixParser
     * @see com.jscape.inet.ftp.NTParser
     * @see com.jscape.inet.ftp.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) {
        Assert.notNull(host);
        ensureNotConnected();

        this.host = host;
    }

    /**
     * Sets the hostname of FTP Server.
     *
     * @param host
     * @param ensureConnected
     */
    public synchronized void setHostname(String host, boolean ensureConnected) {
        Assert.notNull(host);

        if (ensureConnected == true) {
            ensureNotConnected();
        }

        this.host = host;
    }

    // SETUP CONNECTION PARAMETERS

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

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

            if (offset > 0) {
                this.client.restart(Long.toString(offset));
            }

            Socket data = this.client.retrieve(remoteFile);

            return new DataConnectionInputStream(this, data);
        } catch (FtpException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * Sets the linger time for closing data sockets.
     *
     * @param linger the linger value in milliseconds
     */
    public synchronized void setLinger(int linger) {
        Assert.greaterOrEqual(linger, 0, "bad linger value: " + linger);
        ensureNotConnected();

        this.linger = linger;
    }

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

    /**
     * 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) {
        Assert.notNull(localDirectory);
        Assert.isTrue(localDirectory.exists(), "bad directory: "
                + localDirectory);
        Assert.isTrue(localDirectory.isDirectory(), "bad directory: "
                + localDirectory);

        this.localDir = localDirectory;
    }

    // LOCAL DIRECTORY OPERATION

    /**
     * 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 RFC-959 compliant so may not be supported by all FTP servers.
     *
     * @param dirname directory name or an empty <code>String</code> for current directory
     * @param filter  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
     * @see FtpFile
     */
    public Enumeration getMachineDirListing(String dirname, String filter)
            throws FtpException {
        Socket data = null;
        BufferedReader reader = null;
        boolean errorOccurred = false;

        Matcher regex = createRegex(filter);

        ensureConnected();
        prepareDataConnection();

        try {
            data = this.client.mlsd(dirname);
            reader = new BufferedReader(getInputStreamReader(getSocketInputStream(data)));

            Vector files = new Vector();

            for (Enumeration e = MACHINE_PARSER.parse(reader); e.hasMoreElements(); ) {
                FtpFile file = (FtpFile) e.nextElement();

                if (regex.matches(file.getFilename())) {
                    files.addElement(file);
                }
            }

            fireEvent(new FtpListingEvent(this, files.elements()));

            return files.elements();
        } catch (Exception e) {
            errorOccurred = true;
            throw new FtpException(e.getMessage(), e);
        } finally {
            if (isCompressionAllowed() && this.inflaterIn != null) {
                closeInflaterStream();
            }

            IOTools.close(reader);
            IOTools.close(data);
            readLastResponse(errorOccurred);
        }
    }

    /**
     * Gets directory listing using MLSD command.
     * This command is not RFC-959 compliant so may not be supported by all FTP servers.
     *
     * @param dirname the directory name or an empty <code>String</code> to use current directory
     *
     * @return an <code>Enumeration</code> of <code>FtpFile</code>
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public Enumeration getMachineDirListing(String dirname)
            throws FtpException {
        return getMachineDirListing(dirname, null);
    }

    /**
     * Gets the file description for the specified filename using MLST command.
     * This command is not RFC-959 compliant so may not be supported by all FTP servers.
     *
     * @param filename target filename or an empty <code>String</code> for current diectory
     *
     * @return an <code>FtpFile</code>
     *
     * @throws FtpException if an error occurs
     * @see FtpFile
     */
    public FtpFile getMachineFileListing(String filename)
            throws FtpException {
        MLSTRequest request = new MLSTRequest();
        request.setFilename(filename);
        request.execute(new FtpsProtocolInterpreter(this.client));

        return request.getFile();
    }

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

    /**
     * Gets directory listing of files on FTP server matching filter criteria
     * and using NLST command.
     * Filter is any expression supported by server.
     * For example *.txt may return all files containing a .txt filename extension.
     *
     * @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 {
        Assert.notNull(filter);
        ensureConnected();
        prepareDataConnection();

        Socket data = this.client.nameList(filter);

        return readFileList(data).elements();
    }

    /**
     * Gets directory listing of files on FTP server using NLST command.
     *
     * @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 <code>OutputStream</code>
     *
     * @throws IOException if an FTP error occurs.
     */
    public synchronized OutputStream getOutputStream(String fileName,
                                                     long off,
                                                     boolean append)
            throws IOException {
        try {
            prepareDataConnection();

            Socket data = append
                          ? this.client.append(fileName)
                          : this.client.store(fileName);

            return new DataConnectionOutputStream(this, data);
        } catch (FtpException e) {
            throw new IOException(e.getMessage());
        }
    }

    /**
     * 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 IP address of NAT server
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see #setDataPortRange(int, int)
     * @see #setPortAddress(String)
     */
    public void setNATAddress(String ipAddress)
            throws FtpException {
        if (client == null) {
            throw new FtpException("Must establish connection to FTP server before setting NAT address.");
        }
        this.client.setNATAddress(ipAddress);
    }

    /**
     * Get the NAT address.
     *
     * @return address
     */
    public String getNATAddress()
            throws FtpException {
        if (client == null) {
            throw new FtpException("Must establish connection to FTP server before setting NAT address.");
        }
        return this.client.getNATAddress();
    }

    public HandshakeCompletedListener getSslHandshakeCompletedListener() {
        return this.sslHandshakeCompletedListener;
    }

    public void setSslHandshakeCompletedListener(HandshakeCompletedListener value) {
        this.sslHandshakeCompletedListener = value;
        if (this.client != null) {
            this.client.setSslHandshakeCompletedListener(value);
        }
    }

    /**
     * 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.passive = 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.passive;
    }

    /**
     * Sets password of FTP user.
     *
     * @param password the password of FTP user
     */
    public synchronized void setPassword(String password) {
        Assert.notNull(password);
        ensureNotConnected();

        this.password = password;
    }

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

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

        this.port = port;
    }

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

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

        if (this.client != null) {
            this.client.setPortAddress(this.portAddress);
        }
    }

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

    /**
     * Changes the protection level.
     *
     * @param level a protection level of P (private) or C (Clear)
     *
     * @throws FtpException if a FTP or I/O related error occurs
     */
    public void setProtectionLevel(char level)
            throws FtpException {
        this.client.protectionLevel(level);
    }

    /**
     * Gets response code from last FTP command.
     *
     * @return response code or -1 if code cannot be determined
     */
    public synchronized int getResponseCode() {
        try {
            return this.client.getLastResponse().getCode();
        } catch (Exception e) {
            return -1;
        }
    }

    /**
     * Sets the send buffer size option of the ftps connection.
     *
     * @param sendBufferSize Send buffer size.
     */
    public synchronized void setSendBufferSize(int sendBufferSize) {
        this.sendBufferSize = sendBufferSize;
        if (this.client != null) {
            this.client.setSendBufferSize(sendBufferSize);
        }
    }

    /**
     * Sets the receive buffer size option of the ftps connection.
     *
     * @param receiveBufferSize Receive buffer size.
     */
    public synchronized void setReceiveBufferSize(int receiveBufferSize) {
        this.receiveBufferSize = receiveBufferSize;
        if (this.client != null) {
            this.client.setReceiveBufferSize(receiveBufferSize);
        }
    }

    /**
     * Sets optional server certificate to be used during authentication.
     *
     * @param keystore the keystore containing server certificate
     *
     * @throws FtpException if an error occurs reading keystore
     */
    public void setServerCertificates(KeyStore keystore)
            throws FtpException {
        this.ctxFactory.setCertificates(keystore);
        this.context = null;
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename the keystore file containing the server certificate
     * @param password the keystore password
     *
     * @throws FtpException if an I/O error occurs
     */
    public void setServerCertificates(String filename, String password)
            throws FtpException {
        this.ctxFactory.setCertificates(filename, password);
        this.context = null;
    }

    /**
     * Sets optional server certificate to be used during authentication.
     * Default expected keystore format is JKS format. Existing certificate may be imported into keystore using
     * the <code>keytool</code> utility provided in the Sun JDK.
     * <p/>
     * Example:
     * <p/>
     * <pre>
     * keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * The example above imports the certificate x509.cer into a keystore named your_store_name
     * <p/>
     *
     * @param filename  the keystore file containing the server certificate
     * @param password  the keystore password
     * @param storetype the keystore type valid values include jks and pkcs12
     *
     * @throws FtpException if an I/O error occurs
     */
    public void setServerCertificates(String filename, String password,
                                      String storetype)
            throws FtpException {
        this.ctxFactory.setStoreType(storetype);
        setServerCertificates(filename, password);
    }

    /**
     * Sets whether SSL connection should be shutdown after successfully issuing
     * CCC command.  Default is <code>true</code>.
     *
     * @param shutdownCCC <code>true</code> to shutdown, <code>false</code> otherwise
     */
    public void setShutdownCCC(boolean shutdownCCC) {
        this.shutdownCCC = shutdownCCC;

        if (this.client != null) {
            this.client.setShutdownCCC(shutdownCCC);
        }
    }

    /**
     * Gets whether SSL connection should be shutdown after successfully issuing
     * CCC command.  Default is <code>true</code>.
     *
     * @return <code>true</code> to shutdown, <code>false</code> otherwise
     */
    public boolean getShutdownCCC() {
        return this.shutdownCCC;
    }

    // OTHER OPERATIONS

    /**
     * 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 {
        ensureConnected();

        return this.client.system();
    }

    /**
     * Sets the timeout for opening connection to FTP server.
     *
     * @param timeout the timeout in milliseconds
     */
    public synchronized void setTimeout(int timeout) {
        Assert.greaterOrEqual(timeout, 0, "bad timeout value: " + timeout);
        ensureNotConnected();
        this.timeout = timeout;
    }

    /**
     * Gets the timeout for opening connection to FTP server.
     *
     * @return timeout in milliseconds
     */
    public synchronized int getTimeout() {
        return this.timeout;
    }
    
    public boolean getTcpNoDelay() {
        return this.tcpNoDelay;
    }
    
    public void setTcpNoDelay(boolean value) {
        this.tcpNoDelay = value;
    }
    
    /**
     * Sets username of FTP user.
     *
     * @param user the username of FTP user
     */
    public synchronized void setUsername(String user) {
        Assert.notNull(user);
        ensureNotConnected();

        this.user = user;
    }

    /**
     * Gets username of FTP user.
     *
     * @return username
     */
    public synchronized String getUsername() {
        return this.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.useEPSV = useEPSV;
        if (this.client != null) {
            this.client.setUseEPSV(useEPSV);
        }
    }

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

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

    /**
     * Gets whether to use EPSV or PORT command for data tranfers.
     * Default is <code>false</code>, meaning PORT command will be used.
     *
     * @return <code>true</code> to use EPRT command, <code>false</code> to use PORT command.
     */
    public synchronized boolean getUseEPRT() {
        return this.useEPRT;
    }

    /**
     * Sets character set used when issuing commands and transferring files in ASCII mode.
     *
     * @param encoding the character encoding to be used
     */
    public void setWireEncoding(String encoding) {
        this.encodingWire = encoding;
    }

    /**
     * Gets character encoding used when issuing commands and reading files from FTP server in ASCII mode.
     *
     * @return the character encoding to be used
     */
    public String getWireEncoding() {
        return this.encodingWire;
    }

    // LISTENERS & EVENT CONTROL

    /**
     * Sets FTPS certificate verifier.
     *
     * @param listener the listener to add
     *
     * @see FtpsCertificateVerifier
     * @deprecated see {@link #setFtpsCertificateVerifier(FtpsCertificateVerifier)}
     */
    public void addFtpsCertificateVerifier(FtpsCertificateVerifier listener) {
        this.setFtpsCertificateVerifier(listener);
    }

    /**
     * Sets FTPS certificate verifier.
     *
     * @param listener the listener to add
     *
     * @see FtpsCertificateVerifier
     */
    public void setFtpsCertificateVerifier(FtpsCertificateVerifier listener) {
        Assert.notNull(listener);
        certificateListener = listener;
    }

    /**
     * Gets FTPS certificate verifier.
     *
     * @return the FtpsCertificateVerifier for this instance
     *
     * @see FtpsCertificateVerifier
     */
    public FtpsCertificateVerifier getFtpsCertificateVerifier() {
        return this.certificateListener;
    }


    /**
     * Adds FTP event listener.
     *
     * @param listener the listener to add
     *
     * @see FtpListener
     */
    public void addFtpListener(FtpListener listener) {
        Assert.notNull(listener);

        LinkedList listeners = (LinkedList) this.listeners.clone();
        listeners.add(listener);

        synchronized (this) {
            this.listeners = listeners;
        }
    }

    /**
     * Changes the password for current account.
     * 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.issueCommandCheck("SITE PSWD" + " \"" + 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 checksum(in, remoteFile);
        } catch (IOException 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 checksum(new ByteArrayInputStream(data), remoteFile);
    }

    /**
     * Clears command channel.
     *
     * @throws FtpException if an I/O error occurs
     */
    public void clearCommandChannel()
            throws FtpException {
        ensureConnected();
        this.client.clear();
    }

    // CONNECTION CONTROL

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

    /**
     * Connects to FTP server and optionally performs login.
     *
     * @param login true to login false otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized Ftps connect(boolean login)
            throws FtpException {
        if (isConnected()) {
            disconnect();
        }

        ConnectionParameters cp = null;
        if (this.proxyHostname == null) {
            cp = new ConnectionParameters(this.host, this.port, this.timeout);
        } else {
            cp = new ConnectionParameters(this.host,
                                          this.port,
                                          this.timeout,
                                          this.proxyHostname,
                                          this.proxyPort,
                                          this.proxyUsername,
                                          this.proxyPassword,
                                          this.proxyType);
        }
        
        cp.setTcpNoDelay(this.tcpNoDelay);
        
        this.logger.fine("Connecting to [" + this.host + "] at port " + this.port);
        SSLContext sslContext = this.context != null ? this.context : this.ctxFactory.getContext();
        this.client = this.connectionType.createClient(this, cp, sslContext, this.enabledCiphers, this.logger);
        this.client.setSendBufferSize(sendBufferSize);
        this.client.setReceiveBufferSize(receiveBufferSize);

        if (this.portAddress != null) {
            this.client.setPortAddress(this.portAddress);
        }

        fireEvent(new FtpConnectedEvent(this, this.host));

        if (login) {
            login();
        }

        // get the list of commands supported by the server and cache them on the client
        initializeServerFeatureList();

        // get remote directory
        try {
            path = getDir();
        } catch (Exception e) {
            // ignore
        }

        if (this.compression) {
            sendModeZCommand();
        } else {
            this.isModeZSupported = false;
        }

        return this;
    }

    /**
     * Retrieves and stores the commands supported by the server
     */
    private void initializeServerFeatureList() {
        try {
            this.serverFeatures = new Vector();
            Enumeration e = this.getFeatures();
            while (e.hasMoreElements()) {
                this.serverFeatures.add(e.nextElement().toString());
            }
            this.serverFeaturesCommandSuccussful = true;
        } catch (FtpException e) {
            this.serverFeaturesCommandSuccussful = false;
        }
    }

    public boolean getServerFeaturesCommandSuccessful() {
        return this.serverFeaturesCommandSuccussful;
    }


    /**
     * 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 {
        Assert.notNull(remoteDirectory);
        ensureConnected();

        if (recurse) {
            deleteDirRecursive(remoteDirectory);
        } else {
            this.client.removeDirectory(remoteDirectory);
            fireEvent(new FtpDeleteDirEvent(this, remoteDirectory, path));
        }
    }

    /**
     * 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 {
        Assert.notNull(remoteFile);
        ensureConnected();
        this.client.delete(remoteFile);
        fireEvent(new FtpDeleteFileEvent(this, remoteFile, path));
    }

    /**
     * Quits the FTP session and disconnects from the FTP server.
     */
    public synchronized void disconnect() {
        if (this.client != null && !this.client.isClosed()) {
            safeQuit();
            this.client.close();
            fireEvent(new FtpDisconnectedEvent(this, this.host));
        }
    }

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

    // DOWNLOAD CONTROL

    /**
     * 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 {
        download(out, remoteFile, 0);
    }

    /**
     * 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 File 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);
        OutputStream out = getOutputStream(result);

        try {
            this.targetFile = result;
            download(out, remoteFile);
        } catch (FtpException e) {
            if (result.length() == 0) {
                result.delete();
            }

            throw e;
        } finally {
            IOTools.close(out);

            if (this.getPreserveDownloadTimestamp()) {
                boolean success = result.setLastModified(this.getFileTimestamp(remoteFile).getTime());
                if (success) {
                    this.logger.fine("Timestamp of local file "
                                             + result.getAbsolutePath()
                                             + " changed successfully. ["
                                             + new Date(result.lastModified())
                                             + "]");
                } else {
                    this.logger.fine("Timestamp of local file " + result.getAbsolutePath() + " count not be changed.");
                }
            }
        }

        return result;
    }

    /**
     * Downloads file from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteFile the file to download
     *
     * @return File 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 {
        Assert.notNull(remoteDir);
        ensureConnected();

        String curRemoteDir = this.path;
        setDir(remoteDir);

        File curLocalDir = getLocalDir();

        try {
            String dirName = getDirectoryName(remoteDir);

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

            Enumeration files = getDirListing();
            while (files.hasMoreElements() && !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 {
                   File localFileToUpdate = new File(filename);
                   localFileToUpdate.setLastModified(this.getFileTimestamp(remoteDir).getTime());
                   logger.fine("Timestamp for local directory " + localFileToUpdate.getAbsolutePath() + " changed successfully.");
               } catch (FtpException e) {
                   logger.fine("Timestamp for local directory " + filename + " could not be changed");
               }
               */
        }
    }

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

    }


    /**
     * Downloads remote directory and contents recursively from FTP server.
     * Downloads are stored relative to current local directory.
     *
     * @param remoteDir       the remote directory to download
     * @param retryLimit      the retry limit if download fails
     * @param retryInterval   the retry interval in seconds
     * @param performChecksum whether to perform checksum on files downloaded, not supported by all FTP servers
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void downloadDir(String remoteDir, int retryLimit, int retryInterval,
                                         boolean performChecksum)
            throws FtpException {
        Assert.notNull(remoteDir);
        ensureConnected();

        String lastCurrentRemoteDir = this.path;
        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 = this.path;
        File currentLocalDir = getLocalDir();

        for (int i = 0; i <= retryLimit && !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 next attempt
                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 the current remote directory
     * @param currentLocalDir  the current local directory
     * @param performChecksum  whether to perform checksum after download, not supported by all FTP servers
     * @param fileList         the file list to be downloaded
     *
     * @throws IOException  if an I/O error occurs
     * @throws FtpException if an 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) && !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 = this.path;
                if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                    fileDir = fileDir + "/";
                }
                if (!checksum(fileDownloaded, fileDir + tempFile.getName())) {
                    throw new FtpException("Checksum returned false");
                }
            }

            fileList.remove(0);
        }
    }

    /**
     * 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 greater than zero.");
        }

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

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

        String dirName = getDirectoryName(remoteDir);

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

            String filename = dirName + File.separator + remoteDir;
            try {
                File localFileToUpdate = new File(filename);
                localFileToUpdate.setLastModified(this.getFileTimestamp(remoteDir).getTime());
                this.getDebugStream().println("Timestamp for local directory "
                                                      + localFileToUpdate.getAbsolutePath()
                                                      + " changed successfully.");
            } catch (FtpException e) {
                this.getDebugStream().println("Timestamp for local directory " + filename + " could not be changed");
            }
        }

        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.interrupted(); ++i) {
            try {
                downloadDir(currentRemoteDir, currentLocalDir, performChecksum, fileList, threadCount);
                success = true;
            } catch (Exception e) {

                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;
            }
            FtpsDownloadItem item = new FtpsDownloadItem(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++) {
            ((FtpsDownloadItem) downloadItems.get(i)).start();
        }

        boolean error = false;
        String errorMsg = null;

        // wait for upload threads to complete.
        for (int i = 0; i < downloadItems.size(); i++) {
            FtpsDownloadItem item = (FtpsDownloadItem) 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);
        }
    }

    private class FtpsDownloadItem
            extends Thread {

        private Ftps parent = null;
        private Ftps 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 FtpsDownloadItem(Ftps ftp, Vector files, boolean performChecksum, String remoteDir, File localDir) {
            this.parent = ftp;
            this.files = files;
            this.performChecksum = performChecksum;
            this.currentRemoteDir = remoteDir;
            this.currentLocalDir = localDir;
            this.ftp = new Ftps(ftp.getHostname(), ftp.getUsername(), ftp.getPassword());
            this.ftp.setPreserveDownloadTimestamp(ftp.getPreserveDownloadTimestamp());
            this.ftp.setPreserveUploadTimestamp(ftp.getPreserveUploadTimestamp());
            this.ftp.setTimezone(ftp.getTimeZone());
            this.ftp.setConnectionType(ftp.getConnectionType());
            this.ftp.setPassive(ftp.getPassive());
            this.ftp.setPort(ftp.getPort());
            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.setPortAddress(ftp.getPortAddress());
            this.ftp.setProxyAuthentication(ftp.getProxyUsername(), ftp.getProxyPassword());
            this.ftp.setProxyHost(ftp.getProxyHostname(), ftp.getProxyPort());
            this.ftp.setProxyType(ftp.getProxyType());
            this.ftp.setTimeout(ftp.getTimeout());
            this.ftp.setListeners(ftp.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 {
                this.ftp.connect();

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

                Enumeration elements = files.elements();
                while (elements.hasMoreElements() && this.abortAll == false) {
                    ftp.reset();
                    ftp.setDir(currentRemoteDir);
                    ftp.setLocalDir(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()) {
                            continue;
                        } else {
                            this.error = true;
                            this.errorMsg = fe.getMessage();

                            //fire FtpErrorEvent for FtpErrorListener(s)
                            for (Iterator i = listeners.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) {
                // ignore
            } finally {
                this.ftp.disconnect();
                this.connected = false;
            }
        }
    }

    /**
     * Download items.
     */
    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, FtpsDownloadItem> downloadHashMap = new HashMap<String, FtpsDownloadItem>();

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

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

    /**
     * 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.interrupted = true;
    }

    // INTERRUPTION CONTROL

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

    // FTP COMMANDS

    /**
     * Issues the 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 {
        Assert.notNull(cmd);
        ensureConnected();
        fireEvent(new FtpCommandEvent(this, cmd));

        return issueCommandCheck(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 {
        Assert.notNull(cmd);
        ensureConnected();

        Response response = this.client.sendRequest(cmd);

        return response.toString();
    }

    /**
     * Logs into FTP server after establishing a connection.
     * This method should only be used if login is not performed automatically during <code>Ftps#connect</code>.
     *
     * @throws FtpException if an I/O or FTP error occurs
     * @see #connect()
     */
    public void login()
            throws FtpException {
        this.connectionType.authenticate(this.client, this.user, this.password,
                                         this.account);
        setupFileParser();
        setBinary();
    }

    /**
     * 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 {
        Assert.notNull(newDirectory);
        ensureConnected();
        this.client.makeDirectory(newDirectory);
        fireEvent(new FtpCreateDirEvent(this, newDirectory, path));
    }

    /**
     * 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 = this.path;

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

        for (StringTokenizer st = new StringTokenizer(newDirectory, "/"); st.hasMoreElements(); ) {
            String path = (String) st.nextElement();

            try {
                setDir(path);
            } catch (FtpException sd) {
                makeDir(path);
                setDir(path);
            }
        }

        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) {
        Assert.notNull(directoryName);

        File dir = new File(getLocalDir(), directoryName);

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

        return dir;
    }

    /**
     * Deletes multiple files matching regular in FTP server's current directory.
     * For example, to delete all files with .txt extension use the regular expression "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>".
     *
     * @param filter the regular expression 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() && !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 instead.
     * 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() && !interrupted()) {
            String file = fileNames.nextElement().toString();

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

    /**
     * Downloads multiple files matching regular expression from FTP server's current
     * directory. 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>".
     *
     * @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 e = getDirListing(); e.hasMoreElements() && !interrupted(); ) {
            FtpFile file = (FtpFile) e.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 filenames of files 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() && !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
     * all files with extension .txt use the regular expression "<tt>.</tt><tt>*</tt><tt>\\.txt</tt>".
     *
     * @param filter the regular expression 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 i = 0; i < files.length && !interrupted(); ++i) {
            if (regex.matches(files[i].getName())) {
                upload(files[i]);
            }
        }
    }

    /**
     * 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 {
        ensureConnected();

        return this.client.noop().toString();
    }

    /**
     * Reads response from FTP server.
     * 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.
     *
     * @return the response
     *
     * @throws FtpException if an I/O or FTP related error occurs.
     */
    public String readResponse()
            throws FtpException {
        return this.client.readResponse().toString();
    }

    /**
     * Removes specified FTP event listener.
     *
     * @param listener the listener to remove
     *
     * @see FtpListener
     */
    public void removeFtpListener(FtpListener listener) {
        LinkedList listeners = (LinkedList) this.listeners.clone();
        listeners.remove(listener);

        synchronized (this) {
            this.listeners = listeners;
        }
    }

    /**
     * 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 {
        Assert.notNull(remoteFile);
        Assert.notNull(newFile);
        ensureConnected();
        this.client.rename(remoteFile, newFile);
        fireEvent(new FtpRenameFileEvent(this, remoteFile, newFile, path));
    }

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

    /**
     * Resumes download of file from FTP server.
     *
     * @param remoteFile the file to download
     * @param off        file offset in bytes
     *
     * @return 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 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);
        }

        // in this case the file will be downloaded to the
        // tmp-file and than copied to the result file
        // todo: why?
        File result = new File(getLocalDir(), localFile);
        this.targetFile = result;
        File tmp = new File(getLocalDir().getAbsolutePath() + File.separator + System.currentTimeMillis() + ".tmp");
        FileOutputStream out = null;
        RandomAccessFile raf = null;
        BufferedInputStream bin = null;

        try {
            out = new FileOutputStream(tmp);

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

            raf = new RandomAccessFile(result, "rw");
            raf.seek(off);
            bin = new BufferedInputStream(new FileInputStream(tmp));

            byte[] buffer = new byte[this.getBlockTransferSize()];

            for (int read = bin.read(buffer); read != -1; read = bin.read(buffer)) {
                raf.write(buffer, 0, read);
            }
        } catch (IOException e) {
            throw new FtpException(e.getMessage(), e);
        } finally {
            IOTools.close(out);
            IOTools.close(bin);
            safeClose(raf);
            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        the local file offset in bytes
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    public synchronized void resumeUpload(File localFile, String remoteFile, long off)
            throws FtpException {
        Assert.notNull(localFile);
        ensureConnected();

        this.targetFile = localFile;
        long length = localFile.length() - off;
        InputStream in = getInputStream(localFile);
        Socket data = null;
        OutputStream out = null;
        boolean errorOccurred = false;

        try {
            in.skip(off);
            prepareDataConnection();
            this.client.restart(Long.toString(off));
            data = this.client.store(remoteFile);
            out = data.getOutputStream();
            upload(in, out, remoteFile, length, off);
        } catch (FtpException e) {
            errorOccurred = true;
            throw e;
        } catch (IOException e) {
            errorOccurred = true;
            throw new FtpException(e.getMessage(), e);
        } finally {
            IOTools.close(in);
            IOTools.close(out);
            IOTools.close(data);
            readLastResponse(errorOccurred);

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

                        Date date = new Date(localFile.lastModified());
                        this.setFileTimestamp(remoteFile, date);
                        this.getDebugStream().println("Timestamp of remote file "
                                                              + remoteFile
                                                              + " changed successfully. ["
                                                              + date.toString()
                                                              + "]");
                    }
                }
            } catch (FtpException fe) {
                this.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 remote destination file on FTP server
     * @param off        the local file offset in bytes
     *
     * @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);
    }

    // UPLOAD CONTROL

    /**
     * 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);
            String remoteDirectory = this.getDir();
            try {
                this.targetFile = localFile;
                upload(in, remoteFile, append, localFile.length(), 0, false);
            } catch (FtpException e) {
                error = true;
                throw e;
            } finally {
                IOTools.close(in);

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

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

                            Date date = new Date(localFile.lastModified());
                            this.setFileTimestamp(remoteFile, date);
                            this.getDebugStream().println("Timestamp of remote file "
                                                                  + remoteFile
                                                                  + " changed successfully. ["
                                                                  + date.toString()
                                                                  + "]");
                        }
                    }
                } catch (FtpException fe) {
                    try {
                        Date date = new Date(localFile.lastModified());
                        this.setFileModificationTime(remoteFile, date);
                        this.getDebugStream().println("Timestamp of remote file "
                                                              + remoteFile
                                                              + " changed successfully. ["
                                                              + date.toString()
                                                              + "]");
                    } catch (FtpException ie) {
                        this.getDebugStream().println("Timestamp of remote file "
                                                              + remoteFile
                                                              + " could not be changed.");
                    }
                }
            }


        }
    }

    /**
     * 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 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 using temporary extension, then renames file back to original name once uploaded.
     *
     * @param extension the temporary extension to use when uploading files prior to renaming them or <code>null</code> to use default extension
     * @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.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);
        upload(in, remoteFile, append, data.length, 0, 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
     *
     * @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 contents of <code>InputStream</code> to FTP server.
     *
     * @param input      the <code>InputStream</code> to read from
     * @param remoteFile the destination of upload
     * @param append     <code>true</code> if to append to remote destination, <code>false</code> otherwise
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void upload(InputStream input, String remoteFile,
                                    boolean append)
            throws FtpException {
        int available = -1;

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

        upload(input, remoteFile, append, available, 0, false);
    }

    /**
     * Uploads contents of <code>InputStream</code> to FTP server.
     *
     * @param input      the <code>InputStream</code> to read from
     * @param remoteFile the destination of upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void upload(InputStream input, String remoteFile)
            throws FtpException {
        upload(input, 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 when uploading files prior to renaming them 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 {
        Assert.notNull(localDir);
        Assert.isTrue(localDir.isDirectory(), "bad directory: " + localDir);
        Assert.isTrue(localDir.exists(), "bad directory: " + localDir);

        try {
            makeDir(localDir.getName());
        } catch (FtpException e) {
            // ignore - directory already exists. todo: check it more precise
        }

        String curRemoteDir = this.path;
        setDir(localDir.getName());

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

        try {
            Enumeration files = getLocalDirListing();

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

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

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

                    Date date = new Date(localDir.lastModified());
                    this.setFileTimestamp(localDir.getName(), date);
                    this.logger.fine("Timestamp of remote directory "
                                             + localDir.getName()
                                             + " changed successfully. ["
                                             + date.toString()
                                             + "]");
                }
            } catch (Exception e) {
                this.logger.fine("Timestamp of remote directory " + localDir.getName() + " could not be changed");
            }
        }
    }

    /**
     * Uploads local directory and contents to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir        the directory to upload
     * @param retryLimit      retry limit if upload fails
     * @param performChecksum whether to perform checksum after 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, 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      retry limit if upload fails
     * @param performChecksum whether to perform checksum, not supported by all FTP servers
     * @param extension       the temporary extension to use prior to renaming files after upload
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void uploadDir(File localDir, int retryLimit,
                                       boolean performChecksum, String extension)
            throws FtpException {
        this.uploadDir(localDir, retryLimit, 0, performChecksum, extension);
    }

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

    /**
     * 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());
        }
    }

    /**
     * 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 greater 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);

        // try to upload files up until retry limit
        for (int i = 0; i <= retryLimit && !interrupted(); ++i) {
            try {
                uploadDir(currentRemoteDir, performChecksum, extension, fileList, localDir, threadCount);
                success = true;
            } catch (Exception e) {

                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;
            }
            FtpsUploadItem item = new FtpsUploadItem(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++) {
            ((FtpsUploadItem) uploadItems.get(i)).start();
        }

        boolean error = false;
        String errorMsg = null;

        // wait for upload threads to complete.
        for (int i = 0; i < uploadItems.size(); i++) {
            FtpsUploadItem item = (FtpsUploadItem) 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, FtpsUploadItem> uploadHashMap = new HashMap<String, FtpsUploadItem>();

    /**
     * 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++) {
            ((FtpsUploadItem) uploadItems.get(i)).cancelTransfer();
        }
    }

    private class FtpsUploadItem
            extends Thread {

        private Ftps parent = null;
        private Ftps 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 FtpsUploadItem(Ftps 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 Ftps(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.setBlockTransferSize(ftp.getBlockTransferSize());
            this.ftp.setCompression(ftp.getCompression());
            this.ftp.setConnectionType(ftp.getConnectionType());
            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.setPortAddress(ftp.getPortAddress());
            this.ftp.setProxyAuthentication(ftp.getProxyUsername(), ftp.getProxyPassword());
            this.ftp.setProxyHost(ftp.getProxyHostname(), ftp.getProxyPort());
            this.ftp.setProxyType(ftp.getProxyType());
            this.ftp.setTimeout(ftp.getTimeout());
            this.ftp.setListeners(ftp.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();

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

                this.connected = 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() == true) {
                                continue;
                            } else {
                                this.error = true;
                                this.errorMsg = fe.getMessage();

                                for (Iterator i = listeners.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) {
                // ignore
            } finally {
                this.ftp.disconnect();
                this.connected = false;
            }
        }
    }

    /**
     * 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;
        }
    }

    /**
     * Uploads local directory and contents to FTP server.
     * Uploads are stored relative to current remote directory.
     *
     * @param localDir        the directory to upload
     * @param retryLimit      retry limit if upload fails
     * @param retryInterval   the retry interval in seconds
     * @param performChecksum whether to perform checksum, not supported by all FTP servers
     * @param extension       the temporary extension to use prior to renaming files after upload
     *
     * @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 {
        Assert.notNull(localDir);
        Assert.isTrue(localDir.isDirectory(), "bad directory: " + localDir);
        Assert.isTrue(localDir.exists(), "bad directory: " + localDir);

        // send PWD to get return point
        String lastCurrentDir = this.path;
        boolean success = false;

        // try to make directory using MKD
        try {
            makeDir(localDir.getName());
        } catch (FtpException e) {
            // todo: ignore?
        }

        // set directory using CWD and PWD
        setDir(localDir.getName());

        // get previous local directory and set local dir
        File lastLocalDir = getLocalDir();
        setLocalDir(localDir);

        // get list of local files
        Vector fileList = FileTools.listFiles(localDir);

        // get list of empty local directories
        Vector emptyDirList = FileTools.getEmptyDirList(localDir);

        // make empty directories
        makeEmptyDir(emptyDirList, localDir, this.path);

        // get current remote directory
        String currentRemoteDir = this.path;

        // try to upload files up until retry limit
        for (int i = 0; i <= retryLimit && !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());
                } else {
                    try {
                        Thread.sleep(retryInterval * 1000);
                    } catch (InterruptedException ie) {
                        //ignore
                    }
                }
                disconnect();
                connect();
            }

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

    /**
     * Uploads a directory.
     *
     * @param currentRemoteDir the remote directory
     * @param performChecksum  whether or not checksum should be performed, NOT supported by all FTP servers
     * @param extension        temporary extension to use prior to renaming file
     * @param fileList         file list to be uploaded
     * @param localDir         current local directory
     *
     * @throws FtpException if an FTP error occurs
     * @throws IOException  if an I/O error occurs
     */
    private void uploadDir(String currentRemoteDir, boolean performChecksum,
                           String extension, Vector fileList, File localDir)
            throws FtpException, IOException {
        while ((fileList.size() > 0) && !interrupted()) {
            // get next file in list to upload
            String fileName = (String) fileList.elementAt(0);

            // get file instance
            File file = new File(fileName);

            // get relative path
            String relativePath = FileTools.getRelativePath(localDir, file);

            // build absolute path
            String absolutePath = getAbsolutePath(currentRemoteDir, relativePath);

            // make relative path if it does not exist
            if ((relativePath != null) && !relativePath.equals("")) {
                try {
                    // check if both this.path and current
                    setDir(absolutePath);

                } catch (Exception e) {
                    // set directory to base directory
                    setDir(currentRemoteDir);
                    makePath(relativePath);
                }
            } else {
                relativePath = currentRemoteDir;
            }

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

                if ((file.length() > 0) && performChecksum && isFeatureSupported("XCRC")) {
                    String fileDir = this.path;
                    if (!fileDir.endsWith("/") && !fileDir.endsWith("\\")) {
                        fileDir = fileDir + "/";
                    }
                    if (!checksum(file, fileDir + file.getName())) {
                        throw new FtpException("Checksum returned false");
                    }
                }
            }

            fileList.remove(fileName);
        }
    }

    /**
     * Gets absolute virtual path.
     *
     * @param currentRemoteDir the current remove directory virtual path
     * @param relativePath     relative path
     *
     * @return absolute virtual path
     */
    private String getAbsolutePath(String currentRemoteDir, String relativePath) {
        if (currentRemoteDir.endsWith("/")) {
            return currentRemoteDir + relativePath;
        } else {
            return currentRemoteDir + "/" + relativePath;
        }
    }

    private boolean isMatchingPath(String path1, String path2) {
        if (path1.equals(path2)) {
            return true;
        } else if (path1.endsWith("/") && !path2.endsWith("/")) {
            if (path1.substring(0, path1.lastIndexOf("/")).equals(path2)) {
                return true;
            } else {
                return false;
            }
        } else if (path2.endsWith("/") && !path1.endsWith("/")) {
            if (path2.substring(0, path2.lastIndexOf("/")).equals(path1)) {
                return true;
            } else {
                return false;
            }
        } else {
            return false;
        }
    }

    /**
     * Uploads the file which is created in the current directory under a name
     * unique to that directory.
     *
     * @param fileName the filename to upload
     *
     * @return the resulting unique filename on FTP server
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String uploadUnique(String fileName)
            throws FtpException {
        return uploadUnique(new File(fileName));
    }

    /**
     * Uploads the file which is created in the current directory under a name
     * unique to that directory.
     *
     * @param fileName          the filename to upload
     * @param useFilenameAsSeed whether filename should be used as seed value for generating unique filename on server
     *
     * @return the resulting unique filename on FTP server
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String uploadUnique(String fileName, boolean useFilenameAsSeed)
            throws FtpException {
        return uploadUnique(new File(fileName), useFilenameAsSeed);
    }


    /**
     * Uploads the file which is created in the current directory
     * under a name unique to that directory.
     *
     * @param file the file to upload
     *
     * @return the resulting unique filename on FTP server
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String uploadUnique(File file)
            throws FtpException {
        return uploadUnique(file, true);
    }

    /**
     * Uploads the file which is created in the current directory
     * under a name unique to that directory.
     *
     * @param file              the file to upload
     * @param useFilenameAsSeed whether filename should be used as seed value for generating unique filename on server
     *
     * @return the resulting unique filename on FTP server
     *
     * @throws FtpException if an I/O or FTP 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 the file which is created in the current directory
     * under a name unique to that directory.
     *
     * @param in       data input stream.
     * @param fileName the filename seed to use for determining remote filename or <code>null</code> not to pass filename as part of STOU command
     *
     * @return the resulting unique filename on FTP server
     *
     * @throws FtpException if an I/O or FTP error occurs
     */
    public synchronized String uploadUnique(InputStream in, String fileName)
            throws FtpException {
        return upload(in, fileName, false, 0, 0, true);
    }

    /**
     * Fires the FTP event.
     *
     * @param ev event to fire.
     */
    protected void fireEvent(FtpEvent ev) {
        for (Iterator i = this.listeners.iterator(); i.hasNext(); ) {
            FtpListener listener = (FtpListener) i.next();
            ev.accept(listener);
        }
    }

    /**
     * Checks if there are listeners registered to capture events from the Ftp class.
     *
     * @return true if listeners found, false otherwise.
     */
    private synchronized boolean hasListeners() {
        return listeners.size() > 0;
    }

    /**
     * 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) {
        return isCurrentOrParentDir(file.getName());
    }

    /**
     * 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) {
        return isCurrentOrParentDir(file.getFilename());
    }

    /**
     * Checks if the specified file name is parent or current directory sign.
     *
     * @param dirName directory name to check
     *
     * @return <code>true</code> if the specified name is current or parent
     *         directory name; <code>false</code> otherwise
     */
    private static boolean isCurrentOrParentDir(String dirName) {
        return (dirName.equals(CURRENT_DIR) || dirName.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) {
        path = path.trim();

        if (path.startsWith("\"")) {
            path = path.substring(1, path.lastIndexOf("\""));
        }

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

        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);
        }
    }

    /**
     * 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 (Exception e) {
            throw new FtpException(e.getMessage(), 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 = this.path;
        setDir(file.getFilename());

        Enumeration e = getDirListing();

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

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

                break;
            }
        }

        setDir(lastDir);

        return empty;
    }

    /**
     * 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 used to calculate local 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);
        }
    }

    /**
     * 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 = this.path;
                    setDir(file.getFilename());
                    vector.addAll(getRemoteEmptyDirList(remoteDir
                                                                + file.getFilename()));
                    setDir(lastDir);
                }
            }
        }

        return vector;
    }

    /**
     * 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");
        }
    }

    /**
     * Gets all the remote files  from the current directory and subdirectories.
     *
     * @param remoteDir The current remote will be consulted
     *
     * @return A collection of file paths.
     *
     * @throws FtpException If an error occurs
     */
    public synchronized Vector getRemoteFileList(String remoteDir)
            throws FtpException {
        Enumeration e = getDirListing();

        Vector vector = new Vector();

        remoteDir = this.path;

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

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

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

        return vector;
    }

    /**
     * Gets the socket input stream. When compression is allowed, the socket input stream will be
     * returned into a Inflater input stream.
     *
     * @param data Current data socket.
     *
     * @return Socket input stream
     *
     * @throws IOException If an error occurs.
     */
    private InputStream getSocketInputStream(Socket data)
            throws IOException {
        if (!isCompressionAllowed()) {
            return data.getInputStream();
        } else {
            this.inflaterIn = new InflaterInputStream(data.getInputStream());
            return this.inflaterIn;
        }
    }

    /**
     * Sets transfer mode.
     *
     * @param mode desired transfer mode.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private synchronized void setTransferMode(int mode)
            throws FtpException {
        switch (mode) {
            case Ftps.ASCII:
                this.client.representationType("A");
                break;
            case Ftps.BINARY:
                this.client.representationType("I");
                break;
            case Ftps.AUTO:
                break;
            default:
                throw new FtpException("wrong mode passed: " + mode);
        }

        this.modeCode = mode;
    }

    /**
     * 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 checksum(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);
        }
    }

    /**
     * Close ZLIB compression Stream.
     */
    private void closeDeflaterStream() {
        try {
            this.deflaterOut.finish();
            this.deflaterOut.close();
        } catch (Exception e) {
            // todo: ignore?
        }
        this.deflaterOut = null;
    }

    /**
     * Close Inflater stream
     */
    private void closeInflaterStream() {
        try {
            this.inflaterIn.close();
        } catch (Exception e) {
            // todo: ignore?
        }
        this.inflaterIn = null;
    }

    /**
     * Deletes directory recursively.
     *
     * @param remoteName directory to delete.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    private void deleteDirRecursive(String remoteName)
            throws FtpException {
        String curDir = this.path;
        setDir(remoteName);

        // delete directory content
        try {
            for (Enumeration files = getDirListing(""); files.hasMoreElements(); ) {
                FtpFile file = (FtpFile) files.nextElement();

                String name = file.getFilename();

                if (!file.isDirectory()) {
                    deleteFile(name);
                } else if (!isCurrentOrParentDir(file)) {
                    deleteDir(name, true);
                }
            }
        } finally {
            setDir(curDir);
        }

        // delete directory itself
        if (!isCurrentOrParentDir(remoteName)) {
            deleteDir(remoteName);
        }
    }

    /**
     * Downloads the remote file.
     *
     * @param out        the destination <code>OutputStream</code>
     * @param remoteFile the remote file
     * @param offset     the file offset
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    private void download(OutputStream out, String remoteFile, long offset)
            throws FtpException {
        ensureConnected();

        long length = 0;

        if (mode instanceof AutoMode) {
            setAutoTransferMode(remoteFile);
        }

        try {
            length = getFilesize(remoteFile);
        } catch (FtpException e) {

            if (this.errorOnSizeCommand) {
                throw e;
            }
        }

        prepareDataConnection();

        if (offset > 0) {
            this.client.restart(Long.toString(offset));
        }

        Socket data = this.client.retrieve(remoteFile);

        boolean errorOccurred = false;

        try {
            InputStream in = getSocketInputStream(data);
            long startTime = System.currentTimeMillis();
            long total = this.mode.transmit(in, out, remoteFile, FtpProgressEvent.DOWNLOAD, length, offset);
            long totalTime = System.currentTimeMillis() - startTime;

            String targetPath = "";
            if (this.targetFile != null) {
                targetPath = this.targetFile.getAbsolutePath();
            }

            fireEvent(new FtpDownloadEvent(this,
                                           remoteFile,
                                           path,
                                           targetPath,
                                           total,
                                           totalTime));
        } catch (IOException e) {
            errorOccurred = true;
            throw new FtpException(e.getMessage(), e);
        } finally {
            if (isCompressionAllowed() && this.inflaterIn != null) {
                closeInflaterStream();
            }
            IOTools.close(data);
            readLastResponse(errorOccurred);
        }
    }

    /**
     * Ensures that the Ftp is connectected to the server.
     */
    private void ensureConnected() {
        if (!((this.client != null) && !this.client.isClosed())) {
            throw new IllegalStateException("client is not connected");
        }
    }

    /**
     * Ensures that the Ftp is not connected to the server.
     */
    private void ensureNotConnected() {
        if (isConnected()) {
            throw new IllegalStateException("client is connected");
        }
    }

    /**
     * Initializes local directory path.
     */
    private void initLocalDir() {
        String path = CURRENT_DIR;

        try {
            File curDir = new File(CURRENT_DIR);
            path = curDir.getCanonicalPath().replace('\\', '/'); // todo: why?
        } catch (IOException e) {
            // ignore
        }

        this.localDir = new File(path);
    }

    /**
     * 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, String currentPath)
            throws FtpException {
        try {
            Enumeration e = emptyDirList.elements();

            // appends slash to current path if needed
            if (!currentPath.endsWith("/") && !currentPath.endsWith("\\")) {
                currentPath = currentPath + "/";
            }

            // create directories
            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);
    }

    // UTILITIES

    /**
     * Closes file without exceptions.
     *
     * @param file file to close
     */
    private static void safeClose(RandomAccessFile file) {
        try {
            file.close();
        } catch (Exception e) {
            // ignore
        }
    }

    /**
     * Sets the transfer mode (ASCII or BINARY) according with the fileName argument.
     * I has to be used when the transfer mode has been change to auto.
     *
     * @param fileName The file will be transfer
     *
     * @throws FtpException if an error occurs
     */
    private synchronized void setAutoTransferMode(String fileName)
            throws FtpException {
        javax.activation.MimetypesFileTypeMap map = new javax.activation.MimetypesFileTypeMap();
        String tmpContentType = map.getContentType(fileName);

        try {
            javax.activation.MimeType type = new javax.activation.MimeType(tmpContentType);
            boolean isAscii = type.getPrimaryType().equals("text");

            if (isAscii && (this.modeCode == Ftps.BINARY)) {
                setTransferMode(Ftps.ASCII);
            } else if (!isAscii && (this.modeCode != Ftps.BINARY)) {
                setTransferMode(Ftps.BINARY);
            }
        } catch (Exception e) {
            throw new FtpException("Error setting transfer mode.", e);
        }
    }

    private void safeQuit() {
        try {
            this.client.quit();
        } catch (Exception e) {
            // ignore
        }
    }

    /**
     * Sets the end data port range for incomings connections.
     *
     * @param dataPort End data Port range for incomings connections
     */
    private synchronized void setDataPortEnd(int dataPort) {
        if (dataPort < getDataPortStart()) {
            throw new IllegalArgumentException(
                    "Invalid assigment. The end data port must be greater or equal than the start data port");
        }

        this.dataPortRange = new ServerSocketUtil.PortRange(this.dataPortRange.start, dataPort);
    }

    /**
     * Sets the start data port range for incoming connections.
     *
     * @param dataPort Start data Port range for incoming connections.
     */
    private synchronized void setDataPortStart(int dataPort) {
        if (dataPort > getDataPortEnd()) {
            throw new IllegalArgumentException(
                    "Invalid assigment. The start data port must be less or equal than the end data port");
        }

        this.dataPortRange = new ServerSocketUtil.PortRange(dataPort, this.dataPortRange.end);
    }

    /**
     * Returns whether the compression has been set and if the Mode Z command
     * is supported by the FTPs Server.
     *
     * @return <code>true</code> compression is allowed for both the API and the FTPs server.
     */
    private boolean isCompressionAllowed() {
        return this.compression && this.isModeZSupported;
    }

    /**
     * Checks if a firewall or proxy is used.
     *
     * @return true if firewall or proxy is used, false otherwise
     */
    private boolean isFirewallUsed() {
        // get proxy
        //Properties props = System.getProperties();
        //String proxy = props.getProperty("socksProxyHost");
        String proxy = this.proxyHostname;

        // flag to indicate whether proxy or firewall is used
        boolean firewall = ((proxy != null) || ((user != null) && (user
                .indexOf("@") != -1)));

        return firewall;
    }

    /**
     * Creates <code>InputStreamReader</code> instance for passed input stream.
     *
     * @param in input stream.
     *
     * @return new <code>InputStreamReader</code> instance.
     *
     * @throws FtpException if an I/O error occurs.
     */
    private InputStreamReader getInputStreamReader(InputStream in)
            throws FtpException {
        try {
            if (this.encodingWire != null && this.encodingWire.length() > 0) {
                return new InputStreamReader(in, this.encodingWire);
            } else {
                return new InputStreamReader(in);
            }
        } catch (Exception e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    /**
     * Prepares the data connection.
     *
     * @throws FtpException if an I/O or protocol error occurs
     */
    private void prepareDataConnection()
            throws FtpException {
        if (this.passive) {
            this.client.passive(isFirewallUsed());
        } else {
            this.client.dataPort(this.dataPortRange);
        }
    }

    /**
     * Reads the file list from the specified socket.
     *
     * @param data the data socket
     *
     * @return the vector of readed files names
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    private Vector readFileList(Socket data)
            throws FtpException {
        BufferedReader reader = null;
        boolean errorOccurred = false;

        try {
            InputStream in = getSocketInputStream(data);
            reader = new BufferedReader(getInputStreamReader(in));

            Vector files = new Vector();

            for (String line = reader.readLine(); line != null; line = reader.readLine()) {
                files.addElement(line);
            }

            return files;
        } catch (IOException e) {
            if (e instanceof javax.net.ssl.SSLHandshakeException) {
                return new Vector();
            }
            errorOccurred = true;
            throw new FtpException(e.getMessage(), e);
        } finally {
            if (isCompressionAllowed() && this.inflaterIn != null) {
                closeInflaterStream();
            }
            IOTools.close(reader);
            IOTools.close(data);
            readLastResponse(errorOccurred);
        }
    }

    /**
     * Reads the last server response after data transferring commands.
     */
    private void readLastResponse(boolean errorOccurred)
            throws FtpException {
        if (this.client.getLastResponse().getCode() >= Response.POSITIVE_COMPLETION) {
            return;
        }

        try {
            this.client.readResponse();
        } catch (FtpException e) {
            if (!errorOccurred) {
                throw e;
            }
        }
    }

    /**
     * Removes the path from the file name suggested by the client.
     *
     * @param pathName The file and path names
     *
     * @return Returns only the file name
     */
    private synchronized String removePathFromFileName(String pathName) {
        String fileName = "";
        int i = 0;

        for (i = pathName.length() - 1; i > 0; i--) {
            if (pathName.charAt(i - 1) == '/') {
                break;
            }
        }

        fileName = pathName.substring(i, pathName.length());

        return fileName;
    }

    /**
     * Search the file name after execute STOU command. The file name is
     * searched in the server response
     *
     * @param response The response from the server
     * @param fileName The file name suggested by the client
     *
     * @return Returns the new file name
     */
    private synchronized String searchFileUniqueName(String response,
                                                     String fileName) {
        String remoteFile = "";

        if (fileName != null && response != null) {
            fileName = removePathFromFileName(fileName);

            StringTokenizer st = new StringTokenizer(response);

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

                if (token.indexOf(fileName) != -1) {
                    remoteFile = token;
                    break;
                }
            }

            // server may not support STOU with filename parameter ... try to parse again
            if (remoteFile.equals("")) {
                StringTokenizer st2 = new StringTokenizer(response);
                while (st2.hasMoreElements()) {
                    remoteFile = (String) st2.nextElement();
                }
            }
        } else if (fileName == null && response != null) {
            StringTokenizer st = new StringTokenizer(response);
            while (st.hasMoreElements()) {
                remoteFile = (String) st.nextElement();
            }
        }

        // trim any trailing periods
        if (remoteFile.endsWith(".")) {
            remoteFile = remoteFile.substring(0, remoteFile.length() - 1);
        }

        return remoteFile;
    }

    /**
     * Send Mode Z command when it is supported.
     *
     * @throws FtpException If an error occurs.
     */
    private void sendModeZCommand()
            throws FtpException {
        if (isFeatureSupported("MODE Z")) {
            this.client.sendCommand("MODE Z");
            readResponse();
            this.isModeZSupported = true;
        }
    }

    /**
     * Sets the current FTP file parser.
     */
    private void setupFileParser() {
        if (this.fileParser != null) {
            return;
        }

        try {
            String system = getSystemType();

            // get first token to determine OS type
            StringTokenizer st = new StringTokenizer(system);
            system = st.nextToken();

            if (system.equalsIgnoreCase("Windows_NT")) {
                setFtpFileParser(new NTParser());
            } else if (system.equalsIgnoreCase("VMS")) {
                setFtpFileParser(new VMSParser());
            } else if (system.equalsIgnoreCase("OS/400")) {
                setFtpFileParser(new AS400Parser());
            } else {
                setFtpFileParser(new UnixParser());
            }
        } catch (Exception e) {
            setFtpFileParser(new UnixParser());
        }
    }

    /**
     * Translate from one encoding to another.
     *
     * @param in            value to translate
     * @param encodeTypeIn  encoding in
     * @param encodeTypeOut encoding out
     *
     * @return Amount of bytes were transmited
     *
     * @throws IOException If a I/O error occurs
     */
    private static int translateBytes(int in, String encodeTypeIn,
                                      String encodeTypeOut)
            throws IOException {
        // see if translation is required
        if ((encodeTypeIn == null) || (encodeTypeOut == null)) {
            return in;
        }

        if ((encodeTypeIn.length() == 0) || (encodeTypeOut.length() == 0)) {
            return in;
        }

        if (encodeTypeIn.equalsIgnoreCase(encodeTypeOut)) {
            return in;
        }

        byte[] inBytes = new byte[]{ (byte) in };

        String outStr = new String(inBytes, Charset.forName(encodeTypeIn));

        byte[] outBytes = outStr.getBytes(Charset.forName(encodeTypeOut));

        return outBytes[0];
    }

    /**
     * Uploads the <code>InputStream</code> content to the server.
     *
     * @param in         the source <code>InputStream</code>
     * @param remoteFile the target file
     * @param append     data appending flag
     * @param length     the total data to upload
     * @param offset     the data offset from input stream
     * @param unique     Indicate if it is a upload unique
     *
     * @return last response from server or parsed resulting filename if STOU command is used
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    private String upload(InputStream in, String remoteFile, boolean append, long length, long offset, boolean unique)
            throws FtpException {
        Socket data = null;

        if (mode instanceof AutoMode) {
            setAutoTransferMode(remoteFile);
        }

        boolean errorOccurred = false;

        try {
            prepareDataConnection();

            if (unique) {
                data = this.client.storeUnique(remoteFile);
            } else {
                data = append ? this.client.append(remoteFile) : this.client.store(remoteFile);
            }

            OutputStream out = data.getOutputStream();

            if (unique) {
                String remoteFileName = this.searchFileUniqueName(this.client.getLastResponse().toString(), remoteFile);
                upload(in, out, remoteFileName, length, offset);
                return this.searchFileUniqueName(this.client.getLastResponse().toString(), remoteFile);
            } else {
                upload(in, out, remoteFile, length, offset);
                return this.client.getLastResponse().toString();
            }
        } catch (FtpException e) {
            errorOccurred = true;
            throw e;
        } catch (IOException e) {
            errorOccurred = true;
            throw new FtpException(e.getMessage(), e);
        } finally {
            if (isCompressionAllowed() && this.deflaterOut != null) {
                closeDeflaterStream();
            }
            IOTools.close(data);
            readLastResponse(errorOccurred);
        }
    }

    /**
     * Uploads the the input stream content to the output stream.
     *
     * @param in         the source input stream
     * @param out        the destination output stream
     * @param remoteFile the file name
     * @param length     total bytes to transfer
     * @param offset     the data offset from input stream
     *
     * @throws FtpException if an I/O or Ftp related error occurs
     */
    private void upload(InputStream in, OutputStream out, String remoteFile, long length, long offset)
            throws FtpException {
        long startTime = 0;
        long total = 0;
        if (isCompressionAllowed()) {
            this.deflaterOut = new DeflaterOutputStream(out);
            startTime = System.currentTimeMillis();
            total = this.mode.transmit(in, this.deflaterOut, remoteFile, FtpProgressEvent.UPLOAD, length, offset);
        } else {
            startTime = System.currentTimeMillis();
            total = this.mode.transmit(in, out, remoteFile, FtpProgressEvent.UPLOAD, length, offset);
        }
        long totalTime = System.currentTimeMillis() - startTime;

        String targetPath = "";
        if (this.targetFile != null) {
            targetPath = this.targetFile.getAbsolutePath();
        }
        fireEvent(new FtpUploadEvent(this, remoteFile, path, targetPath, total, totalTime));
    }

    /**
     * The FTP client connection strategy.
     */
    public interface ConnectionStrategy {
        /**
         * Performs user authentication.
         *
         * @param client   the connected FTP client
         * @param user     the FTP user name
         * @param password the FTP user password
         * @param account  the optional FTP account
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        void authenticate(FtpsClient client, String user, String password,
                          String account)
                throws FtpException;

        /**
         * Creates an appropriate FTP client.
         *
         * @param source         the source Ftps instance
         * @param parameters     the connection parameters
         * @param ctx            the SSL context
         * @param enabledCiphers Ciphers enable
         * @param logger         the logger
         *
         * @return a new FTP client
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        FtpsClient createClient(Ftps source, ConnectionParameters parameters,
                                SSLContext ctx, String[] enabledCiphers, java.util.logging.Logger logger)
                throws FtpException;
    } // ConnectionStrategy

    // TRANSFER MODES

    /**
     * The data transfer mode abstraction.
     *
     * @author Alex Usun
     * @version 1.0
     */
    private abstract class TransferMode {
        private final int code;

        /**
         * Creates a new mode instance.
         *
         * @param code the mode code
         */
        private TransferMode(int code) {
            this.code = code;
        }

        /**
         * Obtains the current mode code.
         *
         * @return the current mode code
         */
        public int getCode() {
            return this.code;
        }

        /**
         * Tranfers data from the input stream to the output stream.
         *
         * @param in         the source input stream
         * @param out        the destination output stream
         * @param remoteFile the file name
         * @param mode       the event mode code
         * @param length     total bytes to transfer
         * @param offset     the data offset from input stream
         *
         * @return total transferred bytes
         *
         * @throws FtpException if an I/O or FTP error occurs.
         */
        public abstract long transmit(InputStream in, OutputStream out,
                                      String remoteFile, int mode, long length, long offset)
                throws FtpException;
    } // TransferMode

    /**
     * The ordinal FTP connection with following protection.
     */
    private static class AuthSslStrategy
            extends DefaultStrategy {
        /**
         * Performs user authentication.
         *
         * @param client   the connected FTP client
         * @param user     the FTP user name
         * @param password the FTP user password
         * @param account  the optional FTP account
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        public void authenticate(FtpsClient client, String user,
                                 String password, String account)
                throws FtpException {
            client.authorize("SSL");

            boolean authorized = true;
            if (client.getFtpsCertificateVerifier() != null) {
                if (!client.getFtpsCertificateVerifier().authorized()) {
                    authorized = false;
                }
            }

            if (authorized) {
                super.authenticate(client, user, password, account);
                client.bufferSize();
            } else {
                throw new FtpException(
                        "Could not authenticate when has not been authorized");
            }

            try {
                client.protectionLevel(FtpsClient.DATA_PRIVATE);
            } catch (FtpException e) {
                client.protectionLevel(FtpsClient.DATA_CLEAR);
            }
        }
    }

    /**
     * The ordinal FTP connection with following protection.
     */
    private static class AuthTlsStrategy
            extends DefaultStrategy {
        /**
         * Performs user authentication.
         *
         * @param client   the connected FTP client
         * @param user     the FTP user name
         * @param password the FTP user password
         * @param account  the optional FTP account
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        public void authenticate(FtpsClient client, String user,
                                 String password, String account)
                throws FtpException {
            client.authorize("TLS");

            boolean authorized = true;
            if (client.getFtpsCertificateVerifier() != null) {
                if (!client.getFtpsCertificateVerifier().authorized()) {
                    authorized = false;
                }
            }

            if (authorized) {
                super.authenticate(client, user, password, account);
                client.bufferSize();
            } else {
                throw new FtpException(
                        "Could not authenticate when has not been authorized");
            }

            try {
                client.protectionLevel(FtpsClient.DATA_PRIVATE);
            } catch (FtpException e) {
                client.protectionLevel(FtpsClient.DATA_CLEAR);
            }
        }
    }

    /**
     * The auto mode. In this mode the concrete transfer mode is choosed
     * based on the target file name.
     *
     * @author Alex Usun
     * @version 1.0
     */
    private class AutoMode
            extends TransferMode {
        /**
         * Creates a new mode instance.
         */
        private AutoMode() {
            super(AUTO);
        }

        /**
         * Transfers data from the input stream to the output stream.
         *
         * @param in         the source input stream
         * @param out        the destination output stream
         * @param remoteFile the file name
         * @param mode       the event mode code
         * @param length     total bytes to transfer
         * @param offset     offset input
         *
         * @return total transferred bytes
         *
         * @throws FtpException if an I/O or FTP error occurs.
         */
        public long transmit(InputStream in, OutputStream out,
                             String remoteFile, int mode, long length, long offset)
                throws FtpException {
            TransferMode m = getModeFor(remoteFile);

            return m.transmit(in, out, remoteFile, mode, length, offset);
        }

        /**
         * Obtains the correct mode for the specified file name.
         *
         * @param fileName the file name
         *
         * @return the mode for this file name
         */
        private TransferMode getModeFor(String fileName) {
            return isTextFile(fileName)
                   ? (TransferMode) new TextMode()
                   : (TransferMode) new BinaryMode();
        }

        /**
         * Checks if the specified file is text file.
         *
         * @param fileName the file name to check
         *
         * @return <code>true</code> if the file is text file;
         *         <code>false</code> otherwise
         */
        private boolean isTextFile(String fileName) {
            javax.activation.MimetypesFileTypeMap map = new javax.activation.MimetypesFileTypeMap();
            String tmpContentType = map.getContentType(fileName);
            try {
                javax.activation.MimeType type = new javax.activation.MimeType(
                        tmpContentType);
                return type.getPrimaryType().equals("text");
            } catch (javax.activation.MimeTypeParseException e) {
                return false;
            }
        }
    } // AutoMode

    /**
     * The binary mode. In this mode data are transferred "as is".
     *
     * @author Alex Usun
     * @version 1.0
     */
    private class BinaryMode
            extends TransferMode {
        /**
         * Creates a new mode instance.
         */
        private BinaryMode() {
            super(BINARY);
        }

        /**
         * Tranfers data from the input stream to the output stream.
         *
         * @param in         the source input stream
         * @param out        the destination output stream
         * @param remoteFile the file name
         * @param mode       the event mode code
         * @param length     total bytes to transfer
         * @param offset     used
         *
         * @return total transferred bytes
         *
         * @throws FtpException if an I/O or FTP error occurs.
         */
        public long transmit(InputStream in, OutputStream out, String remoteFile, int mode, long length, long offset)
                throws FtpException {
            byte[] buf = new byte[Ftps.this.blockSize];
            long sent = 0;

            try {
                String targetPath = "";
                if (Ftps.this.targetFile != null) {
                    targetPath = Ftps.this.targetFile.getAbsolutePath();
                }

                /**
                 * This is done so Secure FTP Applet can know that a file was transferred. For directories the applet will not know
                 * of the transfer until this event is raised.
                 */
                if (length == 0L) {
                    fireEvent(new FtpProgressEvent(Ftps.this,
                                                   remoteFile,
                                                   Ftps.this.path,
                                                   targetPath,
                                                   mode,
                                                   sent + offset,
                                                   0,
                                                   length));
                }

                for (int read = in.read(buf); (read != -1) && !interrupted(); read = in.read(buf)) {
                    out.write(buf, 0, read);
                    out.flush();
                    sent += read;
                    fireEvent(new FtpProgressEvent(Ftps.this,
                                                   remoteFile,
                                                   Ftps.this.path,
                                                   targetPath,
                                                   mode,
                                                   sent + offset,
                                                   read,
                                                   length));
                }

                return sent;
            } catch (IOException e) {
                if (e instanceof javax.net.ssl.SSLHandshakeException) {
                    return 0;
                }

                throw new FtpException(e.getMessage(), e);
            }
        }
    } // BinaryMode

    private static final class DataConnectionInputStream
            extends InputStream {
        private final Ftps impl;
        private final InputStream in;
        private final Socket connection;

        private DataConnectionInputStream(Ftps impl, Socket connection)
                throws IOException {
            this.impl = impl;
            this.connection = connection;
            this.in = this.connection.getInputStream();
        }

        public int available()
                throws IOException {
            return this.in.available();
        }

        public void close()
                throws IOException {
            IOTools.close(in);
            IOTools.close(this.connection);

            try {
                this.impl.readResponse();
            } catch (FtpException e) {
                // ignore
            }
        }

        public void mark(int readlimit) {
            this.in.mark(readlimit);
        }

        public boolean markSupported() {
            return this.in.markSupported();
        }

        public int read()
                throws IOException {
            return this.in.read();
        }

        public int read(byte[] b)
                throws IOException {
            return this.in.read(b);
        }

        public int read(byte[] b, int off, int len)
                throws IOException {
            return this.in.read(b, off, len);
        }

        public void reset()
                throws IOException {
            this.in.reset();
        }

        public long skip(long n)
                throws IOException {
            return this.in.skip(n);
        }
    }

    private static final class DataConnectionOutputStream
            extends OutputStream {
        private final Ftps impl;
        private final OutputStream out;
        private final Socket socket;

        private DataConnectionOutputStream(Ftps impl, Socket socket)
                throws IOException {
            this.impl = impl;
            this.socket = socket;
            this.out = this.socket.getOutputStream();
        }

        public void close()
                throws IOException {
            IOTools.close(this.out);
            IOTools.close(this.socket);

            try {
                this.impl.readResponse();
            } catch (FtpException e) {
                // ignore
            }
        }

        public void flush()
                throws IOException {
            this.out.flush();
        }

        public void write(int b)
                throws IOException {
            this.out.write(b);
        }

        public void write(byte[] b)
                throws IOException {
            this.out.write(b);
        }

        public void write(byte[] b, int off, int len)
                throws IOException {
            this.out.write(b, off, len);
        }
    }

    /**
     * The ordinal FTP connection (without any protection).
     */
    private static class DefaultStrategy
            implements ConnectionStrategy {
        /**
         * Performs user authentication.
         *
         * @param client   the connected FTP client
         * @param user     the FTP user name
         * @param password the FTP user password
         * @param account  the optional FTP account
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        public void authenticate(FtpsClient client, String user,
                                 String password, String account)
                throws FtpException {
            int responseCode = client.userName(user).getCode();

            if (responseCode > Response.POSITIVE_INTERMEDIATE) {
                client.password(password);
            }

            if (account != null) {
                client.account(account);
            }
        }

        /**
         * Creates an appropriate FTP client.
         *
         * @param source               Ftps source instance
         * @param connectionParameters the connection parameters
         * @param sslContext           the SSL context
         * @param enabledCiphers       Ciphers enable
         * @param logger               the logger
         *
         * @return a new FTP client
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        public FtpsClient createClient(Ftps source,
                                       ConnectionParameters connectionParameters,
                                       SSLContext sslContext,
                                       String[] enabledCiphers,
                                       java.util.logging.Logger logger)
                throws FtpException {
            return FtpsClient.openPlain(source,
                                        connectionParameters,
                                        sslContext,
                                        enabledCiphers,
                                        logger);
        }
    } // DefaultStrategy

    /**
     * The "Implicit SSL" FTP connection (port 990).
     */
    private static class ImplicitSslStrategy
            extends DefaultStrategy {
        public void authenticate(FtpsClient client, String user,
                                 String password, String account)
                throws FtpException {
            boolean authorized = true;
            if (client.getFtpsCertificateVerifier() != null) {
                if (!client.getFtpsCertificateVerifier().authorized()) {
                    authorized = false;
                }
            }
            if (authorized) {
                super.authenticate(client, user, password, account);

                try {
                    client.bufferSize();
                    client.protectionLevel(FtpsClient.DATA_PRIVATE);
                } catch (FtpException e) {
                    // ignore
                }
            } else {
                throw new FtpException("Could not authenticate when has not been authorized");
            }
        }

        /**
         * Creates an appropriate FTP client.
         *
         * @param source               Ftps source instance
         * @param connectionParameters the connection parameters
         * @param sslContext           the SSL context
         * @param enabledCiphers       Ciphers enable
         * @param logger               the logger
         *
         * @return a new FTP client
         *
         * @throws FtpException if an I/O or protocol error occurs
         */
        public FtpsClient createClient(Ftps source,
                                       ConnectionParameters connectionParameters,
                                       SSLContext sslContext,
                                       String[] enabledCiphers,
                                       java.util.logging.Logger logger)
                throws FtpException {
            return FtpsClient.openProtected(source,
                                            connectionParameters,
                                            sslContext,
                                            enabledCiphers,
                                            logger);
        }
    } // ImplicitSsl

    /**
     * The text mode. In this mode the line separators are converted
     * from/to local line separators.
     *
     * @author Alex Usun
     * @version 1.0
     */
    private class TextMode
            extends TransferMode {
        /**
         * Creates a new mode instance.
         */
        private TextMode() {
            super(Ftps.ASCII);
        }

        /**
         * Tranfers data from the input stream to the output stream.
         *
         * @param in         the source input stream
         * @param out        the destination output stream
         * @param remoteFile the file name
         * @param mode       the event mode code
         * @param length     total bytes to transfer
         * @param offset     the data offset from input stream
         *
         * @return total transferred bytes
         *
         * @throws FtpException if an I/O or FTP error occurs.
         */
        public long transmit(InputStream in, OutputStream out,
                             String remoteFile, int mode, long length, long offset)
                throws FtpException {
            try {
                // gets local line termination
                String localTerm = System.getProperties().getProperty(
                        "line.separator");
                long sent = 0;

                // stores line termination for input/output streams
                byte[] termIn = null;
                byte[] termOut = null;

                // set line termination for input/output streams
                if (mode == FtpProgressEvent.UPLOAD) {
                    termIn = localTerm.getBytes(StandardCharsets.US_ASCII);
                    termOut = FtpsClient.EOL.getBytes(StandardCharsets.US_ASCII);
                } else {
                    termIn = FtpsClient.EOL.getBytes(StandardCharsets.US_ASCII);
                    termOut = localTerm.getBytes(StandardCharsets.US_ASCII);
                }

                /**
                 * This is done so Secure FTP Applet can know that a file was transferred. For directories the applet will not know
                 * of the transfer until this event is raised.
                 */
                if (length == 0L) {
                    fireEvent(new FtpProgressEvent(Ftps.this,
                                                   remoteFile,
                                                   Ftps.this.path,
                                                   path,
                                                   mode,
                                                   sent + offset,
                                                   0,
                                                   length));
                }

                // captures byte read
                int i = -1;

                // total number of line termination characters matched
                int match = 0;

                // initializes when next progress event should be sent
                long nextProgressEvent = sent + blockSize;

                // temporary buffer for sending data
                ByteArrayOutputStream bout = new ByteArrayOutputStream();

                String targetPath = "";
                if (Ftps.this.targetFile != null) {
                    targetPath = Ftps.this.targetFile.getAbsolutePath();
                }

                // reads data from input stream and writes to output stream
                while (((i = in.read()) != -1) && !interrupted) {
                    // check if byte is equal to next line terminator byte
                    if (i == termIn[match]) {
                        match++;

                        // check if terminator found, write bytes and reset
                        if (match == termIn.length) {
                            bout.write(termOut);
                            sent += termOut.length;
                            match = 0;
                        }
                    } else {
                        // if match index is > 0, write bytes and reset
                        if (match > 0) {
                            bout.write(termIn, 0, match);
                            sent += match;
                            match = 0;
                        } else {
                            if (mode == FtpProgressEvent.UPLOAD) {
                                i = translateBytes(i, getDiskEncoding(),
                                                   getWireEncoding());
                            } else {
                                i = translateBytes(i, getWireEncoding(),
                                                   getDiskEncoding());
                            }
                        }

                        bout.write(i);
                        sent++;
                    }

                    // fire event if listeners registered
                    if (sent >= nextProgressEvent) {
                        byte[] bytes = bout.toByteArray();
                        out.write(bout.toByteArray());
                        out.flush();
                        bout.reset();

                        if (hasListeners()) {
                            fireEvent(new FtpProgressEvent(this,
                                                           remoteFile,
                                                           path,
                                                           targetPath,
                                                           mode,
                                                           sent + offset,
                                                           bytes.length,
                                                           length));
                            nextProgressEvent = (sent + blockSize);
                        }
                    }
                }

                byte[] remaining = bout.toByteArray();

                if ((remaining != null) && (remaining.length > 0)) {
                    out.write(remaining);
                    bout.reset();
                }

                out.flush();

                // fire event for remainder data
                fireEvent(new FtpProgressEvent(this,
                                               remoteFile,
                                               remoteFile,
                                               targetPath,
                                               mode,
                                               sent + offset,
                                               remaining.length,
                                               length));

                // return total bytes sent
                return sent;
            } catch (IOException ioe) {
                if (ioe instanceof javax.net.ssl.SSLHandshakeException) {
                    return 0;
                }

                throw new FtpException(ioe.getMessage(), ioe);
            }
        }
    } // TextMode

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

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

    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);
        }
    }
}
