JDBCBlobFile.java

/* Copyright (c) 2001-2024, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.jdbc;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.io.Reader;
import java.io.Writer;

import java.sql.Blob;
import java.sql.SQLException;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.hsqldb.error.ErrorCode;
import org.hsqldb.lib.CountdownInputStream;
import org.hsqldb.lib.FileUtil;
import org.hsqldb.lib.FrameworkLogger;
import org.hsqldb.lib.InOutUtil;
import org.hsqldb.lib.KMPSearchAlgorithm;

/**
 * A client-side file-based implementation of Blob.
 *
 * <!-- start Release-specific documentation -->
 * <div class="ReleaseSpecificDocumentation">
 * <p class="rshead">HSQLDB-Specific Information:</p>
 *
 * Starting with 2.1, in addition to HSQLDB driver support for both client-side
 * in-memory and remote SQL CLOB data implementations, this class is provided
 * to expose efficient, relatively high-performance BLOB operations over client
 * accessible files.<p>
 *
 * <b>Design Notes</b><p>
 *
 * Although it is possible to implement a transactional version of this class,
 * the present implementation directly propagates changes to the underlying
 * file such that changes become visible as soon as they are either
 * implicitly or explicitly flushed to disk.
 *
 * </div>
 * <!-- end release-specific documentation -->
 * @author Campbell Burnet (campbell-burnet@users dot sourceforge.net)
 * @version 2.7.3
 * @since HSQLDB 2.1
 */
public class JDBCBlobFile implements java.sql.Blob {

    private static final FrameworkLogger LOG = FrameworkLogger.getLog(
        JDBCBlobFile.class);

    /**
     * Returns the number of bytes in the {@code BLOB} value
     * designated by this {@code Blob} object.
     * @return length of the {@code BLOB} in bytes
     * @throws SQLException if there is an error accessing the
     * length of the {@code BLOB}
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @since JDK 1.2
     */
    public long length() throws SQLException {

        checkClosed();

        try {
            return m_file.length();
        } catch (SecurityException e) {
            throw JDBCUtil.sqlException(e);
        }
    }

    /**
     * Retrieves all or part of the {@code BLOB}
     * value that this {@code Blob} object represents, as an array of
     * bytes.  This {@code byte} array contains up to {@code length}
     * consecutive bytes starting at position {@code pos}.
     *
     * @param pos the ordinal position of the first byte in the
     *        {@code 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 {@code length}
     *         consecutive bytes from the {@code BLOB} value designated
     *         by this {@code Blob} object, starting with the
     *         byte at position {@code pos}
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value; if pos is less than 1 or length is
     * less than 0
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #setBytes
     * @since JDK 1.2
     */
    public byte[] getBytes(
            final long pos,
            final int length)
            throws SQLException {

        InputStream           is   = null;
        ByteArrayOutputStream baos = null;
        final int initialBufferSize = Math.min(
            InOutUtil.DEFAULT_COPY_BUFFER_SIZE,
            length);

        try {
            is   = getBinaryStream(pos, length);
            baos = new ByteArrayOutputStream(initialBufferSize);

            InOutUtil.copy(is, baos, length);
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        } finally {
            closeSafely(is);
        }

        return baos.toByteArray();
    }

    /**
     * Retrieves the {@code BLOB} value designated by this
     * {@code Blob} instance as a stream.
     *
     * @return a stream containing the {@code BLOB} data
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #setBinaryStream
     * @since JDK 1.2
     */
    public InputStream getBinaryStream() throws SQLException {
        return getBinaryStream(1, Long.MAX_VALUE);
    }

    /**
     * Retrieves the byte position at which the specified byte array
     * {@code pattern} begins within the {@code BLOB}
     * value that this {@code Blob} object represents.  The
     * search for {@code pattern} begins at position
     * {@code start}.
     *
     * @param pattern 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
     * {@code BLOB} or if start is less than 1
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @since JDK 1.2
     */
    public long position(
            final byte[] pattern,
            final long start)
            throws SQLException {

        if (start < 1) {
            throw JDBCUtil.outOfRangeArgument("start: " + start);
        } else if (pattern == null || pattern.length == 0) {
            return -1L;
        }

        final long length = this.length();

        if (start > length
                || pattern.length > length
                || start > length - pattern.length) {
            return -1;
        }

        return position0(pattern, start);
    }

