org.postgresql.largeobject.BlobInputStream Maven / Gradle / Ivy
Show all versions of org.apache.servicemix.bundles.postgresql
/*
* Copyright (c) 2003, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.largeobject;
import org.postgresql.jdbc.ResourceLock;
import org.postgresql.util.GT;
import org.checkerframework.checker.nullness.qual.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
/**
* This is an implementation of an InputStream from a large object.
*/
public class BlobInputStream extends InputStream {
static final int DEFAULT_MAX_BUFFER_SIZE = 512 * 1024;
static final int INITIAL_BUFFER_SIZE = 64 * 1024;
/**
* The parent LargeObject.
*/
private @Nullable LargeObject lo;
private final ResourceLock lock = new ResourceLock();
/**
* The absolute position.
*/
private long absolutePosition;
/**
* Buffer used to improve performance.
*/
private byte @Nullable [] buffer;
/**
* Position within buffer.
*/
private int bufferPosition;
/**
* The amount of bytes to read on the next read.
* Currently, we nullify {@link #buffer}, so we can't use {@code buffer.length}.
*/
private int lastBufferSize;
/**
* The buffer size.
*/
private final int maxBufferSize;
/**
* The mark position.
*/
private long markPosition;
/**
* The limit.
*/
private final long limit;
/**
* @param lo LargeObject to read from
*/
public BlobInputStream(LargeObject lo) {
this(lo, DEFAULT_MAX_BUFFER_SIZE);
}
/**
* @param lo LargeObject to read from
* @param bsize buffer size
*/
public BlobInputStream(LargeObject lo, int bsize) {
this(lo, bsize, Long.MAX_VALUE);
}
/**
* @param lo LargeObject to read from
* @param bsize buffer size
* @param limit max number of bytes to read
*/
public BlobInputStream(LargeObject lo, int bsize, long limit) {
this.lo = lo;
this.maxBufferSize = bsize;
// The very first read multiplies the last buffer size by two, so we divide by two to get
// the first read to be exactly the initial buffer size
this.lastBufferSize = INITIAL_BUFFER_SIZE / 2;
// Treat -1 as no limit for backward compatibility
this.limit = limit == -1 ? Long.MAX_VALUE : limit;
}
/**
* The minimum required to implement input stream.
*/
@Override
public int read() throws IOException {
try (ResourceLock ignore = lock.obtain()) {
LargeObject lo = getLo();
if (absolutePosition >= limit) {
buffer = null;
bufferPosition = 0;
return -1;
}
// read more in if necessary
if (buffer == null || bufferPosition >= buffer.length) {
// Don't hold the buffer while waiting for DB to respond
// Note: lo.read(...) does not support "fetching the response into the user-provided buffer"
// See https://github.com/pgjdbc/pgjdbc/issues/3043
int nextBufferSize = getNextBufferSize(1);
buffer = lo.read(nextBufferSize);
bufferPosition = 0;
if (buffer.length == 0) {
// The lob does not produce any more data, so we are at the end of the stream
return -1;
}
}
int ret = buffer[bufferPosition] & 0xFF;
bufferPosition++;
absolutePosition++;
if (bufferPosition >= buffer.length) {
// TODO: support buffer reuse in mark/reset
buffer = null;
bufferPosition = 0;
}
return ret;
} catch (SQLException e) {
long loId = lo == null ? -1 : lo.getLongOID();
throw new IOException(
GT.tr("Can not read data from large object {0}, position: {1}, buffer size: {2}",
loId, absolutePosition, lastBufferSize),
e);
}
}
/**
* Computes the next buffer size to use for reading data from the large object.
* The idea is to avoid allocating too much memory, especially if the user will use just a few
* bytes of the data.
* @param len estimated read request
* @return next buffer size or {@link #maxBufferSize} if the buffer should not be increased
*/
private int getNextBufferSize(int len) {
int nextBufferSize = Math.min(maxBufferSize, this.lastBufferSize * 2);
if (len > nextBufferSize) {
nextBufferSize = Math.min(maxBufferSize, Integer.highestOneBit(len * 2));
}
this.lastBufferSize = nextBufferSize;
return nextBufferSize;
}
@Override
public int read(byte[] dest, int off, int len) throws IOException {
if (len == 0) {
return 0;
}
try (ResourceLock ignore = lock.obtain()) {
int bytesCopied = 0;
LargeObject lo = getLo();
// Check to make sure we aren't at the limit.
if (absolutePosition >= limit) {
return -1;
}
// Check to make sure we are not going to read past the limit
len = Math.min(len, (int) Math.min(limit - absolutePosition, Integer.MAX_VALUE));
// have we read anything into the buffer
if (buffer != null) {
// now figure out how much data is in the buffer
int bytesInBuffer = buffer.length - bufferPosition;
// figure out how many bytes the user wants
int bytesToCopy = Math.min(len, bytesInBuffer);
// copy them in
System.arraycopy(buffer, bufferPosition, dest, off, bytesToCopy);
// move the buffer position
bufferPosition += bytesToCopy;
if (bufferPosition >= buffer.length) {
// TODO: support buffer reuse in mark/reset
buffer = null;
bufferPosition = 0;
}
// position in the blob
absolutePosition += bytesToCopy;
// increment offset
off += bytesToCopy;
// decrement the length
len -= bytesToCopy;
bytesCopied = bytesToCopy;
}
if (len > 0) {
int nextBufferSize = getNextBufferSize(len);
// We are going to read data past the existing buffer, so we release the memory
// before making a DB call
buffer = null;
bufferPosition = 0;
int bytesRead;
try {
if (len >= nextBufferSize) {
// Read directly into the user's buffer
bytesRead = lo.read(dest, off, len);
} else {
// Refill the buffer and copy from it
buffer = lo.read(nextBufferSize);
// Note that actual number of bytes read may be less than requested
bytesRead = Math.min(len, buffer.length);
System.arraycopy(buffer, 0, dest, off, bytesRead);
// If we at the end of the stream, and we just copied the last bytes,
// we can release the buffer
if (bytesRead == buffer.length) {
// TODO: if we want to reuse the buffer in mark/reset we should not release the
// buffer here
buffer = null;
bufferPosition = 0;
} else {
bufferPosition = bytesRead;
}
}
} catch (SQLException ex) {
throw new IOException(
GT.tr("Can not read data from large object {0}, position: {1}, buffer size: {2}",
lo.getLongOID(), absolutePosition, len),
ex);
}
bytesCopied += bytesRead;
absolutePosition += bytesRead;
}
return bytesCopied == 0 ? -1 : bytesCopied;
}
}
/**
* Closes this input stream and releases any system resources associated with the stream.
*
* The close
method of InputStream
does nothing.
*
* @throws IOException if an I/O error occurs.
*/
@Override
public void close() throws IOException {
long loId = 0;
try (ResourceLock ignore = lock.obtain()) {
LargeObject lo = this.lo;
if (lo != null) {
loId = lo.getLongOID();
lo.close();
}
this.lo = null;
} catch (SQLException e) {
throw new IOException(
GT.tr("Can not close large object {0}",
loId),
e);
}
}
/**
* Marks the current position in this input stream. A subsequent call to the reset
* method repositions this stream at the last marked position so that subsequent reads re-read the
* same bytes.
*
* The readlimit
arguments tells this input stream to allow that many bytes to be
* read before the mark position gets invalidated.
*
* The general contract of mark
is that, if the method markSupported
* returns true
, the stream somehow remembers all the bytes read after the call to
* mark
and stands ready to supply those same bytes again if and whenever the method
* reset
is called. However, the stream is not required to remember any data at all
* if more than readlimit
bytes are read from the stream before reset
is
* called.
*
* Marking a closed stream should not have any effect on the stream.
*
* @param readlimit the maximum limit of bytes that can be read before the mark position becomes
* invalid.
* @see java.io.InputStream#reset()
*/
@Override
public void mark(int readlimit) {
try (ResourceLock ignore = lock.obtain()) {
markPosition = absolutePosition;
}
}
/**
* Repositions this stream to the position at the time the mark
method was last
* called on this input stream. NB: If mark is not called we move to the beginning.
*
* @see java.io.InputStream#mark(int)
* @see java.io.IOException
*/
@Override
public void reset() throws IOException {
try (ResourceLock ignore = lock.obtain()) {
LargeObject lo = getLo();
long loId = lo.getLongOID();
try {
if (markPosition <= Integer.MAX_VALUE) {
lo.seek((int) markPosition);
} else {
lo.seek64(markPosition, LargeObject.SEEK_SET);
}
buffer = null;
absolutePosition = markPosition;
} catch (SQLException e) {
throw new IOException(
GT.tr("Can not reset stream for large object {0} to position {1}",
loId, markPosition),
e);
}
}
}
/**
* Tests if this input stream supports the mark
and reset
methods. The
* markSupported
method of InputStream
returns false
.
*
* @return true
if this true type supports the mark and reset method;
* false
otherwise.
* @see java.io.InputStream#mark(int)
* @see java.io.InputStream#reset()
*/
@Override
public boolean markSupported() {
return true;
}
private LargeObject getLo() throws IOException {
if (lo == null) {
throw new IOException("BlobOutputStream is closed");
}
return lo;
}
}