diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java index 3475b2310c..571d1d9ef6 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java @@ -75,24 +75,31 @@ public class BlobProxy implements InvocationHandler { } private InputStream getStream() throws SQLException { - InputStream stream = binaryStream.getInputStream(); + return getUnderlyingStream().getInputStream(); + } + + private BinaryStream getUnderlyingStream() throws SQLException { + resetIfNeeded(); + return binaryStream; + } + + private void resetIfNeeded() throws SQLException { try { if ( needsReset ) { - stream.reset(); + binaryStream.getInputStream().reset(); } } catch ( IOException ioe) { throw new SQLException("could not reset reader"); } needsReset = true; - return stream; } /** * {@inheritDoc} * * @throws UnsupportedOperationException if any methods other than - * {@link Blob#length}, {@link Blob#getUnderlyingStream}, + * {@link Blob#length}, {@link BlobImplementer#getUnderlyingStream}, * {@link Blob#getBinaryStream}, {@link Blob#getBytes}, {@link Blob#free}, * or toString/equals/hashCode are invoked. */ @@ -106,7 +113,7 @@ public class BlobProxy implements InvocationHandler { return Long.valueOf( getLength() ); } if ( "getUnderlyingStream".equals( methodName ) ) { - return binaryStream; + return getUnderlyingStream(); // Reset stream if needed. } if ( "getBinaryStream".equals( methodName ) ) { if ( argCount == 0 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/ClobProxy.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/ClobProxy.java index f45489f2d9..c62e2dd414 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/ClobProxy.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/ClobProxy.java @@ -77,13 +77,16 @@ public class ClobProxy implements InvocationHandler { } protected InputStream getAsciiStream() throws SQLException { - resetIfNeeded(); - return new ReaderInputStream( characterStream.asReader() ); + return new ReaderInputStream( getCharacterStream() ); } protected Reader getCharacterStream() throws SQLException { + return getUnderlyingStream().asReader(); + } + + protected CharacterStream getUnderlyingStream() throws SQLException { resetIfNeeded(); - return characterStream.asReader(); + return characterStream; } protected String getSubString(long start, int length) { @@ -96,8 +99,10 @@ public class ClobProxy implements InvocationHandler { /** * {@inheritDoc} * - * @throws UnsupportedOperationException if any methods other than {@link Clob#length()}, - * {@link Clob#getAsciiStream()}, or {@link Clob#getCharacterStream()} are invoked. + * @throws UnsupportedOperationException if any methods other than {@link Clob#length}, + * {@link Clob#getAsciiStream}, {@link Clob#getCharacterStream}, + * {@link ClobImplementer#getUnderlyingStream}, {@link Clob#getSubString}, + * {@link Clob#free}, or toString/equals/hashCode are invoked. */ @Override @SuppressWarnings({ "UnnecessaryBoxing" }) @@ -109,7 +114,7 @@ public class ClobProxy implements InvocationHandler { return Long.valueOf( getLength() ); } if ( "getUnderlyingStream".equals( methodName ) ) { - return characterStream; + return getUnderlyingStream(); // Reset stream if needed. } if ( "getAsciiStream".equals( methodName ) && argCount == 0 ) { return getAsciiStream(); diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobHolder.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobHolder.java new file mode 100644 index 0000000000..365dcc72af --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobHolder.java @@ -0,0 +1,87 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2009 by Red Hat Inc and/or its affiliates or by + * third-party contributors as indicated by either @author tags or express + * copyright attribution statements applied by the authors. All + * third-party contributions are distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * 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 GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.test.annotations.lob.locator; + +import java.sql.Blob; +import java.sql.Clob; +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +@Entity +public class LobHolder { + @Id + @GeneratedValue + private Long id; + + private Clob clobLocator; + + private Blob blobLocator; + + private Integer counter; + + public LobHolder() { + } + + public LobHolder(Blob blobLocator, Clob clobLocator, Integer counter) { + this.blobLocator = blobLocator; + this.clobLocator = clobLocator; + this.counter = counter; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Clob getClobLocator() { + return clobLocator; + } + + public void setClobLocator(Clob clobLocator) { + this.clobLocator = clobLocator; + } + + public Blob getBlobLocator() { + return blobLocator; + } + + public void setBlobLocator(Blob blobLocator) { + this.blobLocator = blobLocator; + } + + public Integer getCounter() { + return counter; + } + + public void setCounter(Integer counter) { + this.counter = counter; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobLocatorTest.java b/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobLocatorTest.java new file mode 100644 index 0000000000..b721ed709f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/annotations/lob/locator/LobLocatorTest.java @@ -0,0 +1,97 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2009 by Red Hat Inc and/or its affiliates or by + * third-party contributors as indicated by either @author tags or express + * copyright attribution statements applied by the authors. All + * third-party contributions are distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * 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 GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.test.annotations.lob.locator; + +import java.sql.SQLException; + +import org.junit.Assert; +import org.junit.Test; + +import org.hibernate.Session; +import org.hibernate.testing.DialectChecks; +import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.type.descriptor.java.DataHelper; + +/** + * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) + */ +public class LobLocatorTest extends BaseCoreFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { LobHolder.class }; + } + + /** + * Specific JDBC drivers (e.g. SQL Server) may not automatically rewind bound input stream + * during statement execution. Such behavior results in error message similar to: + * {@literal The stream value is not the specified length. The specified length was 4, the actual length is 0.} + */ + @Test + @TestForIssue(jiraKey = "HHH-8193") + @RequiresDialectFeature(DialectChecks.UsesInputStreamToInsertBlob.class) + public void testStreamResetBeforeParameterBinding() throws SQLException { + final Session session = openSession(); + + session.getTransaction().begin(); + LobHolder entity = new LobHolder( + session.getLobHelper().createBlob( "blob".getBytes() ), + session.getLobHelper().createClob( "clob" ), 0 + ); + session.persist( entity ); + session.getTransaction().commit(); + + final Integer updatesLimit = 3; + + for ( int i = 1; i <= updatesLimit; ++i ) { + session.getTransaction().begin(); + entity = (LobHolder) session.get( LobHolder.class, entity.getId() ); + entity.setCounter( i ); + entity = (LobHolder) session.merge( entity ); + session.getTransaction().commit(); + } + + session.getTransaction().begin(); + entity = (LobHolder) session.get( LobHolder.class, entity.getId() ); + entity.setBlobLocator( session.getLobHelper().createBlob( "updated blob".getBytes() ) ); + entity.setClobLocator( session.getLobHelper().createClob( "updated clob" ) ); + entity = (LobHolder) session.merge( entity ); + session.getTransaction().commit(); + + session.clear(); + + session.getTransaction().begin(); + checkState( "updated blob".getBytes(), "updated clob", updatesLimit, (LobHolder) session.get( LobHolder.class, entity.getId() ) ); + session.getTransaction().commit(); + + session.close(); + } + + private void checkState(byte[] blob, String clob, Integer counter, LobHolder entity) throws SQLException { + Assert.assertEquals( counter, entity.getCounter() ); + Assert.assertArrayEquals( blob, DataHelper.extractBytes( entity.getBlobLocator().getBinaryStream() ) ); + Assert.assertEquals( clob, DataHelper.extractString( entity.getClobLocator() ) ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/type/descriptor/java/BlobDescriptorTest.java b/hibernate-core/src/test/java/org/hibernate/test/type/descriptor/java/BlobDescriptorTest.java new file mode 100644 index 0000000000..bb5f5894d9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/type/descriptor/java/BlobDescriptorTest.java @@ -0,0 +1,102 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2010, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * 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 GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.test.type.descriptor.java; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.sql.Blob; +import java.sql.SQLException; + +import org.hibernate.engine.jdbc.BlobImplementer; +import org.hibernate.engine.jdbc.BlobProxy; +import org.hibernate.testing.TestForIssue; +import org.hibernate.type.descriptor.java.AbstractDescriptorTest; +import org.hibernate.type.descriptor.java.BlobTypeDescriptor; +import org.hibernate.type.descriptor.java.DataHelper; +import org.hibernate.type.descriptor.java.PrimitiveByteArrayTypeDescriptor; +import org.junit.Test; + +/** + * @author Steve Ebersole + */ +public class BlobDescriptorTest extends AbstractDescriptorTest { + final Blob original = BlobProxy.generateProxy( new byte[] { 1, 2, 3 } ); + final Blob copy = BlobProxy.generateProxy( new byte[] { 1, 2, 3 } ); + final Blob different = BlobProxy.generateProxy( new byte[] { 3, 2, 1 } ); + + public BlobDescriptorTest() { + super( BlobTypeDescriptor.INSTANCE ); + } + + @Override + protected Data getTestData() { + return new Data( original, copy, different ); + } + + @Override + protected boolean shouldBeMutable() { + return false; + } + + @Test + @Override + public void testEquality() { + // blobs of the same internal value are not really comparable + assertFalse( original == copy ); + assertTrue( BlobTypeDescriptor.INSTANCE.areEqual( original, original ) ); + assertFalse( BlobTypeDescriptor.INSTANCE.areEqual( original, copy ) ); + assertFalse( BlobTypeDescriptor.INSTANCE.areEqual( original, different ) ); + } + + @Test + @Override + public void testExternalization() { + // blobs of the same internal value are not really comparable + String externalized = BlobTypeDescriptor.INSTANCE.toString( original ); + Blob consumed = BlobTypeDescriptor.INSTANCE.fromString( externalized ); + try { + PrimitiveByteArrayTypeDescriptor.INSTANCE.areEqual( + DataHelper.extractBytes( original.getBinaryStream() ), + DataHelper.extractBytes( consumed.getBinaryStream() ) + ); + } + catch ( SQLException e ) { + fail( "SQLException accessing blob : " + e.getMessage() ); + } + } + + @Test + @TestForIssue( jiraKey = "HHH-8193" ) + public void testStreamResetOnAccess() throws IOException { + byte[] bytes = new byte[] { 1, 2, 3, 4 }; + BlobImplementer blob = (BlobImplementer) BlobProxy.generateProxy( bytes ); + int value = blob.getUnderlyingStream().getInputStream().read(); + // Call to BlobImplementer#getUnderlyingStream() should mark input stream for reset. + assertEquals( bytes.length, blob.getUnderlyingStream().getInputStream().available() ); + } +} \ No newline at end of file diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java index 8408818ca0..f9e29799d8 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/DialectChecks.java @@ -44,6 +44,12 @@ abstract public class DialectChecks { } } + public static class UsesInputStreamToInsertBlob implements DialectCheck { + public boolean isMatch(Dialect dialect) { + return dialect.useInputStreamToInsertBlob(); + } + } + public static class SupportsIdentityColumns implements DialectCheck { public boolean isMatch(Dialect dialect) { return dialect.supportsIdentityColumns();