/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2001 (C) Intalio, Inc. All Rights Reserved.
 *
 */
package ctf.jdo.tc7x;

import harness.CastorTestCase;
import harness.TestHarness;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;

import jdo.JDOCategory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.exolab.castor.jdo.CacheManager;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.jdo.OQLQuery;
import org.exolab.castor.jdo.PersistenceException;
import org.exolab.castor.jdo.QueryResults;

/**
 * Expire Cache test. Tests the ability to clear objects from the cache.  This
 * includes clearing objects by class or type, or individually, by 
 * object identities.
 */
public final class TestExpireManyToMany extends CastorTestCase {
    /** The <a href="http://jakarta.apache.org/commons/logging/">Jakarta
     *  Commons Logging</a> instance used for all logging. */
    private static final Log LOG = LogFactory.getLog(TestExpireManyToMany.class);

    private static final boolean BY_TYPE_OR_CLASS   = true;
    private static final boolean BY_OBJECT_IDENTITY = false;

    private static final String JDO_ORIGINAL_VALUE  = "Original JDO String";
    private static final String JDO_UPDATED_VALUE   = "Updated Using JDO";
    private static final String JDBC_UPDATED_VALUE  = "Updated Using JDBC";

    private Database            _db;
    private JDOCategory         _category;
    private Connection          _conn;
    private PreparedStatement   _updateGroupStatement;
    private PreparedStatement   _updatePersonStatement;
    
    private int _groupAId = 201, _groupBId = 202, _groupCId = 203, _groupDId = 204;
    private int _person1Id = 1, _person2Id = 2, _person3Id = 3, _person4Id = 4;
    
    private boolean _debug = false;
    
    /** 
     * Constructor
     */
    public TestExpireManyToMany(final TestHarness category) {
        super(category, "TC75", "Expire Many-To-Many");
        _category = (JDOCategory) category;
    }

    /**
     * Initializes fields
     */
    public void setUp() throws SQLException {
        try {
            _db   = _category.getDatabase();
        } catch (Exception e) {
            LOG.warn("Problem opening a Database instance.", e);
        }
        _conn = _category.getJDBCConnection();

        // initialze JDBC connection
       try {
            _updateGroupStatement = _conn.prepareStatement(
                    "update tc7x_many_group set value1=? where gid=?");
            _updatePersonStatement = _conn.prepareStatement(
                    "update tc7x_many_person set value1=? where pid=?");
        } catch (java.sql.SQLException e) {
            fail("Failed to establish JDBC Connection");
        }

    }

    /**
     * Calls the individual tests embedded in this test case
     */
    public void runTest() throws PersistenceException, SQLException {
        testExpireCache(BY_OBJECT_IDENTITY);
        testExpireCache(BY_TYPE_OR_CLASS);
    }

    /**
     * Close the database and JDBC connection
     */
    public void tearDown() throws PersistenceException, SQLException {
        if (_db.isActive()) { _db.rollback(); }
        _db.close();
        _conn.close();
    }

