/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.apache.jdo.tck;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;

import java.security.AccessController;
import java.security.PrivilegedAction;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;

import javax.jdo.Constants;
import javax.jdo.Extent;
import javax.jdo.JDOException;
import javax.jdo.JDOFatalException;
import javax.jdo.JDOHelper;
import javax.jdo.JDOObjectNotFoundException;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;
import javax.jdo.Query;

import junit.framework.AssertionFailedError;
import junit.framework.TestCase;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ClassUtils;

public abstract class JDO_Test extends TestCase {
    public static final int TRANSIENT                           = 0;
    public static final int PERSISTENT_NEW                      = 1;
    public static final int PERSISTENT_CLEAN                    = 2;
    public static final int PERSISTENT_DIRTY                    = 3;
    public static final int HOLLOW                              = 4;
    public static final int TRANSIENT_CLEAN                     = 5;
    public static final int TRANSIENT_DIRTY                     = 6;
    public static final int PERSISTENT_NEW_DELETED              = 7;
    public static final int PERSISTENT_DELETED                  = 8;
    public static final int PERSISTENT_NONTRANSACTIONAL         = 9;
    public static final int PERSISTENT_NONTRANSACTIONAL_DIRTY   = 10;
    public static final int DETACHED_CLEAN                      = 11;
    public static final int DETACHED_DIRTY                      = 12;
    public static final int NUM_STATES = 13;
    public static final int ILLEGAL_STATE = 13;

    public static final String[] states = {
        "transient",
        "persistent-new",
        "persistent-clean",
        "persistent-dirty",
        "hollow",
        "transient-clean",
        "transient-dirty",
        "persistent-new-deleted",
        "persistent-deleted",
        "persistent-nontransactional",
        "persistent-nontransactional-dirty",
        "detached-clean",
        "detached-dirty",
        "illegal"
    };
    private static final int IS_PERSISTENT       = 0;
    private static final int IS_TRANSACTIONAL    = 1;
    private static final int IS_DIRTY            = 2;
    private static final int IS_NEW              = 3;
    private static final int IS_DELETED          = 4;
    private static final int IS_DETACHED         = 5;
    private static final int NUM_STATUSES        = 6;

    /*
     * This table indicates the values returned by the status interrogation
     * methods for each state. This is used to determine the current lifecycle
     * state of an object.
     */
    private static final boolean state_statuses[][] = {
        // IS_PERSISTENT IS_TRANSACTIONAL    IS_DIRTY      IS_NEW      IS_DELETED  IS_DETACHED
        // transient
        {   false,          false,              false,      false,      false,        false},

        // persistent-new
        {   true,           true,               true,       true,       false,        false},

        // persistent-clean
        {   true,           true,               false,      false,      false,        false},

        // persistent-dirty
        {   true,           true,               true,       false,      false,        false},

        // hollow
        {   true,           false,              false,      false,      false,        false},

        // transient-clean
        {   false,          true,               false,      false,      false,        false},

        // transient-dirty
        {   false,          true,               true,       false,      false,        false},

        // persistent-new-deleted
        {   true,           true,               true,       true,       true,         false},

        // persistent-deleted
        {   true,           true,               true,       false,      true,         false},

        // persistent-nontransactional
        {   true,           false,              false,      false,      false,        false},

        // persistent-nontransactional-dirty
        {   true,           false,              true,       false,      false,        false},

        // detached_clean
        {   false,          false,              false,      false,      false,        true},

        // detached_dirty
        {   false,          false,              true,       false,      false,        true}
    };
  
    /** Name of the PersistenceManagerFactoryClass PMF property. */
    public static final String PMF_CLASS_PROP = 
        "javax.jdo.PersistenceManagerFactoryClass";

    /** Name of the ConnectionURL PMF property. */
    public static final String CONNECTION_URL_PROP = 
        "javax.jdo.option.ConnectionURL";

    /** Name of the ConnectionUserName PMF property. */
    public static final String CONNECTION_USERNAME_PROP = 
        "javax.jdo.option.ConnectionUserName";

    /** Name of the ConnectionPassword PMF property. */
    public static final String CONNECTION_PASSWORD_PROP = 
        "javax.jdo.option.ConnectionPassword";

    /** identitytype value for applicationidentity. */
    public static final String APPLICATION_IDENTITY = "applicationidentity";

    /** identitytype value for datastoreidentity. */
    public static final String DATASTORE_IDENTITY = "datastoreidentity";

