All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.postgresql.largeobject.BlobInputStream Maven / Gradle / Ivy

The newest version!
/*
 * 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; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy