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 java.io.File;
32  import java.io.RandomAccessFile;
33  import java.io.FileInputStream;
34  import java.io.FileNotFoundException;
35  import java.io.IOException;
36  import java.nio.channels.FileChannel;
37  
38  import org.apache.log4j.Logger;
39  import org.apache.mina.common.ByteBuffer;
40  
41  import uk.ac.rdg.resc.jstyx.StyxException;
42  import uk.ac.rdg.resc.jstyx.types.ULong;
43  
44  /***
45   * Class representing a file on a hard drive. Note that creating a new instance
46   * of this class does not create an actual new file on the hard disk; it simply
47   * creates a wrapper for an existing file.
48   *
49   * This class opens a new Input/OutputStream every time a read/write message
50   * arrives, so it is probably not very efficient (but this was the simplest way
51   * to implement reliably).
52   *
53   * @author Jon Blower
54   * $Revision: 476 $
55   * $Date: 2005-11-09 17:42:24 +0000 (Wed, 09 Nov 2005) $
56   * $Log$
57   * Revision 1.19  2005/11/09 17:42:24  jonblower
58   * Modified EOF recognition (now requires zero-byte message to be written to the end of the file)
59   *
60   * Revision 1.18  2005/11/04 19:34:35  jonblower
61   * Added code to recognise EOF (i.e. zero-byte) write messages
62   *
63   * Revision 1.15  2005/09/08 07:08:59  jonblower
64   * Removed "String user" from list of parameters to StyxFile.write()
65   *
66   * Revision 1.13  2005/05/19 14:47:38  jonblower
67   * Realised that new RandomAccessFile("..", "rw") doesn't throw FileNotFoundException
68   *
69   * Revision 1.12  2005/05/11 10:33:50  jonblower
70   * Implemented MonitoredFileOnDisk.java
71   *
72   * Revision 1.11  2005/05/10 19:19:05  jonblower
73   * Reinstated chan.truncate()
74   *
75   * Revision 1.10  2005/05/10 12:43:57  jonblower
76   * Rewrote and simplified: now closes file after each read or write
77   *
78   * Revision 1.9  2005/05/10 08:02:18  jonblower
79   * Changes related to implementing MonitoredFileOnDisk
80   *
81   * Revision 1.8  2005/05/09 07:13:52  jonblower
82   * Changed getFileOnDisk() to getFileOrDirectoryOnDisk()
83   *
84   * Revision 1.7  2005/04/28 08:11:15  jonblower
85   * Modified permissions handling in documentation directory of SGS
86   *
87   * Revision 1.6  2005/04/27 16:11:43  jonblower
88   * Added capability to add documentation files to SGS namespace
89   *
90   * Revision 1.5  2005/03/24 09:48:31  jonblower
91   * Changed 'count' from long to int throughout for reading and writing
92   *
93   * Revision 1.4  2005/03/19 21:47:02  jonblower
94   * Further fixes relating to releasing ByteBuffers
95   *
96   * Revision 1.3  2005/03/16 17:56:23  jonblower
97   * Replaced use of java.nio.ByteBuffer with MINA's ByteBuffer to minimise copying of buffers
98   *
99   * Revision 1.2  2005/03/11 14:02:16  jonblower
100  * Merged MINA-Test_20059309 into main line of development
101  *
102  * Revision 1.1.1.1.2.1  2005/03/10 20:55:37  jonblower
103  * Removed references to Netty
104  *
105  * Revision 1.1.1.1  2005/02/16 18:58:31  jonblower
106  * Initial import
107  *
108  */
109 public class FileOnDisk extends StyxFile
110 {
111     private static final Logger log = Logger.getLogger(FileOnDisk.class);
112     
113     protected File file;
114     protected boolean mustExist; // If this is true, then an exception will be
115         // thrown by the constructor if the underlying java.io.File does not exist.
116         // Furthermore, if the File is deleted, the FileOnDisk will be removed
117         // from the namespace.  If this is set false, a non-existent File will be
118         // represented as a zero-length FileOnDisk in the namespace.
119     protected boolean eofWritten; // Set true when we have received EOF (i.e. a
120         // write of zero bytes) from the client;
121     
122     /***
123      * Gets a StyxFile that wraps the given java.io.File. If the File is a 
124      * directory, this will return an instance of DirectoryOnDisk, otherwise
125      * it will return an instance of FileOnDisk.
126      * @todo Set file permissions correctly
127      * @throws StyxException if the File does not exist
128      */
129     public static StyxFile getFileOrDirectoryOnDisk(File file) throws StyxException
130     {
131         if (!file.exists())
132         {
133             throw new StyxException("file " + file.getName() + " does not exist");
134         }
135         if (file.isDirectory())
136         {
137             return new DirectoryOnDisk(file);
138         }
139         else
140         {
141             return new FileOnDisk(file);
142         }
143     }
144     
145     /***
146      * Creates a new FileOnDisk that wraps the file at the given path
147      */
148     public FileOnDisk(String filepath) throws StyxException
149     {
150         this(new File(filepath));
151     }
152     
153     /***
154      * Creates a new FileOnDisk whose name is the same as that of (the last part
155      * of) the underlying file (i.e. file.getName())
156      * @param file The java.io.File which this represents
157      * @throws StyxException if the java.io.File does not exist
158      */
159     public FileOnDisk(File file) throws StyxException
160     {
161         this(file.getName(), file);
162     }
163     
164     /***
165      * Creates a FileOnDisk with the default permissions (0666, rw-rw-rw-)
166      * @param name The name of this file as it will appear in the namespace
167      * @param file The java.io.File which this represents
168      * @throws StyxException if the java.io.File does not exist
169      */
170     public FileOnDisk(String name, File file) throws StyxException
171     {
172         this(name, file, 0666, true);
173     }
174     
175     /***
176      * Creates a FileOnDisk with default permissions (0666). The name of the file
177      * in the namespace will be file.getName().
178      * @param file The java.io.File which this represents
179      * @param mustExist If this is true, then an exception will be thrown by this
180      * constructor if the underlying java.io.File does not exist.  Furthermore, 
181      * if the File is deleted, the FileOnDisk will be removed from the namespace.
182      * If this is set false, a non-existent File will be represented as a
183      * zero-length read-only FileOnDisk in the namespace.
184      * @throws StyxException if the java.io.File does not exist and mustExist=true
185      */
186     public FileOnDisk(File file, boolean mustExist)
187         throws StyxException
188     {
189         this(file.getName(), file, 0666, mustExist);
190     }
191     
192     /***
193      * @param name The name of this file as it will appear in the namespace
194      * @param file The java.io.File which this represents
195      * @param permissions the permissions of the file
196      * @param mustExist If this is true, then an exception will be thrown by this
197      * constructor if the underlying java.io.File does not exist.  Furthermore, 
198      * if the File is deleted, the FileOnDisk will be removed from the namespace.
199      * If this is set false, a non-existent File will be represented as a
200      * zero-length read-only FileOnDisk in the namespace.
201      * @throws StyxException if the java.io.File does not exist and mustExist=true
202      */
203     public FileOnDisk(String name, File file, int permissions, boolean mustExist)
204         throws StyxException
205     {
206         super(name.trim(), permissions);
207         if (!file.exists() && mustExist)
208         {
209             throw new StyxException("file " + file.getName() + " does not exist");
210         }
211         this.file = file;
212         this.mustExist = mustExist;
213         this.eofWritten = false;
214     }
215     
216     /***
217      * Reads from the underlying java.io.File.  This method opens a new file
218      * channel, reads the data, then closes the channel again.
219      *
220      * If the java.io.File does not exist and mustExist==true, this method will
221      * throw a StyxException.  If the File does not exist and mustExist==false,
222      * this method will simply return zero bytes to the client.
223      */
224     public synchronized void read(StyxFileClient client, long offset, int count, int tag)
225         throws StyxException
226     {
227         try
228         {
229             // Open a new FileChannel for reading
230             FileChannel chan = new FileInputStream(this.file).getChannel();
231 
232             // Get a ByteBuffer from MINA's pool.  This becomes part of the Rread
233             // message and is automatically released when the message is sent
234             ByteBuffer buf = ByteBuffer.allocate(count);
235             // Make sure the position and limit are set correctly (remember that
236             // the actual buffer size might be larger than requested)
237             buf.position(0).limit(count);
238 
239             // Read from the channel. If no bytes were read (due to EOF), the
240             // position of the buffer will not have changed
241             int numRead = chan.read(buf.buf(), offset);
242             log.debug("Read " + numRead + " bytes from " + this.file.getPath());
243             // Close the channel
244             chan.close();
245 
246             buf.flip();
247             this.replyRead(client, buf, tag);
248         }
249         catch(FileNotFoundException fnfe)
250         {
251             // The file does not exist
252             if (mustExist)
253             {
254                 log.debug("The file " + this.file.getPath() +
255                     " has been removed by another process");
256                 // Remove the file from the Styx server
257                 this.remove();
258                 throw new StyxException("The file " + this.name + " was removed.");
259             }
260             else
261             {
262                 // Simply return EOF
263                 this.replyRead(client, new byte[0], tag);
264             }
265         }
266         catch(IOException ioe)
267         {
268             throw new StyxException("An error of class " + ioe.getClass() + 
269                 " occurred when trying to read from " + this.getFullPath() +
270                 ": " + ioe.getMessage());
271         }
272     }
273     
274     /***
275      * Writes data to the underlying java.io.File.  If the File does not exist
276      * and <code>mustExist</code> is true, this throws a StyxException.  If the File
277      * does not exist and <code>mustExist</code> is false.
278      */
279     public synchronized void write(StyxFileClient client, long offset, 
280         int count, ByteBuffer data, boolean truncate, int tag)
281         throws StyxException
282     {
283         if (this.mustExist && !this.file.exists())
284         {
285             // The file has been removed
286             log.debug("The file " + this.file.getPath() +
287                 " has been removed by another process");
288             // Remove the file from the Styx server
289             this.remove();
290             throw new StyxException("The file " + this.name + " was removed.");
291         }
292         try
293         {
294             int nWritten = 0;
295             // If we're writing zero bytes to the end of the file, this is an
296             // EOF signal
297             if (data.remaining() == 0 && offset == this.file.length())
298             {
299                 log.debug("Got EOF signal");
300                 this.eofWritten = true;
301             }
302             else
303             {
304                 // Open a new FileChannel for writing. Can't use FileOutputStream
305                 // as this doesn't allow successful writing at a certain file offset:
306                 // for some reason everything before this offset gets turned into
307                 // blank spaces.
308                 FileChannel chan = new RandomAccessFile(this.file, "rw").getChannel();
309 
310                 // Remember old limit and position
311                 int pos = data.position();
312                 int lim = data.limit();
313                 // Make sure only the requested number of bytes get written
314                 data.limit(data.position() + count);
315             
316                 // Write to the file
317                 nWritten = chan.write(data.buf(), offset);
318 
319                 // Reset former buffer positions
320                 data.limit(lim).position(pos);
321 
322                 // Truncate the file at the end of the new data if requested
323                 if (truncate)
324                 {
325                     log.debug("Truncating file at " + (offset + nWritten) + " bytes");
326                     chan.truncate(offset + nWritten);
327                 }
328                 // We haven't reached EOF yet
329                 this.eofWritten = false;
330                 // Close the channel
331                 chan.close();
332             }
333             // Reply to the client
334             this.replyWrite(client, nWritten, tag);
335         }
336         catch(IOException ioe)
337         {
338             throw new StyxException("An error of class " + ioe.getClass() + 
339                 " occurred when trying to write to " + this.getFullPath() +
340                 ": " + ioe.getMessage());
341         }
342     }
343     
344     /***
345      * Reads all metadata from underlying disk file
346      */
347     public synchronized void refresh()
348     {
349         // Update the last modified time
350         // This returns zero (i.e. Jan 1 1970) if the file does not exist
351         this.lastModifiedTime = this.file.lastModified() / 1000;
352     }
353     
354     /***
355      * @return the length of the file, or zero if the file does not exist
356      */
357     public ULong getLength()
358     {
359         // This returns zero if the file does not exist
360         return new ULong(this.file.length());
361     }
362     
363     /***
364      * @return true if we have received an EOF message from the client when 
365      * the client uploaded data to this file and no data have been received since
366      * this EOF message was received.
367      */
368     public boolean receivedEOF()
369     {
370         return this.eofWritten;
371     }
372     
373     /***
374      * Closes the open FileChannel and removes the underlying file from the disk
375      */
376     protected synchronized void delete()
377     {
378         if (this.file.exists())
379         {
380             this.file.delete();
381         }
382     }
383     
384 }