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

package com.jscape.inet.ftp;

import com.jscape.inet.util.ServerSocketUtil;
import com.jscape.util.Assert;
import com.jscape.util.IOTools;
import com.jscape.util.StringTools;
import com.jscape.util.Timeout;
import com.jscape.util.regex.JDKMatcherFactory;
import com.jscape.util.regex.MatcherFactory;

import java.io.*;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Base implementation of FTP client. Do not use this class directly. Instead
 * use the com.jscape.inet.ftp.Ftp class.
 *
 * @see Ftp
 */
@SuppressWarnings("unchecked")
public class FtpBaseImplementation
        extends FtpImplementation {

    private static final int IP4_PACKET_LENGTH = 4;

    private static final int IP6_PACKET_LENGTH = 16;

    private static final String FTP_REPLY_PENDING = "1";

    private static final String FTP_SUCCESS = "2";

    private static final String FTP_AWAITING_DATA = "3";

    private static final String FTP_TRY_AGAIN = "4";

    private static final String FTP_ERROR = "5";

    private static final String CRLF = "\r\n";

    private static final int DEFAULT_DATA_PORT = 0;

    private static final FtpFileParser MACHINE_PARSER = new MLSDParser();

    private static final MatcherFactory REGEX_FACTORY = new JDKMatcherFactory();

    private boolean passive = true;

    private String response = "";

    private int mode = Ftp.ASCII;

    private int dataPort = DEFAULT_DATA_PORT;

    private ServerSocketUtil.PortRange dataPortRange = new ServerSocketUtil.PortRange(DEFAULT_DATA_PORT, DEFAULT_DATA_PORT);

    private FtpConnection cmdConnection;

    private FtpConnection dataConnection;

    private BufferedReader cmdReader;

    private BufferedWriter cmdWriter;

    private String serverHost;

    private String path;

    private boolean connectBefore = true;

    private String ipNATAddress = null;

    private boolean useEPSV = false;

    private boolean useEPRT = false;

    private boolean errorOnSizeCommand = false;

    private boolean compression = false;

    private Vector serverFeatures;

    /**
     * Creates FtpBaseImplementation instance.
     *
     * @param cmdConnection  command connection
     * @param dataConnection data connection
     */
    public FtpBaseImplementation(FtpConnection cmdConnection, FtpConnection dataConnection) {
        this.port = DEFAULT_FTP_PORT;
        this.cmdConnection = cmdConnection;
        this.dataConnection = dataConnection;
        this.serverFeatures = new Vector();
    }

    /**
     * Creates FtpBaseImplementation instance. Uses socket connection for
     * command and data channels.
     */
    public FtpBaseImplementation() {
        this(new SocketConnection(), new SocketConnection());
    }

    /**
     * Returns response.
     *
     * @return response
     */
    public String getResponse() {
        return this.response;
    }

    

    /**
     * Connects to FTP server and performs login.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void connect()
            throws FtpException {
        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 void connect(boolean login)
            throws FtpException {
        serverHost = null;
        if (isConnected()) {
            disconnect();
        }

        this.debugPrint("Connecting to [" + this.host + "] at port " + this.port);
        if (this.proxyHostname == null) {
            this.cmdConnection.openOutgoing(this.host, this.port, this.timeout, this.linger, this.keepAlive, this.tcpNoDelay);
        } else {
            this.cmdConnection.openOutgoing(this.host,
                                            this.port,
                                            this.timeout,
                                            this.keepAlive,
                                            this.tcpNoDelay,
                                            this.linger,
                                            this.proxyHostname,
                                            this.proxyPort,
                                            this.proxyUsername,
                                            this.proxyPassword,
                                            this.proxyType);
        }


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

        if (replyPending()) {
            checkResponse();
        }

        if (login) {
            login();
        }

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

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

        if (this.compression) {
            sendModeZCommand();
        } else {
            this.dataConnection.setCompression(false);
        }
    }

    /**
     * Retrieves and stores the commands supported by the server
     */
    private void initializeServerFeatureList() {
        try {
            this.serverFeatures = getFeatures();
        } catch (FtpException e) {
            this.serverFeatures = new Vector();
        }
    }

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

        FtpFileParser parser = ftp.getFtpFileParser();

        if (parser == null) {
            try {
                String systemTypeResponse = issueCommandCheck("SYST");
                StringTokenizer st = new StringTokenizer(systemTypeResponse);

                String responseCode = st.nextToken();
                String systemType = st.nextToken();

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

        // initiate transfer mode as binary
        sendCommandAndCheck("TYPE I");
    }

    

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

        if (this.cmdConnection.isOpen()) {
            closeCmdConnection();

            fireEvent(new FtpDisconnectedEvent(this, this.host));
        }
    }

    

    /**
     * Checks if client is connected to FTP server. If connection has been lost
     * then a value of false will be returned and an FtpConnectionLostEvent will
     * be fired to any subscribed FtpListener.
     *
     * @return <code>true</code> if connected <code>false</code> otherwise
     *
     * @see FtpConnectionLostEvent
     */
    public synchronized boolean isConnected() {
        boolean open = this.cmdConnection.isOpen();
        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.ftp, this.host));
        }

        return (open && available);
    }

    

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

        if (replyPending()) {
            readResponse();
        }

        return this.response;
    }

    

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

        if (replyPending()) {
            checkResponse();
        }

        return this.response;
    }

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

    /**
     * Gets the OutputStream from the data connection
     *
     * @param fileName The name of the file
     * @param append   <code>true</code> to append to remote file on FTP server,
     *                 <code>false</code> otherwise
     * @param off      file offset
     *
     * @return Returns a OutputStream from the data connection instance
     *
     * @throws FtpException if FTP error occurs.
     */
    public synchronized OutputStream getOutputStream(String fileName,
                                                     boolean append, long off)
            throws FtpException {
        Assert.notNull(fileName, "Remote filename may not be null.");
        if (off <= 0) {
            openDataConnection((append ? "APPE " : "STOR ") + fileName);
        } else {
            openDataConnection("REST " + off);
            sendCommandAndCheck("STOR " + fileName);
        }

        return new DataConnectionOutputStream(this, this.dataConnection);
    }

    /**
     * Uploads the data as file to be created in the current directory under a
     * name unique to that directory
     *
     * @param in data input stream.
     *
     * @throws FtpException
     * @returns The fileName created
     */
    public synchronized String uploadUnique(InputStream in, String fileName)
            throws FtpException {
        return upload(in, fileName, false, 0, 0, true);
    }

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

    

    /**
     * Uploads data to FTP server.
     *
     * @param in         data input stream.
     * @param remoteName 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
     * @param off        file offset.
     * @param total      total data lenght
     * @param unique     To use upload unique file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized String upload(InputStream in,
                                      String remoteName,
                                      boolean append,
                                      long off,
                                      long total,
                                      boolean unique)
            throws FtpException {
        String responseFileName = "";

        skip(in, off);

        // set transfer mode automatically
        if (autoMode) {
            setAutoTransferMode();
        }

        try {
            if (unique) {
                if (remoteName != null) {
                    openDataConnection("STOU " + remoteName);
                } else {
                    openDataConnection("STOU");
                }
                responseFileName = this.searchFileUniqueName(this.response, remoteName);
            } else {
                if (off <= 0) {
                    openDataConnection((append ? "APPE " : "STOR ") + remoteName);
                } else {
                    openDataConnection("REST " + off);
                    sendCommandAndCheck("STOR " + remoteName);
                }
            }

            OutputStream out = this.dataConnection.getOutputStream();
            long startTime = System.currentTimeMillis();
            off = transmit(in, out, FtpProgressEvent.UPLOAD, off, total);
            long totalTime = System.currentTimeMillis() - startTime;

            if (remoteName == null) {
                fireEvent(new FtpUploadEvent(this.ftp, responseFileName, path, this.targetPath, off, totalTime));
            } else {
                fireEvent(new FtpUploadEvent(this.ftp, remoteName, path, this.targetPath, off, totalTime));
            }
        } finally {
            this.dataConnection.close();
            if (replyPending()) {
                checkResponse();
            }
        }
        return responseFileName;
    }

    

    /**
     * Gets the InputStream from the data connection
     *
     * @param remoteName Remote file name
     * @param off        file offset
     *
     * @return Returns a InputStream from the data connection instance
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized InputStream getInputStream(String remoteName, long off)
            throws FtpException {
        Assert.notNull(remoteName, "Remote filename may not be null.");
        if (off <= 0) {
            openDataConnection("RETR " + remoteName);
        } else {
            openDataConnection("REST " + off);
            sendCommandAndCheck("RETR " + remoteName);
        }

        return new DataConnectionInputStream(this, this.dataConnection);
    }

    

    /**
     * Downloads file from FTP server and writes contents to provided
     * OutputStream.
     *
     * @param out        data output stream.
     * @param remoteName the filename to store file as on FTP server.
     * @param off        file offset
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void download(OutputStream out, String remoteName, long off)
            throws FtpException {
        long total = -1;
        try {
            total = getFileSize(remoteName);
        } catch (FtpException e) {
            if (this.errorOnSizeCommand) {
                throw e;
            }
        }

        //set transfer mode automatically
        if (autoMode) {
            setAutoTransferMode();
        }

        try {
            if (off <= 0) {
                openDataConnection("RETR " + remoteName);
            } else {
                openDataConnection("REST " + off);
                sendCommandAndCheck("RETR " + remoteName);
            }

            InputStream in = this.dataConnection.getInputStream();
            long startTime = System.currentTimeMillis();
            off = transmit(in, out, FtpProgressEvent.DOWNLOAD, off, total);
            long totalTime = System.currentTimeMillis() - startTime;

            fireEvent(new FtpDownloadEvent(this.ftp, remoteName, path, this.targetPath, off, totalTime));
        } finally {
            this.dataConnection.close();

            if (replyPending()) {
                checkResponse();
            }
        }
    }

    

    /**
     * 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 remoteName the remote file
     *
     * @return filesize in bytes
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized long getFileSize(String remoteName)
            throws FtpException {
        sendCommandAndCheck("SIZE " + remoteName);

        StringTokenizer st = new StringTokenizer(this.response);
        st.nextToken();
        String result = st.nextToken();

        try {
            return Long.parseLong(result);
        } catch (NumberFormatException ex) {
            throw new FtpException("unrecognized file size returned: " + result);
        }
    }

    

    /**
     * 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 remoteName 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 remoteName)
            throws FtpException {
        sendCommandAndCheck("MDTM " + remoteName);
        StringTokenizer st = new StringTokenizer(this.response);
        st.nextToken();

        String timestamp = st.nextToken();
        SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
        format.setTimeZone(this.getTimeZone());

        try {
            return format.parse(timestamp);
        } catch (ParseException pe) {
            throw new FtpException("unrecognized date format returned: " + timestamp);
        }
    }

    /**
     * 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);
        sendCommandAndCheck("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);
        sendCommandAndCheck("MFCT " + dateString + " " + remoteName);
    }

    /**
     * Deletes remote file from FTP server.
     *
     * @param remoteName name of file to delete
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void deleteFile(String remoteName)
            throws FtpException {
        sendCommandAndCheck("DELE " + remoteName);
        fireEvent(new FtpDeleteFileEvent(this, remoteName, path));
    }

    

    /**
     * Renames remote file on FTP server.
     *
     * @param remoteName name of file to rename
     * @param newName    new name of file
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void renameFile(String remoteName, String newName)
            throws FtpException {
        sendCommandAndCheck("RNFR " + remoteName);
        sendCommandAndCheck("RNTO " + newName);
        fireEvent(new FtpRenameFileEvent(this, remoteName, newName, path));
    }

    /**
     * Gets file listing of files matching filter criteria.
     *
     * @param filter a filename filter
     *
     * @return a directory listing
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized String getDirListingAsString(String filter)
            throws FtpException {
        String command = "LIST";
        command += (filter != null && filter.length() > 0) ? " " + filter : "";
        StringBuffer results = new StringBuffer();

        try {
            openDataConnection(command);

            InputStreamReader in = getInputStreamReader(this.dataConnection.getInputStream());
            BufferedReader reader = new BufferedReader(in);
            String line = null;
            while ((line = reader.readLine()) != null) {
                results.append(line).append("\r\n");
            }
            return results.toString();
        } catch (IOException ex) {
            throw new FtpException(ex.getMessage(), ex);
        } finally {
            this.dataConnection.close();
            if (replyPending()) {
                checkResponse();
            }
        }
    }

    

    /**
     * Gets directory listing from FTP server.
     *
     * @param filter the filename filter
     *
     * @return a <code>Vector</code> of <code>String</code> objects
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized Vector getDirListing(String filter)
            throws FtpException {
        String command = "LIST";
        command += (filter != null && filter.length() > 0) ? " " + filter : "";

        try {
            openDataConnection(command);

            InputStreamReader in = getInputStreamReader(this.dataConnection.getInputStream());

            BufferedReader reader = new BufferedReader(in);
            FtpFileParser parser = this.ftp.getFtpFileParser();

            Vector files = new Vector();
            for (Enumeration e = parser.parse(reader); e.hasMoreElements(); ) {
                files.addElement(e.nextElement());
            }

            raiseListingEvent(files);

            return files;
        } catch (IOException ex) {
            throw new FtpException(ex.getMessage(), ex);
        } finally {
            this.dataConnection.close();

            if (replyPending()) {
                checkResponse();
            }
        }
    }

    /**
     * @see FtpImplementation#getMachineFileListing(String)
     */
    public FtpFile getMachineFileListing(String filename)
            throws FtpException {
        MLSTRequest request = new MLSTRequest();
        request.setFilename(filename);
        request.execute(new FtpProtocolInterpreter(this));

        return request.getFile();
    }

    /**
     * @see FtpImplementation#getMachineDirListing(String, String)
     */
    public Vector getMachineDirListing(String dirname, String filter)
            throws FtpException {
        com.jscape.util.regex.Matcher regex = createRegex(filter);

        try {
            openDataConnection("MLSD " + dirname);

            InputStreamReader in = getInputStreamReader(this.dataConnection.getInputStream());
            BufferedReader reader = new BufferedReader(in);
            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);
                }
            }

            raiseListingEvent(files);

            return files;
        } catch (IOException e) {
            throw new FtpException(e.getMessage(), e);
        } finally {
            this.dataConnection.close();
            if (replyPending()) {
                checkResponse();
            }
        }
    }

    /**
     * @see FtpImplementation#getMachineDirListing(String)
     */
    public Vector getMachineDirListing(String dirname)
            throws FtpException {
        return getMachineDirListing(dirname, null);
    }

    /**
     * @see com.jscape.inet.ftp.FtpImplementation#getFeatures()
     */
    public Vector getFeatures()
            throws FtpException {
        FeatRequest request = new FeatRequest();
        request.execute(new FtpProtocolInterpreter(this));

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

    

    /**
     * Gets directory listing from FTP server.
     *
     * @param filter the filename filter
     *
     * @return a <code>Vector</code> of <code>String</code> objects
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized Vector getNameListing(String filter)
            throws FtpException {
        String command = "NLST";
        command += (filter != null && filter.length() > 0) ? " " + filter : "";

        try {
            openDataConnection(command);

            InputStreamReader in = getInputStreamReader(this.dataConnection.getInputStream());

            BufferedReader reader = new BufferedReader(in);
            Vector files = new Vector();

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

            raiseListingEvent(files);

            return files;
        } catch (IOException ex) {
            throw new FtpException(ex.getMessage(), ex);
        } finally {
            this.dataConnection.close();

            if (replyPending()) {
                checkResponse();
            }
        }
    }

    

    /**
     * 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 {
        sendCommandAndCheck("PWD");

        try {
            return CurrentDirectoryFormat.parse(this.response);
        } catch (Exception e) {
            throw new FtpException("Unable to retrieve remote directory path", e);
        }
    }

    

    /**
     * Gets current working directory of FTP server. This method is not thread safe.
     *
     * @return current directory
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public String getDirWithNoThreadSynhronization()
            throws FtpException {
        sendCommandAndCheck("PWD");

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

    

    /**
     * Sets current directory on FTP server.
     *
     * @param remoteName the remote directory
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void setDir(String remoteName)
            throws FtpException {
        sendCommandAndCheck("CWD " + remoteName);
        path = getDir();
        fireEvent(new FtpChangeDirEvent(this, path));
    }

    

    /**
     * Sets current directory on FTP server to parent directory.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void dirUp()
            throws FtpException {
        sendCommandAndCheck("CDUP");
        path = getDir();
        fireEvent(new FtpChangeDirEvent(this, path));
    }

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

    /**
     * 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 synchronized boolean getErrorOnSizeCommand() {
        return this.errorOnSizeCommand;
    }

    

    /**
     * Makes directory on FTP server.
     *
     * @param remoteName name of directory to make
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void makeDir(String remoteName)
            throws FtpException {
        sendCommandAndCheck("MKD " + remoteName);
        fireEvent(new FtpCreateDirEvent(this, remoteName, path));
    }

    

    /**
     * Deletes directory from FTP server. If the directory cannot be deleted
     * because it is not empty the recurse parameter will be used to determine
     * files and subdirectories within directory should be removed.
     *
     * @param remoteName the directory to remove
     * @param recurse    true if it should delete files and subdirectories within
     *                   directory if error occurs
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public synchronized void deleteDir(String remoteName, boolean recurse)
            throws FtpException {
        if (recurse) {
            deleteDirRecursive(remoteName);
        } else {
            sendCommandAndCheck("RMD " + remoteName);
            fireEvent(new FtpDeleteDirEvent(this, remoteName, path));
        }
    }

    

    /**
     * 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 {
        sendCommandAndCheck("SYST");

        return this.response.substring(this.response.indexOf(" ")).trim();
    }

    

    /**
     * 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 {
        sendCommandAndCheck("NOOP");

        return this.response;
    }

    

    /**
     * Returns current transfer mode.
     *
     * @return current transfer mode.
     */
    public synchronized int getTransferMode() {
        if (autoMode) {
            return Ftp.AUTO;
        } else {
            return this.mode;
        }
    }

    /**
     * Sets use EPSV indicator. When it is true then will be sent EPSV.
     *
     * @param useEPSV The use EPSV value
     */
    public synchronized void setUseEPSV(boolean useEPSV) {
        this.useEPSV = useEPSV;
    }

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

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

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

    

    /**
     * Sets transfer mode.
     *
     * @param mode desired transfer mode.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    public synchronized void setTransferMode(int mode)
            throws FtpException {
        ensureConnected();

        switch (mode) {
            case Ftp.ASCII:
                sendCommandAndCheck("TYPE A");
                break;
            case Ftp.BINARY:
                sendCommandAndCheck("TYPE I");
                break;
            case Ftp.AUTO:
                break;
            default:
                throw new FtpException("wrong mode passed: " + mode);
        }

        this.mode = mode;
    }

    /**
     * Sets the transfer mode (ASCII or BINARY) according with the targetFile value. I has to be used when the transfer mode
     * has been change to auto
     *
     * @throws FtpException
     */
    private synchronized void setAutoTransferMode()
            throws FtpException {

        javax.activation.MimetypesFileTypeMap map = new javax.activation.MimetypesFileTypeMap();
        String tmpContentType = map.getContentType(this.targetFile);

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

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

    }

    

    /**
     * Tests the passive mode for data connections. <br>
     * In passive mode, the client initiates data connection while server
     * listens. <br>
     * In default, active mode, client is required to provide server side of
     * data connection.<br>
     *
     * @return true if in passive mode.
     */
    public synchronized boolean isPassive() {
        return this.passive;
    }

    

    /**
     * Sets the passive mode for data connections. <br>
     * In passive mode, the client initiates data connection while server
     * listens. <br>
     * In default, active mode, client is required to provide server side of
     * data connection.<br>
     * It is recommended to use passive mode in Applet environments where
     * ServerSocket cant be created due to security restrictions.
     *
     * @param passive if true, the transfers will be passive.
     */
    public synchronized void setPassive(boolean passive) {
        this.passive = passive;
    }

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

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

    /**
     * Sets a NAT address which will be used after PASV command.
     *
     * @param ipAddress The NAT IP address.
     *
     * @throws FtpException If an error occurs.
     */
    public synchronized void setNATAddress(String ipAddress)
            throws FtpException {
        if (isValidFormatIP(ipAddress)) {
            this.ipNATAddress = ipAddress;
        } else {
            throw new FtpException("Invalid NAT IP address format");
        }
    }

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

    /**
     * Sets whether the data will be compressed during the transfering.
     *
     * @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 Ftp server support this feature (MODE Z command)
     *
     * @return <code>true</code> data will be compressed, <code>false</code> otherwise.
     */
    public synchronized boolean getCompression() {
        return this.compression;
    }

    

    /**
     * Returns last server response code.
     *
     * @return last server response code or -1 if code cannot be determined
     */
    public synchronized int getResponseCode() {
        try {
            return Integer.parseInt(this.response.substring(0, 3));
        } catch (Exception se) {
            return -1;
        }

    }

    

    /**
     * Returns current port for incoming data connections.
     *
     * @return current port for incoming data connections.
     */
    public synchronized int getDataPort() {
        return this.dataPort;
    }

    /**
     * Sets the connectBefore property value
     *
     * @param connect It indicates if the connection will be established before send
     *                the command
     */
    public synchronized void setConnectBeforeCommand(boolean connect) {
        this.connectBefore = connect;
    }

    /**
     * Gets the connectBefore indicator
     *
     * @return <code>true</code> the connection will be establiched before
     *         send the command
     */
    public synchronized boolean getConnectBeforeCommand() {
        return this.connectBefore;
    }

    

    /**
     * Sets data port for incoming connections.
     *
     * @param dataPort port for incoming connections.
     */
    public synchronized void setDataPort(int dataPort) {
        this.dataPort = dataPort;
    }

    

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

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

    

    /**
     * Sets the start data port range for incomings connections
     *
     * @param dataPort Start data Port range for incomings connections
     */
    public synchronized void setDataPortStart(int dataPort) {
        if (dataPort > getDataPortEnd()) {
            throw new IllegalArgumentException(
                    "Invalid assigment. The start data port must be minor or equal than the end data port");
        }
        this.dataPortRange = new ServerSocketUtil.PortRange(dataPort, this.dataPortRange.end);
    }

    

    /**
     * Sets the end data port range for incomings connections
     *
     * @param dataPort End data Port range for incomings connections
     */
    public 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);
    }

    

    /**
     * Performs user authentication.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private synchronized void authenticate()
            throws FtpException {
        sendCommandAndCheck("USER " + this.user);

        int responseCode = 0;
        try {
            responseCode = getResponseCode();
        } catch (Exception e) {
            throw new FtpException(
                    "Unable to parse response code from server reply : "
                            + response);
        }

        if (responseCode != 230 && password != null) {
            sendCommandAndCheck("PASS " + this.password);
        }

        if (account != null) {
            sendCommandAndCheck("ACCT " + this.account);
        }
    }

    

    /**
     * Changes the password.
     *
     * @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.sendCommandAndCheck("SITE PSWD" + " \"" + oldPassword + "\" "
                                         + "\"" + newPassword + "\"");
    }

    

    /**
     * Closes command connection.
     */
    private void closeCmdConnection() {
        try {
            sendCommandAndCheck("QUIT");
        } catch (Exception e) {
            // todo: ignore?
        }

        this.cmdConnection.close();
        this.cmdReader = null;
        this.cmdWriter = null;
    }

    

    /**
     * Issues command to the server.
     *
     * @param command command to issue.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    public void sendCommand(String command)
            throws FtpException {
        ensureConnected();
        fireEvent(new FtpCommandEvent(this.ftp, command));

        try {
            debugPrint(command);
            this.cmdWriter.write(command + CRLF);
            this.cmdWriter.flush();
        } catch (IOException ex) {
            throw new FtpConnectionLostException(ex.getMessage(), ex);
        }
    }

    

    /**
     * Issues command to the server.
     *
     * @param command command to issue.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private void sendCommandAndCheck(String command)
            throws FtpException {
        sendCommand(command);
        checkResponse();
    }

    /**
     * Send Mode Z command when it is supported.
     *
     * @throws FtpException If an error occurs.
     */
    private void sendModeZCommand()
            throws FtpException {
        if (isFeatureSupported("MODE Z")) {
            sendCommand("MODE Z");
            checkResponse();
            this.dataConnection.setCompression(true);
        } else {
            this.dataConnection.setCompression(false);
        }
    }

    

    /**
     * Reads response from FTP server and throws exception if error occurs.
     *
     * @throws FtpException if an I/O or FTP related error occurs
     */
    private void checkResponse()
            throws FtpException {
        readResponse();

        if (failure()) {
            throw new FtpException(this.response);
        }
    }

    

    /**
     * Reads server response
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    public void readResponse()
            throws FtpException {
        this.response = "";
        String line = null;
        StringBuffer buffer = new StringBuffer();

        try {
            line = this.cmdReader.readLine();

            // fixes known issue with IIS FTP server where some lines contain
            // empty responses
            if (line != null && line.trim().equals("")) {
                do {
                    line = cmdReader.readLine();
                } while (line != null && line.equals(""));
            }
            debugPrint(line);

            if (line != null) {
                buffer.append(line).append(CRLF);
            }

            if (isMultilineResponse(line)) {
                readMultiLineResponse(buffer);
            }

        } catch (IOException ex) {
            throw new FtpConnectionLostException(ex.getMessage(), ex);
        }

        this.response = buffer.toString();
        fireEvent(new FtpResponseEvent(this.ftp, this.response));
    }

    

    /**
     * Inits command streams.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private void initCommandStreams()
            throws FtpException {
        try {
            InputStream in = this.cmdConnection.getInputStream();
            OutputStream out = this.cmdConnection.getOutputStream();

            if (this.encodingWire != null) {
                this.cmdReader = new BufferedReader(new InputStreamReader(in, this.encodingWire));
                this.cmdWriter = new BufferedWriter(new OutputStreamWriter(out, this.encodingWire));
            } else {
                this.cmdReader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
                this.cmdWriter = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8));

            }
        } catch (UnsupportedEncodingException e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    

    /**
     * Transmits data.
     *
     * @param in    input stream.
     * @param out   output stream.
     * @param mode  trnasfer direction.
     * @param sent  already sent bytes count.
     * @param total total bytes to send.
     *
     * @return total sent bytes.
     *
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private long transmit(InputStream in, OutputStream out, int mode,
                          long sent, long total)
            throws FtpException {
        BufferedInputStream bin = new BufferedInputStream(in);
        BufferedOutputStream bout = new BufferedOutputStream(out);

        try {
            return ((this.mode == Ftp.BINARY) ? transmitBinary(bin, bout, mode, sent, total)
                                              : transmitAscii(bin, bout, mode, sent, total));
        } catch (IOException e) {
            throw new FtpException(e.getMessage(), e);
        }
    }

    

    /**
     * Transmits data in binary mode.
     *
     * @param in    input stream.
     * @param out   output stream.
     * @param mode  trnasfer direction.
     * @param sent  already sent bytes count.
     * @param total total bytes to send.
     *
     * @return total sent bytes.
     *
     * @throws IOException if underlayed I/O error occurs.
     */
    private long transmitBinary(BufferedInputStream in, BufferedOutputStream out, int mode, long sent, long total)
            throws IOException {
        byte[] buf = new byte[this.blockSize];
        int read = 0;

        /**
         * 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 (total == 0L) {
            //fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.targetPath, this.targetPath, mode, sent, read, total));
            fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.path, this.targetPath, mode, sent, read, total));
        }

        while ((read = in.read(buf)) != -1 && !this.interrupted) {
            out.write(buf, 0, read);
            out.flush();
            sent += read;
            if (this.hasListeners()) {
                //fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.targetPath, this.targetPath, mode, sent, read, total));
                fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.path, this.targetPath, mode, sent, read, total));
            }
        }

        return sent;
    }

    

    /**
     * Transmits data in text mode.
     *
     * @param in    input stream.
     * @param out   output stream.
     * @param mode  transfer direction.
     * @param sent  already sent bytes count.
     * @param total total bytes to send.
     *
     * @return total sent bytes.
     *
     * @throws IOException  if underlayed I/O error occurs.
     * @throws FtpException if underlayed I/O or FTP error occurs.
     */
    private long transmitAscii(BufferedInputStream in, BufferedOutputStream out, int mode, long sent, long total)
            throws IOException, FtpException {
        // gets local line termination
        String localTerm = System.getProperties().getProperty("line.separator");

        // 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 = CRLF.getBytes(StandardCharsets.US_ASCII);
        } else {
            termIn = CRLF.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 (total == 0L) {
            //fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.targetPath, this.targetPath, mode, sent, 0, total));
            fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.path, this.targetPath, mode, sent, 0, total));
        }

        // 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 + this.blockSize;
        byte[] buf = new byte[this.blockSize];
        // temporary buffer for sending data
        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        // reads data from input stream and writes to output stream
        while ((i = in.read(buf)) != -1 && !this.interrupted) {
            // check if byte is equal to next line terminator byte
            for (int j = 0; j < i; j++) {
                if (buf[j] == 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 {
                        // TODO code for S390 support
                        if (mode == FtpProgressEvent.UPLOAD) {
                            buf[j] = (byte) translateBytes(buf[j], getDiskEncoding(),
                                                           getWireEncoding());
                        } else {
                            buf[j] = (byte) translateBytes(buf[j], getWireEncoding(),
                                                           getDiskEncoding());
                        }
                    }
                    bout.write(buf[j]);
                    sent++;
                }
            }

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

                if (this.hasListeners()) {
                    //fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.targetPath, this.targetPath, mode, sent, bytes.length, total));
                    fireEvent(new FtpProgressEvent(this.ftp, this.targetFile, this.path, this.targetPath, mode, sent, bytes.length, total));
                    nextProgressEvent = (sent + this.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.ftp,
                                       this.targetFile,
                                       this.path,
                                       this.targetPath,
                                       mode,
                                       sent,
                                       remaining.length,
                                       total));
        return sent;
    }

    public InputStream getDataInputStream()
            throws FtpException {
        return this.dataConnection.getInputStream();
    }

    

    /**
     * Opens data connection from/to FTP server.
     *
     * @param command FTP open connection command.
     *
     * @throws FtpException if an I/O or FTP error occurs.
     */
    public void openDataConnection(String command)
            throws FtpException {
        if (this.passive) {
            try {
                sendPassiveCommandAndCheck();
            } catch (FtpException fe) {
                // passive is not enabled on server .. try active mode
                this.passive = false;
                openDataConnection(command);
            }

            // ignore if passive was disabled on server
            if (this.passive) {
                String[] hostData = parseHostData(this.response);
                // get socket bound to command connection
                Socket s = ((SocketConnection) this.cmdConnection).getSocket();
                // get remote ip address
                InetAddress address = s.getInetAddress();
                String hostAddress = address.getHostAddress();

                // send command before establishing passive socket connection ... this is non-standard behavior
                if (!this.connectBefore) {
                    sendCommandAndCheck(command);
                }

                try {
                    if (this.proxyHostname == null) {
                        this.dataConnection
                                .openOutgoing(serverHost != null ? serverHost : getHost(hostData),
                                              getPort(hostData),
                                              this.timeout,
                                              this.linger,
                                              this.keepAlive,
                                              this.tcpNoDelay);
                    } else {
                        this.dataConnection
                                .openOutgoing(serverHost != null ? serverHost : getHost(hostData),
                                              getPort(hostData),
                                              this.timeout,
                                              this.keepAlive,
                                              this.tcpNoDelay,
                                              this.linger,
                                              this.proxyHostname,
                                              this.proxyPort,
                                              this.proxyUsername,
                                              this.proxyPassword,
                                              this.proxyType);
                    }

                    if (this.connectBefore) {
                        sendCommandAndCheck(command);
                    }

                } catch (Exception ez) {
                    if (!hostAddress.equals(getHost(hostData))) {
                        this.serverHost = hostAddress;
                        this.debugPrint("Unable to connect to host " + getHost(hostData) + ". Substituting server address " + hostAddress);

                        if (this.dataConnection.isOpen()) {
                            this.dataConnection.close();
                        }

                        this.dataConnection.openOutgoing(hostAddress,
                                                         getPort(hostData),
                                                         this.timeout,
                                                         this.linger,
                                                         this.keepAlive,
                                                         this.tcpNoDelay);

                        if (this.connectBefore) {
                            sendCommandAndCheck(command);
                        }
                    } else {
                        throw new FtpException("Unable to connect to host " + getHost(hostData), ez);
                    }
                }
            }
        } else {
            this.dataConnection.prepareIncoming(this.dataPortRange, this.timeout);

            useActiveFtpMode();
            sendCommandAndCheck(command);
            this.dataConnection.openIncoming(this.timeout, this.linger);
        }
    }

    /*
    * Use Active mode, either the PORT or the EPRT command will be used
    */

    private void useActiveFtpMode()
            throws FtpException {

        ActiveModeData hostData = getHostData();

        if (this.useEPRT == true || this.autoDetectIpv6 == true) {
            boolean isIpv6Address = this.isIpv6Address(hostData.getHostname());
            if (isIpv6Address == true) {
                this.debugPrint("Ipv6 hostname/IP detected : " + hostData.getHostname());
            }

            this.useEPRT = (isIpv6Address || this.isFeatureSupported("EPRT")) && hostData.isEPRTRequired();
        }

        if (this.useEPRT) {
            sendEPRTCommandAndCheck(hostData.getCommandData());
        } else {
            sendPORTCommandAndCheck(hostData.getCommandData());
        }
    }

    /*
    * Try sending the EPRT command
    */

    private void sendEPRTCommandAndCheck(String hostData)
            throws FtpException {
        try {
            sendCommandAndCheck("EPRT " + hostData);
        } catch (FtpException e) {
            throw e;
        }
    }

    /*
    * Try sending the PORT command
    */

    private void sendPORTCommandAndCheck(String hostData)
            throws FtpException {
        try {
            sendCommandAndCheck("PORT " + hostData);
        } catch (FtpException e) {
            throw e;
        }
    }

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

    /**
     * Prepares host data for "PORT" or the "EPRT" command.
     *
     * @return string with host data.
     *
     * @throws FtpException if current host cannot be determined.
     */
    private ActiveModeData getHostData()
            throws FtpException {
        ActiveModeData ret = null;
        byte[] nic = this.getLocalNICInterface();
        int length = nic.length;

        if (length == IP6_PACKET_LENGTH && this.useEPRT == false) {
            throw new FtpException("Client is on an IPv6 network but EPRT command is not supported by server.");
        } else if (length == IP6_PACKET_LENGTH && this.useEPRT == true) {
            ret = this.getEPRTHostDataInIpv6Format(nic); // IPv6 and EPRT
        } else if (length == IP4_PACKET_LENGTH && this.useEPRT == true) {
            ret = this.getEPRTHostDataInIpv4Format(nic); // IPv4 and EPRT
        } else if (length == IP4_PACKET_LENGTH && this.useEPRT == false) {
            ret = this.getPORTHostData(nic);             // IPv4 and PORT
        }
        return ret;
    }

    private ActiveModeData getPORTHostData(byte[] nic) {
        String result = "";
        for (int i = 0; i < nic.length; ++i) {
            result += (nic[i] & 0xff) + ",";
        }

        int port = ((SocketConnection) this.dataConnection).getServerSocket().getLocalPort();
        return new ActiveModeData(result,
                                  (result + Integer.toString((port >> 8) & 0xff) + "," + Integer.toString(
                                          port & 0xff)));
    }

    private ActiveModeData getEPRTHostDataInIpv4Format(byte[] nic) {
        String result = "";
        for (int i = 0; i < nic.length; i++) {
            result += (nic[i] & 0xff) + ".";
        }
        String host = result.substring(0, result.length() - 1);
        return new ActiveModeData(host, "|1|" + host + "|" + getPortNumberFromSocketConnection() + "|");
    }

    private ActiveModeData getEPRTHostDataInIpv6Format(byte[] nic) {
        String result = "";
        for (int i = 0; i < nic.length; i += 2) {
            String firstByte = Integer.toString((nic[i] & 0xff) + 0x100, 16).substring(1);
            String secondByte = Integer.toString((nic[i + 1] & 0xff) + 0x100, 16).substring(1);
            result += firstByte + secondByte + ":";
        }
        String host = result.substring(0, result.length() - 1);
        result = "|2|" + result + "|" + getPortNumberFromSocketConnection() + "|";
        return new ActiveModeData(host, result);
    }

    private int getPortNumberFromSocketConnection() {
        int port = ((SocketConnection) this.dataConnection).getServerSocket().getLocalPort();
        String str1 = Integer.toString((port >> 8) & 0xff);
        String str2 = Integer.toString(port & 0xff);
        return ((Integer.parseInt(str1)) * 256) + Integer.parseInt(str2);
    }

    private byte[] getLocalNICInterface()
            throws FtpException {
        byte[] addr = null;

        // gets the local NIC interface that is currently bound to command connection
        try {

            if (getPortAddress() != null) {
                addr = InetAddress.getByName(getPortAddress()).getAddress();
            } else {
                addr = ((SocketConnection) this.cmdConnection).getSocket()
                                                              .getLocalAddress().getAddress();
            }
        } catch (Exception ex) {
            throw new FtpException(ex.getMessage(), ex);
        }
        return addr;
    }

    public void closeDataConnection() {
        this.dataConnection.close();
    }

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

    /**
     * Determines host IP address from parsed host data.
     *
     * @param hostData host data.
     *
     * @return host IP.
     */
    private String getHost(String[] hostData) {

        boolean isIpv6 = false;
        isIpv6 = this.isIpv6Address(this.host);

        if (isIpv6) {
            return hostData[0];
        } else {
            return hostData[0] + "." + hostData[1] + "." + hostData[2] + "." + hostData[3];
        }
    }

    /**
     * Checks to see if address is in Ipv6 format
     *
     * @param address
     *
     * @return boolean value
     */
    private boolean isIpv6Address(String address) {

        /*
           * Check for Ipv4 addressses mapped to Ipv6 addresses
           * For example ::FFFF:129.144.52.38
                         ::129.144.52.38
           */
        if (address.startsWith("::") || address.startsWith("::FFFF:") || address.startsWith("::ffff:")) {
            String ipv4Address = address.substring(address.indexOf(".") - 3);
            StringTokenizer ipTokenizer = new StringTokenizer(ipv4Address.substring(0, ipv4Address.length()), ".");
            if (ipTokenizer.countTokens() == 4) {
                return true;
            }
        }

        Pattern pattern = Pattern.compile("::");
        Matcher matcher = pattern.matcher(address);
        int doubleColons = 0;

        while (matcher.find()) {
            doubleColons++;
        }

        // Must have only one instance of :: in the entire IP string
        if (doubleColons > 1) {
            return false;
        }

        StringTokenizer tokenizer = new StringTokenizer(address, ":", false);
        Vector<String> parts = new Vector<String>();

        while (tokenizer.hasMoreElements()) {
            parts.add(tokenizer.nextToken());
        }

        // Must have only eight parts.
        if (doubleColons == 0 && parts.size() != 8) {
            return false;
        }

        // Now check whether each character of each part is a valid hex character
        pattern = Pattern.compile("([a-fA-F]?)(\\d?)");
        for (int i = 0; i < parts.size(); i++) {
            String entry = parts.get(i);
            for (int count = 0; count < entry.length(); count++) {
                if (pattern.matcher(Character.toString(entry.charAt(count))).find() == false) {
                    return false;
                }
            }
        }

        return true;
    }

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

    /**
     * Determines port # from parsed host data.
     *
     * @param hostData host data.
     *
     * @return host port #.
     */
    private int getPort(String[] hostData) {
        if (this.useEPSV == true) {
            return Integer.parseInt(hostData[hostData.length - 1]);
        } else {
            return (Integer.parseInt(hostData[4]) * 256) + Integer.parseInt(hostData[5]);
        }
    }


    /**
     * Get string data into String array.
     *
     * @param portData String tokenizer data
     *
     * @return A string array
     */
    private String[] getPortDataFromStringTokenizer(StringTokenizer portData) {
        String[] port = new String[portData.countTokens()];
        int i = 0;
        while (portData.hasMoreTokens()) {
            port[i++] = portData.nextToken();
        }

        return port;
    }

    /**
     * Verifies if NAT address is valid.
     *
     * @param ipAddress The will be verifies
     *
     * @return <code>true</code> if the address is contained a format spected
     */
    private boolean isValidFormatIP(String ipAddress) {
        boolean isValid = true;
        StringTokenizer tok = new StringTokenizer(ipAddress.substring(0, ipAddress.length()), ".");
        if (tok.countTokens() == 4) {
            isValid = true;
        }
        return isValid;
    }

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

    /**
     * Parses host data from "PASV" command.
     *
     * @param hostData host data to parse.
     *
     * @return array of separated host data.
     *
     * @throws FtpException if host data cannot be parsed.
     */
    private String[] parseHostData(String hostData)
            throws FtpException {
        if (this.useEPSV) {
            return parseEPSVResponse(hostData);
        } else if (!StringTools.isValidString(this.ipNATAddress)) {
            int start = -1;
            int end = -1;
            StringTokenizer tok;

            start = hostData.indexOf("(");

            if (start == -1) {
                hostData = this.getFormatToParseHostData(hostData);
                tok = new StringTokenizer(hostData, ",");
            } else {
                end = hostData.indexOf(")", start);
                tok = new StringTokenizer(hostData.substring(start + 1, end), ",");
            }

            int size = tok.countTokens();

            if (size < 6) {
                throw new FtpException("wrong host & port for passive connection");
            }

            String[] result = new String[size];

            for (int i = 0; i < result.length; ++i) {
                result[i] = tok.nextToken();
            }

            return result;
        } else {
            return parseNATAddress(hostData);
        }
    }

    /**
     * Parses host data from pasive command and NAT IP address.
     *
     * @param hostData Pasive response
     *
     * @return array of separated host data which includes NAT IP address and
     *         the port from passive command response
     */
    private String[] parseNATAddress(String hostData)
            throws FtpException {
        if (isValidFormatIP(this.ipNATAddress)) {
            StringTokenizer tok = new StringTokenizer(this.ipNATAddress.substring(0,
                                                                                  this.ipNATAddress.length()), ".");
            int start = hostData.indexOf("(");
            int end = hostData.indexOf(")");
            StringTokenizer portData = new StringTokenizer(hostData.substring(start + 1,
                                                                              end), ",");
            String[] portArray = getPortDataFromStringTokenizer(portData);
            int ipSize = tok.countTokens();

            String[] result = new String[ipSize + 2];

            int i = 0;
            // NAT IP address
            for (i = 0; i < ipSize; i++) {
                result[i] = tok.nextToken();
            }
            // Port numbers
            result[i] = portArray[portArray.length - 2];
            result[i + 1] = portArray[portArray.length - 1];

            return result;
        } else {
            throw new FtpException("Invalid NAT IP address format");
        }

    }

    /**
     * Parses server response from command EPSV.
     *
     * @param hostData EPSV server response
     *
     * @return array of separated host data which includes control connect IP address and
     *         the port from EPSV server response
     */
    private String[] parseEPSVResponse(String hostData)
            throws FtpException {

        int start = 0;
        int end = 0;
        if (hostData.indexOf("(") != -1) {
            start = hostData.indexOf("(");
            end = hostData.indexOf(")");
        } else if (hostData.indexOf("|") != -1) {
            start = hostData.indexOf("|");
            end = hostData.length();
        } else {
            throw new FtpException("Invalid EPSV response");
        }

        String responseData = hostData.substring(start + 1, end);
        StringTokenizer portToken = new StringTokenizer(responseData.substring(0,
                                                                               responseData.length()), "|");
        String port = portToken.nextToken();

        Socket s = ((SocketConnection) this.cmdConnection).getSocket();
        // get remote ip address
        InetAddress address = s.getInetAddress();
        String hostAddress = address.getHostAddress();
        StringTokenizer tok = new StringTokenizer(hostAddress.substring(0, hostAddress.length()), ".");
        int ipSize = tok.countTokens();

        String[] result = new String[ipSize + 1];

        int i = 0;
        for (i = 0; i < ipSize; i++) {
            result[i] = tok.nextToken();
        }
        // Port number
        result[i] = port;
        return result;
    }

    /**
     * Gets the format to parse host data.
     *
     * @param hostData host data to parse.
     *
     * @return format to parse.
     *
     * @throws FtpException if the format isn't correct.
     */
    private String getFormatToParseHostData(String hostData)
            throws FtpException {

        StringTokenizer tok = new StringTokenizer(hostData, " ");
        if (tok.countTokens() == 2) {
            tok.nextToken();
            return tok.nextToken();
        } else {
            throw new FtpException("Wrong host & port for passive connection");
        }

    }


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

    /**
     * Ensures that instance is connected to FTP server.
     *
     * @throws FtpException if instance is not connected.
     */
    private void ensureConnected()
            throws FtpException {
        if (!this.cmdConnection.isOpen()) {
            throw new FtpException(
                    "not connected to FTP server.  Must connect to FTP server prior.");
        }
    }

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

    /**
     * Checks if passed line is multiline response.
     *
     * @param line string to check.
     *
     * @return <code>true</code> if passed line is multiline response.
     */
    private boolean isMultilineResponse(String line) {
        return (line != null && line.length() > 3 && line.charAt(3) == '-');
    }

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

    /**
     * Reads multiline buffer.
     *
     * @param buffer <code>StringBuffer</code> object to accumulate
     *               incoming lines.
     *
     * @throws IOException if I/O error occurs.
     */
    private void readMultiLineResponse(StringBuffer buffer)
            throws IOException {

        // delimiter is equal to initial response code followed by a space
        String delim = buffer.toString().substring(0, 3);
        delim += " ";

        String line = null;

        while ((line = cmdReader.readLine()) != null) {
            debugPrint(line);
            buffer.append(line).append(CRLF);
            if (line.startsWith(delim)) {
                break;
            }
        }
    }

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

    /**
     * @return reply
     */
    private boolean replyPending() {
        return (this.response.startsWith(FTP_REPLY_PENDING));
    }

    /**
     * Asynchronous reverse DNS lookup.
     *
     * @author mjawwad
     */
    private class AsyncReverseDNSLookup
            extends Thread {

        private String hostname = null;
        private boolean lookupComplete = false;

        private AsyncReverseDNSLookup(String hostname) {
            this.hostname = hostname;
        }

        public void run() {
            boolean error = false;
            try {
                this.hostname = InetAddress.getByName(hostname).getHostAddress();
            } catch (Exception e) {
                error = true;
                this.lookupComplete = false;
            } finally {
                if (error == false) {
                    this.lookupComplete = true;
                }
            }
        }
    }

    private void sendPassiveCommandAndCheck()
            throws FtpException {

        if (this.useEPSV == true || this.autoDetectIpv6 == true) {
            boolean isIpv6Address = this.isIpv6Address(host);
            boolean isIpv4Address = this.isValidFormatIP(host);
            if (isIpv6Address == false && isIpv4Address == false) {
                try {

                    AsyncReverseDNSLookup addrLookup = new AsyncReverseDNSLookup(this.host);
                    Timeout timeout = new Timeout(1000L);
                    addrLookup.start();

                    while (timeout.isExceeded() == false) {
                        Thread.sleep(1L);
                        if (addrLookup.lookupComplete == true) {
                            this.host = addrLookup.hostname;
                            this.debugPrint("Reverse DNS address lookup completed in " + (timeout.getValue() - timeout.remainedTime()) + " ms [" + host + "]");
                            break;
                        }
                    }

                    if (addrLookup.lookupComplete == false) {
                        String message = "Reverse DNS lookup unsuccessful ";
                        if (timeout.isExceeded()) {
                            message += "(timed out)";
                        }
                        this.debugPrint(message);
                    }

                    isIpv6Address = this.isIpv6Address(host);
                } catch (Exception e) {
                    // ignore
                }
            }
            this.useEPSV = isIpv6Address || this.isFeatureSupported("EPSV");
        }


        if (this.useEPSV) {
            sendEPSVCommandAndCheck();
        } else {
            sendPASVCommandAndCheck();
        }
    }

    /*
    * Try sending the EPSV command
    */

    private void sendEPSVCommandAndCheck()
            throws FtpException {
        try {
            if (this.useEPSV) {
                sendCommandAndCheck("EPSV");
            }
        } catch (FtpException e) {
            this.useEPSV = false;
            sendPASVCommandAndCheck();
        }
    }

    /*
    * Try sending the PASV command
    */

    private void sendPASVCommandAndCheck()
            throws FtpException {
        sendCommandAndCheck("PASV");
    }

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

    /**
     * @return
     */
    private boolean failure() {
        return this.response.startsWith(FTP_TRY_AGAIN) || this.response.startsWith(FTP_ERROR);
    }

    /**
     * Translate from one encoding to another
     *
     * @param in            value to translate
     * @param encodeTypeIn  encoding in
     * @param encodeTypeOut encoding out
     *
     * @return
     *
     * @throws IOException
     */
    private 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;
        }

        // Perform the translation

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

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

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

        return outBytes[0];
    }

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

    private void raiseListingEvent(Vector files) {
        fireEvent(new FtpListingEvent(this.ftp, files.elements()));
    }


    private static class DataConnectionInputStream
            extends InputStream {

        private final FtpBaseImplementation impl;
        private final FtpConnection connection;
        private final InputStream in;

        private DataConnectionInputStream(FtpBaseImplementation impl, FtpConnection connection)
                throws FtpException {
            this.impl = impl;
            this.connection = connection;
            this.in = this.connection.getInputStream();
        }

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

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

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

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

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

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

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

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

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

    }   // DataConnectionInputStream

    private static final class DataConnectionOutputStream
            extends OutputStream {

        private final FtpBaseImplementation impl;
        private final FtpConnection connection;
        private final OutputStream out;

        private DataConnectionOutputStream(FtpBaseImplementation impl, FtpConnection connection)
                throws FtpException {
            this.impl = impl;
            this.connection = connection;
            this.out = this.connection.getOutputStream();
        }

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

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

    /**
     * Class to hold the active mode hostname and full command.
     *
     * @author mjawwad
     */
    private class ActiveModeData {

        private String hostname = null;
        private String commandData = null;

        private ActiveModeData(String hostname, String commandData) {
            this.hostname = hostname;
            this.commandData = commandData;
        }

        public String getHostname() {
            return this.hostname;
        }

        public String getCommandData() {
            return this.commandData;
        }

        public boolean isEPRTRequired() {
            return this.getCommandData().startsWith("|");
        }
    }

    public static class CurrentDirectoryFormat {

        public static String parse(String data) {
            return data.contains("ID:") && data.contains("Current TR")
                   ? parseMainframe(data)
                   : parseCommon(data);
        }

        private static String parseMainframe(String data) {
            Scanner scanner = new Scanner(data).useDelimiter(",|\r\n");
            String id = "";
            String directory = "";
            while (scanner.hasNext()) {
                String propertyData = scanner.next().trim();

                if (propertyData.startsWith("ID")) {
                    id = valueOf(propertyData);
                } else if (propertyData.startsWith("Current TR")) {
                    directory = valueOf(propertyData);
                }
            }

            return String.format("%s%%%%%s", directory, id);
        }

        private static String valueOf(String propertyData) {
            return propertyData.substring(propertyData.indexOf(':') + 1).trim();
        }

        private static String parseCommon(String data) {
            data = data.substring(4, data.length() - 2);

            // RFC959 allows inner quotes in the path which must be escaped with another quote:
            // "/usr/dm/foo""bar"

            StringBuilder buffer = new StringBuilder();
            boolean escaped = false;
            for (int i = 0; i < data.length(); ++i) {
                char currentChar = data.charAt(i);

                if (currentChar == ' ' && escaped && buffer.length() > 0) {
                    break;
                }

                if (currentChar != '\"' || escaped) {
                    buffer.append(currentChar);
                    escaped = false;
                } else {
                    escaped = true;
                }
            }

            return buffer.toString();
        }
    }
}