    private long position0(
            final byte[] pattern,
            final long start)
            throws SQLException {

        InputStream is = null;

        try {
            is = getBinaryStream(start, Long.MAX_VALUE);

            //@todo maybe single-byte encoding reader
            //      and java.util.Scanner.findWithinHorizon.
            //      Need to do comparative benchmark and unit
            //      tests first.
            final long matchOffset = KMPSearchAlgorithm.search(
                is,
                pattern,
                KMPSearchAlgorithm.computeTable(pattern));

            return (matchOffset == -1)
                   ? -1
                   : start + matchOffset;
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        } finally {
            closeSafely(is);
        }
    }

    /**
     * Retrieves the byte position in the {@code BLOB} value
     * designated by this {@code Blob} object at which
     * {@code pattern} begins.  The search begins at position
     * {@code start}.
     *
     * @param pattern the {@code Blob} object designating
     * the {@code BLOB} value for which to search
     * @param start the position in the {@code BLOB} value
     *        at which to begin searching; the first position is 1
     * @return the position at which the pattern begins, else -1
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value or if start is less than 1
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @since JDK 1.2
     */
    public long position(
            final Blob pattern,
            final long start)
            throws SQLException {

        if (start < 1) {
            throw JDBCUtil.outOfRangeArgument("start: " + start);
        }

        if (pattern == null) {
            return -1;
        }

        final long patternLength = pattern.length();

        if (patternLength == 0) {
            return -1;
        } else if (patternLength > Integer.MAX_VALUE) {
            throw JDBCUtil.outOfRangeArgument(
                "pattern.length(): " + patternLength);
        }

        final long length = this.length();

        if (start > length
                || patternLength > length
                || start > length - patternLength) {
            return -1;
        }

        final byte[] bytePattern = (pattern instanceof JDBCBlob)
                                   ? ((JDBCBlob) pattern).data()
                                   : pattern.getBytes(1L, (int) patternLength);

        return position0(bytePattern, start);
    }

    // -------------------------- JDBC 3.0 -----------------------------------

    /**
     * Writes the given array of bytes to the {@code BLOB} value that
     * this {@code Blob} object represents, starting at position
     * {@code pos}, and returns the number of bytes written.
     * The array of bytes will overwrite the existing bytes
     * in the {@code Blob} object starting at the position
     * {@code pos}.  If the end of the {@code Blob} value is reached
     * while writing the array of bytes, then the length of the {@code Blob}
     * value will be increased to accommodate the extra bytes.
     * <p>
     * <b>Note:</b> If the value specified for {@code pos}
     * is greater than the length+1 of the {@code BLOB} value then the
     * behavior is undefined. Some JDBC drivers may throw a
     * {@code SQLException} while other drivers may support this
     * operation.
     *
     * <!-- start release-specific documentation -->
     * <div class="ReleaseSpecificDocumentation">
     * <p class="rshead">HSQLDB-Specific Information:</p>
     *
     * This operation affects only the content of the underlying file; it has no
     * effect upon a value stored in a database. To propagate the updated
     * Blob value to a database, it is required to supply the Blob instance to
     * an updating or inserting setXXX method of a Prepared or Callable
     * Statement, or to supply the Blob instance to an updateXXX method of an
     * updatable ResultSet.
     *
     * </div>
     * <!-- end release-specific documentation -->
     *
     * @param pos the position in the {@code BLOB} object at which
     *        to start writing; the first position is 1
     * @param bytes the array of bytes to be written to the {@code BLOB}
     *        value that this {@code Blob} object represents
     * @return the number of bytes written
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value or if pos is less than 1
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #getBytes
     * @since JDK 1.4
     */
    public int setBytes(
            final long pos,
            final byte[] bytes)
            throws SQLException {

        return setBytes(
            pos,
            bytes,
            0,
            bytes == null
            ? 0
            : bytes.length);
    }