    /**
     * Test the expire cache logic.  This method includes steps to 1) create a 
     * test data set, 2) enter read and write transactions (to test the proper
     * execution of the expire cache logic while transactions are in progress),
     * 3) update several objects outside of JDO using JDBC, 5) validate that
     * subsequent read/write operations on the objects expired from the cache
     * actually reflect values from the backend database, and, finally, 
     * 6) remove the test data set.
     *
     * @param expireByType when <code>true</code> this method will expire
     *      objects from the cache by specifying a single type or class of
     *      objects to be expired; when <code>false</code> this method will
     *      expire objects from the cache using individual object identities
     */
    public void testExpireCache(final boolean expireByType) {
        log("starting testExpireCache "
                + (expireByType ? "by type" : "by object identity"));
        try {
            // delete any data left over from previous tests
            deleteTestDataSet();
            try {
                createTestDataSet();
            } catch (Exception e) {
                // attempt to delete any test data that was created.
                deleteTestDataSet();
                fail("Failed to create test data set");
            }

            // "prime" the cache
            validReadTransaction(_groupAId, JDO_ORIGINAL_VALUE, true);
            validReadTransaction(_groupBId, JDO_ORIGINAL_VALUE, true);
            validReadTransaction(_groupCId, JDO_ORIGINAL_VALUE, true);
            validReadTransaction(_groupDId, JDO_ORIGINAL_VALUE, true);

            // now update the database outside of JDO
            updateGroupUsingJDBC(_groupAId);
            updateGroupUsingJDBC(_groupBId);
            updateGroupUsingJDBC(_groupCId);
            updateGroupUsingJDBC(_groupDId);

            updatePersonUsingJDBC(_person1Id);
            updatePersonUsingJDBC(_person2Id);
            updatePersonUsingJDBC(_person3Id);
            updatePersonUsingJDBC(_person4Id);

            // and force the cache to expire
            expire(expireByType);

            // validate that cached field values are not used in
            // subsequent read/write operations
            boolean success = true;
            if (!validReadTransaction(_groupAId, JDBC_UPDATED_VALUE, true)) {
                success = false;
            }
            if (!validWriteTransaction(_groupAId)) { success = false; }

            // don't validate people because they should now be cached with the
            // JDO_UPDATE_VALUE and won't pass the following validReadTransaction tests.
            if (!validReadTransaction(_groupBId, JDBC_UPDATED_VALUE, false)) {
                success = false;
            }
            if (!validWriteTransaction(_groupBId)) { success = false; }

            if (!validReadTransaction(_groupCId, JDBC_UPDATED_VALUE, false)) {
                success = false;
            }
            if (!validWriteTransaction(_groupCId)) { success = false; }

            if (!validReadTransaction(_groupDId, JDBC_UPDATED_VALUE, false)) {
                success = false;
            }
            if (!validWriteTransaction(_groupDId)) { success = false; }

            if (success) {
                log("Test Completed Successfully.");
            } else {
                fail("Cache was not properly expired");
            }

            deleteTestDataSet();
        } catch (Exception e) {
            log("ERROR: fatal exception encountered during test", e);
            fail("Exception encountered during test");
        }
    }

    /**
     * Create the requisite objects in the database
     */
    private void createTestDataSet() throws Exception {
        log("creating test data set...");
        ManyGroup groupA, groupB, groupC, groupD;
        ManyPerson person1, person2, person3, person4;
        ArrayList al, bl, c1, d1;
        Database db = null;
        try {
            db = this._category.getDatabase();
            db.begin();

            //
            // create four persons
            //
            person1 = new ManyPerson();
            ArrayList gPerson1 = new ArrayList();
            person1.setId(_person1Id);
            person1.setGroup(gPerson1);
            person1.setSthelse("Something else");
            person1.setHelloworld("Hello World!");
            person1.setValue1(JDO_ORIGINAL_VALUE);

            person2 = new ManyPerson();
            ArrayList gPerson2 = new ArrayList();
            person2.setId(_person2Id);
            person2.setGroup(gPerson2);
            person2.setSthelse("Something else");
            person2.setHelloworld("Hello World!");
            person2.setValue1(JDO_ORIGINAL_VALUE);

            person3 = new ManyPerson();
            ArrayList gPerson3 = new ArrayList();
            person3.setId(_person3Id);
            person3.setGroup(gPerson3);
            person3.setSthelse("Something else for person 3");
            person3.setHelloworld("Hello World!");
            person3.setValue1(JDO_ORIGINAL_VALUE);

            person4 = new ManyPerson();
            ArrayList gPerson4 = new ArrayList();
            person4.setId(_person4Id);
            person4.setGroup(gPerson4);
            person4.setSthelse("Something else for person 4");
            person4.setHelloworld("Hello World!");
            person4.setValue1(JDO_ORIGINAL_VALUE);

            //
            // create four groups, assign all persons to each group
            //
            groupA = new ManyGroup();
            groupA.setValue1(JDO_ORIGINAL_VALUE);
            al = new ArrayList();
            al.add(person1);
            al.add(person2);
            al.add(person3);
            al.add(person4);
            groupA.setId(_groupAId);
            groupA.setPeople(al);

            groupB = new ManyGroup();
            groupB.setValue1(JDO_ORIGINAL_VALUE);
            groupB.setId(_groupBId);
            bl = new ArrayList();
            bl.add(person1);
            bl.add(person2);
            bl.add(person3);
            bl.add(person4);
            groupB.setPeople(bl);

            groupC = new ManyGroup();
            groupC.setValue1(JDO_ORIGINAL_VALUE);
            c1 = new ArrayList();
            c1.add(person1);
            c1.add(person2);
            c1.add(person3);
            c1.add(person4);
            groupC.setId(_groupCId);
            groupC.setPeople(c1);

            groupD = new ManyGroup();
            groupD.setValue1(JDO_ORIGINAL_VALUE);
            d1 = new ArrayList();
            d1.add(person1);
            d1.add(person2);
            d1.add(person3);
            d1.add(person4);
            groupD.setId(_groupDId);
            groupD.setPeople(d1);

            //
            // assign all groups to each person
            //
            gPerson1.add(groupA);
            gPerson1.add(groupB);
            gPerson1.add(groupC);
            gPerson1.add(groupD);

            gPerson2.add(groupA);
            gPerson2.add(groupB);
            gPerson2.add(groupC);
            gPerson2.add(groupD);

            gPerson3.add(groupA);
            gPerson3.add(groupB);
            gPerson3.add(groupC);
            gPerson3.add(groupD);

            gPerson4.add(groupA);
            gPerson4.add(groupB);
            gPerson4.add(groupC);
            gPerson4.add(groupD);

            //
            // create persistent groups and persons
            //
            db.create(person1);
            db.create(person2);
            db.create(person3);
            db.create(person4);
            
            db.create(groupA);
            db.create(groupB);
            db.create(groupC);
            db.create(groupD);
            
            db.commit();
        } catch (Exception e) {
            log("createTestDataSet: exception caught: " + e.getMessage());
            throw e;
        }
    }

