/* * 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<Closeable> activeStreams = new ArrayList<Closeable>(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. * <p> * 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 = {new Long(pos)}; SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true); } if (length < 0) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_invalidLength")); Object[] msgArgs = {new Integer(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 = {new Long(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 = {new Long(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 + 1; } 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 = {new Long(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 = {new Integer(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 = {new Integer(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 = {new Long(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. * <p> * 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); } }