package org.hibernate.test.jpa.lock;

import junit.framework.Test;

import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.dialect.HSQLDialect;
import org.hibernate.testing.junit.functional.FunctionalTestClassTestSuite;
import org.hibernate.test.jpa.AbstractJPATest;
import org.hibernate.test.jpa.Item;
import org.hibernate.test.jpa.MyEntity;
import org.hibernate.util.ReflectHelper;

/**
 * Tests specifically relating to section 3.3.5.3 [Lock Modes] of the
 * JPA persistence specification (as of the <i>Proposed Final Draft</i>).
 *
 * @author Steve Ebersole
 */
public class JPALockTest extends AbstractJPATest {
	public JPALockTest(String name) {
		super( name );
	}

	public static Test suite() {
		return new FunctionalTestClassTestSuite( JPALockTest.class );
	}

	/**
	 * Test the equivalent of EJB3 LockModeType.READ
	 * <p/>
	 * From the spec:
	 * <p/>
	 * If transaction T1 calls lock(entity, LockModeType.READ) on a versioned object, the entity
	 * manager must ensure that neither of the following phenomena can occur:<ul>
	 * <li>P1 (Dirty read): Transaction T1 modifies a row. Another transaction T2 then reads that row and
	 * obtains the modified value, before T1 has committed or rolled back. Transaction T2 eventually
	 * commits successfully; it does not matter whether T1 commits or rolls back and whether it does
	 * so before or after T2 commits.
	 * <li>P2 (Non-repeatable read): Transaction T1 reads a row. Another transaction T2 then modifies or
	 * deletes that row, before T1 has committed. Both transactions eventually commit successfully.
	 * <p/>
	 * This will generally be achieved by the entity manager acquiring a lock on the underlying database row.
	 * Any such lock may be obtained immediately (so long as it is retained until commit completes), or the
	 * lock may be deferred until commit time (although even then it must be retained until the commit completes).
	 * Any implementation that supports repeatable reads in a way that prevents the above phenomena
	 * is permissible.
	 * <p/>
	 * The persistence implementation is not required to support calling lock(entity, LockMode-Type.READ)
	 * on a non-versioned object. When it cannot support such a lock call, it must throw the
	 * PersistenceException. When supported, whether for versioned or non-versioned objects, LockMode-Type.READ
	 * must always prevent the phenomena P1 and P2. Applications that call lock(entity, LockModeType.READ)
	 * on non-versioned objects will not be portable.
	 * <p/>
	 * EJB3 LockModeType.READ actually maps to the Hibernate LockMode.OPTIMISTIC
	 */
	public void testLockModeTypeRead() {
		if ( !readCommittedIsolationMaintained( "ejb3 lock tests" ) ) {
			return;
		}
		if ( getDialect().doesReadCommittedCauseWritersToBlockReaders() ) {
			reportSkip( "deadlock", "jpa read locking" );
			return;
		}
		if ( getDialect() instanceof HSQLDialect ) {
			int hsqldbVersion = 18;
			try {
				Class props = ReflectHelper
						.classForName( "org.hsqldb.persist.HsqlDatabaseProperties" );
				String versionString = (String) props.getDeclaredField(
						"THIS_VERSION").get(null);

				hsqldbVersion = Integer.parseInt( versionString.substring( 0, 1 ) ) * 10;
				hsqldbVersion += Integer
						.parseInt( versionString.substring( 2, 3 ) );
			} catch ( Throwable e ) {
				// must be a very old version
			}
			if ( hsqldbVersion < 20 )
				reportSkip( "hsqldb 1.8.x only supports read uncommitted", "lock doesn't work on hsqldb 1.8.x");
			return;
		}
		final String initialName = "lock test";
		// set up some test data
		Session s1 = getSessions().openSession();
		Transaction t1 = s1.beginTransaction();
		Item item = new Item();
		item.setName( initialName );
		s1.save( item );
		t1.commit();
		s1.close();

		Long itemId = item.getId();

		// do the isolated update
		s1 = getSessions().openSession();
		t1 = s1.beginTransaction();
		item = (Item) s1.get( Item.class, itemId );
		s1.lock( item, LockMode.UPGRADE );
		item.setName( "updated" );
		s1.flush();

		Session s2 = getSessions().openSession();
		Transaction t2 = s2.beginTransaction();
		Item item2 = (Item) s2.get( Item.class, itemId );
		assertEquals( "isolation not maintained", initialName, item2.getName() );

		t1.commit();
		s1.close();

		item2 = (Item) s2.get( Item.class, itemId );
		assertEquals( "repeatable read not maintained", initialName, item2.getName() );
		t2.commit();
		s2.close();

		s1 = getSessions().openSession();
		t1 = s1.beginTransaction();
		s1.delete( item );
		t1.commit();
		s1.close();
	}

	/**
	 * Test the equivalent of EJB3 LockModeType.WRITE
	 * <p/>
	 * From the spec:
	 * <p/>
	 * If transaction T1 calls lock(entity, LockModeType.WRITE) on a versioned object, the entity
	 * manager must avoid the phenomena P1 and P2 (as with LockModeType.READ) and must also force
	 * an update (increment) to the entity's version column. A forced version update may be performed immediately,
	 * or may be deferred until a flush or commit. If an entity is removed before a deferred version
	 * update was to have been applied, the forced version update is omitted, since the underlying database
	 * row no longer exists.
	 * <p/>
	 * The persistence implementation is not required to support calling lock(entity, LockMode-Type.WRITE)
	 * on a non-versioned object. When it cannot support a such lock call, it must throw the
	 * PersistenceException. When supported, whether for versioned or non-versioned objects, LockMode-Type.WRITE
	 * must always prevent the phenomena P1 and P2. For non-versioned objects, whether or
	 * not LockModeType.WRITE has any additional behaviour is vendor-specific. Applications that call
	 * lock(entity, LockModeType.WRITE) on non-versioned objects will not be portable.
	 * <p/>
	 * Due to the requirement that LockModeType.WRITE needs to force a version increment,
	 * a new Hibernate LockMode was added to support this behavior: {@link org.hibernate.LockMode#FORCE}.
	 */
	public void testLockModeTypeWrite() {
		if ( !readCommittedIsolationMaintained( "ejb3 lock tests" ) ) {
			return;
		}
		if ( getDialect().doesReadCommittedCauseWritersToBlockReaders() ) {
			reportSkip( "deadlock", "jpa write locking" );
			return;
		}
		final String initialName = "lock test";
		// set up some test data
		Session s1 = getSessions().openSession();
		Transaction t1 = s1.beginTransaction();
		Item item = new Item();
		item.setName( initialName );
		s1.save( item );
		MyEntity myEntity = new MyEntity();
		myEntity.setName( "Test" );
		s1.save( myEntity );
		t1.commit();
		s1.close();

		Long itemId = item.getId();
		long initialVersion = item.getVersion();

		s1 = getSessions().openSession();
		t1 = s1.beginTransaction();
		item = (Item) s1.get( Item.class, itemId );
		s1.lock( item, LockMode.FORCE );
		assertEquals( "no forced version increment", initialVersion + 1, item.getVersion() );

		myEntity = (MyEntity) s1.get( MyEntity.class, myEntity.getId() );
		s1.lock( myEntity, LockMode.FORCE );
		assertTrue( "LockMode.FORCE on a un-versioned entity should degrade nicely to UPGRADE", true );

		s1.lock( item, LockMode.FORCE );
		assertEquals( "subsequent LockMode.FORCE did not no-op", initialVersion + 1, item.getVersion() );

		Session s2 = getSessions().openSession();
		Transaction t2 = s2.beginTransaction();
		Item item2 = (Item) s2.get( Item.class, itemId );
		assertEquals( "isolation not maintained", initialName, item2.getName() );

		item.setName( "updated-1" );
		s1.flush();
		// currently an unfortunate side effect...
		assertEquals( initialVersion + 2, item.getVersion() );

		t1.commit();
		s1.close();

		item2.setName( "updated" );
		try {
			t2.commit();
			fail( "optimistic lock should have failed" );
		}
		catch (Throwable ignore) {
			// expected behavior
			t2.rollback();
		}
		finally {
			s2.close();
		}

		s1 = getSessions().openSession();
		t1 = s1.beginTransaction();
		s1.delete( item );
		s1.delete( myEntity );
		t1.commit();
		s1.close();
	}
}