    /** Map of transaction isolation String values to Integer */
    protected final static Map levelValues = new HashMap();
    static {
        levelValues.put(Constants.TX_READ_UNCOMMITTED, 0);
        levelValues.put(Constants.TX_READ_COMMITTED, 1);
        levelValues.put(Constants.TX_REPEATABLE_READ, 2);
        levelValues.put(Constants.TX_SNAPSHOT, 3);
        levelValues.put(Constants.TX_SERIALIZABLE, 4);
    }

    /** 
     * String indicating the type of identity used for the current test case.
     * The value is either "applicationidentity" or "datastoreidentity".
     */
    protected final String identitytype = System.getProperty("jdo.tck.identitytype");

    /** 
     * String indicating the name of the schema for the current test.
     */
    protected final String schemaname = System.getProperty("jdo.tck.schemaname");

    /** Name of the file containing the properties for the PMF. */
    protected static String PMFProperties = System.getProperty("PMFProperties");

    /** Flag indicating whether to clean up data after tests or not.
     * If false then test will not clean up data from database.
     * The default value is true.
     */
    protected static boolean cleanupData = 
       System.getProperty("jdo.tck.cleanupaftertest", "true").equalsIgnoreCase("true");

    /** Flag indicating whether to close the PMF after each test or not.
     * It defaults to false.
     */   
    protected static final boolean closePMFAfterEachTest =
        System.getProperty("jdo.tck.closePMFAfterEachTest", "false").equalsIgnoreCase("true");
    
    /** The Properties object for the PersistenceManagerFactory. */
    protected static Properties PMFPropertiesObject;

    /** The PersistenceManagerFactory. */
    protected static PersistenceManagerFactory pmf;
    
    /** The collection of supported options of the pmf. */
    protected static Collection supportedOptions;

    /** The name of the pmf supported options summary file. */
    private static final String PMF_SUPPORTED_OPTIONS_FILE_NAME = "pmf_supported_options.txt";

    /** The PersistenceManager. */
    protected PersistenceManager pm;
    
    // Flag indicating successful test run
    protected boolean testSucceeded;

    /** Logger */
    protected Log logger = 
        LogFactory.getFactory().getInstance("org.apache.jdo.tck");

    /** true if debug logging in enabled. */
    protected boolean debug = logger.isDebugEnabled();
    
    /** 
     * Indicates an exception thrown in method <code>tearDown</code>.
     * At the end of method <code>tearDown</code> this field is nullified. 
     */
    private Throwable tearDownThrowable;
    
    /** 
     * A list of registered oid instances. 
     * Corresponding pc instances are deleted in <code>localTearDown</code>.
     */
    private Collection tearDownInstances = new LinkedList();
    
    /** 
     * A list of registered pc classes. 
     * The extents of these classes are deleted in <code>localTearDown</code>.
     */
    private Collection tearDownClasses = new LinkedList();
    
    /**
     * Intended for subclasses so that they may skip this class's normal set up procedure.
     * @return true to run normal set up, false to skip normal set up
     */
    protected boolean preSetUp() {
        return true;
    }

    protected final void setUp() throws Exception {
        if (!preSetUp()) {
            return;
        }

        pmf = getPMF();
        localSetUp();
    }
    
    /**
     * Subclasses may override this method to allocate any data and resources
     * that they need in order to successfully execute this testcase.
     */
    protected void localSetUp() {}
    
    /**
     * Runs the bare test sequence.
     * @exception Throwable if any exception is thrown
     */
    public final void runBare() throws Throwable {
        try {
            testSucceeded = false;
            setUp();
            runTest();
            testSucceeded = true;
        }
        catch (AssertionFailedError e) {
            if (logger.isInfoEnabled())
                logger.info("Exception during setUp or runtest: ", e);
            throw e;
        }
        catch (Throwable t) {
            if (logger.isInfoEnabled())
                logger.info("Exception during setUp or runtest: ", t);
            throw t;
        }
        finally {
            tearDown();
            if (debug) {
                logger.debug("Free memory: " + Runtime.getRuntime().freeMemory());
            }
        }
    }

    /**
     * Sets field <code>tearDownThrowable</code> if it is <code>null</code>.
     * Else, the given throwable is logged using fatal log level. 
     * @param throwable the throwable
     */
    private void setTearDownThrowable(String context, Throwable throwable)
    {
        if (logger.isInfoEnabled())
            logger.info("Exception during "+context+": ", throwable);
        if (this.tearDownThrowable == null) {
            this.tearDownThrowable = throwable;
        }
    }
    
    /**
     * Intended for subclasses so that they may skip this class's normal tear down procedure.
     * @return true to run normal tear down, false to skip normal tear down
     */
    protected boolean preTearDown() {
        return true;
    }
    
