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

import com.jscape.inet.ftp.*;
import com.jscape.inet.ftps.Ftps;
import com.jscape.inet.ftps.FtpsCertificateVerifier;

import javax.net.ssl.HandshakeCompletedEvent;
import javax.net.ssl.HandshakeCompletedListener;
import java.io.*;
import java.security.KeyStore;
import java.text.ParseException;
import java.util.*;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicReference;

import static com.jscape.filetransfer.FileTransferException.wrap;
import static com.jscape.util.Parameters.assertNotNull;

/**
 * Implements the basic functionality of a FTPS client using explicit SSL.
 * <p/>
 * <p/> Example Usage:
 * <p/>
 * <p/>
 * <p/>
 * <pre>
 *  // create new FtpsTransfer instance
 *  FileTransfer ftp = new FtpsTransfer(&quot;ftp.myserver.com&quot;,&quot;anonymous&quot;,&quot;user@myserver.com&quot;);
 *  try {
 *  // connect to FTP server
 *  ftp.connect();
 *  &lt;p/&gt;
 *  // print out directory listing
 *  System.out.println(ftp.getDirListingAsString());
 *  &lt;p/&gt;
 *  // disconnect from FTP server
 *  ftp.disconnect();
 *  } catch(Exception e) {
 *  System.out.println(e);
 *  }
 * </pre>
 */
