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.lang3.ArrayUtils;
029import org.apache.commons.vfs2.FileContent;
030import org.apache.commons.vfs2.FileContentInfo;
031import org.apache.commons.vfs2.FileContentInfoFactory;
032import org.apache.commons.vfs2.FileObject;
033import org.apache.commons.vfs2.FileSystemException;
034import org.apache.commons.vfs2.RandomAccessContent;
035import org.apache.commons.vfs2.util.MonitorInputStream;
036import org.apache.commons.vfs2.util.MonitorOutputStream;
037import org.apache.commons.vfs2.util.MonitorRandomAccessContent;
038import org.apache.commons.vfs2.util.RandomAccessMode;
039import org.apache.commons.vfs2.util.RawMonitorInputStream;
040
041/**
042 * The content of a file.
043 */
044public final class DefaultFileContent implements FileContent {
045
046    /*
047     * static final int STATE_NONE = 0; static final int STATE_READING = 1; static final int STATE_WRITING = 2; static
048     * final int STATE_RANDOM_ACCESS = 3;
049     */
050
051    /**
052     * An input stream for reading content. Provides buffering, and end-of-stream monitoring.
053     */
054    private final class FileContentInputStream extends MonitorInputStream {
055        // avoid gc
056        private final FileObject file;
057
058        FileContentInputStream(final FileObject file, final InputStream instr) {
059            super(instr);
060            this.file = file;
061        }
062
063        FileContentInputStream(final FileObject file, final InputStream instr, final int bufferSize) {
064            super(instr, bufferSize);
065            this.file = file;
066        }
067
068        /**
069         * Closes this input stream.
070         */
071        @Override
072        public void close() throws FileSystemException {
073            try {
074                super.close();
075            } catch (final IOException e) {
076                throw new FileSystemException("vfs.provider/close-instr.error", file, e);
077            }
078        }
079
080        /**
081         * Called after the stream has been closed.
082         */
083        @Override
084        protected void onClose() throws IOException {
085            try {
086                super.onClose();
087            } finally {
088                endInput(this);
089            }
090        }
091    }
092    /**
093     * An output stream for writing content.
094     */
095    final class FileContentOutputStream extends MonitorOutputStream {
096        // avoid gc
097        private final FileObject file;
098
099        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}