/**
 * 
 *  Copyright (C) 2007 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: FileTransferClient.java,v $
 *    Revision 1.19  2010-05-29 07:10:36  hans
 *    Fixed doco.
 *
 *    Revision 1.18  2010-04-26 15:55:46  bruceb
 *    add new dirDetails method with callback
 *
 *    Revision 1.17  2009-06-15 04:46:40  hans
 *    Changed downloadURL to downloadURLStream and added downloadURLFile.  Likewise for uploads.
 *
 *    Revision 1.16  2009-06-15 03:36:04  hans
 *    Added downloadURL and uploadURL.
 *
 *    Revision 1.15  2009-03-20 03:54:37  bruceb
 *    added getLastReply()
 *
 *    Revision 1.14  2008-09-18 06:55:34  bruceb
 *    retry settings
 *
 *    Revision 1.13  2008-06-03 03:20:15  bruceb
 *    cleanup
 *
 *    Revision 1.12  2008-06-03 03:17:59  bruceb
 *    clear stats on connect
 *
 *    Revision 1.11  2008-06-02 06:50:17  bruceb
 *    fix bug re messages not being set
 *
 *    Revision 1.10  2008-05-23 02:45:41  bruceb
 *    moved context
 *
 *    Revision 1.9  2008-05-22 04:20:55  bruceb
 *    moved stuff to internal etc
 *
 *    Revision 1.8  2008-05-02 07:41:30  bruceb
 *    setModTime added
 *
 *    Revision 1.7  2008-03-31 02:16:04  bruceb
 *    fix event listener assignment
 *
 *    Revision 1.6  2008-03-31 01:18:02  bruceb
 *    fix isconnected bug
 *
 *    Revision 1.5  2008-03-31 00:16:23  bruceb
 *    advanced settings rejig
 *
 *    Revision 1.4  2008-03-13 00:22:53  bruceb
 *    various tweaks for j/pro
 *
 *    Revision 1.3  2008-01-09 03:54:21  bruceb
 *    executeCommand() now returns reply code
 *
 *    Revision 1.2  2007-12-20 00:40:16  bruceb
 *    autologin
 *
 *    Revision 1.1  2007-12-18 07:52:06  bruceb
 *    2.0 changes
 *
 *
 */
package com.enterprisedt.net.ftp;

import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.text.ParseException;
import java.util.Date;

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

/**
 * Easy to use FTP client that is thread safe and provides true FTP streams.
 * This class is intended to replace FTPClient, which will eventually be
 * deprecated. 
 * 
 * @author Bruce Blackshaw
 * @version $Revision: 1.19 $
 */
public class FileTransferClient implements FileTransferClientInterface {
    
    private Logger log = Logger.getLogger("FileTransferClient");
    
    /**
     * Context for the client that is the starting point for all 
     * new tasks. If we have a change directory task, when it is completed
     * it will update this master context and the updated context will be used
     * for all future tasks
     */
    protected ConnectionContext masterContext = new ConnectionContext();
    
    protected EventAggregator eventAggregator = null;
        
    /**
     * Event listeners
     */
    protected EventListener listener;
    
    /**
     * Instance of FTPClient 
     */
    private FTPClient ftpClient; 
    
    /**
     * Advanced configuration parameters that often aren't used
     */
    private AdvancedFTPSettings advancedFTPSettings;
    
    private AdvancedGeneralSettings advancedSettings;
    
    private FileStatistics statistics;
    
    /**
     * Default constructor
     */
    public FileTransferClient() {
        ftpClient = new FTPClient(); 
        advancedFTPSettings = new AdvancedFTPSettings(masterContext);
        advancedSettings = new AdvancedGeneralSettings(masterContext);
        statistics = new FileStatistics();
        statistics.addClient(ftpClient);
    } 
    
    /**
     * 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 && !isConnected())
            throw new FTPException("The file transfer client has not yet connected to the server.  "
                    + "The requested action cannot be performed until after a connection has been established.");
        else if (!shouldBeConnected && isConnected())
            throw new FTPException("The file transfer client has already been connected to the server.  "
                    +"The requested action must be performed before a connection is established.");
    }    
        
    /**
     * Is this client currently connected to the server?
     * 
     * @return  true if connected, false otherwise
     */
    public synchronized boolean isConnected() {
        return ftpClient.connected();
    }

    
    /**
     * Returns the IP address or name of the remote host.
     * 
     * @return Returns the remote host.
     */
    public synchronized String getRemoteHost() {
        return masterContext.getRemoteHost();
    }

