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

org.firebirdsql.jdbc.FBBlob Maven / Gradle / Ivy

There is a newer version: 4.0.10.java7
Show newest version
/*
 * Firebird Open Source JavaEE Connector - JDBC Driver
 *
 * Distributable under LGPL license.
 * You may obtain a copy of the License at http://www.gnu.org/copyleft/lgpl.html
 *
 * This program 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
 * LGPL License for more details.
 *
 * This file was created by members of the firebird development team.
 * All individual contributions remain the Copyright (C) of those
 * individuals.  Contributors to this file are either listed here or
 * can be obtained from a source control history command.
 *
 * All rights reserved.
 */
package org.firebirdsql.jdbc;

import org.firebirdsql.gds.ISCConstants;
import org.firebirdsql.gds.JaybirdErrorCodes;
import org.firebirdsql.gds.VaxEncoding;
import org.firebirdsql.gds.impl.GDSHelper;
import org.firebirdsql.gds.ng.FbBlob;
import org.firebirdsql.gds.ng.FbExceptionBuilder;
import org.firebirdsql.gds.ng.FbTransaction;
import org.firebirdsql.gds.ng.TransactionState;
import org.firebirdsql.gds.ng.listeners.TransactionListener;
import org.firebirdsql.logging.Logger;
import org.firebirdsql.logging.LoggerFactory;
import org.firebirdsql.util.SQLExceptionChainBuilder;

import java.io.*;
import java.sql.Blob;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;

/**
 * Firebird implementation of {@link java.sql.Blob}.
 */
public class FBBlob implements FirebirdBlob, TransactionListener, Synchronizable {

    public static final boolean SEGMENTED = true;
    private static final Logger logger = LoggerFactory.getLogger(FBBlob.class);

    /**
     * The size of the buffer for blob input and output streams,
     * also used for the BufferedInputStream/BufferedOutputStream wrappers.
     */
    private final int bufferLength;
    private boolean isNew;
    private long blob_id;
    private volatile GDSHelper gdsHelper;
    private FBObjectListener.BlobListener blobListener;

    private final Collection inputStreams = Collections.synchronizedSet(new HashSet());
    private FBBlobOutputStream blobOut = null;

    private FBBlob(GDSHelper c, boolean isNew, FBObjectListener.BlobListener blobListener) {
        gdsHelper = c;
        this.isNew = isNew;
        bufferLength = c.getBlobBufferLength();
        this.blobListener = blobListener != null ? blobListener : FBObjectListener.NoActionBlobListener.instance();
    }

    /**
     * Create new Blob instance. This constructor creates new fresh Blob, only
     * writing to the Blob is allowed.
     *
     * @param c connection that will be used to write data to blob
     * @param blobListener Blob listener instance
     */
    public FBBlob(GDSHelper c, FBObjectListener.BlobListener blobListener) {
        this(c, true, blobListener);
    }

    /**
     * Create new Blob instance. This constructor creates new fresh Blob, only
     * writing to the Blob is allowed.
     *
     * @param c connection that will be used to write data to blob.
     */
    public FBBlob(GDSHelper c) {
        this(c, null);
    }

    /**
     * Create instance of this class to access existing Blob.
     *
     * @param c connection that will be used to access Blob.
     * @param blob_id ID of the Blob.
     * @param blobListener Blob listener instance
     */
    public FBBlob(GDSHelper c, long blob_id, FBObjectListener.BlobListener blobListener) {
        this(c, false, blobListener);
        this.blob_id = blob_id;
    }

    /**
     * Create instance of this class to access existing Blob.
     *
     * @param c connection that will be used to access Blob.
     * @param blob_id ID of the Blob.
     */
    public FBBlob(GDSHelper c, long blob_id) {
        this(c, blob_id, null);
    }

    public final Object getSynchronizationObject() {
        final GDSHelper gdsHelper = this.gdsHelper;
        if (gdsHelper == null) {
            return this;
        }
        return gdsHelper.getSynchronizationObject();
    }

