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