How to use asynchronous methods

AsyncFileTransferClient provides an interface that enables developers to obtain the advantages of multi-threading without having to explicitly write any multi-threaded code. Multiple file-transfer operations may be launched from a single thread, such that they execute concurrently in the background.

The basic pattern is to call one of the asynchronous methods such as downloadFileAsync(). When an asynchronous method is invoked a file-transfer job is queued and the method returns. The job will continue running in the background until it has completed. Once it has completed it will optionally invoke a user-specified callback function. The developer may place in the callback function any code that needs to be executed after the job is complete. This may involve, for example, some sort of clean-up operation, or perhaps the launching of another job. The following illustrates connecting to a server using an asynchronous method:

public class MyConnectTest implements Connect {
    
private AsyncFileTransferClient client = new AsyncFileTransferClient();
    
public void startConnecting() {
  client.setRemoteHost(host);
  client.setUserName(user);
  client.setPassword(password);
  client.connectAsync(this, null);
}
    
public void onConnect(ConnectResult result)
  throws FTPException, IOException {
  result.endAsync();
  System.out.println("Connected!")
}

Notice that MyConnectTest implements the Connect callback interface. This interface has a single method called "onConnect". This method is what is referred to as a "callback method" and is called once the connection has completed.

As with SecureFileTransferClient, it is important to note that when connectAsync() is called, a thread pool and a connection pool are created for servicing requests. The disconnectAsync() method must be called to terminate the pools. If disconnectAsync() is not called, applications will not exit. Synchronous and asynchronous methods can be mixed, so disconnect() could also be used.

Callback methods

Every asynchronous call can take a callback object, and when the operation is completed (in the background), the callback is notified. Each asynchronous call supports its own callback interface that must be implemented by the callback object. A call-specific result object is passed into the callback. In the case of connectAsync(), the callback class implements the Connect callback interface, and a ConnectResult object is passed into onConnect().

To complete the asynchronous operation, the endAsync() method on the result object must be called. If called from within the callback (as shown above), endAsync() returns immediately, as the operation has completed. As the result object is also returned when an asynchronous call is first initiated, it can be called immediately afterwards. In this instance, it will block until the call is completed.

Callback chaining

Subsequent asynchronous methods can be called from within callbacks, creating a chain of callbacks. This creates what is called the callback context for the chain - a group of settings that are independent of the AsyncFileTransferClient as a whole. These settings can be changed without affecting any other operations on AsyncFileTransferClient, including other callback chains.

Do not call synchronous methods from within a callback. The synchronous method call in the callback will be waiting to get a free connection, while the asynchronous call that resulted in the callback has not yet freed the connection (and it can't until the synch call has completed). This will result in deadlock if the pool has only one connection. Even with more than one connection, as soon as multiple asynchronous method calls are made, deadlock will result.

These settings are the remote directory, the content type (ASCII or binary), and content type detection (and automatic switching). This is so that if multiple callbacks are being called, they can each navigate down a different directory tree, and one can perform binary transfers while another use ASCII.

This means that a changeDirectoryAsync launched from the main thread will eventually change the directory of all connections when it has completed. But if the same method is called from within a callback, only that callback's subsequent operations will see the changed directory. So in the example below, the changeDirectoryAsync() call only changes the remote directory for the callback chain.

public class MyTransferTest implements Connect {
    
  public void startConnecting() {
    AsyncFileTransferClient client = new AsyncFileTransferClient();
    client.connectAsync(this, null);
  }
    
  public void onConnect(ConnectResult result)
    throws FTPException, IOException {
    result.endAsync();
    result.getClient().changeDirectoryAsync(dir, this, null);
  }
    
  public void onChangeDirectory(ChangeDirectoryResult result)
    throws FTPException, IOException {
    result.endAsync();
    result.getClient().uploadFileAsync(localFilepath, remoteFileName, null, null);
  }
}

It should be noted that the result object always has a reference to the AsyncFileTransferClient instance that initiated the asynchronous call. This reference is available via the getClient() method.

Tags

Every asynchronous method has a final optional parameter of type Object, called 'tag'. This is simply a convenient placeholder for any object that the developer wishes to have available in the callback method. It can be obtained by calling getTag() on the result object. It is most useful when the callback method is on a different object altogther. For example, the size of the local file being uploaded might be passed in here, so when the upload is complete, the user can be notified that a file of size x has been uploaded.

Exception handling

Exceptions are reported when endAsync() is called, i.e. if an exception occurred during the operation, it is thrown at this point. An error listener can also be set up to collect exceptions. In the example below, the calling class must implement the ErrorListener interface. Exceptions are reported via the onError() callback:

AsyncFileTransferClient client = new AsyncFileTransferClient();
client.addErrorListener(this);