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.IOException;
020import java.io.InputStream;
021import java.io.OutputStream;
022import java.security.cert.Certificate;
023import java.util.Collections;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.commons.vfs2.FileContent;
028import org.apache.commons.vfs2.FileContentInfo;
029import org.apache.commons.vfs2.FileContentInfoFactory;
030import org.apache.commons.vfs2.FileObject;
031import org.apache.commons.vfs2.FileSystemException;
032import org.apache.commons.vfs2.RandomAccessContent;
033import org.apache.commons.vfs2.util.MonitorInputStream;
034import org.apache.commons.vfs2.util.MonitorOutputStream;
035import org.apache.commons.vfs2.util.MonitorRandomAccessContent;
036import org.apache.commons.vfs2.util.RandomAccessMode;
037
038/**
039 * The content of a file.
040 */
041public final class DefaultFileContent implements FileContent {
042    
043    /*
044     * static final int STATE_NONE = 0; static final int STATE_READING = 1; static final int STATE_WRITING = 2; static
045     * final int STATE_RANDOM_ACCESS = 3;
046     */
047
048    static final int STATE_CLOSED = 0;
049    static final int STATE_OPENED = 1;
050
051    /**
052     * The default buffer size for {@link #write(OutputStream)}
053     */
054    private static final int WRITE_BUFFER_SIZE = 4096;
055
056    private final AbstractFileObject fileObject;
057    private Map<String, Object> attrs;
058    private Map<String, Object> roAttrs;
059    private FileContentInfo fileContentInfo;
060    private final FileContentInfoFactory fileContentInfoFactory;
061
062    private final ThreadLocal<FileContentThreadData> threadLocal = new ThreadLocal<>();
063    private boolean resetAttributes;
064
065    /**
066     * Counts open streams for this file.
067     */
068    private int openStreams;
069
070    public DefaultFileContent(final AbstractFileObject file, final FileContentInfoFactory fileContentInfoFactory) {
071        this.fileObject = file;
072        this.fileContentInfoFactory = fileContentInfoFactory;
073    }
074
075    private FileContentThreadData getOrCreateThreadData() {
076        FileContentThreadData data = this.threadLocal.get();
077        if (data == null) {
078            data = new FileContentThreadData();
079            this.threadLocal.set(data);
080        }
081        return data;
082    }
083
084    void streamOpened() {
085        synchronized (this) {
086            openStreams++;
087        }
088        ((AbstractFileSystem) fileObject.getFileSystem()).streamOpened();
089    }
090
091    void streamClosed() {
092        synchronized (this) {
093            if (openStreams > 0) {
094                openStreams--;
095                if (openStreams < 1) {
096                    fileObject.notifyAllStreamsClosed();
097                }
098            }
099        }
100        ((AbstractFileSystem) fileObject.getFileSystem()).streamClosed();
101    }
102
103    /**
104     * Returns the file that this is the content of.
105     *
106     * @return the FileObject.
107     */
108    @Override
109    public FileObject getFile() {
110        return fileObject;
111    }
112
113    /**
114     * Returns the size of the content (in bytes).
115     *
116     * @return The size of the content (in bytes).
117     * @throws FileSystemException if an error occurs.
118     */
119    @Override
120    public long getSize() throws FileSystemException {
121        // Do some checking
122        if (!fileObject.getType().hasContent()) {
123            throw new FileSystemException("vfs.provider/get-size-not-file.error", fileObject);
124        }
125        /*
126         * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
127         * new FileSystemException("vfs.provider/get-size-write.error", file); }
128         */
129
130        try {
131            // Get the size
132            return fileObject.doGetContentSize();
133        } catch (final Exception exc) {
134            throw new FileSystemException("vfs.provider/get-size.error", exc, fileObject);
135        }
136    }
137
138    /**
139     * Returns the last-modified timestamp.
140     *
141     * @return The last modified timestamp.
142     * @throws FileSystemException if an error occurs.
143     */
144    @Override
145    public long getLastModifiedTime() throws FileSystemException {
146        /*
147         * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
148         * new FileSystemException("vfs.provider/get-last-modified-writing.error", file); }
149         */
150        if (!fileObject.getType().hasAttributes()) {
151            throw new FileSystemException("vfs.provider/get-last-modified-no-exist.error", fileObject);
152        }
153        try {
154            return fileObject.doGetLastModifiedTime();
155        } catch (final Exception e) {
156            throw new FileSystemException("vfs.provider/get-last-modified.error", fileObject, e);
157        }
158    }
159
160    /**
161     * Sets the last-modified timestamp.
162     *
163     * @param modTime The last modified timestamp.
164     * @throws FileSystemException if an error occurs.
165     */
166    @Override
167    public void setLastModifiedTime(final long modTime) throws FileSystemException {
168        /*
169         * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
170         * new FileSystemException("vfs.provider/set-last-modified-writing.error", file); }
171         */
172        if (!fileObject.getType().hasAttributes()) {
173            throw new FileSystemException("vfs.provider/set-last-modified-no-exist.error", fileObject);
174        }
175        try {
176            if (!fileObject.doSetLastModifiedTime(modTime)) {
177                throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject);
178            }
179        } catch (final Exception e) {
180            throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject, e);
181        }
182    }
183
184    /**
185     * Checks if an attribute exists.
186     *
187     * @param attrName The name of the attribute to check.
188     * @return true if the attribute is associated with the file.
189     * @throws FileSystemException if an error occurs.
190     * @since 2.0
191     */
192    @Override
193    public boolean hasAttribute(final String attrName) throws FileSystemException {
194        if (!fileObject.getType().hasAttributes()) {
195            throw new FileSystemException("vfs.provider/exists-attributes-no-exist.error", fileObject);
196        }
197        getAttributes();
198        return attrs.containsKey(attrName);
199    }
200
201    /**
202     * Returns a read-only map of this file's attributes.
203     *
204     * @return a Map of the file's attributes.
205     * @throws FileSystemException if an error occurs.
206     */
207    @Override
208    public Map<String, Object> getAttributes() throws FileSystemException {
209        if (!fileObject.getType().hasAttributes()) {
210            throw new FileSystemException("vfs.provider/get-attributes-no-exist.error", fileObject);
211        }
212        if (resetAttributes || roAttrs == null) {
213            try {
214                synchronized (this) {
215                    attrs = fileObject.doGetAttributes();
216                    roAttrs = Collections.unmodifiableMap(attrs);
217                    resetAttributes = false;
218                }
219            } catch (final Exception e) {
220                throw new FileSystemException("vfs.provider/get-attributes.error", fileObject, e);
221            }
222        }
223        return roAttrs;
224    }
225
226    /**
227     * Used internally to flag situations where the file attributes should be reretrieved.
228     *
229     * @since 2.0
230     */
231    public void resetAttributes() {
232        resetAttributes = true;
233    }
234
235    /**
236     * Lists the attributes of this file.
237     *
238     * @return An array of attribute names.
239     * @throws FileSystemException if an error occurs.
240     */
241    @Override
242    public String[] getAttributeNames() throws FileSystemException {
243        getAttributes();
244        final Set<String> names = attrs.keySet();
245        return names.toArray(new String[names.size()]);
246    }
247
248    /**
249     * Gets the value of an attribute.
250     *
251     * @param attrName The attribute name.
252     * @return The value of the attribute or null.
253     * @throws FileSystemException if an error occurs.
254     */
255    @Override
256    public Object getAttribute(final String attrName) throws FileSystemException {
257        getAttributes();
258        return attrs.get(attrName);
259    }
260
261    /**
262     * Sets the value of an attribute.
263     *
264     * @param attrName The name of the attribute to add.
265     * @param value The value of the attribute.
266     * @throws FileSystemException if an error occurs.
267     */
268    @Override
269    public void setAttribute(final String attrName, final Object value) throws FileSystemException {
270        if (!fileObject.getType().hasAttributes()) {
271            throw new FileSystemException("vfs.provider/set-attribute-no-exist.error", attrName, fileObject);
272        }
273        try {
274            fileObject.doSetAttribute(attrName, value);
275        } catch (final Exception e) {
276            throw new FileSystemException("vfs.provider/set-attribute.error", e, attrName, fileObject);
277        }
278
279        if (attrs != null) {
280            attrs.put(attrName, value);
281        }
282    }
283
284    /**
285     * Removes an attribute.
286     *
287     * @param attrName The name of the attribute to remove.
288     * @throws FileSystemException if an error occurs.
289     * @since 2.0
290     */
291    @Override
292    public void removeAttribute(final String attrName) throws FileSystemException {
293        if (!fileObject.getType().hasAttributes()) {
294            throw new FileSystemException("vfs.provider/remove-attribute-no-exist.error", fileObject);
295        }
296
297        try {
298            fileObject.doRemoveAttribute(attrName);
299        } catch (final Exception e) {
300            throw new FileSystemException("vfs.provider/remove-attribute.error", e, attrName, fileObject);
301        }
302
303        if (attrs != null) {
304            attrs.remove(attrName);
305        }
306    }
307
308    /**
309     * Returns the certificates used to sign this file.
310     *
311     * @return An array of Certificates.
312     * @throws FileSystemException if an error occurs.
313     */
314    @Override
315    public Certificate[] getCertificates() throws FileSystemException {
316        if (!fileObject.exists()) {
317            throw new FileSystemException("vfs.provider/get-certificates-no-exist.error", fileObject);
318        }
319        /*
320         * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
321         * new FileSystemException("vfs.provider/get-certificates-writing.error", file); }
322         */
323
324        try {
325            final Certificate[] certs = fileObject.doGetCertificates();
326            if (certs != null) {
327                return certs;
328            }
329            return new Certificate[0];
330        } catch (final Exception e) {
331            throw new FileSystemException("vfs.provider/get-certificates.error", fileObject, e);
332        }
333    }
334
335    /**
336     * Returns an input stream for reading the content.
337     *
338     * @return The InputStream
339     * @throws FileSystemException if an error occurs.
340     */
341    @Override
342    public InputStream getInputStream() throws FileSystemException {
343        /*
344         * if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
345         * new FileSystemException("vfs.provider/read-in-use.error", file); }
346         */
347
348        // Get the raw input stream
349        final InputStream inputStream = fileObject.getInputStream();
350
351        final InputStream wrappedInputStream = new FileContentInputStream(fileObject, inputStream);
352
353        getOrCreateThreadData().addInstr(wrappedInputStream);
354        streamOpened();
355
356        return wrappedInputStream;
357    }
358
359    /**
360     * Returns an input/output stream to use to read and write the content of the file in an random manner.
361     *
362     * @param mode The RandomAccessMode.
363     * @return A RandomAccessContent object to access the file.
364     * @throws FileSystemException if an error occurs.
365     */
366    @Override
367    public RandomAccessContent getRandomAccessContent(final RandomAccessMode mode) throws FileSystemException {
368        /*
369         * if (getThreadData().getState() != STATE_NONE) { throw new
370         * FileSystemException("vfs.provider/read-in-use.error", file); }
371         */
372
373        // Get the content
374        final RandomAccessContent rastr = fileObject.getRandomAccessContent(mode);
375
376        final FileRandomAccessContent rac = new FileRandomAccessContent(fileObject, rastr);
377
378        getOrCreateThreadData().addRastr(rac);
379        streamOpened();
380
381        return rac;
382    }
383
384    /**
385     * Returns an output stream for writing the content.
386     *
387     * @return The OutputStream for the file.
388     * @throws FileSystemException if an error occurs.
389     */
390    @Override
391    public OutputStream getOutputStream() throws FileSystemException {
392        return getOutputStream(false);
393    }
394
395    /**
396     * Returns an output stream for writing the content in append mode.
397     *
398     * @param bAppend true if the data written should be appended.
399     * @return The OutputStream for the file.
400     * @throws FileSystemException if an error occurs.
401     */
402    @Override
403    public OutputStream getOutputStream(final boolean bAppend) throws FileSystemException {
404        /*
405         * if (getThreadData().getState() != STATE_NONE)
406         */
407        final FileContentThreadData streams = getOrCreateThreadData();
408        if (streams.getOutstr() != null) {
409            throw new FileSystemException("vfs.provider/write-in-use.error", fileObject);
410        }
411
412        // Get the raw output stream
413        final OutputStream outstr = fileObject.getOutputStream(bAppend);
414
415        // Create and set wrapper
416        final FileContentOutputStream wrapped = new FileContentOutputStream(fileObject, outstr);
417        streams.setOutstr(wrapped);
418        streamOpened();
419
420        return wrapped;
421    }
422
423    /**
424     * Closes all resources used by the content, including all streams, readers and writers.
425     *
426     * @throws FileSystemException if an error occurs.
427     */
428    @Override
429    public void close() throws FileSystemException {
430        FileSystemException caught = null;
431        try {
432            final FileContentThreadData fileContentThreadData = getOrCreateThreadData();
433
434            // Close the input stream
435            while (fileContentThreadData.getInstrsSize() > 0) {
436                final FileContentInputStream inputStream = (FileContentInputStream) fileContentThreadData
437                        .removeInstr(0);
438                try {
439                    inputStream.close();
440                } catch (final FileSystemException ex) {
441                    caught = ex;
442
443                }
444            }
445
446            // Close the randomAccess stream
447            while (fileContentThreadData.getRastrsSize() > 0) {
448                final FileRandomAccessContent randomAccessContent = (FileRandomAccessContent) fileContentThreadData
449                        .removeRastr(0);
450                try {
451                    randomAccessContent.close();
452                } catch (final FileSystemException ex) {
453                    caught = ex;
454                }
455            }
456
457            // Close the output stream
458            final FileContentOutputStream outputStream = fileContentThreadData.getOutstr();
459            if (outputStream != null) {
460                fileContentThreadData.setOutstr(null);
461                try {
462                    outputStream.close();
463                } catch (final FileSystemException ex) {
464                    caught = ex;
465                }
466            }
467        } finally {
468            threadLocal.remove();
469        }
470
471        // throw last error (out >> rac >> input) after all closes have been tried
472        if (caught != null) {
473            throw caught;
474        }
475    }
476
477    /**
478     * Handles the end of input stream.
479     */
480    private void endInput(final FileContentInputStream instr) {
481        final FileContentThreadData fileContentThreadData = threadLocal.get();
482        if (fileContentThreadData != null) {
483            fileContentThreadData.removeInstr(instr);
484        }
485        if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
486            // remove even when no value is set to remove key
487            threadLocal.remove();
488        }
489        streamClosed();
490    }
491
492    /**
493     * Handles the end of random access.
494     */
495    private void endRandomAccess(final RandomAccessContent rac) {
496        final FileContentThreadData fileContentThreadData = threadLocal.get();
497        if (fileContentThreadData != null) {
498            fileContentThreadData.removeRastr(rac);
499        }
500        if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
501            // remove even when no value is set to remove key
502            threadLocal.remove();
503        }
504        streamClosed();
505    }
506
507    /**
508     * Handles the end of output stream.
509     */
510    private void endOutput() throws Exception {
511        final FileContentThreadData fileContentThreadData = threadLocal.get();
512        if (fileContentThreadData != null) {
513            fileContentThreadData.setOutstr(null);
514        }
515        if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
516            // remove even when no value is set to remove key
517            threadLocal.remove();
518        }
519        streamClosed();
520        fileObject.endOutput();
521    }
522
523    /**
524     * Checks if a input and/or output stream is open.
525     * <p>
526     * This checks only the scope of the current thread.
527     *
528     * @return true if this is the case
529     */
530    @Override
531    public boolean isOpen() {
532        final FileContentThreadData fileContentThreadData = threadLocal.get();
533        if (fileContentThreadData != null && fileContentThreadData.hasStreams()) {
534            return true;
535        }
536        // threadData.get() created empty entry
537        threadLocal.remove();
538        return false;
539    }
540
541    /**
542     * Checks if an input or output stream is open. This checks all threads.
543     *
544     * @return true if this is the case
545     */
546    public boolean isOpenGlobal() {
547        synchronized (this) {
548            return openStreams > 0;
549        }
550    }
551
552    /**
553     * An input stream for reading content. Provides buffering, and end-of-stream monitoring.
554     */
555    private final class FileContentInputStream extends MonitorInputStream {
556        // avoid gc
557        private final FileObject file;
558
559        FileContentInputStream(final FileObject file, final InputStream instr) {
560            super(instr);
561            this.file = file;
562        }
563
564        /**
565         * Closes this input stream.
566         */
567        @Override
568        public void close() throws FileSystemException {
569            try {
570                super.close();
571            } catch (final IOException e) {
572                throw new FileSystemException("vfs.provider/close-instr.error", file, e);
573            }
574        }
575
576        /**
577         * Called after the stream has been closed.
578         */
579        @Override
580        protected void onClose() throws IOException {
581            try {
582                super.onClose();
583            } finally {
584                endInput(this);
585            }
586        }
587    }
588
589    /**
590     * An input/output stream for reading/writing content on random positions
591     */
592    private final class FileRandomAccessContent extends MonitorRandomAccessContent {
593        // also avoids gc
594        private final FileObject file;
595
596        FileRandomAccessContent(final FileObject file, final RandomAccessContent content) {
597            super(content);
598            this.file = file;
599        }
600
601        /**
602         * Called after the stream has been closed.
603         */
604        @Override
605        protected void onClose() throws IOException {
606            try {
607                super.onClose();
608            } finally {
609                endRandomAccess(this);
610            }
611        }
612
613        @Override
614        public void close() throws FileSystemException {
615            try {
616                super.close();
617            } catch (final IOException e) {
618                throw new FileSystemException("vfs.provider/close-rac.error", file, e);
619            }
620        }
621    }
622
623    /**
624     * An output stream for writing content.
625     */
626    final class FileContentOutputStream extends MonitorOutputStream {
627        // avoid gc
628        private final FileObject file;
629
630        FileContentOutputStream(final FileObject file, final OutputStream outstr) {
631            super(outstr);
632            this.file = file;
633        }
634
635        /**
636         * Closes this output stream.
637         */
638        @Override
639        public void close() throws FileSystemException {
640            try {
641                super.close();
642            } catch (final IOException e) {
643                throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
644            }
645        }
646
647        /**
648         * Called after this stream is closed.
649         */
650        @Override
651        protected void onClose() throws IOException {
652            try {
653                super.onClose();
654            } finally {
655                try {
656                    endOutput();
657                } catch (final Exception e) {
658                    throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
659                }
660            }
661        }
662    }
663
664    /**
665     * Gets the FileContentInfo which describes the content-type, content-encoding
666     *
667     * @return The FileContentInfo.
668     * @throws FileSystemException if an error occurs.
669     */
670    @Override
671    public FileContentInfo getContentInfo() throws FileSystemException {
672        if (fileContentInfo == null) {
673            fileContentInfo = fileContentInfoFactory.create(this);
674        }
675
676        return fileContentInfo;
677    }
678
679    /**
680     * Writes this content to another FileContent.
681     *
682     * @param fileContent The target FileContent.
683     * @return the total number of bytes written
684     * @throws IOException if an error occurs writing the content.
685     * @since 2.1
686     */
687    @Override
688    public long write(final FileContent fileContent) throws IOException {
689        final OutputStream output = fileContent.getOutputStream();
690        try {
691            return this.write(output);
692        } finally {
693            output.close();
694        }
695    }
696
697    /**
698     * Writes this content to another FileObject.
699     *
700     * @param file The target FileObject.
701     * @return the total number of bytes written
702     * @throws IOException if an error occurs writing the content.
703     * @since 2.1
704     */
705    @Override
706    public long write(final FileObject file) throws IOException {
707        return write(file.getContent());
708    }
709
710    /**
711     * Writes this content to an OutputStream.
712     *
713     * @param output The target OutputStream.
714     * @return the total number of bytes written
715     * @throws IOException if an error occurs writing the content.
716     * @since 2.1
717     */
718    @Override
719    public long write(final OutputStream output) throws IOException {
720        return write(output, WRITE_BUFFER_SIZE);
721    }
722
723    /**
724     * Writes this content to an OutputStream.
725     *
726     * @param output The target OutputStream.
727     * @param bufferSize The buffer size to write data chunks.
728     * @return the total number of bytes written
729     * @throws IOException if an error occurs writing the file.
730     * @since 2.1
731     */
732    @Override
733    public long write(final OutputStream output, final int bufferSize) throws IOException {
734        final InputStream input = this.getInputStream();
735        long count = 0;
736        try {
737            // This read/write code from Apache Commons IO
738            final byte[] buffer = new byte[bufferSize];
739            int n = 0;
740            while (-1 != (n = input.read(buffer))) {
741                output.write(buffer, 0, n);
742                count += n;
743            }
744        } finally {
745            input.close();
746        }
747        return count;
748    }
749}