    /**
     * This method frees the Blob object and releases the resources that
     * it holds. The object is invalid once the free
     * method is called.
     *

* 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. *

* * @throws SQLException if an error occurs releasing * the Blob's resources * @exception SQLFeatureNotSupportedException if the JDBC driver does not support * this method * @since 1.6 */ public void free() throws SQLException { synchronized (getSynchronizationObject()) { try { SQLExceptionChainBuilder chain = new SQLExceptionChainBuilder<>(); for (FBBlobInputStream blobIS : new ArrayList<>(inputStreams)) { try { blobIS.close(); } catch (IOException ex) { chain.append(new FBSQLException(ex)); } } inputStreams.clear(); if (blobOut != null) { try { blobOut.close(); } catch (IOException ex) { chain.append(new FBSQLException(ex)); } } if (chain.hasException()) throw chain.getException(); } finally { gdsHelper = null; blobListener = FBObjectListener.NoActionBlobListener.instance(); blobOut = null; } } } /** * Returns an InputStream object that contains a partial 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 Blob is at position 1 * @param length the length in bytes of the partial value to be retrieved * @return InputStream through which the partial 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 Blob or if pos + length is greater than the number of bytes * in the Blob * * @exception SQLFeatureNotSupportedException if the JDBC driver does not support * this method * @since 1.6 */ public InputStream getBinaryStream(long pos, long length) throws SQLException { throw new FBDriverNotCapableException("Method getBinaryStream(long, long) is not supported"); } /** * Get information about this Blob. This method should be considered as * temporary because it provides access to low-level API. More information * on how to use the API can be found in "API Guide". * * @param items items in which we are interested. * @param buffer_length buffer where information will be stored. * * @return array of bytes containing information about this Blob. * * @throws SQLException if something went wrong. */ public byte[] getInfo(byte[] items, int buffer_length) throws SQLException { synchronized (getSynchronizationObject()) { checkClosed(); blobListener.executionStarted(this); try { // TODO Does it make sense to close blob here? try (FbBlob blob = gdsHelper.openBlob(blob_id, SEGMENTED)) { return blob.getBlobInfo(items, buffer_length); } } finally { blobListener.executionCompleted(this); } } } public static final byte[] BLOB_LENGTH_REQUEST = new byte[] { ISCConstants.isc_info_blob_total_length }; /** * Returns the number of bytes in the BLOB value * designated by this Blob object. * @return length of the BLOB in bytes * @exception SQLException if there is an error accessing the * length of the BLOB * @exception SQLFeatureNotSupportedException if the JDBC driver does not support * this method * @since 1.2 */ public long length() throws SQLException { byte[] info = getInfo(BLOB_LENGTH_REQUEST, 20); return interpretLength(info, 0); } /** * Interpret BLOB length from buffer. * * @param info server response. * @param position where to start interpreting. * * @return length of the blob. * * @throws SQLException if length cannot be interpreted. */ long interpretLength(byte[] info, int position) throws SQLException { if (info[position] != ISCConstants.isc_info_blob_total_length) throw new FBSQLException("Length is not available."); int dataLength = VaxEncoding.iscVaxInteger(info, position + 1, 2); return VaxEncoding.iscVaxLong(info, position + 3, dataLength); } /** * Check if blob is segmented. * * @return true if this blob is segmented, * otherwise false * * @throws SQLException if something went wrong. */ public boolean isSegmented() throws SQLException { byte[] info = getInfo(new byte[] { ISCConstants.isc_info_blob_type }, 20); if (info[0] != ISCConstants.isc_info_blob_type) throw new FBSQLException("Cannot determine BLOB type"); int dataLength = VaxEncoding.iscVaxInteger(info, 1, 2); int type = VaxEncoding.iscVaxInteger(info, 3, dataLength); return type == ISCConstants.isc_bpb_type_segmented; } /** * Detach this blob. This method creates new instance of the same blob * database object that is not under result set control. When result set * is closed, all associated resources are also released, including open * blob streams. This method creates an new instance of blob object with * the same blob ID that can be used even when result set is closed. *

* Note, detached blob will not remember the stream position of this object. * This means that you cannot start reading data from the blob, then detach * it, and then continue reading. Reading from detached blob will begin at * the blob start. * * @return instance of {@link FBBlob} that is not under result set control. * * @throws SQLException if Blob cannot be detached. */ public FirebirdBlob detach() throws SQLException { synchronized (getSynchronizationObject()) { checkClosed(); return new FBBlob(gdsHelper, blob_id, blobListener); } } /** * 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 * @exception SQLException if there is an error accessing the * BLOB value; if pos is less than 1 or length is * less than 0 * @exception SQLFeatureNotSupportedException if the JDBC driver does not support * this method * @see #setBytes * @since 1.2 */ public byte[] getBytes(long pos, int length) throws SQLException { if (pos < 1) throw new FBSQLException("Blob position should be >= 1"); if (pos > Integer.MAX_VALUE) throw new FBSQLException("Blob position is limited to 2^31 - 1 " + "due to isc_seek_blob limitations.", SQLStateConstants.SQL_STATE_INVALID_ARG_VALUE); synchronized (getSynchronizationObject()) { blobListener.executionStarted(this); try { FirebirdBlob.BlobInputStream in = (FirebirdBlob.BlobInputStream) getBinaryStream(); try { if (pos != 1) in.seek((int) pos - 1); byte[] result = new byte[length]; in.readFully(result); return result; } finally { in.close(); } } catch (IOException ex) { throw new FBSQLException(ex); } finally { blobListener.executionCompleted(this); } } } public InputStream getBinaryStream() throws SQLException { synchronized (getSynchronizationObject()) { checkClosed(); FBBlobInputStream blobstream = new FBBlobInputStream(this); inputStreams.add(blobstream); return blobstream; } } public long position(byte[] pattern, long start) throws SQLException { throw new FBDriverNotCapableException("Method position(byte[], long) is not supported"); } public long position(Blob pattern, long start) throws SQLException { throw new FBDriverNotCapableException("Method position(Blob, long) is not supported"); } public void truncate(long len) throws SQLException { throw new FBDriverNotCapableException("Method truncate(long) is not supported"); } public int setBytes(long pos, byte[] bytes) throws SQLException { return setBytes(pos, bytes, 0, bytes.length); } public int setBytes(long pos, byte[] bytes, int offset, int len) throws SQLException { synchronized (getSynchronizationObject()) { try (OutputStream out = setBinaryStream(pos)) { out.write(bytes, offset, len); return len; } catch (IOException e) { throw new SQLException("IOException writing bytes to blob", e); } } } public OutputStream setBinaryStream(long pos) throws SQLException { synchronized (getSynchronizationObject()) { checkClosed(); blobListener.executionStarted(this); if (blobOut != null) throw new FBSQLException("OutputStream already open. Only one blob output stream can be open at a time."); if (pos < 1) throw new FBSQLException("You can't start before the beginning of the blob", SQLStateConstants.SQL_STATE_INVALID_ARG_VALUE); if (isNew && pos > 1) throw new FBSQLException("Previous value was null, you must start at position 1", SQLStateConstants.SQL_STATE_INVALID_ARG_VALUE); blobOut = new FBBlobOutputStream(this); if (pos > 1) { //copy pos bytes from input to output //implement this later throw new FBDriverNotCapableException("Offset start positions are not yet supported."); } return blobOut; } } //package methods /** * Get the identifier for this Blob * * @return This Blob's identifier * @throws SQLException if a database access error occurs */ public long getBlobId() throws SQLException { synchronized (getSynchronizationObject()) { if (isNew) throw new FBSQLException("No Blob ID is available in new Blob object."); return blob_id; } } void setBlobId(long blob_id) { synchronized (getSynchronizationObject()) { this.blob_id = blob_id; if (isNew && gdsHelper != null) { FbTransaction currentTransaction = gdsHelper.getCurrentTransaction(); if (currentTransaction != null) { currentTransaction.addWeakTransactionListener(this); } } isNew = false; } } public void copyBytes(byte[] bytes, int pos, int len) throws SQLException { synchronized (getSynchronizationObject()) { try (OutputStream out = setBinaryStream(1)) { out.write(bytes, pos, len); } catch (IOException ex) { throw new FBSQLException(ex); } } } /** * The size of the buffer for blob input and output streams, * also used for the BufferedInputStream/BufferedOutputStream wrappers. * * @return The buffer length */ int getBufferLength() { return bufferLength; } /** * Notifies this blob that stream has been closed. * * @param stream * InputStream that has been closed. */ void notifyClosed(FBBlobInputStream stream) { inputStreams.remove(stream); } /** * @return true when this is an uninitialized output blob, false otherwise. */ boolean isNew() { synchronized (getSynchronizationObject()) { return isNew; } } public GDSHelper getGdsHelper() { return gdsHelper; } /** * Copy the contents of an {@code InputStream} into this Blob. *

* Calling with length {@code -1} is equivalent to calling {@link #copyStream(InputStream)}, and will copy * the whole stream. *

* * @param inputStream the stream from which data will be copied * @param length The maximum number of bytes to read from the InputStream, {@code -1} to read whole stream * @throws SQLException if a database access error occurs */ public void copyStream(InputStream inputStream, long length) throws SQLException { synchronized (getSynchronizationObject()) { if (length == -1L) { copyStream(inputStream); return; } try (OutputStream os = setBinaryStream(1)) { final byte[] buffer = new byte[(int) Math.min(bufferLength, length)]; int chunk; while (length > 0 && (chunk = inputStream.read(buffer, 0, (int) Math.min(buffer.length, length))) != -1) { os.write(buffer, 0, chunk); length -= chunk; } } catch (IOException ioe) { throw new SQLException(ioe); } } } /** * Copy the contents of an InputStream into this Blob. Unlike * the {@link #copyStream(InputStream, long)} method, this one copies bytes * until the EOF is reached. * * @param inputStream the stream from which data will be copied * @throws SQLException if a database access error occurs */ public void copyStream(InputStream inputStream) throws SQLException { synchronized (getSynchronizationObject()) { try (OutputStream os = setBinaryStream(1)) { final byte[] buffer = new byte[bufferLength]; int chunk; while ((chunk = inputStream.read(buffer)) != -1) { os.write(buffer, 0, chunk); } } catch (IOException ioe) { throw new SQLException(ioe); } } } /** * Copy data from a character stream into this Blob. *

* Calling with length {@code -1} is equivalent to calling {@link #copyCharacterStream(Reader, String)}. *

* * @param reader the source of data to copy * @param length The maximum number of bytes to copy, or {@code -1} to read the whole stream * @param encoding The encoding used in the character stream * @deprecated Method signature will change incompatibly in Jaybird 4 */ @Deprecated public void copyCharacterStream(Reader reader, long length, String encoding) throws SQLException { synchronized (getSynchronizationObject()) { if (length == -1L) { copyCharacterStream(reader, encoding); return; } try (OutputStream os = setBinaryStream(1); OutputStreamWriter osw = encoding != null ? new OutputStreamWriter(os, encoding) : new OutputStreamWriter(os)) { final char[] buffer = new char[(int) Math.min(bufferLength, length)]; int chunk; while (length > 0 && (chunk = reader.read(buffer, 0, (int) Math.min(buffer.length, length))) != -1) { osw.write(buffer, 0, chunk); length -= chunk; } } catch (UnsupportedEncodingException ex) { throw new SQLException("Cannot set character stream because " + "the encoding '" + encoding + "' is unsupported in the JVM. " + "Please report this to the driver developers." ); } catch (IOException ioe) { throw new SQLException(ioe); } } } /** * Copy data from a character stream into this Blob. Unlike * the {@link #copyCharacterStream(Reader, long, String)} )} method, this one copies bytes * until the EOF is reached. * * @param reader the source of data to copy * @param encoding The encoding used in the character stream * @deprecated Method signature will change incompatibly in Jaybird 4 */ @Deprecated public void copyCharacterStream(Reader reader, String encoding) throws SQLException { synchronized (getSynchronizationObject()) { try (OutputStream os = setBinaryStream(1); OutputStreamWriter osw = encoding != null ? new OutputStreamWriter(os, encoding) : new OutputStreamWriter(os)) { final char[] buffer = new char[bufferLength]; int chunk; while ((chunk = reader.read(buffer, 0, buffer.length)) != -1) { osw.write(buffer, 0, chunk); } } catch (UnsupportedEncodingException ex) { throw new SQLException("Cannot set character stream because " + "the encoding '" + encoding + "' is unsupported in the JVM. " + "Please report this to the driver developers." ); } catch (IOException ioe) { throw new SQLException(ioe); } } } @Override public void transactionStateChanged(FbTransaction transaction, TransactionState newState, TransactionState previousState) { switch (newState) { case COMMITTED: case ROLLED_BACK: synchronized (getSynchronizationObject()) { try { free(); } catch (SQLException e) { logger.error("Error calling free on blob during transaction end", e); } } break; default: // Do nothing break; } } private void checkClosed() throws SQLException { if (gdsHelper == null) { throw FbExceptionBuilder.forException(JaybirdErrorCodes.jb_blobClosed).toFlatSQLException(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy