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