    /**
     * This method clears data and resources allocated by testcases.
     * It first closes the persistence manager of this testcase.
     * Then it calls method <code>localTearDown</code>. 
     * Subclasses may override that method to clear any data and resources
     * that they have allocated in method <code>localSetUp</code>.
     * Finally, this method closes the persistence manager factory.<p>
     * 
     * <b>Note:</b>These methods are called always, regardless of any exceptions.
     * The first caught exception is kept in field <code>tearDownThrowable</code>. 
     * That exception is thrown as a nested exception of <code>JDOFatalException</code>
     * if and only if the testcase executed successful.
     * Otherwise that exception is logged using fatal log level.
     * All other exceptions are logged using fatal log level, always.<p>
     * 
     * <b>Note:</b>By default, the method tearDown does not close the pmf. 
     *  This is done at the end of each configuration, unless the property
     *  jdo.tck.closePMFAfterEachTest is set to true.
     */
    protected final void tearDown() {
        if (!preTearDown()) {
            return;
        }

        try {
            cleanupPM();
        } 
        catch (Throwable t) {
            setTearDownThrowable("cleanupPM", t);
        }
        
        if ((pmf == null || pmf.isClosed()) && 
            (this.tearDownInstances.size() > 0 || this.tearDownClasses.size() > 0))
            throw new JDOFatalException ("PMF must not be nullified or closed when tear down instances and /or classes have been added.");
        
        if (pmf != null && pmf.isClosed())
            pmf = null;
        
        try {
            if (cleanupData) {
                localTearDown();
            }
        } 
        catch (Throwable t) {
            setTearDownThrowable("localTearDown", t);
        }
        
        if (closePMFAfterEachTest) {
            try {
                closePMF();
            }
            catch (Throwable t) {
                setTearDownThrowable("closePMF", t);
            }
        }
        
        if (this.tearDownThrowable != null) {
            Throwable t = this.tearDownThrowable;
            this.tearDownThrowable = null;
            if (testSucceeded) {
                // runTest succeeded, but this method threw exception => error
                throw new JDOFatalException("Exception during tearDown", t);
            }
        }
    }

    /** 
     * Deletes all registered pc instances and extents of all registered pc classes. 
     * Subclasses may override this method to clear any data and resources
     * that they have allocated in method <code>localSetUp</code>.
     */
    protected void localTearDown() {
        deleteTearDownInstances();
        deleteTearDownClasses();
    }

    protected void addTearDownObjectId(Object oid) {
        // ensure that oid is not a PC instance
        if (JDOHelper.getObjectId(oid) != null ||
            JDOHelper.isTransactional(oid))
            throw new IllegalArgumentException("oid");
        this.tearDownInstances.add(oid);
    }
    
    protected void addTearDownInstance(Object pc) {
        Object oid = JDOHelper.getObjectId(pc);
        addTearDownObjectId(oid);
    }
    
    protected void addTearDownClass(Class pcClass) {
        this.tearDownClasses.add(pcClass);
    }
    
    protected void addTearDownClass(Class[] pcClasses) {
        if (pcClasses == null) return;
        for (int i = 0; i < pcClasses.length; ++i) {
            addTearDownClass(pcClasses[i]);
        }
    }
    
    /**
     * Deletes and removes tear down instances.
     * If there are no tear down instances, 
     * the this method is a noop. 
     * Otherwise, tear down instances are deleted 
     * exactly in the order they have been added.
     * Tear down instances are deleted in a separate transaction.
     */
    protected void deleteTearDownInstances() {
        if (this.tearDownInstances.size() > 0) {
            getPM();
            try {
                this.pm.currentTransaction().begin();
                for (Iterator i = this.tearDownInstances.iterator(); i.hasNext(); ) {
                    Object pc;
                    try {
                        pc = this.pm.getObjectById(i.next(), true);
                    }
                    catch (JDOObjectNotFoundException e) {
                        pc = null;
                    }
                    // we only delete those persistent instances
                    // which have not been deleted by tests already.
                    if (pc != null) {
                        this.pm.deletePersistent(pc);
                    }
                }
                this.pm.currentTransaction().commit();
            }
            finally {
                this.tearDownInstances.clear();
                cleanupPM();
            }
        }
    }

    /**
     * Deletes and removes tear down classes.
     * If there are no tear down classes, 
     * the this method is a noop. 
     * Otherwise, tear down classes are deleted 
     * exactly in the order they have been added.
     * Tear down classes are deleted in a separate transaction.
     * Deleting a tear down class means to delete the extent.
     */
    protected void deleteTearDownClasses() {
        if (this.tearDownClasses.size() > 0) {
            getPM();
            try {
                this.pm.currentTransaction().begin();
                for (Iterator i = this.tearDownClasses.iterator(); i.hasNext(); ) {
                    this.pm.deletePersistentAll(getAllObjects(this.pm, (Class)i.next()));
                }
                this.pm.currentTransaction().commit();
            }
            finally {
                this.tearDownClasses.clear();
                cleanupPM();
            }
        }
    }