    /**
     * Set the IP address or name of the remote host
     * 
     * This may only be done if the client is not already connected to the server.
     * 
     * @param remoteHost The IP address or name of the remote host
     * @throws FTPException Thrown if the client is already connected to the server.
     */
    public synchronized void setRemoteHost(String remoteHost) throws FTPException {
        checkConnection(false);
        masterContext.setRemoteHost(remoteHost);
    }
    
    /**
     * Returns the timeout for socket connections. 
     * 
     * @return Returns the connection timeout in milliseconds
     */
    public synchronized int getTimeout() {
        return masterContext.getTimeout();
    }
    
    /** 
     * Set the timeout for socket connections. Can only do this if
     * not already connected.
     * 
     * @param timeout  the timeout to use in milliseconds
     * @throws FTPException Thrown if the client is already connected to the server.
     */
    public synchronized void setTimeout(int timeout) throws FTPException {
        checkConnection(false);
        masterContext.setTimeout(timeout);
    }

    /**
     * Returns the port being connected to on the remote server. 
     * 
     * @return Returns the port being connected to on the remote server. 
     */
    public synchronized int getRemotePort() {
        return masterContext.getRemotePort();
    }
    
    /** 
     * Set the port to connect to on the remote server. Can only do this if
     * not already connected.
     * 
     * @param remotePort The port to use. 
     * @throws FTPException Thrown if the client is already connected to the server.
     */
    public synchronized void setRemotePort(int remotePort) throws FTPException {
        checkConnection(false);
        masterContext.setRemotePort(remotePort);
    }
    
    /**
     * Set the transfer type for all connections, either ASCII or binary. Setting
     * applies to all subsequent transfers that are initiated.
     * 
     * @param type            transfer type
     * @throws FTPException 
     * @throws IOException 
     * @throws FTPException
     */
    public synchronized void setContentType(FTPTransferType type) 
        throws IOException, FTPException {
        masterContext.setContentType(type);
        if (ftpClient != null && ftpClient.connected())
            ftpClient.setType(type);
    }
    
    /**
     * Get the current content type for all connections.
     * 
     * @return  transfer type
     */
    public synchronized FTPTransferType getContentType() {
        return masterContext.getContentType();
    }
    
    /**
     * Set auto detect of filetypes on or off. If on, the transfer mode is
     * switched from ASCII to binary and vice versa depending on the extension
     * of the file. After the transfer, the mode is always returned to what it
     * was before the transfer was performed. The default is off.
     * 
     * If the filetype is unknown, the transfer mode is unchanged
     * 
     * @param detectContentType    true if detecting content type, false if not
     */
    public void setDetectContentType(boolean detectContentType) {
        masterContext.setDetectContentType(detectContentType);
        if (ftpClient != null)
            ftpClient.setDetectTransferMode(detectContentType);
    }
    
    /**
     * Get the detect content type flag
     * 
     * @return true if we are detecting binary and ASCII transfers from the file type
     */
    public boolean isDetectContentType() {
        return masterContext.getDetectContentType();
    }
    
    /**
     * Set the name of the user to log in with. Can only do this if
     * not already connected.
     * 
     * @param userName          user-name to log in with.
     * @throws FTPException
     */
    public synchronized void setUserName(String userName) throws FTPException {
        checkConnection(false);
        masterContext.setUserName(userName);
    }
    
    /**
     * Get the current user password.
     * 
     * @return current user password
     */
    public synchronized String getPassword() {
        return masterContext.getPassword();
    }
    
    /**
     * Set the password of the user to log in with. Can only do this if
     * not already connected.
     * 
     * @param password          password to log in with.
     * @throws FTPException
     */
    public synchronized void setPassword(String password) throws FTPException {
        checkConnection(false);
        masterContext.setPassword(password);
    }
    
    /**
     * Get the current user name.
     * 
     * @return current user name
     */
    public synchronized String getUserName() {
        return masterContext.getUserName();
    }
    