    /**
     * Update an object outside of JDO using a JDBC connection.
     *
     * @param groupId primary key of object to be updated
     */
    private void updateGroupUsingJDBC(final int groupId) {
        log("updating group " + groupId + " using JDBC...");

        try {
            _updateGroupStatement.setString(1, JDBC_UPDATED_VALUE);
            _updateGroupStatement.setInt(2, groupId);
            int rc = _updateGroupStatement.executeUpdate();
            if (rc <= 0) {
                log("updateGroupUsingJDBC: error updating group "
                        + groupId + ", return code = " + rc);
                return;
            }
        } catch (Exception e) {
            log("updateGroupUsingJDBC: exception updating group "
                    + groupId + ": " + e.getMessage(), e);
        }
    }

    /**
     * Update an object outside of JDO using a JDBC connection.
     *
     * @param personId primary key of object to be updated
     */
    private void updatePersonUsingJDBC(final int personId) {
        log("updating person " + personId + " using JDBC...");

        try {
            _updatePersonStatement.setString(1, JDBC_UPDATED_VALUE);
            _updatePersonStatement.setInt(2, personId);
            int rc = _updatePersonStatement.executeUpdate();
            if (rc <= 0) {
                log("updatePersonUsingJDBC: error updating person "
                        + personId + ", return code = " + rc);
                return;
            }
        } catch (Exception e) {
            log("updatePersonUsingJDBC: exception updating person "
                    + personId + ": " + e.getMessage(), e);
        }
    }

    /**
     * Setup and execute a database expire cache request.
     *
     * @param byType when <code>true</code> this method will request that
     *      objects be expired from the cache by specifying a single type or
     *      class of objects to be expired; when <code>false</code> this method
     *      will request that objects be expired from the cache using individual
     *      object identities
     */
    private void expire(final boolean byType) {
        log("expiring cache...");

        Object[] identityArray = null;
        try {
            CacheManager cacheManager = _db.getCacheManager();
            if (byType) {
                Class[] typeArray = new Class[2];
                typeArray[0] = ManyGroup.class;
                typeArray[1] = ManyPerson.class;
                cacheManager.expireCache(typeArray);
            } else {
                identityArray = new Object[4];
                identityArray[0] = new Integer(_groupAId);
                identityArray[1] = new Integer(_groupBId);
                identityArray[2] = new Integer(_groupCId);
                identityArray[3] = new Integer(_groupDId);
                cacheManager.expireCache(ManyGroup.class, identityArray);
            }
        } catch (Exception e) {
            log("expireCache: exception encountered clearing cache: " + e.getMessage());
        }
    }

