/*
 * 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.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
    
    /** */
    public void loadAndPersistPCPoints(PersistenceManager pm) {
        insertPCPoints(pm, 5);
    }

    /** */
    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();
        }
    }
       
    /** */
    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.
     */
    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.
     */
    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)

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

    /** */
    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, new Boolean(bFlag), (byte)i, new Byte((byte)i),
                    (short)i, new Short((short)i), (int) i, new Integer(i),
                    (long)i, new Long(i), (float)i, new Float(i), 
                    (double)i, new Double(i), charValue, new Character(charValue),
                    Calendar.getInstance().getTime(), strValue,
                    new BigDecimal(String.valueOf(i)), 
                    new BigInteger(String.valueOf(i)),
                    new Long(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.
     */
    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.
     */
    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.
     */
    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.
     */
    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);
    }

    /** */
    protected void checkQueryResultWithOrder(String assertion, 
                                             String query,
                                             Object result, 
                                             Object expected) {
        if (!equals(result, expected)) {
            queryFailed(assertion, query, result, 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.lang.Object)}.
     * 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.
     */
    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
     
    /** */
    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
     * @param query
     * @param queryText
     * @param positive
     */
    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) {
        executeAPIQuery(assertion, queryElementHolder, null, expectedResult);
    }

    /**
     * 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 parameters the parmaters of the query.
     * @param expectedResult the expected query result.
     */
    protected void executeAPIQuery(String assertion,
            QueryElementHolder queryElementHolder, 
            Object[] parameters, Object expectedResult) {
        if (logger.isDebugEnabled()) {
            logger.debug("Executing API query: " + queryElementHolder);
        }
        execute(assertion, queryElementHolder, false, 
                parameters, 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) {
        executeSingleStringQuery(assertion, queryElementHolder, 
                null, 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 parameters the parmaters of the query.
     * @param expectedResult the expected query result.
     */
    protected void executeSingleStringQuery(String assertion,
            QueryElementHolder queryElementHolder, 
            Object[] parameters, Object expectedResult) {
        if (logger.isDebugEnabled())
            logger.debug("Executing single string query: " + 
                    queryElementHolder);
        execute(assertion, queryElementHolder, true, 
                parameters, expectedResult);
    }

    /**
     * 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 parameters the parmaters of the query.
     * @param expectedResult the expected query result.
     * @return the query result
     */
    private Object execute(String assertion, 
            QueryElementHolder queryElementHolder, boolean asSingleString,
            Object parameters, Object expectedResult) {
        getPM();
        Query query = asSingleString ?
                queryElementHolder.getSingleStringQuery(pm) :
                    queryElementHolder.getAPIQuery(pm);
        Object result = execute(assertion, query, 
                queryElementHolder.toString(), 
                queryElementHolder.hasOrdering(), parameters, 
                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 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 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 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));
                }
    
                if (positive) {
                    if (hasOrdering) {
                        checkQueryResultWithOrder(assertion, singleStringQuery, result, 
                                expectedResult);
                    } else {
                        checkQueryResultWithoutOrder(assertion, singleStringQuery, result, 
                                expectedResult);
                    }
                } else {
                    fail(assertion, "Query must throw JDOUserException: " + 
                            singleStringQuery);
                }
            } 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;
    }

    /**
     * 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 parameters the parmaters of the query.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    protected void deletePersistentAllByAPIQuery(String assertion,
            QueryElementHolder queryElementHolder, 
            Object parameters, long expectedNrOfDeletedObjects) {
        if (logger.isDebugEnabled()) {
            logger.debug("Deleting persistent by API query: " + 
                    queryElementHolder);
        }
        delete(assertion, queryElementHolder, false, 
                parameters, 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 parameters the parmaters of the query.
     * @param expectedNrOfDeletedObjects the expected number of deleted objects.
     */
    protected void deletePersistentAllBySingleStringQuery(String assertion,
            QueryElementHolder queryElementHolder, 
            Object parameters, long expectedNrOfDeletedObjects) {
        if (logger.isDebugEnabled()) {
            logger.debug("Deleting persistent by single string query: " + 
                    queryElementHolder);
        }
        delete(assertion, queryElementHolder, true, 
                parameters, 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,
            Object parameters, long expectedNrOfDeletedObjects) {
        getPM();
        Query query = asSingleString ?
                queryElementHolder.getSingleStringQuery(pm) :
                    queryElementHolder.getAPIQuery(pm);
        delete(assertion, query, queryElementHolder.toString(), 
                parameters, expectedNrOfDeletedObjects);
        boolean positive = expectedNrOfDeletedObjects >= 0;
        if (positive) {
            execute(assertion, queryElementHolder, asSingleString, parameters, 
                    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();
            }
        }
    }
}