public class FtpsTransfer
        implements FileTransfer {

    protected final Ftps ftps;
    protected final Set<FileTransferListener> listeners;
    protected boolean mlsdSupported;
    protected boolean useMLSDListing;
    protected boolean useExtendedDirectoryListing;
    private boolean checksumVerificationRequired;
    private final FtpEventConverter eventConverter;

    public FtpsTransfer(String host, int port, String user, String password, File localDirectory, Set<FileTransferListener> listeners) {
        this.ftps = new Ftps(host, user, password, port);

        this.listeners = new CopyOnWriteArraySet<>(listeners);

        this.useMLSDListing = true;
        this.eventConverter = new FtpEventConverter();

        this.ftps.setLocalDir(localDirectory);
        this.ftps.addFtpListener(this.eventConverter);
        this.ftps.setSslHandshakeCompletedListener(new SslHandshakeEventConverter());
    }

    /**
     * Creates FtpsTransfer 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 FtpsTransfer(String host, String user, String pwd, File localDirectory) {
        this(host, 21, user, pwd, localDirectory, Collections.emptySet());
    }

    /**
     * Creates FtpsTransfer 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 FtpsTransfer(String host, String user, String pwd, int port) {
        this(host, port, user, pwd, new File("."), Collections.emptySet());
    }

    /**
     * Method FtpsTransfer
     *
     * @param host
     * @param user
     * @param pwd
     * @param port
     */
    public FtpsTransfer(String host, String user, String pwd, Integer port) {
        this(host, user, pwd, port.intValue());
    }

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

    /**
     * Creates FtpsTransfer instance.
     */
    public FtpsTransfer() {
        this("", "", "");
    }

    /**
     * Set certificate verifier handle acceptance of SSL certificate.
     *
     * @param listener a <code>FtpsCertificateVerifier</code>
     *
     * @see com.jscape.inet.ftps.FtpsCertificateVerifier
     */
    public void addFtpsCertificateVerifier(FtpsCertificateVerifier listener) {
        this.ftps.setFtpsCertificateVerifier(listener);
    }

    /**
     * {@inheritDoc}
     */
    public FtpsTransfer connect()
            throws FileTransferException {
        try {
            this.ftps.connect();
            this.mlsdSupported = ftps.isFeatureSupported("MLSD");

            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Establishes a connection with FTP server with optional login
     *
     * @param login true to login, false otherwise
     *
     * @throws FileTransferException
     */
    public void connect(boolean login)
            throws FileTransferException {
        try {
            ftps.connect(login);
            this.mlsdSupported = ftps.isFeatureSupported("MLSD");
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setPreserveFileUploadTimestamp(boolean value) {
        this.ftps.setPreserveUploadTimestamp(value);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setPreserveFileDownloadTimestamp(boolean value) {
        this.ftps.setPreserveDownloadTimestamp(value);
        return this;
    }

    @Override
    public boolean isChecksumVerificationRequired() {
        return this.checksumVerificationRequired;
    }

    @Override
    public FileTransfer setChecksumVerificationRequired(boolean value) {
        this.checksumVerificationRequired = value;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setTimeZone(TimeZone timeZone) {
        this.ftps.setTimezone(timeZone);
        return this;
    }

    /**
     * Clears command channel.
     *
     * @throws FileTransferException
     */
    public void clearCommandChannel()
            throws FileTransferException {
        try {
            ftps.clearCommandChannel();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Sets whether the data will be compressed during the transfering.
     *
     * @param compression When it is true the data will be compressed before it is
     *                    sent between the client and the server.
     */
    public FtpsTransfer setCompression(boolean compression) {
        this.ftps.setCompression(compression);
        return this;
    }

    /**
     * Logs into FTP server after establishing a connection. This method should
     * only be used if login is not performed automatically during connect.
     *
     * @throws FileTransferException if an I/O or FTP error occurs
     */
    public void login()
            throws FileTransferException {
        try {
            ftps.login();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Issues command to the server.
     *
     * @param command a command
     *
     * @throws FileTransferException if an error occurs
     */
    public void issueCommand(String command)
            throws FileTransferException {
        try {
            this.ftps.issueCommand(command);
        } catch (Exception e) {
            throw wrap(e);
        }
    }


    /**
     * Changes the password.
     *
     * @param oldPassword The old password.
     * @param newPassword The new password.
     *
     * @throws FileTransferException FileTransferException if underlayed I/O or FTP error occurs.
     */
    public void changePassword(String oldPassword, String newPassword)
            throws FileTransferException {
        try {
            ftps.changePassword(oldPassword, newPassword);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteDir(String name)
            throws FileTransferException {
        try {
            ftps.deleteDir(name);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteDir(String name, boolean recursive)
            throws FileTransferException {
        try {
            ftps.deleteDir(name, recursive);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    @Override
    public boolean exists(String path)
            throws FileTransferException {
        try {
            getFilesize(path);
            return true;
        } catch (Exception e1) {
            try {
                getFileTimestamp(path);
                return true;
            } catch (Exception e2) {
                return isDirectory(path);
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public void deleteFile(String remoteFile)
            throws FileTransferException {
        try {
            ftps.deleteFile(remoteFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void disconnect()
            throws FileTransferException {
        try {
            ftps.disconnect();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    @Override
    public void close() {
        try {
            disconnect();
        } catch (Exception e) {
            // ignore
        }
    }

    /**
     * {@inheritDoc}
     */
    public void download(OutputStream out, String remoteFile)
            throws FileTransferException {
        try {
            ftps.download(out, remoteFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public File download(String remoteFile)
            throws FileTransferException {
        try {
            File file = ftps.download(remoteFile);
            safeCheckChecksum(file, remoteFile);
            this.eventConverter.raiseDownloadEvent();
            return file;
        } catch (Exception e) {
//            try {
//                raiseErrorEvent(new FileTransferErrorEvent(this,
//                                                           remoteFile,
//                                                           remoteDir,
//                                                           ftps.getLocalDir() + File.separator + remoteFile,
//                                                           FileTransferProgressEvent.DOWNLOAD));
//            } catch (Exception x) {
//                // todo: ignore?
//            }

            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public File download(String localFile, String remoteFile)
            throws FileTransferException {
        try {
            File file = ftps.download(localFile, remoteFile);
            safeCheckChecksum(file, remoteFile);
            this.eventConverter.raiseDownloadEvent();
            return file;
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void downloadDir(String remoteDir)
            throws FileTransferException {
        try {
            ftps.downloadDir(remoteDir);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void downloadDir(String remoteDir, int retryLimit,
                            boolean performChecksum)
            throws FileTransferException {
        try {
            this.ftps.downloadDir(remoteDir, retryLimit, performChecksum);

        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean getDebug() {
        return ftps.getDebug();
    }

    /**
     * {@inheritDoc}
     */
    public PrintStream getDebugStream() {
        return ftps.getDebugStream();
    }

    /**
     * {@inheritDoc}
     */
    public String getDir()
            throws FileTransferException {
        try {
            return ftps.getDir();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Get MLSD directory listing.
     *
     * @return boolean value
     */
    public boolean getPerformMLSDListing() {
        return this.useMLSDListing;
    }

    /**
     * Set MLSD directory listing.
     *
     * @param mlsdListing - boolean value
     */
    public FtpsTransfer setPerformMLSDListing(boolean mlsdListing) {
        this.useMLSDListing = mlsdListing;
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public Enumeration<FileTransferRemoteFile> getDirListing()
            throws FileTransferException {
        try {
            Enumeration entries = null;
            FtpFileParser oldParser = null;

            if (this.mlsdSupported && this.useMLSDListing) {
                entries = ftps.getMachineDirListing("");
                oldParser = ftps.getFtpFileParser();
                ftps.setFtpFileParser(new MLSDParser());
            } else {
                if (useExtendedDirectoryListing) {
                    entries = ftps.getDirListing("-a");
                } else {
                    entries = ftps.getDirListing();
                }
            }

            Vector results = createRemoteFileElements(entries);
            if (oldParser != null) {
                ftps.setFtpFileParser(oldParser);
            }
            return results.elements();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("unchecked")
    public Enumeration<FileTransferRemoteFile> getDirListing(String filter)
            throws FileTransferException {
        try {
            Enumeration entries = null;
            FtpFileParser oldParser = null;
            if (mlsdSupported) {
                entries = ftps.getMachineDirListing("", filter);
                oldParser = ftps.getFtpFileParser();
                ftps.setFtpFileParser(new MLSDParser());
            } else {
                entries = ftps.getDirListingRegex(filter);
            }

            Vector results = createRemoteFileElements(entries);

            if (oldParser != null) {
                ftps.setFtpFileParser(oldParser);
            }
            return results.elements();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getDirListingAsString()
            throws FileTransferException {
        try {
            return ftps.getDirListingAsString();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getDirListingAsString(String filter)
            throws FileTransferException {
        try {
            return ftps.getDirListingRegexAsString(filter);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    @Override
    public boolean isDirectory(String remotePath)
            throws FileTransferException {
        String currentDir = getDir();
        try {
            setDir(remotePath);
            return true;
        } catch (Exception e) {
            return false;
        } finally {
            try {
                setDir(currentDir);
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    public long getFilesize(String remoteFile)
            throws FileTransferException {
        try {
            return ftps.getFilesize(remoteFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Date getFileTimestamp(String remoteFile)
            throws FileTransferException {
        try {
            return ftps.getFileTimestamp(remoteFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setFileTimestamp(String remoteFile, Date timestamp)
            throws FileTransferException {
        try {
            ftps.setFileTimestamp(remoteFile, timestamp);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setFileModificationTime(String remoteFile, Date dateFile)
            throws FileTransferException {
        try {
            ftps.setFileModificationTime(remoteFile, dateFile);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean sameChecksum(File localFile, String remoteFile)
            throws FileTransferException {
        try {
            return this.ftps.checksum(localFile, remoteFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String getHostname() {
        return ftps.getHostname();
    }

    /**
     * {@inheritDoc}
     */
    public File getLocalDir() {
        return ftps.getLocalDir();
    }

    /**
     * {@inheritDoc}
     */
    public Enumeration getLocalDirListing() {
        return ftps.getLocalDirListing();
    }

    /**
     * {@inheritDoc}
     */
    public int getMode() {
        return ftps.getMode();
    }

    /**
     * {@inheritDoc}
     */
    public Enumeration getNameListing()
            throws FileTransferException {
        try {
            return ftps.getNameListing();
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Enumeration getNameListing(String filter)
            throws FileTransferException {
        try {
            return ftps.getNameListing(filter);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public boolean getPassive() {
        return ftps.getPassive();
    }

    /**
     * {@inheritDoc}
     */
    public String getPassword() {
        return ftps.getPassword();
    }

    /**
     * {@inheritDoc}
     */
    public int getPort() {
        return ftps.getPort();
    }

    /**
     * {@inheritDoc}
     */
    public long getTimeout() {
        return ftps.getTimeout();
    }

    /**
     * {@inheritDoc}
     */
    public String getUsername() {
        return ftps.getUsername();
    }

    /**
     * See FileTransfer interface for method description.
     *
     * @see FileTransfer#interrupt()
     */
    public void interrupt() {
        ftps.interrupt();
    }

    /**
     * {@inheritDoc}
     */
    public boolean interrupted() {
        return ftps.interrupted();
    }

    /**
     * {@inheritDoc}
     */
    public boolean isConnected() {
        return ftps.isConnected();
    }

    /**
     * {@inheritDoc}
     */
    public void makeDir(String name)
            throws FileTransferException {
        try {
            ftps.makeDir(name);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void makeDirRecursive(String name)
            throws FileTransferException {
        try {
            ftps.makeDirRecursive(name);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void mdelete(String filter)
            throws FileTransferException {
        try {
            ftps.mdelete(filter);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public File makeLocalDir(String name) {
        return ftps.makeLocalDir(name);
    }

    /**
     * {@inheritDoc}
     */
    public void mdownload(Enumeration fileNames)
            throws FileTransferException {
        try {
            ftps.mdownload(fileNames);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void mdownload(String filter)
            throws FileTransferException {
        try {
            ftps.mdownload(filter);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void mupload(Enumeration fileNames)
            throws FileTransferException {
        try {
            ftps.mupload(fileNames);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void mupload(String filter)
            throws FileTransferException {
        try {
            ftps.mupload(filter);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void renameFile(String remoteFile, String newFile)
            throws FileTransferException {
        try {
            ftps.renameFile(remoteFile, newFile);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void reset() {
        ftps.reset();
    }

    /**
     * {@inheritDoc}
     */
    public void resumeDownload(String remoteFile, long offset)
            throws FileTransferException {
        try {
            File file = ftps.resumeDownload(remoteFile, offset);
            safeCheckChecksum(file, remoteFile);
            this.eventConverter.raiseDownloadEvent();
        } catch (Exception e) {
//            raiseErrorEvent(new FileTransferErrorEvent(this,
//                                                       remoteFile,
//                                                       remoteDir,
//                                                       ftps.getLocalDir() + File.separator + remoteFile,
//                                                       FileTransferProgressEvent.DOWNLOAD));

            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void resumeDownload(String localFile, String remoteFile, long offset)
            throws FileTransferException {
        try {
            File file = ftps.resumeDownload(localFile, remoteFile, offset);
            safeCheckChecksum(file, remoteFile);
            this.eventConverter.raiseDownloadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void resumeUpload(File file, long offset)
            throws FileTransferException {
        try {
            ftps.resumeUpload(file, offset);
            safeCheckChecksum(file, file.getName());
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void resumeUpload(String file, long offset)
            throws FileTransferException {
        try {
            ftps.resumeUpload(file, offset);
            safeCheckChecksum(new File(getLocalDir(), file), file);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }

    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setAscii() {
        try {
            ftps.setAscii();
            return this;
        } catch (FtpException fex) {
            return this;
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setAuto(boolean auto) {
        try {
            ftps.setAuto(auto);
            return this;
        } catch (FtpException ex) {
            return this;
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setBinary() {
        try {
            ftps.setBinary();
            return this;
        } catch (FtpException fex) {
            return this;
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setDebug(boolean debug) {
        ftps.setDebug(debug);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setDebugStream(PrintStream ps) {
        ftps.setDebugStream(ps);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setDir(String name)
            throws FileTransferException {
        try {
            ftps.setDir(name);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setDirUp()
            throws FileTransferException {
        try {
            ftps.setDirUp();
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Sets the indicator value which will be used by methods that invokes
     * getFileSize.
     *
     * @param error Error indicator value
     */
    public FtpsTransfer setErrorOnSizeCommand(boolean error) {
        this.ftps.setErrorOnSizeCommand(error);
        return this;
    }

    /**
     * Gets the error on size command indicator.
     *
     * @return <code>true</code> then method which invokes getFileSize will throw
     *         an exception when it caused an error, <code>false</code> the error will be ignored.
     */
    public boolean getErrorOnSizeCommand() {
        return this.ftps.getErrorOnSizeCommand();
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setHostname(String host) {
        ftps.setHostname(host);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setPort(int port) {
        ftps.setPort(port);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setLocalDir(File dir) {
        ftps.setLocalDir(dir);
        return this;
    }
    
    public boolean getTcpNoDelay() {
        return this.ftps.getTcpNoDelay();
    }
    
    public FtpsTransfer setTcpNoDelay(boolean value) {
        this.ftps.setTcpNoDelay(value);
        return this;
    }
    
    /**
     * Sets a NAT address which will be used after PASV command.
     *
     * @param ipAddress The NAT IP address.
     *
     * @throws FtpException If an error occurs.
     */
    public FtpsTransfer setNATAddress(String ipAddress)
            throws FileTransferException {
        try {
            this.ftps.setNATAddress(ipAddress);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setPassive(boolean passive) {
        ftps.setPassive(passive);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setPassword(String password) {
        ftps.setPassword(password);
        return this;
    }

    /**
     * Sets optional client certificate keystore for use during authentication.
     *
     * @param keystore the client keystore
     * @param password the keystore password
     *
     * @throws FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setClientCertificates(KeyStore keystore, String password)
            throws FileTransferException {
        try {
            ftps.setClientCertificates(keystore, password);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }


    /**
     * 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/>
     * <p/>
     * <pre>
     *  keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * <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 FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setClientCertificates(String filename, String password)
            throws FileTransferException {
        try {
            ftps.setClientCertificates(filename, password);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * 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/>
     * <p/>
     * <pre>
     *  keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * <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 FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setClientCertificates(String filename, String password, String storetype)
            throws FileTransferException {
        try {
            ftps.setClientCertificates(filename, password, storetype);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * Sets optional server certificate keystore to use during authentication.
     *
     * @param keystore the keystore
     *
     * @throws FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setServerCertificates(KeyStore keystore)
            throws FileTransferException {
        try {
            ftps.setServerCertificates(keystore);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * 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/>
     * <p/>
     * <pre>
     *  keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * <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 FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setServerCertificates(String filename, String password)
            throws FileTransferException {
        try {
            ftps.setServerCertificates(filename, password);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * 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/>
     * <p/>
     * <pre>
     *  keytool.exe -import -file x509.cer -keystore your_store_name
     * </pre>
     * <p/>
     * <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 FileTransferException if an I/O error occurs
     */
    public FtpsTransfer setServerCertificates(String filename, String password, String storetype)
            throws FileTransferException {
        try {
            ftps.setServerCertificates(filename, password, storetype);
            return this;
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setTimeout(long timeout) {
        ftps.setTimeout((int) timeout);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setUsername(String username) {
        ftps.setUsername(username);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FtpsTransfer setUseEPSV(boolean useEPSV) {
        ftps.setUseEPSV(useEPSV);
        return this;
    }

    /**
     * Set use EPRT.
     *
     * @param useEPRT
     */
    public FtpsTransfer setUseEPRT(boolean useEPRT) {
        ftps.setUseEPRT(useEPRT);
        return this;
    }

    /**
     * Set auto detect Ipv6.
     *
     * @param value
     */
    public FtpsTransfer setAutoDetectIpv6(boolean value) {
        ftps.setAutoDetectIpv6(value);
        return this;
    }

    /**
     * Sets the use extended directory listing field.
     *
     * @param useExtendedDirectoryListing The use extended directory listing indicator.
     */
    public FtpsTransfer setUseExtendedDirectoryListing(boolean useExtendedDirectoryListing) {
        this.useExtendedDirectoryListing = useExtendedDirectoryListing;
        return this;
    }

    /**
     * Gets use EPSV value.
     *
     * @return <code>true</code> the passive mode will be sent EPSV command
     */
    public boolean getUseEPSV() {
        return this.ftps.getUseEPSV();
    }

    /**
     * Gets use EPRT value.
     *
     * @return <code>true</code> active mode will be sent EPRT command
     */
    public boolean getUseEPRT() {
        return this.ftps.getUseEPRT();
    }

    /**
     * Get auto detect Ipv6.
     *
     * @return <code>true</code> if auto-detect Ipv6 is set to true
     */
    public boolean getAutoDetectIpv6() {
        return this.ftps.getAutoDetectIpv6();
    }

    /**
     * Sets the connectBefore property value
     *
     * @param before indicates if the connection will be established before send
     *               the command in passive mode
     */
    public FtpsTransfer setConnectBeforeCommand(boolean before) {
        this.ftps.setConnectBeforeCommand(before);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setWireEncoding(String encoding) {
        this.ftps.setWireEncoding(encoding);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public String getWireEncoding() {
        return this.ftps.getWireEncoding();
    }

    /**
     * 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 {
        return this.ftps.getRemoteFileChecksum(remoteFile);
    }

    /**
     * Returns local checksum.
     *
     * @param localFile File which will be calculate the checksum
     *
     * @return checksum Compute value
     *
     * @throws FtpException if an error occurs
     */
    public String getLocalChecksum(File localFile)
            throws FtpException {
        return this.ftps.getLocalChecksum(localFile);
    }

    /**
     * {@inheritDoc}
     */
    public void upload(byte[] data, String name)
            throws FileTransferException {
        try {
            ftps.upload(data, name);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(byte[] data, String name, boolean append)
            throws FileTransferException {
        try {
            ftps.upload(data, name, append);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(File file)
            throws FileTransferException {
        String remoteDir = null;

        try {
            remoteDir = ftps.getDir(); //we save it now for error event
            ftps.upload(file);
            safeCheckChecksum(file, file.getName());
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            raiseErrorEvent(new FileTransferErrorEvent(this,
                                                       file.getName(),
                                                       remoteDir,
                                                       file.getPath(),
                                                       FileTransferProgressEvent.UPLOAD));

            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(File file, boolean append)
            throws FileTransferException {
        try {
            ftps.upload(file, append);
            safeCheckChecksum(file, file.getName());
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(String extension, File file)
            throws FileTransferException {
        String remoteDir = null;

        try {
            remoteDir = ftps.getDir(); //we save it now for error event
            ftps.upload(extension, file);
        } catch (FtpException e) {
            raiseErrorEvent(new FileTransferErrorEvent(this,
                                                       file.getName(),
                                                       remoteDir,
                                                       file.getPath(),
                                                       FileTransferProgressEvent.UPLOAD));

            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(File file, String remoteName)
            throws FileTransferException {
        try {
            ftps.upload(file, remoteName);
            safeCheckChecksum(file, remoteName);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(File file, String remoteName, boolean append)
            throws FileTransferException {
        try {
            ftps.upload(file, remoteName, append);
            safeCheckChecksum(file, remoteName);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(String name)
            throws FileTransferException {
        try {
            ftps.upload(name);
            safeCheckChecksum(new File(getLocalDir(), name), name);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(String name, boolean append)
            throws FileTransferException {
        try {
            ftps.upload(name, append);
            safeCheckChecksum(new File(getLocalDir(), name), name);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(String name, String remoteName)
            throws FileTransferException {
        try {
            ftps.upload(name, remoteName);
            safeCheckChecksum(new File(getLocalDir(), name), remoteName);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void upload(String name, String remoteName, boolean append)
            throws FileTransferException {
        try {
            ftps.upload(name, remoteName, append);
            safeCheckChecksum(new File(getLocalDir(), name), remoteName);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String uploadUnique(String fileName)
            throws FileTransferException {
        try {
            return this.ftps.uploadUnique(fileName);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String uploadUnique(File file)
            throws FileTransferException {
        try {
            return this.ftps.uploadUnique(file);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public String uploadUnique(InputStream in, String remoteFilePrefix)
            throws FileTransferException {
        try {
            return this.ftps.uploadUnique(in, remoteFilePrefix);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void uploadDir(File localDir)
            throws FileTransferException {
        try {
            ftps.uploadDir(localDir);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void uploadDir(File localDir, String extension)
            throws FileTransferException {
        try {
            ftps.uploadDir(localDir, extension);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void uploadDir(File localDir, int retryLimit,
                          boolean performChecksum, String extension)
            throws FileTransferException {
        try {
            this.ftps.uploadDir(localDir, retryLimit, performChecksum, extension);

        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void addFileTransferListener(FileTransferListener listener) {
        assertNotNull(listener);
        this.listeners.add(listener);
    }

    /**
     * {@inheritDoc}
     */
    public void removeFileTransferListener(FileTransferListener listener) {
        this.listeners.remove(listener);
    }

    /**
     * {@inheritDoc}
     */
    public InputStream getInputStream(String remoteFile, long pos)
            throws FileTransferException {
        try {
            return this.ftps.getInputStream(remoteFile, pos);
        } catch (IOException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Object getImplementation() {
        return this.ftps;
    }

    /**
     * {@inheritDoc}
     */
    public OutputStream getOutputStream(String fileName, long off)
            throws FileTransferException {
        try {
            return this.ftps.getOutputStream(fileName, off, false);
        } catch (IOException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer copy() {
        FtpsTransfer transfer = new FtpsTransfer(this.ftps.getHostname(),
                                                 this.ftps.getPort(),
                                                 this.ftps.getUsername(),
                                                 this.ftps.getPassword(),
                                                 this.ftps.getLocalDir(),
                                                 this.listeners);
        transfer.setDebugStream(getDebugStream());
        transfer.setDebug(getDebug());

        return transfer;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setBlockTransferSize(int blocksize) {
        this.ftps.setBlockTransferSize(blocksize);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public int getBlockTransferSize() {
        return this.ftps.getBlockTransferSize();
    }

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

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

    /**
     * {@inheritDoc}
     */
    public FileTransfer setProxyAuthentication(String proxyUsername, String proxyPassword) {
        this.ftps.setProxyAuthentication(proxyUsername, proxyPassword);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setProxyHost(String proxyHostname, int proxyPort) {
        this.ftps.setProxyHost(proxyHostname, proxyPort);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public FileTransfer setProxyType(String proxyType) {
        this.ftps.setProxyType(proxyType);
        return this;
    }

    /**
     * {@inheritDoc}
     */
    public void clearProxySettings() {
        this.ftps.clearProxySettings();
    }

    public boolean isFeatureSupported(String command)
            throws FileTransferException {
        try {
            return ftps.isFeatureSupported(command);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    public void checksum(File localFile, String remoteFile)
            throws FileTransferException {
        try {
            this.ftps.checksum(localFile, remoteFile);
        } catch (Exception e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void abortDownloadThread(String remoteFile) {
        this.ftps.abortDownloadThread(remoteFile);
    }

    /**
     * {@inheritDoc}
     */
    public void abortDownloadThreads() {
        this.ftps.abortDownloadThreads();
    }

    /**
     * {@inheritDoc}
     */
    public void abortUploadThread(String remoteFile) {
        this.ftps.abortUploadThread(remoteFile);
    }

    /**
     * {@inheritDoc}
     */
    public void abortUploadThreads() {
        this.ftps.abortUploadThreads();
    }

    /**
     * {@inheritDoc}
     */
    public void downloadDir(String remoteDir, int retryLimit, boolean performChecksum, int threadCount)
            throws FileTransferException {
        try {
            this.ftps.downloadDir(remoteDir, retryLimit, performChecksum, threadCount);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public Vector<FileTransferRemoteFile> getRemoteFileList(String remoteDirectory)
            throws FileTransferException {
        return new Vector<>(new RecursiveFileListOperation(remoteDirectory).applyTo(this).files());
    }

    /**
     * {@inheritDoc}
     */
    public int getRecursiveDirectoryFileCount(String directory) {
        return this.ftps.getRecursiveDirectoryFileCount(directory);
    }

    /**
     * {@inheritDoc}
     */
    public long getRecursiveDirectorySize(String directory) {
        return this.ftps.getRecursiveDirectorySize(directory);
    }

    /**
     * {@inheritDoc}
     */
    public void uploadDir(File localDir, int retryLimit,
                          boolean performChecksum, String extension, int threadCount)
            throws FileTransferException {
        try {
            this.ftps.uploadDir(localDir, retryLimit, performChecksum, extension, threadCount);
        } catch (FtpException e) {
            throw wrap(e);
        }
    }

    /**
     * {@inheritDoc}
     */
    public void resumeUpload(File localFile, String remoteFile, long localFileOffset)
            throws FileTransferException {
        String remoteDir = null;

        try {
            remoteDir = this.ftps.getDir();
            this.ftps.resumeUpload(localFile, remoteFile, localFileOffset);
            safeCheckChecksum(localFile, remoteFile);
            this.eventConverter.raiseUploadEvent();
        } catch (Exception e) {
            raiseErrorEvent(new FileTransferErrorEvent(this,
                                                       localFile.getName(),
                                                       remoteDir,
                                                       localFile.getPath(),
                                                       FileTransferProgressEvent.UPLOAD));

            throw wrap(e);
        }
    }

    @Override
    public void resumeDownload(OutputStream out, String remoteFile, long remoteFileOffset)
            throws FileTransferException {
        // todo: implement
    }

    @Override
    public void upload(InputStream in, long length, String remoteFile, boolean shouldAppend)
            throws FileTransferException {
        // todo: implement
    }

    @Override
    public void resumeUpload(InputStream in, long length, String remoteFile, long remoteFileOffset)
            throws FileTransferException {
        // todo: implement
    }

    protected void raiseEvent(FileTransferEvent event) {
        for (FileTransferListener listener : this.listeners) {
            event.accept(listener);
        }
    }

    protected void raiseErrorEvent(FileTransferErrorEvent event) {
        for (FileTransferListener listener : this.listeners) {
            if (listener instanceof FileTransferErrorListener) {
                ((FileTransferErrorListener) listener).error(event);
            }
        }
    }

    protected Vector<FileTransferRemoteFile> createRemoteFileElements(Enumeration entries)
            throws FileTransferException {
        Vector<FileTransferRemoteFile> results = new Vector<FileTransferRemoteFile>();

        try {
            while (entries.hasMoreElements()) {
                FtpFile file = (FtpFile) entries.nextElement();

                if (".".equals(file.getFilename()) || "..".equals(file.getFilename())) {
                    continue;
                }

                FileTransferRemoteFile rf = new FileTransferRemoteFile();
                rf.setFilename(file.getFilename());
                rf.setDirectory(file.isDirectory());
                rf.setLink(file.isLink());
                rf.setFilesize(file.getFilesize());
                rf.setLinkTarget(file.getLinkTarget());
                Date fileDate = this.ftps.getFtpFileParser().getDateTime(file);

                rf.setFileDate(fileDate);
                results.addElement(rf);
            }

            return results;
        } catch (ParseException e) {
            throw wrap(e);
        }
    }

    protected void safeCheckChecksum(File localFile, String remoteFile)
            throws Exception {
        if (!this.checksumVerificationRequired) {
            return;
        }

        try {
            boolean sameChecksum = sameChecksum(localFile, remoteFile);

            if (!sameChecksum) {
                throw new Exception(String.format("File checksum mismatch for local file '%s' and remote file '%s'.", localFile, remoteFile));
            }
        } catch (FileTransferException e) {
            // todo: log
        }
    }

    protected class FtpEventConverter
            implements FtpErrorListener {

        private final AtomicReference<FtpDownloadEvent> downloadEvent;
        private final AtomicReference<FtpUploadEvent> uploadEvent;

        protected FtpEventConverter() {
            this.downloadEvent = new AtomicReference<>();
            this.uploadEvent = new AtomicReference<>();
        }

        /**
         * {@inheritDoc}
         */
        public void changeDir(FtpChangeDirEvent event) {
            raiseEvent(new FileTransferChangeDirEvent(FtpsTransfer.this, event.getDirectory()));
        }

        /**
         * {@inheritDoc}
         */
        public void deleteFile(FtpDeleteFileEvent event) {
            raiseEvent(new FileTransferDeleteFileEvent(FtpsTransfer.this, event.getFile(), event.getPath()));
        }

        /**
         * {@inheritDoc}
         */
        public void deleteDir(FtpDeleteDirEvent event) {
            raiseEvent(new FileTransferDeleteDirEvent(FtpsTransfer.this, event.getDirectory(), event.getPath()));
        }

        /**
         * {@inheritDoc}
         */
        public void renameFile(FtpRenameFileEvent event) {
            raiseEvent(new FileTransferRenameFileEvent(FtpsTransfer.this,
                                                       event.getOldName(),
                                                       event.getNewName(),
                                                       event.getPath()));
        }

        /**
         * {@inheritDoc}
         */
        public void createDir(FtpCreateDirEvent event) {
            raiseEvent(new FileTransferCreateDirEvent(FtpsTransfer.this, event.getDirectory(), event.getPath()));
        }

        /**
         * {@inheritDoc}
         */
        public void commandSent(FtpCommandEvent event) {
            raiseEvent(new FileTransferCommandEvent(FtpsTransfer.this, event.getCommand()));
        }

        /**
         * {@inheritDoc}
         */
        public void connected(FtpConnectedEvent event) {
            raiseEvent(new FileTransferConnectedEvent(FtpsTransfer.this, event.getHostname()));
        }

        /**
         * {@inheritDoc}
         */
        public void connectionLost(FtpConnectionLostEvent event) {
            // empty
        }

        /**
         * {@inheritDoc}
         */
        public void disconnected(FtpDisconnectedEvent event) {
            raiseEvent(new FileTransferDisconnectedEvent(FtpsTransfer.this, event.getHostname()));
        }

        /**
         * {@inheritDoc}
         */
        public void download(FtpDownloadEvent event) {
            this.downloadEvent.set(event);
        }

        /**
         * {@inheritDoc}
         */
        public void listing(FtpListingEvent event) {
            // empty
        }

        /**
         * {@inheritDoc}
         */
        public void progress(FtpProgressEvent event) {
            raiseEvent(new FileTransferProgressEvent(FtpsTransfer.this,
                                                     event.getFilename(),
                                                     event.getAbsolutePath(),
                                                     event.getLocalFilePath(),
                                                     event.getMode(),
                                                     event.getBytes(),
                                                     event.getReadBytes(),
                                                     event.getTotalBytes()));
        }

        /**
         * {@inheritDoc}
         */
        public void responseReceived(FtpResponseEvent event) {
            raiseEvent(new FileTransferResponseEvent(FtpsTransfer.this, event.getResponse()));
        }

        /**
         * {@inheritDoc}
         */
        public void upload(FtpUploadEvent event) {
            this.uploadEvent.set(event);
        }

        public void error(FtpErrorEvent event) {
            raiseErrorEvent(new FileTransferErrorEvent(FtpsTransfer.this,
                                                       event.getFilename(),
                                                       event.getPath(),
                                                       event.getLocalPath(),
                                                       event.getMode() == FtpProgressEvent.UPLOAD ? FileTransferProgressEvent.UPLOAD : FileTransferProgressEvent.DOWNLOAD));
        }

        public void raiseDownloadEvent() {
            FtpDownloadEvent event = this.downloadEvent.getAndSet(null);

            if (event != null) {
                raiseEvent(new FileTransferDownloadEvent(FtpsTransfer.this,
                                                         event.getFilename(),
                                                         event.getPath(),
                                                         event.getLocalPath(),
                                                         event.getSize(),
                                                         event.getTime(),
                                                         false));
            }
        }

        public void raiseUploadEvent() {
            FtpUploadEvent event = this.uploadEvent.getAndSet(null);

            if (event != null) {
                raiseEvent(new FileTransferUploadEvent(FtpsTransfer.this,
                                                       event.getFilename(),
                                                       event.getPath(),
                                                       event.getLocalPath(),
                                                       event.getSize(),
                                                       event.getTime()));
            }
        }
    }

    protected class SslHandshakeEventConverter
            implements HandshakeCompletedListener {

        public void handshakeCompleted(HandshakeCompletedEvent event) {
            raiseEvent(new FileTransferSslHandshakeEvent(FtpsTransfer.this, event));
        }
    }
}