    /**
     * Get the advanced FTP configuration parameters object
     * 
     * @return advanced parameters
     */
    public synchronized AdvancedFTPSettings getAdvancedFTPSettings() {
        return advancedFTPSettings;        
    }
    
    /**
     * Get the advanced general configuration parameters object, for none
     * protocol specific parameters
     * 
     * @return advanced parameters
     */
    public synchronized AdvancedGeneralSettings getAdvancedSettings() {
        return advancedSettings;        
    }
    
    /**
     * Set the event listener for transfer event notification
     * 
     * @param listener  event listener reference
     */
    public synchronized void setEventListener(EventListener listener) {
        this.listener = listener;
        eventAggregator = new EventAggregator(listener);
        if (ftpClient != null) {
            eventAggregator.setConnId(ftpClient.getId());       
            ftpClient.setMessageListener(eventAggregator);
            ftpClient.setProgressMonitor(eventAggregator);
            ftpClient.setProgressMonitorEx(eventAggregator);
        }
    }
    
    /**
     * Make a connection to the FTP server. 
     * 
     * @throws FTPException 
     * @throws IOException 
     */
    public synchronized void connect() throws FTPException, IOException {
        // events
        if (eventAggregator != null) {
            eventAggregator.setConnId(ftpClient.getId());
        
            ftpClient.setMessageListener(eventAggregator);
            ftpClient.setProgressMonitor(eventAggregator);
            ftpClient.setProgressMonitorEx(eventAggregator);
        }
        
        //stats
        statistics.clear();
        
        configureClient();
        log.debug("Configured client");
        ftpClient.connect();
        log.debug("Client connected");
        if (masterContext.isAutoLogin()) {
            log.debug("Logging in");
            ftpClient.login(masterContext.getUserName(), masterContext.getPassword());
            log.debug("Logged in");
            configureTransferType(masterContext.getContentType());
        }
        else {
            log.debug("Manual login enabled");
        }
    }
    
    /**
     * Perform a manual login using the credentials that have been set. This
     * should only be performed if auto login is disabled - normally connect()
     * performs the login automatically.
     * 
     * @throws FTPException
     * @throws IOException
     */
    public void manualLogin() throws FTPException, IOException {
        checkConnection(true);
        log.debug("Logging in");
        ftpClient.login(masterContext.getUserName(), masterContext.getPassword());
        log.debug("Logged in");
        configureTransferType(masterContext.getContentType());
    }
    
    /**
     * Apply the master context's settings to the client
     * 
     * @throws IOException
     * @throws FTPException
     */
    private void configureClient() throws IOException, FTPException {
        ftpClient.setRemoteHost(masterContext.getRemoteHost());
        ftpClient.setRemotePort(masterContext.getRemotePort());
        ftpClient.setTimeout(masterContext.getTimeout());  
        ftpClient.setRetryCount(masterContext.getRetryCount());
        ftpClient.setRetryDelay(masterContext.getRetryDelay());
        ftpClient.setControlEncoding(masterContext.getControlEncoding());
        ftpClient.setStrictReturnCodes(masterContext.isStrictReturnCodes());
        ftpClient.setDetectTransferMode(masterContext.getDetectContentType());
        ftpClient.setConnectMode(masterContext.getConnectMode());
        ftpClient.setParserLocales(masterContext.getParserLocales());
        ftpClient.setAutoPassiveIPSubstitution(masterContext.isAutoPassiveIPSubstitution());
        ftpClient.setDeleteOnFailure(masterContext.isDeleteOnFailure());
        ftpClient.setActiveIPAddress(masterContext.getActiveIPAddress());
        ftpClient.setMonitorInterval(masterContext.getTransferNotifyInterval());
        ftpClient.setTransferBufferSize(masterContext.getTransferBufferSize());
        ftpClient.setFileNotFoundMessages(masterContext.getFileNotFoundMessages());
        ftpClient.setDirectoryEmptyMessages(masterContext.getDirectoryEmptyMessages());
        ftpClient.setTransferCompleteMessages(masterContext.getTransferCompleteMessages());
        if (masterContext.getActiveHighPort() >= 0 && masterContext.getActiveLowPort() >= 0)
            ftpClient.setActivePortRange(masterContext.getActiveLowPort(), masterContext.getActiveHighPort());
       
    }
    
    
    private void configureTransferType(FTPTransferType type) 
        throws IOException, FTPException {
        ftpClient.setDetectTransferMode(masterContext.getDetectContentType());
        ftpClient.setType(type);
    }
    
