diff --git a/build.gradle b/build.gradle index 50fdf86be9..5f8d7cf7dd 100644 --- a/build.gradle +++ b/build.gradle @@ -102,7 +102,8 @@ libraries = [ shrinkwrap_api: 'org.jboss.shrinkwrap:shrinkwrap-api:1.0.0-beta-6', shrinkwrap: 'org.jboss.shrinkwrap:shrinkwrap-impl-base:1.0.0-beta-6', validator: 'org.hibernate:hibernate-validator:4.2.0.Final', - h2: "com.h2database:h2:${h2Version}" + h2: "com.h2database:h2:${h2Version}", + mockito: 'org.mockito:mockito-core:1.9.0' ] diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index db28cfd2b0..af840179a1 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -23,6 +23,7 @@ dependencies { testCompile( libraries.validation ) testCompile( libraries.jandex ) testCompile( libraries.classmate ) + testCompile( libraries.mockito ) testCompile( libraries.validator ) { // for test runtime transitive = true diff --git a/hibernate-core/src/main/java/org/hibernate/cache/spi/NaturalIdCacheKey.java b/hibernate-core/src/main/java/org/hibernate/cache/spi/NaturalIdCacheKey.java index 0eda0a7796..92a07f6ea3 100644 --- a/hibernate-core/src/main/java/org/hibernate/cache/spi/NaturalIdCacheKey.java +++ b/hibernate-core/src/main/java/org/hibernate/cache/spi/NaturalIdCacheKey.java @@ -23,6 +23,8 @@ */ package org.hibernate.cache.spi; +import java.io.IOException; +import java.io.ObjectInputStream; import java.io.Serializable; import java.util.Arrays; @@ -44,7 +46,7 @@ public class NaturalIdCacheKey implements Serializable { private final String entityName; private final String tenantId; private final int hashCode; - private final transient ValueHolder toString; + private transient ValueHolder toString; /** * Construct a new key for a caching natural identifier resolutions into the second level cache. @@ -73,7 +75,8 @@ public class NaturalIdCacheKey implements Serializable { result = prime * result + ( ( this.entityName == null ) ? 0 : this.entityName.hashCode() ); result = prime * result + ( ( this.tenantId == null ) ? 0 : this.tenantId.hashCode() ); for ( int i = 0; i < naturalIdValues.length; i++ ) { - final Type type = propertyTypes[naturalIdPropertyIndexes[i]]; + final int naturalIdPropertyIndex = naturalIdPropertyIndexes[i]; + final Type type = propertyTypes[naturalIdPropertyIndex]; final Object value = naturalIdValues[i]; result = prime * result + (value != null ? type.getHashCode( value, factory ) : 0); @@ -82,25 +85,29 @@ public class NaturalIdCacheKey implements Serializable { } this.hashCode = result; - this.toString = new ValueHolder( - new ValueHolder.DeferredInitializer() { - @Override - public String initialize() { - //Complex toString is needed as naturalIds for entities are not simply based on a single value like primary keys - //the only same way to differentiate the keys is to included the disassembled values in the string. - final StringBuilder toStringBuilder = new StringBuilder( entityName ).append( "##NaturalId[" ); - for ( int i = 0; i < naturalIdValues.length; i++ ) { - toStringBuilder.append( naturalIdValues[i] ); - if ( i + 1 < naturalIdValues.length ) { - toStringBuilder.append( ", " ); - } - } - toStringBuilder.append( "]" ); + initTransients(); + } + + private void initTransients() { + this.toString = new ValueHolder( + new ValueHolder.DeferredInitializer() { + @Override + public String initialize() { + //Complex toString is needed as naturalIds for entities are not simply based on a single value like primary keys + //the only same way to differentiate the keys is to included the disassembled values in the string. + final StringBuilder toStringBuilder = new StringBuilder( entityName ).append( "##NaturalId[" ); + for ( int i = 0; i < naturalIdValues.length; i++ ) { + toStringBuilder.append( naturalIdValues[i] ); + if ( i + 1 < naturalIdValues.length ) { + toStringBuilder.append( ", " ); + } + } + toStringBuilder.append( "]" ); - return toStringBuilder.toString(); - } - } - ); + return toStringBuilder.toString(); + } + } + ); } @SuppressWarnings( {"UnusedDeclaration"}) @@ -144,4 +151,10 @@ public class NaturalIdCacheKey implements Serializable { && EqualsHelper.equals( tenantId, other.tenantId ) && Arrays.deepEquals( this.naturalIdValues, other.naturalIdValues ); } + + private void readObject(ObjectInputStream ois) + throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + initTransients(); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/cache/spi/NaturalIdCacheKeyTest.java b/hibernate-core/src/test/java/org/hibernate/cache/spi/NaturalIdCacheKeyTest.java new file mode 100644 index 0000000000..89ff418298 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/cache/spi/NaturalIdCacheKeyTest.java @@ -0,0 +1,73 @@ +package org.hibernate.cache.spi; + +import static junit.framework.Assert.assertEquals; +import static org.junit.Assert.assertArrayEquals; +import static org.mockito.Matchers.anyObject; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.Type; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class NaturalIdCacheKeyTest { + @Test + public void testSerializationRoundTrip() throws Exception { + final EntityPersister entityPersister = mock(EntityPersister.class); + final SessionImplementor sessionImplementor = mock(SessionImplementor.class); + final SessionFactoryImplementor sessionFactoryImplementor = mock(SessionFactoryImplementor.class); + final Type mockType = mock(Type.class); + + when (entityPersister.getRootEntityName()).thenReturn("EntityName"); + + when(sessionImplementor.getFactory()).thenReturn(sessionFactoryImplementor); + + when(entityPersister.getNaturalIdentifierProperties()).thenReturn(new int[] {0, 1, 2}); + when(entityPersister.getPropertyTypes()).thenReturn(new Type[] { + mockType, + mockType, + mockType + }); + + when(mockType.getHashCode(anyObject(), eq(sessionFactoryImplementor))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArguments()[0].hashCode(); + } + }); + + when(mockType.disassemble(anyObject(), eq(sessionImplementor), eq(null))).thenAnswer(new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + return invocation.getArguments()[0]; + } + }); + + final NaturalIdCacheKey key = new NaturalIdCacheKey(new Object[] {"a", "b", "c"}, entityPersister, sessionImplementor); + + final ByteArrayOutputStream baos = new ByteArrayOutputStream(); + final ObjectOutputStream oos = new ObjectOutputStream(baos); + oos.writeObject(key); + + final ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray())); + final NaturalIdCacheKey keyClone = (NaturalIdCacheKey)ois.readObject(); + + assertEquals(key, keyClone); + assertEquals(key.hashCode(), keyClone.hashCode()); + assertEquals(key.toString(), keyClone.toString()); + assertEquals(key.getEntityName(), keyClone.getEntityName()); + assertArrayEquals(key.getNaturalIdValues(), keyClone.getNaturalIdValues()); + assertEquals(key.getTenantId(), keyClone.getTenantId()); + + } +}