    /** */
    protected Collection getAllObjects(PersistenceManager pm, Class pcClass) {
        Collection col = new Vector() ;
        Query query = pm.newQuery();
        query.setClass(pcClass);
        Extent candidates = null;
        try {
            candidates = pm.getExtent(pcClass, false);
        } catch (JDOException ex) {
            if (debug) logger.debug("Exception thrown for getExtent of class " +
                    pcClass.getName());
            return col;
        }
        query.setCandidates(candidates);
        Object result = query.execute();
        return (Collection)result;
    }

    /**
     * Get the <code>PersistenceManagerFactory</code> instance 
     * for the implementation under test.
     * @return field <code>pmf</code> if it is not <code>null</code>, 
     * else sets field <code>pmf</code> to a new instance and returns that instance.
     */
    protected PersistenceManagerFactory getPMF()
    {
        if (pmf == null) {
            PMFPropertiesObject = loadProperties(PMFProperties); // will exit here if no properties
            pmf = JDOHelper.getPersistenceManagerFactory(PMFPropertiesObject);
            if (supportedOptions == null) {
                supportedOptions = pmf.supportedOptions();
            }
        }
        return pmf;
    }

    protected Class getPMFClass()
    {
        if (pmf != null) {
            return pmf.getClass();
        }
        
        PMFPropertiesObject = loadProperties(PMFProperties);
        String name = PMFPropertiesObject.getProperty(PMF_CLASS_PROP);
        try {
            return Class.forName(name);
        } catch (ClassNotFoundException ex) {
            throw new JDOException("Cannot find PMF class '" + name + "'.",
                                   ex);
        }
    }
    
    /**
     * Get the <code>PersistenceManagerFactory</code> instance 
     * for the implementation under test. This method does NOT use the
     * JDOHelper method to retrieve the PMF, instead it creates an instance of
     * the class specified as javax.jdo.PersistenceManagerFactoryClass
     * property. The returned PMF is not configured. 
     * @return field <code>pmf</code> if it is not <code>null</code>, 
     * else sets field <code>pmf</code> to a new instance and returns that instance.
     */
    protected PersistenceManagerFactory getUnconfiguredPMF()
    {
        if (pmf == null) {
            String name = null;
            try {
                Class pmfClass = getPMFClass();
                name = pmfClass.getName();
                pmf = (PersistenceManagerFactory) pmfClass.newInstance();
                if (supportedOptions == null) {
                    supportedOptions = pmf.supportedOptions();
                }
            } catch (InstantiationException ex) {
                throw new JDOException("Cannot instantiate PMF class '" + 
                                       name + "'.", ex);
            } catch (IllegalAccessException ex) {
                throw new JDOException("Cannot access PMF class '" + name + 
                                       "' or its no-arg constructor.", ex);
            }
        }
        return pmf;
    }

    /**
     * Get the <code>PersistenceManager</code> instance 
     * for the implementation under test.
     */
    protected PersistenceManager getPM() {
        if (pm == null) {
            pm = getPMF().getPersistenceManager();
        }
        return pm;
    }

    
    /** 
     * This method cleans up the environment: closes the
     * <code>PersistenceManager</code>.  This  should avoid leaving
     * multiple PersistenceManager instances around, in case the
     * PersistenceManagerFactory performs PersistenceManager pooling.  
     */
    protected void cleanupPM() 
    {
        cleanupPM(pm);
        pm = null;
    }

    /** 
     * This method cleans up the specified 
     * <code>PersistenceManager</code>. If the pm still has an open
     * transaction, it will be rolled back, before closing the pm.
     */
    protected static void cleanupPM(PersistenceManager pm) 
    {
        if ((pm != null) && !pm.isClosed()) {
            if (pm.currentTransaction().isActive()) {
                pm.currentTransaction().rollback();
            }
            pm.close();
        }
    }

    /** Closes the pmf stored in this instance. */
    public static void closePMF() {
        JDOException failure = null;
        while (pmf != null) {
            try {
                if (!pmf.isClosed()) {
                    closePMF(pmf);
                }
                pmf = null;
            }
            catch (JDOException ex) {
                // store failure of first call pmf.close
                if (failure == null)
                    failure = ex;
                PersistenceManager[] pms = getFailedPersistenceManagers(
                    "closePMF", ex);
                for (int i = 0; i < pms.length; i++) {
                    cleanupPM(pms[i]);
                }
            }
            catch (RuntimeException ex) {
                pmf = null;
                ex.printStackTrace(System.out);
                throw ex;
            }
        }

        // rethrow JDOException thrown by pmf.close
        if (failure != null)
            throw failure;
    }