    private void checkTransferSettings() throws FTPException {
        
        if (ftpClient.getDetectTransferMode() != masterContext.getDetectContentType())
            ftpClient.setDetectTransferMode(masterContext.getDetectContentType());
        
        if (ftpClient.isStrictReturnCodes() != masterContext.isStrictReturnCodes())
            ftpClient.setStrictReturnCodes(masterContext.isStrictReturnCodes());
        
        if (!ftpClient.getConnectMode().equals(masterContext.getConnectMode()))
            ftpClient.setConnectMode(masterContext.getConnectMode());        
        
        if (ftpClient.isAutoPassiveIPSubstitution() != masterContext.isAutoPassiveIPSubstitution())
            ftpClient.setAutoPassiveIPSubstitution(masterContext.isAutoPassiveIPSubstitution());
        
        if (ftpClient.isDeleteOnFailure() != masterContext.isDeleteOnFailure())
            ftpClient.setDeleteOnFailure(masterContext.isDeleteOnFailure());
        
        if (ftpClient.getActiveIPAddress() != masterContext.getActiveIPAddress())
            ftpClient.setActiveIPAddress(masterContext.getActiveIPAddress());
        
        if (ftpClient.getTransferBufferSize() != masterContext.getTransferBufferSize())
            ftpClient.setTransferBufferSize(masterContext.getTransferBufferSize());
        
        if (ftpClient.getMonitorInterval() != masterContext.getTransferNotifyInterval())
            ftpClient.setMonitorInterval(masterContext.getTransferNotifyInterval());  
        
        if (masterContext.getActiveHighPort() != ftpClient.getActiveHighPort() ||
            masterContext.getActiveLowPort() != ftpClient.getActiveLowPort())
            ftpClient.setActivePortRange(masterContext.getActiveLowPort(), masterContext.getActiveHighPort());
    }
    
    private void checkListingSettings() throws FTPException {
        ftpClient.setParserLocales(masterContext.getParserLocales());
        checkTransferSettings();
    }

	/**
	 * @param url
	 * @return
	 * @throws IOException
	 * @throws FTPException
	 */
	protected static void prepareURLTransfer(URL url, FileTransferClientInterface client) 
		throws IOException, FTPException {
		if (!url.getProtocol().equals("ftp"))
			throw new MalformedURLException("Unsupported protocol: "
					+ url.getProtocol());
    	client.setRemoteHost(url.getHost());
    	if (url.getPort()>0)
    		client.setRemotePort(url.getPort());
    	String userInfo = url.getUserInfo();
    	String userName = "anonymous", password = "";
    	if (userInfo!=null) {
    		int colonPos = userInfo.indexOf(':'); 
	    	if (colonPos>=0) {
	    		userName = userInfo.substring(0, colonPos);
	    		if (colonPos+1<userInfo.length()-1)
	    			password = userInfo.substring(colonPos+1);
	    	} else
	    		userName = userInfo;
    	}
    	client.setUserName(userName);
    	client.setPassword(password);
    	client.setDetectContentType(true);
    	if (url.getQuery()!=null) {
    		String query = url.getQuery();
    		String TYPE = "type=";
    		int typePos = query.indexOf(TYPE); 
    		if (typePos>=0 && query.length()>typePos+TYPE.length()) {
    			char typeChar = query.toUpperCase().charAt(typePos+TYPE.length());
    			if (typeChar==FTPTransferType.BINARY_CHAR.charAt(0)) {
  					client.setContentType(FTPTransferType.BINARY);
  			    	client.setDetectContentType(true);
    			} else if (typeChar==FTPTransferType.ASCII_CHAR.charAt(0)) {
  					client.setContentType(FTPTransferType.ASCII);
  			    	client.setDetectContentType(true);
    			} else
        			throw new MalformedURLException("Unknown type: " + query.substring(typePos));
    		}
    	}
	}

    
    /**
     * Get statistics on file transfers and deletions. 
     * 
     * @return FTPStatistics
     */
    public synchronized FileStatistics getStatistics() {
        return statistics;
    }
    