    /**
     * Writes all or part of the given {@code byte} array to the
     * {@code BLOB} value that this {@code Blob} object represents
     * and returns the number of bytes written.
     * Writing starts at position {@code pos} in the {@code BLOB}
     * value; {@code len} bytes from the given byte array are written.
     * The array of bytes will overwrite the existing bytes
     * in the {@code Blob} object starting at the position
     * {@code pos}.  If the end of the {@code Blob} value is reached
     * while writing the array of bytes, then the length of the {@code Blob}
     * value will be increased to accommodate the extra bytes.
     * <p>
     * <b>Note:</b> If the value specified for {@code pos}
     * is greater than the length+1 of the {@code BLOB} value then the
     * behavior is undefined. Some JDBC drivers may throw a
     * {@code SQLException} while other drivers may support this
     * operation.
     *
     * <!-- start release-specific documentation -->
     * <div class="ReleaseSpecificDocumentation">
     * <p class="rshead">HSQLDB-Specific Information:</p>
     *
     * This operation affects only the content of the underlying file; it has no
     * effect upon a value stored in a database. To propagate the updated
     * Blob value to a database, it is required to supply the Blob instance to
     * an updating or inserting setXXX method of a Prepared or Callable
     * Statement, or to supply the Blob instance to an updateXXX method of an
     * updatable ResultSet.
     *
     * </div>
     * <!-- end release-specific documentation -->
     *
     * @param pos the position in the {@code BLOB} object at which
     *        to start writing; the first position is 1
     * @param bytes the array of bytes to be written to this {@code BLOB}
     *        object
     * @param offset the offset into the array {@code bytes} at which
     *        to start reading the bytes to be set
     * @param len the number of bytes to be written to the {@code BLOB}
     *        value from the array of bytes {@code bytes}
     * @return the number of bytes written
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value or if pos is less than 1
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #getBytes
     * @since JDK 1.4
     */
    public int setBytes(
            final long pos,
            final byte[] bytes,
            final int offset,
            final int len)
            throws SQLException {

        if (bytes == null) {
            throw JDBCUtil.nullArgument("bytes");
        }

        final OutputStream os = setBinaryStream(pos);

        try {
            os.write(bytes, offset, len);
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        } finally {
            closeSafely(os);
        }

        return len;
    }

    /**
     * Retrieves a stream that can be used to write to the {@code BLOB}
     * value that this {@code Blob} object represents.  The stream begins
     * at position {@code pos}.
     * The  bytes written to the stream will overwrite the existing bytes
     * in the {@code Blob} object starting at the position
     * {@code pos}.  If the end of the {@code Blob} value is reached
     * while writing to the stream, then the length of the {@code Blob}
     * value will be increased to accommodate the extra bytes.
     * <p>
     * <b>Note:</b> If the value specified for {@code pos}
     * is greater than the length+1 of the {@code BLOB} value then the
     * behavior is undefined. Some JDBC drivers may throw a
     * {@code SQLException} while other drivers may support this
     * operation.
     *
     * <!-- start release-specific documentation -->
     * <div class="ReleaseSpecificDocumentation">
     * <p class="rshead">HSQLDB-Specific Information:</p>
     *
     * Data written to the returned stream affects only the content of the
     * underlying file; it has no effect upon a value stored in a database.
     * To propagate the updated Blob value to a database, it is required to
     * supply the Blob instance to an updating or inserting setXXX method of a
     * Prepared or Callable Statement, or to supply the Blob instance to an
     * updateXXX method of an updateable ResultSet.
     *
     * </div>
     * <!-- end release-specific documentation -->
     *
     * @param pos the position in the {@code BLOB} value at which
     *        to start writing; the first position is 1
     * @return a {@code java.io.OutputStream} object to which data can
     *         be written
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value or if pos is less than 1
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #getBinaryStream
     * @since JDK 1.4
     */
    public OutputStream setBinaryStream(final long pos) throws SQLException {

        if (pos < 1) {
            throw JDBCUtil.invalidArgument("pos: " + pos);
        }

        checkClosed();
        createFile();

        OutputStream adapter;

        try {
            adapter = new OutputStreamAdapter(m_file, pos - 1) {

                private boolean closed;
                public synchronized void close() throws IOException {

                    if (closed) {
                        return;
                    }

                    closed = true;

                    try {
                        super.close();
                    } finally {
                        m_streams.remove(this);
                    }
                }
            };
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        } catch (IllegalArgumentException ex) {
            throw JDBCUtil.sqlException(ex);
        } catch (SecurityException ex) {
            throw JDBCUtil.sqlException(ex);
        }

        m_streams.add(adapter);

        final OutputStream result = new BufferedOutputStream(adapter);

        return result;
    }

