/**
 *
 *  edtFTPj
 *
 *  Copyright (C) 2000-2003  Enterprise Distributed Technologies Ltd
 *
 *  www.enterprisedt.com
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2.1 of the License, or (at your option) any later version.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *  Bug fixes, suggestions and comments should be should posted on 
 *  http://www.enterprisedt.com/forums/index.php
 *
 *  Change Log:
 *
 *        $Log: FTPClient.java,v $
 *        Revision 1.122  2011-01-16 22:46:33  bruceb
 *        fix resume bug
 *
 *        Revision 1.121  2010-11-04 01:08:06  bruceb
 *        allow REST to fail
 *
 *        Revision 1.120  2010-09-15 04:07:08  bruceb
 *        fix nptr in connected()
 *
 *        Revision 1.119  2010-08-20 00:25:07  bruceb
 *        IPv6 changes
 *
 *        Revision 1.118  2010-06-30 14:02:43  bruceb
 *        permit 232 to be returned from USER
 *
 *        Revision 1.117  2010-05-28 07:04:55  bruceb
 *        allow aborting the listing
 *
 *        Revision 1.116  2010-04-26 15:55:46  bruceb
 *        add new dirDetails method with callback
 *
 *        Revision 1.115  2010-04-09 21:09:31  hans
 *        Fixed setProgressMonitorEx so that it works when called multiple times.
 *
 *        Revision 1.114  2010-02-25 01:22:48  bruceb
 *        add debug
 *
 *        Revision 1.113  2010-02-17 00:37:46  bruceb
 *        initialise resumeMarker correctly
 *
 *        Revision 1.112  2009-10-18 23:59:03  bruceb
 *        move name resolution to connect()
 *
 *        Revision 1.111  2009-08-20 05:15:09  bruceb
 *        fix re FTPProgressMonitorEx - bytesTransferred not being called
 *
 *        Revision 1.110  2009-07-17 03:04:08  bruceb
 *        proxy changes + change exists to use dirDetails
 *
 *        Revision 1.109  2009-06-18 07:17:04  bruceb
 *        autoPassiveIPSubstitution set to true by default, and throw exception when we cancel transfer
 *
 *        Revision 1.108  2009-04-14 01:47:26  bruceb
 *        PASV/PORT callbacks
 *
 *        Revision 1.107  2009-03-20 04:35:10  bruceb
 *        lots of little changes to fix automode bug, post transfer check methods etc
 *
 *        Revision 1.106  2009-02-20 06:24:24  hans
 *        Removed unused local variables.
 *
 *        Revision 1.105  2009-02-20 06:21:52  hans
 *        Removed unused local variables.
 *
 *        Revision 1.104  2009-01-28 04:15:45  bruceb
 *        processControlChannelException added
 *
 *        Revision 1.103  2009-01-28 03:52:18  bruceb
 *        implement reconnect
 *
 *        Revision 1.102  2009-01-15 03:39:37  bruceb
 *        *** empty log message ***
 *
 *        Revision 1.101  2008-09-22 03:50:00  bruceb
 *        fix doco bug re timeout
 *
 *        Revision 1.100  2008-09-18 06:56:33  bruceb
 *        retry settings
 *
 *        Revision 1.99  2008-09-18 05:18:03  bruceb
 *        sendCommand now throws an FTPException too
 *
 *        Revision 1.98  2008-08-26 04:35:52  bruceb
 *        move resume/size to before data socket creation
 *
 *        Revision 1.97  2008-05-22 04:20:55  bruceb
 *        moved stuff to internal etc
 *
 *        Revision 1.96  2008-05-02 07:41:30  bruceb
 *        setModTime added
 *
 *        Revision 1.95  2008-03-13 04:23:29  bruceb
 *        changed to system()
 *
 *        Revision 1.94  2008-03-13 00:22:14  bruceb
 *        added executeCommand
 *
 *        Revision 1.93  2008-01-09 03:54:21  bruceb
 *        executeCommand() now returns reply code
 *
 *        Revision 1.92  2007-12-18 07:54:58  bruceb
 *        many small changes to prepare for FileTransferClient
 *
 *        Revision 1.91  2007-11-29 02:32:09  hans
 *        Added DEFAULT_LISTING_LOCALES and made related changes to initializers.
 *
 *        Revision 1.90  2007-11-13 07:14:04  bruceb
 *        ListenOnAllInterfaces
 *
 *        Revision 1.89  2007-11-07 23:53:50  bruceb
 *        refactoring for FXP
 *
 *        Revision 1.88  2007-10-12 05:21:13  bruceb
 *        can set multiple locales (and 2 set by default)
 *
 *        Revision 1.87  2007-08-15 03:47:39  bruceb
 *        fix separator
 *
 *        Revision 1.86  2007-08-09 00:50:20  bruceb
 *        added 250 reply to some commands
 *
 *        Revision 1.85  2007-08-07 04:46:25  bruceb
 *        added counts for transfers and deletes plus getLastReply()
 *
 *        Revision 1.84  2007-07-18 02:16:33  bruceb
 *        ignore size() exception in resume
 *
 *        Revision 1.83  2007-07-05 05:28:27  bruceb
 *        add getters for message collections
 *
 *        Revision 1.82  2007-06-14 04:12:48  bruceb
 *        added setForceUniqueNames()
 *
 *        Revision 1.81  2007-06-06 22:20:11  bruceb
 *        FTP_LINE_SEPARATOR now public
 *
 *        Revision 1.80  2007-05-29 04:17:24  bruceb
 *        modify connected() method, and null out control when quitting
 *
 *        Revision 1.79  2007-05-15 01:02:58  bruceb
 *        file factory change if syst doesn't work
 *
 *        Revision 1.78  2007/04/24 01:58:03  bruceb
 *        more debug for validateTransferOnError
 *
 *        Revision 1.77  2007/04/23 23:43:20  bruceb
 *        check on dirname in dirDetails()
 *
 *        Revision 1.76  2007/04/21 04:24:46  bruceb
 *        fix to cope with any ascii file
 *
 *        Revision 1.75  2007/03/22 04:03:47  bruceb
 *        added getOutputStream()
 *
 *        Revision 1.74  2007/03/19 22:07:36  bruceb
 *        set control to null
 *
 *        Revision 1.73  2007/03/13 02:45:08  bruceb
 *        deleteOnFailure flag added
 *
 *        Revision 1.72  2007/03/09 05:04:01  bruceb
 *        fixed bugs in ASCII put & get
 *
 *        Revision 1.71  2007/02/26 07:17:54  bruceb
 *        make various method package or protected visibility so they can be accessed by subclasses etc
 *
 *        Revision 1.70  2007/02/07 23:02:26  bruceb
 *        fixed failure to throw cancellation exception
 *
 *        Revision 1.69  2007/02/06 07:19:24  bruceb
 *        fixed autodetect bug re actual mode not being changed on the server
 *
 *        Revision 1.68  2007/02/04 23:02:40  bruceb
 *        more error logging and extra codes, set strict validation off
 *
 *        Revision 1.67  2007/02/01 06:05:18  bruceb
 *        enable cancelling of large directory listings
 *
 *        Revision 1.66  2007/01/15 23:05:14  bruceb
 *        added fileDetails()
 *
 *        Revision 1.65  2007/01/12 02:04:56  bruceb
 *        extracted string matchers & fixed exists()
 *
 *        Revision 1.64  2007/01/10 02:37:39  bruceb
 *        modify exists to use RETR if necessary
 *
 *        Revision 1.63  2006/12/12 01:04:54  hans
 *        Fixed bug in exists method and added logging of raw directory listing.
 *
 *        Revision 1.62  2006/10/27 15:44:06  bruceb
 *        added sendServerWakeup()
 *
 *        Revision 1.61  2006/10/17 11:03:41  bruceb
 *        fix setActivePortRange comment, include using single port info
 *
 *        Revision 1.60  2006/10/17 10:28:43  bruceb
 *        refactored to get setupDataSocket()
 *
 *        Revision 1.59  2006/10/11 08:38:00  bruceb
 *        controlEncoding applied to directory listings
 *
 *        Revision 1.58  2006/09/11 12:34:00  bruceb
 *        added exists() method
 *
 *        Revision 1.57  2006/08/23 08:48:51  bruceb
 *        don't null out FileFactory when quit() is called
 *
 *        Revision 1.56  2006/07/27 14:12:01  bruceb
 *        IPV6 changes and fixed bug re control channel messages after unexpected close on data connection
 *
 *        Revision 1.55  2006/05/22 01:54:42  hans
 *        Made remoteHost protected.
 *
 *        Revision 1.54  2006/02/16 19:47:09  hans
 *        Added comment
 *
 *        Revision 1.53  2005/11/15 21:02:32  bruceb
 *        more debug
 *
 *        Revision 1.52  2005/11/10 19:46:13  bruceb
 *        delegate resume comments to FTPClientInterface
 *
 *        Revision 1.51  2005/11/10 13:40:28  bruceb
 *        more elaborate versioning info to debug
 *
 *        Revision 1.50  2005/11/09 21:15:38  bruceb
 *        autodetect file types
 *
 *        Revision 1.49  2005/10/10 20:42:56  bruceb
 *        append now in FTPClientInterface
 *
 *        Revision 1.48  2005/09/29 16:03:06  bruceb
 *        permit 350 return from STOR
 *
 *        Revision 1.47  2005/09/21 10:38:06  bruceb
 *        fix for LIST error re empty dir (proFTPD/TLS)
 *
 *        Revision 1.46  2005/09/20 09:44:36  bruceb
 *        extra no files found string, SYST accepts 213
 *
 *        Revision 1.45  2005/09/02 21:03:04  bruceb
 *        no abort() with cancel
 *
 *        Revision 1.44  2005/08/26 17:48:16  bruceb
 *        passive ip address setting + ASCII optimisation
 *
 *        Revision 1.43  2005/06/17 18:25:56  bruceb
 *        fix javadoc
 *
 *        Revision 1.42  2005/06/16 21:39:49  hans
 *        deprecated ControlPort accessors and removed comments for FTPClientInterface methods
 *
 *        Revision 1.41  2005/06/10 15:44:38  bruceb
 *        added noOperation() and connected()
 *
 *        Revision 1.40  2005/06/03 11:25:17  bruceb
 *        ascii fixes, setActivePortRange
 *
 *        Revision 1.41  2005/05/24 11:32:28  bruceb
 *        version + timestamp info in static block
 *
 *        Revision 1.40  2005/05/15 19:46:28  bruceb
 *        changes for testing setActivePortRange + STOR accepting 350 nonstrict
 *
 *        Revision 1.39  2005/04/01 13:58:15  bruceb
 *        restructured dir() exception handling + quote() change
 *
 *        Revision 1.38  2005/03/18 11:04:32  bruceb
 *        deprecated constructors
 *
 *        Revision 1.37  2005/03/11 14:40:11  bruceb
 *        added cdup() and changed buffer defaults
 *
 *        Revision 1.36  2005/03/03 21:07:14  bruceb
 *        implement interface & augment login doco
 *
 *        Revision 1.35  2005/02/04 12:40:35  bruceb
 *        tidied javadoc
 *
 *        Revision 1.34  2005/02/04 12:28:51  bruceb
 *        when getting, if file exists and is readonly, exception is thrown
 *
 *        Revision 1.33  2005/01/28 13:55:39  bruceb
 *        added ACCT handling
 *
 *        Revision 1.32  2005/01/14 20:27:02  bruceb
 *        exception restructuring + ABOR
 *
 *        Revision 1.31  2004/11/19 08:28:10  bruceb
 *        added setPORTIP()
 *
 *        Revision 1.30  2004/10/18 15:54:48  bruceb
 *        clearSOCKS added, set encoding for control sock, locale for parser
 *
 *        Revision 1.29  2004/09/21 21:28:28  bruceb
 *        fixed javadoc comment
 *
 *        Revision 1.28  2004/09/18 14:27:57  bruceb
 *        features() throw exception if not supported
 *
 *        Revision 1.27  2004/09/18 09:33:47  bruceb
 *        1.1.8 tweaks
 *
 *        Revision 1.26  2004/09/17 14:12:38  bruceb
 *        fixed javadoc re filemasks
 *
 *        Revision 1.25  2004/09/14 06:24:03  bruceb
 *        fixed javadoc comment
 *
 *        Revision 1.24  2004/08/31 13:48:29  bruceb
 *        resume,features,restructure
 *
 *        Revision 1.23  2004/07/23 08:34:32  bruceb
 *        strict replies or not, better tfr monitor reporting
 *
 *        Revision 1.22  2004/06/25 11:47:46  bruceb
 *        made 1.1.x compatible
 *
 *        Revision 1.21  2004/06/11 10:20:35  bruceb
 *        permit 200 to be returned from various cmds
 *
 *        Revision 1.20  2004/05/22 16:52:57  bruceb
 *        message listener
 *
 *        Revision 1.19  2004/05/15 22:37:22  bruceb
 *        put debugResponses back in
 *
 *        Revision 1.18  2004/05/13 23:00:34  hans
 *        changed comment
 *
 *        Revision 1.17  2004/05/08 21:14:41  bruceb
 *        checkConnection stuff
 *
 *        Revision 1.14  2004/04/19 21:54:06  bruceb
 *        final tweaks to dirDetails() re caching
 *
 *        Revision 1.13  2004/04/18 11:16:44  bruceb
 *        made validateTransfer() public
 *
 *        Revision 1.12  2004/04/17 18:37:38  bruceb
 *        new parse functionality
 *
 *        Revision 1.11  2004/03/23 20:26:49  bruceb
 *        tweak to size(), catch exceptions on puts()
 *
 *        Revision 1.10  2003/11/15 11:23:55  bruceb
 *        changes required for ssl subclasses
 *
 *        Revision 1.6  2003/05/31 14:53:44  bruceb
 *        1.2.2 changes
 *
 *        Revision 1.5  2003/01/29 22:46:08  bruceb
 *        minor changes
 *
 *        Revision 1.4  2002/11/19 22:01:25  bruceb
 *        changes for 1.2
 *
 *        Revision 1.3  2001/10/09 20:53:46  bruceb
 *        Active mode changes
 *
 *        Revision 1.1  2001/10/05 14:42:03  bruceb
 *        moved from old project
 *
 */

package com.enterprisedt.net.ftp;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.net.InetAddress;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.Properties;
import java.util.TimeZone;
import java.util.Vector;

import com.enterprisedt.net.ftp.internal.FTPDataSocket;
import com.enterprisedt.util.debug.Level;
import com.enterprisedt.util.debug.Logger;

/**
 *  Supports client-side FTP. Most common
 *  FTP operations are present in this class.
 *
 *  @author      Bruce Blackshaw
 *  @version     $Revision: 1.122 $
 */
public class FTPClient implements FTPClientInterface {

    /**
     *  Revision control id
     */
    public static String cvsId = "@(#)$Id: FTPClient.java,v 1.122 2011-01-16 22:46:33 bruceb Exp $";
    
    /**
     * Default byte interval for transfer monitor
     */
    final public static int DEFAULT_MONITOR_INTERVAL = 65535;
    
    /**
     * Default transfer buffer size
     */
    final public static int DEFAULT_BUFFER_SIZE = 16384;
    
    /**
     * Maximum port number
     */
    final private static int MAX_PORT = 65535;
    
    /**
     * Default timeout
     */
    final public static int DEFAULT_TIMEOUT = 60*1000;
    
    /**
     * Short value for a timeout
     */
    final private static int SHORT_TIMEOUT = 500;
    
    /**
     * Default number of retries for file transfers
     */
    final public static int DEFAULT_RETRY_COUNT = 3;

    /**
     * Default retry delay in milliseconds
     */
    final public static int DEFAULT_RETRY_DELAY = 5000;
    
    /**
     * Default encoding used for control data
     */
    final public static String DEFAULT_ENCODING = "US-ASCII";
    
    /**
     * SOCKS port property name
     */
    final private static String SOCKS_PORT = "socksProxyPort";

    /**
     * SOCKS host property name
     */
    final private static String SOCKS_HOST = "socksProxyHost";
    
    /**
     * Line separator
     */
    final private static byte[] LINE_SEPARATOR = System.getProperty("line.separator").getBytes();
    
    /**
     * Used for ASCII translation
     */
    final public static byte CARRIAGE_RETURN = 13;
    
    
    /**
     * Used for ASCII translation
     */
    final public static byte LINE_FEED = 10;
    
    /**
     * Used for ASCII translation
     */
    final public static byte[] FTP_LINE_SEPARATOR = {CARRIAGE_RETURN, LINE_FEED};
                
    /**
     * Marker in reply for STOU reply with filename
     */
    final private static String STOU_FILENAME_MARKER = "FILE:";
    
    /**
     * Store command
     */
    final private static String STORE_CMD = "STOR ";
        
    /**
     * Store unique command
     */
    final private static String STORE_UNIQ_CMD = "STOU ";   
    
    /**
     * MFMT return string keyword
     */
    final private static String MODTIME_STR = "modtime";
    
    /**
     * Default locales
     */
    public static Locale[] DEFAULT_LISTING_LOCALES;    
       
    /**
     * Logging object
     */
    private static Logger log = Logger.getLogger("FTPClient");
    
    /**
     *  Format to interpret MTDM timestamp
     */
    private SimpleDateFormat tsFormat =
        new SimpleDateFormat("yyyyMMddHHmmss");
    
    /**
     *  Socket responsible for controlling
     *  the connection
     */
	protected FTPControlSocket control = null;

    /**
     *  Socket responsible for transferring
     *  the data
     */
    protected FTPDataSocket data = null;
    
    /**
     *  Socket timeout for both data and control. In
     *  milliseconds
     */
    protected int timeout = DEFAULT_TIMEOUT;
    
    /**
     * Interval in seconds in between server wakeups. O is
     * not enabled
     */
    protected int serverWakeupInterval = 0;
    
    /**
     * Address of the remote server.
     */
    protected InetAddress remoteAddr;
    
    /**
     * Name/IP of remote host
     */
    protected String remoteHost;
    
    /**
     * Id of instance
     */
    protected String id;
    
    /**
     * Master id 
     */
    private static int masterId = 0;
    
    /**
     * Control port number.
     */
    protected int controlPort = FTPControlSocket.CONTROL_PORT;
    
    /**
     * If true, uses the original host IP if an internal IP address
     * is returned by the server in PASV mode
     */
    private boolean autoPassiveIPSubstitution = true;
    
    /**
     * IP address to force to use in active mode
     */
    private String activeIP = null;
    
    /**
     * Encoding used on control socket
     */
    protected String controlEncoding = DEFAULT_ENCODING;
      
    /**
     * Use strict return codes if true
     */
    private boolean strictReturnCodes = false;
    
    /**
     * Matcher for directory empty
     */
    protected DirectoryEmptyStrings dirEmptyStrings = new DirectoryEmptyStrings();
    
    /**
     * Matcher for transfer complete
     */
    protected TransferCompleteStrings transferCompleteStrings = new TransferCompleteStrings();
    
    /**
     * Matcher for permission denied
     */
    protected FileNotFoundStrings fileNotFoundStrings = new FileNotFoundStrings();
    /**
     *  Can be used to cancel a transfer
     */
    private boolean cancelTransfer = false;
    
    /**
     * If true, a file transfer is being resumed
     */
    private boolean resume = false;
    
    /**
     * MDTM supported flag
     */
    private boolean mdtmSupported = true;
    
    /**
     * SIZE supported flag
     */
    private boolean sizeSupported = true;
    
    /**
     * Resume byte marker point
     */
    private long resumeMarker = 0;
    
    /**
     * Delete partial files on transfer failure?
     */
    private boolean deleteOnFailure = true;
    
    /**
     * If true, filetypes are autodetected and transfer mode changed to binary/ASCII as 
     * required
     */
    protected boolean detectTransferMode = false;
    
    /**
     * Lowest port in active mode port range
     */
    private int lowPort = -1;

    /**
     * Highest port in active mode port range
     */
    private int highPort = -1;
    
    /**
     * Command sent to server for storing a file
     */
    private String storeCommand = STORE_CMD;
    
    /**
     * Bytes transferred in between monitor callbacks
     */
    protected long monitorInterval = DEFAULT_MONITOR_INTERVAL;
    
    /**
     * Size of transfer buffers
     */
    protected int transferBufferSize = DEFAULT_BUFFER_SIZE;
    
    /**
     * Size of data socket's receive buffer
     */
    protected int dataReceiveBufferSize =0;
    
    /**
     * Size of data socket's send buffer
     */
    protected int dataSendBufferSize =0;
    
    /**
     * Count of downloaded files
     */
    private int downloadCount = 0;
    
    /**
     * Count of uploaded files
     */
    private int uploadCount = 0;
    
    /**
     * Count of deleted files
     */
    private int deleteCount = 0;
    
    /**
     * Number of times to retry a transfer operation before giving up.
     */
    private int retryCount = DEFAULT_RETRY_COUNT; 

    /**
     * Number of milliseconds to wait before retrying.
     */
    private int retryDelay = DEFAULT_RETRY_DELAY;  
    
    /**
     * Listen to all interfaces in active mode
     */
    private boolean listenOnAllInterfaces = true;
        
    /**
     * Parses LIST output
     */
    private FTPFileFactory fileFactory = null;
    
    /**
     * Locales for date parsing
     */
    private Locale[] listingLocales;
    
    /**
     * Parses the MLSD and MLST formats
     */
    private MLSXEntryParser mlsxParser = new MLSXEntryParser();
    
    /**
     *  Progress monitor
     */
    protected FTPProgressMonitor monitor = null;  
    
    /**
     * Message listener
     */
    protected FTPMessageListener messageListener = null;
    
    /**
     * File transfer listener
     */
    protected FTPProgressMonitorEx monitorEx = null;

    /**
     *  Record of the transfer type - make the default ASCII
     */
    protected FTPTransferType transferType = FTPTransferType.ASCII;

    /**
     *  Record of the connect mode - make the default PASV (as this was
     *  the original mode supported)
     */
    private FTPConnectMode connectMode = FTPConnectMode.PASV;

    /**
     *  Holds the last valid reply from the server on the control socket
     */
	protected FTPReply lastValidReply;   
    
    /**
     *  Holds the last reply from the server on the control socket
     */
    protected FTPReply lastReply;   
    
    /**
     * Username cached
     */
    protected String user;
    
    /**
     * Password cached
     */
    protected String password;
    
    /**
     * Threshold for throttling
     */
    protected BandwidthThrottler throttler = null;
    
    /**
     * Pasv callback method
     */
    protected DataChannelCallback dataChannelCallback = null;
    
    /**
     * set default listing locales.
     */
    static {
    	DEFAULT_LISTING_LOCALES = new Locale[2];
    	DEFAULT_LISTING_LOCALES[0] = Locale.ENGLISH;
    	DEFAULT_LISTING_LOCALES[1] = Locale.getDefault();
    }
    
    /**
     *  Instance initializer. Sets formatter to GMT.
     */
    {
        tsFormat.setTimeZone(TimeZone.getTimeZone("GMT"));
        listingLocales = DEFAULT_LISTING_LOCALES;
        id = Integer.toString(++masterId);
    }  
    
    
    /**
     * Get the version of edtFTPj
     * 
     * @return int array of {major,middle,minor} version numbers 
     */
    public static int[] getVersion() {
        return VersionDetails.getVersion();
    }
    
    /**
     * Get the build timestamp
     * 
     * @return d-MMM-yyyy HH:mm:ss z build timestamp 
     */
    public static String getBuildTimestamp() {
        return VersionDetails.getBuildTimestamp();
    }

    /**
     *  Constructor. Creates the control
     *  socket
     *
     *  @param   remoteHost  the remote hostname
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(String remoteHost)
        throws IOException, FTPException {

		this(remoteHost, FTPControlSocket.CONTROL_PORT, 0);
    }

    /**
     *  Constructor. Creates the control
     *  socket
     *
     *  @param   remoteHost  the remote hostname
     *  @param   controlPort  port for control stream (-1 for default port)
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(String remoteHost, int controlPort)
        throws IOException, FTPException {

		this(remoteHost, controlPort, 0);
    }
    
    
    /**
     *  Constructor. Creates the control
     *  socket
     *
     *  @param   remoteHost  the remote hostname
     *  @param   controlPort  port for control stream (use -1 for the default port)
     *  @param  timeout       the length of the timeout, in milliseconds
     *                        (pass in 0 for no timeout)
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(String remoteHost, int controlPort, int timeout)
    throws IOException, FTPException {

        this(InetAddress.getByName(remoteHost), controlPort, timeout);
    }

    /**
     *  Constructor. Creates the control
     *  socket
     *
     *  @param   remoteHost  the remote hostname
     *  @param   controlPort  port for control stream (use -1 for the default port)
     *  @param  timeout       the length of the timeout, in milliseconds
     *                        (pass in 0 for no timeout)
     *  @param   encoding         character encoding used for data
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(String remoteHost, int controlPort, int timeout, String encoding)
        throws IOException, FTPException {

        this(InetAddress.getByName(remoteHost), controlPort, timeout, encoding);
    }

    /**
     *  Constructor. Creates the control
     *  socket
     *
     *  @param   remoteAddr  the address of the
     *                       remote host
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(InetAddress remoteAddr)
        throws IOException, FTPException {

		this(remoteAddr, FTPControlSocket.CONTROL_PORT, 0);
    }
    

    /**
     *  Constructor. Creates the control
     *  socket. Allows setting of control port (normally
     *  set by default to 21).
     *
     *  @param   remoteAddr  the address of the
     *                       remote host
     *  @param   controlPort  port for control stream
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(InetAddress remoteAddr, int controlPort)
        throws IOException, FTPException {

		this(remoteAddr, controlPort, 0);
    }

    /**
     *  Constructor. Creates the control
     *  socket. Allows setting of control port (normally
     *  set by default to 21).
     *
     *  @param   remoteAddr    the address of the
     *                          remote host
     *  @param   controlPort   port for control stream (-1 for default port)
     *  @param  timeout        the length of the timeout, in milliseconds 
     *                         (pass in 0 for no timeout)
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(InetAddress remoteAddr, int controlPort, int timeout)
        throws IOException, FTPException {
        if (controlPort < 0)
            controlPort = FTPControlSocket.CONTROL_PORT;
		initialize(new FTPControlSocket(remoteAddr, controlPort, timeout, DEFAULT_ENCODING, null));
    }
    
    /**
     *  Constructor. Creates the control
     *  socket. Allows setting of control port (normally
     *  set by default to 21).
     *
     *  @param   remoteAddr    the address of the
     *                          remote host
     *  @param   controlPort   port for control stream (-1 for default port)
     *  @param   timeout        the length of the timeout, in milliseconds 
     *                         (pass in 0 for no timeout)
     *  @param   encoding         character encoding used for data
     *  @deprecated  use setter methods to set properties
     */
    public FTPClient(InetAddress remoteAddr, int controlPort, int timeout, String encoding)
        throws IOException, FTPException {
        if (controlPort < 0)
            controlPort = FTPControlSocket.CONTROL_PORT;
        initialize(new FTPControlSocket(remoteAddr, controlPort, timeout, encoding, null));
    }
    
