Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ public final class BlobProxy implements Blob, BlobImplementer {
// no longer necessary. The class name could be updated to reflect this but that would break APIs.

private final BinaryStream binaryStream;
private final int markBytes;
private boolean resetAllowed;
private boolean needsReset;

/**
Expand All @@ -50,6 +52,8 @@ public final class BlobProxy implements Blob, BlobImplementer {
*/
private BlobProxy(byte[] bytes) {
binaryStream = new ArrayBackedBinaryStream( bytes );
markBytes = bytes.length + 1;
setStreamMark();
}

/**
Expand All @@ -61,6 +65,19 @@ private BlobProxy(byte[] bytes) {
*/
private BlobProxy(InputStream stream, long length) {
this.binaryStream = new StreamBackedBinaryStream( stream, length );
this.markBytes = (int) length + 1;
setStreamMark();
}

private void setStreamMark() {
final InputStream inputStream = binaryStream.getInputStream();
if ( inputStream != null && inputStream.markSupported() ) {
inputStream.mark( markBytes );
resetAllowed = true;
}
else {
resetAllowed = false;
}
}

private InputStream getStream() throws SQLException {
Expand All @@ -76,7 +93,14 @@ public BinaryStream getUnderlyingStream() throws SQLException {
private void resetIfNeeded() throws SQLException {
try {
if ( needsReset ) {
binaryStream.getInputStream().reset();
final InputStream inputStream = binaryStream.getInputStream();
if ( !resetAllowed && inputStream != null) {
throw new SQLException( "Underlying stream does not allow reset" );
}
if ( inputStream != null ) {
inputStream.reset();
setStreamMark();
}
}
}
catch ( IOException ioe) {
Expand All @@ -99,6 +123,11 @@ public static Blob generateProxy(byte[] bytes) {
/**
* Generates a BlobImpl proxy using a given number of bytes from an InputStream.
*
* Be aware that certain database drivers will automatically close the provided InputStream after the
* contents have been written to the database. This may cause unintended side effects if the entity
* is also audited by Envers. In this case, it's recommended to use {@link #generateProxy(byte[])}
* instead as it isn't affected by this non-standard behavior.
*
* @param stream The input stream of bytes to be created as a Blob.
* @param length The number of bytes from stream to be written to the Blob.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* Copyright Red Hat Inc. and Hibernate Authors
*/
package org.hibernate.orm.test.envers.integration.blob;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import org.hamcrest.Matchers;
import org.hibernate.dialect.PostgreSQLDialect;
import org.hibernate.dialect.SQLServerDialect;
import org.hibernate.engine.jdbc.proxy.BlobProxy;
import org.hibernate.envers.Audited;
import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase;
import org.hibernate.orm.test.envers.Priority;
import org.hibernate.testing.SkipForDialect;
import org.junit.Test;

import java.io.BufferedInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.Blob;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hibernate.testing.transaction.TransactionUtil.doInJPA;
import static org.junit.Assert.fail;

/**
* @author Chris Cranford
*/
public class BasicBlobTest extends BaseEnversJPAFunctionalTestCase {

@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] {Asset.class};
}

@Test
@Priority(10)
public void testGenerateProxyNoStream() {
final Path path = Path.of( Thread.currentThread().getContextClassLoader()
.getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).getPath() );
doInJPA( this::entityManagerFactory, entityManager -> {
final Asset asset = new Asset();
asset.setFileName( "blob.txt" );

try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) {
assertThat( stream.markSupported(), Matchers.is( true ) );

// We use the method readAllBytes instead of passing the raw stream to the proxy
// since this is the only guaranteed way that will work across all dialects in a
// deterministic way. Postgres and Sybase will automatically close the stream
// after the blob has been written by the driver, which prevents Envers from
// then writing the contents of the stream to the audit table.
//
// If the driver and dialect are known not to close the input stream after the
// contents have been written by the driver, then it's safe to pass the stream
// here instead and the stream will be automatically marked and reset so that
// Envers can serialize the data after Hibernate has done so. Dialects like
// H2, MySQL, Oracle, SQL Server work this way.
//
//
Blob blob = BlobProxy.generateProxy( stream.readAllBytes() );

asset.setData( blob );
entityManager.persist( asset );
}
catch (Exception e) {
e.printStackTrace();
fail( "Failed to persist the entity" );
}
} );

}

@Test
@Priority(10)
@SkipForDialect(value = PostgreSQLDialect.class,
comment = "The driver closes the stream, so it cannot be reused by envers")
@SkipForDialect(value = SQLServerDialect.class,
comment = "The driver closes the stream, so it cannot be reused by envers")
public void testGenerateProxyStream() {
final Path path = Path.of( Thread.currentThread().getContextClassLoader()
.getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).getPath() );

try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) {
doInJPA( this::entityManagerFactory, entityManager -> {
final Asset asset = new Asset();
asset.setFileName( "blob.txt" );

assertThat( stream.markSupported(), Matchers.is( true ) );

// We use the method readAllBytes instead of passing the raw stream to the proxy
// since this is the only guaranteed way that will work across all dialects in a
// deterministic way. Postgres and Sybase will automatically close the stream
// after the blob has been written by the driver, which prevents Envers from
// then writing the contents of the stream to the audit table.
//
// If the driver and dialect are known not to close the input stream after the
// contents have been written by the driver, then it's safe to pass the stream
// here instead and the stream will be automatically marked and reset so that
// Envers can serialize the data after Hibernate has done so. Dialects like
// H2, MySQL, Oracle, SQL Server work this way.
//
//
Blob blob = BlobProxy.generateProxy( stream, 9192L );

asset.setData( blob );
entityManager.persist( asset );
} );
}
catch (Exception e) {
e.printStackTrace();
fail( "Failed to persist the entity" );
}
}

@Audited
@Entity(name = "Asset")
public static class Asset {
@Id
@GeneratedValue
private Integer id;
private String fileName;
private Blob data;

public Integer getId() {
return id;
}

public void setId(Integer id) {
this.id = id;
}

public String getFileName() {
return fileName;
}

public void setFileName(String fileName) {
this.fileName = fileName;
}

public Blob getData() {
return data;
}

public void setData(Blob data) {
this.data = data;
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version='1.0' encoding='utf-8'?>
<!--
~ SPDX-License-Identifier: LGPL-2.1-or-later
~ Copyright Red Hat Inc. and Hibernate Authors
-->
<!DOCTYPE hibernate-configuration PUBLIC
"-//Hibernate/Hibernate Configuration DTD//EN"
"http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
<session-factory>
<property name="hbm2ddl.auto">create-drop</property>

<property name="show_sql">false</property>
<property name="format_sql">false</property>

<property name="dialect">org.hibernate.dialect.H2Dialect</property>
<property name="connection.url">jdbc:h2:mem:envers</property>
<property name="connection.driver_class">org.h2.Driver</property>
<property name="connection.username">sa</property>
<property name="connection.password"></property>

<!--<property name="dialect">org.hibernate.dialect.MySQL5Dialect</property>-->
<!--<property name="connection.url">jdbc:mysql:///hibernate_tests?useUnicode=true&amp;characterEncoding=UTF-8</property>-->
<!--<property name="connection.driver_class">com.mysql.jdbc.Driver</property>-->
<!--<property name="connection.username">root</property>-->
<!--<property name="connection.password"></property>-->

<!--<property name="hibernate.jdbc.batch_size">100</property>-->
</session-factory>
</hibernate-configuration>
Loading