    /** Closes the pmf passed as a parameter. 
     * This must be done in a doPrivileged block.
     */
    public static void closePMF(final PersistenceManagerFactory PMF) {
        if (PMF != null) {
            if (!PMF.isClosed()) {
                AccessController.doPrivileged(
                    new PrivilegedAction () {
                        public Object run () {
                            PMF.close();
                            return null;
                        }
                    }
                );
            }
        }
    }


    /** */
    protected static PersistenceManager[] getFailedPersistenceManagers(
        String assertionFailure, JDOException ex) {
        Throwable[] nesteds = ex.getNestedExceptions();
        int numberOfExceptions = nesteds==null ? 0 : nesteds.length;
        PersistenceManager[] result = new PersistenceManager[numberOfExceptions];
        for (int i = 0; i < numberOfExceptions; ++i) {
            JDOException exc = (JDOException)nesteds[i];
            Object failedObject = exc.getFailedObject();
            if (exc.getFailedObject() instanceof PersistenceManager) {
                result[i] = (PersistenceManager)failedObject;
            } else {
                throw new JDOFatalException(assertionFailure,
                     "Unexpected failed object of type: " +
                     failedObject.getClass().getName());
            }
        }
        return result;
    }

    /**
     * This method load Properties from a given file.
     */
    protected Properties loadProperties(String fileName)
    {
        if (fileName == null) {
            fileName = System.getProperty("user.home") + "/.jdo/PMFProperties.properties";
        }
        Properties props = new Properties();
        InputStream propStream = null;
        try {
            propStream = new FileInputStream(fileName);
        }
        catch (IOException ex) {
            System.out.println("Could not open properties file \"" + fileName + "\"");
            System.out.println("Please specify a system property PMFProperties " + 
                               "with the PMF properties file name as value " + 
                               "(defaults to {user.home}/.jdo/PMFProperties.properties)");
            System.exit(1);
        }
        try {
            props.load(propStream);
        } 
        catch (IOException ex) {
            System.out.println("Error loading properties file \"" + fileName + "\"");
            ex.printStackTrace();
            System.exit(1);
        }
        return props;
    }

    /** 
     * Prints the specified msg (if debug is true), before it aborts the
     * test case.
     */
    public void fail(String assertionFailure, String msg) {
        if (debug) logger.debug(msg);
        fail(assertionFailure + NL + msg);
    }

    // Helper methods to check for supported options

    /**
     * Dump the supportedOptions to the a file in the specified directory.
     */
    public static void dumpSupportedOptions(String directory) {
        if (supportedOptions == null)
            return;
        File file = new File(directory, PMF_SUPPORTED_OPTIONS_FILE_NAME);
        if (file.exists())
            // PMF supported options have been dumped before => return
            return;
        PrintStream resultStream = null;
        try {
            resultStream = new PrintStream(new FileOutputStream(file));
            for (Iterator it = supportedOptions.iterator(); it.hasNext();) {
                resultStream.println((String)it.next());
            }
        } catch (FileNotFoundException e) {
            throw new JDOFatalException(
                "dumpSupportedOptions: cannot create file " + file.getName(), e);
        } finally {
            if (resultStream != null)
                resultStream.close();
        }
    }
     
    /** 
     * Prints a message (if debug is true) saying the test with the
     * specified name is not executed, because the JDO implementation under
     * test does not support the specified optional feature. 
     * @param testName the name of the test method that is skipped.
     * @param optionalFeature the name of the option not supported by the
     * JDO implementation under tets.
     */
    protected void printUnsupportedOptionalFeatureNotTested(
        String testName, String optionalFeature) {
        if (debug) {
            logger.debug(
                "Test " + testName + 
                " was not run, because optional feature " + optionalFeature + 
                " is not supported by the JDO implementation under test");
        }
    }

