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.server;
30  
31  import org.apache.mina.protocol.ProtocolSession;
32  import org.apache.mina.common.ByteBuffer;
33  
34  import java.util.Date;
35  import java.util.Vector;
36  import java.util.Enumeration;
37  
38  import uk.ac.rdg.resc.jstyx.messages.RreadMessage;
39  import uk.ac.rdg.resc.jstyx.messages.RwriteMessage;
40  
41  import uk.ac.rdg.resc.jstyx.StyxException;
42  import uk.ac.rdg.resc.jstyx.StyxUtils;
43  
44  import uk.ac.rdg.resc.jstyx.types.DirEntry;
45  import uk.ac.rdg.resc.jstyx.types.Qid;
46  import uk.ac.rdg.resc.jstyx.types.ULong;
47  
48  /***
49   * Class representing a file (or directory) on a Styx server. There may
50   * be different types of file; a file might map directly to a file on disk, or 
51   * it may be a synthetic file representing a program interface.  This class
52   * creates a StyxFile that does nothing useful, returning errors when reading
53   * from or writing to it.  Subclasses should override the read(), write()
54   * and getLength() methods to implement the desired behaviour.
55   *
56   * Currently each StyxFile has exactly one parent. Therefore symbolic links
57   * on the host filesystem cannot currently be handled.
58   *
59   * @author Jon Blower
60   * $Revision: 602 $
61   * $Date: 2006-03-21 09:06:15 +0000 (Tue, 21 Mar 2006) $
62   * $Log$
63   * Revision 1.25  2006/03/21 09:06:15  jonblower
64   * Still implementing authentication
65   *
66   * Revision 1.24  2006/03/20 17:51:50  jonblower
67   * Adding authentication to base JStyx system
68   *
69   * Revision 1.23  2006/02/17 09:22:32  jonblower
70   * Added rename() method
71   *
72   * Revision 1.22  2006/01/04 16:47:29  jonblower
73   * Reworked getName() and getFullPath()
74   *
75   * Revision 1.21  2005/12/01 08:21:56  jonblower
76   * Fixed javadoc comments
77   *
78   * Revision 1.20  2005/11/04 19:33:41  jonblower
79   * Changed contentsChanged() to fileContentsChanged() in StyxFileChangeListener
80   *
81   * Revision 1.19  2005/11/03 21:50:04  jonblower
82   * Added clarification to comments
83   *
84   * Revision 1.18  2005/09/08 07:08:59  jonblower
85   * Removed "String user" from list of parameters to StyxFile.write()
86   *
87   * Revision 1.17  2005/08/30 16:29:00  jonblower
88   * Added processAndReplyRead() helper functions to StyxFile
89   *
90   * Revision 1.16  2005/07/06 17:42:47  jonblower
91   * Changed getUniqueID() to be based on creation time in addition to file name
92   *
93   * Revision 1.15  2005/05/10 08:02:18  jonblower
94   * Changes related to implementing MonitoredFileOnDisk
95   *
96   * Revision 1.14  2005/05/09 07:12:52  jonblower
97   * Clarified some comments
98   *
99   * Revision 1.13  2005/04/28 08:11:15  jonblower
100  * Modified permissions handling in documentation directory of SGS
101  *
102  * Revision 1.12  2005/04/27 16:11:43  jonblower
103  * Added capability to add documentation files to SGS namespace
104  *
105  * Revision 1.11  2005/03/24 14:47:47  jonblower
106  * Provided default read() and write() methods for StyxFile so it is no longer abstract
107  *
108  * Revision 1.10  2005/03/24 09:48:32  jonblower
109  * Changed 'count' from long to int throughout for reading and writing
110  *
111  * Revision 1.9  2005/03/24 07:57:41  jonblower
112  * Improved code for reading SSL info from SGSconfig file and included parameter
113  * information for the Grid Services in the config file
114  *
115  * Revision 1.8  2005/03/19 21:47:02  jonblower
116  * Further fixes relating to releasing ByteBuffers
117  *
118  * Revision 1.7  2005/03/18 16:45:18  jonblower
119  * Released ByteBuffers after use
120  *
121  * Revision 1.6  2005/03/18 13:56:00  jonblower
122  * Improved freeing of ByteBuffers, and bug fixes
123  *
124  * Revision 1.5  2005/03/16 22:16:43  jonblower
125  * Added Styx Grid Service classes to core module
126  *
127  * Revision 1.4  2005/03/16 17:56:24  jonblower
128  * Replaced use of java.nio.ByteBuffer with MINA's ByteBuffer to minimise copying of buffers
129  *
130  * Revision 1.3  2005/03/11 14:02:16  jonblower
131  * Merged MINA-Test_20059309 into main line of development
132  *
133  * Revision 1.2.2.2  2005/03/10 11:53:54  jonblower
134  * Modified for MINA framework
135  *
136  * Revision 1.2.2.1  2005/03/09 19:44:18  jonblower
137  * Changes concerned with migration to MINA
138  *
139  * Revision 1.2  2005/03/01 13:47:43  jonblower
140  * Changed default user and group to 'user' and 'group'
141  *
142  * Revision 1.1.1.1  2005/02/16 18:58:32  jonblower
143  * Initial import
144  *
145  */
146 public class StyxFile
147 {
148     
149     protected String name;           // The name of the file
150     protected StyxDirectory parent;  // The parent of the file (N.B. the root file
151                                      // has no parent so this will be null)
152     
153     protected boolean directory;     // True if this is a directory
154     private boolean appendOnly;      // True if this is an append-only file
155     private boolean exclusive;       // True if this file can be opened by only one client at a time
156     protected boolean auth;            // True if this is a file to be used by the authentication mechanism
157                                      // (normally false)
158     private int permissions;         // Permissions represented as a number (e.g. 0755 in octal)
159     
160     private long version;            // Version of the file (incremented when it is modified)
161                                      // This can only be modified through the incrementVersion()
162                                      // method
163     
164     private long creationTime;       // Time of creation (milliseconds since the epoch,
165                                      // used to generate a unique ID for the file)
166     private long lastAccessTime;     // last access time (seconds since the epoch)
167     protected long lastModifiedTime; // last modification time (seconds since the epoch)
168     private String owner;            // owner name
169     private String group;            // group name
170     private String lastModifiedBy;   // name of the user who last modified the file
171     
172     private Vector clients;          // The clients who have a connection to this file
173                                      // (i.e. clients who have opened this file)
174     private Vector changeListeners;  // Objects that will get notified when this
175                                      // file changes
176     
177     /***
178      * @todo check that the name is valid (no trailing or leading slashes unless
179      * it is the root directory, no spaces)
180      * @todo according to the Manual, the parent of the root of the tree is itself
181      * @throws StyxException if an attempt is made to create a file with the name
182      * "", "." or ".."
183      */
184     public StyxFile(String name, String owner, String group, int permissions,
185         boolean isAppendOnly, boolean isExclusive)
186         throws StyxException
187     {
188         name = name.trim();
189         if (name.equals("") || name.equals(".") || name.equals(".."))
190         {
191             throw new StyxException("illegal file name");
192         }
193         this.parent = null;
194         this.name = name;
195         this.directory = false;
196         this.auth = false;
197         this.permissions = permissions;
198         this.appendOnly = isAppendOnly;
199         this.exclusive = isExclusive;
200         this.version = 0;
201         this.creationTime = System.currentTimeMillis();
202         this.lastAccessTime = StyxUtils.now();
203         this.lastModifiedTime = StyxUtils.now();
204         this.owner = owner.trim();
205         this.group = group.trim();
206         this.lastModifiedBy = "";
207         this.clients = new Vector();
208         this.changeListeners = new Vector();
209     }
210     
211     /***
212      * Creates a StyxFile with the default username and group
213      */
214     public StyxFile(String name, int permissions, boolean isAppendOnly,
215         boolean isExclusive) throws StyxException
216     {
217         // TODO specify the default user and group in a config file?
218         this(name, "user", "group", permissions, isAppendOnly, isExclusive);
219     }
220     
221     public StyxFile(String name, int permissions)
222         throws StyxException
223     {
224         this(name, permissions, false, false);
225     }
226     
227     /***
228      * Creates a StyxFile with the default permissions (0666, rw-rw-rw-)
229      */
230     public StyxFile(String name) throws StyxException
231     {
232         this(name, 0666);
233     }
234     
235     /***
236      * @return the name of the file, or the empty string if this is the root
237      * directory
238      */
239     public String getName()
240     {
241         return this.name;
242     }
243     
244     /***
245      * @throws StyxException if the name of this file cannot be changed
246      */
247     public void checkSetName(String newName) throws StyxException
248     {
249         return;
250     }
251     
252     /***
253      * Changes the name of the file. Must have checked for correct permissions
254      * before doing this. Also must call checkSetName() to check if this method
255      * will succeed.
256      */
257     public void setName(String name)
258     {
259         this.name = name.trim();
260     }
261     
262     /***
263      * @return true if this StyxFile is a directory (should always return the
264      * same result as "instanceof StyxDirectory")
265      */
266     public boolean isDirectory()
267     {
268         return this.directory;
269     }
270     
271     public boolean isAppendOnly()
272     {
273         return this.appendOnly;
274     }
275     
276     /***
277      * @return true if this file is marked for exclusive use
278      */
279     public boolean isExclusive()
280     {
281         return this.exclusive;
282     }
283     
284     public boolean isAuth()
285     {
286         return this.auth;
287     }
288     
289     /***
290      * Gets the full path relative to the root of this file system.
291      */
292     public String getFullPath()
293     {
294         if (this.auth)
295         {
296             // Auth files don't have a parent
297             return this.getName();
298         }
299         else
300         {
301             return this.parent.getFullPath() + this.getName();
302         }
303     }
304     
305     /***
306      * @return the length of the file. This default implementation returns
307      * zero; subclasses should override this method
308      */
309     public ULong getLength()
310     {
311         return ULong.ZERO;
312     }
313     
314     /***
315      * Returns the parent of this file. The parent of the root directory is
316      * the root itself (according to the Inferno manual)
317      */
318     public StyxDirectory getParent()
319     {
320         return this.parent;
321     }
322     
323     public String getOwner()
324     {
325         return this.owner;
326     }
327     
328     public String getGroup()
329     {
330         return this.group;
331     }
332     
333     public synchronized DirEntry getDirEntry()
334     {
335         return new DirEntry(this.getQid(), this.getMode(), this.lastAccessTime,
336             this.lastModifiedTime, this.getLength(), this.name, this.owner,
337             this.group, this.lastModifiedBy);
338     }
339     
340     public synchronized Qid getQid()
341     {
342         // For the Qid, we only need the high byte of the type
343         int type = (int)(this.getType() >> 24);
344         return new Qid(type, this.version, this.getUniqueID());
345     }
346     
347     /***
348      * Gets the type of the file as a number representing the OR of DMDIR,
349      * DMAPPEND, DMEXCL and DMAUTH as appropriate, used to create the Qid
350      */
351     private long getType()
352     {
353         long type = 0;
354         if (this.directory)
355         {
356             type |= StyxUtils.DMDIR;
357         }
358         if (this.appendOnly)
359         {
360             type |= StyxUtils.DMAPPEND;
361         }
362         if (this.exclusive)
363         {
364             type |= StyxUtils.DMEXCL;
365         }
366         if (this.auth)
367         {
368             type |= StyxUtils.DMAUTH;
369         }
370         return type;
371     }
372     
373     /***
374      * @return the permissions of the file as an integer (e.g. 0755). Does not
375      * include the DMDIR, DMAPPEND, DMEXCL, DMAUTH flags (check these with the
376      * accessor methods isDirectory(), isAppendOnly(), isExclusive(), isAuth()
377      */
378     public int getPermissions()
379     {
380         return this.permissions;
381     }
382     
383     /***
384      * Sets the permissions of the file
385      * @param permissions the permissions of the file as an integer (e.g. 0755).
386      */
387     public void setPermissions(int permissions)
388     {
389         this.permissions = permissions;
390     }
391     
392     /***
393      * Makes the file read-only (e.g. rwxrwxr-x gets turned to r-xrr-xr-x).
394      */
395     public void setReadOnly()
396     {
397         this.permissions |= 0555;
398     }
399     
400     /***
401      * Checks to see if this file allows the mode (permissions plus flags) of
402      * the file to be changed. This is called when the server receives a Twstat
403      * message. This default implementation does nothing.
404      * @param newMode the new mode of the file (permissions plus any other flags
405      * such as DMDIR, DMAPPEND, DMEXCL, DMAUTH)
406      * @throws StyxException if the mode of this file cannot be changed
407      */
408     public void checkSetMode(long newMode) throws StyxException
409     {
410         return;
411     }
412     
413     /***
414      * Sets the mode of the file (permissions plus other flags). Must check all 
415      * relevant permissions and call checkSetMode() before calling this method
416      * as the system will assume that this method will always succeed.
417      */
418     public void setMode(long newMode)
419     {
420         this.appendOnly = ((newMode & StyxUtils.DMAPPEND) == StyxUtils.DMAPPEND);
421         this.exclusive = ((newMode & StyxUtils.DMEXCL) == StyxUtils.DMEXCL);
422         this.auth = ((newMode & StyxUtils.DMAUTH) == StyxUtils.DMAUTH);
423         this.permissions = (int)(newMode & 1023);
424     }
425     
426     /***
427      * Check to see if the length of this file can be changed to the given
428      * value. If this does not throw an exception then the setLength() method
429      * will be assumed to be guaranteed to succeed. Subclasses should make sure
430      * that this method throws an exception if this StyxFile is a directory.
431      * This default implementation always throws an exception; subclasses should
432      * override this method if they wish to allow the length of this file to
433      * be changed.
434      */
435     public void checkSetLength(ULong newLength) throws StyxException
436     {
437         throw new StyxException("cannot change the length of this file directly");
438     }
439     
440     /***
441      * Sets the length of the file. Should check for all relevant
442      * permissions and call checkSetLength() before calling this. The
443      * system will assume that this method is guaranteed to succeed (it throws
444      * no exceptions and returns no value). This default implementation does
445      * nothing; subclasses should override it.
446      */
447     public void setLength(ULong newLength)
448     {
449         return;
450     }
451     
452     /***
453      * @throws StyxException if the last modified time cannot be changed directly
454      * (i.e. with a Twstat message)
455      */
456     public void checkSetLastModifiedTime(long lastModifiedTime) throws StyxException
457     {
458         return;
459     }
460     
461     /***
462      * Sets the last modified time of the file. Should check for all relevant
463      * permissions and, for a Twstat message,  call checkSetLastModifiedTime()
464      * before calling this. The system will assume that this method is guaranteed
465      * to succeed (it throws no exceptions and returns no value).
466      * This method also always sets the last access time to the same value.
467      * @param lastModifiedTime time as represented in a stat entry (e.g. in a 
468      * TwstatMessage), i.e. the number of seconds (not milliseconds) since the
469      * epoch (Jan 1 00:00 1970 GMT)
470      * @param user The user who is modifying the file
471      */
472     public void setLastModified(long lastModifiedTime, User user)
473     {
474         this.lastModifiedTime = lastModifiedTime;
475         this.setLastAccessTime(lastModifiedTime);
476         this.lastModifiedBy = user.getUsername();
477     }
478     
479     public void setLastAccessTime(long lastAccessTime)
480     {
481         this.lastAccessTime = lastAccessTime;
482     }
483     
484     /***
485      * Renames this file to the given name.  If a file with the given name already
486      * exists in the parent directory this method will throw a StyxException.
487      * Also throws a StyxException if this is the root directory.
488      */
489     public void rename(String newName) throws StyxException
490     {
491         if (this.getParent() == null && this.getParent() == this)
492         {
493             throw new StyxException("Cannot change the name of the root directory");
494         }
495         else
496         {
497             // This has a valid parent that is not itself (so this isn't the root
498             // directory)
499             if (this.getParent().childExists(newName))
500             {
501                 throw new StyxException("A file with name " + newName + 
502                     " already exists in this directory");
503             }
504             else
505             {
506                 this.name = newName;
507             }
508         }
509     }
510     
511     /***
512      * Gets the unique numeric ID for the path of this file (generated from the
513      * low-order bytes of the creation time and the hashcode of the full path).
514      * If the file is deleted and re-created the unique ID will change (except
515      * for the extremely unlikely case in which the low-order bytes of the creation
516      * time happen to be the same in the new file and the old file).
517      */
518     private long getUniqueID()
519     {
520         // Get the low-order bytes of the creation time
521         long timeBytes = this.creationTime & 0xffffffffL;
522         // Create the ID from the low-order bytes of the creation time and use
523         // the hashcode of the path as the high-order bytes of the ID
524         return (this.getFullPath().hashCode() << 32) | timeBytes;
525     }
526     
527     /***
528      * Gets the mode of this file (permissions and flags)
529      */
530     private long getMode()
531     {
532         return this.getType() | this.permissions;
533     }
534     
535     /***
536      * Reads data from this file. This method could be synchronized in subclasses,
537      * but watch out for blocks if the read is expected to take some time to
538      * complete. Subclasses must make sure they reply to the read request by
539      * creating a java.nio.ByteBuffer or byte array of data, then calling the 
540      * appropriate readReply() (this can be done at any time; it does not have 
541      * to be done within the read() method).
542      *
543      * This default implementation simply throws a StyxException, which will 
544      * result in an Rerror message being sent back to the client. Subclasses 
545      * should override this to provide the desired behaviour when the file is
546      * read.
547      *
548      * @param client The client that is performing the read
549      * @param offset The point in the file at which to start reading
550      * @param count The maximum number of bytes to read
551      * @param tag The tag of the incoming Tread message (this is needed when
552      * calling readReply())
553      */
554     public void read(StyxFileClient client, long offset, int count, int tag)
555         throws StyxException
556     {
557         throw new StyxException("Cannot read from this file");
558     }
559     
560     /***
561      * Writes data to this file. Must check that the file is open for writing
562      * before this. We have already dealt with the possibility that this is an
563      * append-only file before calling this method so subclasses do not need to
564      * check this. Subclasses must make sure they reply to the write request by
565      * calling writeReply() (although this can be done at any time; it does not
566      * have to be done within the write() method).
567      *
568      * After this method is called, ByteBuffer containing the data will be 
569      * returned to the pool. If subclasses wish to keep the ByteBuffer after
570      * this method is complete, they should call data.acquire() to increase
571      * the reference count to the buffer.
572      *
573      * This default implementation simply throws a StyxException, which will 
574      * result in an Rerror message being sent back to the client. Subclasses 
575      * should override this to provide the desired behaviour when the file is
576      * written to.
577      *
578      * @param client The client that is performing the write operation
579      * @param offset The place in the file where the new data will be added
580      * @param count The number of bytes to write
581      * @param data The data to write. The position and limit of this ByteBuffer
582      * will be set correctly, but subclasses should note that the position might
583      * not be zero.
584      * @param truncate If this is true the file will be truncated at the end of 
585      * the new data
586      * @param tag The tag of the incoming Twrite message (this is needed when
587      * calling writeReply())
588      */
589     public void write(StyxFileClient client, long offset, int count,
590         ByteBuffer data, boolean truncate, int tag)
591         throws StyxException
592     {
593         throw new StyxException("Cannot write to this file");
594     }
595     
596     /***
597      * Refreshes this file (if it represents another entity, such as a file on disk,
598      * this method is used to make sure that the file metadata (length, access
599      * time etc) are up to date. This default implementation does nothing; 
600      * subclasses must override this to provide the correct functionality
601      */
602     public synchronized void refresh()
603     {
604         return;
605     }
606     
607     /***
608      * Removes this file from the Styx server
609      */
610     public synchronized void remove() throws StyxException
611     {
612         // TODO: abort outstanding i/o (search for outstanding tags)
613         this.delete();
614         // TODO: what if we're trying to remove the root directory?
615         this.getParent().removeChild(this);
616         return;
617     }    
618     
619     /***
620      * Called when the file is removed from the server. Aborts all outstanding
621      * i/o and frees any resources associated with the file. This default
622      * implementation does nothing; subclasses should implement appropriate
623      * methods if necessary.
624      */
625     protected synchronized void delete()
626     {
627         return;
628     }
629     
630     /***
631      * Adds the given client to the file's list of connected clients.
632      * Records the mode with which the client has the file open. Fires the
633      * clientConnected() event
634      */
635     public void addClient(StyxFileClient client)
636     {
637         this.clients.add(client);
638         this.clientConnected(client);
639     }
640     
641     /***
642      * Called after a client connects to this file (i.e. opens it).
643      * This default implementation does nothing, but subclasses might want to
644      * catch this event and do something, e.g. open file handles when the first
645      * client has connected (i.e. when getNumClients() == 0)
646      */
647     protected void clientConnected(StyxFileClient client)
648     {
649         return;
650     }
651     
652     /***
653      * Gets the StyxFileClient associated with the given Session and fid, or null
654      * if client does not exist
655      */
656     public StyxFileClient getClient(ProtocolSession session, long fid)
657     {
658         synchronized (this.clients)
659         {
660             for (int i = 0; i < this.clients.size(); i++)
661             {
662                 StyxFileClient client = (StyxFileClient)this.clients.get(i);
663                 if ( (client.getSession() == session) && (client.getFid() == fid) )
664                 {
665                     return client;
666                 }
667             }
668         }
669         return null;
670     }
671     
672     /***
673      * @return The number of clients that have this file open (rememember that
674      * several open handles to this file might exist on the same connection.
675      * This essentially counts the number of unique client/fid pairs)
676      */
677     public int getNumClients()
678     {
679         synchronized(this.clients)
680         {
681             this.removeDeadClients();
682             return this.clients.size();
683         }
684     }
685     
686     /***
687      * Checks for clients that have disconnected or been lost and removes them.
688      * This may not be necessary if we can trust the architecture to reliably
689      * determine when a client has disconnected, in which case this method
690      * just adds unnecessary overhead.
691      * @todo do logging to find out whether this method is ever necessary?
692      */
693     private void removeDeadClients()
694     {
695         synchronized(this.clients)
696         {
697             // Get the list of keys; the keys are the Session objects
698             for (int i = 0; i < this.clients.size(); i++)
699             {
700                 StyxFileClient client = (StyxFileClient)this.clients.get(i);
701                 ProtocolSession session = client.getSession();
702                 if (session == null || !session.isConnected())
703                 {
704                     this.removeClient(client);
705                 }
706             }
707         }
708     }
709     
710     /***
711      * Removes the client that is connected on the given session. Fires the
712      * clientDisconnected event. If client is null, this will do nothing
713      */
714     public void removeClient(StyxFileClient client)
715     {
716         if (client != null)
717         {
718             this.clients.remove(client);
719             this.clientDisconnected(client);
720         }
721     }
722     
723     /***
724      * Called after a client disconnects from this file (i.e. clunks the fid).
725      * This default implementation does nothing, but subclasses might want to
726      * catch this event and do something, e.g. close file handles when the last
727      * client has disconnected (i.e. when getNumClients() == 0).
728      * @param client The client that has just disconnected from the file. This
729      * will not be null.
730      */
731     protected void clientDisconnected(StyxFileClient client)
732     {
733         return;
734     }
735     
736     /***
737      * Method for processing a read request and replying appropriately to the
738      * client, based on the contents of the file.  This method is used
739      * for StyxFiles whose contents can be represented as a String (i.e. fairly
740      * short files).
741      * @param fileContents String representing the <b>entire contents</b> of 
742      * the file.
743      * @param client the StyxFileClient making the request
744      * @param offset the index of the first byte in the file to return to the client
745      * @param count the maximum number of bytes to return to the client
746      * @param tag the tag of the incoming read message
747      */
748     public void processAndReplyRead(String fileContents, StyxFileClient client,
749         long offset, int count, int tag)
750     {
751         // Convert the string to bytes using the UTF-8 character set
752         byte[] bytes = StyxUtils.strToUTF8(fileContents);
753         this.processAndReplyRead(bytes, client, offset, count, tag);
754     }
755     
756     /***
757      * Method for processing a read request and replying appropriately to the
758      * client, based on the contents of the file.  This method is used
759      * for StyxFiles whose contents can be represented as a byte array.
760      * @param fileContents Byte array representing the <b>entire contents</b> of 
761      * the file.
762      * @param client the StyxFileClient making the request
763      * @param offset the index of the first byte in the file to return to the client
764      * @param count the maximum number of bytes to return to the client
765      * @param tag the tag of the incoming read message
766      */
767     public void processAndReplyRead(byte[] fileContents, StyxFileClient client,
768         long offset, int count, int tag)
769     {
770         // Check to see if the offset is beyond the end of the file
771         if (offset >= fileContents.length)
772         {
773             // The client has reached end-of-file.  Return no bytes.
774             this.replyRead(client, new byte[0], tag);
775         }
776         else
777         {
778             // Calculate the number of bytes to return to the client 
779             int numBytesToReturn = Math.min(fileContents.length - (int)offset, count);
780             // Now reply to the client
781             this.replyRead(client, fileContents, (int)offset, numBytesToReturn, tag);
782         }
783     }
784     
785     /***
786      * Method for processing a read request and replying appropriately to the
787      * client, based on the contents of the file.  This method is used
788      * for StyxFiles whose contents can be represented as a ByteBuffer.
789      * @param fileContents ByteBuffer representing the <b>entire contents</b> of 
790      * the file. The position and limit of this buffer will be unchanged by this
791      * method.  This can be null: in this case all read requests will return zero bytes.
792      * @param client the StyxFileClient making the request
793      * @param offset the index of the first byte in the file to return to the client
794      * @param count the maximum number of bytes to return to the client
795      * @param tag the tag of the incoming read message
796      */
797     public void processAndReplyRead(ByteBuffer fileContents, StyxFileClient client,
798         long offset, int count, int tag)
799     {
800         if (fileContents == null || offset >= fileContents.limit())
801         {
802             // Attempt to read off the end of the file, or no data have yet
803             // been written to the file
804             this.replyRead(client, new byte[0], tag);
805         }
806         else
807         {
808             int numBytesToReturn = Math.min(fileContents.limit() - (int)offset, count);
809             // Remember the position and limit of the buffer
810             int oldPos = fileContents.position();
811             int oldLimit = fileContents.limit();
812             // Set the position and limit of the buffer so that the correct bytes
813             // get returned
814             fileContents.position((int)offset);
815             fileContents.limit((int)offset + numBytesToReturn);
816             this.replyRead(client, fileContents, tag);
817             // Reset the buffer position and limit
818             fileContents.position(oldPos);
819             fileContents.limit(oldLimit);
820         }
821     }
822     
823     /***
824      * Method to reply to a Read message. One of the replyRead() methods
825      * must be called by all subclasses when sending data back to the client in
826      * response to a read request.
827      * @param client The connection on which the reply will be sent
828      * @param bytes The data to include in the message. All the data in this 
829      * array will be written
830      * @param tag The tag to be attached to the message
831      */
832     protected void replyRead(StyxFileClient client, byte[] bytes, int tag)
833     {
834         this.replyRead(client, bytes, 0, bytes.length, tag);
835     }
836     
837     /***
838      * Method to reply to a Read message. One of the replyRead() methods
839      * must be called by all subclasses when sending data back to the client in
840      * response to a read request. Leaves the position of the input ByteBuffer
841      * unchanged.
842      * @param client The connection on which the reply will be sent
843      * @param buf a java.nio.ByteBuffer containing the data to write to the file.
844      * All the remaining data in the buffer will be sent back to the client.
845      * @param tag The tag to be attached to the message
846      */
847     protected void replyRead(StyxFileClient client, java.nio.ByteBuffer buf, int tag)
848     {
849         byte[] bytes;
850         if (buf.hasArray())
851         {
852             // We can just use the backing array for this buffer
853             bytes = buf.array();
854             // Write the right number of bytes from the right position in the array
855             this.replyRead(client, bytes, buf.position(), buf.remaining(), tag);
856         }
857         else
858         {
859             // We must copy the data from the array
860             int oldPos = buf.position();
861             bytes = new byte[buf.remaining()];
862             buf.get(bytes);
863             buf.position(oldPos);
864             this.replyRead(client, bytes, tag);
865         }
866     }
867     
868     /***
869      * Method to reply to a Read message. One of the replyRead() methods
870      * must be called by all subclasses when sending data back to the client in
871      * response to a read request. Leaves the position of the input ByteBuffer
872      * unchanged.  The buffer that is provided to this method will be released
873      * automatically so users of this method should not release the buffer
874      * themselves.
875      * @param client The connection on which the reply will be sent
876      * @param buf a org.apache.mina.common.ByteBuffer containing the data to
877      * write to the file.  All the remaining data in the buffer will be sent
878      * back to the client.
879      * @param tag The tag to be attached to the message
880      */
881     protected void replyRead(StyxFileClient client, ByteBuffer buf, int tag)
882     {
883         RreadMessage rReadMsg = new RreadMessage(buf);
884         rReadMsg.setTag(tag);
885         this.replyRead(client, rReadMsg);
886     }
887     
888     /***
889      * Method to reply to a Read message. One of the replyRead() methods
890      * must be called by all subclasses when sending data back to the client in
891      * response to a read request.
892      * @param client The connection on which the reply will be sent
893      * @param bytes The data to include in the message.
894      * @param pos The index of the first byte in the array to be written
895      * @param count The number of bytes in the array to write
896      * @param tag The tag to be attached to the message
897      */
898     protected void replyRead(StyxFileClient client, byte[] bytes, int pos,
899         int count, int tag)
900     {
901         RreadMessage rReadMsg = new RreadMessage(bytes, pos, count);
902         rReadMsg.setTag(tag);
903         this.replyRead(client, rReadMsg);
904     }
905     
906     /***
907      * Method to reply to a Read message. One of the replyRead() methods
908      * must be called by all subclasses when sending data back to the client in
909      * response to a read request.
910      * @param client The connection on which the reply will be sent
911      * @param rReadMsg The RreadMessage to send back to the client. The tag of
912      * this message must be set correctly.
913      */
914     protected void replyRead(StyxFileClient client, RreadMessage rReadMsg)
915     {
916         ProtocolSession session = client.getSession();
917         StyxSessionState sessionState = (StyxSessionState)session.getAttachment();
918         synchronized (sessionState)
919         {
920             int tag = rReadMsg.getTag();
921             // If the tag has been flushed, don't reply
922             if (sessionState.tagInUse(tag))
923             {
924                 this.setLastAccessTime(StyxUtils.now());
925                 session.write(rReadMsg);
926                 sessionState.releaseTag(tag);
927             }
928         }
929     }
930     
931     /***
932      * Method to reply to a Write message. This must be called by all subclasses
933      * when sending data back to the client in response to a write request.
934      * @param client The connection on which the reply will be sent
935      * @param count The number of bytes actually written to the file in question.
936      * @param tag The tag to be attached to the message
937      */
938     protected void replyWrite(StyxFileClient client, int count, int tag)
939     {
940         ProtocolSession session = client.getSession();
941         StyxSessionState sessionState = (StyxSessionState)session.getAttachment();
942         synchronized (sessionState)
943         {
944             // If the tag has been flushed, don't reply
945             if (sessionState.tagInUse(tag))
946             {
947                 this.setLastModified(StyxUtils.now(), sessionState.getUser());
948                 this.contentsChanged();
949                 RwriteMessage rWriteMsg = new RwriteMessage(count);
950                 rWriteMsg.setTag(tag);
951                 session.write(rWriteMsg);
952                 sessionState.releaseTag(tag);
953             }
954         }
955     }
956     
957     public long getVersion()
958     {
959         return this.version;
960     }
961     
962     /***
963      * Call this to indicate that the file's data have changed. This is called
964      * automatically when the file is written to with a TwriteMessage. This
965      * default implementation simply increments the version number with a call
966      * to this.incrementVersion(). Subclasses may override this method, for
967      * example to notify waiting clients that the data have changed. This will
968      * fire the contentsChanged() event on any registered StyxFileChangeListeners.
969      */
970     public void contentsChanged()
971     {
972         this.incrementVersion();
973         this.fireContentsChanged();
974     }
975     
976     /***
977      * Increments the file's version number and ensures that the version number
978      * never exceeds the maximum value of an unsigned 4-byte integer (it wraps
979      * back to zero if the version number gets this large)
980      */
981     protected final void incrementVersion()
982     {
983         this.version++;
984         if (this.version > StyxUtils.MAXUINT)
985         {
986             this.version = 0;
987         }        
988     }
989     
990     /***
991      * Registers a StyxFileChangeListener.  If it is already registered, this
992      * method does nothing.
993      */
994     public void addChangeListener(StyxFileChangeListener listener)
995     {
996         synchronized(this.changeListeners)
997         {
998             if (!this.changeListeners.contains(listener))
999             {
1000                 this.changeListeners.add(listener);
1001             }
1002         }
1003     }
1004     
1005     /***
1006      * Removes a change listener.  If the given change listener is not registered,
1007      * this method does nothing.
1008      */
1009     public void removeChangeListener(StyxFileChangeListener listener)
1010     {
1011         synchronized(this.changeListeners)
1012         {
1013             this.changeListeners.remove(listener);
1014         }
1015     }
1016     
1017     /***
1018      * This method is called when the contents of the file change. This can be 
1019      * due to two things: (1) When a client writes to the file using a
1020      * TwriteMessage. In this case this method is called just before the Rwrite
1021      * message is sent to the client. (2) It is also called when
1022      * this.contentsChanged() is called.
1023      */
1024     protected void fireContentsChanged()
1025     {
1026         synchronized(this.changeListeners)
1027         {
1028             StyxFileChangeListener listener;
1029             for (int i = 0; i < this.changeListeners.size(); i++)
1030             {
1031                 listener = (StyxFileChangeListener)this.changeListeners.get(i);
1032                 listener.fileContentsChanged();
1033             }
1034         }
1035     }
1036 }