View Javadoc

1   /* 
2    * Copyright 2002-2004 The Apache Software Foundation
3    * Licensed  under the  Apache License,  Version 2.0  (the "License");
4    * you may not use  this file  except in  compliance with the License.
5    * You may obtain a copy of the License at 
6    * 
7    *   http://www.apache.org/licenses/LICENSE-2.0
8    * 
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed  under the  License is distributed on an "AS IS" BASIS,
11   * WITHOUT  WARRANTIES OR CONDITIONS  OF ANY KIND, either  express  or
12   * implied.
13   * 
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.struts.flow.core.source.impl;
18  
19  import java.io.File;
20  import java.io.FileInputStream;
21  import java.io.FileNotFoundException;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.net.MalformedURLException;
27  import java.net.URLConnection;
28  import java.util.Arrays;
29  import java.util.Collection;
30  import java.util.ConcurrentModificationException;
31  
32  import org.apache.struts.flow.core.source.ModifiableSource;
33  import org.apache.struts.flow.core.source.ModifiableTraversableSource;
34  import org.apache.struts.flow.core.source.MoveableSource;
35  import org.apache.struts.flow.core.source.Source;
36  import org.apache.struts.flow.core.source.SourceException;
37  import org.apache.struts.flow.core.source.SourceNotFoundException;
38  import org.apache.struts.flow.core.source.SourceUtil;
39  import org.apache.struts.flow.core.source.SourceValidity;
40  import org.apache.struts.flow.core.source.impl.validity.FileTimeStampValidity;
41  
42  /***
43   * A {@link ModifiableTraversableSource} for filesystem objects.
44   *
45   * @author <a href="mailto:dev@avalon.apache.org">Avalon Development Team</a>
46   * @version $Id: FileSource.java,v 1.5 2004/02/28 11:47:24 cziegeler Exp $
47   */
48  
49  public class FileSource implements ModifiableTraversableSource, MoveableSource
50  {
51  
52      /*** The file */
53      private File m_file;
54  
55      /*** The scheme */
56      private String m_scheme;
57  
58      /*** The URI of this source */
59      private String m_uri;
60  
61      /***
62       * Builds a FileSource given an URI, which doesn't necessarily have to start with "file:"
63       * @param uri
64       * @throws SourceException
65       * @throws MalformedURLException
66       */
67      public FileSource(String uri) throws SourceException, MalformedURLException
68      {
69          int pos = SourceUtil.indexOfSchemeColon(uri);
70          if (pos == -1)
71          {
72              throw new MalformedURLException("Invalid URI : " + uri);
73          }
74  
75          String scheme = uri.substring(0, pos);
76          String fileName = uri.substring(pos + 1);
77          fileName = SourceUtil.decodePath(fileName);
78          init(scheme, new File(fileName));
79      }
80  
81      /***
82       * Builds a FileSource, given an URI scheme and a File.
83       * 
84       * @param scheme
85       * @param file
86       * @throws SourceException
87       */
88      public FileSource(String scheme, File file) throws SourceException
89      {
90          init(scheme, file);
91      }
92  
93      private void init(String scheme, File file) throws SourceException
94      {
95          m_scheme = scheme;
96  
97          String uri;
98          try
99          {
100             uri = file.toURL().toExternalForm();
101         }
102         catch (MalformedURLException mue)
103         {
104             // Can this really happen ?
105             throw new SourceException("Failed to get URL for file " + file, mue);
106         }
107 
108         if (!uri.startsWith(scheme))
109         {
110             // Scheme is not "file:"
111             uri = scheme + ':' + uri.substring(uri.indexOf(':') + 1);
112         }
113 
114         m_uri = uri;
115 
116         m_file = file;
117     }
118 
119     /***
120      * Get the associated file
121      */
122     public File getFile()
123     {
124         return m_file;
125     }
126 
127     //----------------------------------------------------------------------------------
128     //  Source interface methods
129     //----------------------------------------------------------------------------------
130 
131     /***
132      * @see org.apache.struts.flow.core.source.Source#getContentLength()
133      */
134     public long getContentLength()
135     {
136         return m_file.length();
137     }
138 
139     /***
140      * @see org.apache.struts.flow.core.source.Source#getInputStream()
141      */
142     public InputStream getInputStream() throws IOException, SourceNotFoundException
143     {
144         try
145         {
146             return new FileInputStream(m_file);
147         }
148         catch (FileNotFoundException fnfe)
149         {
150             throw new SourceNotFoundException(m_uri + " doesn't exist.", fnfe);
151         }
152     }
153 
154     /***
155      * @see org.apache.struts.flow.core.source.Source#getLastModified()
156      */
157     public long getLastModified()
158     {
159         return m_file.lastModified();
160     }
161 
162     /***
163      * @see org.apache.struts.flow.core.source.Source#getMimeType()
164      */
165     public String getMimeType()
166     {
167         return URLConnection.getFileNameMap().getContentTypeFor(m_file.getName());
168     }
169 
170     /* (non-Javadoc)
171      * @see org.apache.struts.flow.core.source.Source#getScheme()
172      */
173     public String getScheme()
174     {
175         return m_scheme;
176 
177     }
178 
179     /* (non-Javadoc)
180      * @see org.apache.struts.flow.core.source.Source#getURI()
181      */
182     public String getURI()
183     {
184         return m_uri;
185     }
186 
187     /***
188      * Return a validity object based on the file's modification date.
189      * 
190      * @see org.apache.struts.flow.core.source.Source#getValidity()
191      */
192     public SourceValidity getValidity()
193     {
194         if (m_file.exists())
195         {
196             return new FileTimeStampValidity(m_file);
197         }
198         else
199         {
200             return null;
201         }
202     }
203 
204     /***
205      * @see org.apache.struts.flow.core.source.Source#refresh()
206      */
207     public void refresh()
208     {
209         // Nothing to do...
210     }
211 
212     /***
213      * Does this source actually exist ?
214      *
215      * @return true if the resource exists.
216      */
217     public boolean exists()
218     {
219         return getFile().exists();
220     }
221 
222     //----------------------------------------------------------------------------------
223     //  TraversableSource interface methods
224     //----------------------------------------------------------------------------------
225 
226     /***
227      * @see org.apache.struts.flow.core.source.TraversableSource#getChild(java.lang.String)
228      */
229     public Source getChild(String name) throws SourceException
230     {
231         if (!m_file.isDirectory())
232         {
233             throw new SourceException(getURI() + " is not a directory");
234         }
235 
236         return new FileSource(this.getScheme(), new File(m_file, name));
237 
238     }
239 
240     /***
241      * @see org.apache.struts.flow.core.source.TraversableSource#getChildren()
242      */
243     public Collection getChildren() throws SourceException
244     {
245 
246         if (!m_file.isDirectory())
247         {
248             throw new SourceException(getURI() + " is not a directory");
249         }
250 
251         // Build a FileSource object for each of the children
252         File[] files = m_file.listFiles();
253 
254         FileSource[] children = new FileSource[files.length];
255         for (int i = 0; i < files.length; i++)
256         {
257             children[i] = new FileSource(this.getScheme(), files[i]);
258         }
259 
260         // Return it as a list
261         return Arrays.asList(children);
262     }
263 
264     /***
265      * @see org.apache.struts.flow.core.source.TraversableSource#getName()
266      */
267     public String getName()
268     {
269         return m_file.getName();
270     }
271 
272     /***
273      * @see org.apache.struts.flow.core.source.TraversableSource#getParent()
274      */
275     public Source getParent() throws SourceException
276     {
277         return new FileSource(getScheme(), m_file.getParentFile());
278     }
279 
280     /***
281      * @see org.apache.struts.flow.core.source.TraversableSource#isCollection()
282      */
283     public boolean isCollection()
284     {
285         return m_file.isDirectory();
286     }
287 
288     //----------------------------------------------------------------------------------
289     //  ModifiableSource interface methods
290     //----------------------------------------------------------------------------------
291 
292     /***
293      * Get an <code>InputStream</code> where raw bytes can be written to.
294      * The signification of these bytes is implementation-dependent and
295      * is not restricted to a serialized XML document.
296      *
297      * The output stream returned actually writes to a temp file that replaces
298      * the real one on close. This temp file is used as lock to forbid multiple
299      * simultaneous writes. The real file is updated atomically when the output
300      * stream is closed.
301      *
302      * The returned stream must be closed or cancelled by the calling code.
303      *
304      * @return a stream to write to
305      * @throws ConcurrentModificationException if another thread is currently
306      *         writing to this file.
307      */
308     public OutputStream getOutputStream() throws IOException
309     {
310         // Create a temp file. It will replace the right one when writing terminates,
311         // and serve as a lock to prevent concurrent writes.
312         File tmpFile = new File(getFile().getPath() + ".tmp");
313 
314         // Ensure the directory exists
315         tmpFile.getParentFile().mkdirs();
316 
317         // Can we write the file ?
318         if (getFile().exists() && !getFile().canWrite())
319         {
320             throw new IOException("Cannot write to file " + getFile().getPath());
321         }
322 
323         // Check if it temp file already exists, meaning someone else currently writing
324         if (!tmpFile.createNewFile())
325         {
326             throw new ConcurrentModificationException(
327                 "File " + getFile().getPath() + " is already being written by another thread");
328         }
329 
330         // Return a stream that will rename the temp file on close.
331         return new FileSourceOutputStream(tmpFile, this);
332     }
333 
334     /***
335      * Can the data sent to an <code>OutputStream</code> returned by
336      * {@link #getOutputStream()} be cancelled ?
337      *
338      * @return true if the stream can be cancelled
339      */
340     public boolean canCancel(OutputStream stream)
341     {
342         if (stream instanceof FileSourceOutputStream)
343         {
344             FileSourceOutputStream fsos = (FileSourceOutputStream) stream;
345             if (fsos.getSource() == this)
346             {
347                 return fsos.canCancel();
348             }
349         }
350 
351         // Not a valid stream for this source
352         throw new IllegalArgumentException("The stream is not associated to this source");
353     }
354 
355     /***
356      * Cancel the data sent to an <code>OutputStream</code> returned by
357      * {@link #getOutputStream()}.
358      * <p>
359      * After cancel, the stream should no more be used.
360      */
361     public void cancel(OutputStream stream) throws SourceException
362     {
363         if (stream instanceof FileSourceOutputStream)
364         {
365             FileSourceOutputStream fsos = (FileSourceOutputStream) stream;
366             if (fsos.getSource() == this)
367             {
368                 try
369                 {
370                     fsos.cancel();
371                 }
372                 catch (Exception e)
373                 {
374                     throw new SourceException("Exception during cancel.", e);
375                 }
376                 return;
377             }
378         }
379 
380         // Not a valid stream for this source
381         throw new IllegalArgumentException("The stream is not associated to this source");
382     }
383 
384     /***
385      * Delete the source.
386      */
387     public void delete() throws SourceException
388     {
389         if (!m_file.exists())
390         {
391             throw new SourceNotFoundException("Cannot delete non-existing file " + m_file.toString());
392         }
393         
394         if (!m_file.delete())
395         {
396             throw new SourceException("Could not delete " + m_file.toString() + " (unknown reason)");
397         } 
398     }
399 
400     //----------------------------------------------------------------------------------
401     //  ModifiableTraversableSource interface methods
402     //----------------------------------------------------------------------------------
403 
404     /***
405      * @see org.apache.struts.flow.core.source.ModifiableTraversableSource#makeCollection()
406      */
407     public void makeCollection() throws SourceException
408     {
409         m_file.mkdirs();
410     }
411 
412     //----------------------------------------------------------------------------------
413     //  MoveableSource interface methods
414     //----------------------------------------------------------------------------------
415 
416     /***
417      * @see org.apache.struts.flow.core.source.MoveableSource#copyTo(org.apache.struts.flow.core.source.Source)
418      */
419     public void copyTo(Source destination) throws SourceException
420     {
421         try
422         {
423             SourceUtil.copy(this.getInputStream(), ((ModifiableSource) destination).getOutputStream());
424         }
425         catch (IOException ioe)
426         {
427             throw new SourceException("Couldn't copy " + getURI() + " to " + destination.getURI(), ioe);
428         }
429     }
430 
431     /***
432      * @see org.apache.struts.flow.core.source.MoveableSource#moveTo(org.apache.struts.flow.core.source.Source)
433      */
434     public void moveTo(Source destination) throws SourceException
435     {
436         if (destination instanceof FileSource)
437         {
438             final File dest = ((FileSource) destination).getFile();
439             final File parent = dest.getParentFile();
440 
441             if (parent != null)
442             {
443                 parent.mkdirs(); // ensure parent directories exist
444             }
445 
446             if (!m_file.renameTo(dest))
447             {
448                 throw new SourceException("Couldn't move " + getURI() + " to " + destination.getURI());
449             }
450         }
451         else
452         {
453             SourceUtil.move(this, destination);
454         }
455 
456     }
457 
458     //----------------------------------------------------------------------------------
459     //  Private helper class for ModifiableSource implementation
460     //----------------------------------------------------------------------------------
461 
462     /***
463      * A file outputStream that will rename the temp file to the destination file upon close()
464      * and discard the temp file upon cancel().
465      */
466     private static class FileSourceOutputStream extends FileOutputStream
467     {
468 
469         private File m_tmpFile;
470         private boolean m_isClosed = false;
471         private FileSource m_source;
472 
473         public FileSourceOutputStream(File tmpFile, FileSource source) throws IOException
474         {
475             super(tmpFile);
476             m_tmpFile = tmpFile;
477             m_source = source;
478         }
479 
480         public void close() throws IOException
481         {
482             if (!m_isClosed)
483             {
484                 super.close();
485                 try
486                 {
487                     // Delete destination file
488                     if (m_source.getFile().exists())
489                     {
490                         m_source.getFile().delete();
491                     }
492                     // Rename temp file to destination file
493                     if (!m_tmpFile.renameTo(m_source.getFile())) 
494                     {
495                        throw new IOException("Could not rename " + 
496                          m_tmpFile.getAbsolutePath() + 
497                          " to " + m_source.getFile().getAbsolutePath());
498                     }
499 
500                 }
501                 finally
502                 {
503                     // Ensure temp file is deleted, ie lock is released.
504                     // If there was a failure above, written data is lost.
505                     if (m_tmpFile.exists())
506                     {
507                         m_tmpFile.delete();
508                     }
509                     m_isClosed = true;
510                 }
511             }
512 
513         }
514 
515         public boolean canCancel()
516         {
517             return !m_isClosed;
518         }
519 
520         public void cancel() throws Exception
521         {
522             if (m_isClosed)
523             {
524                 throw new IllegalStateException("Cannot cancel : outputstrem is already closed");
525             }
526 
527             m_isClosed = true;
528             super.close();
529             m_tmpFile.delete();
530         }
531 
532         public void finalize()
533         {
534             if (!m_isClosed && m_tmpFile.exists())
535             {
536                 // Something wrong happened while writing : delete temp file
537                 m_tmpFile.delete();
538             }
539         }
540 
541         public FileSource getSource()
542         {
543             return m_source;
544         }
545     }
546 }