Reading and Writing Styx files

The most common tasks (from the client's point of view at least) in a Styx system are reading from and writing to files. There are several ways to do this, each with advantages and disadvantages. In this section of the tutorial, we'll go through the options.

getContents() and setContents()

The easiest way to read from and write to files is to use the getContents() and setContents() methods, as used in the SimpleClient from earlier in this tutorial. These methods are suitable if the entire contents of the file can fit sensibly in a String, i.e. for relatively small data volumes.

Once you have a handle to a CStyxFile object, you can call setContents() and getContents() to write and read the entire contents of the file as Strings:

    file.setContents("hello JStyx world");
    System.out.println(file.getContents());
    
Note that both setContents() and getContents can throw StyxExceptions and so you will have to catch this or re-throw it from the method. If you run this code the string "hello JStyx world" should be printed out. (Try running the SimpleServer again and try this out. You can adapt the SimpleClient class to produce the client code.)

InputStreams and OutputStreams

Another easy-to-use option for reading and writing is through streams. This is probably one of the most familiar ways of dealing with I/O to Java programmers. In essence, once you have a CStyxFile object you can turn it into an InputStream or OutputStream by using the wrapper classes CStyxFileInputStream and CStyxFileOutputStream respectively. You can then use standard stream I/O to get data from and to the files on the Styx server.

Character-based I/O can be achieved by further wrapping these streams in CStyxFileInputStreamReader and CStyxFileOutputStreamWriter objects. These convert the streams into character streams by using the UTF-8 character set. These Readers and Writers can then be wrapped yet again as BufferedReaders and BufferedWriters to allow, for example, reading and writing data a line at a time from a remote file.

Using URLs to get handles to streams

You can get a handle to a Styx file on a remote server using a URL. For example, the URL of a file called readme in the root directory of a Styx server on localhost, port 9876 would be styx://localhost:9876/readme. You can use this URL to get an Input- or OutputStream for interacting with this file, as in this code snippet:

      URL url = new URL("styx://localhost:9876/readme");
      InputStream is = url.openStream();
      OutputStream os = url.openConnection().getOutputStream();
      
Note that you do not have to instantiate or open a StyxConnection before you do this. This is done automatically in the protocol handler for the styx:// URLs.

In order to make Java recognize styx:// URLs, you have to add the string uk.ac.rdg.resc.jstyx.client.protocol to the system property java.protocol.handler.pkgs. This is done automatically by the JStyxRun script in the bin/ directory of the JStyx distribution. If you don't set this property, you will get MalformedURLExceptions when trying to create URL objects from styx:// URLs.

download() and upload()

The download() and upload() methods of the CStyxFile class provide convenient methods for copying data from a remote Styx file to a local java.io.File or vice-versa.

Some technical details

The above methods of reading and writing completely hide the details of the Styx protocol mechanisms from the user. In order to understand the remainder of this section of the tutorial, you will need to know a little about how Styx works.

The most important thing you need to know is that when you read from - or write to - Styx files, you do so in chunks. When you read from a file, you are actually making lots of individual requests for data. By default, JStyx reads and writes data a maximum of 8KB at a time. So, if you are downloading a file of 1MB in size, you are actually making at least 128 separate requests for 8KB of data. (It is possible to choose a different maximum message size at the point of making a connection to a server: see the various constructors for the StyxConnection class. However, it is generally recommended to stick with the default message size unless you know what you're doing.)

When the server receives a request for a chunk of data, it can respond with a chunk of any size from zero bytes to the requested chunk size. If the server responds with zero bytes, this means that the end of the file has been reached. Clients can make requests for any chunk size up to the maximum allowable on the connection.

This feature of the Styx protocol has several advantages, including the fact that it is easy to download data from arbitrary positions in the remote file. However, it means that reading and writing large amounts of data are rather slower than with a system (e.g. HTTP) that simply opens a socket connection and passes the data in one long stream. The speed can be significantly increased by selecting a larger maximum message size when making the connection to the server (64KB is suggested as a maximum) or by using an "accelerated download" by making several simultaneous read requests, thereby attempting to saturate the connection (see the download(File file, int numRequests) method). However, Styx file transfer rates generally do not exceed HTTP transfer rates for static files.

read() and write()

There may be situations in which you want to have more control over the reading and writing of files: perhaps you want to read or write data from or to a specific position in the remote file. In this case you can use the read() and write() methods of CStyxFile.

The read() method takes as an argument the offset (position) in the remote file from which you wish to read data. It returns a ByteBuffer of data, but this is not the normal java.nio.ByteBuffer to which you might be accustomed, although it is very similar. This is a ByteBuffer from the MINA framework, which is the networking software that underlies JStyx. MINA ByteBuffers are obtained from a pool and returned to the pool when they are no longer needed. This means that ByteBuffers are not continually being created and garbage-collected. This gain in efficiency comes at a price: when using the read() method of CStyxFile you must remember to call the release() method on the ByteBuffer that is returned, once you have finished with the data.

There are a few versions of the write() method. In each case you provide a byte array containing the data to write and specify the position in the remote file where you want the data to go. You can also specify whether you want the remote file to be truncated at the end of the data. If the byte array that you provide is larger than the maximum message size... [TODO: I don't think JStyx checks for this at the moment!!] To save you worrying about how big the input array is, the writeAll() method allows you to write an array of any size: the data in the array will be split across several messages if necessary.

When using the read() and write() methods, the file is opened automatically in the correct mode. However, you should remember to close() the file when you have finished with it.

Asynchronous reading and writing

So far, all the methods we have used have been synchronous in nature. That is to say, the methods only return when their job is done. However, there may be situations in which there may be a significant time gap between sending a read request and actually getting the data back: this may not be because of a slow server, but by deliberate design of the Styx system (see the section of the tutorial on asynchronous files for example). Also, when writing graphical programs, you will want to keep the user interface responsive and it will be undesirable to have your program hang while waiting for data. You can solve this by firing off lots of threads but there is a neater way: use the asynchronous versions of the reading and writing methods.

There are a couple of ways of doing asynchronous reading and writing, but both are based on the idea that you send the read and write message using one method, which returns immediately, leaving your program to do other things. When the reply arrives, a specified callback method is called so that you can deal with it.

Using a change listener

The first way to use asynchronous reading and writing is by creating a class that implements the CStyxFileChangeListener interface. (Or, for convenience, you might choose to subclass the CStyxFileChangeAdapter abstract class, which provides empty default implementations of all the methods in the interface.)

Having got a CStyxFile, you register your change listener using the addChangeListener() method. Then you call one of the ...Async() methods (e.g. readAsync()) and the relevant method in the change listener will be called when the reply arrives. For example, here is a code snippet that will read a file from a remote server:

      public class DataReader extends CStyxFileChangeAdapter
      {
          ...
          public void readFile(CStyxFile file)
          {
              // Register this object as a change listener
              file.addChangeListener(this);
              // Read the first chunk of data from the file
              file.readAsync(0);
              // This returns immediately
          }
          ...
          public void dataArrived(CStyxFile file,
                  TreadMessage tReadMsg, ByteBuffer data)
          {
              // This method is called when the data arrive.  The arguments to
              // this method contain the file that is being read, the original
              // read message and the data themselves.
              if (data.hasRemaining())
              {
                  // We got some data back.  Work out the offset (file position)
                  // of the next chunk
                  long offset = tReadMsg.getOffset().asLong() + data.remaining();
                  // ... (Do something with the data here)
                  // Now read the next chunk of data.  This method will be
                  // called again when the data arrive.
                  file.readAsync(offset);
              }
              else
              {
                  // We have reached end of file.  Close the file.
                  file.close();
              }
          }
          ...
      }
      
Writing data is very similar, except that you use the writeAsync() method and, when the write confirmation arrives, the dataWritten() method of all registered change listeners will be called. These are all the asynchronous methods with their relevant callbacks in the CStyxFileChangeListener interface:
PurposeCStyxFile methodChange listener callback
Reading datareadAsync()dataArrived()
Writing datawriteAsync()dataWritten()
Opening a fileopenAsync()fileOpen()
Creating a filecreateAsync()fileCreated()
Getting the children of a directorygetChildrenAsync()childrenFound()
Downloading a filedownloadAsync()downloadComplete()
Uploading a fileuploadAsync()uploadComplete()
Getting the stat (properties) of a filerefreshAsync()statChanged()
Catching errorsall methodserror()
Note that errors from all asynchronous methods are caught in the error() method of the change listener.

One Golden Rule

When implementing callback functions (such as dataArrived()), you must be very careful to avoid using non-asynchronous (blocking) methods such as read() and write(). This will cause deadlock (you will block the thread that dispatches Styx replies). You can only use asynchronous methods within callback functions. The Javadoc comments for each function will tell you whether a method blocks, but in general, only methods called xxxAsync() will be guaranteed not to block. An exception to this is the close() method, which never blocks (it doesn't wait for the reply to the close request).

Using MessageCallbacks

Sometimes you might not want to use a CStyxFileChangeListener: perhaps you want more control over individual Styx messages or you don't like the way that all errors are caught in the same error() callback in the change listener. In this case, you can create individual callback objects for each call to an asynchronous method.

To do this, you create an instance of the MessageCallback abstract class. This requires you to implement two methods: replyArrived(), which is called if the operation succeeds; and error(), which is called if an error occurs. (The error() callback is equivalent to the throwing of a StyxException in the synchronous methods). The following example will set the contents of the remote file to the given String (i.e. the asynchronous equivalent of setContents():

      public void writeString(CStyxFile file, String str)
      {
          // Write the string to the beginning of the file (offset=0).
          // The file will be truncated at the end of the string
          file.writeAsync(str, 0, new WriteStringCallback());
      }
      private class WriteStringCallback extends MessageCallback
      {
          public void replyArrived(StyxMessage rMessage, StyxMessage tMessage)
          {
              // The arguments to this method are the request (the tMessage)
              // and the reply (the rMessage), but we don't always use them.
              System.out.println("Write confirmation arrived");
          }
          public void error(String errString, StyxMessage tMessage)
          {
              // The arguments to this method are the request (the tMessage)
              // and the error string
              System.err.println("An error occurred: " + errString);
          }
      }
      
There are a number of writeAsync() methods that can be used: see the code or the Javadoc for the CStyxFile class.