    /**
     *  Default constructor should now always be used together with setter methods
     *  in preference to other constructors (now deprecated). The {@link #connect()}
     *  method is used to perform the actual connection to the remote host - but only
     *  for this constructor. Deprecated constructors connect in the constructor and
     *  connect() is not required (and cannot be called).
     */
    public FTPClient() {
        log.debug(VersionDetails.report(this));
    }
    
    /**
     * Connects to the server at the address and port number defined
     * in the constructor. Must be performed <b>before</b> login() or user() is
     * called.
     * 
     * @throws IOException Thrown if there is a TCP/IP-related error.
     * @throws FTPException Thrown if there is an error related to the FTP protocol. 
     */
    public void connect() throws IOException, FTPException {

        checkConnection(false);
        
        if (remoteAddr == null)
            remoteAddr = InetAddress.getByName(remoteHost);
       
        log.debug("Connecting to " + remoteAddr + ":" + controlPort);
        
        initialize(new FTPControlSocket(remoteAddr, controlPort, timeout, 
                                         controlEncoding, messageListener));
    }
    
    /**
     * Is this client connected? 
     * 
     * @return true if connected, false otherwise
     */
    public boolean connected() {
        if (control==null)
           return false;
        else {
            return control.controlSock == null ? false : control.controlSock.isConnected();
        }
    }
    
    /**
     * Checks if the client has connected to the server and throws an exception if it hasn't.
     * This is only intended to be used by subclasses
     * 
     * @throws FTPException Thrown if the client has not connected to the server.
     */
    protected void checkConnection(boolean shouldBeConnected) throws FTPException {
    	if (shouldBeConnected && !connected())
    		throw new FTPException("The FTP client has not yet connected to the server.  "
    				+ "The requested action cannot be performed until after a connection has been established.");
    	else if (!shouldBeConnected && connected())
    		throw new FTPException("The FTP client has already been connected to the server.  "
    				+"The requested action must be performed before a connection is established.");
    }
    	
    /**
     * Set the control socket explicitly
     * 
     * @param control   control socket reference
     */
	protected void initialize(FTPControlSocket control) throws IOException {
		this.control = control;
        control.setMessageListener(messageListener);
        control.setStrictReturnCodes(strictReturnCodes);
        control.setListenOnAllInterfaces(listenOnAllInterfaces);
        control.setTimeout(timeout);
        control.setAutoPassiveIPSubstitution(autoPassiveIPSubstitution);
        control.setDataChannelCallback(dataChannelCallback);
        if (activeIP != null)
            control.setActivePortIPAddress(activeIP);
        if (lowPort > 0 && highPort > 0)
            control.setActivePortRange(lowPort, highPort);
	}
    
    /**
     *  Switch debug of responses on or off
     *
     *  @param  on  true if you wish to have responses to
     *              the log stream, false otherwise
     *  @deprecated  use the Logger class to switch debugging on and off
     */
    public void debugResponses(boolean on) {
        if (on)
            Logger.setLevel(Level.DEBUG);
        else
            Logger.setLevel(Level.OFF);
    }    
    
    /**
     * Get the identifying string for this instance
     */
    public String getId() {
        return id;
    }

    /**
     * Set the identifying string for this instance
     * 
     * @param id    identifying string
     */
    public void setId(String id) {
        this.id = id;
    }
    
    /**
     * Get the number of files downloaded since the count was
     * reset
     * 
     * @return  download file count
     */
    public int getDownloadCount() {
        return downloadCount;
    }
    
    /**
     * Reset the count of downloaded files to zero.
     *
     */
    public void resetDownloadCount() {
        downloadCount = 0;
    }
    
    /**
     * Get the number of files uploaded since the count was
     * reset
     * 
     * @return  upload file count
     */
    public int getUploadCount() {
        return uploadCount;
    }
    
    /**
     * Reset the count of uploaded files to zero.
     *
     */
    public void resetUploadCount() {
        uploadCount = 0;
    }
    
    /**
     * Get the number of files deleted since the count was
     * reset
     * 
     * @return  deleted file count
     */
    public int getDeleteCount() {
        return deleteCount;
    }
    
    /**
     * Reset the count of deleted files to zero.
     *
     */
    public void resetDeleteCount() {
        deleteCount = 0;
    }
    
    /**
     * Set the data channel callback, which notifies of the
     * ip and port number to be connected to, and gives an opportunity
     * to modify these values
     * 
     * @param callback  callback to set
     */
    public void setDataChannelCallback(DataChannelCallback callback) {
        this.dataChannelCallback = callback;
        if (control != null)
            control.setDataChannelCallback(callback);
    }
    

    /**
     * Set strict checking of FTP return codes. If strict 
     * checking is on (the default) code must exactly match the expected 
     * code. If strict checking is off, only the first digit must match.
     * 
     * @param strict    true for strict checking, false for loose checking
     */
    public void setStrictReturnCodes(boolean strict) {
        this.strictReturnCodes = strict;
        if (control != null)
            control.setStrictReturnCodes(strict);
    }
    
    /**
     * Determine if strict checking of return codes is switched on. If it is 
     * (the default), all return codes must exactly match the expected code.  
     * If strict checking is off, only the first digit must match.
     * 
     * @return  true if strict return code checking, false if non-strict.
     */
    public boolean isStrictReturnCodes() {
        return strictReturnCodes;
    }
    
    /**
     * Listen on all interfaces for active mode transfers (the default).
     * 
     * @param listenOnAll   true if listen on all interfaces, false to listen on the control interface
     */
    public void setListenOnAllInterfaces(boolean listenOnAll) {
        this.listenOnAllInterfaces = listenOnAll;
        if (control != null)
            control.setListenOnAllInterfaces(listenOnAll);
    }
    
    /**
     * Are we listening on all interfaces in active mode, which is the default?
     * 
     * @return true if listening on all interfaces, false if listening just on the control interface
     */
    public boolean getListenOnAllInterfaces() {
        return listenOnAllInterfaces;
    }
    
    /**
     * Get class that holds fragments of server messages that indicate a file was 
     * not found. New messages can be added.
     * <p>
     * The fragments are used when it is necessary to examine the message
     * returned by a server to see if it is saying a file was not found. 
     * If an FTP server is returning a different message that still clearly 
     * indicates a file was not found, use this property to add a new server 
     * fragment to the repository via the add method. It would be helpful to
     * email support at enterprisedt dot com to inform us of the message so
     * it can be added to the next build.
     * 
     * @return  messages class
     */
    public FileNotFoundStrings getFileNotFoundMessages() {
        return fileNotFoundStrings;
    }
    
    /**
     * Set a new instance of the strings class
     * 
     * @param fileNotFoundStrings  new instance
     */
    public void setFileNotFoundMessages(FileNotFoundStrings fileNotFoundStrings) {
        this.fileNotFoundStrings = fileNotFoundStrings;
    }
    
    /**
     * Get class that holds fragments of server messages that indicate a transfer completed. 
     * New messages can be added.
     * <p>
     * The fragments are used when it is necessary to examine the message
     * returned by a server to see if it is saying a transfer completed. 
     * If an FTP server is returning a different message that still clearly 
     * indicates a transfer failed, use this property to add a new server 
     * fragment to the repository via the add method. It would be helpful to
     * email support at enterprisedt dot com to inform us of the message so
     * it can be added to the next build.
     * 
     * @return  messages class
     */
    public TransferCompleteStrings getTransferCompleteMessages() {
        return transferCompleteStrings;
    }
    
    /**
     * Set a new instance of the strings class
     * 
     * @param transferCompleteStrings  new instance
     */
    public void setTransferCompleteMessages(TransferCompleteStrings transferCompleteStrings) {
        this.transferCompleteStrings = transferCompleteStrings;
    }
    
    /**
     * Get class that holds fragments of server messages that indicate a  
     * directory is empty. New messages can be added.
     * <p>
     * The fragments are used when it is necessary to examine the message
     * returned by a server to see if it is saying a directory is empty. 
     * If an FTP server is returning a different message that still clearly 
     * indicates a directory is empty, use this property to add a new server 
     * fragment to the repository via the add method. It would be helpful to
     * email support at enterprisedt dot com to inform us of the message so
     * it can be added to the next build.
     * 
     * @return  messages class
     */
    public DirectoryEmptyStrings getDirectoryEmptyMessages() {
        return dirEmptyStrings;
    }
    
    /**
     * Set a new instance of the strings class
     * 
     * @param dirEmptyStrings  new instance
     */
    public void setDirectoryEmptyMessages(DirectoryEmptyStrings dirEmptyStrings) {
        this.dirEmptyStrings = dirEmptyStrings;
    }
    
    
    /* (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#setDetectTransferMode(boolean)
     */
    public void setDetectTransferMode(boolean detectTransferMode) {
        this.detectTransferMode = detectTransferMode;
    }

    /* (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#getDetectTransferMode()
     */
    public boolean getDetectTransferMode() {
         return detectTransferMode;
    }
    
    /**
     * Set to true if the STOU command is always to be used when
     * uploading files, even if a filename is supplied. Normally
     * STOU is only used if the supplied remote filename is null or
     * the empty string.
     * 
     * @param forceUnique   true if STOU is always to be used
     */
    public void setForceUniqueNames(boolean forceUnique) {
        if (forceUnique)
            storeCommand = STORE_UNIQ_CMD;
        else
            storeCommand = STORE_CMD;
    }

    /**
     * Switch the transfer mode if requested and if necessary
     * 
     * @param filename      filename of file to be transferred
     * @throws FTPException 
     * @throws IOException 
     */
    protected FTPTransferType chooseTransferMode(String filename) 
        throws IOException, FTPException {
        if (detectTransferMode) {
            if (filename == null) {
                log.warn("Cannot choose transfer mode as filename not supplied");
                return getType();
            }
            if (FileTypes.ASCII.matches(filename) && 
                transferType.equals(FTPTransferType.BINARY)) {
                setType(FTPTransferType.ASCII);
                log.debug("Autodetect on - changed transfer type to ASCII");
            }
            else if (FileTypes.BINARY.matches(filename) && 
                      transferType.equals(FTPTransferType.ASCII)) {
                setType(FTPTransferType.BINARY);
                log.debug("Autodetect on - changed transfer type to binary");
            }
        }
        return getType();
    }

    /**
     *   Set the SO_TIMEOUT in milliseconds on the underlying socket.
     *   If set this means the socket will block in a read operation
     *   only for this length of time - useful if the FTP sever has 
     *   hung, for example. The default is 60,000 milliseconds. 
     *
     *   Note that for JREs 1.4+, the timeout is also used when first 
     *   connecting to the remote host. 
     *
     *   @param millis The length of the timeout, in milliseconds
     */
    public void setTimeout(int millis)
        throws IOException {

        this.timeout = millis;
        if (control != null)
            control.setTimeout(millis);
    }
    
 
    /**
     *  Get the TCP timeout 
     *  
     *  @return timeout that is used, in milliseconds
     */
    public int getTimeout() {
        return timeout;
    }
    
    /**
     * Returns the control-port being connected to on the remote server. 
     * 
     * Note that this method replaces {@link #getControlPort()}.
     * 
     * @return Returns the port being connected to on the remote server. 
     */
    public int getRemotePort() {
        return controlPort;
    }
    
    /** 
     * Set the control to connect to on the remote server. Can only do this if
     * not already connected.
     * 
     * Note that this method replaces {@link #setControlPort(int)}.
     * 
     * @param remotePort The port to use. 
     * @throws FTPException Thrown if the client is already connected to the server.
     */
    public void setRemotePort(int remotePort) throws FTPException {
        checkConnection(false);
        this.controlPort = remotePort;
    }

    /**
     * Returns the control-port being connected to on the remote server. 
     * @return Returns the port being connected to on the remote server. 
     * @deprecated Use {@link com.enterprisedt.net.ftp.FTPClientInterface#getRemotePort()} instead.
     */
    public int getControlPort() {
        return controlPort;
    }
    
    /** 
     * Set the control to connect to on the remote server. Can only do this if
     * not already connected.
     * 
     * @param controlPort The port to use. 
     * @throws FTPException Thrown if the client is already connected to the server.
     * @deprecated Use {@link com.enterprisedt.net.ftp.FTPClientInterface#setRemotePort(int)} instead.
     */
    public void setControlPort(int controlPort) throws FTPException {
        checkConnection(false);
        this.controlPort = controlPort;
    }
    
    /**
     * @return Returns the remoteAddr.
     */
    public InetAddress getRemoteAddr() {
        return remoteAddr;
    }
    
