/*-
 * See the file LICENSE for redistribution information.
 *
 * Copyright (c) 2002-2003
 *	Sleepycat Software.  All rights reserved.
 *
 * $Id: TransactionTest.java,v 1.14 2003/10/18 19:51:30 mhayes Exp $
 */

package com.sleepycat.bdb.test;

import com.sleepycat.db.Db;
import com.sleepycat.db.DbEnv;
import com.sleepycat.db.DbTxn;
import com.sleepycat.bdb.CurrentTransaction;
import com.sleepycat.bdb.DataStore;
import com.sleepycat.bdb.TransactionRunner;
import com.sleepycat.bdb.TransactionWorker;
import com.sleepycat.bdb.collection.StoredContainer;
import com.sleepycat.bdb.collection.StoredCollections;
import com.sleepycat.bdb.collection.StoredIterator;
import com.sleepycat.bdb.collection.StoredList;
import com.sleepycat.bdb.collection.StoredSortedMap;
import com.sleepycat.bdb.util.RuntimeExceptionWrapper;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.SortedSet;
import junit.framework.Test;
import junit.framework.TestCase;
import junit.framework.TestSuite;

/**
 * @author Mark Hayes
 */
public class TransactionTest extends TestCase {

    private static final Long ONE = new Long(1);
    private static final Long TWO = new Long(2);
    private static final Long THREE = new Long(3);

    /**
     * Runs a command line collection test.
     * @see #usage
     */
    public static void main(String[] args)
        throws Exception {

        if (args.length == 1 &&
            (args[0].equals("-h") || args[0].equals("-help"))) {
            usage();
        } else {
            junit.textui.TestRunner.run(suite());
        }
    }

    private static void usage() {

        System.out.println(
          "Usage: java com.sleepycat.bdb.test.TransactionTest [-h | -help]\n");
        System.exit(2);
    }

    public static Test suite()
        throws Exception {

        TestSuite suite = new TestSuite(TransactionTest.class);
        return suite;
    }

    private DbEnv env;
    private CurrentTransaction currentTxn;
    private DataStore store;
    private StoredSortedMap map;
    private TestStore testStore = TestStore.BTREE_UNIQ;

    public TransactionTest(String name) {

        super(name);
    }

    public void setUp()
        throws Exception {

        System.out.println(DbTestUtil.qualifiedTestName(this));
        env = TestEnv.TXN.open("TransactionTests");
        currentTxn = CurrentTransaction.getInstance(env);
        store = testStore.open(env, null);
        map = new StoredSortedMap(store, testStore.getKeyBinding(),
                                  testStore.getValueBinding(), true);
    }

    public void tearDown() {

        try {
            if (store != null) {
                store.close();
                store = null;
            }
            if (env != null) {
                env.close(0);
                env = null;
            }
        } catch (Exception e) {
            System.out.println("Ignored exception during tearDown: " + e);
        }
    }