    /**
     * Request that the remote server execute the literal command supplied. In
     * FTP, this is the equivalent of 'quote'. It could be used to send a SITE
     * command to the server, e.g. "SITE recfm=FB lrecl=180 blksize=5400". 
     *  
     * @param command   command string
     * @return result string returned by server
     * @throws FTPException
     * @throws IOException
     */
    public synchronized String executeCommand(String command) 
        throws FTPException, IOException {
        return ftpClient.quote(command);
    }
    
    /**
     * Cancel current transfer if underway
     */
    public void cancelAllTransfers() {

        log.debug("cancelAllTransfers() called");
        ftpClient.cancelTransfer();
    }
    
    
    /**
     *  Gets the last reply from the server, whether valid or not
     *
     *  @return  reply object encapsulating last server response
     */
    public FTPReply getLastReply() {
        return ftpClient.getLastReply();
    }

    
    /**
     * Get a string that represents the remote system that the client is logged
     * into.
     * 
     * @return system string
     * @throws FTPException
     * @throws IOException
     */
    public synchronized String getSystemType() 
        throws FTPException, IOException {
        return ftpClient.system();
    }
    
    /**
     * List the names of files and directories in the current directory on the FTP server.
     * 
     * @return String[]
     * @throws FTPException, IOException 
     */
    public synchronized String[] directoryNameList() 
        throws FTPException, IOException {
        return directoryNameList("", false);
    }

    
    /**
     * List the names of files and directories of a directory on the FTP server.
     * 
     * @param directoryName    name of the directory (generally not a path). Some 
     *                          servers will accept a wildcard.
     * @param isLongListing    true if the listing is a long format listing
     * @return String[]
     * @throws FTPException, IOException 
     */
    public synchronized String[] directoryNameList(String directoryName, boolean isLongListing) 
        throws FTPException, IOException {
        checkListingSettings();
        return ftpClient.dir(directoryName, isLongListing);
    }
    
    /**
     * List the current directory on the FTP server.
     * 
     * @throws FTPException, IOException 
     * @throws ParseException 
     */
    public synchronized FTPFile[] directoryList() 
        throws FTPException, IOException, ParseException {
        return directoryList("");
    }
   
    /**
     * List a directory on the FTP server.
     * 
     * @param directoryName    name of the directory (generally not a path). Some 
     *                          servers will accept a wildcard.
     * @throws FTPException, IOException 
     * @throws ParseException 
     */
    public synchronized FTPFile[] directoryList(String directoryName) 
        throws FTPException, IOException, ParseException {
        checkListingSettings();
        return ftpClient.dirDetails(directoryName);
    }
    
    
    /**
     * List a directory on the FTP server.
     * 
     * @param directoryName    name of the directory (generally not a path)
     * @throws FTPException, IOException 
     */
    public synchronized void directoryList(String directoryName, DirectoryListCallback lister) 
        throws FTPException, IOException, ParseException {
        checkListingSettings();
        ftpClient.dirDetails(directoryName, lister);
    }

    
    /**
     * Download a file from the FTP server into a byte array.
     * 
     * @param remoteFileName   name of the remote file to be downloaded
     * @throws FTPException 
     */
    public synchronized byte[] downloadByteArray(String remoteFileName) throws FTPException, IOException {
        checkTransferSettings();
        return ftpClient.get(remoteFileName); 
    }
    
    /**
     * Download a file from the FTP server .
     * 
     * @param localFileName    name (or full path) of the local file to be downloaded to
     * @param remoteFileName   name of the remote file to be downloaded
     * @throws FTPException 
     */
    public synchronized void downloadFile(String localFileName, String remoteFileName) throws FTPException, IOException {
        downloadFile(localFileName, remoteFileName, WriteMode.OVERWRITE);
    }
    