    /**
     * Set the remote address
     * 
     * @param remoteAddr The remoteAddr to set.
     * @throws FTPException
     */
    public void setRemoteAddr(InetAddress remoteAddr) throws FTPException {
        checkConnection(false);
        this.remoteAddr = remoteAddr;
        this.remoteHost = remoteAddr.getHostAddress();
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#getRemoteHost()
     */
    public String getRemoteHost() {
        return remoteHost;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#setRemoteHost(java.lang.String)
     */
    public void setRemoteHost(String remoteHost) throws IOException, FTPException {
        checkConnection(false);
        this.remoteHost = remoteHost;
    }
    
    
    /**
     * Is automatic substitution of the remote host IP set to
     * be on for passive mode connections?
     * 
     * @return true if set on, false otherwise
     */
    public boolean isAutoPassiveIPSubstitution() {
        return autoPassiveIPSubstitution;
    }

    /**
     * Set automatic substitution of the remote host IP on if
     * in passive mode. If proxies are used (e.g. SOCKS) then this
     * setting is ignored.
     * 
     * @param autoPassiveIPSubstitution true if set to on, false otherwise
     */
    public void setAutoPassiveIPSubstitution(boolean autoPassiveIPSubstitution) {
        this.autoPassiveIPSubstitution = autoPassiveIPSubstitution;
        if (control != null)
            control.setAutoPassiveIPSubstitution(autoPassiveIPSubstitution);
    }
    
    /**
     * Get server wakeup interval in seconds. A value of 0
     * means it is disabled (the default).
     * 
     * @return interval in seconds
     */
    public int getServerWakeupInterval() {
        return serverWakeupInterval;
    }
    
    /**
     * Set server wakeup interval in seconds. A value of 0 
     * means it is disabled (the default). This may hang or confuse 
     * the FTP server - use with caution.
     * 
     * @param interval  interval in seconds
     */
    public void setServerWakeupInterval(int interval) {
        this.serverWakeupInterval = interval;
    }
    
    
    /**
     * Get the encoding used for the control connection
     * 
     * @return Returns the current controlEncoding.
     */
    public String getControlEncoding() {
        return controlEncoding;
    }
    
    /**
     * Set the size of the data socket's receive buffer.
     * 
     * @param size  must be > 0
     */
    public void setDataReceiveBufferSize(int size) {
        this.dataReceiveBufferSize = size;
    }
    
    /**
     * Get the size of the data socket's receive buffer.
     * A value of 0 means the defaults are being used.
     * 
     * @return  size
     */
    public int getDataReceiveBufferSize() {
        return dataReceiveBufferSize;
    }
    
    /**
     * Set the size of the data socket's send buffer.
     * 
     * @param size  must be > 0
     */
    public void setDataSendBufferSize(int size) {
        this.dataSendBufferSize = size;
    }
    
    /**
     * Get the size of the data socket's send buffer.
     * A value of 0 means the defaults are being used.
     * 
     * @return  size
     */
    public int getDataSendBufferSize() {
        return dataSendBufferSize;
    }
    
    
    /**
     * Set the control socket's encoding. Can only do this if
     * not connected
     * 
     * @param controlEncoding The controlEncoding to set, which is the name of a Charset
     * @see java.nio.charset.Charset
     * @throws FTPException
     */
    public void setControlEncoding(String controlEncoding) throws FTPException {
        checkConnection(false);
        this.controlEncoding = controlEncoding;
    }
    /**
     * @return Returns the messageListener.
     */
    public FTPMessageListener getMessageListener() {
        return messageListener;
    }
    
    /**
     * Set a listener that handles all FTP messages
     * 
     * @param listener  message listener
     */
    public void setMessageListener(FTPMessageListener listener) {
        this.messageListener = listener;
        if (control != null)
           control.setMessageListener(listener);
    }
    
    /**
     * Get reference to the transfer listener
     * 
     * @return FTPProgressMonitorEx
     */
    public FTPProgressMonitorEx getProgressMonitorEx() {
        return monitorEx;
    }

    /**
     * Set reference to the transfer listener
     * 
     * @param monitorEx  transfer listener
     */
    public void setProgressMonitorEx(FTPProgressMonitorEx monitorEx) {
        this.monitorEx = monitorEx;
        this.monitor = monitorEx;
    }

    /**
     *  Set the connect mode
     *
     *  @param  mode  ACTIVE or PASV mode
     */
    public void setConnectMode(FTPConnectMode mode) {
        connectMode = mode;
    }
    
    /**
     * @return Returns the connectMode.
     */
    public FTPConnectMode getConnectMode() {
        return connectMode;
    }
    

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#setProgressMonitor(com.enterprisedt.net.ftp.FTPProgressMonitor, long)
     */
    public void setProgressMonitor(FTPProgressMonitor monitor, long interval) {
        this.monitor = monitor;
        this.monitorInterval = interval;
    }    

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#setProgressMonitor(com.enterprisedt.net.ftp.FTPProgressMonitor)
     */
    public void setProgressMonitor(FTPProgressMonitor monitor) {
        this.monitor = monitor;
    }   
    
    /**
     * Get the reference to the progress monitor
     * 
     * @return  progress monitor
     */
    public FTPProgressMonitor getProgressMonitor() {
        return monitor;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#getMonitorInterval()
     */
    public long getMonitorInterval() {
        return monitorInterval;
    }
    
    /**
     *  Set the number of bytes transferred between each callback on the
     *  progress monitor
     * 
     * param interval     bytes to be transferred before a callback
     */
    public void setMonitorInterval(long interval) {
        this.monitorInterval = interval;
    }
    
    /**
     * Set the size of the buffers used in writing to and reading from
     * the data sockets
     * 
     * @param size  new size of buffer in bytes
     */
    public void setTransferBufferSize(int size) {
        transferBufferSize = size;
    }
    
    /**
     * Get the size of the buffers used in writing to and reading from
     * the data sockets
     * 
     * @return  transfer buffer size
     */
    public int getTransferBufferSize() {
        return transferBufferSize;
    }
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#cancelTransfer()
     */
    public void cancelTransfer() {
        cancelTransfer = true;
        log.warn("cancelTransfer() called");
    } 
    
    /**
     * Has the current transfer been cancelled?
     * 
     * @return true if cancel, false otherwise
     */
    public boolean isTransferCancelled() {
        return cancelTransfer;
    }
    
    /**
     * If true, delete partially written files when exceptions are thrown
     * during a download
     * 
     * @return true if delete local file on error
     */
    public boolean isDeleteOnFailure() {
        return deleteOnFailure;
    }

    /**
     * Switch on or off the automatic deletion of partially written files 
     * that are left when an exception is thrown during a download
     * 
     * @param deleteOnFailure  true if delete when a failure occurs
     */
    public void setDeleteOnFailure(boolean deleteOnFailure) {
        this.deleteOnFailure = deleteOnFailure;
    }
    
    /**
     * We can force PORT to send a fixed IP address, which can be useful with certain
     * NAT configurations. Must be connected to the remote host to call this method.
     * 
     * @param IPAddress     IP address to force, in 192.168.1.0 form
     * @deprecated
     */
    public void setPORTIP(String IPAddress) 
        throws FTPException {
        setActiveIPAddress(IPAddress);
    }
    
    /**
     * We can force PORT to send a fixed IP address, which can be useful with certain
     * NAT configurations. Must be connected to the remote host to call this method.
     * 
     * @param activeIP     IP address to force, in 192.168.1.0 form or in IPV6 form, e.g.
     *                            1080::8:800:200C:417A
     */
    public void setActiveIPAddress(String activeIP) 
        throws FTPException {
        
        this.activeIP = activeIP;
        if (control != null)
            control.setActivePortIPAddress(activeIP);
    }
    
    /**
     * Get the active IP address that is set.
     * 
     * @return  active IP address or null if not set
     */
    public String getActiveIPAddress() {
        return activeIP;
    }
    
    /**
     * Force a certain range of ports to be used in active mode. This is
     * generally so that a port range can be configured in a firewall. Note
     * that if lowest == highest, a single port will be used. This works well
     * for uploads, but downloads generally require multiple ports, as most
     * servers fail to create a connection repeatedly for the same port.
     * 
     * @param lowest     Lower limit of range.
     * @param highest    Upper limit of range.
     */
    public void setActivePortRange(int lowest, int highest) 
        throws FTPException {
                
        this.lowPort = lowest;
        this.highPort = highest;
        
        if (lowest < 0 || lowest > highest || highest > MAX_PORT)
            throw new FTPException("Invalid port range specified");
        
        if (control != null)        
            control.setActivePortRange(lowest, highest);
        
        log.debug("setActivePortRange(" + lowest + "," + highest + ")");
    }
    
    /**
     * Get the lower limit of the port range for active mode.
     * 
     * @return lower limit, or -1 if not set
     */
    public int getActiveLowPort() {
        return lowPort;
    }

    /**
     * Get the upper limit of the port range for active mode.
     * 
     * @return upper limit, or -1 if not set
     */
    public int getActiveHighPort() {
        return highPort;
    }
    
       
    /**
     *  Login into an account on the FTP server. This
     *  call completes the entire login process. Note that
     *  connect() must be called first.
     *
     *  @param   user       user name
     *  @param   password   user's password
     */
    public void login(String user, String password)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
    	this.user = user;
    	this.password = password;
        
        user(user);

        if (lastValidReply.getReplyCode().equals("230") || lastValidReply.getReplyCode().equals("232"))
            return;
        else {
            password(password);
        }
    }
    
    /**
     *  Login into an account on the FTP server. This call completes the 
     *  entire login process. This method permits additional account information 
     *  to be supplied. FTP servers can use combinations of these parameters in 
     *  many different ways, e.g. to pass in proxy details via this method, some 
     *  servers use the "user" as 'ftpUser + "@" + ftpHost + " " + ftpProxyUser', 
     *  the "password" as the FTP user's password, and the accountInfo as the proxy 
     *  password. Note that connect() must be called first.
     *
     *  @param   user           user name
     *  @param   password       user's password
     *  @param   accountInfo    account info string
     */
    public void login(String user, String password, String accountInfo)
        throws IOException, FTPException {
        
        checkConnection(true);
        
        this.user = user;
        this.password = password;
        
        user(user);

        if (lastValidReply.getReplyCode().equals("230") || lastValidReply.getReplyCode().equals("232")) // no pwd
            return;
        else {
            password(password);
            if (lastValidReply.getReplyCode().equals("332")) // requires acct info
                account(accountInfo);
        }
    }    
    
    /**
     *  Supply the user name to log into an account
     *  on the FTP server. Must be followed by the
     *  password() method - but we allow for no password.
     *  Note that connect() must be called first.
     *
     *  @param   user       user name
     */
    public void user(String user)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
    	this.user = user;
    	
        lastReply = control.sendCommand("USER " + user);

        // we allow for a site with no password - 230 response
        String[] validCodes = {"230", "232", "331"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }


    /**
     *  Supplies the password for a previously supplied
     *  username to log into the FTP server. Must be
     *  preceeded by the user() method
     *
     *  @param   password       The password.
     */
    public void password(String password)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        this.password = password;
    	
        lastReply = control.sendCommand("PASS " + password);

        // we allow for a site with no passwords (202) or requiring
        // ACCT info (332)
        String[] validCodes = {"230", "202", "332"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }
    
    
    /**
     *  Supply account information string to the server. This can be
     *  used for a variety of purposes - for example, the server could
     *  indicate that a password has expired (by sending 332 in reply to
     *  PASS) and a new password automatically supplied via ACCT. It
     *  is up to the server how it uses this string.
     *
     *  @param   accountInfo    account information string
     */
    public void account(String accountInfo)
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("ACCT " + accountInfo);

        // ok or not implemented
        String[] validCodes = {"230", "202"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }


    /**
     *  Set up SOCKS v4/v5 proxy settings. This can be used if there
     *  is a SOCKS proxy server in place that must be connected thru.
     *  Note that setting these properties directs <b>all</b> TCP
     *  sockets in this JVM to the SOCKS proxy
     *
     *  @param  port  SOCKS proxy port
     *  @param  host  SOCKS proxy hostname
     */
    public static void initSOCKS(String port, String host) {
        Properties props = System.getProperties();
        props.put(SOCKS_PORT, port);
        props.put(SOCKS_HOST, host);
        System.setProperties(props);
    }

    /**
     *  Set up SOCKS username and password for SOCKS username/password
     *  authentication. Often, no authentication will be required
     *  but the SOCKS server may be configured to request these.
     *
     *  @param  username   the SOCKS username
     *  @param  password   the SOCKS password
     */
    public static void initSOCKSAuthentication(String username,
                                               String password) {
        Properties props = System.getProperties();
        props.put("java.net.socks.username", username);
        props.put("java.net.socks.password", password);
        System.setProperties(props);
    }
    
    /**
     * Clear SOCKS settings. Note that setting these properties affects 
     * <b>all</b> TCP sockets in this JVM
     */
    public static void clearSOCKS() {
        
        Properties prop = System.getProperties(); 
        prop.remove(SOCKS_HOST); 
        prop.remove(SOCKS_PORT); 
        System.setProperties(prop); 
    }

    /**
     *  Get the name of the remote host
     *
     *  @return  remote host name
     */
    String getRemoteHostName() {
        return control.getRemoteHostName();
    }

    /**
     *  Issue arbitrary ftp commands to the FTP server.
     *
     *  @param command     ftp command to be sent to server
     *  @param validCodes  valid return codes for this command. If null
     *                      is supplied no validation is performed
     * 
     *  @return  the text returned by the FTP server
     */
    public String quote(String command, String[] validCodes)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand(command);

        // allow for no validation to be supplied
        if (validCodes != null) {
            lastValidReply = control.validateReply(lastReply, validCodes);
        }
        else { // no validation
            lastValidReply = lastReply; // assume valid
        }
        return lastValidReply.getReplyText();       
    }
    
    /**
     *  Issue arbitrary ftp commands to the FTP server.
     *
     *  @param command     ftp command to be sent to server
     *  @return  the raw text returned by the FTP server including reply code
     */
    public String quote(String command)
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastValidReply = control.sendCommand(command);        
        return lastValidReply.getRawReply();   
    }
    
    /**
     * Request that the remote server execute the literal command supplied. 
     * This is a synonym for the quote() command.
     * 
     * @param command   command string
     * @return result string by server
     * @throws FTPException
     * @throws IOException
     */
    public String executeCommand(String command) 
        throws FTPException, IOException {
        return quote(command);
    }
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#exists(java.lang.String)
     */
    public boolean exists(String remoteFile) throws IOException, FTPException {
        checkConnection(true);
        
        // first try the SIZE command
        if (sizeSupported)
        {
            lastReply = control.sendCommand("SIZE " + remoteFile);
            char ch = lastReply.getReplyCode().charAt(0);
            if (ch == '2')
                return true;
            if (ch == '5' && fileNotFoundStrings.matches(lastReply.getReplyText()))
                return false;
                
            sizeSupported = false;
            log.debug("SIZE not supported - trying MDTM");
        }

        // then try the MDTM command
        if (mdtmSupported)
        {
            lastReply = control.sendCommand("MDTM " + remoteFile);
            char ch = lastReply.getReplyCode().charAt(0);
            if (ch == '2')
                return true;
            if (ch == '5' && fileNotFoundStrings.matches(lastReply.getReplyText()))
                return false;
             
            mdtmSupported = false;
            log.debug("MDTM not supported - trying LIST");
        }

        try {
            FTPFile[] files = dirDetails(".");
            for (int i = 0; i < files.length; i++) {
                if (files[i].getName().equals(remoteFile)) {
                    if (files[i].isFile())
                        return true;
                    else
                        return false;
                }
            }
            return false;
        }
        catch (ParseException ex) {
            log.warn(ex.getMessage());
            return false;
        }
    }
    
    
    /**
     * Read reply from control socket
     * 
     * @return
     * @throws IOException
     */
    FTPReply readReply() throws IOException, FTPException {
        return control.readReply();
    }
    
    /**
     * Get the PASV address string (including port numbers)
     * @param pasvReply
     * @return
     */
    String getPASVAddress(String pasvReply) {
        int start = -1;
        int i = 0;
        while (i < pasvReply.length()) {
            if (Character.isDigit(pasvReply.charAt(i))) {
                start = i;
                break;
            }
            i++;
        }
        int end = -1;
        i = pasvReply.length() -1;
        while (i >= 0) {
            if (Character.isDigit(pasvReply.charAt(i))) {
                end = i;
                break;
            }
            i--;
        }
        if (start < 0 || end < 0)
            return null;
        
        return pasvReply.substring(start, end+1);
    }
    
    /**
     * Send a command to the server and get the reply
     * @param command   command
     * @return FTPReply
     * @throws IOException
     * @throws FTPException 
     */
    public FTPReply sendCommand(String command) throws IOException, FTPException  {
        return control.sendCommand(command);
    }
    
    /**
     * Validate an FTPReply 
     * 
     * @param reply         reply object
     * @param expectedReplyCode  expected code
     * @throws FTPException
     */
    public void validateReply(FTPReply reply, String expectedReplyCode) throws FTPException {
        control.validateReply(reply, expectedReplyCode);
    }
    
