View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.vfs2.provider;
18  
19  import java.io.BufferedInputStream;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.OutputStream;
23  import java.security.cert.Certificate;
24  import java.util.Collections;
25  import java.util.Map;
26  import java.util.Set;
27  
28  import org.apache.commons.lang3.ArrayUtils;
29  import org.apache.commons.vfs2.FileContent;
30  import org.apache.commons.vfs2.FileContentInfo;
31  import org.apache.commons.vfs2.FileContentInfoFactory;
32  import org.apache.commons.vfs2.FileObject;
33  import org.apache.commons.vfs2.FileSystemException;
34  import org.apache.commons.vfs2.RandomAccessContent;
35  import org.apache.commons.vfs2.util.MonitorInputStream;
36  import org.apache.commons.vfs2.util.MonitorOutputStream;
37  import org.apache.commons.vfs2.util.MonitorRandomAccessContent;
38  import org.apache.commons.vfs2.util.RandomAccessMode;
39  import org.apache.commons.vfs2.util.RawMonitorInputStream;
40  
41  /**
42   * The content of a file.
43   */
44  public final class DefaultFileContent implements FileContent {
45  
46      /*
47       * static final int STATE_NONE = 0; static final int STATE_READING = 1; static final int STATE_WRITING = 2; static
48       * final int STATE_RANDOM_ACCESS = 3;
49       */
50  
51      private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = new Certificate[0];
52      static final int STATE_CLOSED = 0;
53      static final int STATE_OPENED = 1;
54  
55      /**
56       * The default buffer size for {@link #write(OutputStream)}
57       */
58      private static final int WRITE_BUFFER_SIZE = 4096;
59  
60      private final AbstractFileObject<?> fileObject;
61      private Map<String, Object> attrs;
62      private Map<String, Object> roAttrs;
63      private FileContentInfo fileContentInfo;
64      private final FileContentInfoFactory fileContentInfoFactory;
65  
66      private final ThreadLocal<FileContentThreadData> threadLocal = ThreadLocal.withInitial(FileContentThreadData::new);
67      private boolean resetAttributes;
68  
69      /**
70       * Counts open streams for this file.
71       */
72      private int openStreams;
73  
74      public DefaultFileContent(final AbstractFileObject file, final FileContentInfoFactory fileContentInfoFactory) {
75          this.fileObject = file;
76          this.fileContentInfoFactory = fileContentInfoFactory;
77      }
78  
79      private FileContentThreadData getFileContentThreadData() {
80          return this.threadLocal.get();
81      }
82  
83      void streamOpened() {
84          synchronized (this) {
85              openStreams++;
86          }
87          ((AbstractFileSystem) fileObject.getFileSystem()).streamOpened();
88      }
89  
90      void streamClosed() {
91          synchronized (this) {
92              if (openStreams > 0) {
93                  openStreams--;
94                  if (openStreams < 1) {
95                      fileObject.notifyAllStreamsClosed();
96                  }
97              }
98          }
99          ((AbstractFileSystem) fileObject.getFileSystem()).streamClosed();
100     }
101 
102     /**
103      * Returns the file that this is the content of.
104      *
105      * @return the FileObject.
106      */
107     @Override
108     public FileObject getFile() {
109         return fileObject;
110     }
111 
112     /**
113      * Returns the size of the content (in bytes).
114      *
115      * @return The size of the content (in bytes).
116      * @throws FileSystemException if an error occurs.
117      */
118     @Override
119     public long getSize() throws FileSystemException {
120         // Do some checking
121         if (!fileObject.getType().hasContent()) {
122             throw new FileSystemException("vfs.provider/get-size-not-file.error", fileObject);
123         }
124         /*
125          * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
126          * new FileSystemException("vfs.provider/get-size-write.error", file); }
127          */
128 
129         try {
130             // Get the size
131             return fileObject.doGetContentSize();
132         } catch (final Exception exc) {
133             throw new FileSystemException("vfs.provider/get-size.error", exc, fileObject);
134         }
135     }
136 
137     /**
138      * Returns the last-modified timestamp.
139      *
140      * @return The last modified timestamp.
141      * @throws FileSystemException if an error occurs.
142      */
143     @Override
144     public long getLastModifiedTime() throws FileSystemException {
145         /*
146          * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
147          * new FileSystemException("vfs.provider/get-last-modified-writing.error", file); }
148          */
149         if (!fileObject.getType().hasAttributes()) {
150             throw new FileSystemException("vfs.provider/get-last-modified-no-exist.error", fileObject);
151         }
152         try {
153             return fileObject.doGetLastModifiedTime();
154         } catch (final Exception e) {
155             throw new FileSystemException("vfs.provider/get-last-modified.error", fileObject, e);
156         }
157     }
158 
159     /**
160      * Sets the last-modified timestamp.
161      *
162      * @param modTime The last modified timestamp.
163      * @throws FileSystemException if an error occurs.
164      */
165     @Override
166     public void setLastModifiedTime(final long modTime) throws FileSystemException {
167         /*
168          * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
169          * new FileSystemException("vfs.provider/set-last-modified-writing.error", file); }
170          */
171         if (!fileObject.getType().hasAttributes()) {
172             throw new FileSystemException("vfs.provider/set-last-modified-no-exist.error", fileObject);
173         }
174         try {
175             if (!fileObject.doSetLastModifiedTime(modTime)) {
176                 throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject);
177             }
178         } catch (final Exception e) {
179             throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject, e);
180         }
181     }
182 
183     /**
184      * Checks if an attribute exists.
185      *
186      * @param attrName The name of the attribute to check.
187      * @return true if the attribute is associated with the file.
188      * @throws FileSystemException if an error occurs.
189      * @since 2.0
190      */
191     @Override
192     public boolean hasAttribute(final String attrName) throws FileSystemException {
193         if (!fileObject.getType().hasAttributes()) {
194             throw new FileSystemException("vfs.provider/exists-attributes-no-exist.error", fileObject);
195         }
196         getAttributes();
197         return attrs.containsKey(attrName);
198     }
199 
200     /**
201      * Returns a read-only map of this file's attributes.
202      *
203      * @return a Map of the file's attributes.
204      * @throws FileSystemException if an error occurs.
205      */
206     @Override
207     public Map<String, Object> getAttributes() throws FileSystemException {
208         if (!fileObject.getType().hasAttributes()) {
209             throw new FileSystemException("vfs.provider/get-attributes-no-exist.error", fileObject);
210         }
211         if (resetAttributes || roAttrs == null) {
212             try {
213                 synchronized (this) {
214                     attrs = fileObject.doGetAttributes();
215                     roAttrs = Collections.unmodifiableMap(attrs);
216                     resetAttributes = false;
217                 }
218             } catch (final Exception e) {
219                 throw new FileSystemException("vfs.provider/get-attributes.error", fileObject, e);
220             }
221         }
222         return roAttrs;
223     }
224 
225     /**
226      * Used internally to flag situations where the file attributes should be reretrieved.
227      *
228      * @since 2.0
229      */
230     public void resetAttributes() {
231         resetAttributes = true;
232     }
233 
234     /**
235      * Lists the attributes of this file.
236      *
237      * @return An array of attribute names.
238      * @throws FileSystemException if an error occurs.
239      */
240     @Override
241     public String[] getAttributeNames() throws FileSystemException {
242         getAttributes();
243         final Set<String> names = attrs.keySet();
244         return names.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
245     }
246 
247     /**
248      * Gets the value of an attribute.
249      *
250      * @param attrName The attribute name.
251      * @return The value of the attribute or null.
252      * @throws FileSystemException if an error occurs.
253      */
254     @Override
255     public Object getAttribute(final String attrName) throws FileSystemException {
256         getAttributes();
257         return attrs.get(attrName);
258     }
259 
260     /**
261      * Sets the value of an attribute.
262      *
263      * @param attrName The name of the attribute to add.
264      * @param value The value of the attribute.
265      * @throws FileSystemException if an error occurs.
266      */
267     @Override
268     public void setAttribute(final String attrName, final Object value) throws FileSystemException {
269         if (!fileObject.getType().hasAttributes()) {
270             throw new FileSystemException("vfs.provider/set-attribute-no-exist.error", attrName, fileObject);
271         }
272         try {
273             fileObject.doSetAttribute(attrName, value);
274         } catch (final Exception e) {
275             throw new FileSystemException("vfs.provider/set-attribute.error", e, attrName, fileObject);
276         }
277 
278         if (attrs != null) {
279             attrs.put(attrName, value);
280         }
281     }
282 
283     /**
284      * Removes an attribute.
285      *
286      * @param attrName The name of the attribute to remove.
287      * @throws FileSystemException if an error occurs.
288      * @since 2.0
289      */
290     @Override
291     public void removeAttribute(final String attrName) throws FileSystemException {
292         if (!fileObject.getType().hasAttributes()) {
293             throw new FileSystemException("vfs.provider/remove-attribute-no-exist.error", fileObject);
294         }
295 
296         try {
297             fileObject.doRemoveAttribute(attrName);
298         } catch (final Exception e) {
299             throw new FileSystemException("vfs.provider/remove-attribute.error", e, attrName, fileObject);
300         }
301 
302         if (attrs != null) {
303             attrs.remove(attrName);
304         }
305     }
306 
307     /**
308      * Returns the certificates used to sign this file.
309      *
310      * @return An array of Certificates.
311      * @throws FileSystemException if an error occurs.
312      */
313     @Override
314     public Certificate[] getCertificates() throws FileSystemException {
315         if (!fileObject.exists()) {
316             throw new FileSystemException("vfs.provider/get-certificates-no-exist.error", fileObject);
317         }
318         /*
319          * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
320          * new FileSystemException("vfs.provider/get-certificates-writing.error", file); }
321          */
322 
323         try {
324             final Certificate[] certs = fileObject.doGetCertificates();
325             if (certs != null) {
326                 return certs;
327             }
328             return EMPTY_CERTIFICATE_ARRAY;
329         } catch (final Exception e) {
330             throw new FileSystemException("vfs.provider/get-certificates.error", fileObject, e);
331         }
332     }
333 
334     /**
335      * Returns an input stream for reading the content.
336      *
337      * @return The InputStream
338      * @throws FileSystemException if an error occurs.
339      */
340     @Override
341     public InputStream getInputStream() throws FileSystemException {
342         return buildInputStream(0);
343     }
344 
345     /**
346      * Returns an input stream for reading the content.
347      *
348      * @param bufferSize The buffer size to use.
349      * @return The InputStream
350      * @throws FileSystemException if an error occurs.
351      * @since 2.4
352      */
353     @Override
354     public InputStream getInputStream(final int bufferSize) throws FileSystemException {
355         return buildInputStream(bufferSize);
356     }
357 
358     /**
359      * Returns an input/output stream to use to read and write the content of the file in an random manner.
360      *
361      * @param mode The RandomAccessMode.
362      * @return A RandomAccessContent object to access the file.
363      * @throws FileSystemException if an error occurs.
364      */
365     @Override
366     public RandomAccessContent getRandomAccessContent(final RandomAccessMode mode) throws FileSystemException {
367         /*
368          * if (getThreadData().getState() != STATE_NONE) { throw new
369          * FileSystemException("vfs.provider/read-in-use.error", file); }
370          */
371 
372         // Get the content
373         final RandomAccessContent rastr = fileObject.getRandomAccessContent(mode);
374 
375         final FileRandomAccessContent rac = new FileRandomAccessContent(fileObject, rastr);
376 
377         getFileContentThreadData().add(rac);
378         streamOpened();
379 
380         return rac;
381     }
382 
383     /**
384      * Returns an output stream for writing the content.
385      *
386      * @return The OutputStream for the file.
387      * @throws FileSystemException if an error occurs.
388      */
389     @Override
390     public OutputStream getOutputStream() throws FileSystemException {
391         return getOutputStream(false);
392     }
393 
394     /**
395      * Returns an output stream for writing the content in append mode.
396      *
397      * @param bAppend true if the data written should be appended.
398      * @return The OutputStream for the file.
399      * @throws FileSystemException if an error occurs.
400      */
401     @Override
402     public OutputStream getOutputStream(final boolean bAppend) throws FileSystemException {
403         return buildOutputStream(bAppend, 0);
404     }
405 
406     /**
407      * Returns an output stream for writing the content.
408      *
409      * @param bufferSize The buffer size to use.
410      * @return The OutputStream for the file.
411      * @throws FileSystemException if an error occurs.
412      * @since 2.4
413      */
414     @Override
415     public OutputStream getOutputStream(final int bufferSize) throws FileSystemException {
416         return buildOutputStream(false, bufferSize);
417     }
418 
419     /**
420      * Returns an output stream for writing the content in append mode.
421      *
422      * @param bAppend true if the data written should be appended.
423      * @param bufferSize The buffer size to use.
424      * @return The OutputStream for the file.
425      * @throws FileSystemException if an error occurs.
426      * @since 2.4
427      */
428     @Override
429     public OutputStream getOutputStream(final boolean bAppend, final int bufferSize) throws FileSystemException {
430         return buildOutputStream(bAppend, bufferSize);
431     }
432 
433     /**
434      * Closes all resources used by the content, including all streams, readers and writers.
435      *
436      * @throws FileSystemException if an error occurs.
437      */
438     @Override
439     public void close() throws FileSystemException {
440         FileSystemException caught = null;
441         try {
442             final FileContentThreadData threadData = getFileContentThreadData();
443 
444             // Close the input stream
445             while (threadData.hasInputStream()) {
446                 final InputStream inputStream = threadData.removeInputStream(0);
447                 try {
448                     if (inputStream instanceof FileContentInputStream) {
449                         ((FileContentInputStream) inputStream).close();
450                     } else if (inputStream instanceof RawFileContentInputStream) {
451                         ((RawFileContentInputStream) inputStream).close();
452                     } else {
453                        caught = new FileSystemException("Unsupported InputStream type: " + inputStream);
454                     }
455                 } catch (final FileSystemException ex) {
456                     caught = ex;
457 
458                 }
459             }
460 
461             // Close the randomAccess stream
462             while (threadData.hasRandomAccessContent()) {
463                 final FileRandomAccessContent randomAccessContent = (FileRandomAccessContent) threadData
464                         .removeRandomAccessContent(0);
465                 try {
466                     randomAccessContent.close();
467                 } catch (final FileSystemException ex) {
468                     caught = ex;
469                 }
470             }
471 
472             // Close the output stream
473             final FileContentOutputStream outputStream = threadData.getOutputStream();
474             if (outputStream != null) {
475                 threadData.setOutputStream(null);
476                 try {
477                     outputStream.close();
478                 } catch (final FileSystemException ex) {
479                     caught = ex;
480                 }
481             }
482         } finally {
483             threadLocal.remove();
484         }
485 
486         // throw last error (out >> rac >> input) after all closes have been tried
487         if (caught != null) {
488             throw caught;
489         }
490     }
491 
492     private InputStream buildInputStream(final int bufferSize) throws FileSystemException {
493         /*
494          * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
495          * new FileSystemException("vfs.provider/read-in-use.error", file); }
496          */
497         // Get the raw input stream
498         // @formatter:off
499         final InputStream inputStream = bufferSize == 0
500                 ? fileObject.getInputStream()
501                 : fileObject.getInputStream(bufferSize);
502         // @formatter:on
503         // Double buffering may take place here.
504 //        final InputStream wrappedInputStream = bufferSize == 0
505 //                    ? new FileContentInputStream(fileObject, inputStream)
506 //                    : new FileContentInputStream(fileObject, inputStream, bufferSize);
507 
508         final InputStream wrappedInputStream;
509         if (inputStream instanceof BufferedInputStream) {
510             // Don't double buffer.
511             wrappedInputStream = new RawFileContentInputStream(fileObject, inputStream);
512         } else
513         {
514             // @formatter:off
515             wrappedInputStream = bufferSize == 0
516                     ? new FileContentInputStream(fileObject, inputStream)
517                     : new FileContentInputStream(fileObject, inputStream, bufferSize);
518             // @formatter:on
519         }
520         getFileContentThreadData().add(wrappedInputStream);
521         streamOpened();
522 
523         return wrappedInputStream;
524     }
525 
526     private OutputStream buildOutputStream(final boolean bAppend, final int bufferSize) throws FileSystemException {
527         /*
528          * if (getThreadData().getState() != STATE_NONE)
529          */
530         final FileContentThreadData threadData = getFileContentThreadData();
531 
532         if (threadData.getOutputStream() != null) {
533             throw new FileSystemException("vfs.provider/write-in-use.error", fileObject);
534         }
535 
536         // Get the raw output stream
537         final OutputStream outstr = fileObject.getOutputStream(bAppend);
538 
539         // Create and set wrapper
540         final FileContentOutputStream wrapped = bufferSize == 0 ?
541             new FileContentOutputStream(fileObject, outstr) :
542             new FileContentOutputStream(fileObject, outstr, bufferSize);
543         threadData.setOutputStream(wrapped);
544         streamOpened();
545 
546         return wrapped;
547     }
548 
549     /**
550      * Handles the end of input stream.
551      */
552     private void endInput(final InputStream instr) {
553         final FileContentThreadData fileContentThreadData = threadLocal.get();
554         if (fileContentThreadData != null) {
555             fileContentThreadData.remove(instr);
556         }
557         if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
558             // remove even when no value is set to remove key
559             threadLocal.remove();
560         }
561         streamClosed();
562     }
563 
564     /**
565      * Handles the end of random access.
566      */
567     private void endRandomAccess(final RandomAccessContent rac) {
568         final FileContentThreadData fileContentThreadData = threadLocal.get();
569         if (fileContentThreadData != null) {
570             fileContentThreadData.remove(rac);
571         }
572         if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
573             // remove even when no value is set to remove key
574             threadLocal.remove();
575         }
576         streamClosed();
577     }
578 
579     /**
580      * Handles the end of output stream.
581      */
582     private void endOutput() throws Exception {
583         final FileContentThreadData fileContentThreadData = threadLocal.get();
584         if (fileContentThreadData != null) {
585             fileContentThreadData.setOutputStream(null);
586         }
587         if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
588             // remove even when no value is set to remove key
589             threadLocal.remove();
590         }
591         streamClosed();
592         fileObject.endOutput();
593     }
594 
595     /**
596      * Checks if a input and/or output stream is open.
597      * <p>
598      * This checks only the scope of the current thread.
599      * </p>
600      *
601      * @return true if this is the case
602      */
603     @Override
604     public boolean isOpen() {
605         final FileContentThreadData fileContentThreadData = threadLocal.get();
606         if (fileContentThreadData != null && fileContentThreadData.hasStreams()) {
607             return true;
608         }
609         // threadData.get() created empty entry
610         threadLocal.remove();
611         return false;
612     }
613 
614     /**
615      * Checks if an input or output stream is open. This checks all threads.
616      *
617      * @return true if this is the case
618      */
619     public boolean isOpenGlobal() {
620         synchronized (this) {
621             return openStreams > 0;
622         }
623     }
624 
625     /**
626      * An input stream for reading content. Provides buffering, and end-of-stream monitoring.
627      */
628     private final class FileContentInputStream extends MonitorInputStream {
629         // avoid gc
630         private final FileObject file;
631 
632         FileContentInputStream(final FileObject file, final InputStream instr) {
633             super(instr);
634             this.file = file;
635         }
636 
637         FileContentInputStream(final FileObject file, final InputStream instr, final int bufferSize) {
638             super(instr, bufferSize);
639             this.file = file;
640         }
641 
642         /**
643          * Closes this input stream.
644          */
645         @Override
646         public void close() throws FileSystemException {
647             try {
648                 super.close();
649             } catch (final IOException e) {
650                 throw new FileSystemException("vfs.provider/close-instr.error", file, e);
651             }
652         }
653 
654         /**
655          * Called after the stream has been closed.
656          */
657         @Override
658         protected void onClose() throws IOException {
659             try {
660                 super.onClose();
661             } finally {
662                 endInput(this);
663             }
664         }
665     }
666 
667     /**
668      * An input stream for reading content. Provides buffering, and end-of-stream monitoring.
669      * <p>
670      * This is the same as {@link FileContentInputStream} but without the buffering.
671      * </p>
672      */
673     private final class RawFileContentInputStream extends RawMonitorInputStream {
674         // avoid gc
675         private final FileObject file;
676 
677         RawFileContentInputStream(final FileObject file, final InputStream instr) {
678             super(instr);
679             this.file = file;
680         }
681 
682         /**
683          * Closes this input stream.
684          */
685         @Override
686         public void close() throws FileSystemException {
687             try {
688                 super.close();
689             } catch (final IOException e) {
690                 throw new FileSystemException("vfs.provider/close-instr.error", file, e);
691             }
692         }
693 
694         /**
695          * Called after the stream has been closed.
696          */
697         @Override
698         protected void onClose() throws IOException {
699             try {
700                 super.onClose();
701             } finally {
702                 endInput(this);
703             }
704         }
705     }
706 
707     /**
708      * An input/output stream for reading/writing content on random positions
709      */
710     private final class FileRandomAccessContent extends MonitorRandomAccessContent {
711         // also avoids gc
712         private final FileObject file;
713 
714         FileRandomAccessContent(final FileObject file, final RandomAccessContent content) {
715             super(content);
716             this.file = file;
717         }
718 
719         /**
720          * Called after the stream has been closed.
721          */
722         @Override
723         protected void onClose() throws IOException {
724             try {
725                 super.onClose();
726             } finally {
727                 endRandomAccess(this);
728             }
729         }
730 
731         @Override
732         public void close() throws FileSystemException {
733             try {
734                 super.close();
735             } catch (final IOException e) {
736                 throw new FileSystemException("vfs.provider/close-rac.error", file, e);
737             }
738         }
739     }
740 
741     /**
742      * An output stream for writing content.
743      */
744     final class FileContentOutputStream extends MonitorOutputStream {
745         // avoid gc
746         private final FileObject file;
747 
748         FileContentOutputStream(final FileObject file, final OutputStream outstr) {
749             super(outstr);
750             this.file = file;
751         }
752 
753         FileContentOutputStream(final FileObject file, final OutputStream outstr, final int bufferSize) {
754             super(outstr, bufferSize);
755             this.file = file;
756         }
757 
758         /**
759          * Closes this output stream.
760          */
761         @Override
762         public void close() throws FileSystemException {
763             try {
764                 super.close();
765             } catch (final IOException e) {
766                 throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
767             }
768         }
769 
770         /**
771          * Called after this stream is closed.
772          */
773         @Override
774         protected void onClose() throws IOException {
775             try {
776                 super.onClose();
777             } finally {
778                 try {
779                     endOutput();
780                 } catch (final Exception e) {
781                     throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
782                 }
783             }
784         }
785     }
786 
787     /**
788      * Gets the FileContentInfo which describes the content-type, content-encoding
789      *
790      * @return The FileContentInfo.
791      * @throws FileSystemException if an error occurs.
792      */
793     @Override
794     public FileContentInfo getContentInfo() throws FileSystemException {
795         if (fileContentInfo == null) {
796             fileContentInfo = fileContentInfoFactory.create(this);
797         }
798 
799         return fileContentInfo;
800     }
801 
802     /**
803      * Writes this content to another FileContent.
804      *
805      * @param fileContent The target FileContent.
806      * @return the total number of bytes written
807      * @throws IOException if an error occurs writing the content.
808      * @since 2.1
809      */
810     @Override
811     public long write(final FileContent fileContent) throws IOException {
812         try (OutputStream output = fileContent.getOutputStream()) {
813             return this.write(output);
814         }
815     }
816 
817     /**
818      * Writes this content to another FileObject.
819      *
820      * @param file The target FileObject.
821      * @return the total number of bytes written
822      * @throws IOException if an error occurs writing the content.
823      * @since 2.1
824      */
825     @Override
826     public long write(final FileObject file) throws IOException {
827         return write(file.getContent());
828     }
829 
830     /**
831      * Writes this content to an OutputStream.
832      *
833      * @param output The target OutputStream.
834      * @return the total number of bytes written
835      * @throws IOException if an error occurs writing the content.
836      * @since 2.1
837      */
838     @Override
839     public long write(final OutputStream output) throws IOException {
840         return write(output, WRITE_BUFFER_SIZE);
841     }
842 
843     /**
844      * Writes this content to an OutputStream.
845      *
846      * @param output The target OutputStream.
847      * @param bufferSize The buffer size to write data chunks.
848      * @return the total number of bytes written
849      * @throws IOException if an error occurs writing the file.
850      * @since 2.1
851      */
852     @Override
853     public long write(final OutputStream output, final int bufferSize) throws IOException {
854         final InputStream input = this.getInputStream();
855         long count = 0;
856         try {
857             // This read/write code from Apache Commons IO
858             final byte[] buffer = new byte[bufferSize];
859             int n;
860             while (-1 != (n = input.read(buffer))) {
861                 output.write(buffer, 0, n);
862                 count += n;
863             }
864         } finally {
865             input.close();
866         }
867         return count;
868     }
869 }