    /**
     * Download a file from the FTP server .
     * 
     * @param localFileName    name (or full path) of the local file to be downloaded to
     * @param remoteFileName   name of the remote file to be downloaded
     * @param writeMode        mode in which the file is written to the client machine
     * @throws FTPException 
     */
    public synchronized void downloadFile(String localFileName, String remoteFileName, WriteMode writeMode) 
        throws FTPException, IOException {
        checkTransferSettings();
        if (writeMode.equals(WriteMode.RESUME)) {
            ftpClient.resume();
        }
        else if (writeMode.equals(WriteMode.APPEND)) {
            throw new FTPException("Append not permitted for downloads");
        }
        ftpClient.get(localFileName, remoteFileName); 
    }
    
    /**
     * Open an InputStream for the given FTP URL.  The URL must be of the form
     * <code>ftp://[user[:password]@]host[:port]/path</code>.  The stream should be closed when 
     * the transfer is complete.
	 * @param ftpURL The URL must be of the form <code>ftp://[user[:password]@]host[:port]/path</code>.
     * @return FileTransferInputStream for the given URL.  The stream should be closed when 
     * the transfer is complete.
     * @throws MalformedURLException Thrown when an invalid URL is provided.
     * @throws IOException Thrown when an I/O-related error occurs.
     * @throws FTPException Thrown when an FTP-protocol-related error occurs.
     */
    public static FileTransferInputStream downloadURLStream(String ftpURL) 
    	throws MalformedURLException, IOException, FTPException {
    	URL url = new URL(ftpURL);
    	if (!url.getProtocol().equals("ftp"))
    		throw new MalformedURLException("Unsupported protocol: " + url.getProtocol());
    	FileTransferClient client = new FileTransferClient();
		prepareURLTransfer(url, client);
    	client.connect();
		return new AutoCloseFileTransferInputStream(client.downloadStream(url.getPath()), client);
    }

	/**
	 * Downloads the given file to the given FTP URL. The URL must be of the form
	 * <code>ftp://[user[:password]@]host[:port]/path</code>.
	 * @param localFileName Path to local file
	 * @param ftpURL The URL must be of the form <code>ftp://[user[:password]@]host[:port]/path</code>.
	 * @return FileTransferOutputStream for the given URL. The stream should be closed when the transfer is complete.
	 * @throws MalformedURLException Thrown when an invalid URL is provided.
	 * @throws IOException Thrown when an I/O-related error occurs.
	 * @throws FTPException Thrown when an FTP-protocol-related error occurs.
	 */
    public static void downloadURLFile(String localFileName, String ftpURL)
			throws MalformedURLException, IOException, FTPException {
		URL url = new URL(ftpURL);
		FileTransferClient client = new FileTransferClient();
		if (!url.getProtocol().equals("ftp"))
			throw new MalformedURLException("Unsupported protocol: "
					+ url.getProtocol());
		prepareURLTransfer(url, client);
		client.connect();
		try {
			client.downloadFile(localFileName, url.getPath());
		} finally {
			client.disconnect();
		}
	}
    
     /**
     * Download a file from the FTP server as a stream. The close() method <b>must</b>
     * be called on the stream once the stream has been read.
     * 
     * @param remoteFileName   name of the remote file to be downloaded
     * @return InputStream
     * @throws FTPException 
     */
     public synchronized FileTransferInputStream downloadStream(String remoteFileName) 
         throws FTPException, IOException {
         checkTransferSettings();
         return new FTPInputStream(ftpClient, remoteFileName);
    }
     
      /**
       * Upload a file to the FTP server. If a null is supplied as
       * the remoteFileName, the STOU (store unique) FTP feature will be used (if 
       * available) and the FTP server will generate a unique name for the file. 
       * 
       * @param localFileName
       *            name (or full path) of the local file to be downloaded to
       * @param remoteFileName
       *            name of the remote file to be downloaded (or null to generate a unique name)
       * @return name of remote file
       * @throws FTPException 
       */
      public synchronized String uploadFile(String localFileName, String remoteFileName) 
          throws FTPException, IOException {
          return uploadFile(localFileName, remoteFileName, WriteMode.OVERWRITE);
      }
      