    /** Reports whether TransientTransactional is supported. */
    public boolean isTransientTransactionalSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.TransientTransactional");
    }
    
    /** Reports whether NontransactionalRead is supported. */
    public boolean isNontransactionalReadSupported(){
        return supportedOptions.contains(
            "javax.jdo.option.NontransactionalRead");
    }
    
    /** Reports whether NontransactionalWrite is supported. */
    public boolean isNontransactionalWriteSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.NontransactionalWrite");
    }

    /** Reports whether RetainValues is supported. */
    public boolean isRetainValuesSupported()
    {
        return supportedOptions.contains(
            "javax.jdo.option.RetainValues");
    }

    /** Reports whether Optimistic is supported. */
    public boolean isOptimisticSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.Optimistic");
    }

    /** Reports whether Application Identity is supported. */
    public boolean isApplicationIdentitySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.ApplicationIdentity");
    }

    /** Reports whether Datastore Identity is supported. */
    public boolean isDatastoreIdentitySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.DatastoreIdentity");
    }

    /** Reports whether Non-Durable Identity is supported. */
    public boolean isNonDurableIdentitySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.NonDurableIdentity");
    }

    /** Reports whether an <code>ArrayList</code> collection is supported. */
    public boolean isArrayListSupported() {        
        return supportedOptions.contains(
            "javax.jdo.option.ArrayList");
    }

    /** Reports whether a <code>HashMap</code> collection is supported. */
    public boolean isHashMapSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.HashMap");
    }

    /** Reports whether a <code>Hashtable</code> collection is supported. */
    public boolean isHashtableSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.Hashtable");
    }

    /** Reports whether a <code>LinkedList</code> collection is supported. */
    public boolean isLinkedListSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.LinkedList");
    }

    /** Reports whether a <code>TreeMap</code> collection is supported. */
    public boolean isTreeMapSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.TreeMap");
    }

    /** Reports whether a <code>TreeSet</code> collection is supported. */
    public boolean isTreeSetSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.TreeSet");
    }

    /** Reports whether a <code>Vector</code> collection is supported. */
    public boolean isVectorSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.Vector");
    }

    /** Reports whether a <code>Map</code> collection is supported. */
    public boolean isMapSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.Map");
    }

    /** Reports whether a <code>List</code> collection is supported. */
    public boolean isListSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.List");
    }

    /** Reports whether arrays are supported. */
    public boolean isArraySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.Array");
    }

    /** Reports whether a null collection is supported. */
    public boolean isNullCollectionSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.NullCollection");
    }

    /** Reports whether Changing Application Identity is supported. */
    public boolean isChangeApplicationIdentitySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.ChangeApplicationIdentity");
    }

    /** Reports whether Binary Compatibility is supported. */
    public boolean isBinaryCompatibilitySupported() {
        return supportedOptions.contains(
            "javax.jdo.option.BinaryCompatibility");
    }

    /** Reports whether UnconstrainedVariables is supported. */
    public boolean isUnconstrainedVariablesSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.UnconstrainedVariables");
    }
    
    /** Reports whether SQL queries are supported. */
    public boolean isSQLSupported() {
        return supportedOptions.contains(
            "javax.jdo.query.SQL");
    }
    
    /** Reports whether getting the DataStoreConnection is supported. */
    public boolean isDataStoreConnectionSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.GetDataStoreConnection");
    }

    /** Reports whether canceling a running query is supported. */
    public boolean isQueryCancelSupported() {
        return supportedOptions.contains(
            "javax.jdo.option.QueryCancel");
    }

    /** Reports whether setting a Datastore timout is supported. */
    public boolean isDatastoreTimeoutSupported() {
        return supportedOptions.contains(
            Constants.OPTION_DATASTORE_TIMEOUT);
    }

    /** Reports whether a feature is supported */
    public boolean isSupported(String option) {
        return supportedOptions.contains(option);
    }

    /**
     * Determine if a class is loadable in the current environment.
     */
    public static boolean isClassLoadable(String className) {
        try {
            Class.forName(className);
            return true;
        } catch (ClassNotFoundException ex) {
            return false;
        }
    }

    /** 
     * Determine if the environment is 1.4 version of JRE or better.
     */
    public static boolean isJRE14orBetter() {
        return isClassLoadable("java.util.Currency");
    }

    /**
     * This utility method returns a <code>String</code> that indicates the
     * current state of an instance. 
     * @param o The object.
     * @return The current state of the instance, by using the
     * <code>JDOHelper</code> state interrogation methods.
     */
    public static String getStateOfInstance(Object o)
    {
        boolean existingEntries = false;
        StringBuffer buff = new StringBuffer("{");
        if( JDOHelper.isPersistent(o) ){
            buff.append("persistent");
            existingEntries = true;
        }
        if( JDOHelper.isTransactional(o) ){
            if( existingEntries ) buff.append(", ");
            buff.append("transactional");
            existingEntries = true;
        }
        if( JDOHelper.isDirty(o) ){
            if( existingEntries ) buff.append(", ");
            buff.append("dirty");
            existingEntries = true;
        }
        if( JDOHelper.isNew(o) ){
            if( existingEntries ) buff.append(", ");
            buff.append("new");
            existingEntries = true;
        }
        if( JDOHelper.isDeleted(o) ){
            if( existingEntries ) buff.append(", ");
            buff.append("deleted");
        }
        if( JDOHelper.isDetached(o) ){
            if( existingEntries ) buff.append(", ");
            buff.append("detached");
        }
        buff.append("}");
        return buff.toString();
    }
    
    /**
     * This method will return the current lifecycle state of an instance.
     */
    public static int currentState(Object o)
    {
        boolean[] status = new boolean[NUM_STATUSES];
        status[IS_PERSISTENT]       = JDOHelper.isPersistent(o);
        status[IS_TRANSACTIONAL]    = JDOHelper.isTransactional(o);
        status[IS_DIRTY]            = JDOHelper.isDirty(o);
        status[IS_NEW]              = JDOHelper.isNew(o);
        status[IS_DELETED]          = JDOHelper.isDeleted(o);
        status[IS_DETACHED]         = JDOHelper.isDetached(o);
        int i, j;
    outerloop:
        for( i = 0; i < NUM_STATES; ++i ){
            for( j = 0; j < NUM_STATUSES; ++j ){
                if( status[j] != state_statuses[i][j] )
                    continue outerloop;
            }
            return i;
        }
        return NUM_STATES;
    }

    /**
     * Tests if a found state matches an expected state.
     */
    public static boolean compareStates(int found_state, int expected_state)
    {
        // status interrogation gives same values for PERSISTENT_NONTRANSACTIONAL and HOLLOW
        return (expected_state < 0
                || found_state == expected_state
                || (found_state == HOLLOW && expected_state == PERSISTENT_NONTRANSACTIONAL)
                || (found_state == PERSISTENT_NONTRANSACTIONAL && expected_state == HOLLOW));
    }

    /** This method mangles an object by changing all its non-static, 
     *  non-final fields.
     *  It returns true if the object was mangled, and false if there
     *  are no fields to mangle.
     */
    protected boolean mangleObject (Object oid) 
        throws Exception {
        Field[] fields = getModifiableFields(oid);
        if (fields.length == 0) return false;
        for (int i = 0; i < fields.length; ++i) {
            Field field = fields[i];
            Class fieldType = field.getType();
            if (fieldType == long.class) {
                field.setLong(oid, 10000L + field.getLong(oid));
            } else if (fieldType == int.class) {
                field.setInt(oid, 10000 + field.getInt(oid));
            } else if (fieldType == short.class) {
                field.setShort(oid, (short)(10000 + field.getShort(oid)));
            } else if (fieldType == byte.class) {
                field.setByte(oid, (byte)(100 + field.getByte(oid)));
            } else if (fieldType == char.class) {
                field.setChar(oid, (char)(10 + field.getChar(oid)));
            } else if (fieldType == String.class) {
                field.set(oid, "This is certainly a challenge" + (String)field.get(oid));
            } else if (fieldType == Integer.class) {
                field.set(oid, new Integer(10000 + ((Integer)field.get(oid)).intValue()));
            } else if (fieldType == Long.class) {
                field.set(oid, new Long(10000L + ((Long)field.get(oid)).longValue()));
            } else if (fieldType == Short.class) {
                field.set(oid, new Short((short)(10000 + ((Short)field.get(oid)).shortValue())));
            } else if (fieldType == Byte.class) {
                field.set(oid, new Byte((byte)(100 + ((Byte)field.get(oid)).byteValue())));
            } else if (fieldType == Character.class) {
                field.set(oid, new Character((char)(10 + ((Character)(field.get(oid))).charValue())));
            }
        }
        return true;
    }
    
    /** Returns modifiable Fields of the class of the parameter. 
     * Fields are considered modifiable if they are not static or final.
     * This method requires several permissions in order to run with
     * a SecurityManager, hence the doPrivileged block:
     * <ul>
     * <li>ReflectPermission("suppressAccessChecks")</li>
     * <li>RuntimePermission("accessDeclaredMembers")</li>
     * </ul>
     */
    protected Field[] getModifiableFields(final Object obj) {
        return (Field[])AccessController.doPrivileged(
            new PrivilegedAction () {
                public Object run () {
                    Class cls = obj.getClass();
                    List result = new ArrayList();
                    Field[] fields = cls.getFields();
                    for (int i = 0; i < fields.length; ++i) {
                        Field field = fields[i];
                        int modifiers = field.getModifiers();
                        if (Modifier.isFinal(modifiers) ||
                                Modifier.isStatic(modifiers))
                            continue;
                        field.setAccessible(true);
                        result.add(field);
                    }
                    return result.toArray(new Field[result.size()]);
                }
            }
        );
    }

    /**
     * Returns <code>true</code> if the current test runs with application
     * identity. This means the system property jdo.tck.identitytype has the
     * value applicationidentity.
     * @return <code>true</code> if current test runs with application
     * identity; <code>false</code> otherwise:
     */
    public boolean runsWithApplicationIdentity() {
        return APPLICATION_IDENTITY.equals(identitytype);
    }
    
    /** 
     * Prints a message (if debug is true) saying the test with the
     * specified name is not executed, because the JDO implementation under
     * test is run for an inapplicable identity type. 
     * @param testName the name of the test method that is skipped.
     * @param requiredIdentityType the name of the required identity type.
     */
    protected void printNonApplicableIdentityType(
        String testName, String requiredIdentityType) {
        if (debug) {
            logger.debug(
                "Test " + testName + 
                " was not run, because it is only applicable for identity type " + 
                requiredIdentityType + 
                ". The identity type of the current configuration is " +
                identitytype);
        }
    }

    /**
     * Returns the value of the PMF property 
     * given by argument <code>key</code>.
     * @param key the key
     * @return the value
     */
    protected String getPMFProperty(String key) {
        return PMFPropertiesObject.getProperty(key);
    }

    /**
     * Returns <code>true</code> if the implementation under test
     * supports all JDO options contained in system property
     * <code>jdo.tck.requiredOptions</code>.
     * @return <code>true</code> if the implementation under test
     * supports all JDO options contained in system property
     * <code>jdo.tck.requiredOptions</code>
     */
    protected boolean isTestToBePerformed() {
        boolean isTestToBePerformed = true;
        String requiredOptions = System.getProperty("jdo.tck.requiredOptions");
//        Collection supportedOptions = supportedOptions;
        StringTokenizer tokenizer = new StringTokenizer(
                requiredOptions, " ,;\n\r\t");
        while (tokenizer.hasMoreTokens()) {
            String requiredOption = tokenizer.nextToken();
            logger.debug("Required option: " + requiredOption);
            if (!requiredOption.equals("") &&
                !supportedOptions.contains(requiredOption)) {
                isTestToBePerformed = false;
                printUnsupportedOptionalFeatureNotTested(
                        getClass().getName(), requiredOption);
            }
        }
        return isTestToBePerformed;
    }
    
    /** New line.
     */
    public static final String NL = System.getProperty("line.separator");
    
    /** A buffer of of error messages.
     */
    protected static StringBuffer messages;
    
    /** Appends to error messages.
     */
    protected static synchronized void appendMessage(String message) {
        if (messages == null) {
            messages = new StringBuffer(NL);
        }
        messages.append(message);
        messages.append(NL);
    }
    
    /** Appends to error messages.
     */
    protected static synchronized void deferredAssertTrue(boolean test,
        String context, String message) {
        if (!test) {
            appendMessage(context + ": " +message);
        }
    }

    /** Appends an error if the actual value does not equal the expected value.
     * Primitive values are autoboxed. Null values are ok for both expected and actual.
     */
    protected void errorIfNotEqual(String message, Object expected, Object actual) {
        if (expected == null) {
            if (actual != null) {
                appendMessage(message + " failed. expected: null; actual: " + actual);
            }
        } else {
            if (!expected.equals(actual)) {
                appendMessage(message + " failed. expected: " + expected + "; actual: " + actual);
            }
        }
    }

    /** Appends an error if the actual value equals the unexpected value.
     * Primitive values are autoboxed. Null values are ok for both unexpected and actual.
     */
    protected void errorIfEqual(String message, Object unexpected, Object actual) {
        if (unexpected == null) {
            if (actual == null) {
                appendMessage(message + " failed. unexpected: null");
            }
        } else {
            if (unexpected.equals(actual)) {
                appendMessage(message + " failed. unexpected: " + unexpected);
            }
        }
    }

    /**
     * Returns collected error messages, or <code>null</code> if there
     * are none, and clears the buffer.
     */
    protected static synchronized String retrieveMessages() {
        if (messages == null) {
            return null;
        }
        final String msg = messages.toString();
        messages = null;
        return msg;
    }

    /** 
     * Fail the test if there are any error messages.
     */
    protected void failOnError() {
        String errors = retrieveMessages();
        if (errors != null) {
            fail (errors);
        }
    }

    /** Validate an actual isolation level against the requested level. */
    protected boolean validLevelSubstitution(String requested, String actual) {
        int requestedLevel = ((Integer)levelValues.get(requested)).intValue();
        int actualLevel = ((Integer)levelValues.get(actual)).intValue();
        return actualLevel >= requestedLevel;
    }

}
