View Javadoc

1   /*
2    * Copyright (c) 2005 The University of Reading
3    * All rights reserved.
4    *
5    * Redistribution and use in source and binary forms, with or without
6    * modification, are permitted provided that the following conditions
7    * are met:
8    * 1. Redistributions of source code must retain the above copyright
9    *    notice, this list of conditions and the following disclaimer.
10   * 2. Redistributions in binary form must reproduce the above copyright
11   *    notice, this list of conditions and the following disclaimer in the
12   *    documentation and/or other materials provided with the distribution.
13   * 3. Neither the name of the University of Reading, nor the names of the
14   *    authors or contributors may be used to endorse or promote products
15   *    derived from this software without specific prior written permission.
16   * 
17   * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
18   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
19   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20   * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
21   * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
22   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24   * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25   * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26   * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27   */
28  
29  package uk.ac.rdg.resc.jstyx.client;
30  
31  import java.util.Vector;
32  import java.util.Iterator;
33  import java.util.Date;
34  
35  import java.io.File;
36  import java.io.InputStream;
37  
38  import org.apache.log4j.Logger;
39  
40  import org.apache.mina.common.ByteBuffer;
41  
42  import uk.ac.rdg.resc.jstyx.StyxException;
43  import uk.ac.rdg.resc.jstyx.StyxUtils;
44  
45  import uk.ac.rdg.resc.jstyx.types.DirEntry;
46  import uk.ac.rdg.resc.jstyx.types.ULong;
47  import uk.ac.rdg.resc.jstyx.types.Qid;
48  
49  import uk.ac.rdg.resc.jstyx.messages.*;
50  import uk.ac.rdg.resc.jstyx.client.callbacks.*;
51  
52  /***
53   * A Styx file (or directory) from the point of view of the client. (It is called
54   * a CStyxFile in order to avoid confusion with the server-side StyxFile class.)
55   * To create a CStyxFile, open a StyxConnection and use the getFile() method.
56   * CStyxFiles cannot be created directly.
57   * @todo implement a create() method that automatically creates all necessary
58   * subdirectories
59   * @todo implement changing of stat data (length etc) via a Twstat message
60   *
61   * @author Jon Blower
62   * $Revision: 609 $
63   * $Date: 2006-03-31 18:09:42 +0100 (Fri, 31 Mar 2006) $
64   * $Log$
65   * Revision 1.46  2006/03/21 14:58:41  jonblower
66   * Implemented clear-text password-based authentication and did some simple tests
67   *
68   * Revision 1.45  2006/01/06 10:14:22  jonblower
69   * Clarified comments
70   *
71   * Revision 1.44  2005/12/07 08:51:13  jonblower
72   * Added option to readAsync() to open file for reading and writing with truncation
73   *
74   * Revision 1.43  2005/12/01 08:21:55  jonblower
75   * Fixed javadoc comments
76   *
77   * Revision 1.42  2005/11/07 22:00:54  jonblower
78   * Added upload(java.io.File) method
79   *
80   * Revision 1.41  2005/10/14 18:05:19  jonblower
81   * Added walkFid() and exists() methods
82   *
83   * Revision 1.40  2005/09/11 18:50:42  jonblower
84   * Added toString() method
85   *
86   * Revision 1.39  2005/09/02 16:54:26  jonblower
87   * Added new writeAsync() methods
88   *
89   * Revision 1.38  2005/08/31 17:07:15  jonblower
90   * Fixed bug in getContents() and released ByteBuffer in fireDataArrived()
91   *
92   * Revision 1.36  2005/08/10 18:31:55  jonblower
93   * Bug fixes, plus added synchronous openOrCreate() method
94   *
95   * Revision 1.34  2005/08/05 13:46:40  jonblower
96   * Factored out all callback objects from CStyxFile into separate classes
97   *
98   * Revision 1.33  2005/08/04 16:48:57  jonblower
99   * Added and edited upload() methods in CStyxFile
100  *
101  * Revision 1.32  2005/07/28 16:37:35  jonblower
102  * Added isSameFile() method
103  *
104  * Revision 1.31  2005/07/06 17:46:19  jonblower
105  * Added setDirEntry()
106  *
107  * Revision 1.30  2005/06/27 17:18:03  jonblower
108  * Added accelerated downloading methods, and responded to changes with MessageCallback
109  *
110  * Revision 1.29  2005/06/22 17:08:34  jonblower
111  * Changed to allow target file to be null in download()
112  *
113  * Revision 1.28  2005/06/20 17:20:34  jonblower
114  * Added download() and downloadAsync() to CStyxFile
115  *
116  * Revision 1.27  2005/06/14 07:45:16  jonblower
117  * Implemented setting of params and async notification of parameter changes
118  *
119  * Revision 1.26  2005/05/26 16:48:48  jonblower
120  * Fixed bug in uploadFileAsync()
121  *
122  * Revision 1.24  2005/05/25 16:58:01  jonblower
123  * Changed implementation of openOrCreate()
124  *
125  * Revision 1.23  2005/05/25 15:36:55  jonblower
126  * No longer requires open fid, implemented createAsync() and openOrCreateAsync() methods
127  *
128  * Revision 1.22  2005/05/24 12:55:30  jonblower
129  * Added uploadFile()
130  *
131  * Revision 1.21  2005/05/23 16:48:17  jonblower
132  * Overhauled CStyxFile (esp. asynchronous methods) and StyxConnection (added cache of CStyxFiles)
133  *
134  * Revision 1.20  2005/05/23 07:36:19  jonblower
135  * Implementing uploadFileAsync
136  *
137  * Revision 1.19  2005/05/18 17:12:01  jonblower
138  * Added getURL() method
139  *
140  * Revision 1.18  2005/05/17 14:36:11  jonblower
141  * Fixed bug with getChildrenAsync()
142  *
143  * Revision 1.17  2005/05/12 15:59:59  jonblower
144  * Implemented getChildrenAsync()
145  *
146  * Revision 1.16  2005/05/12 14:20:55  jonblower
147  * Changed dataSent() method to dataWritten() (more accurate name)
148  *
149  * Revision 1.15  2005/05/12 08:00:33  jonblower
150  * Added getChildrenAsync() to CStyxFile and childrenFound() to CStyxFileChangeListener
151  *
152  * Revision 1.14  2005/05/12 07:40:52  jonblower
153  * CStyxFile.close() no longer throws a StyxException
154  *
155  * Revision 1.12  2005/05/05 07:08:37  jonblower
156  * Improved handling of buffers in change listeners
157  *
158  * Revision 1.11  2005/03/19 21:46:58  jonblower
159  * Further fixes relating to releasing ByteBuffers
160  *
161  * Revision 1.10  2005/03/18 13:55:59  jonblower
162  * Improved freeing of ByteBuffers, and bug fixes
163  *
164  * Revision 1.9  2005/03/16 17:55:52  jonblower
165  * Replaced use of java.nio.ByteBuffer with MINA's ByteBuffer to minimise copying of buffers
166  *
167  * Revision 1.8  2005/03/11 13:58:25  jonblower
168  * Merged MINA-Test_20059309 into main line of development
169  *
170  * Revision 1.7.2.3  2005/03/11 12:30:29  jonblower
171  * Changed so that message payloads are always ints, not longs
172  *
173  * Revision 1.7.2.2  2005/03/10 20:54:57  jonblower
174  * Removed references to Netty
175  *
176  * Revision 1.7.2.1  2005/03/10 11:49:17  jonblower
177  * Removed unneeded reference to StyxBuffer
178  *
179  * Revision 1.7  2005/03/07 08:29:18  jonblower
180  * Minor changes (better handling of path building)
181  *
182  * Revision 1.6  2005/02/28 11:43:36  jonblower
183  * Tidied up logging code
184  *
185  * Revision 1.5  2005/02/21 18:06:13  jonblower
186  * Un-synchronized many methods, made more robust in case of multiple calls to asynchronous methods
187  *
188  * Revision 1.4  2005/02/18 17:57:31  jonblower
189  * Changed a constructor to private access
190  *
191  * Revision 1.3  2005/02/18 09:11:35  jonblower
192  * Remove 'synchronized' from some methods, added some comments
193  *
194  * Revision 1.1.1.1  2005/02/16 18:58:17  jonblower
195  * Initial import
196  *
197  */
198 public class CStyxFile
199 {
200     
201     private static final Logger log = Logger.getLogger(CStyxFile.class);
202     
203     private StyxConnection conn;  // The connection on which the file sits
204     private String path;          // The path of the file relative to the
205                                   // root of the file server
206     private String name;          // The name of the file (i.e. the last
207                                   // part of the path)
208     private long fid;             // The client's file identifier
209     private Qid qid;              // The server's unique identifier for this file
210     private DirEntry dirEntry;    // The server's representation of this file
211     private int ioUnit;           // The maximum number of bytes that can be
212                                   // written to, or read from, this file in
213                                   // a single operation.
214     private int mode;             // The mode under which we have the file open
215                                   // -1 means that the file is not open
216     private Vector listeners;     // The CStyxFileChangeListeners that are waiting
217                                   // for notification of changes to this file
218     private CStyxFile[] children; // The children of this directory
219     
220     /***
221      * Creates a new instance of CStyxFile. This doesn't actually open or create
222      * the file; use open() or create() for this.
223      * @throws InvalidPathException if the path is not valid
224      */
225     CStyxFile(StyxConnection conn, String path)
226     {
227         this.conn = conn;
228         this.path = getCanonicalPath(path);
229         // Get the name of the file (the last part of the path)
230         int lastSlash = this.path.lastIndexOf("/");
231         this.name = this.path.substring(lastSlash + 1);
232         this.fid = -1;
233         this.ioUnit = 0;
234         this.mode = -1;
235         this.qid = null;
236         this.dirEntry = null;
237         this.listeners = new Vector();
238     }
239     
240     /***
241      * @return The connection on which this file sits
242      */
243     public StyxConnection getConnection()
244     {
245         return this.conn;
246     }
247     
248     /***
249      * Gets the name of the file, i.e. the last part of the path
250      */
251     public String getName()
252     {
253         return this.name;
254     }
255     
256     /***
257      * Gets the full path of the file
258      */
259     public String getPath()
260     {
261         return this.path;
262     }
263     
264     /***
265      * Gets the full path of the parent of this file
266      */
267     public String getParentPath()
268     {
269         int lastSlash = this.path.lastIndexOf("/");
270         return this.path.substring(0, lastSlash);
271     }
272     
273     /***
274      * @return the full URL to this file (e.g. "styx://localhost:9092/path/to/this")
275      * @todo Include user name in the URL?
276      */
277     public String getURL()
278     {
279         StringBuffer buf = new StringBuffer("styx://");
280         buf.append(this.conn.getRemoteHost());
281         buf.append(":");
282         buf.append(this.conn.getRemotePort());
283         buf.append(this.getPath());
284         return buf.toString();
285     }
286     
287     /***
288      * @return true if this is a directory. This method will block if it is
289      * necessary to send a message to get the stat of this file.
290      */
291     public boolean isDirectory() throws StyxException
292     {
293         if (this.qid == null)
294         {
295             // TODO: we can get the qid with a walk to this location, as long
296             // as it isn't the root directory
297             this.refresh();
298         }
299         return this.qid.isDirectory();
300     }
301     
302     /***
303      * @return true if this is an authorization file.
304      */
305     public boolean isAuth()
306     {
307         // If the qid has not been set then this can't be an auth file: for 
308         // an auth file the qid will have been set by the handshaking mechanism
309         // in StyxConnection (see TauthCallback)
310         return this.qid == null ? false : this.qid.isAuth();
311     }
312     
313     /***
314      * @return the owner of the file
315      */
316     public String getOwner() throws StyxException
317     {
318         if (this.dirEntry == null)
319         {
320             this.refresh();
321         }
322         return this.dirEntry.getOwner();
323     }
324     
325     /***
326      * @return the group of the file
327      */
328     public String getGroup() throws StyxException
329     {
330         if (this.dirEntry == null)
331         {
332             this.refresh();
333         }
334         return this.dirEntry.getGroup();
335     }
336     
337     /***
338      * @return the length of the file in bytes. This method will block if it is
339      * necessary to send a message to get the stat of this file.
340      */
341     public long getLength() throws StyxException
342     {
343         if (this.dirEntry == null)
344         {
345             this.refresh();
346         }
347         return this.dirEntry.getFileLength().asLong();
348     }
349     
350     /***
351      * @return the last modified time of the file. This method will block if it is
352      * necessary to send a message to get the stat of this file.
353      */
354     public Date getLastModified() throws StyxException
355     {
356         if (this.dirEntry == null)
357         {
358             this.refresh();
359         }
360         return new Date(this.dirEntry.getLastModifiedTime() * 1000);
361     }
362     
363     /***
364      * @return the DirEntry object containing this file's attributes. If the
365      * DirEntry has not yet been set, this will return null.  In this case, 
366      * call refresh() on this file.  Alternatively, if all you want is the Qid, 
367      * it is possible that the Qid will be set, even if the DirEntry has not:
368      * try getQid().
369      */
370     public DirEntry getDirEntry()
371     {
372         return this.dirEntry;
373     }
374     
375     /***
376      * Sets the dirEntry of this file.  This method is not used often.  It is most
377      * useful when creating a CStyxFile from reading a directory: in this case
378      * we have the DirEntry without having to separately stat the file.
379      */
380     public void setDirEntry(DirEntry dirEntry)
381     {
382         this.dirEntry = dirEntry;
383         // TODO: nasty duplication of Qid information in DirEntry object and Qid
384         this.qid = this.dirEntry.getQid();
385     }
386     
387     /***
388      * @return true if a fid has been set for this file
389      */
390     public boolean hasFid()
391     {
392         return (this.fid >= 0);
393     }
394     
395     /***
396      * @return the fid of this file
397      */
398     public long getFid()
399     {
400         return this.fid;
401     }
402     
403     /***
404      * Set the mode of this file: this should not normally be called, except
405      * by the OpenCallback, when the file has been open
406      */
407     public void setMode(int newMode)
408     {
409         this.mode = newMode;
410     }
411     
412     /***
413      * @return the mode under which this file is open, or -1 if this file is 
414      * not open.
415      */
416     public int getMode()
417     {
418         return this.mode;
419     }
420     
421     /***
422      * Sets the fid of the file.  This should not normally be called except
423      * by GetFidCallback
424      */
425     public void setFid(long newFid)
426     {
427         this.fid = newFid;
428     }
429     
430     /***
431      * Sets the qid of the file.  This should not normally be called except
432      * by the callbacks in the uk.ac.rdg.resc.jstyx.client.callbacks package
433      */
434     public void setQid(Qid newQid)
435     {
436         this.qid = newQid;
437     }
438     
439     /***
440      * @return the Qid of this file, or null if the qid has not yet been set
441      */
442     public Qid getQid()
443     {
444         return this.qid;
445     }
446     
447     /***
448      * @return true if this file is open (i.e. its mode has been set)
449      */
450     public boolean isOpen()
451     {
452         return (this.mode >= 0);
453     }
454     
455     /***
456      * Returns true if this CStyxFile represents the same file on the server
457      * as the CStyxFile passed in the argument.  This method may block if the
458      * dirEntry of one or both of the files is not set.  Compares the qids of
459      * the two files (see Qid.equals())
460      * @throws StyxException if the dirEntry of one of the files was not set and
461      * there was an error getting the file's stat.  If you know that the dirEntry
462      * of both files is already set, you can safely ignore this exception.
463      */
464     public boolean isSameFile(CStyxFile otherFile) throws StyxException
465     {
466         if (this.getQid() == null)
467         {
468             this.refresh();
469         }
470         if (otherFile.getQid() == null)
471         {
472             otherFile.refresh();
473         }
474         return this.getQid().equals(otherFile.getQid());
475     }
476     
477     /***
478      * Sets the maximum number of bytes that can be read from or written to
479      * this file in a single operation.  This method should not normally be
480      * called directly, except by the OpenCallback.
481      */
482     public void setIoUnit(int newIoUnit)
483     {
484         this.ioUnit = newIoUnit;
485     }
486     
487     /***
488      * @return the maximum number of bytes that can be read from or written to
489      * this file in a single operation. Will only return valid data once the file
490      * has been opened (or created).
491      */
492     public int getIoUnit()
493     {
494         return this.ioUnit;
495     }
496     
497     /***
498      * Sets the children of this file.  This is not normally called directly,
499      * except by the GetChildrenCallback.
500      */
501     public void setChildren(CStyxFile[] children)
502     {
503         this.children = children;
504     }
505     
506     /***
507      * @return the Logger used by this class
508      */
509     public static Logger getLogger()
510     {
511         return log;
512     }
513     
514     /***
515      * Gets a new fid from the pool and walks it to the
516      * location of this file. Does not block; calls the replyArrived() method
517      * of the given callback if the walk was successful, or the error() method
518      * otherwise.
519      */
520     public void walkFidAsync(MessageCallback callback)
521     {
522         new GetFidCallback(this, callback).walkFid();
523     }
524     
525     /***
526      * Gets a new fid from the pool and walks it to the
527      * location of this file. This method blocks until the fid has been walked
528      * to the location of the file
529      * @throws StyxException if there was an error walking the fid (i.e. the 
530      * file does not exist)
531      */
532     public void walkFid() throws StyxException
533     {
534         StyxReplyCallback callback = new StyxReplyCallback();
535         this.walkFidAsync(callback);
536         // We don't need to do anything with the reply
537         callback.getReply();
538     }
539     
540     /***
541      * Tests to see if this file exists on the server, returning true if so
542      * and false if not.  This method may block (it will do so if we have
543      * to get a new fid and walk it to this file location, i.e. if the file
544      * has not been opened before).
545      */
546     public boolean exists()
547     {
548         if (this.hasFid())
549         {
550             return true; // TODO: doesn't check if the file has been deleted since
551                          // we created this fid
552         }
553         else
554         {
555             try
556             {
557                 this.walkFid();
558                 return true;
559             }
560             catch (StyxException ex)
561             {
562                 // the file does not exist
563                 return false;
564             }
565         }
566     }
567     
568     /***
569      * Opens the file on the server, i.e. prepares the file for reading or
570      * writing. This blocks until the open is complete. Use of this method will
571      * not cause the fileOpen() events of registered change listeners to be fired.
572      * @param mode Integer representing the mode - see the constants in StyxUtils.
573      * For example, to open a file for reading, use StyxUtils.OREAD. To open a
574      * file for writing with truncation use StyxUtils.OWRITE | StyxUtils.OTRUNC.
575      */
576     public void open(int mode) throws StyxException
577     {
578         StyxReplyCallback callback = new StyxReplyCallback();
579         this.openAsync(mode, callback);
580         // The properties of the file (mode, iounit, offset) will have been set
581         // automatically by the OpenCallback so we don't need to read the Ropen
582         // message.
583         callback.getReply();
584     }
585     
586     /***
587      * Opens the file on the server. This call does not block; the fileOpen()
588      * method of any registered change listeners will be called when the Ropen message
589      * arrives. The error() method of any registered change listeners will be
590      * called if an error occurs opening the file.
591      * @param mode Integer representing the mode - see the constants in StyxUtils.
592      * For example, to open a file for reading, use StyxUtils.OREAD. To open a
593      * file for writing with truncation use StyxUtils.OWRITE | StyxUtils.OTRUNC.
594      * @throws IllegalStateException if the file is already open
595      */
596     public synchronized void openAsync(int mode)
597     {
598         this.openAsync(mode, null);
599     }
600     
601     /***
602      * Opens the file on the server. This call does not block; the replyArrived()
603      * method of the provided callback object will be called when the Ropen message
604      * arrives. The error() method of the provided callback will be called if
605      * an error occurred when opening the file, or if the file was already open
606      * under a different mode.
607      * @param mode Integer representing the mode - see the constants in StyxUtils.
608      * For example, to open a file for reading, use StyxUtils.OREAD. To open a
609      * file for writing with truncation use StyxUtils.OWRITE | StyxUtils.OTRUNC.
610      * @param callback The MessageCallback object that will handle the Ropen message
611      */
612     public void openAsync(int mode, MessageCallback callback)
613     {
614         new OpenCallback(this, mode, callback).nextStage();
615     }
616     
617     /***
618      * Creates this file on the remote server, provided that its parent directory
619      * exists.  Sends an error message to the callback if the file already exists.
620      */
621     public void createAsync(boolean isDirectory, int permissions, int mode,
622         MessageCallback callback)
623     {
624         new CreateCallback(this, isDirectory, permissions, mode, callback).nextStage();
625     }
626     
627     /***
628      * Opens or creates this file: if the file exists it will be opened with 
629      * the given mode.  If it does not exist it will be created, provided that
630      * the parent directory exists.  Files will be created with 0666 permissions
631      * and directories with 0777, subject to the permissions of the parent 
632      * directory.
633      */
634     public void openOrCreateAsync(boolean isDirectory, int mode,
635         MessageCallback callback)
636     {
637         new OpenOrCreateCallback(this, isDirectory, mode, callback).nextStage();
638     }
639     
640     /***
641      * Opens or creates this file: if the file exists it will be opened with 
642      * the given mode.  If it does not exist it will be created, provided that
643      * the parent directory exists.  Files will be created with 0666 permissions
644      * and directories with 0777, subject to the permissions of the parent 
645      * directory.  Blocks until the opening or creation is complete
646      */
647     public void openOrCreate(boolean isDirectory, int mode) throws StyxException
648     {
649         StyxReplyCallback callback = new StyxReplyCallback();
650         this.openOrCreateAsync(isDirectory, mode, callback);
651         // Blocks until the process is complete
652         callback.getReply();
653     }
654     
655     /***
656      * Closes the file (i.e. clunks the fid). If the fid isn't set, this
657      * will do nothing. This sends the Tclunk message but does not wait for a
658      * reply (this doesn't matter because the rules of Styx say that the fid
659      * of the file is invalid as soon as the Tclunk is sent, whether the Rclunk
660      * arrives or not).  I.e. this method will not block.  The Rclunk message
661      * is handled in the StyxConnection class.
662      */
663     public synchronized void close()
664     {
665         if (this.fid >= 0)
666         {
667             // Send the message to close the file (note that this will not wait
668             // for a reply). We don't need to set a callback; when the reply arrives,
669             // the fid will be returned to the connection's pool by the
670             // StyxConnection class.
671             this.conn.sendAsync(new TclunkMessage(this.fid), null);
672         }
673         // We reset all the properties of this file immediately; we don't need
674         // to wait for the rClunk to arrive. This is because the fid will be
675         // invalid even if the clunk fails - see clunk(5) in the Inferno manual.
676         this.fid = -1;
677         this.ioUnit = 0;
678         this.mode = -1;
679     }
680     
681     /***
682      * Reads a chunk of data from the file. Reads the maximum amount of data
683      * allowed in a single message, starting at the given file offset. Blocks
684      * until the server replies with the data.
685      *
686      * When you have finished with the data in the ByteBuffer that is returned,
687      * call release() on the buffer to ensure that the buffer can be re-used.
688      *
689      * @return a ByteBuffer containing the data that have been read.  The position
690      * and limit of this buffer will be set correctly.  Note that the position
691      * may not necessarily be zero (the buffer may well contain the entire contents
692      * of an RreadMessage, header and all).
693      */
694     public ByteBuffer read(long offset) throws StyxException
695     {
696         StyxReplyCallback callback = new StyxReplyCallback();
697         this.readAsync(offset, callback);
698         RreadMessage rReadMsg = (RreadMessage)callback.getReply();
699         return rReadMsg.getData();
700     }
701     
702     /***
703      * Reads a chunk of data from the file. Reads the maximum amount of data
704      * allowed in a single message, starting at the given file offset.  If the
705      * file was not open before this method is called, the file will be opened
706      * with mode StyxUtils.OREAD (but the fileOpen() event will not be fired).
707      * Returns immediately; the dataArrived() events of registered
708      * CStyxFileChangeListeners will be called when the data arrive, and the
709      * error() events of registered CStyxFileChangeListeners will be called if
710      * an error occurs.
711      */
712     public void readAsync(long offset)
713     {
714         this.readAsync(offset, false);
715     }
716     
717     /***
718      * Reads a chunk of data from the file. Reads the maximum amount of data
719      * allowed in a single message, starting at the given file offset.  If the
720      * file was not open before this method is called, the file will be opened
721      * (but the fileOpen() event will not be fired).
722      * Returns immediately; the dataArrived() events of registered
723      * CStyxFileChangeListeners will be called when the data arrive, and the
724      * error() events of registered CStyxFileChangeListeners will be called if
725      * an error occurs.
726      * @param offset The index of the first byte of data in the file to read
727      * @param openForWriting If this is true, the file will be opened for reading
728      * <em>and</em> writing with truncation, <em>provided that the file is not open already</em>.
729      * If this is false, the file will be opened for reading only
730      */
731     public void readAsync(long offset, boolean openForWriting)
732     {
733         this.readAsync(offset, -1, openForWriting, null);
734     }
735     
736     /***
737      * Reads a chunk of data from the file.  Reads the maximum number of bytes
738      * allowed in a single message, starting at the given file offset. If the
739      * file was not open before this method is called, the file will be opened
740      * for reading only (but the fileOpen() event will not be fired).
741      * Returns immediately; the replyArrived() events of the provided callback
742      * will be called when the data arrive, and the error() method of the
743      * callback will be called if an error occurs.  When the reply arrives,
744      * the data will be contained in a ByteBuffer that is part of the RreadMessage:
745      * use RreadMessage.getData() to get the buffer.  After you have finished with 
746      * the buffer, call release() on the buffer to return it to the pool.
747      */
748     public void readAsync(long offset, MessageCallback callback)
749     {
750         this.readAsync(offset, -1, false, callback);
751     }
752     
753     /***
754      * Reads a chunk of data from the file.  Sends request to read the given
755      * number of bytes, starting at the given file offset. If bytesRequired < 0,
756      * the maximum number of bytes allowed in a single message will be read. If the
757      * file was not open before this method is called, the file will be opened
758      * for reading (but the fileOpen() event will not be fired).
759      * Returns immediately; the replyArrived() events of the provided callback
760      * will be called when the data arrive, and the error() method of the
761      * callback will be called if an error occurs.  When the reply arrives,
762      * the data will be contained in a ByteBuffer that is part of the RreadMessage:
763      * use RreadMessage.getData() to get the buffer.  After you have finished with 
764      * the buffer, call release() on the buffer to return it to the pool.
765      * @param offset The index of the first byte of data in the file to read
766      * @param bytesRequired The maximum number of bytes required
767      * @param callback The class that will be notified if the read was successful,
768      * or if there was an error
769      */
770     public void readAsync(long offset, int bytesRequired, MessageCallback callback)
771     {
772         this.readAsync(offset, bytesRequired, false, callback);
773     }
774     
775     /***
776      * Reads a chunk of data from the file.  Sends request to read the given
777      * number of bytes, starting at the given file offset. If bytesRequired < 0,
778      * the maximum number of bytes allowed in a single message will be read.
779      * Returns immediately; the replyArrived() events of the provided callback
780      * will be called when the data arrive, and the error() method of the
781      * callback will be called if an error occurs.  When the reply arrives,
782      * the data will be contained in a ByteBuffer that is part of the RreadMessage:
783      * use RreadMessage.getData() to get the buffer.  After you have finished with 
784      * the buffer, call release() on the buffer to return it to the pool.
785      * @param offset The index of the first byte of data in the file to read
786      * @param bytesRequired The maximum number of bytes required
787      * @param openForWriting If this is true, the file will be opened for reading
788      * <em>and</em> writing with truncation, <em>provided that the file is not open already</em>.
789      * If this is false, the file will be opened for reading only
790      * @param callback The class that will be notified if the read was successful,
791      * or if there was an error
792      */
793     public void readAsync(long offset, int bytesRequired, boolean openForWriting,
794         MessageCallback callback)
795     {
796         new ReadCallback(this, offset, bytesRequired, openForWriting, callback).nextStage();
797     }
798     
799     /***
800      * Reads the entire contents of the file and returns them as a String.
801      * This should only be used for relatively short files that can sensibly
802      * fit into a String - do not use for large files as not only will this 
803      * method block until the file is read, you may run into memory problems.
804      */
805     public String getContents() throws StyxException
806     {
807         boolean wasOpen = false;
808         if (this.isOpen())
809         {
810             wasOpen = true;
811         }
812         StringBuffer strBuf = new StringBuffer();
813         ByteBuffer buf;
814         byte[] arr = null;
815         long pos = 0;
816         boolean stillReading = true;
817         while(stillReading)
818         {
819             // Read the data from the file
820             buf = this.read(pos);
821             if (buf.hasRemaining())
822             {
823                 // Update the position of the file pointer
824                 pos += buf.remaining();
825                 // Convert the data to a string and append to the buffer
826                 strBuf.append(StyxUtils.dataToString(buf));
827                 // Release the buffer back to the pool
828                 buf.release();
829             }
830             else
831             {
832                 // We have reached EOF
833                 stillReading = false;
834             }
835         }
836         if (!wasOpen)
837         {
838             // If the file wasn't open before we called this function, close it
839             this.close();
840         }
841         return strBuf.toString();        
842     }
843     
844     /***
845      * Sets the contents of the file to the given string. Overwrites anything
846      * else in the file. Writes EOF and closes the file after use unless the file was open
847      * before this method was called.  If the file was open before this message
848      * is called, it must be open for writing with truncation
849      * (i.e. StyxUtils.OWRITE | StyxUtils.OTRUNC)
850      * @param str The new file contents
851      * @throws StyxException if there was an error opening or writing to the file
852      */
853     public void setContents(String str) throws StyxException
854     {
855         boolean wasOpen = true;
856         if (this.mode < 0)
857         {
858             // If the file isn't open, open it for writing with truncation
859             this.open(StyxUtils.OWRITE | StyxUtils.OTRUNC);
860             wasOpen = false;
861         }
862         
863         byte[] bytes = StyxUtils.strToUTF8(str);
864         // the writeAll() method will automatically take care of splitting the
865         // input across multiple Styx messages if necessary. It will also check
866         // that the file is open in the correct mode
867         this.writeAll(bytes, 0);
868         // If this file wasn't open before we called this function, close it
869         if (!wasOpen)
870         {
871             // Write EOF to this file first
872             this.write(new byte[0], bytes.length, true);
873             this.close();
874         }
875     }
876     
877     /***
878      * Writes a block of data to the file at the given offset. Will write the data
879      * in several separate messages if necessary. The file will be truncated at
880      * the end of the new data.
881      * @throws StyxException if there is an error writing to the file
882      */
883     public void writeAll(byte[] bytes, long offset) throws StyxException
884     {
885         // Store the original position of the buffer
886         long filePos = offset;
887         // The position in the byte array of the first byte to write
888         int pos = 0;
889         do
890         {
891             // Calculate the number of bytes still to be written
892             int bytesRemaining = bytes.length - pos;
893             
894             // Calculate the number of bytes that we can write in a single message
895             int bytesToWrite = Math.min(bytesRemaining, this.ioUnit);
896             
897             // Write the bytes to the file with truncation
898             int bytesWritten = (int)this.write(bytes, pos, bytesToWrite, filePos, true);
899             
900             // Update the pointers
901             pos += bytesWritten;
902             filePos += bytesWritten;
903             
904         } while (pos < bytes.length);
905     }
906     
907     /***
908      * Writes a block of data to the file at the given offset. Cannot write more
909      * than this.getIOUnit() bytes in a single message. Blocks until the write
910      * confirmation arrives. Does not change the offset of the file
911      * @param bytes The data to write. Will attempt to write all the data in this array.
912      * @param offset The position in the file at which to write the data
913      * @param truncate True if the file is to be truncated at the end of the
914      * new data
915      * @return The number of bytes written to the file
916      * @throws StyxException if there is an error writing to the file
917      */
918     public long write(byte[] bytes, long offset, boolean truncate) throws StyxException
919     {
920         return this.write(bytes, 0, bytes.length, offset, truncate);
921     }
922     
923     /***
924      * Writes a block of data to the file at the given offset. Cannot write more
925      * than this.getIOUnit() bytes in a single message. Blocks until the write
926      * confirmation arrives. Does not change the offset of the file
927      * @param bytes The data to write.
928      * @param pos The index of the first data point in the byte array to write
929      * @param count The number of bytes from the input array to write
930      * @param offset The position in the file at which to write the data
931      * @param truncate True if the file is to be truncated at the end of the
932      * new data
933      * @return The number of bytes written to the file
934      * @throws StyxException if there is an error writing to the file
935      */
936     public long write(byte[] bytes, int pos, int count, long offset,
937         boolean truncate) throws StyxException
938     {
939         StyxReplyCallback callback = new StyxReplyCallback();
940         this.writeAsync(bytes, pos, count, offset, truncate, callback);
941         RwriteMessage rWriteMsg = (RwriteMessage)callback.getReply();
942         return rWriteMsg.getNumBytesWritten();
943     }
944     
945     /***
946      * Writes a string to the file at the given offset, with truncation. When the write
947      * confirmation arrives, the dataWritten() method of any registered
948      * CStyxFileChangeListeners will be called. The file will be truncated
949      * at the end of the string.
950      */
951     public void writeAsync(String str, long offset)
952     {
953         this.writeAsync(str, offset, null);
954     }
955     
956     /***
957      * Writes a string to the file at the given offset, with truncation. When the write
958      * confirmation arrives, the callback's replyArrived() method will be called.
959      * The callback's error() method will be called if an error occurs. The file
960      * will be truncated at the end of the string.
961      */
962     public void writeAsync(String str, long offset, MessageCallback callback)
963     {
964         this.writeAsync(StyxUtils.strToUTF8(str), offset, true, callback);
965     }
966     
967     /***
968      * Writes a chunk of data to the file at the given file offset.
969      * Returns immediately; the dataWritten() method of any waiting change listeners
970      * will be called when the write confirmation arrives, and the error() method
971      * of any waiting change listeners will be called if an error occurs.
972      * @param bytes The byte array containing the data to write. This will attempt to
973      * write all the data in the array
974      * @param offset The position in the file at which the data will be written
975      * @param truncate True if the file should be truncated at the end of the new data
976      */
977     public void writeAsync(byte[] bytes, long offset, boolean truncate)
978     {
979         this.writeAsync(bytes, offset, truncate, null);
980     }
981     
982     /***
983      * Writes a chunk of data to the file at the given file offset.
984      * Returns immediately; the callback's replyArrived() method will be called
985      * when the reply arrives and the callback's error() method will be called
986      * if an error occurs.
987      * @param bytes The array of bytes to write
988      * @param offset The position in the file at which the data will be written
989      * @param truncate If this is true, the file will be truncated at the end
990      * of the new data
991      * @param callback The replyArrived() method of this callback object will be
992      * called when the write confirmation arrives
993      */
994     public void writeAsync(byte[] bytes, long offset, boolean truncate,
995         MessageCallback callback)
996     {
997         this.writeAsync(bytes, 0, bytes.length, offset, truncate, callback);
998     }
999     
1000     /***
1001      * Writes a chunk of data to the file at the given file offset.
1002      * Returns immediately; the callback's replyArrived() method will be called
1003      * when the reply arrives and the callback's error() method will be called
1004      * if an error occurs.
1005      * @param bytes The array of bytes to write
1006      * @param pos The position in the array of the first byte to write
1007      * @param count The number of bytes from the byte array to write
1008      * @param offset The position in the file at which the data will be written
1009      * @param truncate If this is true, the file will be truncated at the end
1010      * of the new data
1011      * @param callback The replyArrived() method of this callback object will be
1012      * called when the write confirmation arrives
1013      */
1014     public void writeAsync(byte[] bytes, int pos, int count, long offset,
1015         boolean truncate, MessageCallback callback)
1016     {
1017         new WriteCallback(this, bytes, pos, count, offset, truncate, callback).nextStage();
1018     }
1019     
1020     /***
1021      * Refreshes the stat (DirEntry) of the file. Blocks until the operation
1022      * is complete. Does not fire the statChanged() event on any registered
1023      * change listeners.
1024      */
1025     public void refresh() throws StyxException
1026     {
1027         StyxReplyCallback callback = new StyxReplyCallback();
1028         this.refreshAsync(callback);
1029         RstatMessage rStatMsg = (RstatMessage)callback.getReply();
1030         // the dirEntry will already have been set by this stage
1031     }
1032     
1033     /***
1034      * Refreshes the status of the file by sending a TStatMessage. Does not wait
1035      * for a reply; when the reply arrives, the dirEntry of this file will be 
1036      * set and the statChanged() event of any registered change listeners will be
1037      * fired.
1038      * @todo In the case of a directory, should this refresh the list of children?
1039      */
1040     public void refreshAsync()
1041     {
1042         this.refreshAsync(null);
1043     }
1044     
1045     /***
1046      * Refreshes the status of the file by sending a TstatMessage. Does not wait
1047      * for a reply; when the reply arrives, the dirEntry of this file will be 
1048      * set and the replyArrived() method of the provided callback object
1049      * will be called with an RwalkMessage as the argument.
1050      * @todo In the case of a directory, should this refresh the list of children?
1051      * @todo If there is already a Tstat message in flight, what should we do?
1052      * Does it matter much?
1053      */
1054     public void refreshAsync(MessageCallback callback)
1055     {
1056         new RefreshCallback(this, callback).nextStage();
1057     }
1058     
1059     /***
1060      * Gets a reference to a file on the same connection as this CStyxFile.
1061      * This does not open, create or check the existence of the file: no
1062      * messages are sent to the server in this method so this will never block.
1063      * @param path The path of the file to be opened, <i>relative to this file</i>.
1064      * @throws InvalidPathException if the path is not valid
1065      */
1066     public CStyxFile getFile(String path)
1067     {
1068         StringBuffer fullPath = new StringBuffer(this.getPath());
1069         if (!this.getPath().endsWith("/"))
1070         {
1071             fullPath.append("/");
1072         }
1073         fullPath.append(path);
1074         return this.conn.getFile(fullPath.toString());
1075     }
1076     
1077     /***
1078      * Gets a file on the same connection as this CStyxFile, then opens it. This
1079      * blocks until the file is opened.
1080      * @param path The path of the file to be opened, <i>relative to this file</i>.
1081      * @param mode The mode with which to open the file
1082      * @throws StyxException if the file could not be opened with the given mode
1083      */
1084     public CStyxFile openFile(String path, int mode) throws StyxException
1085     {
1086         CStyxFile sf = this.getFile(path);
1087         sf.open(mode);
1088         return sf;
1089     }
1090     
1091     /***
1092      * Gets all the children of this directory. If this is not a directory,
1093      * this will return null (TODO: does it?  Or does it throw an Exception?).
1094      * (For an empty directory, this will return a zero- length array.)
1095      * This method blocks until the directory has been read.
1096      */
1097     public CStyxFile[] getChildren() throws StyxException
1098     {
1099         StyxReplyCallback callback = new StyxReplyCallback();
1100         this.getChildrenAsync(callback);
1101         // Wait for the reply to arrive.  The reply will be the last Rmessage
1102         // in the sequence needed to read the directory contents, i.e. an 
1103         // empty RreadMessage.  This is just a signal that the process is
1104         // complete.
1105         RreadMessage rReadMsg = (RreadMessage)callback.getReply();
1106         // The children of this directory will now have been set
1107         return this.children;
1108     }
1109     
1110     /***
1111      * Reads this directory to get all its children. When the process is completed
1112      * the childrenFound() event is fired on all registered listeners.
1113      */
1114     public void getChildrenAsync()
1115     {
1116         this.getChildrenAsync(null);
1117     }
1118     
1119     /***
1120      * Reads this directory to get all its children. When the process is completed
1121      * the replyArrived() event is fired on the callback, with a zero-length
1122      * RreadMessage as the argument.
1123      */
1124     private void getChildrenAsync(MessageCallback callback)
1125     {
1126         new GetChildrenCallback(this, callback).nextStage();
1127     }
1128     
1129     /***
1130      * Downloads the data from this file and writes to a local java.io.File.
1131      * If this file already exists it will be overwritten.
1132      * This method blocks; it returns when the download is complete and throws
1133      * a StyxException if an error occurs.
1134      * @param file The java.io.File to which the data will be written.  If this
1135      * file already exists it will be overwritten.  If this is null, the data
1136      * will be downloaded but not written to a file
1137      */
1138     public void download(File file) throws StyxException
1139     {
1140         StyxReplyCallback callback = new StyxReplyCallback();
1141         this.downloadAsync(file, callback);
1142         // The getReply() method blocks until the download is complete.
1143         StyxMessage message = callback.getReply();
1144     }
1145     
1146     /***
1147      * Downloads the data from this file and writes to a local java.io.File.
1148      * If this file already exists it will be overwritten.
1149      * This method blocks; it returns when the download is complete and throws
1150      * a StyxException if an error occurs.
1151      * @param file The java.io.File to which the data will be written.  If this
1152      * file already exists it will be overwritten.  If this is null, the data
1153      * will be downloaded but not written to a file
1154      * @param numRequests The number of simultaneous read requests that will be 
1155      * sent.  The download may proceed faster with a larger number of simultaneous
1156      * read requests.  If numRequests > 1, the file on the remote server <b>must</b>
1157      * be seekable otherwise unpredictable behaviour may result.
1158      */
1159     public void download(File file, int numRequests) throws StyxException
1160     {
1161         StyxReplyCallback callback = new StyxReplyCallback();
1162         this.downloadAsync(file, numRequests, callback);
1163         // The getReply() method blocks until the download is complete.
1164         StyxMessage message = callback.getReply();
1165     }
1166     
1167     /***
1168      * Downloads the data from this file and writes to a local java.io.File. 
1169      * This method returns immediately; when the download has finished, the
1170      * downloadComplete() event will be fired all all registered change listeners.
1171      * @param file The java.io.File to which the data will be written.  If this
1172      * file already exists it will be overwritten.
1173      * @param numRequests The number of simultaneous read requests that will be 
1174      * sent.  The download may proceed faster with a larger number of simultaneous
1175      * read requests.  If numRequests > 1, the file on the remote server <b>must</b>
1176      * be seekable otherwise unpredictable behaviour may result.
1177      */
1178     public void downloadAsync(File file, int numRequests)
1179     {
1180         this.downloadAsync(file, numRequests, null);
1181     }
1182     
1183     /***
1184      * Downloads the data from this file and writes to a local java.io.File. 
1185      * This method returns immediately; when the download has finished, the
1186      * downloadComplete() event will be fired all all registered change listeners.
1187      * @param file The java.io.File to which the data will be written.  If this
1188      * file already exists it will be overwritten.
1189      */
1190     public void downloadAsync(File file)
1191     {
1192         this.downloadAsync(file, null);
1193     }
1194     
1195     /***
1196      * Downloads the data from this file and writes to a local java.io.File. 
1197      * This method returns immediately; when the download has finished, the
1198      * replyArrived() method of the given MessageCallback will be called.
1199      * @param file The java.io.File to which the data will be written.  If this
1200      * file already exists it will be overwritten.
1201      */
1202     public void downloadAsync(File file, MessageCallback callback)
1203     {
1204         this.downloadAsync(file, 1, callback);
1205     }
1206     
1207     /***
1208      * Downloads the data from this file and writes to a local java.io.File. 
1209      * This method returns immediately; when the download has finished, the
1210      * replyArrived() method of the given MessageCallback will be called.
1211      * @param file The java.io.File to which the data will be written.  If this
1212      * file already exists it will be overwritten.
1213      * @param numRequests The number of simultaneous read requests that will be 
1214      * sent.  The download may proceed faster with a larger number of simultaneous
1215      * read requests.  If numRequests > 1, the file on the remote server <b>must</b>
1216      * be seekable otherwise unpredictable behaviour may result.
1217      * @throws IllegalArgumentException if numRequests is less than 1 or greater
1218      * than 100.
1219      */
1220     public void downloadAsync(File file, int numRequests, MessageCallback callback)
1221     {
1222         if (numRequests < 1 || numRequests > 100)
1223         {
1224             throw new IllegalArgumentException("numRequests must be between 1 and 100 inclusive");
1225         }
1226         new DownloadCallback(this, file, numRequests, callback).nextStage();
1227     }
1228     
1229     /***
1230      * Uploads data from an InputStream to this file.  If this file does
1231      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1232      * to the permissions of the host directory.  Blocks until the file has been
1233      * uploaded, or throws a StyxException if an error occurred.  After the 
1234      * upload is complete, the InputStream will <b>not</b> be closed.
1235      * @param in The InputStream from which to read data to be written to this file
1236      * @todo Add a flag to prevent overwriting a file if it already exists?
1237      * @todo Allow a callback to be provided for progress monitoring?
1238      */
1239     public void upload(InputStream in) throws StyxException
1240     {
1241         StyxReplyCallback callback = new StyxReplyCallback();
1242         this.uploadAsync(in, callback);
1243         // The getReply() method blocks until the download is complete.
1244         StyxMessage message = callback.getReply();
1245     }
1246     
1247     /***
1248      * Uploads data from an local file to this file.  If this (Styx) file does
1249      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1250      * to the permissions of the host directory.  Blocks until the file has been
1251      * uploaded, or throws a StyxException if an error occurred.
1252      * @param fromFile The File from which to read data to be written to this file
1253      * @todo Add a flag to prevent overwriting a file if it already exists?
1254      * @todo Allow a callback to be provided for progress monitoring?
1255      */
1256     public void upload(File fromFile) throws StyxException
1257     {
1258         StyxReplyCallback callback = new StyxReplyCallback();
1259         this.uploadAsync(fromFile, callback);
1260         // The getReply() method blocks until the download is complete.
1261         StyxMessage message = callback.getReply();
1262     }
1263     
1264     /***
1265      * Uploads data from an InputStream to this file.  If this file does
1266      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1267      * to the permissions of the host directory.  When the process is finished,
1268      * the uploadComplete() event will be fired on all registered
1269      * CStyxFileChangeListeners.  If an error occurs, the error() event will be
1270      * fired on registered change listeners. After the 
1271      * upload is complete, the InputStream will <b>not</b> be closed.
1272      * @param in The InputStream from which to read data to be written to this file
1273      * @todo Add a flag to prevent overwriting a file if it already exists?
1274      * @todo Allow a callback to be provided for progress monitoring?
1275      */
1276     public void uploadAsync(InputStream in)
1277     {
1278         this.uploadAsync(in, null);
1279     }
1280     
1281     /***
1282      * Uploads data from an InputStream to this file.  If this file does
1283      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1284      * to the permissions of the host directory.  When the process is finished,
1285      * the replyArrived() method of the provided callback object will be called.
1286      * If an error occurs, the error() method of the provided callback object
1287      * will be called.
1288      * @param in The InputStream from which to read data to be written to this file
1289      * @param callback The MessageCallback that will be notified when the
1290      * upload process is complete.
1291      * @todo Add a flag to prevent overwriting a file if it already exists?
1292      * @todo Allow a callback to be provided for progress monitoring?
1293      */
1294     public void uploadAsync(InputStream in, MessageCallback callback)
1295     {
1296         new UploadCallback(this, in, callback).nextStage(null, null);
1297     }
1298     
1299     /***
1300      * Uploads data from a local java.io.File to this file.  If this (Styx) file does
1301      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1302      * to the permissions of the host directory.  When the process is finished,
1303      * the uploadComplete() event will be fired on all registered
1304      * CStyxFileChangeListeners.  If an error occurs, the error() event will be
1305      * fired on registered change listeners.
1306      * @param file The File to copy/upload
1307      * @todo Add a flag to prevent overwriting a file if it already exists?
1308      * @todo Allow a callback to be provided for progress monitoring?
1309      */
1310     public void uploadAsync(File file)
1311     {
1312         this.uploadAsync(file, null);
1313     }
1314     
1315     /***
1316      * Uploads data from a local java.io.File to this file.  If this (Styx) file does
1317      * not exist it will be created with rw-rw-rw- (0666) permissions, subject
1318      * to the permissions of the host directory.  When the process is finished,
1319      * the replyArrived() method of the provided callback object will be called.
1320      * If an error occurs, the error() method of the provided callback object
1321      * will be called.
1322      * @param file The File to copy/upload
1323      * @param callback The MessageCallback that will be notified when the
1324      * upload process is complete.
1325      * @todo Add a flag to prevent overwriting a file if it already exists?
1326      * @todo Allow a callback to be provided for progress monitoring?
1327      */
1328     public void uploadAsync(File file, MessageCallback callback)
1329     {
1330         new UploadCallback(this, file, callback).nextStage(null, null);
1331     }
1332     
1333     /***
1334      * Adds a CStyxFileChangeListener to this file. The methods of the change
1335      * listener will be called when events happen, such as new data arriving
1336      * or a change in permissions, etc (note that this notification will only
1337      * happen if the changes are made by calling methods of this CStyxFile
1338      * instance.  If another client makes changes, we will not be notified here).
1339      * If this listener is already registered, this method does nothing.
1340      */
1341     public void addChangeListener(CStyxFileChangeListener listener)
1342     {
1343         synchronized(this.listeners)
1344         {
1345             if (!this.listeners.contains(listener))
1346             {
1347                 this.listeners.add(listener);
1348             }
1349         }
1350     }
1351     
1352     /***
1353      * Removes the given change listener
1354      */
1355     public void removeChangeListener(CStyxFileChangeListener listener)
1356     {
1357         this.listeners.remove(listener);
1358     }
1359     
1360     /***
1361      * Fires the error() method on all registered listeners
1362      */
1363     public void fireError(String message)
1364     {
1365         synchronized(this.listeners)
1366         {
1367             for (int i = 0; i < listeners.size(); i++)
1368             {
1369                 CStyxFileChangeListener listener =
1370                     (CStyxFileChangeListener)this.listeners.get(i);
1371                 listener.error(this, message);
1372             }
1373         }
1374     }
1375     
1376     /***
1377      * Fires the fileOpen() method on all registered listeners
1378      */
1379     public void fireOpen()
1380     {
1381         synchronized(this.listeners)
1382         {
1383             for (int i = 0; i < listeners.size(); i++)
1384             {
1385                 CStyxFileChangeListener listener =
1386                     (CStyxFileChangeListener)this.listeners.get(i);
1387                 listener.fileOpen(this, mode);
1388             }
1389         }
1390     }
1391     
1392     /***
1393      * Fires the fileCreated() method on all registered listeners
1394      */
1395     public void fireCreated()
1396     {
1397         synchronized(this.listeners)
1398         {
1399             for (int i = 0; i < listeners.size(); i++)
1400             {
1401                 CStyxFileChangeListener listener =
1402                     (CStyxFileChangeListener)this.listeners.get(i);
1403                 listener.fileCreated(this, this.mode);
1404             }
1405         }
1406     }
1407     
1408     /***
1409      * Fires the dataArrived() method on all registered listeners
1410      */
1411     public void fireDataArrived(TreadMessage tReadMsg, RreadMessage rReadMsg)
1412     {
1413         synchronized(this.listeners)
1414         {
1415             ByteBuffer data = rReadMsg.getData();
1416             // Remember the position and limit of the buffer
1417             int pos = data.position();
1418             int limit = data.limit();
1419             for (int i = 0; i < listeners.size(); i++)
1420             {
1421                 CStyxFileChangeListener listener =
1422                     (CStyxFileChangeListener)this.listeners.get(i);
1423                 listener.dataArrived(this, tReadMsg, data);
1424                 // Reset the position and limit of the buffer, ready for the next
1425                 // listener
1426                 data.position(pos);
1427                 data.limit(limit);
1428             }
1429             // Release the buffer
1430             data.release();
1431         }
1432     }
1433     
1434     /***
1435      * Fires the statChanged() method on all registered listeners
1436      */
1437     public void fireStatChanged(RstatMessage rStatMsg)
1438     {
1439         this.setDirEntry(rStatMsg.getDirEntry());
1440         // Notify all the listeners that the stat may have changed
1441         synchronized(this.listeners)
1442         {
1443             for (int i = 0; i < listeners.size(); i++)
1444             {
1445                 CStyxFileChangeListener listener =
1446                     (CStyxFileChangeListener)this.listeners.get(i);
1447                 listener.statChanged(this, dirEntry);
1448             }
1449         }
1450     }
1451     
1452     /***
1453      * Fires the dataWritten() method on all registered listeners
1454      */
1455     public void fireDataWritten(TwriteMessage tWriteMsg)
1456     {
1457         // Notify all listeners that the data have been written
1458         synchronized(this.listeners)
1459         {
1460             for (int i = 0; i < listeners.size(); i++)
1461             {
1462                 CStyxFileChangeListener listener =
1463                     (CStyxFileChangeListener)this.listeners.get(i);
1464                 listener.dataWritten(this, tWriteMsg);
1465             }
1466         }
1467     }
1468     
1469     /***
1470      * Fires the childrenFound() method on all registered listeners
1471      */
1472     public void fireChildrenFound()
1473     {
1474         synchronized(this.listeners)
1475         {
1476             for (int i = 0; i < listeners.size(); i++)
1477             {
1478                 CStyxFileChangeListener listener =
1479                     (CStyxFileChangeListener)this.listeners.get(i);
1480                 listener.childrenFound(this, this.children);
1481             }
1482         }
1483     }
1484     
1485     /***
1486      * Fires the uploadComplete() method on all registered listeners
1487      */
1488     public void fireUploadComplete()
1489     {
1490         synchronized(this.listeners)
1491         {
1492             for (int i = 0; i < listeners.size(); i++)
1493             {
1494                 CStyxFileChangeListener listener =
1495                     (CStyxFileChangeListener)this.listeners.get(i);
1496                 listener.uploadComplete(this);
1497             }
1498         }
1499     }
1500     
1501     /***
1502      * Fires the downloadComplete() method on all registered listeners
1503      */
1504     public void fireDownloadComplete()
1505     {
1506         synchronized(this.listeners)
1507         {
1508             for (int i = 0; i < listeners.size(); i++)
1509             {
1510                 CStyxFileChangeListener listener =
1511                     (CStyxFileChangeListener)this.listeners.get(i);
1512                 listener.downloadComplete(this);
1513             }
1514         }
1515     }
1516     
1517     /***
1518      * Gets the canonical version of the given path.  Removes all duplicate
1519      * slashes, collapses all "." and ".." and checks that the path is
1520      * valid and absolute, throwing an InvalidPathException if not.  This canonical
1521      * path is used as the key for the Hashtable of known CStyxFiles.
1522      * @todo Move to StyxUtils?
1523      */
1524     private static String getCanonicalPath(String path) throws InvalidPathException
1525     {
1526         if (!path.startsWith("/"))
1527         {
1528             throw new InvalidPathException("Path must be absolute");
1529         }
1530         Vector canonicalPathEls = new Vector();
1531         String[] pathEls = path.split("/");
1532         for (int i = 0; i < pathEls.length; i++)
1533         {
1534             // Ignore any "." path elements
1535             if (!pathEls[i].equals("."))
1536             {
1537                 // Deal with calls to previous directory
1538                 if (pathEls[i].equals(".."))
1539                 {
1540                     if (canonicalPathEls.size() > 0)
1541                     {
1542                         canonicalPathEls.remove(canonicalPathEls.size() - 1);
1543                     }
1544                 }
1545                 else
1546                 {
1547                     // Check for spaces or backslashes in the filename
1548                     if ((pathEls[i].indexOf(" ") != -1) || 
1549                         (pathEls[i].indexOf("//") != -1))
1550                     {
1551                         throw new InvalidPathException("'" + pathEls[i] + 
1552                             "' is an invalid filename");
1553                     }
1554                     else
1555                     {
1556                         // TODO: check for illegal names that consist entirely of dots
1557                         if (!pathEls[i].equals(""))
1558                         {
1559                             canonicalPathEls.add(pathEls[i]);
1560                         }
1561                     }
1562                 }
1563             }
1564         }
1565         // Reconstruct the path
1566         StringBuffer canonicalPath = new StringBuffer("/");
1567         for (int i = 0; i < canonicalPathEls.size(); i++)
1568         {
1569             canonicalPath.append(canonicalPathEls.get(i));
1570             if (i < canonicalPathEls.size() - 1)
1571             {
1572                 canonicalPath.append("/");
1573             }
1574         }
1575         return canonicalPath.toString();
1576     }
1577     
1578     /***
1579      * @return The name of the file (not the full path).
1580      */
1581     public String toString()
1582     {
1583         return this.getName();
1584     }
1585     
1586     public static void main(String[] args)
1587     {
1588         StyxConnection conn = new StyxConnection("localhost", 9999);
1589         try
1590         {
1591             conn.connect();
1592             CStyxFile file = conn.getFile("hello.txt");
1593             System.out.println(file.getContents());
1594             file.setContents("goodbye from client");
1595         }
1596         catch(StyxException se)
1597         {
1598             se.printStackTrace();
1599         }
1600         finally
1601         {
1602             conn.close();
1603         }
1604     }
1605 }