com.microsoft.sqlserver.jdbc.SQLServerBlob Maven / Gradle / Ivy
Show all versions of mssql-jdbc Show documentation
/*
* Microsoft JDBC Driver for SQL Server
*
* Copyright(c) Microsoft Corporation All rights reserved.
*
* This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information.
*/
package com.microsoft.sqlserver.jdbc;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Blob;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* SQLServerBlob represents a binary LOB object and implements a java.sql.Blob.
*/
public final class SQLServerBlob implements java.sql.Blob, java.io.Serializable {
private static final long serialVersionUID = -3526170228097889085L;
// The value of the BLOB that this Blob object represents.
// This value is never null unless/until the free() method is called.
private byte[] value;
private transient SQLServerConnection con;
private boolean isClosed = false;
// Active streams which must be closed when the Blob is closed
//
// Initial size of the array is based on an assumption that a Blob object is
// typically used either for input or output, and then only once. The array size
// grows automatically if multiple streams are used.
ArrayList activeStreams = new ArrayList<>(1);
static private final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerBlob");
static private final AtomicInteger baseID = new AtomicInteger(0); // Unique id generator for each instance (used for logging).
final private String traceID;
final public String toString() {
return traceID;
}
// Returns unique id for each instance.
private static int nextInstanceID() {
return baseID.incrementAndGet();
}
/**
* Create a new BLOB
*
* @param connection
* the database connection this blob is implemented on
* @param data
* the BLOB's data
* @deprecated Use {@link SQLServerConnection#createBlob()} instead.
*/
@Deprecated
public SQLServerBlob(SQLServerConnection connection,
byte data[]) {
traceID = " SQLServerBlob:" + nextInstanceID();
con = connection;
// Disallow Blobs with internal null values. We throw a NullPointerException here
// because the method signature of the public constructor does not permit a SQLException
// to be thrown.
if (null == data)
throw new NullPointerException(SQLServerException.getErrString("R_cantSetNull"));
value = data;
if (logger.isLoggable(Level.FINE)) {
String loggingInfo = (null != connection) ? connection.toString() : "null connection";
logger.fine(toString() + " created by (" + loggingInfo + ")");
}
}
SQLServerBlob(SQLServerConnection connection) {
traceID = " SQLServerBlob:" + nextInstanceID();
con = connection;
value = new byte[0];
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " created by (" + connection.toString() + ")");
}
SQLServerBlob(BaseInputStream stream) throws SQLServerException {
traceID = " SQLServerBlob:" + nextInstanceID();
activeStreams.add(stream);
if (logger.isLoggable(Level.FINE))
logger.fine(toString() + " created by (null connection)");
}
/**
* Frees this Blob object and releases the resources that it holds.
*
* After free() has been called, any attempt to invoke a method other than free() will result in a SQLException being thrown. If free() is called
* multiple times, the subsequent calls to free are treated as a no-op.
*/
public void free() throws SQLException {
if (!isClosed) {
// Close active streams, ignoring any errors, since nothing can be done with them after that point anyway.
if (null != activeStreams) {
for (Closeable stream : activeStreams) {
try {
stream.close();
}
catch (IOException ioException) {
logger.fine(toString() + " ignored IOException closing stream " + stream + ": " + ioException.getMessage());
}
}
activeStreams = null;
}
// Discard the value
value = null;
isClosed = true;
}
}
/**
* Throws a SQLException if the LOB has been freed.
*/
private void checkClosed() throws SQLServerException {
if (isClosed) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_isFreed"));
SQLServerException.makeFromDriverError(con, null, form.format(new Object[] {"Blob"}), null, true);
}
}
public InputStream getBinaryStream() throws SQLException {
checkClosed();
if (null == value && !activeStreams.isEmpty()) {
InputStream stream = (InputStream) activeStreams.get(0);
try {
stream.reset();
}
catch (IOException e) {
throw new SQLServerException(e.getMessage(), null, 0, e);
}
return (InputStream) activeStreams.get(0);
}
else {
if (value == null) {
throw new SQLServerException("Unexpected Error: blob value is null while all streams are closed.", null);
}
return getBinaryStreamInternal(0, value.length);
}
}
public InputStream getBinaryStream(long pos,
long length) throws SQLException {
// Not implemented - partial materialization
throw new SQLFeatureNotSupportedException(SQLServerException.getErrString("R_notSupported"));
}
private InputStream getBinaryStreamInternal(int pos,
int length) {
assert null != value;
assert pos >= 0;
assert 0 <= length && length <= value.length - pos;
assert null != activeStreams;
InputStream getterStream = new ByteArrayInputStream(value, pos, length);
activeStreams.add(getterStream);
return getterStream;
}
/**
* Retrieves all or part of the BLOB value that this Blob object represents, as an array of bytes. This byte array contains up to length
* consecutive bytes starting at position pos.
*
* @param pos
* - the ordinal position of the first byte in the BLOB value to be extracted; the first byte is at position 1
* @param length
* - the number of consecutive bytes to be copied; the value for length must be 0 or greater
* @return a byte array containing up to length consecutive bytes from the BLOB value designated by this Blob object, starting with the byte at
* position pos
* @throws SQLException
* - if there is an error accessing the BLOB value; if pos is less than 1 or length is less than 0
*/
public byte[] getBytes(long pos,
int length) throws SQLException {
checkClosed();
getBytesFromStream();
if (pos < 1) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex"));
Object[] msgArgs = {pos};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
if (length < 0) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength"));
Object[] msgArgs = {length};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
// Adjust pos to zero based.
pos--;
// Bound the starting position if necessary
if (pos > value.length)
pos = value.length;
// Bound the length if necessary
if (length > value.length - pos)
length = (int) (value.length - pos);
byte bTemp[] = new byte[length];
System.arraycopy(value, (int) pos, bTemp, 0, length);
return bTemp;
}
/**
* Return the length of the BLOB
*
* @throws SQLException
* when an error occurs
* @return the data length
*/
public long length() throws SQLException {
checkClosed();
getBytesFromStream();
return value.length;
}
/**
* Converts stream to byte[]
* @throws SQLServerException
*/
private void getBytesFromStream() throws SQLServerException {
if (null == value) {
BaseInputStream stream = (BaseInputStream) activeStreams.get(0);
try {
stream.reset();
}
catch (IOException e) {
throw new SQLServerException(e.getMessage(), null, 0, e);
}
value = stream.getBytes();
}
}
/**
* Retrieves the byte position in the BLOB value designated by this Blob object at which pattern begins. The search begins at position start.
*
* @param pattern
* - the Blob object designating the BLOB value for which to search
* @param start
* - the position in the BLOB value at which to begin searching; the first position is 1
* @return the postion at which the pattern begins, else -1
* @throws SQLException
* - if there is an error accessing the BLOB value or if start is less than 1
*/
public long position(Blob pattern,
long start) throws SQLException {
checkClosed();
getBytesFromStream();
if (start < 1) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex"));
Object[] msgArgs = {start};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
if (null == pattern)
return -1;
return position(pattern.getBytes((long) 1, (int) pattern.length()), start);
}
/**
* Retrieves the byte position at which the specified byte array pattern begins within the BLOB value that this Blob object represents. The search
* for pattern begins at position start.
*
* @param bPattern
* - the byte array for which to search
* @param start
* - the position at which to begin searching; the first position is 1
* @return the position at which the pattern appears, else -1
* @throws SQLException
* - if there is an error accessing the BLOB or if start is less than 1
*/
public long position(byte[] bPattern,
long start) throws SQLException {
checkClosed();
getBytesFromStream();
if (start < 1) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex"));
Object[] msgArgs = {start};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
// Back compat: Handle null search string as not found rather than throw an exception.
// JDBC spec doesn't describe the behavior for a null pattern.
if (null == bPattern)
return -1;
// Adjust start to zero based.
start--;
// Search for pattern in value.
for (int pos = (int) start; pos <= value.length - bPattern.length; ++pos) {
boolean match = true;
for (int i = 0; i < bPattern.length; ++i) {
if (value[pos + i] != bPattern[i]) {
match = false;
break;
}
}
if (match) {
return pos + 1L;
}
}
return -1;
}
/* JDBC 3.0 methods */
/**
* Truncate a BLOB
*
* @param len
* the new length for the BLOB
* @throws SQLException
* when an error occurs
*/
public void truncate(long len) throws SQLException {
checkClosed();
getBytesFromStream();
if (len < 0) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength"));
Object[] msgArgs = {len};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
if (value.length > len) {
byte bNew[] = new byte[(int) len];
System.arraycopy(value, 0, bNew, 0, (int) len);
value = bNew;
}
}
/**
* Retrieves a stream that can be used to write to the BLOB value that this Blob object represents
*
* @param pos
* - the position in the BLOB value at which to start writing; the first position is 1
* @return a java.io.OutputStream object to which data can be written
* @throws SQLException
* - if there is an error accessing the BLOB value or if pos is less than 1
*/
public java.io.OutputStream setBinaryStream(long pos) throws SQLException {
checkClosed();
if (pos < 1) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex"));
SQLServerException.makeFromDriverError(con, null, form.format(new Object[] {pos}), null, true);
}
return new SQLServerBlobOutputStream(this, pos);
}
/**
* Writes the given array of bytes into the Blob starting at position pos, and returns the number of bytes written.
*
* @param pos
* the position (1 based) in the Blob object at which to start writing the data.
* @param bytes
* the array of bytes to be written into the Blob.
* @throws SQLException
* if there is an error accessing the BLOB value.
* @return the number of bytes written.
*/
public int setBytes(long pos,
byte[] bytes) throws SQLException {
checkClosed();
getBytesFromStream();
if (null == bytes)
SQLServerException.makeFromDriverError(con, null, SQLServerException.getErrString("R_cantSetNull"), null, true);
return setBytes(pos, bytes, 0, bytes.length);
}
/**
* Writes all or part of the given byte array to the BLOB value that this Blob object represents and returns the number of bytes written. Writing
* starts at position pos in the BLOB value; len bytes from the given byte wrray are written. The array of bytes will overwrite the existing bytes
* in the Blob object starting at the position pos. If the end of the Blob value is reached while writing the array bytes, then the length of the
* Blob value will be increased to accomodate the extra bytes.
*
* SQL Server behavior: If the value specified for pos is greater than the length+1 of the BLOB value then a SQLException is thrown.
*
* @param pos
* - the position in the BLOB object at which to start writing; the first position is 1
* @param bytes
* - the array of bytes to be written to this BLOB object.
* @param offset
* - the offset (0-based) into the array bytes at which to start reading the bytes to set
* @param len
* - the number of bytes to be written to the BLOB value from the array of bytes bytes
* @return the number of bytes written.
* @throws SQLException
* - if there is an error accessing the BLOB value or if pos is less than 1
*/
public int setBytes(long pos,
byte[] bytes,
int offset,
int len) throws SQLException {
checkClosed();
getBytesFromStream();
if (null == bytes)
SQLServerException.makeFromDriverError(con, null, SQLServerException.getErrString("R_cantSetNull"), null, true);
// Offset must be within incoming bytes boundary.
if (offset < 0 || offset > bytes.length) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidOffset"));
Object[] msgArgs = {offset};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
// len must be within incoming bytes boundary.
if (len < 0 || len > bytes.length - offset) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength"));
Object[] msgArgs = {len};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
// Note position for Blob.setBytes is 1 based not zero based.
// Position must be in range of existing Blob data or exactly 1 byte
// past the end of data to request "append" mode.
if (pos <= 0 || pos > value.length + 1) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidPositionIndex"));
Object[] msgArgs = {pos};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
// Adjust pos to zero based.
pos--;
// Overwrite past end of value case.
if (len >= value.length - pos) {
// Make sure the new value length wouldn't exceed the maximum allowed
DataTypes.getCheckedLength(con, JDBCType.BLOB, pos + len, false);
assert pos + len <= Integer.MAX_VALUE;
// Start with the original value, up to the starting position
byte combinedValue[] = new byte[(int) pos + len];
System.arraycopy(value, 0, combinedValue, 0, (int) pos);
// Copy rest of data.
System.arraycopy(bytes, offset, combinedValue, (int) pos, len);
value = combinedValue;
}
else {
// Overwrite internal to value case.
System.arraycopy(bytes, offset, value, (int) pos, len);
}
return len;
}
}
/**
* SQLServerBlobOutputStream is a simple java.io.OutputStream interface implementing class that forwards all calls to SQLServerBlob.setBytes. This
* class is returned to caller by SQLServerBlob class when setBinaryStream is called.
*
* SQLServerBlobOutputStream starts writing at postion startPos and continues to write in a forward only manner. Reset/mark are not supported.
*/
final class SQLServerBlobOutputStream extends java.io.OutputStream {
private SQLServerBlob parentBlob = null;
private long currentPos;
SQLServerBlobOutputStream(SQLServerBlob parentBlob,
long startPos) {
this.parentBlob = parentBlob;
this.currentPos = startPos;
}
// java.io.OutputStream interface methods.
public void write(byte[] b) throws IOException {
if (null == b)
return;
write(b, 0, b.length);
}
public void write(byte[] b,
int off,
int len) throws IOException {
try {
// Call parent's setBytes and update position.
// setBytes can throw a SQLServerException, we translate
// this to an IOException here.
int bytesWritten = parentBlob.setBytes(currentPos, b, off, len);
currentPos += bytesWritten;
}
catch (SQLException ex) {
throw new IOException(ex.getMessage());
}
}
public void write(int b) throws java.io.IOException {
byte[] bTemp = new byte[1];
bTemp[0] = (byte) (b & 0xFF);
write(bTemp, 0, bTemp.length);
}
}