    /**
     * Validate an FTPReply 
     * 
     * @param reply         reply object
     * @param expectedReplyCodes  expected codes
     * @throws FTPException
     */
    public void validateReply(FTPReply reply, String[] expectedReplyCodes) throws FTPException {
        control.validateReply(reply, expectedReplyCodes);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#size(java.lang.String)
     */
     public long size(String remoteFile)
         throws IOException, FTPException {
     	
     	 checkConnection(true);
     	
         lastReply = control.sendCommand("SIZE " + remoteFile);
         lastValidReply = control.validateReply(lastReply, "213");

         // parse the reply string .
         String replyText = lastValidReply.getReplyText();
         
         // trim off any trailing characters after a space, e.g. webstar
         // responds to SIZE with 213 55564 bytes
         int spacePos = replyText.indexOf(' ');
         if (spacePos >= 0)
             replyText = replyText.substring(0, spacePos);
         
         // parse the reply
         try {
             return Long.parseLong(replyText);
         }
         catch (NumberFormatException ex) {
             throw new FTPException("Failed to parse reply: " + replyText);
         }         
     }
     
     /*
      *  (non-Javadoc)
      * @see com.enterprisedt.net.ftp.FTPClientInterface#resume()
      */
     public void resume() throws FTPException {
         if (transferType.equals(FTPTransferType.ASCII))
             throw new FTPException("Resume only supported for BINARY transfers");
         resume = true;
         log.info("Resume=true");
     }
     
     /*
      *  (non-Javadoc)
      * @see com.enterprisedt.net.ftp.FTPClientInterface#cancelResume()
      */
     public void cancelResume() 
         throws IOException, FTPException {
         try {
             restart(0);
         }
         catch (FTPException ex){                
             log.debug("REST failed which is ok (" + ex.getMessage() + ")");
         }
         resumeMarker = 0;
         resume = false;
     }
     
     /**
      * Force the resume flag off. Internal use only.
      */
     protected void forceResumeOff() {
         resume = false;
     }
     
     /**
      * Issue the RESTart command to the remote server. This indicates the byte
      * position that REST is performed at. For put, bytes start at this point, while
      * for get, bytes are fetched from this point.
      * 
      * @param size     the REST param, the mark at which the restart is 
      *                  performed on the remote file. For STOR, this is retrieved
      *                  by SIZE
      * @throws IOException
      * @throws FTPException
      */
     public void restart(long size) 
         throws IOException, FTPException {
         lastReply = control.sendCommand("REST " + size);
         lastValidReply = control.validateReply(lastReply, "350");
     }
     
     
     /**
      * Get the retry count for retrying file transfers. Default
      * is 3 attempts.
      * 
      * @return number of times a transfer is retried
      */
     public int getRetryCount() {
         return retryCount;
     }

     /**
      * Set the retry count for retrying file transfers.
      * 
      * @param retryCount    new retry count
      */
     public void setRetryCount(int retryCount) {
         this.retryCount = retryCount;
     }

     /**
      * Get the retry delay between retry attempts, in milliseconds.
      * Default is 5000.
      * 
      * @return  retry delay in milliseconds
      */
     public int getRetryDelay() {
         return retryDelay;
     }

     /**
      * Set the retry delay between retry attempts, in milliseconds
      * 
      * @param  new retry delay in milliseconds
      */
     public void setRetryDelay(int retryDelay) {
         this.retryDelay = retryDelay;
     }

     
     private boolean processTransferException(Exception ex, int attemptNumber)
     {
         if (attemptNumber <= retryCount+1) {
             if (retryDelay > 0) {
                 try {
                     log.debug("Sleeping for " + retryDelay + " ms prior to retry");
                     Thread.sleep(retryDelay);
                 }
                 catch (InterruptedException ignore) {}
             }
             log.error("Transfer error on attempt #" + attemptNumber
                     + " retrying: ", ex);           
             return true;
         } else {
             if (attemptNumber > 0)
                 log.info("Failed " + attemptNumber + " attempts - giving up");
             return false;
         }
     }
     
     /*
      *  (non-Javadoc)
      * @see com.enterprisedt.net.ftp.FTPClientInterface#get(java.lang.String, java.lang.String)
      */
     public void get(String localPath, String remoteFile)
         throws IOException, FTPException {
         
         String cwd = safePwd();
         
         FTPTransferType previousType = transferType;
         FTPTransferType currentTransferType = chooseTransferMode(remoteFile);
         
         File localFile = new File(localPath);    
         if (localFile.isDirectory()) {
             localPath = localPath + File.separator + remoteFile;
             log.debug("Setting local path to " + localPath);
         }
         
         try {

            if (retryCount == 0)
                getFile(localPath, remoteFile);
            else {
                for (int attempt = 1;; attempt++) {
                    try {
                        if (attempt > 1
                                && getType().equals(FTPTransferType.BINARY))
                            resume();
                        log.debug("Attempt #" + attempt);
                        getFile(localPath, remoteFile);
                        break;
                    } catch (ControlChannelIOException ex) {
                        if (!processControlChannelException(cwd, ex, attempt))
                            throw ex;
                    } catch (MalformedReplyException ex) {
                        throw ex;
                    } catch (IOException ex) {
                        if (!processTransferException(ex, attempt))
                            throw ex;
                    }
                }
            }
         } finally {
            resetTransferMode(previousType);
         }
         
         postTransferChecks(localPath, remoteFile, currentTransferType, false);
     }
     
     /*
      * (non-Javadoc)
      * 
      * @see com.enterprisedt.net.ftp.FTPClientInterface#put(java.io.InputStream,
      *      java.lang.String, boolean)
      */
     public String put(InputStream srcStream, String remoteFile, boolean append)
         throws IOException, FTPException {
         
         FTPTransferType previousType = transferType;
         FTPTransferType currentTransferType = chooseTransferMode(remoteFile);
         
         try {
             return putStream(srcStream, remoteFile, append);
         } finally {
             resetTransferMode(previousType);
         }
     }    
     
     private boolean processControlChannelException(String cwd, Exception ex, int attemptNumber) 
         throws IOException, FTPException {
         if (attemptNumber <= retryCount+1) {
             if (retryDelay > 0) {
                 try {
                     log.debug("Sleeping for " + retryDelay + " ms prior to retry");
                     Thread.sleep(retryDelay);
                 }
                 catch (InterruptedException ignore) {}
             }
             log.error("Transfer error on attempt #" + attemptNumber
                     + ": reconnecting & retrying: ", ex);  
             reconnect(cwd);
             return true;
         } else {
             log.info("Failed " + attemptNumber + " attempts - giving up");
             return false;
         }
     }
     
     
     /**
      * Reconnect to the server
      * 
      * @param cwd      current working dir
      * @throws IOException
      * @throws FTPException
      */
     protected void reconnect(String cwd) throws IOException, FTPException {
         try {
             quitImmediately();
         }
         catch (Exception ignore) {}
         log.info("Reconnecting");
         connect();
         login(user, password);
         setType(transferType);
         if (cwd != null)
             chdir(cwd); // switch to the target directory         
     }


     /*
      *  (non-Javadoc)
      * @see com.enterprisedt.net.ftp.FTPClientInterface#put(java.lang.String, java.lang.String)
      */
    public String put(String localPath, String remoteFile)
        throws IOException, FTPException {

        return put(localPath, remoteFile, false);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#put(java.io.InputStream, java.lang.String)
     */
    public String put(InputStream srcStream, String remoteFile)
            throws IOException, FTPException {

        return put(srcStream, remoteFile, false);
    }


    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#put(java.lang.String, java.lang.String, boolean)
     */
    public String put(String localPath, String remoteFile, boolean append)
        throws IOException, FTPException {
    	    	     
        String cwd = safePwd();
        
        FTPTransferType previousType = transferType;
        FTPTransferType currentTransferType = chooseTransferMode(remoteFile);

        try {
            InputStream srcStream = null;
            if (retryCount == 0 || append) {
                srcStream = new FileInputStream(localPath);
                remoteFile = putStream(srcStream, remoteFile, append);
            } else {
                for (int attempt = 1;; attempt++) {
                    try {
                        if (attempt > 1
                                && getType().equals(FTPTransferType.BINARY))
                            resume();
                        log.debug("Attempt #" + attempt);
                        srcStream = new FileInputStream(localPath);
                        remoteFile = putStream(srcStream, remoteFile, append);
                        break;
                    } catch (ControlChannelIOException ex) {
                        if (!processControlChannelException(cwd, ex, attempt))
                            throw ex;
                    } catch (MalformedReplyException ex) {
                        throw ex;
                    } catch (IOException ex) {
                        if (!processTransferException(ex, attempt))
                            throw ex;
                    }
                }
            }

        } finally {
            resetTransferMode(previousType);
        }
         
        postTransferChecks(localPath, remoteFile, currentTransferType, append);
        
        return remoteFile;        
     }

    /*
     *  Internal method that puts a stream to the server
     */
    private String putStream(InputStream srcStream, String remoteFile, boolean append)
        throws IOException, FTPException {

        try {        
            if (monitorEx != null)
                monitorEx.transferStarted(TransferDirection.UPLOAD, remoteFile);   
            remoteFile = putData(srcStream, remoteFile, append);
            validateTransfer();
            uploadCount++;
        }
        catch (FTPException ex) {
            throw ex;
        }
        catch (ControlChannelIOException ex) {
            throw ex;       
        }
        catch (IOException ex) {
            validateTransferOnError(ex);
            throw ex;        
        }
        finally {
            if (monitorEx != null)
                monitorEx.transferComplete(TransferDirection.UPLOAD, remoteFile);   
        }
        return remoteFile;
    }

    /**
     * Validate that the put() or get() was successful.  This method is not
     * for general use.
     */
    public void validateTransfer()
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        // check the control response
        String[] validCodes = {"225", "226", "250"};
        lastReply = control.readReply();
        
        // if we cancelled the transfer throw an exception
        if (cancelTransfer) {
            lastValidReply = lastReply;
            log.warn("Transfer has been cancelled!");
            throw new FTPTransferCancelledException();
        }
        
        lastValidReply = control.validateReply(lastReply, validCodes);
    }
    
    /**
     * Validate a transfer when an error has occurred on the data channel.
     * Set a very short transfer in case things have hung. Set it back
     * at the end.
     * 
     * @throws IOException
     * @throws FTPException
     */
    protected void validateTransferOnError(IOException ex) 
        throws IOException, FTPException {
        
        log.debug("Validate transfer on error after exception", ex);
        checkConnection(true);
        
        control.setTimeout(SHORT_TIMEOUT);
        try {
            validateTransfer();
        }
        catch (Exception e) {
            log.warn("Validate transfer on error failed", e);
        }
        finally {
            control.setTimeout(timeout);
        }
    }
    
    /**
     * Close the data socket
     */
    private void closeDataSocket() {
        if (data != null) {
            try {
                data.close();
                data = null;
            } catch (IOException ex) {
                log.warn("Caught exception closing data socket", ex);
            }
        }
    }
    
       
    /**
     * Close stream for data socket. Not for 
     * general use!
     * 
     * @param stream    stream reference
     */
    protected void closeDataSocket(InputStream stream) {
        if (stream != null) {
            try {
                stream.close();
            } 
            catch (IOException ex) {
                log.warn("Caught exception closing data socket", ex);
            }
        }

        closeDataSocket();
    }
    
    
    /**
     * Close stream for data socket
     * 
     * @param stream    stream reference
     */
    protected void closeDataSocket(OutputStream stream) {
        if (stream != null) {
            try {
                 stream.close();
            } 
            catch (IOException ex) {
                log.warn("Caught exception closing data socket", ex);
            }
        }

        closeDataSocket();
    }
    
    
    /**
     * Set up the data socket
     * 
     * @throws FTPException 
     * @throws IOException 
     */
    protected void setupDataSocket() 
        throws IOException, FTPException {
        
        data = control.createDataSocket(connectMode);
        data.setTimeout(timeout);
        if (dataReceiveBufferSize > 0)
            data.setReceiveBufferSize(dataReceiveBufferSize);
        if (dataSendBufferSize > 0)
            data.setSendBufferSize(dataSendBufferSize);
     }

    /**
     * Request the server to set up the put
     * 
     * @param remoteFile
     *            name of remote file in current directory
     * @param append
     *            true if appending, false otherwise
     */
    protected String initPut(String remoteFile, boolean append)
        throws IOException, FTPException {
    	
    	checkConnection(true);
        
        // if a remote filename isn't supplied, assume STOU is to be used
        boolean storeUnique = (remoteFile == null || remoteFile.length() == 0);
        if (storeUnique) {
            remoteFile = "";
            // check STOU isn't used with append
            if (append) {
                String msg = "A remote filename must be supplied when appending";
                log.error(msg);
                throw new FTPException(msg);
            }
        }
    	
        // reset the cancel flag
        cancelTransfer = false;

        boolean close = false;
        try {
            resumeMarker = 0;
            
            // if resume is requested, we must obtain the size of the
            // remote file 
            if (resume) {
                if (transferType.equals(FTPTransferType.ASCII))
                    throw new FTPException("Resume only supported for BINARY transfers");
                try {                   
                    resumeMarker = size(remoteFile);
                }
                catch (FTPException ex) {
                    resumeMarker = 0;
                    resume = false;
                    log.warn("SIZE failed '" + remoteFile + "' - resume will not be used (" + ex.getMessage() + ")");
                }
            }
            
            // set up data channel
            setupDataSocket();
            
            // issue REST
            if (resume) {
                try {
                    restart(resumeMarker);
                }
                catch (FTPException ex) {
                    resumeMarker = 0;
                    resume = false;
                    log.warn("REST failed - resume will not be used (" + ex.getMessage() + ")");
                }
            }
    
            // send the command to store
            String cmd = append ? "APPE " : (storeUnique ? "STOU" : storeCommand);
            lastReply = control.sendCommand(cmd + remoteFile);
    
            // Can get a 125 or a 150, also allow 350 (for Global eXchange Services server)
            // JScape returns 151
            String[] validCodes = {"125", "150", "151", "350"};
            lastValidReply = control.validateReply(lastReply, validCodes);
            
            String replyText = lastValidReply.getReplyText();
            if (storeUnique) {
                int pos = replyText.indexOf(STOU_FILENAME_MARKER);
                if (pos >= 0) {
                    pos += STOU_FILENAME_MARKER.length();
                    remoteFile = replyText.substring(pos).trim();
                }
                else { // couldn't find marker, just return last word of reply
                       // e.g. 150 Opening BINARY mode data connection for FTP0000004.
                    log.debug("Could not find " + STOU_FILENAME_MARKER + " in reply - using last word instead.");
                    pos = replyText.lastIndexOf(' ');
                    remoteFile = replyText.substring(++pos);
                    int len = remoteFile.length();
                    if (len > 0 && remoteFile.charAt(len-1) == '.') {
                        remoteFile = remoteFile.substring(0, len-1);
                    }  
                }
            }
            return remoteFile;
        }
        catch (IOException ex) {
            close = true;
            log.error("Caught and rethrowing exception in initPut()", ex);
            throw ex;
        }
        catch (FTPException ex) {
            close = true;
            log.error("Caught and rethrowing exception in initPut()", ex);
            throw ex;
        }
        finally {
            if (close) {
                resume = false;
                closeDataSocket();
            }
        }
    }

    /**
     *  Put data. For ASCII, translate line terminators, coping 
     *  with \r\n, \r or \n in the local file
     *
     *  @param  srcStream   input stream of data to put
     *  @param  remoteFile  name of remote file we are writing to
     *  @param  append      true if appending, false otherwise
     */
    private String putData(InputStream srcStream, String remoteFile,
                           boolean append)
        throws IOException, FTPException {

        IOException storedEx = null;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        long size = 0;
        try {
            in = new BufferedInputStream(srcStream);
    
            remoteFile = initPut(remoteFile, append);
    
            // get an output stream
            out = new BufferedOutputStream(
                    new DataOutputStream(getOutputStream()), transferBufferSize*2);
            
            // if resuming, we skip over the unwanted bytes
            if (resume && resumeMarker > 0) {
                in.skip(resumeMarker);
            }
            else
                resumeMarker = 0;
    
            byte[] buf = new byte[transferBufferSize];
            byte[] prevBuf = new byte[FTP_LINE_SEPARATOR.length];
            int matchpos = 0;
    
            // read a chunk at a time and write to the data socket            
            long monitorCount = 0;
            int count = 0;
            boolean isASCII = getType() == FTPTransferType.ASCII;
            long start = System.currentTimeMillis();
            if (throttler != null) {
                throttler.reset();
            }
            
            while ((count = in.read(buf)) > 0 && !cancelTransfer) {
                if (isASCII) { // we want to allow \r\n, \r and \n
                    for (int i = 0; i < count; i++) {
                        // LF without preceding CR (i.e. Unix text file)
                        if (buf[i] == LINE_FEED && matchpos == 0) {
                            out.write(CARRIAGE_RETURN);
                            out.write(LINE_FEED);
                            size += 2;
                            monitorCount += 2;
                        }
                        else if (buf[i] == FTP_LINE_SEPARATOR[matchpos]) {
                            prevBuf[matchpos] = buf[i];
                            matchpos++;
                            if (matchpos == FTP_LINE_SEPARATOR.length) {
                                out.write(CARRIAGE_RETURN);
                                out.write(LINE_FEED);
                                size += 2;
                                monitorCount += 2;
                                matchpos = 0;
                            }
                        }
                        else { // no match current char 
                            // this must be a matching \r if we matched first char
                            if (matchpos > 0) {
                                out.write(CARRIAGE_RETURN);
                                out.write(LINE_FEED);
                                size += 2;
                                monitorCount += 2;
                            }
                            out.write(buf[i]);
                            size++;
                            monitorCount++;
                            matchpos = 0;
                        }                              
                    }
                }
                else { // binary
                    out.write(buf, 0, count);
                    size += count;
                    monitorCount += count;
                }
                
                if (throttler != null) {
                    throttler.throttleTransfer(size);
                }
                                    
                if (monitor != null && monitorCount > monitorInterval) {
                    monitor.bytesTransferred(size); 
                    monitorCount = 0;  
                }
                if (serverWakeupInterval > 0 && System.currentTimeMillis() - start > serverWakeupInterval*1000) {
                    start = System.currentTimeMillis();
                    sendServerWakeup();
                }
            }
            // write out anything left at the end that has been saved
            // - must be a \r which we convert into a line terminator
            if (isASCII && matchpos > 0) {
                out.write(CARRIAGE_RETURN);
                out.write(LINE_FEED);
                size += 2;
                monitorCount += 2;
            }
        }
        catch (IOException ex) {
            storedEx = ex;
            log.error("Caught and rethrowing exception in getDataAfterInitGet()", ex);
        }
        finally {
            resume = false;
            try {
                if (in != null)
                    in.close();
            }
            catch (IOException ex) {
                log.warn("Caught exception closing input stream", ex);
            }
                
            closeDataSocket(out);
            
            // if we failed to write the file, rethrow the exception
            if (storedEx != null)
                throw storedEx;
            
            // notify the final transfer size
            if (monitor != null)
                monitor.bytesTransferred(size);  
            // log bytes transferred
            log.debug("Transferred " + size + " bytes to remote host");           
        }
        return remoteFile;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#put(byte[], java.lang.String)
     */
    public String put(byte[] bytes, String remoteFile)
        throws IOException, FTPException {

        return put(bytes, remoteFile, false);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#put(byte[], java.lang.String, boolean)
     */
    public String put(byte[] bytes, String remoteFile, boolean append)
        throws IOException, FTPException {
        
        String cwd = safePwd();   
        
        FTPTransferType previousType = transferType;
        FTPTransferType currentTransferType = chooseTransferMode(remoteFile);
        
        String result = null;       
        try {

            ByteArrayInputStream input = null;
            if (retryCount == 0 || append) {
                input = new ByteArrayInputStream(bytes);
                result = putStream(input, remoteFile, append);
            } else {
                for (int attempt = 1;; attempt++) {
                    try {
                        if (attempt > 1
                                && getType().equals(FTPTransferType.BINARY))
                            resume();
                        log.debug("Attempt #" + attempt);
                        input = new ByteArrayInputStream(bytes);
                        result = putStream(input, remoteFile, append);
                        break;
                    } catch (ControlChannelIOException ex) {
                        if (!processControlChannelException(cwd, ex, attempt))
                            throw ex;
                    } catch (MalformedReplyException ex) {
                        throw ex;
                    } catch (IOException ex) {
                        if (!processTransferException(ex, attempt))
                            throw ex;
                    }
                }
            }
        } finally {
            resetTransferMode(previousType);
        }   
        postTransferChecks(bytes, remoteFile, currentTransferType, append);
        
        return result; 
    }

    /*
     * Internal method that gets a file
     */
    private void getFile(String localPath, String remoteFile)
        throws IOException, FTPException {

        try {        
            if (monitorEx != null)
                monitorEx.transferStarted(TransferDirection.DOWNLOAD, remoteFile);
            getData(localPath, remoteFile);
            validateTransfer();
            downloadCount++;
        }
        catch (FTPException ex) {
            throw ex;
        }
        catch (ControlChannelIOException ex) {
            throw ex;       
        }
        catch (IOException ex) {
            validateTransferOnError(ex);
            throw ex;        
        }
        finally {
            if (monitorEx != null)
                monitorEx.transferComplete(TransferDirection.DOWNLOAD, remoteFile);   
        }
    }
    
    /**
     * Can be overridden by subclasses to do any necessary post transfer 
     * checking.
     * 
     * @param localPath         local file
     * @param remotePath        remote file
     * @param transferType      binary or ASCII
     * @param append            
     */
    protected void postTransferChecks(String localPath, String remotePath, 
            FTPTransferType transferType, boolean append) throws FTPException, IOException {        
    }
    
    /**
     * Can be overridden by subclasses to do any necessary post transfer 
     * checking.
     * 
     * @param localBytes        local bytes to transfer
     * @param remotePath        remote file
     * @param transferType      binary or ASCII
     * @param append            
     */
    protected void postTransferChecks(byte[] localBytes, String remotePath, 
            FTPTransferType transferType, boolean append) throws FTPException, IOException {        
    }    

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#get(java.io.OutputStream, java.lang.String)
     */
    public void get(OutputStream destStream, String remoteFile)
        throws IOException, FTPException {

        FTPTransferType previousType = transferType;
        chooseTransferMode(remoteFile);
        boolean resetMode = true;
        try {        
            if (monitorEx != null)
                monitorEx.transferStarted(TransferDirection.DOWNLOAD, remoteFile);
            getData(destStream, remoteFile);
            validateTransfer();
            downloadCount++;
        }
        catch (FTPException ex) {
            throw ex;
        }
        catch (ControlChannelIOException ex) {
            throw ex;       
        }
        catch (IOException ex) {
            resetMode = false;
            validateTransferOnError(ex);
            throw ex;        
        }
        finally {
            if (monitorEx != null)
                monitorEx.transferComplete(TransferDirection.DOWNLOAD, remoteFile);
            if (resetMode)
                resetTransferMode(previousType);
        }
    }
    
    
    /**
     * Reset the transfer mode back to what it should be, if 
     * it has changed.
     * 
     * @param previousType      previous transfer type
     * @throws IOException
     * @throws FTPException
     */
    public void resetTransferMode(FTPTransferType previousType) 
        throws IOException, FTPException {
        
        if (!transferType.equals(previousType)) {
            setType(previousType);
        }
    }


    /**
     *  Request to the server that the get is set up
     *
     *  @param  remoteFile  name of remote file
     */
    protected void initGet(String remoteFile)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        // reset the cancel flag
        cancelTransfer = false;            

        boolean close = false;
        try {
            // set up data channel
            setupDataSocket();
            
            // if resume is requested, we must issue REST
            if (resume) {
                if (transferType.equals(FTPTransferType.ASCII))
                    throw new FTPException("Resume only supported for BINARY transfers");
                try {
                    restart(resumeMarker);
                }
                catch (FTPException ex) {
                    resumeMarker = 0;
                    resume = false;
                    log.warn("REST failed - resume will not be used (" + ex.getMessage() + ")");
                }
            }
            else
                resumeMarker = 0;
    
            // send the retrieve command
            lastReply = control.sendCommand("RETR " + remoteFile);
    
            // Can get a 125 or a 150
            String[] validCodes1 = {"125", "150"};
            lastValidReply = control.validateReply(lastReply, validCodes1);
        }
        catch (IOException ex) {
            close = true;
            log.error("Caught and rethrowing exception in initGet()", ex);
            throw ex;
        }
        catch (FTPException ex) {
            close = true;
            log.error("Caught and rethrowing exception in initGet()", ex);
            throw ex;
        }
        finally {
            if (close) {
                resume = false;
                closeDataSocket();
            }
        }
    }
    

    /**
     *  Get as binary file, i.e. straight transfer of data
     *
     *  @param localPath   full path of local file to write to
     *  @param remoteFile  name of remote file
     */
    private void getData(String localPath, String remoteFile)
        throws IOException, FTPException {

        // B. McKeown: Need to store the local file name so the file can be
        // deleted if necessary.
        resumeMarker = 0;
        File localFile = new File(localPath);    
        if (localFile.exists()) {
            // if resuming, we must find the marker
            if (!localFile.canWrite())
                throw new FTPException(localPath
                        + " is readonly - cannot write");
            if (resume)
                resumeMarker = localFile.length();
        }

        // B.McKeown:
        // Call initGet() before creating the FileOutputStream.
        // This will prevent being left with an empty file if a FTPException
        // is thrown by initGet().
        initGet(remoteFile);

        // create the buffered output stream for writing the file
        FileOutputStream out =
                new FileOutputStream(localPath, resume);
        
        try {
            getDataAfterInitGet(out);
        }
        catch (IOException ex) {
            if (deleteOnFailure) {
                localFile.delete();
                log.debug("Deleting local file '" + localFile.getAbsolutePath() + "'");
            }
            else {
                log.debug("Possibly partial local file not deleted");
            }
            throw ex;
        }        
    }

    /**
     *  Get as binary file, i.e. straight transfer of data
     *
     *  @param destStream  stream to write to
     *  @param remoteFile  name of remote file
     */
    private void getData(OutputStream destStream, String remoteFile)
        throws IOException, FTPException {

        resumeMarker = 0;
        initGet(remoteFile);        
        getDataAfterInitGet(destStream);
    }
    
    /**
     * Get the data input stream. Not for general use!
     * 
     * @return
     * @throws IOException
     */
    protected InputStream getInputStream() throws IOException {
        return data.getInputStream();
    }
    
    /**
     * Get the data input stream. Not for general use!
     * 
     * @return
     * @throws IOException
     */
    protected OutputStream getOutputStream() throws IOException {
        return data.getOutputStream();
    }
    
    
    /**
     *  Get as binary file, i.e. straight transfer of data
     *
     *  @param destStream  stream to write to
     */
    private void getDataAfterInitGet(OutputStream destStream)
        throws IOException, FTPException {

        // create the buffered output stream for writing the file
        BufferedOutputStream out =
            new BufferedOutputStream(destStream);
        
        BufferedInputStream in = null;
        long size = 0;
        IOException storedEx = null;
        try {
            // get an input stream to read data from ... AFTER we have
            // the ok to go ahead AND AFTER we've successfully opened a
            // stream for the local file
            in = new BufferedInputStream(
                    new DataInputStream(getInputStream()));
        
            // do the retrieving
            long monitorCount = 0; 
            byte [] chunk = new byte[transferBufferSize];
            int count;
            boolean isASCII = getType() == FTPTransferType.ASCII;
            long start = System.currentTimeMillis();
            if (throttler != null) {
                throttler.reset();
            }

            byte[] prevBuf = new byte[FTP_LINE_SEPARATOR.length];
            int matchpos = 0;

            // read from socket & write to file in chunks        
            while ((count = readChunk(in, chunk, transferBufferSize)) >= 0 && !cancelTransfer) {
                if (isASCII) {
                    for (int i = 0; i < count; i++) {
                        if (chunk[i] == FTP_LINE_SEPARATOR[matchpos]) {
                            prevBuf[matchpos] = chunk[i];
                            matchpos++;
                            if (matchpos == FTP_LINE_SEPARATOR.length) {
                                out.write(LINE_SEPARATOR);
                                size += LINE_SEPARATOR.length;
                                monitorCount += LINE_SEPARATOR.length;
                                matchpos = 0;
                            }
                        }
                        else { // no match
                            // write out existing matches
                            if (matchpos > 0) {
                                out.write(prevBuf, 0, matchpos);
                                size += matchpos;
                                monitorCount += matchpos;
                            }
                            out.write(chunk[i]);
                            size++;
                            monitorCount++;
                            matchpos = 0;
                        }                              
                    }                
                }
                else { // binary
                    out.write(chunk, 0, count);
                    size += count;
                    monitorCount += count;
                }
                
                if (throttler != null) {
                    throttler.throttleTransfer(size);
                }
                
                if (monitor != null && monitorCount > monitorInterval) {
                    monitor.bytesTransferred(size); 
                    monitorCount = 0;  
                }    
    
                if (serverWakeupInterval > 0 && System.currentTimeMillis() - start > serverWakeupInterval*1000) {
                    start = System.currentTimeMillis();
                    sendServerWakeup();
                }
            }            
            
            // write out anything left at the end that has been saved
            if (isASCII && matchpos > 0) {
                out.write(prevBuf, 0, matchpos);
                size += matchpos;
                monitorCount += matchpos;
            }
        }
        catch (IOException ex) {
            storedEx = ex;
            log.error("Caught and rethrowing exception in getDataAfterInitGet()", ex);
        }
        finally {
            try {
                if (out != null)
                    out.close();
            }
            catch (IOException ex) {
                log.warn("Caught exception closing output stream", ex);
            }

            resume = false;
    
            // close streams
            closeDataSocket(in);
    
            // if we failed to write the file, rethrow the exception
            if (storedEx != null)
                throw storedEx;
            else if (monitor != null)
                monitor.bytesTransferred(size);  
    
            // log bytes transferred
            log.debug("Transferred " + size + " bytes from remote host");
        }
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#get(java.lang.String)
     */
    public byte[] get(String remoteFile)
        throws IOException, FTPException {

        FTPTransferType previousType = transferType;
        chooseTransferMode(remoteFile);
        boolean resetMode = true;
        try {        
            if (monitorEx != null)
                monitorEx.transferStarted(TransferDirection.DOWNLOAD, remoteFile);
            ByteArrayOutputStream result = new ByteArrayOutputStream(transferBufferSize);
            getData(result, remoteFile);
            validateTransfer();
            downloadCount++;
            return result == null ? null : result.toByteArray();
        }
        catch (FTPException ex) {
            throw ex;
        }
        catch (ControlChannelIOException ex) {
            throw ex;       
        }
        catch (IOException ex) {
            resetMode = false;
            validateTransferOnError(ex);
            throw ex;        
        }
        finally {
            if (monitorEx != null)
                monitorEx.transferComplete(TransferDirection.DOWNLOAD, remoteFile);
            if (resetMode)
                resetTransferMode(previousType);
        }
    }


    /**
     *  Run a site-specific command on the
     *  server. Support for commands is dependent
     *  on the server
     *
     *  @param  command   the site command to run
     *  @return true if command ok, false if
     *          command not implemented
     */
    public boolean site(String command)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        // send the retrieve command
        lastReply = control.sendCommand("SITE " + command);

        // Can get a 200 (ok) or 202 (not impl). Some
        // FTP servers return 502 (not impl). Added 250 for leitch
        String[] validCodes = {"200", "202", "250", "502"};
        lastValidReply = control.validateReply(lastReply, validCodes);

        // return true or false? 200 is ok, 202/502 not
        // implemented
        if (lastReply.getReplyCode().equals("200"))
            return true;
        else
            return false;
    }


    /**
     *  List a directory's contents
     *
     *  @param  dirname  the name of the directory (<b>not</b> a file mask)
     *  @return a string containing the line separated
     *          directory listing
     *  @deprecated  As of FTP 1.1, replaced by {@link #dir(String)}
     */
    public String list(String dirname)
        throws IOException, FTPException {

        return list(dirname, false);
    }


    /**
     *  List a directory's contents as one string. A detailed
     *  listing is available, otherwise just filenames are provided.
     *  The detailed listing varies in details depending on OS and
     *  FTP server.
     *
     *  @param  dirname  the name of the directory(<b>not</b> a file mask)
     *  @param  full     true if detailed listing required
     *                   false otherwise
     *  @return a string containing the line separated
     *          directory listing
     *  @deprecated  As of FTP 1.1, replaced by {@link #dir(String,boolean)}
     */
    public String list(String dirname, boolean full)
        throws IOException, FTPException {

        String[] list = dir(dirname, full);

        StringBuffer result = new StringBuffer();
        String sep = System.getProperty("line.separator");

        // loop thru results and make into one string
        for (int i = 0; i < list.length; i++) {
            result.append(list[i]);
            result.append(sep);
        }

        return result.toString();
    }
    
    
    /**
     * Override the chosen file factory with a user created one - meaning
     * that a specific parser has been selected
     * 
     * @param fileFactory
     */
    public void setFTPFileFactory(FTPFileFactory fileFactory) {
        this.fileFactory = fileFactory;
        log.debug("Set new FTPFileFactory: " + fileFactory.toString());
    }
    
    /**
     * Set the locale for date parsing of dir listings
     * 
     * @param locale    new locale to use
     * @deprecated @see FTPClient#setParserLocales(Locale[])
     */
    public void setParserLocale(Locale locale) {
        listingLocales = new Locale[1];
        listingLocales[0] = locale;
    }
    
    /**
     * Set the list of locales to be tried for date parsing of dir listings
     * 
     * @param locales    locales to use
     */
    public void setParserLocales(Locale[] locales) {
        listingLocales = locales;
    }
    
    /**
     * Uses the MLST command to find out details about the
     * named file. A single filename should be supplied. Note
     * that the MLST command is not supported by many servers.
     * 
     * @param name   name of a file
     * @return  if it exists, an FTPFile object
     */
    public FTPFile fileDetails(String name)  
        throws IOException, FTPException, ParseException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("MLST " + name);
        lastValidReply = control.validateReply(lastReply, "250");
        
        if (lastReply.getReplyData() != null)
            return mlsxParser.parse(lastReply.getReplyData()[0]);
        
        log.warn("No file data returned from MLST");
        return null;
    }
    
    /**
     * Internal use only 
     */
    interface DirectoryCallback {
        
        public DirectoryListArgument listEntry(String entry) throws ParseException;
    }
    
    /**
     * Internal use only 
     */
    class DirectoryCallbackImpl implements DirectoryCallback {

        private FTPFileFactory fileFactory;
        private DirectoryListCallback lister;
        private String path;
        
        DirectoryCallbackImpl(FTPFileFactory fileFactory, DirectoryListCallback lister, String path) {
            this.fileFactory = fileFactory;
            this.lister = lister;
            this.path = path;
        }
        
        public DirectoryListArgument listEntry(String entry) throws ParseException {
            FTPFile file = fileFactory.parse(entry);
             if (lister != null && file != null) {
                file.setPath(path);
                DirectoryListArgument arg = new DirectoryListArgument(file);
                lister.listDirectoryEntry(arg);
                return arg;
            }
            return null;
        }
    }
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dirDetails(java.lang.String,com.enterprisedt.net.ftp.DirectoryListCallback)
     */
    public void dirDetails(String dirname, DirectoryListCallback lister) 
        throws IOException, FTPException, ParseException {

        String path = setupDirDetails(dirname);      
        DirectoryCallbackImpl callback = new DirectoryCallbackImpl(fileFactory, lister, path);        
        dir(dirname, true, null, callback);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dirDetails(java.lang.String)
     */
    public FTPFile[] dirDetails(String dirname) 
        throws IOException, FTPException, ParseException {
        
        String path = setupDirDetails(dirname);
                
        // get the details and parse. Set the directory for each file
        FTPFile[] result = fileFactory.parse(dir(dirname, true));
        if (path != null) {
            for (int i = 0; i < result.length; i++) {
                result[i].setPath(path);
            }
        }
                
        return result;
    }
    
    /**
     * Setup the dirDetails method
     * 
     * @param dirname
     * @return
     * @throws FTPException
     * @throws IOException
     */
    private String setupDirDetails(String dirname) throws FTPException, IOException {
        // create the factory
        if (fileFactory == null) {
            try {
                fileFactory = new FTPFileFactory(system());
            }
            catch (FTPException ex) {
                log.warn("SYST command failed - setting Unix as default parser", ex);
                fileFactory = new FTPFileFactory(FTPFileFactory.UNIX_STR);
            }
        }
        fileFactory.setLocales(listingLocales);
        
        String path = safePwd();
        
        // add dirname to path if it looks like a directory name
        // and has no obvious wildcards
        if (path != null && dirname != null && dirname.length() > 0 && 
                dirname.indexOf('*') < 0 && dirname.indexOf('?') < 0) {

            path += "/" + dirname;
        }
        
        return path;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dir()
     */
    public String[] dir()
        throws IOException, FTPException {

        return dir(null, false);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dir(java.lang.String)
     */
    public String[] dir(String dirname)
        throws IOException, FTPException {

        return dir(dirname, false);
    }
    
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dir(java.lang.String, boolean)
     */
    private void dir(String dirname, boolean full, Vector lines, DirectoryCallback lister)
        throws IOException, FTPException, ParseException {
        
        checkConnection(true);
        
        // reset the cancel flag
        cancelTransfer = false;
        
        try {
            // set up data channel
            setupDataSocket();
    
            // send the retrieve command
            String command = full ? "LIST ":"NLST ";
            if (dirname != null)
                command += dirname;
    
            // some FTP servers bomb out if NLST has whitespace appended
            command = command.trim();
            lastReply = control.sendCommand(command);
    
            // check the control response. wu-ftp returns 550 if the
            // directory is empty, so we handle 550 appropriately. Similarly
            // proFTPD returns 450 or 226 (depending on NLST or LIST)
            String[] validCodes1 = {"125", "150", "226", "450", "550"};
            lastValidReply = control.validateReply(lastReply, validCodes1);  
    
            // an empty array of files for 450/550
            String[] result = new String[0];
            
            // a normal reply ... extract the file list
            String replyCode = lastValidReply.getReplyCode();
            if (!replyCode.equals("450") && !replyCode.equals("550") && !replyCode.equals("226")) {
                // get a character input stream to read data from .
                LineNumberReader in = null;
                 try {
                    in = new LineNumberReader(
                            new InputStreamReader(getInputStream(), controlEncoding));
    
                    // read a line at a time
                    String line = null;
                    while ((line = readLine(in)) != null && !cancelTransfer) {
                        if (lines != null)
                            lines.addElement(line);
                        if (lister != null) {
                            DirectoryListArgument arg = lister.listEntry(line);
                            if (arg != null && arg.isListingAborted()) {
                                log.warn("Aborting listing");
                                cancelTransfer = true;
                            }
                        }
                        log.log(Level.ALL, line, null);
                    }
                }
                catch (IOException ex) {
                    validateTransferOnError(ex);
                    throw ex;
                }
                finally {
                    try {
                        if (in != null)
                            in.close();
                    }
                    catch (IOException ex) {
                        log.error("Failed to close socket in dir()", ex);
                    }
                    closeDataSocket();
                }
                    
                // check the control response
                String[] validCodes2 = {"226", "250"};
                lastReply = control.readReply();
                lastValidReply = control.validateReply(lastReply, validCodes2);
    
                // empty array is default
                if (lines != null && !lines.isEmpty()) {
                    result = new String[lines.size()];
                    lines.copyInto(result);
                }
            }
            else { // throw exception if not "No files" or other message
                String replyText = lastValidReply.getReplyText().toUpperCase();
                if (!dirEmptyStrings.matches(replyText)
                        && !transferCompleteStrings.matches(replyText))
                    throw new FTPException(lastReply);
            }
        }
        finally {
            closeDataSocket();
        }        
    }
    

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#dir(java.lang.String, boolean)
     */
    public String[] dir(String dirname, boolean full)
        throws IOException, FTPException {
        
        Vector lines = new Vector();
        try {
            dir(dirname, full, lines, null);
        }
        catch (ParseException ignore) {}
        
        // empty array is default
        String[] result = new String[0];
        if (!lines.isEmpty()) {
            result = new String[lines.size()];
            lines.copyInto(result);
        }
        
        return result;
    }
    
    /**
	 * Attempts to read a specified number of bytes from the given
	 * <code>InputStream</code> and place it in the given byte-array. The
	 * purpose of this method is to permit subclasses to execute any additional
	 * code necessary when performing this operation.
	 * 
	 * @param in
	 *            The <code>InputStream</code> to read from.
	 * @param chunk
	 *            The byte-array to place read bytes in.
	 * @param chunksize
	 *            Number of bytes to read.
	 * @return Number of bytes actually read.
	 * @throws IOException
	 *             Thrown if there was an error while reading.
	 */
    public int readChunk(BufferedInputStream in, byte[] chunk, int chunksize) 
    	throws IOException {
    		
    	return in.read(chunk, 0, chunksize);
    }
    
    /**
     * Attempts to read a single character from the given <code>InputStream</code>. 
     * The purpose of this method is to permit subclasses to execute
     * any additional code necessary when performing this operation. 
     * @param in The <code>LineNumberReader</code> to read from.
     * @return The character read.
     * @throws IOException Thrown if there was an error while reading.
     */
    protected int readChar(LineNumberReader in) 
    	throws IOException {
    		
    	return in.read();
    }
    
    /**
     * Attempts to read a single line from the given <code>InputStream</code>. 
     * The purpose of this method is to permit subclasses to execute
     * any additional code necessary when performing this operation. 
     * @param in The <code>LineNumberReader</code> to read from.
     * @return The string read.
     * @throws IOException Thrown if there was an error while reading.
     */
    protected String readLine(LineNumberReader in) 
    	throws IOException {
    		
    	return in.readLine();
    }

    /**
     *  Gets the latest valid reply from the server
     *
     *  @return  reply object encapsulating last valid server response
     */
    public FTPReply getLastValidReply() {
        return lastValidReply;
    }

    /**
     *  Gets the last reply from the server, whether valid or not
     *
     *  @return  reply object encapsulating last server response
     */
    public FTPReply getLastReply() {
        return lastReply;
    }

    /**
     *  Get the current transfer type
     *
     *  @return  the current type of the transfer,
     *           i.e. BINARY or ASCII
     */
    public FTPTransferType getType() {
        return transferType;
    }

    /**
     *  Set the transfer type
     *
     *  @param  type  the transfer type to
     *                set the server to
     */
    public void setType(FTPTransferType type)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        // determine the character to send
        String typeStr = FTPTransferType.ASCII_CHAR;
        if (type.equals(FTPTransferType.BINARY))
            typeStr = FTPTransferType.BINARY_CHAR;

        // send the command
        String[] validCodes = {"200", "250"};
        lastReply = control.sendCommand("TYPE " + typeStr);
        lastValidReply = control.validateReply(lastReply, validCodes);

        // record the type
        transferType = type;
    }

    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#delete(java.lang.String)
     */
    public void delete(String remoteFile)
        throws IOException, FTPException {
    	
    	checkConnection(true);
        String[] validCodes = {"200", "250"};
        lastReply = control.sendCommand("DELE " + remoteFile);
        lastValidReply = control.validateReply(lastReply, validCodes);
        deleteCount++;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#rename(java.lang.String, java.lang.String)
     */
    public void rename(String from, String to)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("RNFR " + from);
        lastValidReply = control.validateReply(lastReply, "350");

        lastReply = control.sendCommand("RNTO " + to);
        lastValidReply = control.validateReply(lastReply, "250");
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#rmdir(java.lang.String)
     */
    public void rmdir(String dir)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("RMD " + dir);

        // some servers return 200,257, technically incorrect but
        // we cater for it ...
        String[] validCodes = {"200", "250", "257"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }


    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#mkdir(java.lang.String)
     */
    public void mkdir(String dir)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("MKD " + dir);
        
        // some servers return 200,257, technically incorrect but
        // we cater for it ...
        String[] validCodes = {"200", "250", "257"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#chdir(java.lang.String)
     */
    public void chdir(String dir)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("CWD " + dir);
        lastValidReply = control.validateReply(lastReply, "250");
    }
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#cdup()
     */
    public void cdup()
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("CDUP");
        String[] validCodes = {"200", "250"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#modtime(java.lang.String)
     */
    public Date modtime(String remoteFile)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("MDTM " + remoteFile);
        lastValidReply = control.validateReply(lastReply, "213");

        // parse the reply string ...
        Date ts = tsFormat.parse(lastValidReply.getReplyText(),
                                 new ParsePosition(0));
        return ts;
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#setModTime(java.lang.String)
     */
    public void setModTime(String remoteFile, Date modTime)
        throws IOException, FTPException {
        
        checkConnection(true);
        
        String time = tsFormat.format(modTime);
        lastReply = control.sendCommand("MFMT " + time + " " + remoteFile);
        lastValidReply = control.validateReply(lastReply, "213");
    }

    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#pwd()
     */
    public String pwd()
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("PWD");
        lastValidReply = control.validateReply(lastReply, "257");

        // get the reply text and extract the dir
        // listed in quotes, if we can find it. Otherwise
        // just return the whole reply string
        String text = lastValidReply.getReplyText();
        int start = text.indexOf('"');
        int end = text.lastIndexOf('"');
        if (start >= 0 && end > start)
            return text.substring(start+1, end);
        else
            return text;
    }
    
    private String safePwd() throws IOException {
        String result = null;
        try {
            result = pwd();
        }
        catch (FTPException ex) {
            log.debug("Ignoring exception: " + ex.getMessage());
        }
        return result;
    }
    
    
    /**
     *  Get the server supplied features
     *
     *  @return   string containing server features, or null if no features or not
     *             supported
     */
    public String[] features()
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("FEAT");
        String[] validCodes = {"211", "500", "502"};
        lastValidReply = control.validateReply(lastReply, validCodes);
        if (lastValidReply.getReplyCode().equals("211"))
            return lastValidReply.getReplyData();
        else
            throw new FTPException(lastReply);
    }
    
    /**
     *  Get the type of the OS at the server
     *
     *  @return   the type of server OS
     */
    public String system()
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("SYST");
        String[] validCodes = {"200", "213", "215", "250"}; // added 250 for leitch
        lastValidReply = control.validateReply(lastReply, validCodes);
        return lastValidReply.getReplyText();
    }    
    
    /**
     *  Send a "no operation" message that does nothing. Can be
     *  called periodically to prevent the connection timing out
     */
    public void noOperation()
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("NOOP");
        String[] validCodes = {"200", "250"}; // added 250 for leitch
        lastValidReply = control.validateReply(lastReply, validCodes);
    }

    /**
     *  Sends stat message to enquire about the status of a
     *  transfer. 
     */
    public String stat()
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("STAT");
        String[] validCodes = {"211", "212", "213"};
        lastValidReply = control.validateReply(lastReply, validCodes);
        return lastValidReply.getReplyText();
    }
    