      /**
       * Upload a file to the FTP server. If a null is supplied as
       * the remoteFileName, the STOU (store unique) FTP feature will be used (if 
       * available) and the FTP server will generate a unique name for the file. 
       * 
       * @param localFileName
       *            name (or full path) of the local file to be downloaded to
       * @param remoteFileName
       *            name of the remote file to be downloaded (or null to generate a unique name)
       * @param writeMode   mode to write to the remote file with
       * @return name of remote file
       * @throws FTPException 
       */
      public synchronized String uploadFile(String localFileName, String remoteFileName, WriteMode writeMode) 
          throws FTPException, IOException {  
          checkTransferSettings();
          boolean append = false;
          if (writeMode.equals(WriteMode.RESUME)) {
              ftpClient.resume();
          }
          else if (writeMode.equals(WriteMode.APPEND)) {
              append = true;
          }
          return ftpClient.put(localFileName, remoteFileName, append);
      }
      
      /**
       * Upload a file to the FTP server by writing to a stream. The close() method <b>must</b>
       * be called on the stream once the stream has been read.
       * If a null is supplied as
       * the remoteFileName, the STOU (store unique) FTP feature will be used (if 
       * available) and the FTP server will generate a unique name for the file. This
       * name can be obtained afterwards from {@see FileTransferOutputStream#getRemoteFile()}
       * 
       * @param remoteFileName   name of the remote file to be uploaded
       * @return FTPOutputStream
       * @throws FTPException 
       * @throws IOException 
       */
       public synchronized FileTransferOutputStream uploadStream(String remoteFileName) 
           throws FTPException, IOException {
           return uploadStream(remoteFileName, WriteMode.OVERWRITE);
      }

       /**
        * Upload a file to the FTP server by writing to a stream. The close() method *must*
        * be called on the stream once the stream has been read.
        * If a null is supplied as
        * the remoteFileName, the STOU (store unique) FTP feature will be used (if 
        * available) and the FTP server will generate a unique name for the file. This
        * name can be obtained afterwards from {@see FileTransferOutputStream#getRemoteFile()}
        * 
        * @param remoteFileName   name of the remote file to be uploaded
        * @param writeMode        mode for writing to the server (supporting use of append)
        * @return FTPOutputStream
        * @throws FTPException 
        * @throws IOException 
        */
        public synchronized FileTransferOutputStream uploadStream(String remoteFileName, WriteMode writeMode) 
            throws FTPException, IOException {
            checkTransferSettings();
            if (WriteMode.RESUME.equals(writeMode))
                throw new FTPException("Resume not supported for stream uploads");
            boolean append = WriteMode.APPEND.equals(writeMode);
            return new FTPOutputStream(ftpClient, remoteFileName, append);
       }
        
    /**
	 * Open an OutputStream for the given FTP URL. The URL must be of the form
	 * <code>ftp://[user[:password]@]host[:port]/path</code>. The stream should
	 * be closed when the transfer is complete.
	 * @param ftpURL The URL must be of the form <code>ftp://[user[:password]@]host[:port]/path</code>.
	 * @return FileTransferOutputStream for the given URL. The stream should be closed when the transfer is complete.
	 * @throws MalformedURLException Thrown when an invalid URL is provided.
	 * @throws IOException Thrown when an I/O-related error occurs.
	 * @throws FTPException Thrown when an FTP-protocol-related error occurs.
	 */
	public static FileTransferOutputStream uploadURLStream(String ftpURL)
			throws MalformedURLException, IOException, FTPException {
		URL url = new URL(ftpURL);
		FileTransferClient client = new FileTransferClient();
		if (!url.getProtocol().equals("ftp"))
			throw new MalformedURLException("Unsupported protocol: "
					+ url.getProtocol());
		prepareURLTransfer(url, client);
		client.connect();
		return new AutoCloseFileTransferOutputStream(client.uploadStream(url
				.getPath()), client);
	}

	/**
	 * Uploads the given file to the given FTP URL. The URL must be of the form
	 * <code>ftp://[user[:password]@]host[:port]/path</code>.
	 * @param localFileName Path to local file
	 * @param ftpURL The URL must be of the form <code>ftp://[user[:password]@]host[:port]/path</code>.
	 * @return FileTransferOutputStream for the given URL. The stream should be closed when the transfer is complete.
	 * @throws MalformedURLException Thrown when an invalid URL is provided.
	 * @throws IOException Thrown when an I/O-related error occurs.
	 * @throws FTPException Thrown when an FTP-protocol-related error occurs.
	 */
    public static void uploadURLFile(String localFileName, String ftpURL)
			throws MalformedURLException, IOException, FTPException {
		URL url = new URL(ftpURL);
		FileTransferClient client = new FileTransferClient();
		prepareURLTransfer(url, client);
		client.connect();
		try {
			client.uploadFile(localFileName, url.getPath());
		} finally {
			client.disconnect();
		}
	}
        
