001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.vfs2.provider;
018
019import java.io.BufferedInputStream;
020import java.io.IOException;
021import java.io.InputStream;
022import java.io.OutputStream;
023import java.security.cert.Certificate;
024import java.util.Collections;
025import java.util.Map;
026import java.util.Set;
027
028import org.apache.commons.io.IOUtils;
029import org.apache.commons.lang3.ArrayUtils;
030import org.apache.commons.vfs2.FileContent;
031import org.apache.commons.vfs2.FileContentInfo;
032import org.apache.commons.vfs2.FileContentInfoFactory;
033import org.apache.commons.vfs2.FileObject;
034import org.apache.commons.vfs2.FileSystemException;
035import org.apache.commons.vfs2.RandomAccessContent;
036import org.apache.commons.vfs2.util.MonitorInputStream;
037import org.apache.commons.vfs2.util.MonitorOutputStream;
038import org.apache.commons.vfs2.util.MonitorRandomAccessContent;
039import org.apache.commons.vfs2.util.RandomAccessMode;
040import org.apache.commons.vfs2.util.RawMonitorInputStream;
041
042/**
043 * The content of a file.
044 */
045public final class DefaultFileContent implements FileContent {
046
047    /*
048     * static final int STATE_NONE = 0; static final int STATE_READING = 1; static final int STATE_WRITING = 2; static
049     * final int STATE_RANDOM_ACCESS = 3;
050     */
051
052    /**
053     * An input stream for reading content. Provides buffering, and end-of-stream monitoring.
054     */
055    private final class FileContentInputStream extends MonitorInputStream {
056        // avoid gc
057        private final FileObject file;
058
059        FileContentInputStream(final FileObject file, final InputStream instr) {
060            super(instr);
061            this.file = file;
062        }
063
064        FileContentInputStream(final FileObject file, final InputStream instr, final int bufferSize) {
065            super(instr, bufferSize);
066            this.file = file;
067        }
068
069        /**
070         * Closes this input stream.
071         */
072        @Override
073        public void close() throws FileSystemException {
074            try {
075                super.close();
076            } catch (final IOException e) {
077                throw new FileSystemException("vfs.provider/close-instr.error", file, e);
078            }
079        }
080
081        /**
082         * Called after the stream has been closed.
083         */
084        @Override
085        protected void onClose() throws IOException {
086            try {
087                super.onClose();
088            } finally {
089                endInput(this);
090            }
091        }
092    }
093    /**
094     * An output stream for writing content.
095     */
096    final class FileContentOutputStream extends MonitorOutputStream {
097        // avoid gc
098        private final FileObject file;
099
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}