    /**
     * Wake up the server during a transfer to prevent a
     * timeout from occuring. This may hang or confuse the server -
     * use with caution.
     * 
     * @throws IOException 
     * @throws FTPException 
     *
     */
    public void sendServerWakeup() throws IOException, FTPException {
        noOperation();
     }
    
    /**
     *  Tries to keep the current connection alive by 
     *  sending an innocuous command to signal that the 
     *  client is still active
     */
    public void keepAlive() throws IOException, FTPException {
        log.debug("keepAlive() called");
        int op = (int)Math.ceil(Math.random()*2);
        switch (op) {
            case 1:    
                noOperation();
                break;
            case 2:
                pwd();
                break;
            default:
                pwd();
        }
    }
    
    /**
     *  Get the help text for the specified command
     *
     *  @param  command  name of the command to get help on
     *  @return help text from the server for the supplied command
     */
    public String help(String command)
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        lastReply = control.sendCommand("HELP " + command);
        String[] validCodes = {"211", "214"};
        lastValidReply = control.validateReply(lastReply, validCodes);
        return lastValidReply.getReplyText();
    }
    
     /**
     *  Abort the current action
     */
    protected void abort()
        throws IOException, FTPException {
        
        checkConnection(true);
        
        lastReply = control.sendCommand("ABOR");
        String[] validCodes = {"426", "226"};
        lastValidReply = control.validateReply(lastReply, validCodes);
    }

    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#quit()
     */
    public void quit()
        throws IOException, FTPException {
    	
    	checkConnection(true);
    	
        try {
            lastReply = control.sendCommand("QUIT");
            String[] validCodes = {"221", "226"};
            lastValidReply = control.validateReply(lastReply, validCodes);
        }
        finally { // ensure we clean up the connection
            try {
                control.logout();
            }
            finally {
                control = null;
            }
        }
    }
    
    /*
     *  (non-Javadoc)
     * @see com.enterprisedt.net.ftp.FTPClientInterface#quitImmediately()
     */
    public void quitImmediately() 
        throws IOException, FTPException {
        
        cancelTransfer();
        try {
            if (control != null && control.controlSock != null)
                control.controlSock.close();
        }
        finally {
            control = null;
        }
    }
    
    /**
     * String representation
     */
    public String toString() {
        StringBuffer result = new StringBuffer("[");
        result.append("FTP").append(",").append(remoteHost).append(",").append(controlPort).
            append(",").append(getId()).append("]");
        return result.toString();
    }



}


