View Javadoc

1   //
2   // Copyright (c) 2003, Caltha - Gajda, Krzewski, Mach, Potempski Sp.J.
3   // All rights reserved.
4   //
5   // Redistribution and use in source and binary forms, with or without modification, 
6   // are permitted provided that the following conditions are met:
7   //
8   // * Redistributions of source code must retain the above copyright notice, 
9   //	 this list of conditions and the following disclaimer.
10  // * Redistributions in binary form must reproduce the above copyright notice, 
11  //	 this list of conditions and the following disclaimer in the documentation 
12  //	 and/or other materials provided with the distribution.
13  // * Neither the name of the Caltha - Gajda, Krzewski, Mach, Potempski Sp.J. 
14  //	 nor the names of its 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 COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
18  // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
19  // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
20  // IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
21  // INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
22  // BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
23  // OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
24  // WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
25  // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
26  // POSSIBILITY OF SUCH DAMAGE.
27  //
28  
29  package org.objectledge.filesystem;
30  
31  import java.io.ByteArrayOutputStream;
32  import java.io.IOException;
33  import java.io.InputStream;
34  import java.io.InputStreamReader;
35  import java.io.OutputStream;
36  import java.io.OutputStreamWriter;
37  import java.io.Reader;
38  import java.io.StringWriter;
39  import java.io.UnsupportedEncodingException;
40  import java.io.Writer;
41  import java.net.MalformedURLException;
42  import java.net.URL;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.HashSet;
46  import java.util.Iterator;
47  import java.util.List;
48  import java.util.Set;
49  import java.util.StringTokenizer;
50  
51  /***
52   * Provides an abstration of files available in the local file system, in the ClassLoader, web 
53   * application context, or through java.net.URL mechanism.
54   *
55   * @author <a href="rafal@caltha.pl">Rafal.Krzewski</a>
56   * @version $Id: FileSystem.java,v 1.31 2006/02/08 18:22:33 zwierzem Exp $
57   */
58  public class FileSystem
59  {
60      private String protocol = "ledge";
61  
62      /*** The providers. */
63      private List<FileSystemProvider> providers = new ArrayList<FileSystemProvider>();
64  
65      /*** The size of the buffer. */
66      private int bufferSize;
67  
68      /*** Maximum size of a file that is loaded into memory in one chunk by the. 
69       * read() methods. */
70      private int maxReadSize;
71  
72      /***
73       * Protected no-arg constructor to allow mocking.
74       */
75      protected FileSystem()
76      {
77          // needed by jMock
78      }
79      
80      /***
81       * Creates a new instance of the File System.
82       * 
83       * @param providers the backend implementations of the service.
84       * @param bufferSize the size of the buffers using for reading/writing files.
85       * @param maxReadSize maximum size of file that is loaded into memory as a single chunk.
86       */
87      public FileSystem(FileSystemProvider[] providers, int bufferSize, int maxReadSize)
88      {
89          this.providers = Arrays.asList(providers);
90          this.bufferSize = bufferSize;
91          this.maxReadSize = maxReadSize;
92      }
93  
94      // FileSystem interface ////////////////////////////////////////////////
95  
96      /***
97       * Return the URL protocol name for this FileSystem.
98       * 
99       * @return the protocol prefix used by the service.
100      */
101     public String getProtocol()
102     {
103         return protocol;
104     }
105 
106     /***
107      * Return the installed providers.
108      * 
109      * <p>The provider objects are returned in the same order in which they are
110      * accessed.</p>
111      * 
112      * @return the installed providers.
113      */
114     public FileSystemProvider[] getProviders()
115     {
116         FileSystemProvider[] result = new FileSystemProvider[providers.size()];
117         providers.toArray(result);
118         return result;
119     }
120 
121     /***
122      * Return the provider with the specified name.
123      *
124      * @param name the name of the provider
125      * @return the provider.
126      * @throws IllegalArgumentException if the requested provider is not installed.
127      */
128     public FileSystemProvider getProvider(String name) throws IllegalArgumentException
129     {
130         for (int i = 0; i < providers.size(); i++)
131         {
132             FileSystemProvider fp = (FileSystemProvider)providers.get(i);
133             if (fp.getName().equals(name))
134             {
135                 return fp;
136             }
137         }
138         throw new IllegalArgumentException("provider " + name + " is not installed");
139     }
140 
141     /***
142      * Returns an URL poiting to a file.
143      * 
144      * @param path the abstract pathname
145      * @return an URL of <code>null</code> if the file is not found.
146      * @throws MalformedURLException if the pathname is not valid.
147      */
148     public URL getResource(String path)
149         throws MalformedURLException
150     {
151         for (Iterator i = providers.iterator(); i.hasNext();)
152         {
153             FileSystemProvider fp = (FileSystemProvider)i.next();
154             URL url = fp.getResource(path);
155             if (url != null)
156             {
157                 return url;
158             }
159         }
160         return null;
161     }
162 
163     /***
164      * Returns an InputStream for reading the file.
165      *
166      * @param path the abstract pathname.
167      * @return an InputStream, or <code>null</code> if the file is not found.
168      */
169     public InputStream getInputStream(String path)
170     {
171         for (Iterator i = providers.iterator(); i.hasNext();)
172         {
173             FileSystemProvider fp = (FileSystemProvider)i.next();
174             InputStream is = fp.getInputStream(path);
175             if (is != null)
176             {
177                 return is;
178             }
179         }
180         return null;
181     }
182     
183     /***
184      * Returns a Reader for reading the file.
185      * 
186      * @param path the abstract pathname.
187      * @param encoding character encoding to use.
188      * @return a Reader, or <code>null</code> if the file is not found.
189      * @throws UnsupportedEncodingException if the requested encoding is not supported.
190      */
191     public Reader getReader(String path, String encoding)
192         throws UnsupportedEncodingException
193     {
194         InputStream is = getInputStream(path);
195         if(is != null)
196         {
197             return new InputStreamReader(is, encoding);
198         }
199         else
200         {
201             return null;
202         }
203     }
204 
205     /***
206      * Returns an OutputStream for writing the file, or appending to it.
207      *
208      * @param path the abstract pathname.
209      * @param append <code>false</code> to truncate the file,
210      *        <code>true</code> to append.
211      * @return an OutputStream, or <code>null</code> if the operation is not
212      *         supported, or the file could not be opened for writing.
213      */
214     public OutputStream getOutputStream(String path, boolean append)
215     {
216         for (Iterator i = providers.iterator(); i.hasNext();)
217         {
218             FileSystemProvider fp = (FileSystemProvider)i.next();
219             OutputStream os = fp.getOutputStream(path, append);
220             if (os != null)
221             {
222                 return os;
223             }
224         }
225         return null;
226     }
227     
228     /***
229      * Returns an OutputStream for writing the file.
230      *
231      * @param path the abstract pathname.
232      * @return an OutputStream, or <code>null</code> if the operation is not
233      *         supported, or the file could not be opened for writing.
234      */
235     public OutputStream getOutputStream(String path)
236     {
237         return getOutputStream(path, false);
238     }
239 
240     /***
241      * Returns a Writer for writing the file, or appendig to it.
242      *
243      * @param path the abstract pathname.
244      * @param encoding the character encoding to use.
245      * @param append <code>false</code> to truncate the file,
246      *        <code>true</code> to append.  
247      * @return a Writer, or <code>null</code> if the operation is not
248      *         supported, or the file could not be opened for writing.
249      * @throws UnsupportedEncodingException if the requested encoding is not supported.
250      */
251     public Writer getWriter(String path, String encoding, boolean append)
252         throws UnsupportedEncodingException
253     {
254         OutputStream os = getOutputStream(path, append);
255         if(os != null)
256         {
257             return new OutputStreamWriter(os, encoding);
258         }
259         else
260         {
261             return null;
262         }
263     }
264 
265     /***
266      * Returns a Writer for writing the file, or appendig to it.
267      *
268      * @param path the abstract pathname.
269      * @param encoding the character encoding to use.
270      * @return a Writer, or <code>null</code> if the operation is not
271      *         supported, or the file could not be opened for writing.
272      * @throws UnsupportedEncodingException if the requested encoding is not supported.
273      */
274     public Writer getWriter(String path, String encoding)
275         throws UnsupportedEncodingException
276     {
277         return getWriter(path, encoding, false);
278     }
279     
280     /***
281      * Returns a <code>RandomAccess</code> interface implementation for accessing the file at
282      * arbitrary positions.
283      *
284      * @param path the abstract pathname.
285      * @param mode the string which defines the opening mode for this random access file,
286      *              the form of this string is equal to the <code>mode</code> parameter in
287      *              <code>java.io.RandomAccessFile</code> constructor.
288      * @return an RandomAccess interface implementation, or <code>null</code>
289      *         if the operation is not supported.
290      */
291     public RandomAccessFile getRandomAccess(String path, String mode)
292     {
293         for (Iterator i = providers.iterator(); i.hasNext();)
294         {
295             FileSystemProvider fp = (FileSystemProvider)i.next();
296             RandomAccessFile ra = fp.getRandomAccess(path, mode);
297             if (ra != null)
298             {
299                 return ra;
300             }
301         }
302         return null;
303     }
304 
305     /***
306      * Returns <code>true</code> if the file specified by the abstract
307      * pathname exists.
308      *
309      * @param path the abstract pathname
310      * @return <code>true</code> if the file specified by the abstract
311      *         pathname exists.
312      */
313     public boolean exists(String path)
314     {
315         for (Iterator i = providers.iterator(); i.hasNext();)
316         {
317             FileSystemProvider fp = (FileSystemProvider)i.next();
318             if (fp.exists(path))
319             {
320                 return true;
321             }
322         }
323         return false;
324     }
325 
326     /***
327      * Returns <code>true</code> if the abstract pathname points to an
328      * ordinary file.
329      *
330      * @param path the abstract pathname
331      * @return <code>true</code> if the file specified by the abstract
332      *         pathname exists.
333      */
334     public boolean isFile(String path)
335     {
336         for (Iterator i = providers.iterator(); i.hasNext();)
337         {
338             FileSystemProvider fp = (FileSystemProvider)i.next();
339             if (fp.exists(path))
340             {
341                 return fp.isFile(path);
342             }
343         }
344         return false;
345     }
346 
347     /***
348      * Returns <code>true</code> if the abstract pathname points to a
349      * directory.
350      *
351      * @param path the abstract pathname
352      * @return <code>true</code> if the file specified by the abstract
353      *         pathname exists.
354      */
355     public boolean isDirectory(String path)
356     {
357         for (Iterator i = providers.iterator(); i.hasNext();)
358         {
359             FileSystemProvider fp = (FileSystemProvider)i.next();
360             if (fp.exists(path))
361             {
362                 return fp.isDirectory(path);
363             }
364         }
365         return false;
366     }
367 
368     /***
369      * Returns <code>true</code> if the file specified by the abstract
370      * pathname can be read.
371      *
372      * @param path the abstract pathname
373      * @return <code>true</code> if the file specified by the abstract
374      *         pathname can be read.
375      */
376     public boolean canRead(String path)
377     {
378         for (Iterator i = providers.iterator(); i.hasNext();)
379         {
380             FileSystemProvider fp = (FileSystemProvider)i.next();
381             if (fp.exists(path))
382             {
383                 return fp.canRead(path);
384             }
385         }
386         return false;
387     }
388 
389     /***
390      * Returns <code>true</code> if the file specified by the abstract
391      * pathname can be written.
392      *
393      * @param path the abstract pathname
394      * @return <code>true</code> if the file specified by the abstract
395      *         pathname can be written.
396      */
397     public boolean canWrite(String path)
398     {
399         for (Iterator i = providers.iterator(); i.hasNext();)
400         {
401             FileSystemProvider fp = (FileSystemProvider)i.next();
402             if (fp.exists(path))
403             {
404                 return fp.canWrite(path);
405             }
406         }
407         return false;
408     }
409 
410     /***
411      * Returns the time of the last modificaion of the specified file.
412      *
413      * <p>The time of the modification is returned as the number of
414      * milliseconds sice the epoch (Jan 1 1970), or -1L if the feature is not
415      * supported.</p>
416      *
417      * @param path the abstract pathname.
418      * @return the time of last modification of the specified file.
419      */
420     public long lastModified(String path)
421     {
422         for (Iterator i = providers.iterator(); i.hasNext();)
423         {
424             FileSystemProvider fp = (FileSystemProvider)i.next();
425             if (fp.exists(path))
426             {
427                 return fp.lastModified(path);
428             }
429         }
430         return -1L;
431     }
432 
433     /***
434      * Returns the size of the specified file.
435      *
436      * <p>If the operation is not supported -1L is returned.</p>
437      * 
438      * @param path an abstract pathname.
439      * @return the lenght of the file
440      */
441     public long length(String path)
442     {
443         for (Iterator i = providers.iterator(); i.hasNext();)
444         {
445             FileSystemProvider fp = (FileSystemProvider)i.next();
446             if (fp.exists(path))
447             {
448                 return fp.length(path);
449             }
450         }
451         return -1L;
452     }
453 
454     /***
455      * Lists the contents of a directory.
456      * 
457      * @param path the directory to list.
458      * @return array of names of the contents of a directory.
459      * @throws IOException if the pathname does not point to a directory.
460      */
461     public String[] list(String path)
462         throws IOException
463     {
464         if (!exists(path)) {
465             throw new IOException(path+" does not exist");        
466         }
467         Set<String> results = new HashSet<String>();
468         for (Iterator i = providers.iterator(); i.hasNext();)
469         {
470             FileSystemProvider fp = (FileSystemProvider)i.next();
471             if (fp.exists(path))
472             {
473                 if (fp.isDirectory(path))
474                 {
475                     if(fp.canRead(path))
476                     {
477                         results.addAll(fp.list(path));
478                     }
479                     else
480                     {
481                         throw new IOException(fp.getName() + " provider has " + path
482                             + " but it's not readable.");
483                     }
484                 }
485                 else
486                 {
487                     throw new IOException(fp.getName() + " provider has " + path
488                         + " but it's not a directory.");                    
489                 }
490             }
491         }
492         return results.toArray(new String[0]);
493     }
494 
495     /***
496      * Atomically creates a new, empty file named by this abstract pathname if 
497      * and only if a file with this name does not yet exist. 
498      * 
499      * <p>The check for the existence of the file and the creation of the file 
500      * if it does not exist are a single operation that is atomic with respect 
501      * to all other filesystem activities that might affect the file.</p>
502      * 
503      * @param path the pathname of the file to create.
504      * @return <code>true</code> if the named file does not exist and was 
505      *          successfully created; <code>false</code> if the named file 
506      *          already exists.
507      * @throws UnsupportedCharactersInFilePathException if the given path contains
508      *  characters incompatible with underlying filesystem.
509      * @throws IOException if the operation fails.
510      */
511     public boolean createNewFile(String path)
512         throws IOException, UnsupportedCharactersInFilePathException
513     {
514         String dir = directoryPath(path);
515         if (!exists(dir))
516         {
517             throw new IOException(dir + " does not exist");
518         }
519         if (!isDirectory(dir))
520         {
521             throw new IOException(dir + " is not a directory");
522         }
523         if (!canWrite(dir))
524         {
525             throw new IOException(dir + ": access denied");
526         }
527         for (Iterator i = providers.iterator(); i.hasNext();)
528         {
529             FileSystemProvider fp = (FileSystemProvider)i.next();
530             if (fp.exists(dir))
531             {
532                 return fp.createNewFile(path);
533             }
534         }
535         throw new IOException("internal error");
536     }
537 
538     /***
539      * Creates a directory and all neccessary parent directories.
540      * 
541      * @param path the directory name.
542      * @throws UnsupportedCharactersInFilePathException if the given path contains
543      *  characters incompatible with underlying filesystem.
544      * @throws IOException if the operation fails.
545      */
546     public void mkdirs(String path)
547         throws IOException, UnsupportedCharactersInFilePathException
548     {
549         path = normalizedPath(path);
550         StringTokenizer st = new StringTokenizer(path, "/");
551         StringBuilder sb = new StringBuilder();
552         String parent = "/";
553         while (st.hasMoreTokens())
554         {
555             sb.append('/').append(st.nextToken());
556             String dir = sb.toString();
557             if (exists(dir))
558             {
559                 if (!isDirectory(dir))
560                 {
561                     throw new IOException(dir + " exists and is not a directory");
562                 }
563             }
564             inner : for (Iterator i = providers.iterator(); i.hasNext();)
565             {
566                 FileSystemProvider fp = (FileSystemProvider)i.next();
567                 if (fp.canWrite(parent) && !fp.exists(dir))
568                 {
569                     fp.mkdirs(dir);
570                     break inner;
571                 }
572             }
573             parent = dir;
574         }
575     }
576 
577     /***
578      * Deletes a file or directory.
579      * 
580      * <p>A directory must be empty at the time it is deleted.</p>
581      * 
582      * @param path the path of the file or directory
583      * @throws IOException if the operation fails.
584      */        
585     public void delete(String path) throws IOException
586     {
587         if (!exists(path))
588         {
589             throw new IOException(path + " does not exist");
590         }
591         String dir = directoryPath(path);
592         if (!canWrite(dir))
593         {
594             throw new IOException(dir + " : access denied");
595         }
596         for (Iterator i = providers.iterator(); i.hasNext();)
597         {
598             FileSystemProvider fp = (FileSystemProvider)i.next();
599             if (fp.exists(dir))
600             {
601                 fp.delete(path);
602                 return;
603             }
604         }
605     }
606 
607 	/***
608 	 * Deletes recursive a directory.
609 	 * 
610 	 * @param path the path of the directory.
611 	 * @throws IOException if the operation fails.
612 	 */        
613 	public void deleteRecursive(String path) throws IOException
614 	{
615 		if (!exists(path))
616 		{
617 			throw new IOException(path + " does not exist");
618 		}
619 		String[] files = list(path);
620 		for(int i = 0; i < files.length;i++)
621 		{
622 			if(isDirectory(path+"/"+files[i]))
623 			{
624 				deleteRecursive(path + "/" + files[i]);
625             }
626 			else
627             {
628 			    delete(path+"/"+files[i]);
629 			}
630 		}
631 		String dir = directoryPath(path);
632 		if (!canWrite(dir))
633 		{
634 			throw new IOException(dir + " : access denied");
635 		}
636 		for (Iterator i = providers.iterator(); i.hasNext();)
637 		{
638 			FileSystemProvider fp = (FileSystemProvider)i.next();
639 			if (fp.exists(dir))
640 			{
641 				fp.delete(path);
642 				return;
643 			}
644 		}
645 	}
646 
647     /***
648      * Atomically renames a file or directory.
649      * 
650      * @param from source path.
651      * @param to destination path.
652      * @throws UnsupportedCharactersInFilePathException if the given destination path contains
653      *  characters incompatible with underlying filesystem.
654      * @throws IOException if the operation fails.
655      */
656     public void rename(String from, String to)
657         throws IOException, UnsupportedCharactersInFilePathException    
658     {
659         if (!exists(from))
660         {
661             throw new IOException("source file " + from + " does not exist");
662         }
663         String dir = directoryPath(to);
664         if (!exists(dir))
665         {
666             throw new IOException("destination directory " + dir + " does not exist");
667         }
668         if (!canWrite(dir))
669         {
670             throw new IOException(dir + " : access denied");
671         }
672         for (Iterator i = providers.iterator(); i.hasNext();)
673         {
674             FileSystemProvider fp = (FileSystemProvider)i.next();
675             if (fp.exists(dir))
676             {
677                 fp.rename(from, to);
678                 return;
679             }
680         }
681     }
682 
683     /***
684      * Copies a file from one location to another.
685      * 
686      * @param from the source path.
687      * @param to the destination path.
688      * @throws UnsupportedCharactersInFilePathException if the given destination path contains
689      *  characters incompatible with underlying filesystem.
690      * @throws IOException if the operation fails.
691      */
692     public void copyFile(String from, String to) 
693         throws IOException, UnsupportedCharactersInFilePathException    
694     {
695         InputStream in = getInputStream(from);
696         if (in == null)
697         {
698             throw new IOException("failed to open source file " + from);
699         }
700         String dir = directoryPath(to);
701         mkdirs(dir);
702         OutputStream out = getOutputStream(to);
703         if (out == null)
704         {
705             throw new IOException("failed to open destination file " + to);
706         }
707         byte[] buffer = new byte[bufferSize];
708         int count = 0;
709         do
710         {
711             count = in.read(buffer, 0, bufferSize);
712             if (count > 0)
713             {
714                 out.write(buffer, 0, count);
715             }
716         }
717         while (count > 0);
718         out.flush();
719     }
720 
721     /***
722      * Copies the contents of a directory to another location.
723      * 
724      * @param src source directory.
725      * @param dst destination directory.
726      * @throws UnsupportedCharactersInFilePathException if the given destination path contains
727      *  characters incompatible with underlying filesystem.
728      * @throws IOException if the operation fails.
729      */
730     public void copyDir(String src, String dst)
731         throws IOException, UnsupportedCharactersInFilePathException    
732     {
733         if (!exists(src))
734         {
735             throw new IOException("source directory " + src + " does not exist");
736         }
737         if (!canRead(src))
738         {
739             throw new IOException("source directory " + src + " is not readable");
740         }
741         if (!isDirectory(src))
742         {
743             throw new IOException(src + " is not a directory");
744         }
745 
746         mkdirs(dst);
747         String[] srcFiles = list(src);
748         for (int i = 0; i < srcFiles.length; i++)
749         {
750             String name = srcFiles[i];
751             if (isDirectory(src + "/" + name))
752             {
753                 copyDir(src + "/" + name, dst + "/" + name);
754             }
755             else
756             {
757                 copyFile(src + "/" + name, dst + "/" + name);
758             }
759         }
760     }
761 
762     /***
763      * Read the contents of a file and write them into an OutputStream.
764      * 
765      * @param path the pathname of the file.
766      * @param out the stream to write file contents to.
767      * @throws IOException if the operation fails.
768      */
769     public void read(String path, OutputStream out) throws IOException
770     {
771         InputStream ins = getInputStream(path);
772         if (ins == null)
773         {
774             throw new IOException(path + " does not exist");
775         }
776         byte[] buffer = new byte[bufferSize];
777         int count = 0;
778         do
779         {
780             count = ins.read(buffer, 0, bufferSize);
781             if (count > 0)
782             {
783                 out.write(buffer, 0, count);
784             }
785         }
786         while (count > 0);
787         out.flush();
788     }
789 
790     /***
791      * Read the contents of a file into a byte array.
792      *  
793      * @param path the pathnamame of the file.
794      * @return the contents of the file.
795      * @throws IOException if the operation fails.
796      */ 
797     public byte[] read(String path) throws IOException
798     {
799         InputStream in = getInputStream(path);
800         if (in == null)
801         {
802             throw new IOException(path + " does not exist");
803         }
804         long length = length(path);
805         if (length < 0)
806         {
807             length = bufferSize;
808         }
809         if (length > maxReadSize)
810         {
811             throw new IOException(path + " is too large (" + length + "b)");
812         }
813         ByteArrayOutputStream out = new ByteArrayOutputStream((int)length);
814         read(path, out);
815         return out.toByteArray();
816     }
817 
818     /***
819      * Read the contents of a file into a String.
820      *  
821      * @param path the pathnamame of the file.
822      * @param encoding the character encoding to use for decoding bytes into 
823      *        Unicode characters.
824      * @return the contents of the file.
825      * @throws IOException if the file cannot be read, or the specified encoding 
826      *         is not supported
827      */ 
828     public String read(String path, String encoding)
829         throws IOException
830     {
831         if(!exists(path))
832         {
833             throw new IOException(path + " does not exist");
834         }
835         long length = length(path);
836         if (length < 0)
837         {
838             length = bufferSize;
839         }
840         if (length > maxReadSize)
841         {
842             throw new IOException(path + " is too large (" + length + "b)");
843         }
844         Reader in = null;
845         try
846         {
847             InputStream ins = getInputStream(path);
848             in = new InputStreamReader(ins, encoding);
849             StringWriter out = new StringWriter((int)length);
850             char[] buffer = new char[bufferSize];
851             int count = 0;
852             do
853             {
854                 count = in.read(buffer, 0, bufferSize);
855                 if (count > 0)
856                 {
857                     out.write(buffer, 0, count);
858                 }
859             }
860             while (count > 0);
861             return out.toString();
862         }
863         finally
864         {
865             if(in != null)
866             {
867                 in.close();
868             }
869         }
870     }
871 
872     /***
873      * Write the data read from an InputStream into a file.
874      * 
875      * @param path the pathname of the file
876      * @param in the steram to read data from.
877      * @throws IOException if the opreation fails.
878      */
879     public void write(String path, InputStream in) throws IOException
880     {
881         OutputStream out = getOutputStream(path);
882         if (out == null)
883         {
884             throw new IOException("failed to open output file " + path);
885         }
886         try
887         {
888             byte[] buffer = new byte[bufferSize];
889             int count = 0;
890             do
891             {
892                 count = in.read(buffer, 0, bufferSize);
893                 if (count > 0)
894                 {
895                     out.write(buffer, 0, count);
896                 }
897             }
898             while (count > 0);
899         }
900         finally
901         {
902             out.close();
903         }
904     }
905 
906     /***
907      * Write contents of a byte array into a file.
908      * 
909      * @param path the pathname of the file.
910      * @param bytes the bytes to be written
911      * @throws IOException if the operation fails. 
912      */
913     public void write(String path, byte[] bytes) throws IOException
914     {
915         OutputStream out = getOutputStream(path);
916         if (out == null)
917         {
918             throw new IOException("failed to open output file " + path);
919         }
920         try
921         {
922             out.write(bytes);
923         }
924         finally
925         {
926             out.close();
927         }
928     }
929 
930     /***
931      * Write contents of a byte array into a file.
932      * 
933      * @param path the pathname of the file.
934      * @param string the String to be written
935      * @param encoding the character encoding to use for encoding Unicode 
936      *        characters into bytes.
937      * @throws IOException if the file cannot be written to, or the specified encoding is not 
938      *         supported.
939      */
940     public void write(String path, String string, String encoding)
941         throws IOException
942     {
943         OutputStream outs = getOutputStream(path);
944         if (outs == null)
945         {
946             throw new IOException("failed to open output file " + path);
947         }
948         Writer out = new OutputStreamWriter(outs, encoding);
949         try
950         {
951             out.write(string);
952         }
953         finally
954         {
955             out.close();
956         }
957     }
958     
959     // pathnames ////////////////////////////////////////////////////////////
960     
961     /***
962      * Normalizes a pathname.
963      *
964      * <p>This method removes redundant / characters, removes . and .. path elements,
965      * taking care that the paths dont reach outside filesystem root, removes trailing /
966      * from directories and adding leading / as neccessary.</p>
967      * 
968      * @param path the path.
969      * @return normalized path.
970      * @throws IllegalArgumentException if the path reaches outside the filesystem root.
971      */
972     public static String normalizedPath(String path)
973         throws IllegalArgumentException
974     {
975         if(path.length()==0 || path.equals("/"))
976         {
977             return "/";
978         }
979         StringTokenizer st = new StringTokenizer(path, "/");
980         ArrayList<String> temp = new ArrayList<String>(st.countTokens());
981         while(st.hasMoreTokens())
982         {
983             String t = st.nextToken();
984             if(t.equals("."))
985             {
986                 continue;
987             }
988             else if(t.equals(".."))
989             {
990                 if(temp.isEmpty())
991                 {
992                     throw new IllegalArgumentException("path outside filesystem root: "+path);  
993                 }
994                 else
995                 {
996                     temp.remove(temp.size()-1);
997                 }
998             }
999             else
1000             {
1001                 temp.add(t);
1002             }
1003         }
1004         StringBuilder sb = new StringBuilder();
1005         for(int i=0; i<temp.size(); i++)
1006         {
1007             sb.append('/').append((String)temp.get(i));
1008         }
1009         return sb.toString();
1010     }
1011     
1012     /***
1013      * Returns the base name of a file.
1014      * 
1015      * <p>This method returns the contents of the pathname after the last '/' 
1016      * character. </p>
1017      *
1018      * @param path the pathname of the file.
1019      * @return the basename of the file.     
1020      */
1021     public static String basePath(String path)
1022     {
1023         int pos = path.lastIndexOf('/');
1024         if(pos < 0)
1025         {
1026             return path;
1027         }
1028         else
1029         {
1030             return path.substring(pos+1);
1031         }
1032     }
1033     
1034     /***
1035      * Returns hte directory name of a file.
1036      * 
1037      * <p>This method returns the normalized path before the last '/' character
1038      * in the path.</p>
1039      * 
1040      * @param path the pathname of the file.
1041      * @return the directory name of the file.   
1042      */
1043     public static String directoryPath(String path)
1044     {
1045         path = normalizedPath(path);
1046         return path.substring(0, path.lastIndexOf('/'));        
1047     }
1048     
1049     /***
1050      * Returns the relative pathname of a file with respect to given
1051      * base directory.
1052      *
1053      * @param path the pathname of a file.
1054      * @param base the base pathname.
1055      * @return the relative pathname.
1056      * @throws IllegalArgumentException if the file is contained
1057      *         outside of base.
1058      */
1059     public static String relativePath(String path, String base)
1060         throws IllegalArgumentException
1061     {
1062         base = normalizedPath(base);
1063         path = normalizedPath(path);
1064         if(!path.startsWith(base))
1065         {
1066             throw new IllegalArgumentException(path+" is not contained in "+base);
1067         }
1068         return path.substring(base.length());
1069     } 
1070     
1071     /***
1072      * Returns <code>true</code> if the given path contains acceptable characters.
1073      *
1074      * @param path the path
1075      * @return <code>true</code> if the given path contains acceptable characters.
1076      */
1077     public boolean checkPathChars(String path)
1078     {
1079         if(path == null || path.length() == 0)
1080         {
1081             return false;
1082         }
1083         for (Iterator i = providers.iterator(); i.hasNext();)
1084         {
1085             FileSystemProvider fp = (FileSystemProvider)i.next();
1086             if (!fp.checkPathChars(path))
1087             {
1088                 return false;
1089             }
1090         }
1091         return true;
1092     }
1093     
1094     // standard filesystems /////////////////////////////////////////////////
1095     
1096     /***
1097      * Creates a standard file system.
1098      * 
1099      * <p>The file system is composed of a local file system, with the specified root, augmented
1100      * with a classpath file system.</p>
1101      * 
1102      * @param root the local filesystem root.
1103      * @return a standard file system.
1104      */
1105     public static FileSystem getStandardFileSystem(String root)
1106     {
1107         FileSystemProvider lfs = new org.objectledge.filesystem.
1108             LocalFileSystemProvider("local", root);
1109         FileSystemProvider cfs = new org.objectledge.filesystem.
1110             ClasspathFileSystemProvider("classpath", 
1111             FileSystem.class.getClassLoader());
1112         return new FileSystem(new FileSystemProvider[] { lfs, cfs }, 4096, 65536);
1113     }
1114     
1115     /***
1116      * Creates a classpath file system.
1117      * 
1118      * @return a classpath file system.
1119      */
1120     public static FileSystem getClasspathFileSystem()
1121     {
1122         FileSystemProvider cfs = new org.objectledge.filesystem.
1123             ClasspathFileSystemProvider("classpath", 
1124             FileSystem.class.getClassLoader());
1125         return new FileSystem(new FileSystemProvider[] { cfs }, 4096, 65536);
1126     }
1127 }