/*
   Copyright (c) 2010, 2013, Oracle and/or its affiliates. All rights reserved.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is also distributed with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have included with MySQL.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
*/

package testsuite.clusterj;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import java.util.TreeSet;

import com.mysql.clusterj.Session;

import testsuite.clusterj.model.AllPrimitives;
import testsuite.clusterj.model.Dn2id;
import testsuite.clusterj.model.Employee;
import testsuite.clusterj.model.IdBase;

public abstract class AbstractClusterJModelTest extends AbstractClusterJTest {

    /** The local system default time zone, which is reset by resetLocalSystemDefaultTimeZone */
    protected static TimeZone localSystemTimeZone = TimeZone.getDefault();

    /** ONE_SECOND is the number of milliseconds in one second. */
    protected static final long ONE_SECOND = 1000L;

    /** ONE_MINUTE is the number of milliseconds in one minute. */
    protected static final long ONE_MINUTE = 1000L * 60L;

    /** ONE_HOUR is the number of milliseconds in one hour. */
    protected static final long ONE_HOUR = 1000L * 60L * 60L;

    /** TEN_HOURS is the number of milliseconds in ten hours. */
    protected static final long TEN_HOURS = 1000L * 60L * 60L * 10L;

    /** ONE_DAY is the number of milliseconds in one day. */
    protected static final long ONE_DAY = 1000L * 60L * 60L * 24L;