    /**
     * Truncates the {@code BLOB} value that this {@code Blob}
     * object represents to be {@code len} bytes in length.
     * <p>
     * <b>Note:</b> If the value specified for {@code pos}
     * is greater than the length+1 of the {@code BLOB} value then the
     * behavior is undefined. Some JDBC drivers may throw a
     * {@code SQLException} while other drivers may support this
     * operation.
     *
     * <!-- start release-specific documentation -->
     * <div class="ReleaseSpecificDocumentation">
     * <p class="rshead">HSQLDB-Specific Information:</p>
     *
     * This operation affects only the length of the underlying file; it has no
     * effect upon a value stored in a database. To propagate the truncated
     * Blob value to a database, it is required to supply the Blob instance to
     * an updating or inserting setXXX method of a Prepared or Callable
     * Statement, or to supply the Blob instance to an updateXXX method of an
     * updatable ResultSet.
     *
     * </div>
     * <!-- end release-specific documentation -->
     *
     * @param len the length, in bytes, to which the {@code BLOB} value
     *        that this {@code Blob} object represents should be truncated
     * @throws SQLException if there is an error accessing the
     *            {@code BLOB} value or if len is less than 0
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @since JDK 1.4
     */
    public void truncate(long len) throws SQLException {

        if (len < 0) {
            throw JDBCUtil.invalidArgument("len: " + len);
        }

        checkClosed();

        RandomAccessFile randomAccessFile = null;

        try {
            randomAccessFile = new RandomAccessFile(m_file, "rw");

            randomAccessFile.setLength(len);
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        } catch (IllegalArgumentException ex) {
            throw JDBCUtil.sqlException(ex);
        } catch (SecurityException ex) {
            throw JDBCUtil.sqlException(ex);
        } finally {
            closeSafely(randomAccessFile);
        }
    }

    /**
     * This method frees the {@code Blob} object and releases the resources that
     * it holds. The object is invalid once the {@code free}
     * method is called.
     * <p>
     * After {@code free} has been called, any attempt to invoke a
     * method other than {@code free} will result in a {@code SQLException}
     * being thrown.  If {@code free} is called multiple times, the subsequent
     * calls to {@code free} are treated as a no-op.
     *
     * <!-- start release-specific documentation -->
     * <div class="ReleaseSpecificDocumentation">
     * <p class="rshead">HSQLDB-Specific Information:</p>
     *
     * This operation closes any input and/or output streams obtained
     * via {@link #getBinaryStream()}, {@link #getBinaryStream(long, long)} or
     * {@link #setBinaryStream(long)}. <p>
     *
     * Additionally, if the property {@link #isDeleteOnFree()} is true, then
     * an attempt is made to delete the backing file.
     *
     * </div>
     * <!-- end release-specific documentation -->
     *
     * @throws SQLException if an error occurs releasing
     * the Blob's resources
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @see #setDeleteOnFree(boolean)
     * @see #isDeleteOnFree()
     * @since JDK 1.6
     */
    public synchronized void free() throws SQLException {

        if (m_closed) {
            return;
        }

        m_closed = true;

        final List<Object> streams = new ArrayList<>(m_streams);

        m_streams = null;

        for (Iterator<Object> itr = streams.iterator(); itr.hasNext(); ) {
            final Object stream = itr.next();

            closeSafely(stream);
        }

        if (m_deleteOnFree) {
            try {
                m_file.delete();
            } catch (SecurityException e) {}
        }
    }