    public void testGetters()
        throws Exception {

        assertNotNull(env);
        assertNotNull(currentTxn);
        assertNull(currentTxn.getTxn());

        currentTxn.beginTxn();
        assertNotNull(currentTxn.getTxn());
        currentTxn.commitTxn();
        assertNull(currentTxn.getTxn());

        currentTxn.beginTxn();
        assertNotNull(currentTxn.getTxn());
        currentTxn.abortTxn();
        assertNull(currentTxn.getTxn());

        // dirty-read property should be inherited

        assertTrue(!map.isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.values()).isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.keySet()).isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.entrySet()).isDirtyReadEnabled());

        StoredSortedMap other =
            (StoredSortedMap) StoredCollections.dirtyReadMap(map);
        assertTrue(other.isDirtyReadEnabled());
        assertTrue(((StoredContainer) other.values()).isDirtyReadEnabled());
        assertTrue(((StoredContainer) other.keySet()).isDirtyReadEnabled());
        assertTrue(((StoredContainer) other.entrySet()).isDirtyReadEnabled());
        assertTrue(!map.isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.values()).isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.keySet()).isDirtyReadEnabled());
        assertTrue(!((StoredContainer) map.entrySet()).isDirtyReadEnabled());

        // auto-commit property should be inherited

        assertTrue(!map.isAutoCommit());
        assertTrue(!((StoredContainer) map.values()).isAutoCommit());
        assertTrue(!((StoredContainer) map.keySet()).isAutoCommit());
        assertTrue(!((StoredContainer) map.entrySet()).isAutoCommit());

        other = (StoredSortedMap) StoredCollections.autoCommitMap(map);
        assertTrue(other.isAutoCommit());
        assertTrue(((StoredContainer) other.values()).isAutoCommit());
        assertTrue(((StoredContainer) other.keySet()).isAutoCommit());
        assertTrue(((StoredContainer) other.entrySet()).isAutoCommit());
        assertTrue(!map.isAutoCommit());
        assertTrue(!((StoredContainer) map.values()).isAutoCommit());
        assertTrue(!((StoredContainer) map.keySet()).isAutoCommit());
        assertTrue(!((StoredContainer) map.entrySet()).isAutoCommit());

        // auto-commit property should be ORed with environment auto-commit

        env.setFlags(Db.DB_AUTO_COMMIT, true);
        assertTrue(map.isAutoCommit());
        assertTrue(((StoredContainer) map.values()).isAutoCommit());
        assertTrue(((StoredContainer) map.keySet()).isAutoCommit());
        assertTrue(((StoredContainer) map.entrySet()).isAutoCommit());

        env.setFlags(Db.DB_AUTO_COMMIT, false);
        assertTrue(!map.isAutoCommit());
        assertTrue(!((StoredContainer) map.values()).isAutoCommit());
        assertTrue(!((StoredContainer) map.keySet()).isAutoCommit());
        assertTrue(!((StoredContainer) map.entrySet()).isAutoCommit());
    }

    public void testTransactional()
        throws Exception {

        // is transactional because DB_AUTO_COMMIT was passed to Db.open()
        //
        assertTrue(map.isTransactional());
        store.close();
        store = null;

        // is not transactional because is not opened in a transaction
        //
        Db db = new Db(env, 0);
        db.open(null, null, null, Db.DB_BTREE, 0, 0);
        store = new DataStore(db, TestStore.BYTE_FORMAT,
                                  TestStore.BYTE_FORMAT, null);
        map = new StoredSortedMap(store,
                testStore.getKeyBinding(), testStore.getValueBinding(), true);
        assertTrue(!map.isTransactional());
        map.put(ONE, ONE);
        readCheck(map, ONE, ONE);
        store.close();

        // is transactional because is opened in a transaction
        //
        currentTxn.beginTxn();
        db = new Db(env, 0);
        db.open(currentTxn.getTxn(), null, null, Db.DB_BTREE, 0, 0);
        currentTxn.commitTxn();
        store = new DataStore(db, TestStore.BYTE_FORMAT,
                                  TestStore.BYTE_FORMAT, null);
        map = new StoredSortedMap(store,
                testStore.getKeyBinding(), testStore.getValueBinding(), true);
        assertTrue(map.isTransactional());
        currentTxn.beginTxn();
        map.put(ONE, ONE);
        readCheck(map, ONE, ONE);
        currentTxn.commitTxn();
    }

    public void testExceptions()
        throws Exception {

        try {
            currentTxn.commitTxn();
            fail();
        } catch (IllegalStateException expected) {}

        try {
            currentTxn.abortTxn();
            fail();
        } catch (IllegalStateException expected) {}
    }

    public void testNested()
        throws Exception {

        assertNull(currentTxn.getTxn());

        DbTxn txn1 = currentTxn.beginTxn();
        assertNotNull(txn1);
        assertTrue(txn1 == currentTxn.getTxn());

        assertNull(map.get(ONE));
        assertNull(map.put(ONE, ONE));
        assertEquals(ONE, map.get(ONE));

        DbTxn txn2 = currentTxn.beginTxn();
        assertNotNull(txn2);
        assertTrue(txn2 == currentTxn.getTxn());
        assertTrue(txn1 != txn2);

        assertNull(map.put(TWO, TWO));
        assertEquals(TWO, map.get(TWO));

        DbTxn txn3 = currentTxn.beginTxn();
        assertNotNull(txn3);
        assertTrue(txn3 == currentTxn.getTxn());
        assertTrue(txn1 != txn2);
        assertTrue(txn1 != txn3);
        assertTrue(txn2 != txn3);

        assertNull(map.put(THREE, THREE));
        assertEquals(THREE, map.get(THREE));

        DbTxn txn = currentTxn.abortTxn();
        assertTrue(txn == txn2);
        assertTrue(txn == currentTxn.getTxn());
        assertNull(map.get(THREE));
        assertEquals(TWO, map.get(TWO));

        txn3 = currentTxn.beginTxn();
        assertNotNull(txn3);
        assertTrue(txn3 == currentTxn.getTxn());
        assertTrue(txn1 != txn2);
        assertTrue(txn1 != txn3);
        assertTrue(txn2 != txn3);

        assertNull(map.put(THREE, THREE));
        assertEquals(THREE, map.get(THREE));

        txn = currentTxn.commitTxn();
        assertTrue(txn == txn2);
        assertTrue(txn == currentTxn.getTxn());
        assertEquals(THREE, map.get(THREE));
        assertEquals(TWO, map.get(TWO));

        txn = currentTxn.commitTxn();
        assertTrue(txn == txn1);
        assertTrue(txn == currentTxn.getTxn());
        assertEquals(THREE, map.get(THREE));
        assertEquals(TWO, map.get(TWO));
        assertEquals(ONE, map.get(ONE));

        txn = currentTxn.commitTxn();
        assertNull(txn);
        assertNull(currentTxn.getTxn());
        assertEquals(THREE, map.get(THREE));
        assertEquals(TWO, map.get(TWO));
        assertEquals(ONE, map.get(ONE));
    }

    public void testRunnerCommit()
        throws Exception {

        commitTest(false);
    }

    public void testExplicitCommit()
        throws Exception {

        commitTest(true);
    }

    private void commitTest(final boolean explicit)
        throws Exception {

        final TransactionRunner runner = new TransactionRunner(env);

        assertNull(currentTxn.getTxn());

        runner.run(new TransactionWorker() {
            public void doWork() throws Exception {
                final DbTxn txn1 = currentTxn.getTxn();
                assertNotNull(txn1);
                assertNull(map.put(ONE, ONE));
                assertEquals(ONE, map.get(ONE));

                runner.run(new TransactionWorker() {
                    public void doWork() throws Exception {
                        final DbTxn txn2 = currentTxn.getTxn();
                        assertNotNull(txn2);
                        assertTrue(txn1 != txn2);
                        assertNull(map.put(TWO, TWO));
                        assertEquals(TWO, map.get(TWO));
                        assertEquals(ONE, map.get(ONE));
                        if (explicit) currentTxn.commitTxn();
                    }
                });

                DbTxn txn3 = currentTxn.getTxn();
                assertSame(txn1, txn3);

                assertEquals(TWO, map.get(TWO));
                assertEquals(ONE, map.get(ONE));
            }
        });

        assertNull(currentTxn.getTxn());
    }

    public void testRunnerAbort()
        throws Exception {

        abortTest(false);
    }

    public void testExplicitAbort()
        throws Exception {

        abortTest(true);
    }

    private void abortTest(final boolean explicit)
        throws Exception {

        final TransactionRunner runner = new TransactionRunner(env);

        assertNull(currentTxn.getTxn());

        runner.run(new TransactionWorker() {
            public void doWork() throws Exception {
                final DbTxn txn1 = currentTxn.getTxn();
                assertNotNull(txn1);
                assertNull(map.put(ONE, ONE));
                assertEquals(ONE, map.get(ONE));

                try {
                    runner.run(new TransactionWorker() {
                        public void doWork() throws Exception {
                            final DbTxn txn2 = currentTxn.getTxn();
                            assertNotNull(txn2);
                            assertTrue(txn1 != txn2);
                            assertNull(map.put(TWO, TWO));
                            assertEquals(TWO, map.get(TWO));
                            if (explicit)
                                currentTxn.abortTxn();
                            else
                                throw new IllegalArgumentException(
                                                                "test-abort");
                        }
                    });
                    assertTrue(explicit);
                } catch (IllegalArgumentException e) {
                    assertTrue(!explicit);
                    assertEquals("test-abort", e.getMessage());
                }

                DbTxn txn3 = currentTxn.getTxn();
                assertSame(txn1, txn3);

                assertEquals(ONE, map.get(ONE));
                assertNull(map.get(TWO));
            }
        });

        assertNull(currentTxn.getTxn());
    }

    public void testDirtyReadCollection()
        throws Exception {

        StoredSortedMap dirtyMap =
            (StoredSortedMap) StoredCollections.dirtyReadSortedMap(map);

        // original map is not dirty-read
        assertTrue(map.isDirtyReadAllowed());
        assertTrue(!map.isDirtyReadEnabled());

        // all dirty-read containers are dirty-read
        checkDirtyReadProperty(dirtyMap);
        checkDirtyReadProperty(StoredCollections.dirtyReadMap(map));
        checkDirtyReadProperty(StoredCollections.dirtyReadCollection(
                                map.values()));
        checkDirtyReadProperty(StoredCollections.dirtyReadSet(
                                map.keySet()));
        checkDirtyReadProperty(StoredCollections.dirtyReadSortedSet(
                                (SortedSet) map.keySet()));

        // create a list just so we can call dirtyReadList()
        DataStore listStore = TestStore.RECNO_RENUM.open(env, null);
        List list = new StoredList(listStore, TestStore.VALUE_BINDING, true);
        checkDirtyReadProperty(StoredCollections.dirtyReadList(list));
        listStore.close();

        doDirtyRead(dirtyMap);
    }

    private void checkDirtyReadProperty(Object container) {

        assertTrue(((StoredContainer) container).isDirtyReadAllowed());
        assertTrue(((StoredContainer) container).isDirtyReadEnabled());
    }

    public void testDirtyReadTransaction()
        throws Exception {

        TransactionRunner runner = new TransactionRunner(env);
        runner.setDirtyRead(true);
        assertNull(currentTxn.getTxn());
        runner.run(new TransactionWorker() {
            public void doWork() throws Exception {
                assertNotNull(currentTxn.getTxn());
                doDirtyRead(map);
            }
        });
        assertNull(currentTxn.getTxn());
    }

    private synchronized void doDirtyRead(StoredSortedMap dirtyMap)
        throws Exception {

        // start thread one
        DirtyReadThreadOne t1 = new DirtyReadThreadOne(env, this);
        t1.start();
        wait();

        // put ONE
        synchronized (t1) { t1.notify(); }
        wait();
        readCheck(dirtyMap, ONE, ONE);
        assertTrue(!dirtyMap.isEmpty());

        // abort ONE
        synchronized (t1) { t1.notify(); }
        t1.join();
        readCheck(dirtyMap, ONE, null);
        assertTrue(dirtyMap.isEmpty());

        // start thread two
        DirtyReadThreadTwo t2 = new DirtyReadThreadTwo(env, this);
        t2.start();
        wait();

        // put TWO
        synchronized (t2) { t2.notify(); }
        wait();
        readCheck(dirtyMap, TWO, TWO);
        assertTrue(!dirtyMap.isEmpty());

        // commit TWO
        synchronized (t2) { t2.notify(); }
        t2.join();
        readCheck(dirtyMap, TWO, TWO);
        assertTrue(!dirtyMap.isEmpty());
    }

    private static class DirtyReadThreadOne extends Thread {

        private DbEnv env;
        private CurrentTransaction currentTxn;
        private TransactionTest parent;
        private StoredSortedMap map;

        private DirtyReadThreadOne(DbEnv env, TransactionTest parent) {

            this.env = env;
            this.currentTxn = CurrentTransaction.getInstance(env);
            this.parent = parent;
            this.map = parent.map;
        }

        public synchronized void run() {

            try {
                assertNull(currentTxn.getTxn());
                assertNotNull(currentTxn.beginTxn());
                assertNotNull(currentTxn.getTxn());
                readCheck(map, ONE, null);
                synchronized (parent) { parent.notify(); }
                wait();

                // put ONE
                assertNull(map.put(ONE, ONE));
                readCheck(map, ONE, ONE);
                synchronized (parent) { parent.notify(); }
                wait();

                // abort ONE
                assertNull(currentTxn.abortTxn());
                assertNull(currentTxn.getTxn());
            } catch (Exception e) {
                throw new RuntimeExceptionWrapper(e);
            }
        }
    }

    private static class DirtyReadThreadTwo extends Thread {

        private DbEnv env;
        private CurrentTransaction currentTxn;
        private TransactionTest parent;
        private StoredSortedMap map;

        private DirtyReadThreadTwo(DbEnv env, TransactionTest parent) {

            this.env = env;
            this.currentTxn = CurrentTransaction.getInstance(env);
            this.parent = parent;
            this.map = parent.map;
        }

        public synchronized void run() {

            try {
                final TransactionRunner runner = new TransactionRunner(env);
                final Object thread = this;
                assertNull(currentTxn.getTxn());

                runner.run(new TransactionWorker() {
                    public void doWork() throws Exception {
                        assertNotNull(currentTxn.getTxn());
                        readCheck(map, TWO, null);
                        synchronized (parent) { parent.notify(); }
                        thread.wait();

                        // put TWO
                        assertNull(map.put(TWO, TWO));
                        readCheck(map, TWO, TWO);
                        synchronized (parent) { parent.notify(); }
                        thread.wait();

                        // commit TWO
                    }
                });
                assertNull(currentTxn.getTxn());
            } catch (Exception e) {
                throw new RuntimeExceptionWrapper(e);
            }
        }
    }

    private static void readCheck(StoredSortedMap checkMap, Object key,
                                  Object expect) {
        if (expect == null) {
            assertNull(checkMap.get(key));
            assertTrue(checkMap.tailMap(key).isEmpty());
            assertTrue(!checkMap.tailMap(key).containsKey(key));
            assertTrue(!checkMap.keySet().contains(key));
            assertTrue(checkMap.duplicates(key).isEmpty());
            Iterator i = checkMap.keySet().iterator();
            try {
                while (i.hasNext()) {
                    assertTrue(!key.equals(i.next()));
                }
            } finally { StoredIterator.close(i); }
        } else {
            assertEquals(expect, checkMap.get(key));
            assertEquals(expect, checkMap.tailMap(key).get(key));
            assertTrue(!checkMap.tailMap(key).isEmpty());
            assertTrue(checkMap.tailMap(key).containsKey(key));
            assertTrue(checkMap.keySet().contains(key));
            assertTrue(checkMap.values().contains(expect));
            assertTrue(!checkMap.duplicates(key).isEmpty());
            assertTrue(checkMap.duplicates(key).contains(expect));
            Iterator i = checkMap.keySet().iterator();
            try {
                boolean found = false;
                while (i.hasNext()) {
                    if (expect.equals(i.next()))
                        found = true;
                }
                assertTrue(found);
            }
            finally { StoredIterator.close(i); }
        }
    }
}