    /**
     * Read, or load, an object and validate that the field values
     * reflect values updated outside of JDO and not from previously
     * cached values.
     *
     * @param groupId primary key of object to be read
     */
    private boolean validReadTransaction(final int groupId,
            final String expectedValue, final boolean checkPeople) {
        log("validating read transaction for group " + groupId + "...");
        Database db = null;
        boolean valid = true;
        try {
            db = this._category.getDatabase();
            db.begin();
            ManyGroup group = (ManyGroup) db.load(ManyGroup.class, new Integer(groupId));
            if (group.getValue1().compareTo(expectedValue) != 0) {
                log("validReadTransaction: value in group " + group.getId()
                        + " does not match expected value, value: " + group.getValue1()
                        + ", expected: " + expectedValue);
                valid = false;
            }
            if (checkPeople) {
                Iterator itor = group.getPeople().iterator();
                while (itor.hasNext()) {
                    ManyPerson person = (ManyPerson) itor.next();
                    if (person.getValue1().compareTo(expectedValue) != 0) {
                        log("validReadTransaction: value in person " + person.getId()
                                + " does not match expected value, value: "
                                + person.getValue1() + ", expected: " + expectedValue);
                        valid = false;
                    }
                }
            }
            db.rollback();
            db.close();
        } catch (Exception e) {
            if (db != null) {
                try {
                    db.close();
                } catch (Exception se) {
                    LOG.warn("Problem closing Database instance.", se);
                }
            }
            log("validReadTransaction: exception caught while validating read for group "
                    + groupId + " : " + e.getMessage(), e);
            valid = false;
        }
        return valid;
    }

    /**
     * Modify fields of an object and validate that the transaction completes
     * successfully.
     *
     * @param groupId primary key of object to be updated
     */
    private boolean validWriteTransaction(final int groupId) {
        log("validating write transaction for group " + groupId + "...");
        Database db = null;
        try {
            db = this._category.getDatabase();
            db.begin();
            ManyGroup group = (ManyGroup) db.load(ManyGroup.class, new Integer(groupId));
            group.setValue1(JDO_UPDATED_VALUE);
            Iterator itor = group.getPeople().iterator();
            while (itor.hasNext()) {
                ManyPerson person = (ManyPerson) itor.next();
                person.setValue1(JDO_UPDATED_VALUE);
            }
            db.commit();
            db.close();
        } catch (Exception e) {
            if (db != null) {
                try {
                    db.close();
                } catch (Exception se) {
                    LOG.warn("Problem closing Database instance.", se);
                }
            }
            log("validWriteTransaction: exception caught while validating group write "
                    + groupId + " : " + e.getMessage(), e);
            return false;
        }
        return true;
    }

    /**
     * Delete all objects in test data set.
     */
    private void deleteTestDataSet() {
        log("deleting test data set...");
        QueryResults enumeration;
        ManyGroup group = null;
        ManyPerson person = null;
        Database db = null;
        try {
            // select an group and delete it, if it exist!
            db = this._category.getDatabase();
            db.begin();
            OQLQuery oqlclean = db.getOQLQuery(
                    "SELECT object FROM " + ManyGroup.class.getName()
                    + " object WHERE object.id < $1");
            oqlclean.bind(Integer.MAX_VALUE);
            enumeration = oqlclean.execute();
            while (enumeration.hasMore()) {
                group = (ManyGroup) enumeration.next();
                stream.println("Retrieved object: " + group);
                db.remove(group);
                stream.println("Deleted object: " + group);
            }
            db.commit();

            db.begin();
            oqlclean = db.getOQLQuery(
                    "SELECT object FROM " + ManyPerson.class.getName()
                    + " object WHERE object.id < $1");
            oqlclean.bind(Integer.MAX_VALUE);
            enumeration = oqlclean.execute();
            while (enumeration.hasMore()) {
                person = (ManyPerson) enumeration.next();
                stream.println("Retrieved object: " + person);
                db.remove(person);
                stream.println("Deleted object: " + person);
            } 
            db.commit();
        } catch (Exception e) {
            if (db != null) {
                try {
                    db.close();
                } catch (Exception se) {
                    LOG.warn("Problem closing Database instance.", se);
                }
            }
            log("deleteTestDataSet: exception caught: " + e.getMessage(), e);
        }
    }

    /**
     * log a message with exception
     */
    private void log(final String s, final Throwable t) {
        log(s);
    }

    /**
     * log a message
     */
    private void log(final String s) {
        if (_debug) {
            System.out.println(s);
        } else {
            stream.println(s);
        }
    }
}