// jTDS JDBC Driver for Microsoft SQL Server and Sybase
// Copyright (C) 2004 The jTDS Project
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
package net.sourceforge.jtds.util;
import java.io.*;
import java.sql.SQLException;
import net.sourceforge.jtds.jdbc.Messages;
/**
* Manages a buffer (backed by optional disk storage) for use as a data store
* by the CLOB and BLOB objects.
* <p/>
* The data can be purely memory based until the size exceeds the value
* dictated by the <code>lobBuffer</code> URL property after which it will be
* written to disk. The disk array is accessed randomly one page (1024 bytes)
* at a time.
* <p/>
* This class is not synchronized and concurrent open input and output
* streams can conflict.
* <p/>
* Tuning hints:
* <ol>
* <li>The <code>PAGE_SIZE</code> governs how much data is buffered when
* reading or writing data a byte at a time. 1024 bytes seems to work well
* but if very large objects are being written a byte at a time 4096 may be
* better. <b>NB.</b> ensure that the <code>PAGE_MASK</code> and
* <code>BYTE_MASK</code> fields are also adjusted to match.
* <li>Reading or writing byte arrays that are greater than or equal to the
* page size will go directly to or from the random access file cutting out
* an ArrayCopy operation.
* <li>If BLOBs are being buffered exclusively in memory you may wish to
* adjust the <code>MAX_BUF_INC</code> value. Every time the buffer is
* expanded the existing contents are copied and this may get expensive
* with very large BLOBs.
* <li>The BLOB file will be kept open for as long as there are open input or
* output streams. Therefore BLOB streams should be explicitly closed as
* soon as they are finished with.
* </ol>
*
* @author Mike Hutchinson
* @version $Id: BlobBuffer.java,v 1.4.2.1 2009-08-03 12:31:00 ickzon Exp $
*/
public class BlobBuffer {
/**
* Default zero length buffer.
*/
private static final byte[] EMPTY_BUFFER = new byte[0];
/**
* Default page size (must be power of 2).
*/
private static final int PAGE_SIZE = 1024;
/**
* Mask for page component of read/write pointer.
*/
private static final int PAGE_MASK = 0xFFFFFC00;
/**
* Mask for page offset component of R/W pointer.
*/
private static final int BYTE_MASK = 0x000003FF;
/**
* Maximum buffer increment.
*/
private static final int MAX_BUF_INC = 16384;
/**
* Invalid page marker.
*/
private static final int INVALID_PAGE = -1;
/**
* The BLOB buffer or the current page buffer.
*/
private byte[] buffer;
/**
* The total length of the valid data in buffer.
*/
private int length;
/**
* The number of the current page in memory.
*/
private int currentPage;
/**
* The name of the temporary BLOB disk file.
*/
private File blobFile;
/**
* The RA file object reference or null if closed.
*/
private RandomAccessFile raFile;
/**
* Indicates page in memory must be saved.
*/
private boolean bufferDirty;
/**
* Count of callers that have opened the BLOB file.
*/
private int openCount;
/**
* True if attempts to create a BLOB file have failed or the buffer is
* created without specifying a buffer directory.
*/
private boolean isMemOnly;
/**
* The directory to buffer data to.
*/
private final File bufferDir;
/**
* The maximum size of an in memory buffer.
*/
private final int maxMemSize;
/**
* Creates a blob buffer.
*
* @param bufferDir
* @param maxMemSize the maximum size of the in memory buffer
*/
public BlobBuffer(File bufferDir, long maxMemSize) {
if (maxMemSize > Integer.MAX_VALUE)
throw new IllegalArgumentException("The maximum in-memory buffer size of a blob buffer cannot exceed 2GB");
this.bufferDir = bufferDir;
this.maxMemSize = (int) maxMemSize;
buffer = EMPTY_BUFFER;
}
/**
* Finalizes this object by deleting any work files.
*/
protected void finalize() throws Throwable {
try {
if (raFile != null) {
raFile.close();
}
} catch (IOException e) {
// Ignore we are going to delete anyway
} finally {
if (blobFile != null) {
blobFile.delete();
}
}
}
/**
* Creates a random access disk file to use as backing storage for the LOB
* data.
* <p/>
* This method may fail due to security exceptions or local disk problems,
* in which case the blob storage will remain entirely in memory.
*/
public void createBlobFile() {
if (bufferDir == null) {
isMemOnly = true;
return;
}
try {
blobFile = File.createTempFile("jtds", ".tmp", bufferDir);
// blobFile.deleteOnExit(); memory leak, see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6664633
raFile = new RandomAccessFile(blobFile, "rw");
if (length > 0) {
raFile.write(buffer, 0, length);
}
buffer = new byte[PAGE_SIZE];
currentPage = INVALID_PAGE;
openCount = 0;
} catch (SecurityException e) {
blobFile = null;
raFile = null;
isMemOnly = true;
Logger.println("SecurityException creating BLOB file:");
Logger.logException(e);
} catch (IOException ioe) {
blobFile = null;
raFile = null;
isMemOnly = true;
Logger.println("IOException creating BLOB file:");
Logger.logException(ioe);
}
}
/**
* Opens the BLOB disk file.
* <p/>
* A count of open and close requests is kept so that the file may be
* closed when no longer required thus keeping the number of open files to
* a minimum.
*
* @throws IOException if an I/O error occurs
*/
public void open() throws IOException {
if (raFile == null && blobFile != null) {
// reopen file
raFile = new RandomAccessFile(blobFile, "rw");
openCount = 1;
currentPage = INVALID_PAGE;
buffer = new byte[PAGE_SIZE];
return;
}
if (raFile != null) {
openCount++;
}
}
/**
* Reads byte from the BLOB buffer at the specified location.
* <p/>
* The read pointer is partitioned into a page number and an offset within
* the page. This routine will read new pages as required. The page size
* must be a power of 2 and is currently set to 1024 bytes.
*
* @param readPtr the offset in the buffer of the required byte
* @return the byte value as an <code>int</code> or -1 if at EOF
* @throws IOException if an I/O error occurs
*/
public int read(int readPtr) throws IOException {
if (readPtr >= length) {
// At end of file.
return -1;
}
if (raFile != null) {
// Paged storage as a file exists
if (currentPage != (readPtr & PAGE_MASK)) {
// Requested page not in memory so read it
readPage(readPtr);
}
// Use the byte offset to return the correct
// byte from the page.
return buffer[readPtr & BYTE_MASK] & 0xFF;
} else {
// In memory buffer just return byte.
return buffer[readPtr] & 0xFF;
}
}
/**
* Reads bytes from the BLOB buffer at the specified location.
*
* @param readPtr the offset in the buffer of the required byte
* @param bytes the byte array to fill
* @param offset the start position in the byte array
* @param len the number of bytes to read
* @return the number of bytes read or -1 if at end of file
* @throws IOException if an I/O error occurs
*/
public int read(int readPtr, byte[] bytes, int offset, int len)
throws IOException {
// Validate parameters
if (bytes == null) {
throw new NullPointerException();
} else if ((offset < 0) || (offset > bytes.length) || (len < 0)
|| ((offset + len) > bytes.length) || ((offset + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return 0;
}
if (readPtr >= length) {
// At end of file
return -1;
}
if (raFile != null) {
// Need to read from disk file
len = Math.min(length - readPtr, len);
if (len >= PAGE_SIZE) {
// This is a big write so we optimize by reading directly
// from the RA File.
if (bufferDirty) {
writePage(currentPage);
}
currentPage = INVALID_PAGE;
raFile.seek(readPtr);
raFile.readFully(bytes, offset, len);
} else {
//
// Partial read so buffer locally
//
int count = len;
while (count > 0) {
if (currentPage != (readPtr & PAGE_MASK)) {
// Requested page not in memory so read it
readPage(readPtr);
}
int inBuffer = Math.min(PAGE_SIZE - (readPtr & BYTE_MASK), count);
System.arraycopy(buffer, readPtr & BYTE_MASK, bytes, offset, inBuffer);
offset += inBuffer;
readPtr += inBuffer;
count -= inBuffer;
}
}
} else {
// In memory buffer
len = Math.min(length - readPtr, len);
System.arraycopy(buffer, readPtr, bytes, offset, len);
}
return len;
}
/**
* Inserts a byte into the buffer at the specified location.
* <p/>
* The write pointer is partitioned into a page number and an offset within
* the page. This routine will write new pages as required. The page size
* must be a power of 2 and is currently set to 1024 bytes.
*
* @param writePtr the offset in the buffer of the required byte
* @param b the byte value to write
* @throws IOException if an I/O error occurs
*/
public void write(int writePtr, int b) throws IOException {
if (writePtr >= length) {
if (writePtr > length) {
// Probably because the user called truncate at
// the same time as writing to the blob!
throw new IOException("BLOB buffer has been truncated");
}
// We are writing beyond the current length
// of the buffer and need to update the total length.
if (++length < 0) {
// We have wrapped 31 bits!
// This should ensure that the disk file is limited to 2GB.
// If in memory JVM will probably have failed by now anyway.
throw new IOException("BLOB may not exceed 2GB in size");
}
}
if (raFile != null) {
// OK we have a disk based buffer
if (currentPage != (writePtr & PAGE_MASK)) {
// The page we need is not in memory
readPage(writePtr);
}
buffer[writePtr & BYTE_MASK] = (byte) b;
// Ensure change will saved if buffer is replaced
bufferDirty = true;
} else {
// In memory buffer only (only used here if disk unavailable
if (writePtr >= buffer.length) {
growBuffer(writePtr + 1);
}
buffer[writePtr] = (byte) b;
}
}
/**
* Inserts bytes into the buffer at the specified location.
*
* @param writePtr the offset in the buffer of the required byte
* @param bytes the byte array value to write
* @param offset the start position in the byte array
* @param len the number of bytes to write
* @throws IOException if an I/O error occurs
*/
void write(int writePtr, byte[] bytes, int offset, int len)
throws IOException {
// Validate parameters
if (bytes == null) {
throw new NullPointerException();
} else if ((offset < 0) || (offset > bytes.length) || (len < 0) ||
((offset + len) > bytes.length) || ((offset + len) < 0)) {
throw new IndexOutOfBoundsException();
} else if (len == 0) {
return;
}
if ((long) writePtr + len > Integer.MAX_VALUE) {
throw new IOException("BLOB may not exceed 2GB in size");
}
if (writePtr > length) {
// Probably because the user called truncate at
// the same time as writing to the blob!
throw new IOException("BLOB buffer has been truncated");
}
if (raFile != null) {
// dealing with disk storage (normal case)
//
if (len >= PAGE_SIZE) {
// This is a big write so we optimize by writing directly
// to the RA File.
if (bufferDirty) {
writePage(currentPage);
}
currentPage = INVALID_PAGE;
raFile.seek(writePtr);
raFile.write(bytes, offset, len);
writePtr += len;
} else {
// Small writes so use the page buffer for
// effeciency.
int count = len;
while (count > 0) {
// Paged storage as a file exists
if (currentPage != (writePtr & PAGE_MASK)) {
// Requested page not in memory so read it
readPage(writePtr);
}
int inBuffer = Math.min(
PAGE_SIZE - (writePtr & BYTE_MASK), count);
System.arraycopy(bytes, offset, buffer,
writePtr & BYTE_MASK, inBuffer);
bufferDirty = true;
offset += inBuffer;
writePtr += inBuffer;
count -= inBuffer;
}
}
} else {
// In memory (only used here if disk not available)
if (writePtr + len > buffer.length) {
growBuffer(writePtr + len);
}
System.arraycopy(bytes, offset, buffer, writePtr, len);
writePtr += len;
}
if (writePtr > length) {
length = writePtr;
}
}
/**
* Reads in the specified page from the disk buffer.
* <p/>
* Any existing dirty page is first saved to disk.
*
* @param page the page number
* @throws IOException if an I/O error occurs
*/
public void readPage(int page) throws IOException {
page = page & PAGE_MASK;
if (bufferDirty) {
writePage(currentPage);
}
if (page > raFile.length()) {
throw new IOException("readPage: Invalid page number " + page);
}
currentPage = page;
// Locate and read requested page
// NB. Page may not be completely filled.
raFile.seek(currentPage);
// Repeat reading until buffer is filled or EOF is reached
int count = 0, res;
do {
res = raFile.read(buffer, count, buffer.length - count);
count += (res == -1) ? 0 : res;
} while (count < PAGE_SIZE && res != -1);
}
/**
* Writes the specified page to the disk buffer.
*
* @param page the page number
* @throws IOException if an I/O error occurs
*/
public void writePage(int page) throws IOException {
page = page & PAGE_MASK;
if (page > raFile.length()) {
throw new IOException("writePage: Invalid page number " + page);
}
if (buffer.length != PAGE_SIZE) {
throw new IllegalStateException("writePage: buffer size invalid");
}
raFile.seek(page);
raFile.write(buffer);
bufferDirty = false;
}
/**
* Logically closes the file or physically close it if the open count is
* now zero.
* <p/>
* Any updated buffer in memory is flushed to disk before the file is
* closed.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (openCount > 0) {
if (--openCount == 0 && raFile != null) {
if (bufferDirty) {
writePage(currentPage);
}
raFile.close();
raFile = null;
// Allow buffer to be garbage collected
buffer = EMPTY_BUFFER;
currentPage = INVALID_PAGE;
}
}
}
/**
* Increases the size of the in memory buffer for situations where disk
* storage of BLOB is not possible.
*
* @param minSize the minimum size of buffer required
*/
public void growBuffer(int minSize) {
if (buffer.length == 0) {
// Assign initial buffer
buffer = new byte[Math.max(PAGE_SIZE, minSize)];
} else {
byte[] tmp;
if (buffer.length * 2 > minSize && buffer.length <= MAX_BUF_INC) {
tmp = new byte[buffer.length * 2];
} else {
tmp = new byte[minSize + MAX_BUF_INC];
}
// Copy over existing data
System.arraycopy(buffer, 0, tmp, 0, buffer.length);
buffer = tmp; // Assign new buffer.
}
}
/**
* Sets the initial buffer to an existing byte array.
*
* @param bytes the byte array containing the BLOB data
* @param copy true if a local copy of the data is required
*/
public void setBuffer(byte[] bytes, boolean copy) {
if (copy) {
buffer = new byte[bytes.length];
System.arraycopy(bytes, 0, buffer, 0, buffer.length);
} else {
buffer = bytes;
}
length = buffer.length;
}
//
// ---- Inner classes implementing the various input/output stream classes ---
//
/**
* An <code>InputStream</code> over the BLOB buffer.
*/
private class BlobInputStream extends InputStream {
private int readPtr;
private boolean open;
/**
* Costructs an <code>InputStream</code> object over the BLOB buffer.
*
* @param pos the starting position (from 0)
* @throws IOException if an I/O error occurs
*/
public BlobInputStream(long pos) throws IOException {
open();
open = true;
readPtr = (int) pos;
}
/**
* Ensures underlying BLOB file can be closed even if user does not
* close this stream.
*/
protected void finalize() throws Throwable {
if (open) {
try {
close();
} catch (IOException e) {
// Ignore closing anyway
} finally {
super.finalize();
}
}
}
/**
* Returns the number of bytes available to read.
*
* @throws IOException if an I/O error occurs
*/
public int available() throws IOException {
return (int) getLength() - readPtr;
}
/**
* Reads the next byte from the stream.
*
* @return the next byte as an <code>int</code> or -1 if at EOF
* @throws IOException if an I/O error occurs
*/
public int read() throws IOException {
int b = BlobBuffer.this.read(readPtr);
if (b >= 0) {
readPtr++;
}
return b;
}
/**
* Reads a bytes from the stream.
*
* @param bytes the byte array to fill
* @param offset the start position in the byte array
* @param len the number of bytes to read
* @return the number of bytes read or -1 if at end of file
* @throws IOException if an I/O error occurs
*/
public int read(byte[] bytes, int offset, int len) throws IOException {
int b = BlobBuffer.this.read(readPtr, bytes, offset, len);
if (b > 0) {
readPtr += b;
}
return b;
}
/**
* Closes the output stream.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (open) {
BlobBuffer.this.close();
open = false;
}
}
}
/**
* A Big Endian Unicode <code>InputStream</code> over the CLOB buffer.
*/
private class UnicodeInputStream extends InputStream {
private int readPtr;
private boolean open;
/**
* Costructs an InputStream object over the BLOB buffer.
*
* @param pos the starting position (from 0)
* @throws IOException if an I/O error occurs
*/
public UnicodeInputStream(long pos) throws IOException {
open();
open = true;
readPtr = (int) pos;
}
/**
* Ensures underlying BLOB file can be closed even if user does not
* close this stream.
*/
protected void finalize() throws Throwable {
if (open) {
try {
close();
} catch (IOException e) {
// Ignore closing anyway
} finally {
super.finalize();
}
}
}
/**
* Returns the number of bytes available to read.
*
* @throws IOException if an I/O error occurs
*/
public int available() throws IOException {
return (int) getLength() - readPtr;
}
/**
* Reads the next byte from the stream.
*
* @return the next byte as an <code>int</code> or -1 if at EOF
* @throws IOException if an I/O error occurs
*/
public int read() throws IOException {
//
// The XOR of 1 with the readPtr forces the bytes to be returned
// in big endian order.
//
int b = BlobBuffer.this.read(readPtr ^ 1);
if (b >= 0) {
readPtr++;
}
return b;
}
/**
* Close the output stream.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (open) {
BlobBuffer.this.close();
open = false;
}
}
}
/**
* An ASCII <code>InputStream</code> over the CLOB buffer.
* <p/>
* This class interprets ASCII as anything which has a value below 0x80.
* This is more rigid than other drivers which allow any character below
* 0x100 to be converted to returned. The more relaxed coding is useful
* when dealing with most single byte character sets and if this behaviour
* is desired, comment out the line indicated in the read method.
*/
private class AsciiInputStream extends InputStream {
private int readPtr;
private boolean open;
/**
* Costructs an InputStream object over the BLOB buffer.
*
* @param pos the starting position (from 0)
* @throws IOException if an I/O error occurs
*/
public AsciiInputStream(long pos) throws IOException {
open();
open = true;
readPtr = (int) pos;
}
/**
* Ensures underlying BLOB file can be closed even if user does not
* close this stream.
*/
protected void finalize() throws Throwable {
if (open) {
try {
close();
} catch (IOException e) {
// Ignore closing anyway
} finally {
super.finalize();
}
}
}
/**
* Returns the number of bytes available to read.
*
* @throws IOException if an I/O error occurs
*/
public int available() throws IOException {
return ((int) getLength() - readPtr) / 2;
}
/**
* Read the next byte from the stream.
*
* @return the next byte as an <code>int</code> or -1 if at EOF
* @throws IOException if an I/O error occurs
*/
public int read() throws IOException {
int b1 = BlobBuffer.this.read(readPtr);
if (b1 >= 0) {
readPtr++;
int b2 = BlobBuffer.this.read(readPtr);
if (b2 >= 0) {
readPtr++;
if (b2 != 0
|| b1 > 0x7F // Comment out this line for a more
// permissive interpretation of 'ASCII'.
) {
b1 = '?'; // Not ASCII set to '?'
}
return b1;
}
}
return -1;
}
/**
* Closes the output stream.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (open) {
BlobBuffer.this.close();
open = false;
}
}
}
/**
* Implements an <code>OutputStream</code> for BLOB data.
*/
private class BlobOutputStream extends OutputStream {
private int writePtr;
private boolean open;
/**
* Costructs an OutputStream object over the BLOB buffer.
*
* @param pos the starting position (from 0)
* @throws IOException if an I/O error occurs
*/
BlobOutputStream(long pos) throws IOException {
open();
open = true;
writePtr = (int) pos;
}
/**
* Ensures underlying BLOB file can be closed even if user does not
* close this stream.
*/
protected void finalize() throws Throwable {
if (open) {
try {
close();
} catch (IOException e) {
// Ignore closing anyway
} finally {
super.finalize();
}
}
}
/**
* Write a byte to the BLOB buffer.
*
* @param b the byte value to write
* @throws IOException if an I/O error occurs
*/
public void write(int b) throws IOException {
BlobBuffer.this.write(writePtr++, b);
}
/**
* Write bytes to the BLOB buffer.
*
* @param bytes the byte array value to write
* @param offset the start position in the byte array
* @param len the number of bytes to write
* @throws IOException if an I/O error occurs
*/
public void write(byte[] bytes, int offset, int len) throws IOException {
BlobBuffer.this.write(writePtr, bytes, offset, len);
writePtr += len;
}
/**
* Close the output stream.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (open) {
BlobBuffer.this.close();
open = false;
}
}
}
/**
* Implements an ASCII <code>OutputStream</code> for CLOB data.
*/
private class AsciiOutputStream extends OutputStream {
private int writePtr;
private boolean open;
/**
* Costructs an ASCII <code>OutputStream</code> object over the BLOB
* buffer.
*
* @param pos the starting position (from 0)
* @throws IOException if an I/O error occurs
*/
AsciiOutputStream(long pos) throws IOException {
open();
open = true;
writePtr = (int) pos;
}
/**
* Ensures underlying BLOB file can be closed even if user does not
* close this stream.
*/
protected void finalize() throws Throwable {
if (open) {
try {
close();
} catch (IOException e) {
// Ignore closing anyway
} finally {
super.finalize();
}
}
}
/**
* Writes a byte to the BLOB buffer.
*
* @param b the byte value to write
* @throws IOException if an I/O error occurs
*/
public void write(int b) throws IOException {
BlobBuffer.this.write(writePtr++, b);
BlobBuffer.this.write(writePtr++, 0);
}
/**
* Closes the output stream.
*
* @throws IOException if an I/O error occurs
*/
public void close() throws IOException {
if (open) {
BlobBuffer.this.close();
open = false;
}
}
}
//
// ---- Support methods for CLOB/BLOB ----
//
/**
* Returns the BLOB data as a byte array.
*
* @param pos the start position in the BLOB buffer (from 1)
* @param len the number of bytes to copy
* @return the requested data as a <code>byte[]</code>
*/
public byte[] getBytes(long pos, int len) throws SQLException {
pos--;
if (pos < 0) {
throw new SQLException(Messages.get("error.blobclob.badpos"), "HY090");
}
if (pos > length) {
throw new SQLException(Messages.get("error.blobclob.badposlen"), "HY090");
}
if (len < 0) {
throw new SQLException(Messages.get("error.blobclob.badlen"), "HY090");
}
if (pos + len > length) {
// Don't throw an exception, just return as much data as available
len = (int) (length - pos);
}
try {
// Should not do this. It could cause trouble.
// if (pos == 0 && len == buffer.length && blobFile == null) {
// // There is no file and we do not need a subset of the data.
// // We should copy the buffer as the user may modify its
// // contents but this would be wasteful in most cases.
// return buffer;
// }
// We do need a subset or we are reading from the file
byte[] data = new byte[len];
if (blobFile == null) {
// Just copy subset from memory buffer
System.arraycopy(buffer, (int) (pos), data, 0, len);
} else {
// Copy data from disk buffer
InputStream is = new BlobInputStream(pos);
int bc = is.read(data);
is.close();
if (bc != data.length) {
throw new IOException("Unexpected EOF on BLOB data file bc=" +
bc + " data.len=" + data.length);
}
}
return data;
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror", e.getMessage()),
"HY000");
}
}
/**
* Retrieve the BLOB data as an <code>InputStream</code>.
*
* @param ascii true if an ASCII input stream should be returned
* @return the <code>InputStream</code> built over the BLOB data
* @throws SQLException if an error occurs
*/
public InputStream getBinaryStream(boolean ascii) throws SQLException {
try {
if (ascii) {
return new AsciiInputStream(0);
} else {
return new BlobInputStream(0);
}
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
}
}
/**
* Retrieve the BLOB data as an Big Endian Unicode
* <code>InputStream</code>.
*
* @return the <code>InputStream</code> built over the BLOB data
* @throws SQLException if an error occurs
*/
public InputStream getUnicodeStream() throws SQLException {
try {
return new UnicodeInputStream(0);
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
}
}
/**
* Creates an <code>OutputStream</code> that can be used to update the
* BLOB.
* <p/>
* Given that we cannot know the final size of a BLOB created by the caller
* of this method, we assume the worst and create a disk BLOB by default.
*
* @param pos the start position in the buffer (from 1)
* @param ascii true if an ASCII output stream is required
* @return the <code>OutputStream</code> to be used to update the BLOB
* @throws SQLException if an error occurs
*/
public OutputStream setBinaryStream(long pos, boolean ascii)
throws SQLException {
pos--;
if (pos < 0) {
throw new SQLException(Messages.get("error.blobclob.badpos"),
"HY090");
}
if (pos > length) {
throw new SQLException(Messages.get("error.blobclob.badposlen"),
"HY090");
}
try {
if (!isMemOnly && blobFile == null) {
createBlobFile();
}
if (ascii) {
return new AsciiOutputStream(pos);
} else {
return new BlobOutputStream(pos);
}
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
}
}
/**
* Sets the content of the BLOB to the supplied byte array value.
* <p/>
* If the following conditions are met:
* <ol>
* <li>The start position is 1
* <li>The existing BLOB length is smaller or the same as the length of
* the new data
* <li>The new data length does not exceed the in memory limit
* </ol>
* then the new data is buffered entirely in memory, otherwise a disk file
* is created.
*
* @param pos the start position in the buffer (from 1)
* @param bytes the byte array containing the data to copy
* @param offset the start position in the byte array (from 0)
* @param len the number of bytes to copy
* @param copy true if a local copy of the byte array is required
* @return the number of bytes copied
* @throws SQLException if an error occurs
*/
public int setBytes(long pos, byte[] bytes, int offset, int len, boolean copy)
throws SQLException {
pos--;
if (pos < 0) {
throw new SQLException(Messages.get("error.blobclob.badpos"),
"HY090");
}
if (pos > length) {
throw new SQLException(Messages.get("error.blobclob.badposlen"),
"HY090");
}
if (bytes == null) {
throw new SQLException(Messages.get("error.blob.bytesnull"),
"HY009");
}
if (offset < 0 || offset > bytes.length) {
throw new SQLException(Messages.get("error.blobclob.badoffset"),
"HY090");
}
if (len < 0 || pos + len > Integer.MAX_VALUE
|| offset + len > bytes.length) {
throw new SQLException(Messages.get("error.blobclob.badlen"),
"HY090");
}
//
// If there is no disk file and this data will replace the
// existing contents of the BLOB then just copy byte data to
// a new buffer array if the size is small enough.
//
if (blobFile == null && pos == 0
&& len >= length
&& len <= maxMemSize) {
if (copy) {
buffer = new byte[len];
System.arraycopy(bytes, offset, buffer, 0, len);
} else {
// A copy is not always required
buffer = bytes;
}
length = len;
return len;
}
try {
//
// OK we will now try and create a BLOB file as this
// is a more complex update.
//
if (!isMemOnly && blobFile == null) {
createBlobFile();
}
//
// Open the BLOB file
//
open();
int ptr = (int) pos;
write(ptr, bytes, offset, len);
close();
return len;
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
}
}
/**
* Retrieves the length of this BLOB buffer in bytes.
*
* @return the length of the BLOB data in bytes
*/
public long getLength() {
return length;
}
/**
* Retrieves the length of the BLOB buffer (in memory version only).
*
* @param length the length of the valid data in the buffer
*/
public void setLength(long length) {
this.length = (int) length;
}
/**
* Truncates the BLOB buffer to the specified size.
*
* @param len the required length
* @throws SQLException if an error occurs
*/
public void truncate(long len) throws SQLException {
if (len < 0) {
throw new SQLException(Messages.get("error.blobclob.badlen"),
"HY090");
}
if (len > length) {
throw new SQLException(Messages.get("error.blobclob.lentoolong"),
"HY090");
}
length = (int) len;
if (len == 0) {
try {
// Try to discard and delete work file
// Any open input streams will get EOF
// open write streams will probably fail.
if (blobFile != null) {
if (raFile != null) {
raFile.close();
}
blobFile.delete();
}
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
} finally {
buffer = EMPTY_BUFFER;
blobFile = null;
raFile = null;
openCount = 0;
currentPage = INVALID_PAGE;
}
}
}
/**
* Provides support for pattern searching methods.
*
* @param pattern the byte array containg the search pattern
* @param start the start position in the BLOB (from 1)
* @return the <code>int</code> start index for the pattern (from 1) or -1
* if the pattern is not found.
* @throws SQLException if an error occurs
*/
public int position(byte[] pattern, long start) throws SQLException {
try {
start--;
if (start < 0) {
throw new SQLException(Messages.get("error.blobclob.badpos"),
"HY090");
}
if (start >= length) {
throw new SQLException(Messages.get("error.blobclob.badposlen"),
"HY090");
}
if (pattern == null) {
throw new SQLException(Messages.get("error.blob.badpattern"),
"HY009");
}
if (pattern.length == 0 || length == 0 || pattern.length > length) {
// Impossible for there to be a match
return -1;
}
// FIXME Implement a better (O(n)) search algorithm
int limit = length - pattern.length;
if (blobFile == null) {
for (int i = (int) start; i <= limit; i++) {
int p;
for (p = 0;
p < pattern.length && buffer[i + p] == pattern[p];
p++);
if (p == pattern.length) {
return i + 1;
}
}
} else {
open();
for (int i = (int) start; i <= limit; i++) {
int p;
for (p = 0;
p < pattern.length && read(i + p) == (pattern[p] & 0xFF);
p++);
if (p == pattern.length) {
close();
return i + 1;
}
}
close();
}
return -1;
} catch (IOException e) {
throw new SQLException(Messages.get("error.generic.ioerror",
e.getMessage()),
"HY000");
}
}
}