    /**
     * Returns an {@code InputStream} object that contains a partial {@code Blob} value,
     * starting  with the byte specified by pos, which is length bytes in length.
     *
     * @param pos the offset to the first byte of the partial value to be retrieved.
     *  The first byte in the {@code Blob} is at position 1
     * @param length the length in bytes of the partial value to be retrieved
     * @return {@code InputStream} through which the partial {@code Blob} value can be read.
     * @throws SQLException if pos is less than 1 or if pos is greater than the number of bytes
     * in the {@code Blob} or if pos + length is greater than the number of bytes
     * in the {@code Blob}
     *
     * @throws java.sql.SQLFeatureNotSupportedException if the JDBC driver does not support
     * this method
     * @since JDK 1.6
     */
    public InputStream getBinaryStream(
            final long pos,
            final long length)
            throws SQLException {

        if (pos < 1) {
            throw JDBCUtil.outOfRangeArgument("pos: " + pos);
        }

        checkClosed();

        InputStream        result;
        final List<Object> streams = m_streams;

        try {
            result = new InputStreamAdapter(m_file, pos - 1, length) {

                private boolean           closed;
                private final InputStream self = this;
                public void close() throws IOException {

                    if (closed) {
                        return;
                    }

                    closed = true;

                    try {
                        super.close();
                    } finally {
                        streams.remove(self);
                    }
                }
            };
        } catch (Exception ex) {
            throw JDBCUtil.sqlException(ex);
        }

        streams.add(result);

        return result;
    }

    /**
     * Retrieves the canonical {@code File} object denoting the file that
     * backs this BLOB.
     *
     * @return the file that backs this BLOB.
     */
    public File getFile() {
        return m_file;
    }

    /**
     * Retrieves whether an attempt to delete the backing file
     * is made in response to invocation of {@link #free()}.
     *
     * @return true if backing file deletion is attempted; otherwise false.
     */
    public boolean isDeleteOnFree() {
        return m_deleteOnFree;
    }

    /**
     * Assigns whether an attempt to delete the backing file
     * is made in response to invocation of {@link #free()}.
     *
     * @param deleteOnFree the new value to assign
     */
    public void setDeleteOnFree(boolean deleteOnFree) {
        m_deleteOnFree = deleteOnFree;
    }

    //--------------------------------------------------------------------------
    // Internal Implementation
    //--------------------------------------------------------------------------
    public static final String TEMP_FILE_PREFIX = "hsql_jdbc_blob_file_";
    public static final String TEMP_FILE_SUFFIX = ".tmp";

    //
    private final File   m_file;
    private boolean      m_closed;
    private boolean      m_deleteOnFree;
    private List<Object> m_streams = new ArrayList<>();

    /**
     * Convenience constructor; equivalent to JDBCBlobFile(true);
     *
     * @throws SQLException If a file could not be created or if a security
     *         manager exists and its {@code {@link
     *         java.lang.SecurityManager#checkWrite(java.lang.String)}}
     *         method does not allow a file to be created.
     */
    public JDBCBlobFile() throws SQLException {
        this(true);
    }

