/*
 * Copyright 2008 Udai Gupta
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.castor.cpa.test.test89;

import java.sql.Date;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Iterator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.castor.cpa.test.framework.CPATestCase;
import org.castor.cpa.test.framework.xml.types.DatabaseEngineType;
import org.exolab.castor.jdo.CacheManager;
import org.exolab.castor.jdo.Database;
import org.exolab.castor.persist.spi.Identity;

/**
 * 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 TestLazyEmployeeExpiration extends CPATestCase {
    private static final Log LOG = LogFactory
            .getLog(TestLazyEmployeeExpiration.class);

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

    private static final Date JDO_ORIGINAL_DATE = Date.valueOf("2000-01-02");
    private static final Date JDO_UPDATED_DATE = Date.valueOf("2001-03-04");
    private static final Date JDBC_UPDATED_DATE = Date.valueOf("2002-05-06");

    private static final String JDO_ORIGINAL_STRING = "Original JDO String";
    private static final String JDO_UPDATED_STRING = "Updated Using JDO";
    private static final String JDBC_UPDATED_STRING = "Updated Using JDBC";

    private static final String DBNAME = "test89";
    private static final String MAPPING = "/org/castor/cpa/test/test89/mapping.xml";
    private Database _db;

    public TestLazyEmployeeExpiration(final String name) {
        super(name);
    }

    // Test are only included/excluded for engines that have been tested with this test suite.

    public boolean include(final DatabaseEngineType engine) {
        return (engine == DatabaseEngineType.DERBY)
            || (engine == DatabaseEngineType.HSQL)
            || (engine == DatabaseEngineType.MYSQL)
            || (engine == DatabaseEngineType.ORACLE)
            || (engine == DatabaseEngineType.POSTGRESQL);
    }

    public void setUp() throws Exception {
        _db = getJDOManager(DBNAME, MAPPING).getDatabase();
    }

    public void tearDown() throws Exception {
        if (_db.isActive()) {
            _db.rollback();
        }
        _db.close();
    }

    public void testExpireCache() throws Exception {
        expireCache(BY_OBJECT_IDENTITY);
        expireCache(BY_TYPE_OR_CLASS);
    }

    /**
     * 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
     * @throws Exception
     */
    public void expireCache(final boolean expireByType) throws Exception {

        LOG.info("starting testExpireCache "
                + (expireByType ? "by type" : "by object identity"));

        // delete any data left over from previous tests
        deleteTestDataSet();
        createTestDataSet();

        // now update the database outside of JDO
        updatePersonUsingJDBC("First", "Person", JDBC_UPDATED_DATE);
        updateEmplUsingJDBC("First", "Person", JDBC_UPDATED_DATE);

        updateAddrUsingJDBC(1, Integer.toString(1) + JDBC_UPDATED_STRING);
        updateAddrUsingJDBC(2, Integer.toString(2) + JDBC_UPDATED_STRING);
        updateAddrUsingJDBC(3, Integer.toString(3) + JDBC_UPDATED_STRING);

        // 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("First", "Person")) {
            success = false;
        }
        if (!validWriteTransaction("First", "Person")) {
            success = false;
        }

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

    }

    private void createTestDataSet() throws Exception {
        LOG.info("creating test data set...");
        _db.begin();

        LazyEmployee person = new LazyEmployee();
        person.setFirstName("First");
        person.setLastName("Person");
        person.setBirthday(JDO_ORIGINAL_DATE);
        person.setStartDate(JDO_ORIGINAL_DATE);

        LazyAddress address1 = new LazyAddress();
        address1.setId(1);
        address1.setStreet(Integer.toString(1) + JDO_ORIGINAL_STRING);
        address1.setCity("First City");
        address1.setState("AB");
        address1.setZip("10000");
        address1.setPerson(person);

        LazyAddress address2 = new LazyAddress();
        address2.setId(2);
        address2.setStreet(Integer.toString(2) + JDO_ORIGINAL_STRING);
        address2.setCity("Second City");
        address2.setState("BC");
        address2.setZip("22222");
        address2.setPerson(person);

        LazyAddress address3 = new LazyAddress();
        address3.setId(3);
        address3.setStreet(Integer.toString(3) + JDO_ORIGINAL_STRING);
        address3.setCity("Third Ave");
        address3.setState("AB");
        address3.setZip("30003");
        address3.setPerson(person);

        ArrayList<LazyAddress> addresslist = new ArrayList<LazyAddress>();
        addresslist.add(address1);
        addresslist.add(address2);
        addresslist.add(address3);

        person.setAddress(addresslist);

        LazyPayRoll pr1 = new LazyPayRoll();
        pr1.setId(1);
        pr1.setHoliday(15);
        pr1.setHourlyRate(25);
        pr1.setEmployee(person);
        person.setPayRoll(pr1);

        LazyContractCategory cc = new LazyContractCategory();
        cc.setId(101);
        cc.setName("Full-time slave");
        _db.create(cc);

        LazyContractCategory cc2 = new LazyContractCategory();
        cc2.setId(102);
        cc2.setName("Full-time employee");
        _db.create(cc2);

        ArrayList<LazyContractCategory> category = new ArrayList<LazyContractCategory>();
        category.add(cc);
        category.add(cc2);

        LazyContract con = new LazyContract();
        con.setPolicyNo(1001);
        con.setComment("80 hours a week, no pay hoilday, "
                + "no sick leave, arrive office at 7:30am everyday");
        con.setContractNo(78);
        con.setEmployee(person);
        con.setCategory(category);
        person.setContract(con);
        _db.create(person);

        _db.commit();
    }

    /**
     * Update a person outside of JDO using a JDBC connection.
     * 
     * @param firstName
     *            first part of primary key of object to be updated
     * @param lastName
     *            first part of primary key of object to be updated
     * @param newBirthdate
     *            new value of persons birthdate
     * @throws Exception
     */
    private void updatePersonUsingJDBC(final String firstName,
            final String lastName, final Date newBirthdate) throws Exception {

        LOG.info("updatePersonUsingJDBC: updating " + firstName + " "
                + lastName);
        _db.begin();
        PreparedStatement updatePersonStatement = _db
                .getJdbcConnection()
                .prepareStatement(
                    "update test89_pks_person set bday=? where fname=? and lname=?");
        updatePersonStatement.setDate(1, newBirthdate);
        updatePersonStatement.setString(2, firstName);
        updatePersonStatement.setString(3, lastName);
        int rc = updatePersonStatement.executeUpdate();
        if (rc <= 0) {
            LOG.error(//
                    "updatePersonUsingJDBC: error updating person, return = "
                            + rc);
            return;
        }
        _db.commit();

    }

    /**
     * Update a person outside of JDO using a JDBC connection.
     * 
     * @param firstName
     *            first part of primary key of object to be updated
     * @param lastName
     *            first part of primary key of object to be updated
     * @param newStartDate
     *            new value of persons birthdate
     * @throws Exception
     */
    private void updateEmplUsingJDBC(final String firstName,
            final String lastName, final Date newStartDate) throws Exception {

        LOG.info("updateEmployeeUsingJDBC: updating " + firstName + " "
                + lastName);
        _db.begin();
        PreparedStatement updateEmployeeStatement = _db
                .getJdbcConnection()
                .prepareStatement(
                    "update test89_pks_employee set start_date=? where fname=? and lname=?");
        updateEmployeeStatement.setDate(1, newStartDate);
        updateEmployeeStatement.setString(2, firstName);
        updateEmployeeStatement.setString(3, lastName);
        int rc = updateEmployeeStatement.executeUpdate();
        if (rc <= 0) {
            LOG.error("updateEmplUsingJDBC: error updating employee, return = "
                    + rc);
            return;
        }
        _db.commit();
    }

    /**
     * Update an address outside of JDO using a JDBC connection.
     * 
     * @param addressId
     *            primary key of object to be updated
     * @param newStreet
     *            new value of addresses street field
     * @throws Exception
     */
    private void updateAddrUsingJDBC(final int addressId, final String newStreet)
            throws Exception {
        LOG.info("updateAddressUsingJDBC: updating " + addressId);
        _db.begin();
        PreparedStatement updateAddressStatement = _db
        .getJdbcConnection()
        .prepareStatement(
                "update test89_pks_address set street=? where id=?");
        updateAddressStatement.setString(1, newStreet);
        updateAddressStatement.setInt(2, addressId);
        int rc = updateAddressStatement.executeUpdate();
        if (rc <= 0) {
            LOG.error(//
                    "updateAddrUsingJDBC: error updating address, return = "
                            + rc);
            return;
        }
        _db.commit();
    }

    /**
     * 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.info("expiring cache...");

        try {
            CacheManager cacheManager = _db.getCacheManager();
            if (byType) {
                Class<?>[] typeArray = new Class[5];
                typeArray[0] = LazyContract.class;
                typeArray[1] = LazyContractCategory.class;
                typeArray[2] = LazyPayRoll.class;
                typeArray[3] = LazyAddress.class;
                typeArray[4] = LazyEmployee.class;
                cacheManager.expireCache(typeArray);
            } else {
                Object[] identityArray = new Object[1];
                identityArray[0] = new Identity("First", "Person");
                cacheManager.expireCache(LazyEmployee.class, identityArray);
            }
        } catch (Exception e) {
            LOG.error("expireCache: exception encountered clearing cache", e);
        }
    }

    /**
     * Read, or load, an object and validate that the field values reflect
     * values updated outside of JDO and not from previously cached values.
     * 
     * @param firstName
     *            primary key of object to be read
     * @param lastName
     *            primary key of object to be read
     * @return True if read transaction is valid.
     */
    private boolean validReadTransaction(final String firstName,
            final String lastName) {
        LOG.info("validating read transaction for person " + firstName + " "
                + lastName + "...");
        boolean valid = true;

        try {
            _db.begin();

            Identity fullname = new Identity("First", "Person");
            LazyEmployee person;

            person = _db.load(LazyEmployee.class, fullname);
            if (person.getBirthday().compareTo(JDBC_UPDATED_DATE) != 0) {
                LOG.debug("validReadTransaction: birthdate for "
                        + person.getFirstName() + " " + person.getLastName()
                        + " does not match expected value, value: "
                        + person.getBirthday().toString() + ", expected: "
                        + JDBC_UPDATED_DATE.toString());
                valid = false;
            }
            if (person.getStartDate().compareTo(JDBC_UPDATED_DATE) != 0) {
                LOG.debug("validReadTransaction: start date for "
                        + person.getFirstName() + " " + person.getLastName()
                        + " does not match expected value, value: "
                        + person.getStartDate().toString() + ", expected: "
                        + JDBC_UPDATED_DATE.toString());
                valid = false;
            }
            Iterator<LazyAddress> itor = person.getAddress().iterator();
            while (itor.hasNext()) {
                LazyAddress address = itor.next();
                String expectedStreet = new String(Integer.toString(address
                        .getId())
                        + JDBC_UPDATED_STRING);
                if (address.getStreet().compareTo(expectedStreet) != 0) {
                    LOG.debug("validReadTransaction: street in address "
                            + address.getId()
                            + " does not match expected value, value: "
                            + address.getStreet() + ", expected: "
                            + expectedStreet);
                    valid = false;
                }
            }
            _db.rollback();
        } catch (Exception e) {
            LOG.error(
                "validReadTransaction: exception while validating read for "
                        + firstName + " " + lastName, e);
            valid = false;
        }
        return valid;
    }

    /**
     * Modify fields of an object and validate that the transaction completes
     * successfully.
     * 
     * @param firstName
     *            First name.
     * @param lastName
     *            Last name.
     * @return True if write transaction is valid.
     */
    private boolean validWriteTransaction(final String firstName,
            final String lastName) {
        LOG.info("validating write transaction for group " + firstName + " "
                + lastName + "...");
        try {
            _db.begin();
            Identity fullname = new Identity("First", "Person");
            LazyEmployee person;

            person = _db.load(LazyEmployee.class, fullname);
            person.setBirthday(JDO_UPDATED_DATE);
            Iterator<LazyAddress> itor = person.getAddress().iterator();
            while (itor.hasNext()) {
                LazyAddress address = itor.next();
                String newStreet = new String(Integer.toString(address.getId())
                        + JDO_UPDATED_STRING);
                address.setStreet(newStreet);
            }
            _db.commit();
        } catch (Exception e) {
            LOG.error(
                "validWriteTransaction: exception while validating write for "
                        + firstName + " " + lastName, e);
            return false;
        }
        return true;
    }

    /**
     * Delete all objects in test data set.
     * 
     * @throws Exception 
     */
    private void deleteTestDataSet() throws Exception {
        LOG.info("deleting test data set...");
        _db.begin();
        Statement stmt = _db.getJdbcConnection().createStatement();
        stmt.executeUpdate("DELETE FROM test89_pks_person");
        stmt.executeUpdate("DELETE FROM test89_pks_employee");
        stmt.executeUpdate("DELETE FROM test89_pks_payroll");
        stmt.executeUpdate("DELETE FROM test89_pks_address");
        stmt.executeUpdate("DELETE FROM test89_pks_contract");
        stmt.executeUpdate("DELETE FROM test89_pks_category");
        stmt.executeUpdate("DELETE FROM test89_pks_project");
        _db.commit();
    }
}