blob: ebe3895946c7d55356aeb232b8a2146fffb6ac3a [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.vfs2.provider;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.cert.Certificate;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.vfs2.FileContent;
import org.apache.commons.vfs2.FileContentInfo;
import org.apache.commons.vfs2.FileContentInfoFactory;
import org.apache.commons.vfs2.FileObject;
import org.apache.commons.vfs2.FileSystemException;
import org.apache.commons.vfs2.RandomAccessContent;
import org.apache.commons.vfs2.util.MonitorInputStream;
import org.apache.commons.vfs2.util.MonitorOutputStream;
import org.apache.commons.vfs2.util.MonitorRandomAccessContent;
import org.apache.commons.vfs2.util.RandomAccessMode;
import org.apache.commons.vfs2.util.RawMonitorInputStream;
/**
* The content of a file.
*/
public final class DefaultFileContent implements FileContent {
/*
* static final int STATE_NONE = 0; static final int STATE_READING = 1; static final int STATE_WRITING = 2; static
* final int STATE_RANDOM_ACCESS = 3;
*/
private static final Certificate[] EMPTY_CERTIFICATE_ARRAY = new Certificate[0];
static final int STATE_CLOSED = 0;
static final int STATE_OPENED = 1;
/**
* The default buffer size for {@link #write(OutputStream)}
*/
private static final int WRITE_BUFFER_SIZE = 4096;
private final AbstractFileObject fileObject;
private Map<String, Object> attrs;
private Map<String, Object> roAttrs;
private FileContentInfo fileContentInfo;
private final FileContentInfoFactory fileContentInfoFactory;
private final ThreadLocal<FileContentThreadData> threadLocal = ThreadLocal.withInitial(FileContentThreadData::new);
private boolean resetAttributes;
/**
* Counts open streams for this file.
*/
private int openStreams;
public DefaultFileContent(final AbstractFileObject file, final FileContentInfoFactory fileContentInfoFactory) {
this.fileObject = file;
this.fileContentInfoFactory = fileContentInfoFactory;
}
private FileContentThreadData getFileContentThreadData() {
return this.threadLocal.get();
}
void streamOpened() {
synchronized (this) {
openStreams++;
}
((AbstractFileSystem) fileObject.getFileSystem()).streamOpened();
}
void streamClosed() {
synchronized (this) {
if (openStreams > 0) {
openStreams--;
if (openStreams < 1) {
fileObject.notifyAllStreamsClosed();
}
}
}
((AbstractFileSystem) fileObject.getFileSystem()).streamClosed();
}
/**
* Returns the file that this is the content of.
*
* @return the FileObject.
*/
@Override
public FileObject getFile() {
return fileObject;
}
/**
* Returns the size of the content (in bytes).
*
* @return The size of the content (in bytes).
* @throws FileSystemException if an error occurs.
*/
@Override
public long getSize() throws FileSystemException {
// Do some checking
if (!fileObject.getType().hasContent()) {
throw new FileSystemException("vfs.provider/get-size-not-file.error", fileObject);
}
/*
* if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
* new FileSystemException("vfs.provider/get-size-write.error", file); }
*/
try {
// Get the size
return fileObject.doGetContentSize();
} catch (final Exception exc) {
throw new FileSystemException("vfs.provider/get-size.error", exc, fileObject);
}
}
/**
* Returns the last-modified timestamp.
*
* @return The last modified timestamp.
* @throws FileSystemException if an error occurs.
*/
@Override
public long getLastModifiedTime() throws FileSystemException {
/*
* if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
* new FileSystemException("vfs.provider/get-last-modified-writing.error", file); }
*/
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/get-last-modified-no-exist.error", fileObject);
}
try {
return fileObject.doGetLastModifiedTime();
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/get-last-modified.error", fileObject, e);
}
}
/**
* Sets the last-modified timestamp.
*
* @param modTime The last modified timestamp.
* @throws FileSystemException if an error occurs.
*/
@Override
public void setLastModifiedTime(final long modTime) throws FileSystemException {
/*
* if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
* new FileSystemException("vfs.provider/set-last-modified-writing.error", file); }
*/
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/set-last-modified-no-exist.error", fileObject);
}
try {
if (!fileObject.doSetLastModifiedTime(modTime)) {
throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject);
}
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/set-last-modified.error", fileObject, e);
}
}
/**
* Checks if an attribute exists.
*
* @param attrName The name of the attribute to check.
* @return true if the attribute is associated with the file.
* @throws FileSystemException if an error occurs.
* @since 2.0
*/
@Override
public boolean hasAttribute(final String attrName) throws FileSystemException {
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/exists-attributes-no-exist.error", fileObject);
}
getAttributes();
return attrs.containsKey(attrName);
}
/**
* Returns a read-only map of this file's attributes.
*
* @return a Map of the file's attributes.
* @throws FileSystemException if an error occurs.
*/
@Override
public Map<String, Object> getAttributes() throws FileSystemException {
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/get-attributes-no-exist.error", fileObject);
}
if (resetAttributes || roAttrs == null) {
try {
synchronized (this) {
attrs = fileObject.doGetAttributes();
roAttrs = Collections.unmodifiableMap(attrs);
resetAttributes = false;
}
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/get-attributes.error", fileObject, e);
}
}
return roAttrs;
}
/**
* Used internally to flag situations where the file attributes should be reretrieved.
*
* @since 2.0
*/
public void resetAttributes() {
resetAttributes = true;
}
/**
* Lists the attributes of this file.
*
* @return An array of attribute names.
* @throws FileSystemException if an error occurs.
*/
@Override
public String[] getAttributeNames() throws FileSystemException {
getAttributes();
final Set<String> names = attrs.keySet();
return names.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
}
/**
* Gets the value of an attribute.
*
* @param attrName The attribute name.
* @return The value of the attribute or null.
* @throws FileSystemException if an error occurs.
*/
@Override
public Object getAttribute(final String attrName) throws FileSystemException {
getAttributes();
return attrs.get(attrName);
}
/**
* Sets the value of an attribute.
*
* @param attrName The name of the attribute to add.
* @param value The value of the attribute.
* @throws FileSystemException if an error occurs.
*/
@Override
public void setAttribute(final String attrName, final Object value) throws FileSystemException {
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/set-attribute-no-exist.error", attrName, fileObject);
}
try {
fileObject.doSetAttribute(attrName, value);
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/set-attribute.error", e, attrName, fileObject);
}
if (attrs != null) {
attrs.put(attrName, value);
}
}
/**
* Removes an attribute.
*
* @param attrName The name of the attribute to remove.
* @throws FileSystemException if an error occurs.
* @since 2.0
*/
@Override
public void removeAttribute(final String attrName) throws FileSystemException {
if (!fileObject.getType().hasAttributes()) {
throw new FileSystemException("vfs.provider/remove-attribute-no-exist.error", fileObject);
}
try {
fileObject.doRemoveAttribute(attrName);
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/remove-attribute.error", e, attrName, fileObject);
}
if (attrs != null) {
attrs.remove(attrName);
}
}
/**
* Returns the certificates used to sign this file.
*
* @return An array of Certificates.
* @throws FileSystemException if an error occurs.
*/
@Override
public Certificate[] getCertificates() throws FileSystemException {
if (!fileObject.exists()) {
throw new FileSystemException("vfs.provider/get-certificates-no-exist.error", fileObject);
}
/*
* if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
* new FileSystemException("vfs.provider/get-certificates-writing.error", file); }
*/
try {
final Certificate[] certs = fileObject.doGetCertificates();
if (certs != null) {
return certs;
}
return EMPTY_CERTIFICATE_ARRAY;
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/get-certificates.error", fileObject, e);
}
}
/**
* Returns an input stream for reading the content.
*
* @return The InputStream
* @throws FileSystemException if an error occurs.
*/
@Override
public InputStream getInputStream() throws FileSystemException {
return buildInputStream(0);
}
/**
* Returns an input stream for reading the content.
*
* @param bufferSize The buffer size to use.
* @return The InputStream
* @throws FileSystemException if an error occurs.
* @since 2.4
*/
@Override
public InputStream getInputStream(final int bufferSize) throws FileSystemException {
return buildInputStream(bufferSize);
}
/**
* Returns an input/output stream to use to read and write the content of the file in an random manner.
*
* @param mode The RandomAccessMode.
* @return A RandomAccessContent object to access the file.
* @throws FileSystemException if an error occurs.
*/
@Override
public RandomAccessContent getRandomAccessContent(final RandomAccessMode mode) throws FileSystemException {
/*
* if (getThreadData().getState() != STATE_NONE) { throw new
* FileSystemException("vfs.provider/read-in-use.error", file); }
*/
// Get the content
final RandomAccessContent rastr = fileObject.getRandomAccessContent(mode);
final FileRandomAccessContent rac = new FileRandomAccessContent(fileObject, rastr);
getFileContentThreadData().add(rac);
streamOpened();
return rac;
}
/**
* Returns an output stream for writing the content.
*
* @return The OutputStream for the file.
* @throws FileSystemException if an error occurs.
*/
@Override
public OutputStream getOutputStream() throws FileSystemException {
return getOutputStream(false);
}
/**
* Returns an output stream for writing the content in append mode.
*
* @param bAppend true if the data written should be appended.
* @return The OutputStream for the file.
* @throws FileSystemException if an error occurs.
*/
@Override
public OutputStream getOutputStream(final boolean bAppend) throws FileSystemException {
return buildOutputStream(bAppend, 0);
}
/**
* Returns an output stream for writing the content.
*
* @param bufferSize The buffer size to use.
* @return The OutputStream for the file.
* @throws FileSystemException if an error occurs.
* @since 2.4
*/
@Override
public OutputStream getOutputStream(final int bufferSize) throws FileSystemException {
return buildOutputStream(false, bufferSize);
}
/**
* Returns an output stream for writing the content in append mode.
*
* @param bAppend true if the data written should be appended.
* @param bufferSize The buffer size to use.
* @return The OutputStream for the file.
* @throws FileSystemException if an error occurs.
* @since 2.4
*/
@Override
public OutputStream getOutputStream(final boolean bAppend, final int bufferSize) throws FileSystemException {
return buildOutputStream(bAppend, bufferSize);
}
/**
* Closes all resources used by the content, including all streams, readers and writers.
*
* @throws FileSystemException if an error occurs.
*/
@Override
public void close() throws FileSystemException {
FileSystemException caught = null;
try {
final FileContentThreadData threadData = getFileContentThreadData();
// Close the input stream
while (threadData.hasInputStream()) {
final InputStream inputStream = threadData.removeInputStream(0);
try {
if (inputStream instanceof FileContentInputStream) {
((FileContentInputStream) inputStream).close();
} else if (inputStream instanceof RawFileContentInputStream) {
((RawFileContentInputStream) inputStream).close();
} else {
caught = new FileSystemException("Unsupported InputStream type: " + inputStream);
}
} catch (final FileSystemException ex) {
caught = ex;
}
}
// Close the randomAccess stream
while (threadData.hasRandomAccessContent()) {
final FileRandomAccessContent randomAccessContent = (FileRandomAccessContent) threadData
.removeRandomAccessContent(0);
try {
randomAccessContent.close();
} catch (final FileSystemException ex) {
caught = ex;
}
}
// Close the output stream
final FileContentOutputStream outputStream = threadData.getOutputStream();
if (outputStream != null) {
threadData.setOutputStream(null);
try {
outputStream.close();
} catch (final FileSystemException ex) {
caught = ex;
}
}
} finally {
threadLocal.remove();
}
// throw last error (out >> rac >> input) after all closes have been tried
if (caught != null) {
throw caught;
}
}
private InputStream buildInputStream(final int bufferSize) throws FileSystemException {
/*
* if (getThreadData().getState() == STATE_WRITING || getThreadData().getState() == STATE_RANDOM_ACCESS) { throw
* new FileSystemException("vfs.provider/read-in-use.error", file); }
*/
// Get the raw input stream
// @formatter:off
final InputStream inputStream = bufferSize == 0
? fileObject.getInputStream()
: fileObject.getInputStream(bufferSize);
// @formatter:on
// Double buffering may take place here.
// final InputStream wrappedInputStream = bufferSize == 0
// ? new FileContentInputStream(fileObject, inputStream)
// : new FileContentInputStream(fileObject, inputStream, bufferSize);
final InputStream wrappedInputStream;
if (inputStream instanceof BufferedInputStream) {
// Don't double buffer.
wrappedInputStream = new RawFileContentInputStream(fileObject, inputStream);
} else
{
// @formatter:off
wrappedInputStream = bufferSize == 0
? new FileContentInputStream(fileObject, inputStream)
: new FileContentInputStream(fileObject, inputStream, bufferSize);
// @formatter:on
}
getFileContentThreadData().add(wrappedInputStream);
streamOpened();
return wrappedInputStream;
}
private OutputStream buildOutputStream(final boolean bAppend, final int bufferSize) throws FileSystemException {
/*
* if (getThreadData().getState() != STATE_NONE)
*/
final FileContentThreadData threadData = getFileContentThreadData();
if (threadData.getOutputStream() != null) {
throw new FileSystemException("vfs.provider/write-in-use.error", fileObject);
}
// Get the raw output stream
final OutputStream outstr = fileObject.getOutputStream(bAppend);
// Create and set wrapper
final FileContentOutputStream wrapped = bufferSize == 0 ?
new FileContentOutputStream(fileObject, outstr) :
new FileContentOutputStream(fileObject, outstr, bufferSize);
threadData.setOutputStream(wrapped);
streamOpened();
return wrapped;
}
/**
* Handles the end of input stream.
*/
private void endInput(final InputStream instr) {
final FileContentThreadData fileContentThreadData = threadLocal.get();
if (fileContentThreadData != null) {
fileContentThreadData.remove(instr);
}
if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
// remove even when no value is set to remove key
threadLocal.remove();
}
streamClosed();
}
/**
* Handles the end of random access.
*/
private void endRandomAccess(final RandomAccessContent rac) {
final FileContentThreadData fileContentThreadData = threadLocal.get();
if (fileContentThreadData != null) {
fileContentThreadData.remove(rac);
}
if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
// remove even when no value is set to remove key
threadLocal.remove();
}
streamClosed();
}
/**
* Handles the end of output stream.
*/
private void endOutput() throws Exception {
final FileContentThreadData fileContentThreadData = threadLocal.get();
if (fileContentThreadData != null) {
fileContentThreadData.setOutputStream(null);
}
if (fileContentThreadData == null || !fileContentThreadData.hasStreams()) {
// remove even when no value is set to remove key
threadLocal.remove();
}
streamClosed();
fileObject.endOutput();
}
/**
* Checks if a input and/or output stream is open.
* <p>
* This checks only the scope of the current thread.
* </p>
*
* @return true if this is the case
*/
@Override
public boolean isOpen() {
final FileContentThreadData fileContentThreadData = threadLocal.get();
if (fileContentThreadData != null && fileContentThreadData.hasStreams()) {
return true;
}
// threadData.get() created empty entry
threadLocal.remove();
return false;
}
/**
* Checks if an input or output stream is open. This checks all threads.
*
* @return true if this is the case
*/
public boolean isOpenGlobal() {
synchronized (this) {
return openStreams > 0;
}
}
/**
* An input stream for reading content. Provides buffering, and end-of-stream monitoring.
*/
private final class FileContentInputStream extends MonitorInputStream {
// avoid gc
private final FileObject file;
FileContentInputStream(final FileObject file, final InputStream instr) {
super(instr);
this.file = file;
}
FileContentInputStream(final FileObject file, final InputStream instr, final int bufferSize) {
super(instr, bufferSize);
this.file = file;
}
/**
* Closes this input stream.
*/
@Override
public void close() throws FileSystemException {
try {
super.close();
} catch (final IOException e) {
throw new FileSystemException("vfs.provider/close-instr.error", file, e);
}
}
/**
* Called after the stream has been closed.
*/
@Override
protected void onClose() throws IOException {
try {
super.onClose();
} finally {
endInput(this);
}
}
}
/**
* An input stream for reading content. Provides buffering, and end-of-stream monitoring.
* <p>
* This is the same as {@link FileContentInputStream} but without the buffering.
* </p>
*/
private final class RawFileContentInputStream extends RawMonitorInputStream {
// avoid gc
private final FileObject file;
RawFileContentInputStream(final FileObject file, final InputStream instr) {
super(instr);
this.file = file;
}
/**
* Closes this input stream.
*/
@Override
public void close() throws FileSystemException {
try {
super.close();
} catch (final IOException e) {
throw new FileSystemException("vfs.provider/close-instr.error", file, e);
}
}
/**
* Called after the stream has been closed.
*/
@Override
protected void onClose() throws IOException {
try {
super.onClose();
} finally {
endInput(this);
}
}
}
/**
* An input/output stream for reading/writing content on random positions
*/
private final class FileRandomAccessContent extends MonitorRandomAccessContent {
// also avoids gc
private final FileObject file;
FileRandomAccessContent(final FileObject file, final RandomAccessContent content) {
super(content);
this.file = file;
}
/**
* Called after the stream has been closed.
*/
@Override
protected void onClose() throws IOException {
try {
super.onClose();
} finally {
endRandomAccess(this);
}
}
@Override
public void close() throws FileSystemException {
try {
super.close();
} catch (final IOException e) {
throw new FileSystemException("vfs.provider/close-rac.error", file, e);
}
}
}
/**
* An output stream for writing content.
*/
final class FileContentOutputStream extends MonitorOutputStream {
// avoid gc
private final FileObject file;
FileContentOutputStream(final FileObject file, final OutputStream outstr) {
super(outstr);
this.file = file;
}
FileContentOutputStream(final FileObject file, final OutputStream outstr, final int bufferSize) {
super(outstr, bufferSize);
this.file = file;
}
/**
* Closes this output stream.
*/
@Override
public void close() throws FileSystemException {
try {
super.close();
} catch (final IOException e) {
throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
}
}
/**
* Called after this stream is closed.
*/
@Override
protected void onClose() throws IOException {
try {
super.onClose();
} finally {
try {
endOutput();
} catch (final Exception e) {
throw new FileSystemException("vfs.provider/close-outstr.error", file, e);
}
}
}
}
/**
* Gets the FileContentInfo which describes the content-type, content-encoding
*
* @return The FileContentInfo.
* @throws FileSystemException if an error occurs.
*/
@Override
public FileContentInfo getContentInfo() throws FileSystemException {
if (fileContentInfo == null) {
fileContentInfo = fileContentInfoFactory.create(this);
}
return fileContentInfo;
}
/**
* Writes this content to another FileContent.
*
* @param fileContent The target FileContent.
* @return the total number of bytes written
* @throws IOException if an error occurs writing the content.
* @since 2.1
*/
@Override
public long write(final FileContent fileContent) throws IOException {
try (OutputStream output = fileContent.getOutputStream()) {
return this.write(output);
}
}
/**
* Writes this content to another FileObject.
*
* @param file The target FileObject.
* @return the total number of bytes written
* @throws IOException if an error occurs writing the content.
* @since 2.1
*/
@Override
public long write(final FileObject file) throws IOException {
return write(file.getContent());
}
/**
* Writes this content to an OutputStream.
*
* @param output The target OutputStream.
* @return the total number of bytes written
* @throws IOException if an error occurs writing the content.
* @since 2.1
*/
@Override
public long write(final OutputStream output) throws IOException {
return write(output, WRITE_BUFFER_SIZE);
}
/**
* Writes this content to an OutputStream.
*
* @param output The target OutputStream.
* @param bufferSize The buffer size to write data chunks.
* @return the total number of bytes written
* @throws IOException if an error occurs writing the file.
* @since 2.1
*/
@Override
public long write(final OutputStream output, final int bufferSize) throws IOException {
final InputStream input = this.getInputStream();
long count = 0;
try {
// This read/write code from Apache Commons IO
final byte[] buffer = new byte[bufferSize];
int n;
while (-1 != (n = input.read(buffer))) {
output.write(buffer, 0, n);
count += n;
}
} finally {
input.close();
}
return count;
}
}