    /**
     * Constructs a new instance backed by a File object created in response
     * to invoking File.createTempFile(TEMP_FILE_PREFIX,TEMP_FILE_SUFFIX).
     *
     * @param deleteOnFree Assigns whether an attempt to delete the backing file
     *                     is to be made in response to invocation of {@link #free()}.
     *
     * @throws SQLException If a file could not be created or if a security
     *         manager exists and its {@code {@link
     *         java.lang.SecurityManager#checkWrite(java.lang.String)}}
     *         method does not allow a file to be created.
     */
    public JDBCBlobFile(boolean deleteOnFree) throws SQLException {

        m_deleteOnFree = deleteOnFree;

        try {
            m_file = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX)
                         .getCanonicalFile();
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        }
    }

    /**
     * Convenience constructor; equivalent to JDBCBlobFile(file, false);
     *
     * @param file used to back this BLOB instance.
     * @throws SQLException If an I/O error occurs, which is possible because
     *         the construction of the canonical pathname may require file system
     *         queries; if a required system property value cannot be accessed,
     *         or if a security manager exists and its {@code {@link
     *         java.lang.SecurityManager#checkRead}} method denies
     *         read access to the file
     */
    public JDBCBlobFile(final File file) throws SQLException {
        this(file, false);
    }

    /**
     *  Constructs a new instance backed by the given File object.
     *
     * @param file used to back this BLOB instance.
     * @param deleteOnFree Assigns whether an attempt to delete the backing file
     *                     is to be made in response to invocation of {@link #free()}.
     * @throws SQLException If an I/O error occurs, which is possible because
     *         the construction of the canonical pathname may require file system
     *         queries; if a required system property value cannot be accessed,
     *         or if a security manager exists and its {@code {@link
     *         java.lang.SecurityManager#checkRead}} method denies
     *         read access to the file
     */
    public JDBCBlobFile(
            final File file,
            boolean deleteOnFree)
            throws SQLException {

        m_deleteOnFree = deleteOnFree;

        try {
            m_file = file.getCanonicalFile();
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        }

        checkIsFile( /*checkExists*/false);
    }

    protected final void checkIsFile(boolean checkExists) throws SQLException {

        boolean exists = false;
        boolean isFile = false;

        try {
            exists = m_file.exists();
        } catch (Exception ex) {
            throw JDBCUtil.sqlException(ex);
        }

        if (exists) {
            try {
                isFile = m_file.isFile();
            } catch (Exception ex) {
                throw JDBCUtil.sqlException(ex);
            }
        }

        if (exists) {
            if (!isFile) {
                throw JDBCUtil.invalidArgument("Is not a file: " + m_file);
            }
        } else if (checkExists) {
            throw JDBCUtil.invalidArgument("Does not exist: " + m_file);
        }
    }

    private void checkClosed() throws SQLException {
        if (m_closed) {
            throw JDBCUtil.sqlException(ErrorCode.X_07501);
        }
    }

    private void createFile() throws SQLException {

        try {
            if (!m_file.exists()) {
                FileUtil.getFileUtil().makeParentDirectories(m_file);
                m_file.createNewFile();
            }
        } catch (IOException ex) {
            throw JDBCUtil.sqlException(ex);
        }

        checkIsFile( /*checkExists*/true);
    }

    //<editor-fold defaultstate="collapsed" desc="JAVA 1.2 compliant closeSafely(...)">
    private static void closeSafely(RandomAccessFile target) {

        if (target != null) {
            try {
                target.close();
            } catch (IOException ignoredIoe) {
                LOG.info(ignoredIoe.getMessage(), ignoredIoe);
            } catch (Throwable ignoredRex) {
                LOG.info(ignoredRex.getMessage(), ignoredRex);
            }
        }
    }

    private static void closeSafely(InputStream target) {

        if (target != null) {
            try {
                target.close();
            } catch (IOException ignoredIoe) {
                LOG.info(ignoredIoe.getMessage(), ignoredIoe);
            } catch (Throwable ignoredRex) {
                LOG.info(ignoredRex.getMessage(), ignoredRex);
            }
        }
    }

    private static void closeSafely(OutputStream target) {

        if (target != null) {
            try {
                target.close();
            } catch (IOException ignoredIoe) {
                LOG.info(ignoredIoe.getMessage(), ignoredIoe);
            } catch (Throwable ignoredRex) {
                LOG.info(ignoredRex.getMessage(), ignoredRex);
            }
        }
    }

    private static void closeSafely(Reader target) {

        if (target != null) {
            try {
                target.close();
            } catch (IOException ignoredIoe) {
                LOG.info(ignoredIoe.getMessage(), ignoredIoe);
            } catch (Throwable ignoredRex) {
                LOG.info(ignoredRex.getMessage(), ignoredRex);
            }
        }
    }

    private static void closeSafely(Writer target) {

        if (target != null) {
            try {
                target.close();
            } catch (IOException ignoredIoe) {
                LOG.info(ignoredIoe.getMessage(), ignoredIoe);
            } catch (Throwable ignoredRex) {
                LOG.info(ignoredRex.getMessage(), ignoredRex);
            }
        }
    }

    private void closeSafely(Object target) {

        if (target instanceof RandomAccessFile) {
            closeSafely((RandomAccessFile) target);
        } else if (target instanceof InputStream) {
            closeSafely((InputStream) target);
        } else if (target instanceof OutputStream) {
            closeSafely((OutputStream) target);
        } else if (target instanceof Reader) {
            closeSafely((Reader) target);
        } else if (target instanceof Writer) {
            closeSafely((Writer) target);
        }
    }

    //</editor-fold>
    // @todo JDK7 + can use java.nio.SeekableByteChannel instead of adapters.
    protected static class OutputStreamAdapter extends OutputStream {

        private final RandomAccessFile m_randomAccessFile;

        protected OutputStreamAdapter(
                final File file,
                final long pos)
                throws IOException,
                       IllegalArgumentException,
                       NullPointerException,
                       SecurityException {

            if (pos < 0) {
                throw new IllegalArgumentException("pos: " + pos);
            }

            m_randomAccessFile = new RandomAccessFile(file, "rw");

            boolean seekSucceeded = false;

            try {
                m_randomAccessFile.seek(pos);

                seekSucceeded = true;
            } finally {
                if (!seekSucceeded) {
                    closeSafely(m_randomAccessFile);
                }
            }
        }

        protected OutputStreamAdapter(RandomAccessFile randomAccessFile) {

            if (randomAccessFile == null) {
                throw new NullPointerException();
            }

            m_randomAccessFile = randomAccessFile;
        }

        public void write(int b) throws IOException {
            m_randomAccessFile.write(b);
        }

        public void write(byte[] b) throws IOException {
            m_randomAccessFile.write(b);
        }

        public void write(byte[] b, int off, int len) throws IOException {
            m_randomAccessFile.write(b, off, len);
        }

        public void flush() throws IOException {
            if (m_randomAccessFile.getFD().valid()) {
                m_randomAccessFile.getFD().sync();
            }
        }

        public void close() throws IOException {
            m_randomAccessFile.close();
        }
    }

    static class InputStreamAdapter extends InputStream {

        private final CountdownInputStream m_ciStream;

        InputStreamAdapter(
                final File file,
                final long pos,
                final long length)
                throws IOException,
                       SecurityException,
                       NullPointerException,
                       IllegalArgumentException {

            if (file == null) {
                throw new NullPointerException("file");
            }

            if (pos < 0) {
                throw new IllegalArgumentException("pos: " + pos);
            }

            if (length < 0) {
                throw new IllegalArgumentException("length: " + length);
            }

            FileInputStream fis     = null;
            boolean         success = false;

            try {
                fis = new FileInputStream(file);

                if (pos > 0) {

                    // skip is a 'native' method and is likely to
                    // be at least as efficient as RandomAccessFile.seek
                    final long actualPos = fis.skip(pos);

                    // Ignoring actualPos because it is 'ok' if
                    // pos > file.length or pos > file.length - length, since
                    // the client code ends up with an 'empty' stream (no data).
                }

                success = true;
            } finally {
                if (!success) {
                    closeSafely(fis);
                }
            }

            final BufferedInputStream  bis = new BufferedInputStream(fis);
            final CountdownInputStream cis = new CountdownInputStream(bis);

            cis.setCount(length);

            m_ciStream = cis;
        }

        public int available() throws IOException {
            return m_ciStream.available();
        }

        public int read() throws IOException {
            return m_ciStream.read();
        }

        public int read(byte[] b) throws IOException {
            return m_ciStream.read(b);
        }

        public int read(byte[] b, int off, int len) throws IOException {
            return m_ciStream.read(b, off, len);
        }

        public long skip(long n) throws IOException {
            return m_ciStream.skip(n);
        }

        public void close() throws IOException {
            m_ciStream.close();
        }
    }
}