    /** Convert year, month, day, hour, minute, second into milliseconds after the Epoch, UCT.
     * @param year the year
     * @param month the month (0 for January)
     * @param day the day of the month
     * @param hour the hour of the day
     * @param minute the minute
     * @param second the second
     * @return
     */
    protected static long getMillisFor(int year, int month, int day, int hour, int minute, int second) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month);
        calendar.set(Calendar.DATE, day);
        calendar.set(Calendar.HOUR, hour);
        calendar.set(Calendar.MINUTE, minute);
        calendar.set(Calendar.SECOND, second);
        calendar.set(Calendar.MILLISECOND, 0);
        long result = calendar.getTimeInMillis();
        return result;
    }

    /** Convert year, month, day into milliseconds after the Epoch, UCT.
     * Set hours, minutes, seconds, and milliseconds to zero.
     * @param year the year
     * @param month the month (0 for January)
     * @param day the day of the month
     * @return
     */
    protected static long getMillisFor(int year, int month, int day) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(Calendar.YEAR, year);
        calendar.set(Calendar.MONTH, month);
        calendar.set(Calendar.DATE, day);
        calendar.set(Calendar.HOUR, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        long result = calendar.getTimeInMillis();
        return result;
    }

    /** Convert hours, minutes, and seconds into milliseconds after the Epoch, UCT.
     * Default year, month, and day,
     * as these are assumed by Calendar to be the Epoch.
     * @param day the number of days: ignored
     * @param hour the hour (or number of hours)
     * @param minute the minute (or number of minutes)
     * @param second the second (or number of seconds)
     * @return millis past the Epoch UCT
     */
    protected static long getMillisFor(int days, int hour, int minute, int second) {
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(Calendar.HOUR, hour);
        calendar.set(Calendar.MINUTE, minute);
        calendar.set(Calendar.SECOND, second);
        calendar.set(Calendar.MILLISECOND, 0);
        long result = calendar.getTimeInMillis();
        return result;
    }

    /** A1 values. */
    String[] a1values = new String[]{"dc=abc", "dc=prs", "dc=xyz"};

    protected List<Employee> employees;

    protected List<Dn2id> dn2ids;

    protected static Object[] dn2idPK = setupDn2idPK();

    /** The instances used in the tests, generated by generateInstances */
    protected List<IdBase> instances = new ArrayList<IdBase>();

    /** List of expected results, generated by generateInstances */
    private List<Object[]> expected = null;

    /** The column descriptors as provided by subclasses */
    ColumnDescriptor[] columnDescriptors = null;

    /** The class loader for the domain object type */
    protected ClassLoader loader;

    public AbstractClusterJModelTest() {
        columnDescriptors = getColumnDescriptors();
    }

    protected boolean getCleanupAfterTest() {
        return true;
    }

    /** Set this to false to avoid running test (e.g. if no model class) */
    protected boolean runTest = true;

    @Override
    public void localSetUp() {
        createSessionFactory();
        session = sessionFactory.getSession();
        setAutoCommit(connection, false);
        try {
            session.newInstance(getModelClass());
            runTest = true;
        } catch (Exception e) {
            System.out.println("Ignoring test; no model class " + getModelClass().getName());
            runTest = false;
        }

        if (getModelClass() != null && getCleanupAfterTest()) {
            addTearDownClasses(getModelClass());
        }
    }

    /** Reset the local system default time zone to the time zone used
     * by the MySQL server. This guarantees that there is no time zone
     * offset between the time zone in the client and the time zone
     * in the server.
     * @param connection 
     */
    protected static void resetLocalSystemDefaultTimeZone(Connection connection) {
        try {
            PreparedStatement statement = connection.prepareStatement("select @@global.time_zone, @@global.system_time_zone, @@session.time_zone");
            ResultSet rs = statement.executeQuery();
            // there are two columns in the result
            rs.next();
            String globalTimeZone = rs.getString(1);
            String globalSystemTimeZone = rs.getString(2);
            String sessionTimeZone = rs.getString(3);
//            if (debug) System.out.println("Global time zone: " + globalTimeZone + 
//                    " Global system time zone: " + globalSystemTimeZone +" Session time zone: " + sessionTimeZone);
            connection.commit();
            if ("SYSTEM".equalsIgnoreCase(globalTimeZone)) {
                globalTimeZone = globalSystemTimeZone;
            } else {
                globalTimeZone = "GMT" + globalTimeZone;
            }
            localSystemTimeZone = TimeZone.getTimeZone(globalTimeZone);
//            if (debug) System.out.println("Local system time zone set to: " + globalTimeZone + "(" + localSystemTimeZone + ")");
//            TimeZone.setDefault(localSystemTimeZone);
            // get a new connection after setting local default time zone
            // because a connection contains a session calendar used to create Timestamp instances
            connection.close();
        } catch (SQLException e) {
            throw new RuntimeException("setServerTimeZone failed", e);
        }
    }

    protected void setAutoCommit(Connection connection, boolean b) {
        try {
            connection.setAutoCommit(false);
        } catch (SQLException e) {
            throw new RuntimeException("setAutoCommit failed", e);
        }
    }

    protected void createEmployeeInstances(int count) {
        employees = new ArrayList<Employee>(count);
        for (int i = 0; i < count; ++i) {
            Employee emp = session.newInstance(Employee.class);
            emp.setId(i);
            emp.setName("Employee number " + i);
            emp.setAge(i);
            emp.setMagic(i);
            employees.add(emp);
        }
    }

    protected void consistencyCheck(Employee emp) {
        int id = emp.getId();
        String expectedName = "Employee number " + id;
        String actualName = emp.getName();
        if (!expectedName.equals(actualName)) {
//            System.out.println("expected " + dump(expectedName));
//            System.out.println("actual " + dump(actualName));
            error("Employee " + id
                    + " name mismatch; expected length: " + expectedName.length() + "'" + expectedName
                    + "'; actual length: " + actualName.length() + "'" + actualName + "'");
        }
        int actualAge = emp.getAge();
        if (!(actualAge == id)) {
            error("Employee " + id
                    + " age mismatch; expected " + id
                    + "'; actual '" + actualAge);
        }
        int actualMagic = emp.getMagic();
        if (!(actualMagic == id)) {
            error("Employee " + id
                    + " magic mismatch; expected " + id
                    + "'; actual '" + actualMagic);
        }
    }

    protected <T> void consistencyCheck(Iterable<T> instances) {
        for (T instance: instances) {
            if (instance instanceof Employee) {
                consistencyCheck((Employee)instance);
            } else if (instance instanceof Dn2id) {
                consistencyCheck((Dn2id)instance);
            }
        }
    }

    protected void createDn2idInstances(int number) {
        dn2ids = new ArrayList<Dn2id>();
        for (int i = 0; i < number; ++i) {
            Dn2id d = session.newInstance(Dn2id.class);
            d.setEid(i);
            d.setObjectClasses("testObject");
            // XObjectClasses has a NullValue=DEFAULT so don't need to set it
            d.setA0("dc=com");
            // a1 should pick all of the a1values equally
            d.setA1(getA1for(number, i));
            d.setA2("ou=people");
            d.setA3(getA3for(i));
            d.setA4("");
            d.setA5("");
            d.setA6("");
            d.setA7("");
            d.setA8("");
            d.setA9("");
            d.setA10("");
            d.setA11("");
            d.setA12("");
            d.setA13("");
            d.setA14("");
            d.setA15("");
            dn2ids.add(d);
        }
    }

    protected void consistencyCheck(Dn2id dn2id) {
        long eid = dn2id.getEid();
        String expected = getA3for(eid);
        String actual = dn2id.getA3();
        if (!expected.equals(actual)) {
            error("Dn2id " + eid
                    + " a3 mismatch; expected '" + expected
                    + "'; actual '" + actual + "'");
        }
    }

    /** Subclasses usually should not override this method to provide the list of expected results */
    protected List<Object[]> getExpected() {
        return expected;
    }

    /** Subclasses must override this method to provide the name of the table for the test */
    protected String getTableName() {
        return null;
    }

    /** Subclasses must override this method to provide the number of instances to create */
    protected int getNumberOfInstances() {
        return 0;
    }

    /** Subclasses must override this method to provide the column descriptors for the test */
    protected ColumnDescriptor[] getColumnDescriptors() {
        return null;
    }

    /** Subclasses must override this method to provide the model class for the test */
    Class<? extends IdBase> getModelClass() {
        return null;
    }

    /** Subclasses must override this method to provide values for rows (i) and columns (j) */
    protected Object getColumnValue(int i, int j) {
        return null;
    }

    /** Subclasses may override this method to convert an int into a key value */
    protected Object convertToKey(int i) {
        return Integer.valueOf(i);
    }

    /** Write data via JDBC and read back the data via NDB */
    protected void writeJDBCreadNDB() {
        if (!runTest) return;
        generateInstances(getColumnDescriptors());
        removeAll(getModelClass());
        List<Object[]> result = null;
        writeToJDBC(columnDescriptors, instances);
        result = readFromNDB(columnDescriptors);
        verify("writeJDBCreadNDB", getExpected(), result);
    }

    /** Write data via JDBC and read back the data via JDBC */
    protected void writeJDBCreadJDBC() {
        if (!runTest) return;
        generateInstances(getColumnDescriptors());
        removeAll(getModelClass());
        List<Object[]> result = null;
        writeToJDBC(columnDescriptors, instances);
        result = readFromJDBC(columnDescriptors);
        verify("writeJDBCreadJDBC", getExpected(), result);
    }

    /** Write data via NDB and read back the data via NDB */
    protected void writeNDBreadNDB() {
        if (!runTest) return;
        generateInstances(getColumnDescriptors());
        removeAll(getModelClass());
        List<Object[]> result = null;
        writeToNDB(columnDescriptors, instances);
        result = readFromNDB(columnDescriptors);
        verify("writeNDBreadNDB", getExpected(), result);
    }

    /** Write data via NDB and read back the data via JDBC */
    protected void writeNDBreadJDBC() {
        if (!runTest) return;
        generateInstances(getColumnDescriptors());
        removeAll(getModelClass());
        List<Object[]> result = null;
        writeToNDB(columnDescriptors, instances);
        result = readFromJDBC(columnDescriptors);
        verify("writeNDBreadJDBC", getExpected(), result);
    }

    /** Dump the contents of the expected or actual results of the operation */
    private String dumpListOfObjectArray(List<Object[]> results) {
        StringBuffer result = new StringBuffer(results.size() + " rows\n");
        for (Object[] row: results) {
            result.append("Id: ");
            for (Object column: row) {
                result.append(column);
                result.append(' ');
            }
            result.append('\n');
        }
        return result.toString();
    }

    protected void queryAndVerifyResults(String where, ColumnDescriptor[] columnDescriptors,
            String conditions, Object[] parameters, int... objectIds) {
        List<Object[]> results = queryJDBC(columnDescriptors, conditions, parameters);
        verifyQueryResults(where, results, objectIds);
    }

    /** Read data via JDBC */
    protected List<Object[]> queryJDBC(ColumnDescriptor[] columnDescriptors,
            String conditions, Object[] parameters) {
        getConnection();
        String tableName = getTableName();
        List<Object[]> result = new ArrayList<Object[]>();
        StringBuffer buffer = new StringBuffer("SELECT id");
        for (ColumnDescriptor columnDescriptor: columnDescriptors) {
            buffer.append(", ");
            buffer.append(columnDescriptor.getColumnName());
        }
        buffer.append(" FROM ");
        buffer.append(tableName);
        buffer.append(" WHERE ");
        buffer.append(conditions);
        String statement = buffer.toString();
        if (debug) System.out.println(statement);
        PreparedStatement preparedStatement = null;
        try {
            int p = 1;
            preparedStatement = connection.prepareStatement(statement);
            for (Object parameter: parameters) {
                preparedStatement.setObject(p++, parameter);
            }
            ResultSet rs = preparedStatement.executeQuery();
            while (rs.next()) {
                Object[] row = new Object[columnDescriptors.length + 1];
                int j = 1;
                row[0] = rs.getInt(1);
                for (ColumnDescriptor columnDescriptor: columnDescriptors) {
                    row[j] = columnDescriptor.getResultSetValue(rs, j + 1);
                    ++j;
                }
                result.add(row);
            }
            if (!connection.getAutoCommit()) {
                connection.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to read " + tableName, e);
        }
        if (debug) System.out.println("readFromJDBC: " + dumpObjectArray(result));
        return result;
    }

    /** Dump the contents of the expected or actual results of the operation */
    private String dumpObjectArray(List<Object[]> results) {
        StringBuffer result = new StringBuffer(results.size() + " rows\n");
        for (Object[] row: results) {
            result.append("Id: ");
            for (Object column: row) {
                result.append(column);
                result.append(' ');
            }
            result.append('\n');
        }
        return result.toString();
    }

    protected void verifyQueryResults(String where, List<Object[]> results, int... objectIds) {
        errorIfNotEqual(where + " mismatch in number of results.", objectIds.length, results.size());
        for (Object[] result: results) {
            int id = (Integer)result[0];
            if (Arrays.binarySearch(objectIds, id) < 0) {
                // couldn't find it
                error(where + " result " + id + " not expected.");
            }
        }
    }

    /** Verify that the actual results match the expected results. If not, use the multiple error
     * reporting method errorIfNotEqual defined in the superclass.
     * @param where the location of the verification of results, normally the name of the test method
     * @param expecteds the expected results
     * @param actuals the actual results
     */
    protected void verify(String where, List<Object[]> expecteds, List<Object[]> actuals) {
        if (expecteds.size() != actuals.size()) {
            error(where + " failure on size of results: expected: " + expecteds.size() + " actual: " + actuals.size());
            return;
        }
        for (int i = 0; i < expecteds.size(); ++i) {
            Object[] expected = expecteds.get(i);
            Object[] actual = actuals.get(i);
            errorIfNotEqual(where + " got failure on id for row " + i, i, actual[0]);
            for (int j = 1; j < expected.length; ++j) {
                errorIfNotEqual(where + " got failure to match column data for row "
                        + i + " column " + j,
                        expected[j], actual[j]);
            }
        }
    }

    /** Generated instances to persist. When using JDBC, the data is obtained from the instance
     * via the column descriptors. As a side effect (!) create the list of expected results from read.
     * @param columnDescriptors the column descriptors
     * @return the generated instances
     */
    protected void generateInstances(ColumnDescriptor[] columnDescriptors) {
        Class<? extends IdBase> modelClass = getModelClass();
        expected = new ArrayList<Object[]>();
        instances = new ArrayList<IdBase>();
        IdBase instance = null;
        int numberOfInstances = getNumberOfInstances();
        for (int i = 0; i < numberOfInstances; ++i) {
            // create the instance
            instance = getNewInstance(modelClass);
            instance.setId(i);
            // create the expected result row
            int j = 0;
            for (ColumnDescriptor columnDescriptor: columnDescriptors) {
                Object value = getColumnValue(i, j);
                if (debug) System.out.println("generateInstances set field " + columnDescriptor.getColumnName()
                        + " to value "  + value);
                // set the column value in the instance
                columnDescriptor.setFieldValue(instance, value);
                // check that the value was set correctly
                Object actual = columnDescriptor.getFieldValue(instance);
                errorIfNotEqual("generateInstances value mismatch for " + columnDescriptor.getColumnName(),
                        dump(value), dump(actual));
                ++j;
            }
            instances.add(instance);
            // set the column values in the expected result
            Object[] expectedRow = createRow(columnDescriptors, instance);
            expected.add(expectedRow);
        }
        if (debug) System.out.println("Created " + instances.size() + " instances of " + modelClass.getName());
    }

    /** Create a new instance of the parameter interface
     * @param modelClass the interface to instantiate
     * @return an instance of the class
     */
    protected IdBase getNewInstance(Class<? extends IdBase> modelClass) {
        IdBase instance;
        instance = session.newInstance(modelClass);
        return instance;
    }

    /** Write data to JDBC. */
    protected void writeToJDBC(ColumnDescriptor[] columnDescriptors, List<IdBase> instances) {
        String tableName = getTableName();
        StringBuffer buffer = new StringBuffer("INSERT INTO ");
        buffer.append(tableName);
        buffer.append(" (id");
        for (ColumnDescriptor columnDescriptor: columnDescriptors) {
            buffer.append(", ");
            buffer.append(columnDescriptor.getColumnName());
        }
        buffer.append(") VALUES (?");
        for (ColumnDescriptor columnDescriptor: columnDescriptors) {
            buffer.append(", ?");
        }
        buffer.append(")");
        String statement = buffer.toString();
        if (debug) System.out.println(statement);
    
        PreparedStatement preparedStatement = null;
        int i = 0;
        try {
            preparedStatement = connection.prepareStatement(statement);
            if (debug) System.out.println(preparedStatement.toString());
            for (i = 0; i < instances.size(); ++i) {
                IdBase instance = instances.get(i);
                preparedStatement.setInt(1, instance.getId());
                int j = 2;
                for (ColumnDescriptor columnDescriptor: columnDescriptors) {
                    Object value = columnDescriptor.getFieldValue(instance);
                    columnDescriptor.setPreparedStatementValue(preparedStatement, j++, value);
                    if (debug) System.out.println("writeToJDBC set column: " + columnDescriptor.getColumnName() + " to value: " + value);
                }
                preparedStatement.execute();
            }
            if (!connection.getAutoCommit()) {
                connection.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to insert " + tableName + " at instance " + i, e);
        }
    }

    /** Write data via NDB */
    protected void writeToNDB(ColumnDescriptor[] columnDescriptors, List<IdBase> instances) {
        session.currentTransaction().begin();
        session.makePersistentAll(instances);
        session.currentTransaction().commit();
    }

    /** Read data via NDB */
    protected List<Object[]> readFromNDB(ColumnDescriptor[] columnDescriptors) {
        Class<? extends IdBase> modelClass = getModelClass();
        List<Object[]> result = new ArrayList<Object[]>();
        session.currentTransaction().begin();
        for (int i = 0; i < getNumberOfInstances() ; ++i) {
            IdBase instance = session.find(modelClass, convertToKey(i));
            if (instance != null) {
                Object[] row = createRow(columnDescriptors, instance);
                result.add(row);
            }
        }
        session.currentTransaction().commit();
        if (debug) System.out.println("readFromNDB: " + dumpListOfObjectArray(result));
        return result;
    }

    /** Create row data from an instance.
     * @param columnDescriptors the column descriptors describing the data
     * @param instance the instance to extract data from
     * @return the row data representing the instance
     */
    private Object[] createRow(ColumnDescriptor[] columnDescriptors,
            IdBase instance) {
        Object[] row = new Object[columnDescriptors.length + 1];
        row[0] = instance.getId();
        int j = 1;
        for (ColumnDescriptor columnDescriptor: columnDescriptors) {
            row[j++] = columnDescriptor.getFieldValue(instance);
        }
        return row;
    }

    /** Read data via JDBC ordered by id */
    protected List<Object[]> readFromJDBC(ColumnDescriptor[] columnDescriptors) {
        String tableName = getTableName();
        List<Object[]> result = new ArrayList<Object[]>();
        Set<Object[]> rows = new TreeSet<Object[]>(new Comparator<Object[]>(){
            public int compare(Object[] me, Object[] other) {
                return ((Integer)me[0]) - ((Integer)other[0]);
            }
        });
        StringBuffer buffer = new StringBuffer("SELECT id");
        for (ColumnDescriptor columnDescriptor: columnDescriptors) {
            buffer.append(", ");
            buffer.append(columnDescriptor.getColumnName());
        }
        buffer.append(" FROM ");
        buffer.append(tableName);
        String statement = buffer.toString();
        if (debug) System.out.println(statement);
        PreparedStatement preparedStatement = null;
        int i = 0;
        try {
            preparedStatement = connection.prepareStatement(statement);
            ResultSet rs = preparedStatement.executeQuery();
            while (rs.next()) {
                Object[] row = new Object[columnDescriptors.length + 1];
                int j = 1;
                row[0] = rs.getInt(1);
                for (ColumnDescriptor columnDescriptor: columnDescriptors) {
                    row[j] = columnDescriptor.getResultSetValue(rs, j + 1);
                    ++j;
                }
                ++i;
                rows.add(row);
            }
            if (!connection.getAutoCommit()) {
                connection.commit();
            }
        } catch (SQLException e) {
            throw new RuntimeException("Failed to read " + tableName + " at instance " + i, e);
        }
        result = new ArrayList<Object[]>(rows);
        if (debug) System.out.println("readFromJDBC: " + dumpListOfObjectArray(result));
        return result;
    }

    @SuppressWarnings("unchecked") // cast proxy to T
    protected <T> T proxyFor (final Class<T> cls) {
        InvocationHandler handler = new InvocationHandler() {
            private Map<String, Object> values = new HashMap<String, Object>();
            public Object invoke(Object instance, Method method, Object[] args)
                    throws Throwable {
                String methodName = method.getName();
                String propertyName = methodName.substring(3);
                String methodPrefix = methodName.substring(0, 3);
                if ("get".equals(methodPrefix)) {
                    return values.get(propertyName);
                } else if ("set".equals(methodPrefix)) {
                    values.put(propertyName, args[0]);
                    return null;
                }
                // error
                throw new RuntimeException("Not a get/set method: " + methodName);
            }
            
        };
        Object proxy = Proxy.newProxyInstance(loader, new Class[] {cls}, handler);
        return (T)proxy;
    }

    /** This class describes columns and fields for a table and model class. 
     * A subclass will instantiate instances of this class and provide handlers to
     * read and write fields and columns via methods defined in the instance handler.
     */
    protected static class ColumnDescriptor {

        private String columnName;
    
        protected InstanceHandler instanceHandler;
    
        public String getColumnName() {
            return columnName;
        }
    
        public Object getResultSetValue(ResultSet rs, int j) throws SQLException {
            return instanceHandler.getResultSetValue(rs, j);
        }
    
        public Object getFieldValue(IdBase instance) {
            return instanceHandler.getFieldValue(instance);
        }
    
        public void setFieldValue(IdBase instance, Object value) {
            this.instanceHandler.setFieldValue(instance, value);
        }
    
        public void setPreparedStatementValue(PreparedStatement preparedStatement, int j, Object value)
                throws SQLException {
            instanceHandler.setPreparedStatementValue(preparedStatement, j, value);
        }
    
        public ColumnDescriptor(String name, InstanceHandler instanceHandler) {
            this.columnName = name;
            this.instanceHandler = instanceHandler;
        }
    }

    protected interface InstanceHandler {
        void setFieldValue(IdBase instance, Object value);
        Object getResultSetValue(ResultSet rs, int j)
                throws SQLException;
        Object getFieldValue(IdBase instance);
        public void setPreparedStatementValue(PreparedStatement preparedStatement, int j, Object value)
                throws SQLException;
    }

    protected String getA1for(int number, int index) {
        int a1factor = 1 + number/a1values.length;
        return a1values[index/a1factor];
    }

    protected String getA3for(long i) {
        return "employeenumber=100000" + i;
    }

    protected void createAllPrimitivesInstances(int number) {
        createAllPrimitivesInstances(session, number);
    }

    protected void createAllPrimitivesInstances(Session session, int number) {
        for (int i = 0; i < number; ++i) {
            AllPrimitives instance = createAllPrimitiveInstance(session, i);
            instances.add(instance);
        }
    }

    protected AllPrimitives createAllPrimitiveInstance(Session session, int i) {
        AllPrimitives instance = session.newInstance(AllPrimitives.class, i);
        initialize(instance, i);
        return instance;
    }

    protected void initialize(AllPrimitives instance, int i) {
        instance.setInt_not_null_hash(i);
        instance.setInt_not_null_btree(i);
        instance.setInt_not_null_both(i);
        instance.setInt_not_null_none(i);
        instance.setInt_null_hash(i);
        instance.setInt_null_btree(i);
        instance.setInt_null_both(i);
        instance.setInt_null_none(i);

        instance.setLong_not_null_hash((long)i);
        instance.setLong_not_null_btree((long)i);
        instance.setLong_not_null_both((long)i);
        instance.setLong_not_null_none((long)i);
        instance.setLong_null_hash((long)i);
        instance.setLong_null_btree((long)i);
        instance.setLong_null_both((long)i);
        instance.setLong_null_none((long)i);

        instance.setByte_not_null_hash((byte)i);
        instance.setByte_not_null_btree((byte)i);
        instance.setByte_not_null_both((byte)i);
        instance.setByte_not_null_none((byte)i);
        instance.setByte_null_hash((byte)i);
        instance.setByte_null_btree((byte)i);
        instance.setByte_null_both((byte)i);
        instance.setByte_null_none((byte)i);

        instance.setShort_not_null_hash((short)i);
        instance.setShort_not_null_btree((short)i);
        instance.setShort_not_null_both((short)i);
        instance.setShort_not_null_none((short)i);
        instance.setShort_null_hash((short)i);
        instance.setShort_null_btree((short)i);
        instance.setShort_null_both((short)i);
        instance.setShort_null_none((short)i);
    }

    protected static Object[] setupDn2idPK() {
        Object[] result = new Object[16];
        result[0] = "dc=com";
        // pk[1] changes and is set inside loop
        result[1] = "dc=example";
        result[2] = "ou=people";
        // pk[3] changes and is set inside loop
        result[4] = "";
        result[5] = "";
        result[6] = "";
        result[7] = "";
        result[8] = "";
        result[9] = "";
        result[10] = "";
        result[11] = "";
        result[12] = "";
        result[13] = "";
        result[14] = "";
        result[15] = "";
        return result;
    }

}
