/*
 * 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.query;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.jdo.JDOException;
import javax.jdo.JDOFatalInternalException;
import javax.jdo.JDOQLTypedQuery;
import javax.jdo.JDOUserException;
import javax.jdo.PersistenceManager;
import javax.jdo.Query;
import javax.jdo.Transaction;

import org.apache.jdo.tck.AbstractReaderTest;
import org.apache.jdo.tck.pc.company.CompanyModelReader;
import org.apache.jdo.tck.pc.mylib.MylibReader;
import org.apache.jdo.tck.pc.mylib.PCPoint;
import org.apache.jdo.tck.pc.mylib.PrimitiveTypes;
import org.apache.jdo.tck.util.ConversionHelper;
import org.apache.jdo.tck.util.EqualityHelper;

public abstract class QueryTest extends AbstractReaderTest {

    /** */
    public static final String SERIALZED_QUERY = "query.ser";

    /** */
    public static final String COMPANY_TESTDATA = 
        "org/apache/jdo/tck/pc/company/companyForQueryTests.xml";

    /** */
    public static final String MYLIB_TESTDATA = 
        "org/apache/jdo/tck/pc/mylib/mylibForQueryTests.xml";

    /** 
     * List of inserted instances (see methods insertPCPoints and
     * getFromInserted). 
     */
    protected List inserted = new ArrayList();
    
    /**
     * The company model reader is used 
     * to read company model instances from an XML file.
     * Instances refered by this reader are made persistent by
     * {@link QueryTest#loadAndPersistCompanyModel(PersistenceManager)}.
     */
    private CompanyModelReader companyModelReaderForPersistentInstances;

    /**
     * The company model reader is used 
     * to read company model instances from an XML file.
     * Instances refered by this reader remain transient.
     */
    private CompanyModelReader companyModelReaderForTransientInstances;

    /**
     * The mylib reader is used to read mylib instances from an XML file.
     * Instances refered by this reader are made persistent by
     * {@link QueryTest#loadAndPersistMylib(PersistenceManager)}.
     */
    private MylibReader mylibReaderForPersistentInstances;

    /**
     * The mylib reader is used to read mylib instances from an XML file.
     * Instances refered by this reader are made persistent by
     */
    private MylibReader mylibReaderForTransientInstances;

    // Helper methods to create persistent PCPoint instances

    /**
     *
     * @param pm the PersistenceManager
     */
    public void loadAndPersistPCPoints(PersistenceManager pm) {
        insertPCPoints(pm, 5);
    }

    /**
     *
     * @param pm the PersistenceManager
     * @param numInsert number of instances to be inserted
     */
    protected void insertPCPoints(PersistenceManager pm, int numInsert) {
        Transaction tx = pm.currentTransaction();
        try {
            tx.begin();
            for(int i = 0; i<numInsert; i++) {
                Object pc = new PCPoint(i, i);
                pm.makePersistent(pc);
                inserted.add(pc);
            }
            tx.commit();
            tx = null;
            if (debug) logger.debug("Total objects inserted : " + numInsert);
        } 
        finally {
            if ((tx != null) && tx.isActive())
                tx.rollback();
        }
    }
       
    /**
     * @param list list
     * @return list
     */
    public List getFromInserted(List list) {
        if (list == null)
            return null;
        
        List result = new ArrayList();
        for (Iterator iter = list.iterator(); iter.hasNext();) {
            Object pc = iter.next();
            for (Iterator iteri = inserted.iterator(); iteri.hasNext();) {
                Object pci = iteri.next();
                if (((PCPoint)pc).getX() == ((PCPoint)pci).getX()) {
                    result.add(pci);
                    break;
                }
            }
        }
        return result;
    }
    
    // Company model and mylib helper methods

    /**
     * Returns the name of the company test data resource.
     * @return name of the company test data resource. 
     */
    protected String getCompanyTestDataResource() {
        return COMPANY_TESTDATA;
    }
    
    /**
     * Initializes and returns the company model reader 
     * for persistent instances.
     * @return the company model reader for persistent instances.
     */
    private CompanyModelReader getCompanyModelReaderForPersistentInstances() {
        if (companyModelReaderForPersistentInstances == null) {
            companyModelReaderForPersistentInstances = 
                new CompanyModelReader(getCompanyTestDataResource());
        }
        return companyModelReaderForPersistentInstances;
    }
    
    /**
     * Initializes and returns the company model reader 
     * for transient instances.
     * @return the company model reader for transient instances.
     */
    private CompanyModelReader getCompanyModelReaderForTransientInstances() {
        if (companyModelReaderForTransientInstances == null) {
            companyModelReaderForTransientInstances = 
                new CompanyModelReader(getCompanyTestDataResource());
        }
        return companyModelReaderForTransientInstances;
    }
    
    /**
     * Initializes and returns the mylib reader 
     * for persistent instances.
     * @return the mylib reader for persistent instances.
     */
    private MylibReader getMylibReaderForPersistentInstances() {
        if (mylibReaderForPersistentInstances == null) {
            mylibReaderForPersistentInstances = 
                new MylibReader(MYLIB_TESTDATA);
        }
        return mylibReaderForPersistentInstances;
    }
    
    /**
     * Initializes and returns the mylib reader 
     * for transient instances.
     * @return the mylib reader for transient instances.
     */
    private MylibReader getMylibReaderForTransientInstances() {
        if (mylibReaderForTransientInstances == null) {
            mylibReaderForTransientInstances = 
                new MylibReader(MYLIB_TESTDATA);
        }
        return mylibReaderForTransientInstances;
    }
    
    /** 
     * Reads a graph of company model objects from the internal reader. This 
     * methods explictly calls makePersistent for all named instances using the
     * specified PersistenceManager. The method returns the CompanyModelReader 
     * instance allowing to access a compay model instance by name.
     * @param pm the PersistenceManager
     * @return the company model reader
     */
    public CompanyModelReader loadAndPersistCompanyModel(PersistenceManager pm) {
        makePersistentAll(
            pm, getRootList(getCompanyModelReaderForPersistentInstances()));
        return getCompanyModelReaderForPersistentInstances();
    }
    
    /** 
     * Reads a graph of mylib objects from the internal reader. This 
     * methods explictly calls makePersistent for all named instances using the
     * specified PersistenceManager. The method returns the CompanyModelReader 
     * instance allowing to access a compay model instance by name.
     * @param pm the PersistenceManager
     * @return mylib reader
     */
    public MylibReader loadAndPersistMylib(PersistenceManager pm) {
        makePersistentAll(
            pm, getRootList(getMylibReaderForPersistentInstances()));
        return getMylibReaderForPersistentInstances();
    }

    /**
     * Persists the given pc instances.
     * @param pm the PersistenceManager
     * @param pcInstances the pc instances to persist
     */
    private void makePersistentAll(PersistenceManager pm, List pcInstances) {
        Transaction tx = pm.currentTransaction();
        tx.begin();
        try {
            pm.makePersistentAll(pcInstances);
            if (debug) logger.debug("inserted " + pcInstances);
            tx.commit();
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
    }
    
    /**
     * Returns a persistent company model instance for the given bean name.
     * @param beanName the bean name.
     * @return the persistent company model instance. 
     */
    protected Object getPersistentCompanyModelInstance(String beanName) {
        return beanName == null ? null :
            getBean(getCompanyModelReaderForPersistentInstances(),beanName);
    }
    
    /**
     * Returns a transient company model instance for the given bean name.
     * @param beanName the bean name.
     * @return the transient company model instance. 
     */
    protected Object getTransientCompanyModelInstance(String beanName) {
        return beanName == null ? null :
            getBean(getCompanyModelReaderForTransientInstances(),beanName);
    }
    
    /**
     * Returns an array of persistent company model instances for bean names 
     * in the given argument.
     * @param beanNames the bean names of company mode instances.
     * @return the array of persistent company model instances. 
     */
    protected Object[] getPersistentCompanyModelInstances(String[] beanNames) {
        Object[] result = new Object[beanNames.length];
        for (int i = 0; i < beanNames.length; i++) {
            result[i] = getPersistentCompanyModelInstance(beanNames[i]);
        }
        return result;
    }
    
    /**
     * Returns an array of transient company model instances for bean names 
     * in the given argument.
     * @param beanNames the bean names of company mode instances.
     * @return the array of transient company model instances. 
     */
    protected Object[] getTransientCompanyModelInstances(String[] beanNames) {
        Object[] result = new Object[beanNames.length];
        for (int i = 0; i < beanNames.length; i++) {
            result[i] = getTransientCompanyModelInstance(beanNames[i]);
        }
        return result;
    }
    
    /**
     * Returns a list of persistent company model instances instances 
     * for beans names in the given argument.
     * @param beanNames the bean names of company model instances.
     * @return the list of persistent company model instances. 
     */
    protected List getPersistentCompanyModelInstancesAsList(
            String[] beanNames) {
        return new ArrayList(
                Arrays.asList(getPersistentCompanyModelInstances(beanNames)));
    }
    
    /**
     * Returns a list of transient company model instances instances 
     * for beans names in the given argument.
     * @param beanNames the bean names of company model instances.
     * @return the list of transient company model instances. 
     */
    protected List getTransientCompanyModelInstancesAsList(
            String[] beanNames) {
        return new ArrayList(
                Arrays.asList(getTransientCompanyModelInstances(beanNames)));
    }
    
    /**
     * Returns a persistent mylib instance for the given bean name.
     * @param beanName the bean name.
     * @return the persistent mylib instance. 
     */
    protected Object getPersistentMylibInstance(String beanName) {
        return beanName == null ? 
                null : getBean(getMylibReaderForPersistentInstances(),beanName);
    }
    
    /**
     * Returns a transient mylib instance for the given bean name.
     * @param beanName the bean name.
     * @return the transient mylib instance. 
     */
    protected Object getTransientMylibInstance(String beanName) {
        return beanName == null ? 
                null : getBean(getMylibReaderForTransientInstances(),beanName);
    }
    
    /**
     * Returns an array of persistent mylib instances for beans names 
     * in the given argument.
     * @param beanNames the bean names of mylib instances.
     * @return the array of persistent mylib instances. 
     */
    protected Object[] getPersistentMylibInstances(String[] beanNames) {
        Object[] result = new Object[beanNames.length];
        for (int i = 0; i < beanNames.length; i++) {
            result[i] = getPersistentMylibInstance(beanNames[i]);
        }
        return result;
    }
    
    /**
     * Returns an array of transient mylib instances for beans names 
     * in the given argument.
     * @param beanNames the bean names of mylib instances.
     * @return the array of transient mylib instances. 
     */
    protected Object[] getTransientMylibInstances(String[] beanNames) {
        Object[] result = new Object[beanNames.length];
        for (int i = 0; i < beanNames.length; i++) {
            result[i] = getTransientMylibInstance(beanNames[i]);
        }
        return result;
    }
    
    /**
     * Returns a list of persistent mylib instances for beans names 
     * in the given argument.
     * @param beanNames the bean names of mylib instances.
     * @return the list of persistent mylib instances. 
     */
    protected List getPersistentMylibInstancesAsList(String[] beanNames) {
        return Arrays.asList(getPersistentMylibInstances(beanNames));
    }
    
    /**
     * Returns a list of transient mylib instances for beans names 
     * in the given argument.
     * @param beanNames the bean names of mylib instances.
     * @return the list of transient mylib instances. 
     */
    protected List getTransientMylibInstancesAsList(String[] beanNames) {
        return Arrays.asList(getTransientMylibInstances(beanNames));
    }
    
    // PrimitiveTypes helper methods (creation and query)

    /**
     *
     * @param pm the PersistenceManager
     */
    public void loadAndPersistPrimitiveTypes(PersistenceManager pm) {
        insertPrimitiveTypes(pm);
    }

    /**
     *
     * @param pm the PersistenceManager
     */
    protected void insertPrimitiveTypes(PersistenceManager pm) {
        boolean bFlag = false;
        String strValue = "";
        char charValue = '\u0000';
        int numInsert = 10;
        
        Transaction tx = pm.currentTransaction();
        try {
            tx.begin();
            for (int i = 1; i <= numInsert; i++ ) {
                if (i%2 == 1) {
                    bFlag = true;
                    strValue = "Odd" + i;
                    charValue = 'O';
                } 
                else {
                    bFlag = false;
                    strValue = "Even" + i;
                    charValue = 'E';
                }
                PrimitiveTypes primitiveObject = new PrimitiveTypes(
                    (long)i, bFlag, Boolean.valueOf(bFlag), (byte)i, Byte.valueOf((byte)i),
                    (short)i, Short.valueOf((short)i), (int) i, Integer.valueOf(i),
                    (long)i, Long.valueOf(i), (float)i, Float.valueOf((float)i),
                    (double)i, Double.valueOf(i), charValue, Character.valueOf(charValue),
                    Calendar.getInstance().getTime(), strValue,
                    new BigDecimal(String.valueOf(i)), 
                    new BigInteger(String.valueOf(i)),
                    Long.valueOf(i));
                pm.makePersistent(primitiveObject);
            }
            tx.commit();
            tx = null;
            if (debug) logger.debug("Total objects inserted : " + numInsert);
        } 
        finally {
            if ((tx != null) && tx.isActive())
                tx.rollback();
        }
    }

    /**
     * Creates and executes a PrimitiveTypes query with the specified filter.
     * The method checks whether the query returns the expected result.
     * @param filter the filter
     * @param pm the PersistenceManager
     * @param expected expected
     * @param assertion assertion
     */
    protected void runSimplePrimitiveTypesQuery(String filter,
                                                PersistenceManager pm,
                                                Collection expected,
                                                String assertion) {
        Query q = pm.newQuery();
        q.setClass(PrimitiveTypes.class);
        q.setFilter(filter);
        Collection results = (Collection)q.execute();
        if (debug)
            logger.debug("execute '" + filter + "' returns " +  results.size() +
                         " instance(s)");
        checkQueryResultWithoutOrder(assertion, filter, results, expected);
    }

    /**
     * Creates and executes a PrimitiveTypes query with the specified filter,
     * parameter declarations and parameter values. The method checks whether
     * the query returns the expected result.
     * @param filter the filter
     * @param paramDecl the parameter declaration
     * @param paramValue the parameter values
     * @param pm the PersistenceManager
     * @param expected expected
     * @param assertion assertion
     */
    protected void runParameterPrimitiveTypesQuery(String filter,
                                                   String paramDecl,
                                                   Object paramValue,
                                                   PersistenceManager pm,
                                                   Collection expected,
                                                   String assertion) {
        Query q = pm.newQuery();
        q.setClass(PrimitiveTypes.class);
        q.setFilter(filter);
        q.declareParameters(paramDecl);
        Collection results = (Collection)q.execute(paramValue);
        if (debug)
            logger.debug("execute '" + filter + "' with param '" + paramValue +
                         "' returns " +  results.size() + " instance(s)");
        checkQueryResultWithoutOrder(assertion, filter, results, expected);
    }

    // Helper methods to check query result

    /**
     * Verify that expected equals result, including the order of the elements.
     * If not equal, fail the test.
     * If there is a filter != null, do not use this method. Use the method
     * of the same name that takes a String as the second argument.
     * @param assertion assertion
     * @param result the result
     * @param expected the expected
     */
    protected void checkQueryResultWithOrder(String assertion,
                                             Object result, 
                                             Object expected) {
        if (!equals(result, expected)) {
            queryFailed(assertion, "null", result, expected);
        }
    }
    
    /**
     * Verify that expected equals result, ignoring the order of the elements.
     * If not equal, fail the test.
     * If there is a filter != null, do not use this method. Use the method
     * of the same name that takes a String as the second argument.
     * @param assertion assertion
     * @param result the result
     * @param expected the expected
     */
    protected void checkQueryResultWithoutOrder(String assertion, 
                                                Object result, 
                                                Object expected) {
        // We need to explicitly check on collections passed as parameters
        // because equals(Object, Object) checks on lists and afterwards 
        // on collections. This ensures, lists are compared without order
        // for queries without an ordering specification.
        if (result instanceof Collection && expected instanceof Collection) {
            if (!equalsCollection((Collection)result, (Collection)expected)) {
                queryFailed(assertion, "null", result, expected);
            }
        } else {
            if (!equals(result, expected)) {
                queryFailed(assertion, "null", result, expected);
            }
        }
    }
    
    private void queryFailed(String assertion, String query, Object result, Object expected) {
        String lf = System.getProperty("line.separator");
        result = 
            ConversionHelper.convertObjectArrayElements(result);
        expected = 
            ConversionHelper.convertObjectArrayElements(expected);
        fail(assertion,
             "Wrong query result: " + lf +
             "query: " + query + lf +
             "expected: " + expected.getClass().getName() + 
                " of size " + size(expected) + lf + expected + lf +
             "got:      " + result.getClass().getName() + 
                " of size " + size(result) + lf + result);
    }

    /**
     *
     * @param assertion assertion
     * @param query the query
     * @param result the result
     * @param expected expected
     */
    protected void checkQueryResultWithOrder(String assertion,
                                             String query,
                                             Object result, 
                                             Object expected) {
        if (!equals(result, expected)) {
            queryFailed(assertion, query, result, expected);
        }
    }

    /**
     *
     * @param assertion assertion
     * @param query the query
     * @param result the result
     * @param expected expected
     */
    protected void checkQueryResultWithoutOrder(String assertion,
                                                String query,
                                                Object result, 
                                                Object expected) {
        // We need to explicitly check on collections passed as parameters
        // because equals(Object, Object) checks on lists and afterwards 
        // on collections. This ensures, lists are compared without order
        // for queries without an ordering specification.
        if (result instanceof Collection && expected instanceof Collection) {
            if (!equalsCollection((Collection)result, (Collection)expected)) {
                queryFailed(assertion, query, result, expected);
            }
        } else {
            if (!equals(result, expected)) {
                queryFailed(assertion, query, result, expected);
            }
        }
    }
    
    /**
     * Returns <code>true</code> 
     * if <code>o1</code> and <code>o2</code> equal.
     * This method is capable to compare object arrays, 
     * collections of object arrays, maps of object arrays.
     * This method implements a narrowing in case of floating point values.
     * In case of big decimals it calls 
     * {@link BigDecimal#compareTo(java.math.BigDecimal)}.
     * It allows <code>o1</code> and/or <code>o2</code>
     * to be <code>null</code>. 
     * @param o1 the first object
     * @param o2 the second object
     * @return <code>true</code> if <code>o1</code> and <code>o2</code> equal.
     */
    protected boolean equals(Object o1, Object o2) {
        boolean result;
        if (o1 == o2) {
            result = true;
        } else if ((o1 instanceof Object[]) && (o2 instanceof Object[])) {
            result = equalsObjectArray((Object[])o1, (Object[])o2);
        } else if ((o1 instanceof List) && (o2 instanceof List)) {
            result = equalsList((List)o1, (List)o2);
        } else if ((o1 instanceof Collection) && (o2 instanceof Collection)) {
            result = equalsCollection((Collection)o1, (Collection)o2);
        } else if ((o1 instanceof Map) && (o2 instanceof Map)) {
            result = equalsMap((Map)o1, (Map)o2);
        } else if ((o1 instanceof Float) && (o2 instanceof Float)) {
            result = closeEnough(((Float)o1).floatValue(), 
                    ((Float)o2).floatValue());
        } else if ((o1 instanceof Double) && (o2 instanceof Double)) {
            result = closeEnough(((Double)o1).floatValue(), 
                    ((Double)o2).floatValue());
        } else if ((o1 instanceof BigDecimal) && (o2 instanceof BigDecimal)) {
            result = ((BigDecimal)o1).compareTo((BigDecimal)o2) == 0;
        } else if (o1 != null) {
            result = o1.equals(o2);
        } else {
            // Due to the first if and due to the last if, we have:
            // o1 == null && o2 != null
            result = false;
        }
        return result;
    }
    
    /**
     * Returns <code>true</code> 
     * if <code>o1</code> and <code>o2</code> equal.
     * This method iterates over both object arrays and calls
     * {@link QueryTest#equals(Object, Object)} passing
     * corresponding instances.
     * {@link QueryTest#equals(Object, Object)} is called rather than
     * {@link Object#equals(java.lang.Object)} because object arrays
     * having equal elements cannot be compared calling  
     * {@link Object#equals(java.lang.Object)}.
     * This method does not allow <code>o1</code> and <code>o2</code>
     * to be <code>null</code> both. 
     * @param o1 the first object array
     * @param o2 the second object array
     * @return <code>true</code> if <code>o1</code> and <code>o2</code> equal.
     */
    protected boolean equalsObjectArray(Object[] o1, Object[] o2) {
        boolean result = true;
        if (o1 != o2) {
            if (o1.length != o2.length) {
                result = false;
            } else {
                for (int i = 0; i < o1.length; i++ ) {
                    if (!equals(o1[i], o2[i])) {
                        result = false;
                        break;
                    }
                }
            } 
        }
        return result;
    }

    /**
     * Returns <code>true</code> 
     * if <code>o1</code> and <code>o2</code> equal.
     * This method iterates both lists and 
     * calls {@link QueryTest#equals(Object, Object)} on corresponding elements.
     * {@link QueryTest#equals(Object, Object)} is called rather than
     * {@link Object#equals(java.lang.Object)} because object arrays
     * having equal elements cannot be compared calling  
     * {@link Object#equals(java.lang.Object)}.
     * This method does not allow <code>o1</code> and <code>o2</code>
     * to be <code>null</code> both. 
     * @param o1 the first list
     * @param o2 the second list
     * @return <code>true</code> if <code>o1</code> and <code>o2</code> equal.
     */
    protected boolean equalsList(List o1, List o2) {
        boolean result = true;
        if (o1 != o2) {
            if (o1.size() != o2.size()) {
                result = false;
            } else {
                Iterator i = o1.iterator();
                Iterator ii = o2.iterator();
                while (i.hasNext()) {
                    Object firstObject = i.next();
                    Object secondObject = ii.next();
                    if (!equals(firstObject, secondObject)) {
                        result = false;
                        break;
                    }
                }
            }
        }
        return result;
    }
                        
    /**
     * Returns <code>true</code> 
     * if <code>o1</code> and <code>o2</code> equal.
     * This method iterates over the first collection and 
     * checks if each instance is contained in the second collection
     * by calling remove(Collection, Object). This guarantees that
     * the cardinality of each instance in the first collection matches
     * the cardinality of each instance in the second collection.
     * This method does not allow <code>o1</code> and <code>o2</code>
     * to be <code>null</code> both. 
     * @param o1 the first collection
     * @param o2 the second collection
     * @return <code>true</code> if <code>o1</code> and <code>o2</code> equal.
     */
    protected boolean equalsCollection(Collection o1, Collection o2) {
        // make a copy of o2 so we can destroy it
        Collection o2copy = new ArrayList();
        Iterator i2 = o2.iterator();
        while (i2.hasNext()) {
            o2copy.add(i2.next());
        }
        boolean result = true;
        if (o1 != o2) {
            if (o1.size() != o2.size()) {
                result = false;
            } else {
                for (Iterator i = o1.iterator(); i.hasNext(); ) {
                    Object oo1 = i.next();
                    if (!remove(o2copy, oo1)) {
                        result = false;
                        break;
                    }
                }
            } 
        }
        return result;
    }
    
    /**
     * Returns <code>true</code> 
     * if <code>o1</code> and <code>o2</code> equal.
     * This method checks if the key sets and the value sets of both
     * maps equal calling equalsCollection(Collection, Collection).
     * This method does not allow <code>o1</code> and <code>o2</code>
     * to be <code>null</code> both. 
     * @param o1 the first map
     * @param o2 the second map
     * @return <code>true</code> if <code>o1</code> and <code>o2</code> equal.
     */
    protected boolean equalsMap(Map o1, Map o2) {
        boolean result = true;
        if (o1 != o2) {
            if (o1.size() != o2.size()) {
                result = false;
            } else {
                for (Iterator i = o1.entrySet().iterator(); i.hasNext(); ) {
                    Map.Entry entry = (Map.Entry) i.next();
                    Object key = entry.getKey();
                    Object value = entry.getValue();
                    Object value2 = o2.get(key);
                    if (!equals(value, value2)) {
                        result = false;
                        break;
                    }
                }
            } 
        }
        return result;
    }
    
    /**
     * Returns <code>true</code> if <code>o</code> is contained
     * in the given collection.
     * This method iterates the given collection and calls
     * {@link QueryTest#equals(Object, Object)} for each instance.
     * {@link QueryTest#equals(Object, Object)} is called rather than
     * {@link Object#equals(java.lang.Object)} because object arrays
     * having equal elements cannot be compared calling  
     * {@link Object#equals(java.lang.Object)}.
     * @param col the collection
     * @param o the object
     * @return <code>true</code> if <code>o</code> is contained
     * in the given collection.
     */
    private boolean contains(Collection col, Object o) {
        for (Iterator i = col.iterator(); i.hasNext(); ) {
            if (equals(o, i.next())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns <code>true</code> if <code>o</code> is contained
     * in the given collection and was removed.
     * This method iterates the given collection and calls
     * {@link QueryTest#equals(Object, Object)} for each instance.
     * {@link QueryTest#equals(Object, Object)} is called rather than
     * {@link Object#equals(java.lang.Object)} because object arrays
     * having equal elements cannot be compared calling  
     * {@link Object#equals(java.lang.Object)}.
     * @param col the collection
     * @param o the object
     * @return <code>true</code> if <code>o</code> is contained
     * in the given collection and was removed.
     */
    private boolean remove(Collection col, Object o) {
        for (Iterator i = col.iterator(); i.hasNext(); ) {
            if (equals(o, i.next())) {
                i.remove();
                return true;
            }
        }
        return false;
    }

    /** Returns <code>true</code> if the specified float values are close
     * enough to be considered to be equal for a deep equals
     * comparison. Floating point values are not exact, so comparing them
     * using <code>==</code> might not return useful results. This method
     * checks that both double values are within some percent of each
     * other. 
     * @param d1 one double to be tested for close enough 
     * @param d2 the other double to be tested for close enough 
     * @return <code>true</code> if the specified values are close enough.
     */
    public boolean closeEnough(double d1, double d2) {
        if (d1 == d2)
            return true;

        double diff = Math.abs(d1 - d2);
        return diff < Math.abs((d1 + d2) * EqualityHelper.DOUBLE_EPSILON);
    }

    /**
     * Returns <code>true</code> if the specified float values are close
     * enough to be considered to be equal for a deep equals
     * comparison. Floating point values are not exact, so comparing them 
     * using <code>==</code> might not return useful results. This method
     * checks that both float values are within some percent of each
     * other. 
     * @param f1 one float to be tested for close enough 
     * @param f2 the other float to be tested for close enough 
     * @return <code>true</code> if the specified values are close enough.
     */
    public boolean closeEnough(float f1, float f2) {
        if (f1 == f2)
            return true;

        float diff = Math.abs(f1 - f2);
        return diff < Math.abs((f1 + f2) * EqualityHelper.FLOAT_EPSILON);
    }


    /** 
     * Returns the size of the object. If it is a multivalued object
     * (Collection, Map, or array) return the number of elements. If not, 
     * return 1.
     * @param o the object
     * @return size
     */
    protected int size(Object o) {
        if (o instanceof Collection) {
            return ((Collection)o).size();
        }
        if (o instanceof Object[]) {
            return ((Object[])o).length;
        }
        if (o instanceof Map) {
            return ((Map)o).size();
        }
        return 1;
    }

    // Debugging helper methods

    /**
     *
     * @param results the results
     * @param expected expected values
     */
    protected void printOutput(Object results, Collection expected) {
        if (!debug)
            return;

        Iterator iter = null;
        PCPoint pcp = null;
        if (results == null) {
            logger.debug("Query returns null");
        }
        if (!(results instanceof Collection)) {
            logger.debug("Query result is not a collection: " +
                         results.getClass().getName());
        }
        logger.debug("Retrived Objects are:");
        iter = ((Collection)results).iterator();
        while (iter.hasNext()) {
            pcp = (PCPoint)iter.next();
            logger.debug("X = " + pcp.getX() + "\tY = " + pcp.getY());
        }
            
        logger.debug("Expected Objects are:");
        iter = ((Collection)expected).iterator();
        while (iter.hasNext()) {
            pcp = (PCPoint)iter.next();
            logger.debug("X = " + pcp.getX() + "\tY = " + pcp.getY());
        }
    }
    
    // compile query methods

    /**
     * Compiles the given query element holder instance as a JDO API query.
     * Argument <code>positive</code> determines if the compilation is supposed
     * to succeed or to fail. If <code>true</code> and the compilation fails,
     * then the test case fails prompting argument <code>assertion</code>.
     * If <code>false</code> and the compilation succeeds,
     * then the test case fails prompting argument <code>assertion</code>.
     * Otherwise the test case succeeds. 
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param positive determines if the compilation is supposed
     * to succeed or to fail.
     */
    protected void compileAPIQuery(String assertion,
            QueryElementHolder queryElementHolder, boolean positive) {
        if (logger.isDebugEnabled()) {
            logger.debug("Compiling API query: " + queryElementHolder);
        }
        compile(assertion, queryElementHolder, false, 
                queryElementHolder.toString(), positive);
    }

    /**
     * Compiles the given query element holder instance 
     * as a JDO single string query.
     * Argument <code>positive</code> determines if the compilation is supposed
     * to succeed or to fail. If <code>true</code> and the compilation fails,
     * then the test case fails prompting argument <code>assertion</code>.
     * If <code>false</code> and the compilation succeeds,
     * then the test case fails prompting argument <code>assertion</code>.
     * Otherwise the test case succeeds. 
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param positive determines if the compilation is supposed
     * to succeed or to fail.
     */
    protected void compileSingleStringQuery(String assertion,
            QueryElementHolder queryElementHolder, boolean positive) {
        if (logger.isDebugEnabled())
            logger.debug("Compiling single string query: " + 
                    queryElementHolder);
        compile(assertion, queryElementHolder, true, 
                queryElementHolder.toString(), positive);
    }
    
    /**
     * Compiles the given single string query.
     * Argument <code>positive</code> determines if the compilation is supposed
     * to succeed or to fail. If <code>true</code> and the compilation fails,
     * then the test case fails prompting argument <code>assertion</code>.
     * If <code>false</code> and the compilation succeeds,
     * then the test case fails prompting argument <code>assertion</code>.
     * Otherwise the test case succeeds. 
     * @param assertion the assertion to prompt if the test case fails.
     * @param singleStringQuery the single string query
     * @param positive determines if the compilation is supposed
     * to succeed or to fail.
     */
    protected void compileSingleStringQuery(String assertion, 
            String singleStringQuery, boolean positive) {
        if (logger.isDebugEnabled())
            logger.debug("Compiling single string query: " + 
                    singleStringQuery);
        compile(assertion, null, true, singleStringQuery, positive);
    }

    /**
     * Compiles the given query element holder instance 
     * as a JDO API query or single string query, 
     * depending on argument <code>asSingleString</code>.
     * Argument <code>singleStringQuery</code> is used to support queries
     * which cannot be expressed as query element holder instances.
     * That argument is ignored if argument <code>queryElementHolder</code> 
     * is set.
     * Argument <code>positive</code> determines if the compilation is supposed
     * to succeed or to fail. If <code>true</code> and the compilation fails,
     * then the test case fails prompting argument <code>assertion</code>.
     * If <code>false</code> and the compilation succeeds,
     * then the test case fails prompting argument <code>assertion</code>.
     * Otherwise the test case succeeds. 
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to compile.
     * @param asSingleString determines if the query specified by 
     * <code>queryElementHolder</code> is compiled as single string query 
     * or as API query. 
     * @param singleStringQuery the query to compile 
     * as a JDO single string query if there is no query element holder.
     * @param positive determines if the compilation is supposed
     * to succeed or to fail.
     */
    private void compile(String assertion, 
            QueryElementHolder queryElementHolder, boolean asSingleString,  
            String singleStringQuery, boolean positive) {
        getPM();
        try {
            Query query;
            if (queryElementHolder != null) {
                if (asSingleString) {
                    query = queryElementHolder.getSingleStringQuery(pm);
                } else {
                    query = queryElementHolder.getAPIQuery(pm);
                }
            } else {
                query = getPM().newQuery(singleStringQuery);
            }
            compile(assertion, query, singleStringQuery, positive);
        } catch (JDOUserException e) {
            // This exception handler considers a JDOUserException
            // to be thrown in newQuery methods.
            // A JDOUserException may be expected in case of negative tests. 
            if (positive) {
                fail(assertion + "Query '" + queryElementHolder +
                        "' must be compilable. The exception message is: " + 
                        e.getMessage());
            }
        }
    }
    
    /**
     * Compiles the given query instance.
     * Argument <code>positive</code> determines if the compilation is supposed
     * to succeed or to fail. 
     * If <code>true</code> and the compilation fails,
     * then the test case fails prompting arguments <code>assertion</code>
     * and <code>queryText</code>. 
     * If <code>false</code> and the compilation succeeds,
     * then the test case fails prompting argument <code>assertion</code>
     * and <code>queryText</code>. 
     * Otherwise the test case succeeds. 
     * @param assertion asserting text
     * @param query the query instance
     * @param queryText the text representation of the query
     * @param positive positive test
     */
    protected void compile(String assertion, 
            Query query, String queryText, boolean positive) {
        getPM();
        Transaction tx = pm.currentTransaction();
        tx.begin();
        try {
            query.compile();
            if (!positive) {
                fail(assertion, 
                        "Query compilation must throw JDOUserException: " + 
                        queryText);
            }
        } catch (JDOUserException e) {
            if (positive) {
                fail(assertion, "Query '" + queryText +
                        "' must be compilable. The exception message is: " + 
                        e.getMessage());
            }
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
    }
    
    // execute query methods
    
    /**
     * Executes the given query element holder instance as a JDO API query.
     * The result of that query is compared against the given argument 
     * <code>expectedResult</code>. 
     * If the expected result does not match the returned query result,
     * then the test case fails prompting argument <code>assertion</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param expectedResult the expected query result.
     */
    protected void executeAPIQuery(String assertion,
            QueryElementHolder queryElementHolder, Object expectedResult) {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing API query: " + queryElementHolder);
        }
        execute(assertion, queryElementHolder, false, expectedResult);
    }
    
    /**
     * Executes the given query element holder instance 
     * as a JDO single string query.
     * The result of that query is compared against the given argument 
     * <code>expectedResult</code>. 
     * If the expected result does not match the returned query result,
     * then the test case fails prompting argument <code>assertion</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param expectedResult the expected query result.
     */
    protected void executeSingleStringQuery(String assertion,
            QueryElementHolder queryElementHolder, Object expectedResult) {
        if (logger.isDebugEnabled())
            logger.debug("Executing single string query: " +
                    queryElementHolder);
        execute(assertion, queryElementHolder, true, expectedResult);
    }

    /**
     * 
     * @param assertion assertion text
     * @param queryElementHolder query elements
     * @param expectedResult expected query result
     */
    protected void executeJDOQLTypedQuery(String assertion, QueryElementHolder queryElementHolder,
                                          Object expectedResult) {
        executeJDOQLTypedQuery(assertion, queryElementHolder, null, false, expectedResult);
    }

    /**
     *
     * @param assertion assertion text
     * @param queryElementHolder query elements
     * @param resultClass result class object
     * @param resultClauseSpecified flag whether result clause is specified
     * @param expectedResult the expected query result
     */
    protected void executeJDOQLTypedQuery(String assertion, QueryElementHolder queryElementHolder,
                                          Class<?> resultClass, boolean resultClauseSpecified,
                                          Object expectedResult) {
        String singleStringQuery = queryElementHolder.toString();
        getPM();
        Transaction tx = pm.currentTransaction();
        tx.begin();
        try {
            JDOQLTypedQuery<?> query = queryElementHolder.getJDOQLTypedQuery();
            if (query == null) {
                throw new JDOFatalInternalException("Missing JDOQLTyped instance");
            }
            Object result = null;
            try {
                Map<String, ?> paramValues = queryElementHolder.getParamValues();
                if (paramValues != null) {
                    query.setParameters(paramValues);
                }

                if (resultClass != null) {
                    if (queryElementHolder.isUnique()) {
                        // result class specified and unique result
                        result = query.executeResultUnique(resultClass);
                    } else {
                        // result class specified and list result
                        result = query.executeResultList(resultClass);
                    }
                } else if (resultClauseSpecified) {
                    if (queryElementHolder.isUnique()) {
                        // result class specified and unique result
                        result = query.executeResultUnique();
                    } else {
                        // result class specified and list result
                        result = query.executeResultList();
                    }
                } else {
                    if (queryElementHolder.isUnique()) {
                        // no result class and unique result
                        result = query.executeUnique();
                    } else {
                        // no result class and list result
                        result = query.executeList();
                    }
                }

                if (logger.isDebugEnabled()) {
                    logger.debug("Query result: " + ConversionHelper.
                            convertObjectArrayElements(result));
                }

                checkResult(assertion, singleStringQuery, queryElementHolder.hasOrdering(),
                        result, expectedResult, true);
            } finally {
                query.close(result);
            }
        } catch (JDOUserException e) {
            String msg = "JDOUserException thrown while executing query:\n" + singleStringQuery;
            throw new JDOException(msg, e);
        } catch (JDOException e) {
            String msg = "JDOException thrown while executing query:\n" + singleStringQuery;
            throw new JDOException(msg, e);
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
    }

    /**
     * Converts the given query element holder instance 
     * to a JDO query instance,
     * based on argument <code>asSingleString</code>.
     * Afterwards, delegates to method 
     * {@link QueryTest#execute(String, Query, String, boolean, Object, Object, boolean)}.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param asSingleString determines if the query is executed as
     * single string query or as API query.
     * @param expectedResult the expected query result.
     * @return the query result
     */
    private Object execute(String assertion, QueryElementHolder queryElementHolder,
                           boolean asSingleString, Object expectedResult) {
        getPM();
        Query query = asSingleString ?
                queryElementHolder.getSingleStringQuery(pm) :
                    queryElementHolder.getAPIQuery(pm);
        Object result = execute(assertion, query, 
                queryElementHolder.toString(), 
                queryElementHolder.hasOrdering(), queryElementHolder.getParamValues(),
                expectedResult, true);
        return result;
    }

    /**
     * Executes the given query instance delegating to
     * execute(String, Query, String, boolean, Object, Object, boolean).
     * Logs argument <code>singleStringQuery</code> 
     * if debug logging is enabled.
     * @param assertion the assertion to prompt if the test case fails.
     * @param query the query to execute.
     * @param singleStringQuery the single string representation of the query.
     * This parameter is only used as part of the falure message.
     * @param hasOrdering indicates if the query has an ordering clause.
     * @param parameters the parmaters of the query.
     * @param expectedResult the expected query result.
     * @param positive indicates if query execution is supposed to fail
     * @return the query result
     */
    protected Object executeJDOQuery(String assertion, Query query, 
            String singleStringQuery, boolean hasOrdering,
            Object[] parameters, Object expectedResult, boolean positive) {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing JDO query: " + singleStringQuery);
        }
        return execute(assertion, query, singleStringQuery, hasOrdering,
                parameters, expectedResult, positive);
    }

    /**
     * Executes the given SQL string as a JDO SQL query.
     * The result of that query is compared against the given argument 
     * <code>expectedResult</code>. 
     * If the expected result does not match the returned query result,
     * then the test case fails prompting argument <code>assertion</code>.
     * Argument <code>unique</code> indicates, if the query is supposed
     * to return a single result.
     * Argument <code>sql</code> may contain the substring <code>"{0}"</code>.
     * All occurences of this substring are replaced 
     * by the value of PMF property <code>"javax.jdo.mapping.Schema"</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param sql the SQL string.
     * @param candidateClass the candidate class.
     * @param resultClass the result class.
     * @param positive positive option
     * @param parameters the parameters of the query.
     * @param expectedResult the expected query result.
     * @param unique indicates, if the query is supposed 
     * to return a single result.
     */
    protected void executeSQLQuery(String assertion, String sql, 
            Class candidateClass, Class resultClass, boolean positive,
            Object parameters, Object expectedResult, boolean unique) {
        String schema = getPMFProperty("javax.jdo.mapping.Schema");
        sql = MessageFormat.format(sql, new Object[]{schema});
        if (logger.isDebugEnabled())
            logger.debug("Executing SQL query: " + sql);
        Query query = getPM().newQuery("javax.jdo.query.SQL", sql);
        if (unique) {
            query.setUnique(unique);
        }
        if (candidateClass != null) {
            query.setClass(candidateClass);
        }
        if (resultClass != null) {
            query.setResultClass(resultClass);
        }
        execute(assertion, query, sql, false, 
                parameters, expectedResult, positive);
    }

    /**
     * Executes the given query instance.
     * If argument <code>parameters</code> is an object array,
     * then it is passed as an argument
     * to the method {@link Query#executeWithArray(java.lang.Object[])}.
     * If argument <code>parameters</code> is a map,
     * then it is passed as an argument
     * to the method {@link Query#executeWithMap(java.util.Map)}.
     * If argument <code>parameters</code> is a list,
     * then the list elements are passed as arguments
     * to the execute methods taking actual parameter values.
     * If argument <code>parameters</code> is <code>null</code>,
     * then method {@link Query#execute()} is called 
     * on the given query instance instead.<p>
     * 
     * The result of query execution is compared against the argument 
     * <code>expectedResult</code>. If the two values differ, 
     * then this method throws an {@link junit.framework.AssertionFailedError} and
     * the calling test case fails prompting argument 
     * <code>assertion</code>.<p>
     * 
     * If argument <code>positive</code> is <code>false</code>,
     * then the test case invoking this method is considered to be
     * a negative test case. 
     * Then, query execution is expected to throw a {@link JDOUserException}.
     * If query execution succeeds in this case, then this method throws
     * an {@link junit.framework.AssertionFailedError} and the calling test case fails
     * prompting argument <code>assertion</code>.<p>
     * 
     * @param assertion the assertion to prompt if the test case fails.
     * @param query the query to execute.
     * @param singleStringQuery the single string representation of the query.
     * This parameter is only used as part of the falure message.
     * @param hasOrdering indicates if the query has an ordering clause.
     * @param parameters the parmaters of the query.
     * @param expectedResult the expected query result.
     * @param positive indicates if query execution is supposed to fail
     * @return the query result
     */
    private Object execute(String assertion, Query query, 
            String singleStringQuery, boolean hasOrdering,
            Object parameters, Object expectedResult, boolean positive) {
        Object result = null;
        getPM();
        Transaction tx = pm.currentTransaction();
        tx.begin();
        try {
            try {
                if (parameters == null) {
                    result = query.execute();
                } else if (parameters instanceof Object[]) {
                    result = query.executeWithArray((Object[])parameters);
                } else if (parameters instanceof Map) {
                    result = query.executeWithMap((Map)parameters);
                } else if (parameters instanceof List) {
                    List list = (List) parameters;
                    switch (list.size()) {
                        case 1:
                            result = query.execute(list.get(0));
                            break;
                        case 2:
                            result = query.execute(list.get(0), list.get(1));
                            break;
                        case 3:
                            result = query.execute(
                                    list.get(0), list.get(1), list.get(2));
                            break;
                        default:
                            throw new IllegalArgumentException(
                                "Argument parameters is a list " +
                                "and must have 1, 2, or 3 elements.");
                    }
                } else {
                    throw new IllegalArgumentException("Argument parameters " +
                            "must be instance of List, Map, Object[], or null.");
                }
                
                if (logger.isDebugEnabled()) {
                    logger.debug("Query result: " + ConversionHelper.
                        convertObjectArrayElements(result));
                }
    
                checkResult(assertion, singleStringQuery, hasOrdering, result, expectedResult, positive);

            } finally {
                query.close(result);
            }
        } catch (JDOUserException e) {
            if (positive) {
                String msg = "JDOUserException thrown while executing query:\n" +
                        singleStringQuery;
                throw new JDOException(msg, e);
            }
        } catch (JDOException e) {
            String msg = "JDOException thrown while executing query:\n" +
                    singleStringQuery;
            throw new JDOException(msg, e);
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
        return result;
    }

    /**
     * @param assertion the assertion to prompt if the test case fails.
     * @param singleStringQuery the single string representation of the query.
     * This parameter is only used as part of the falure message.
     * @param hasOrdering indicates if the query has an ordering clause.
     * @param positive indicates if query execution is supposed to fail
     * @param result the query result.
     * @param expectedResult the expected query result.
    */
    private void checkResult(String assertion, String singleStringQuery, boolean hasOrdering,
                             Object result, Object expectedResult, boolean positive) {
        if (positive) {
            if (hasOrdering) {
                checkQueryResultWithOrder(assertion, singleStringQuery, result,
                        expectedResult);
            } else {
                checkQueryResultWithoutOrder(assertion, singleStringQuery, result,
                        expectedResult);
            }
        } else {
            fail(assertion, "Query must throw JDOUserException: " +
                    singleStringQuery);
        }
    }

    /**
     * Converts the given query element holder instance to a
     * JDO query instance.
     * Calls {@link Query#deletePersistentAll()}, or
     * {@link Query#deletePersistentAll(java.util.Map)}, or
     * {@link Query#deletePersistentAll(java.lang.Object[])}
     * depending on the type of argument <code>parameters</code>.
     * If the number of deleted objects does not 
     * match <code>expectedNrOfDeletedObjects</code>,
     * then the test case fails prompting argument <code>assertion</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    protected void deletePersistentAllByAPIQuery(String assertion,
            QueryElementHolder queryElementHolder, 
            long expectedNrOfDeletedObjects) {
        if (logger.isDebugEnabled()) {
            logger.debug("Deleting persistent by API query: " + 
                    queryElementHolder);
        }
        delete(assertion, queryElementHolder, false, expectedNrOfDeletedObjects);
    }
    
    /**
     * Converts the given query element holder instance to a
     * JDO query instance.
     * Calls {@link Query#deletePersistentAll()}, or
     * {@link Query#deletePersistentAll(java.util.Map)}, or
     * {@link Query#deletePersistentAll(java.lang.Object[])}
     * depending on the type of argument <code>parameters</code>.
     * If the number of deleted objects does not 
     * match <code>expectedNrOfDeletedObjects</code>,
     * then the test case fails prompting argument <code>assertion</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    protected void deletePersistentAllBySingleStringQuery(String assertion,
            QueryElementHolder queryElementHolder, long expectedNrOfDeletedObjects) {
        if (logger.isDebugEnabled()) {
            logger.debug("Deleting persistent by single string query: " + 
                    queryElementHolder);
        }
        delete(assertion, queryElementHolder, true, expectedNrOfDeletedObjects);
    }

    /**
     * Converts the given query element holder instance to a
     * JDO query based on argument <code>asSingleString</code>.
     * Calls {@link Query#deletePersistentAll()}, or
     * {@link Query#deletePersistentAll(java.util.Map), or
     * {@link Query#deletePersistentAll(java.lang.Object[])
     * depending on the type of argument <code>parameters</code>.
     * If the number of deleted objects does not 
     * match <code>expectedNrOfDeletedObjects</code>,
     * then the test case fails prompting argument <code>assertion</code>.
     * @param assertion the assertion to prompt if the test case fails.
     * @param queryElementHolder the query to execute.
     * @param asSingleString determines if the query is executed as
     * single string query or as API query.
     * @param parameters the parmaters of the query.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    private void delete(String assertion, 
            QueryElementHolder queryElementHolder, boolean asSingleString, long expectedNrOfDeletedObjects) {
        getPM();
        Query query = asSingleString ?
                queryElementHolder.getSingleStringQuery(pm) :
                    queryElementHolder.getAPIQuery(pm);
        delete(assertion, query, queryElementHolder.toString(), 
                queryElementHolder.getParamValues(), expectedNrOfDeletedObjects);
        boolean positive = expectedNrOfDeletedObjects >= 0;
        if (positive) {
            execute(assertion, queryElementHolder, asSingleString,
                    queryElementHolder.isUnique() ? null : new ArrayList());
        }
    }

    /**
     * Calls {@link Query#deletePersistentAll()}, or
     * {@link Query#deletePersistentAll(java.util.Map), or
     * {@link Query#deletePersistentAll(java.lang.Object[])
     * depending on the type of argument <code>parameters</code>.
     * If the number of deleted objects does not 
     * match <code>expectedNrOfDeletedObjects</code>,
     * then the test case fails prompting argument <code>assertion</code>.
     * Argument <code>singleStringQuery</code> is only used as part
     * of the failure message.
     * @param assertion the assertion to prompt if the test case fails.
     * @param query the query to execute.
     * @param singleStringQuery the single string representation of the query.
     * @param parameters the parmaters of the query.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    private void delete(String assertion, Query query, 
            String singleStringQuery, Object parameters, 
            long expectedNrOfDeletedObjects) {
        boolean positive = expectedNrOfDeletedObjects >= 0;
        getPM();
        Transaction tx = pm.currentTransaction();
        tx.begin();
        try {
            try {
                long nr;
                if (parameters == null) {
                    nr = query.deletePersistentAll();
                } else if (parameters instanceof Object[]) {
                    nr = query.deletePersistentAll((Object[])parameters);
                } else if (parameters instanceof Map) {
                    nr = query.deletePersistentAll((Map)parameters);
                } else {
                    throw new IllegalArgumentException("Argument parameters " +
                            "must be instance of Object[], Map, or null.");
                }
                if (logger.isDebugEnabled()) {
                    logger.debug(nr + " objects deleted.");
                }
                
                if (positive) {
                    if (nr != expectedNrOfDeletedObjects) {
                        fail(assertion, "deletePersistentAll returned " + nr +
                                ", expected is " + expectedNrOfDeletedObjects + 
                                ". Query: " + singleStringQuery);
                    }
                } else {
                    fail(assertion, "deletePersistentAll must throw JDOUserException: " + 
                            singleStringQuery);
                }
            } finally {
                query.closeAll();
            }
            tx.commit();
        } catch (JDOUserException e) {
            if (positive) {
                String msg = "JDOUserException thrown while executing query:\n" +
                        singleStringQuery;
                throw new JDOException(msg, e);
            }
        } catch (JDOException e) {
            String msg = "JDOException thrown while executing query:\n" +
                    singleStringQuery;
            throw new JDOException(msg, e);
        } finally {
            if (tx.isActive()) {
                tx.rollback();
            }
        }
    }
}