     /**
      * Get the size of a remote file.
      * 
      * @param remoteFileName   name of remote file
      * @return long
      * @throws FTPException 
      */
     public synchronized long getSize(String remoteFileName) 
         throws FTPException,IOException {
         return ftpClient.size(remoteFileName);
     }
     

    /**
     * Get the modified-time of a remote file.
     * 
     * @param remoteFileName   name of remote file
     * @return Date
     * @throws FTPException 
     */
    public synchronized Date getModifiedTime(String remoteFileName) throws FTPException, IOException {
        return ftpClient.modtime(remoteFileName);
    }
    
    /**
     * Set the modified-time of a remote file. May not be supported by
     * all servers.
     * 
     * @param remoteFileName   name of remote file
     * @param modifiedTime    new modified time
     * @throws FTPException 
     */
    public synchronized void setModifiedTime(String remoteFileName, Date modifiedTime) 
        throws FTPException, IOException {
        ftpClient.setModTime(remoteFileName, modifiedTime);
    }

    
    /**
     * Determine if a remote file exists.
     * 
     * @param remoteFileName   name of remote file
     * @throws FTPException 
     */
    public synchronized boolean exists(String remoteFileName) 
        throws FTPException, IOException {  
        return ftpClient.exists(remoteFileName);
    }
    
    
    /**
     * Deletes a remote file.
     * 
     * @param remoteFileName   name of remote file
     * @throws FTPException 
     * @throws IOException 
     */
    public synchronized void deleteFile(String remoteFileName) 
        throws FTPException, IOException {
        ftpClient.delete(remoteFileName);
    }

    
    /**
     * Rename a remote file or directory.
     * 
     * @param renameFromName
     *            original name
     * @param renameToName
     *            new name
      * @throws FTPException, IOException
     */
    public synchronized void rename(String renameFromName, String renameToName) 
        throws FTPException, IOException {
        ftpClient.rename(renameFromName, renameToName);
    }
    
    
    /**
     * Change directory on the FTP server. 
     * 
     * @param directoryName
     *            name the remote directory to change into
     * @throws FTPException, IOException 
     */
    public synchronized void changeDirectory(String directoryName) throws FTPException, IOException {
        ftpClient.chdir(directoryName);
    }
   
 
    /**
     * Change to parent directory on the FTP server. 
     * 
     * @throws FTPException, IOException 
     */
    public synchronized void changeToParentDirectory() throws FTPException, IOException {
        ftpClient.cdup();
    }    
    
    /**
     * Get the current remote directory.
     * 
     * @return current remote directory
     * @throws FTPException 
     * @throws IOException 
     */
    public synchronized String getRemoteDirectory() throws IOException, FTPException {
        return ftpClient.pwd();
    }
    
    /**
     * Create directory on the FTP server. 
     * 
     * @param directoryName
     *            name the remote directory to create
     * @throws FTPException, IOException 
     */
    public synchronized void createDirectory(String directoryName) throws FTPException, IOException {
        ftpClient.mkdir(directoryName);        
    }

    
    /**
     * Delete directory on the FTP server. The directory must be empty. Note
     * that edtFTPj/PRO supports recursive directory deletions.
     * 
     * @param directoryName
     *            name the remote directory to create
     * @throws FTPException, IOException 
     */
    public synchronized void deleteDirectory(String directoryName) 
        throws FTPException, IOException {
        ftpClient.rmdir(directoryName);
     }

    /**
     * Disconnect from the FTP server.
     * 
     * @throws FTPException, IOException
     */
    public synchronized void disconnect() 
        throws FTPException, IOException {
        ftpClient.quit();
    }
    
    /**
     * Disconnect from the FTP server.
     * 
     * @throws FTPException, IOException
     */
    public synchronized void disconnect(boolean immediate) 
        throws FTPException, IOException {
        if (immediate)
            ftpClient.quitImmediately();
        else
            ftpClient.quit();
    }
    
}
