com.quinsoft.zeidon.dbhandler.JdbcHandler.java Source code

Java tutorial

Introduction

Here is the source code for com.quinsoft.zeidon.dbhandler.JdbcHandler.java

Source

/**
This file is part of the Zeidon Java Object Engine (Zeidon JOE).
    
Zeidon JOE is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
    
Zeidon JOE 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 Lesser General Public License for more details.
    
You should have received a copy of the GNU Lesser General Public License
along with Zeidon JOE.  If not, see <http://www.gnu.org/licenses/>.
    
Copyright 2009-2015 QuinSoft
 */
package com.quinsoft.zeidon.dbhandler;

import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.dbcp.BasicDataSource;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.format.DateTimeFormatter;

import com.google.common.collect.MapMaker;
import com.quinsoft.zeidon.AbstractOptionsConfiguration;
import com.quinsoft.zeidon.ActivateFlags;
import com.quinsoft.zeidon.Application;
import com.quinsoft.zeidon.CursorPosition;
import com.quinsoft.zeidon.EntityCursor;
import com.quinsoft.zeidon.EntityInstance;
import com.quinsoft.zeidon.ObjectEngine;
import com.quinsoft.zeidon.Pagination;
import com.quinsoft.zeidon.Task;
import com.quinsoft.zeidon.View;
import com.quinsoft.zeidon.ZeidonException;
import com.quinsoft.zeidon.domains.Domain;
import com.quinsoft.zeidon.objectdefinition.AttributeDef;
import com.quinsoft.zeidon.objectdefinition.DataField;
import com.quinsoft.zeidon.objectdefinition.DataRecord;
import com.quinsoft.zeidon.objectdefinition.EntityDef;
import com.quinsoft.zeidon.objectdefinition.RelRecord;
import com.quinsoft.zeidon.utils.IntegerLinkedHashMap;
import com.quinsoft.zeidon.utils.JoeUtils;
import com.quinsoft.zeidon.utils.KeyStringBuilder;

/**
 * A DB handler for JDBC.
 *
 * @author DG
 *
 */
public class JdbcHandler extends AbstractSqlHandler {
    private final Map<String, PreparedStatementCacheValue> cachedStatements;
    private JdbcConnectionPool connectionPool;

    /**
     * The number of PreparedStatements loaded from the cache.
     */
    private final int cachedStatementCount = 0;

    protected JdbcTransaction transaction;

    /**
     * For inserts, this is the list of keys generated by the DB.
     */
    private ArrayList<Object> generatedKeys;

    private final String configGroupName;
    private JdbcDomainTranslator translator;
    private DateTimeFormatter dateFormat;
    private DateTimeFormatter dateTimeFormat;
    private String drivers;

    /**
     * @param task
     * @param application
     * @param view
     * @param config
     */
    public JdbcHandler(Task task, AbstractOptionsConfiguration options) {
        super(task, options);
        configGroupName = options.getConfigValue("_JDBC", "JdbcConfigGroupName");
        task.log().debug("JDBC config group = %s", configGroupName);

        if (isBindAllValues())
            cachedStatements = new HashMap<String, PreparedStatementCacheValue>();
        else
            cachedStatements = null; // Indicate that we aren't caching prepared statements.
    }

    @Override
    protected String getConfigValue(String key) {
        return options.getConfigValue(configGroupName, key);
    }

    protected void initializeTransaction() {
        // Nothing needs to be done for default JDBC implementations.
    }

    protected JdbcTransaction getTransaction(Application application) {
        if (transaction != null)
            return transaction;

        // Synchronize on the task.  This way we can handle multiple asynchronous activates.
        synchronized (getTask()) {
            // Are we sharing a single transaction for activates?
            if (options.isSingleTransaction()) {
                // Yes.  See if there's already a transaction open for this task.
                transaction = getTask().getCacheMap().get(JdbcTransaction.class);
                if (transaction != null) {
                    if (transaction.isClosed())
                        throw new ZeidonException("Shared transaction has already been closed");

                    task.dblog().debug("JDBC: using shared connection to %s", options.getOiSourceUrl());
                    return transaction;
                }
            }

            String url = options.getOiSourceUrl();
            Connection conn = getConnectionPool().getConnection(url, task, this, application);
            task.dblog().debug("JDBC: got a connection to %s", options.getOiSourceUrl());

            if (conn == null)
                throw new ZeidonException("Error creating transaction to %s", url);

            transaction = new JdbcTransaction(conn);
            initializeTransaction();
            if (options.isSingleTransaction()) {
                getTask().getCacheMap().put(JdbcTransaction.class, transaction);
                getTask().addTaskCleanupWork(transaction);
            }

            return transaction;
        }
    }

    /**
     * This doesn't need to be synchronized because the use of cacheMap does it for us.
     *
     * @return
     */
    private JdbcConnectionPool getConnectionPool() {
        if (connectionPool == null) {
            Task systemTask = getTask().getSystemTask();
            connectionPool = systemTask.getCacheMap().getOrCreate(JdbcConnectionPool.class);
        }

        return connectionPool;
    }

    @Override
    protected void getSqlValue(SqlStatement stmt, Domain domain, AttributeDef attributeDef, StringBuilder buffer,
            Object value) {
        try {
            if (getTranslator().appendSqlValue(stmt, buffer, domain, attributeDef, value))
                return;

            throw new ZeidonException("JdbcDomainTranslator did not correctly translate an attribute value");
        } catch (Exception e) {
            throw ZeidonException.wrapException(e).prependAttributeDef(attributeDef).appendMessage("Value = %s",
                    value);
        }
    }

    @Override
    public boolean beginTransaction(View view) {
        transaction = getTransaction(application);

        // If we're doing the activating using a shared transaction then we need
        // to add a cleanup task to the drop task to close the transaction.
        if (activateOptions != null && activateOptions.isSingleTransaction() && view != null)
            view.addViewCleanupWork(transaction);

        return true;
    }

    /* (non-Javadoc)
     * @see com.quinsoft.zeidon.dbhandler.DbHandler#endTransaction(java.lang.Object, com.quinsoft.zeidon.Task, com.quinsoft.zeidon.View, boolean)
     */
    @Override
    public void endTransaction(boolean commit) {
        try {
            // Close any statements that were cached.
            if (cachedStatements != null) {
                for (PreparedStatementCacheValue v : cachedStatements.values())
                    DbUtils.closeQuietly(v.ps);
                task.dblog().trace("Loaded %d statements from cache\nTotal cache size = %d", cachedStatementCount,
                        cachedStatements.size());
                cachedStatements.clear();
            }

            if (closeTransaction) {
                if (commit)
                    transaction.getConnection().commit();
                else
                    transaction.getConnection().rollback();

                transaction.close();
                task.dblog().debug("JDBC: closed transaction");
                transaction = null;
            }
        } catch (Throwable e) {
            throw ZeidonException.prependMessage(e, "JDBC = %s", options.getOiSourceUrl());
        }
    }

    /**
     * Returns the concatenated key values from the current row in the ResultSet.
     * This retrieves *ALL* keys, including parent keys.
     *
     * @param entityDef
     * @param rs
     * @return
     */
    private String getFullKeyValuesForCurrentRow(EntityDef entityDef, ResultSet rs, SqlStatement stmt,
            Map<Integer, Object> loadedObjects) {
        List<String> values = new ArrayList<String>();

        // Add the entity name so we can keep track of the keys for multiple entities.
        values.add(entityDef.getName());

        // Get the values for the keys.  If we're doing a join then we may need to add parent
        // keys as well.  This is necessary if a child entity instance is the child of two
        // different parents.
        for (EntityDef ve = entityDef; ve != null; ve = ve.getParent()) {
            DataRecord dataRecord = ve.getDataRecord();
            assert dataRecord != null;

            // If ve is not in the dataRecords map then it's not part of this select statement.
            if (!stmt.dataRecords.containsKey(dataRecord)) {
                // The first entityDef should *always* be in the dataRecords so if it's not
                // throw an assertion error.
                assert ve != entityDef : "entityDef is not in dataRecords map";
                break;
            }

            List<AttributeDef> keys = ve.getKeys();
            for (AttributeDef key : keys) {
                DataField dataField = dataRecord.getDataField(key);
                Integer columnIdx = stmt.getColumns().get(dataField);
                assert columnIdx != null;
                Object value = getSqlObject(rs, columnIdx, dataField, loadedObjects);
                String str = value.toString();
                values.add(str);
            }
        }

        return StringUtils.join(values, "|");
    }

    /**
     * Returns the concatenated key values from the current entity in the ResultSet.
     *
     * Internal note: We want this to produce the same output as EntityInstance.getKeyString()
     *
     * @param entityDef
     * @param rs
     * @return
     */
    private String getKeyValuesForCurrentEntity(EntityDef entityDef, ResultSet rs, SqlStatement stmt,
            Map<Integer, Object> loadedObjects) {
        KeyStringBuilder builder = new KeyStringBuilder();

        DataRecord dataRecord = entityDef.getDataRecord();
        assert dataRecord != null;

        List<AttributeDef> keys = entityDef.getKeys();
        for (AttributeDef key : keys) {
            DataField dataField = dataRecord.getDataField(key);
            Integer columnIdx = stmt.getColumns().get(dataField);
            assert columnIdx != null;
            Object value = getSqlObject(rs, columnIdx, dataField, loadedObjects);
            String str = value.toString();
            builder.appendKey(str);
        }

        return builder.toString();
    }

    @Override
    protected void addActivateLimit(int limit, SqlStatement stmt) {
        stmt.activateLimit = limit;
    }

    @Override
    protected void addPageOffset(Pagination pagingOptions, SqlStatement stmt) {
        assert stmt.activateLimit > 1;

        int offset = pagingOptions.getPageSize() * (pagingOptions.getPageNumber() - 1);
        String str = String.format(" LIMIT %d OFFSET %d", stmt.activateLimit, offset);
        stmt.appendSuffix(str);
    }

    @Override
    protected int executeLoad(View view, EntityDef entityDef, SqlStatement stmt) {
        int rc = 0;
        String sql = stmt.getAssembledCommand();
        logSql(stmt);
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            Map<String, EntityInstance> loadedEntities = new HashMap<String, EntityInstance>();
            IntegerLinkedHashMap<EntityDef> entityCounts = new IntegerLinkedHashMap<EntityDef>();
            ps = prepareAndBind(stmt, sql, view, entityDef, stmt.commandType);
            rs = ps.executeQuery();

            while (rs.next()) {
                loadAttributes(stmt, rs, view, loadedEntities, entityCounts);

                // Check to see if we've loaded 2 root entities.  If so, then see if we're only
                // activating a single root.  We have to check after we've called loadAttributes
                // because the root entity could be repeated in the result set if it is joined
                // with children.
                if (entityCounts.get(entityDef) == 2) {
                    rc = 1; // Set return code to indicate we found multiple roots.

                    // If we're loading the root and we're only supposed to load a single root
                    // then stop loading.
                    if (entityDef.getParent() == null && activateFlags.contains(ActivateFlags.fSINGLE)) {
                        // We've loaded 2 root entities.  Delete the second (current) one.
                        view.cursor(entityDef).dropEntity();
                        break;
                    }
                }
            }

            if (task.dblog().isDebugEnabled()) {
                for (EntityDef ve : entityCounts.keySet()) {
                    EntityDef parentEntity = ve.getParent();
                    if (parentEntity != null) {
                        EntityCursor parentCursor = view.cursor(parentEntity);
                        task.dblog().debug("Activated %d %s entities for %s", entityCounts.get(ve), ve,
                                parentCursor);
                    } else
                        task.dblog().debug("Activated %d %s entities", entityCounts.get(ve), ve);
                }
            }
        } catch (Exception e) {
            throw ZeidonException.prependMessage(e, "SQL => %s\nDB: %s", sql, options.getOiSourceUrl())
                    .prependEntityDef(entityDef).prependDataRecord(entityDef.getDataRecord());
        } finally {
            close(rs, ps);
        }

        return rc;
    }

    private Object getSqlObject(ResultSet rs, Integer idx, DataField dataField,
            Map<Integer, Object> loadedObjects) {
        try {
            Object o = loadedObjects.get(idx);
            if (o == null) {
                o = rs.getObject(idx);
                loadedObjects.put(idx, o);
            }

            return o;
        } catch (SQLException e) {
            throw ZeidonException.wrapException(e).appendMessage("DataField: %s, column idx: %d", dataField, idx);
        }
    }

    /**
     * Sets the attribute using the value retrieved from the DB.
     *
     * @param entityInstance
     * @param attributeDef
     * @param value
     * @throws SQLException
     */
    protected void setAttribute(EntityInstance entityInstance, AttributeDef attributeDef, Object value)
            throws SQLException {
        Object convertedValue = getTranslator().convertDbValue(attributeDef.getDomain(), value);
        entityInstance.getAttribute(attributeDef).setInternalValue(convertedValue, false);

        assert !entityInstance.getAttribute(attributeDef).isUpdated() : "Attribute is updated "
                + attributeDef.toString();
    }

    /**
     * This will load all the attributes for a row in the ResultSet.  This will
     * potentially create entities.
     *
     * @param loadedEntities
     * @param entityCount
     * @throws SQLException
     */
    private void loadAttributes(SqlStatement stmt, ResultSet rs, View view,
            Map<String, EntityInstance> loadedEntities, IntegerLinkedHashMap<EntityDef> entityCount)
            throws SQLException {
        // Some JDBC drivers don't allow us to retrieve the same column twice from the same statement.
        // To get around this we'll store the values in an array in case we need to use them again.
        Map<Integer, Object> loadedObjects = new HashMap<Integer, Object>(10);

        // Loop through each of the DataRecords that are part of this statement.
        for (DataRecord dataRecord : stmt.dataRecords.keySet()) {
            final EntityDef entityDef = dataRecord.getEntityDef();
            EntityInstance entityInstance = null;

            // For each DataRecord, load the attributes.
            for (DataField dataField : stmt.dataRecords.get(dataRecord)) {
                try {
                    Integer columnIdx = stmt.getColumns().get(dataField);
                    Object value = getSqlObject(rs, columnIdx, dataField, loadedObjects);
                    if (value == null)
                        continue; // Value is null so don't bother setting it.

                    // Create the new entity if we haven't already loaded this instance, otherwise set the cursor to it.
                    if (entityInstance == null) {
                        // It is possible for an entity instance to appear more than once in the result set.  This can
                        // happen if qualification returns it twice or if we're doing a join of small 1-to-many tables
                        // and the parent instance is returned for each child table.
                        //
                        // To handle this we'll grab the key values for the entity and see if we've already loaded
                        // an instance with those keys.  Get a key string of all keys of this entity instance and
                        // its parents.
                        String keyString = getFullKeyValuesForCurrentRow(entityDef, rs, stmt, loadedObjects);
                        entityInstance = loadedEntities.get(keyString);
                        if (entityInstance != null) {
                            // We've already loaded this entity instance.  Set the cursor and stop
                            // loading the attributes for this instance.  We need to set the cursor because
                            // we might be loading instances that are children of entityDef.
                            view.cursor(entityDef).setCursor(entityInstance);
                            break;
                        }

                        // If we're loading all instances of EntityDef then we need to set the parent cursor
                        // to point to the correct entity.
                        EntityDef parent = entityDef.getParent();
                        RelRecord relRecord = dataRecord.getRelRecord();
                        if (selectAllInstances(entityDef)) {
                            DataField keyField;
                            switch (relRecord.getRelationshipType()) {
                            case MANY_TO_MANY:
                                keyField = relRecord.getParentRelField().getSrcDataField();
                                break;
                            case ONE_TO_MANY:
                                keyField = relRecord.getRelFields().get(0).getRelDataField();
                                break;
                            default:
                                throw new ZeidonException("Unsupported reltype"); // Should never happen.
                            }

                            columnIdx = stmt.getColumns().get(keyField);
                            Object key = getSqlObject(rs, columnIdx, keyField, loadedObjects);

                            EntityInstance parentEi = loadedInstances.get(parent).get(key);
                            if (parentEi == null)
                                throw new ZeidonException("Didn't find parent EI by key: %s", key);

                            view.cursor(parent).setCursor(parentEi);
                        }

                        // Create the entity but tell the OE not to spawn because this will cause the OE
                        // to attempt to spawn entities that we've already loaded.
                        entityInstance = view.cursor(entityDef).createEntity(CursorPosition.LAST, CREATE_FLAGS);

                        loadedEntities.put(keyString, entityInstance);
                        entityCount.increment(entityDef);

                        // We've created a new instance so now check to see if we link this instance with another.
                        String entityKeyString = getKeyValuesForCurrentEntity(entityDef, rs, stmt, loadedObjects);

                        if (entityDef.isRecursive())
                            checkForInfiniteRecursiveLoop(view, entityDef, entityKeyString);

                        if (entityDef.isDuplicateEntity()) {
                            if (entityLinker.addEntity(entityInstance, entityKeyString))
                                break; // This entity was relinked with another entity so we can stop loading it.
                        }
                    }

                    AttributeDef AttributeDef = dataField.getAttributeDef();

                    // If the attributeDef does not belong to entityDef then it's a field from a many-to-many
                    // relationship that was used to set the cursor and shouldn't be copied.
                    if (AttributeDef.getEntityDef() != entityDef)
                        continue;

                    setAttribute(entityInstance, AttributeDef, value);

                    // Check to see if we should save this instance in the map of all loaded
                    // instances.
                    if (AttributeDef.isKey() && loadedInstances.containsKey(entityDef)) {
                        // If we have a situation where the key is already in the map then
                        // there are multiple instances of entityDef.  This can happen if
                        // one of the parent relationships is many-to-one.  Duplicate keys
                        // nullifies the ability to load the children in a single select
                        // so remove the entityDef from loadedInstances to indicate we
                        // can't load the children in one select.
                        if (loadedInstances.get(entityDef).containsKey(value))
                            loadedInstances.remove(entityDef);
                        else
                            loadedInstances.get(entityDef).put(value, entityInstance);
                    }
                } catch (Exception e) {
                    throw ZeidonException.wrapException(e).prependAttributeDef(dataField.getAttributeDef())
                            .prependMessage("Column = %s.%s", dataRecord.getRecordName(), dataField.getName());
                }
            } // for each DataField...

            assert assertNotNullKey(view, entityDef) : "Activated entity has null key";

        } // for each DataRecord...
    }

    /**
     * Make sure the key string doesn't exist in parent entities.  This indicates an infinite loop.
     *
     * @param view
     * @param entityDef
     * @param entityKeyString
     */
    private void checkForInfiniteRecursiveLoop(View view, EntityDef entityDef, String entityKeyString) {
        EntityDef parent = entityDef.getParent();

        // Search through the parent chain of the current entity.
        for (EntityInstance ei = view.cursor(parent).getEntityInstance(); ei != null; ei = ei.getParent()) {
            // If the ei has a different relationship from entityDef then it's not
            // part of a recursive loop.
            if (ei.getEntityDef().getErRelToken() != entityDef.getErRelToken())
                continue;

            String parentKeyString = ei.getKeyString();
            if (StringUtils.equals(entityKeyString, parentKeyString)) {
                throw new ZeidonException("Infinite recursive loop detected while activating OI")
                        .appendMessage("Child entity: %s", entityDef.getName())
                        .appendMessage("Child key string: %s", entityKeyString)
                        .appendMessage("Parent entity: %s", ei.getEntityDef().getName())
                        .appendMessage("Parent key string: %s", parentKeyString);
            }
        }
    }

    private void close(ResultSet rs, PreparedStatement ps) {
        try {
            close(rs);

            if (cachedStatements == null) // Are we caching PreparedStatements?
                DbUtils.closeQuietly(ps);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void close(ResultSet rs) {
        try {
            if (rs != null) {
                rs.close();
            }
        } catch (SQLException e) {
            task.dblog().error(e);
        }
    }

    @Override
    protected int executeSql(String sql) {
        logSql(sql);

        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            beginTransaction(null);
            ps = prepareAndBind(null, sql, null, null, null);
            ps.execute();
        } catch (Exception e) {
            throw ZeidonException.prependMessage(e, "SQL => %s", sql);
        } finally {
            close(rs, ps);
            endTransaction(true);
        }

        return 0;
    }

    /**
     * Returns a PreparedStatement for the sql.  If we are caching PreparedStatements then this will
     * perform necessary logic for caching.  Will bind attributes if there are any.
     * @param stmt TODO
     * @param sql
     * @param view TODO
     * @param entityDef TODO
     * @param commandType
     *
     * @return
     * @throws SQLException
     */
    private PreparedStatement prepareAndBind(SqlStatement stmt, String sql, View view, EntityDef entityDef,
            SqlCommand commandType) throws SQLException {
        PreparedStatement ps = null;

        // Not every statement can be cached.
        boolean cacheThisCommand = false;
        if (commandType != null) {
            switch (commandType) {
            case SELECT:
            case DELETE:
                cacheThisCommand = true;
                break;

            default:
                break;
            }
        }

        if (cacheThisCommand && cachedStatements != null && entityDef != null) // Are we using cached PreparedStatements?
        {
            PreparedStatementCacheKey key = new PreparedStatementCacheKey(entityDef, commandType, sql);
            PreparedStatementCacheValue value = cachedStatements.get(key.getKey());
            if (value == null) {
                ps = useDbGenerateKeys()
                        ? transaction.getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)
                        : transaction.getConnection().prepareStatement(sql);

                if (stmt.activateLimit > 0) {
                    task.dblog().debug("setMaxRows = %d", stmt.activateLimit);
                    ps.setMaxRows(stmt.activateLimit);
                }

                value = new PreparedStatementCacheValue(ps, sql);
                cachedStatements.put(key.getKey(), value);
            } else {
                task.dblog().trace("Using cached statement for Entity => %s \n=> %s", entityDef, sql);
                ps = value.ps;
            }
        } else {
            // Some JDBC implementations don't support Statement.NO_GENERATED_KEYS (SQLDroid I'm looking
            // at you) so we have to use the single-argument prepareStatement if we aren't keeping the
            // generated keys.
            if (useDbGenerateKeys())
                ps = transaction.getConnection().prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            else
                ps = transaction.getConnection().prepareStatement(sql);

            if (stmt != null && stmt.activateLimit > 0) {
                task.dblog().debug("setMaxRows = %d", stmt.activateLimit);
                ps.setMaxRows(stmt.activateLimit);
            }
        }

        if (stmt != null) // When executing simple statements this will be null.
        {
            int idx = 0;
            for (Object boundValue : stmt.getBoundValues()) {
                idx++;
                String valueAsString;
                if (boundValue instanceof DataField) {
                    DataField dataField = (DataField) boundValue;
                    valueAsString = getTranslator().bindAttributeValue(ps, view, dataField, idx);
                } else {
                    valueAsString = getTranslator().bindAttributeValue(ps, boundValue, idx);
                }

                if (task.dblog().isDebugEnabled())
                    task.dblog().debug("Bind idx %d = %s (attr value)", idx, leftStr(valueAsString));
            }
        }

        return ps;
    }

    private String leftStr(String str) {
        if (str.length() <= 100)
            return str;

        return StringUtils.left(str, 100) + "<truncated>";
    }

    private String generateErrorMessageWithBoundAttributes(String sql, EntityDef entityDef, SqlStatement stmt) {
        StringBuilder sb = new StringBuilder("SQL => ");
        sb.append(sql);

        int count = 0;
        for (Object o : stmt.getBoundValues()) {
            count++;
            if (o == null)
                sb.append("\n   Value null");
            else
                sb.append("\n   Value ").append(count).append(" : ").append(o).append(" [")
                        .append(o.getClass().getCanonicalName()).append("]");
        }

        sb.append("\n   EntityDef : ").append(entityDef);
        return sb.toString();
    }

    @Override
    protected int executeStatement(View view, EntityDef entityDef, SqlStatement stmt) {
        String sql = stmt.getAssembledCommand();
        logSql(stmt);

        PreparedStatement ps = null;
        ResultSet rs = null;

        try {
            ps = prepareAndBind(stmt, sql, view, entityDef, stmt.commandType);

            if (stmt.commandType == SqlCommand.INSERT) {
                ps.executeUpdate();

                if (useDbGenerateKeys()) {
                    generatedKeys = new ArrayList<Object>();
                    ResultSet rs2 = ps.getGeneratedKeys();
                    try {
                        while (rs2.next()) {
                            Integer i = rs2.getInt(1);
                            generatedKeys.add(i);
                        }
                    } finally {
                        DbUtils.closeQuietly(rs2);
                    }
                } else
                    generatedKeys = null;

            } else if (stmt.commandType == SqlCommand.SELECT) {
                // This should be getting the count.
                rs = ps.executeQuery();
                return rs.getInt(1);
            } else {
                ps.execute();
            }
        } catch (Exception e) {
            throw ZeidonException.prependMessage(e, generateErrorMessageWithBoundAttributes(sql, entityDef, stmt));
        } finally {
            close(rs, ps);
        }

        return 0;
    }

    /**
     * This is a temporary object that is used to create a key in the PreparedStatement cache.
     * Currently this uses the SQL string as part of the key but comparisons take longer than I'd
     * like.  It'd be nice to come up with a different solution.  The problem is that update statements
     * update only the attributes that were changed and this causes the SQL to look different for some
     * updates.
     *
     */
    static private class PreparedStatementCacheKey {
        private final EntityDef entityDef;
        private final SqlCommand commandType;
        private final String sql;

        private PreparedStatementCacheKey(EntityDef entityDef, SqlCommand commandType, String sql) {
            assert entityDef != null;
            assert commandType != null;
            assert sql != null;

            this.entityDef = entityDef;
            this.commandType = commandType;
            this.sql = sql;
        }

        private String getKey() {
            return sql;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#hashCode()
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + commandType.hashCode();
            result = prime * result + entityDef.hashCode();
            result = prime * result + sql.hashCode();
            return result;
        }

        /* (non-Javadoc)
         * @see java.lang.Object#equals(java.lang.Object)
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            PreparedStatementCacheKey other = (PreparedStatementCacheKey) obj;
            if (commandType != other.commandType)
                return false;
            if (entityDef != other.entityDef)
                return false;
            if (!sql.equals(other.sql))
                return false;

            return true;
        }

        @Override
        public String toString() {
            return entityDef.toString() + " " + commandType.toString() + " " + StringUtils.substring(sql, 0, 50);
        }
    }

    static private class PreparedStatementCacheValue {
        private final PreparedStatement ps;
        private final String sql;

        private PreparedStatementCacheValue(PreparedStatement ps, String sql) {
            assert ps != null;
            assert sql != null;

            this.ps = ps;
            this.sql = sql;
        }

        @Override
        public String toString() {
            return sql;
        }
    }

    /**
     * Create our own private class that extends GenericObjectPool so we can insert it into
     * the task cache without worry of collision.
     */
    private static class JdbcConnectionPool {
        /**
         * We need to keep a separate pool for each type of transaction string.
         */
        private final ConcurrentMap<String, BasicDataSource> poolMap;

        private JdbcConnectionPool() {
            poolMap = new MapMaker().concurrencyLevel(4).makeMap();
        }

        private Connection getConnection(String url, Task task, JdbcHandler handler, Application application) {
            Connection connection;

            try {
                BasicDataSource pool = getPool(url, task, handler, application);
                connection = pool.getConnection();
                connection.setAutoCommit(false);
            } catch (SQLException e) {
                throw ZeidonException.wrapException(e).appendMessage("Connection String = %s", url)
                        .appendMessage("Username: %s", handler.getUserName());
            }

            return connection;
        }

        private String getDriver(String url, Task task, JdbcHandler handler) {
            String driver = handler.getDrivers();
            if (StringUtils.isBlank(driver)) {
                // Drivers wasn't specified in the config, so if possible we'll guess
                // by using the transaction string.
                if (url.startsWith("jdbc:mysql:"))
                    driver = "com.mysql.jdbc.Driver";
                else if (url.startsWith("jdbc:sqlite:"))
                    driver = "org.sqlite.JDBC";
                else if (url.startsWith("jdbc:odbc:"))
                    driver = "sun.jdbc.odbc.JdbcOdbcDriver";
                else if (url.startsWith("jdbc:sqlserver:"))
                    driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
                else
                    throw new ZeidonException("JDBC Driver wasn't specified in config for %s", url);
            }

            return driver;
        }

        private BasicDataSource getPool(String url, Task task, JdbcHandler handler, Application application) {
            BasicDataSource pool = poolMap.get(url);
            if (pool == null) {
                // We should get here only rarely.  Let's synchronize using the connStr.
                url = url.intern();
                synchronized (url) {
                    pool = new BasicDataSource();

                    String driverClassName = getDriver(url, task, handler);
                    pool.setDriverClassName(driverClassName);

                    String username = handler.getUserName();
                    String password = handler.getPassword();

                    pool.setUsername(username);
                    pool.setPassword(password);
                    pool.setUrl(url);
                    pool.setTestOnBorrow(true);
                    pool.setValidationQuery("select 1");
                    poolMap.putIfAbsent(url, pool);

                    // It's even less likely that two threads created their own transaction
                    // pool but it doesn't hurt handle it.
                    pool = poolMap.get(url);
                }
            }

            return pool;
        }
    }

    /**
     * Returns the keys generated by the last insert.
     */
    @Override
    public List<Object> getKeysGeneratedByDb() {
        return generatedKeys;
    }

    static private final Class<?>[] translatorConstructorArgs = new Class<?>[] { Task.class, JdbcHandler.class };

    @SuppressWarnings("unchecked")
    public JdbcDomainTranslator getTranslator() {
        if (translator == null) {
            String transName = getConfigValue("Translator");

            // If translator name isn't defined, use the standard one.
            if (StringUtils.isBlank(transName)) {
                // Translator isn't specified.  Let's try to be smart and determine the
                // correct translator from the transaction string.
                String connStr = options.getOiSourceUrl();
                if (!StringUtils.isBlank(connStr)) {
                    if (connStr.contains("sqlite"))
                        return new SqliteJdbcTranslator(task, this);
                }

                return new StandardJdbcTranslator(task, this);
            }

            try {
                ObjectEngine oe = task.getObjectEngine();
                ClassLoader classLoader = oe.getClassLoader(transName);
                Class<? extends JdbcDomainTranslator> translatorClass;
                translatorClass = (Class<? extends JdbcDomainTranslator>) classLoader.loadClass(transName);
                Constructor<? extends JdbcDomainTranslator> constructor = translatorClass
                        .getConstructor(translatorConstructorArgs);
                translator = constructor.newInstance(task, this);
            } catch (Throwable t) {
                throw ZeidonException.prependMessage(t, "Error trying to load translator class = '%s', DB=%s",
                        transName, options.getOiSourceUrl());
            }
        }

        return translator;
    }

    public DateTimeFormatter getDateFormatter() {
        if (dateFormat == null) {
            String format = getConfigValue("DateFormat");
            if (StringUtils.isBlank(format))
                format = "yyyy-MM-dd";

            dateFormat = JoeUtils.createDateFormatterFromEditString(format);
        }

        return dateFormat;
    }

    public DateTimeFormatter getDateTimeFormatter() {
        if (dateTimeFormat == null) {
            String format = getConfigValue("dateTimeFormat");
            if (StringUtils.isBlank(format)) {
                format = getConfigValue("DateFormat");
                if (StringUtils.isBlank(format))
                    format = "yyyy-MM-dd";

                format = format + " HH:mm:ss.SSS|" + format + " HH:mm:ss";
            }

            dateTimeFormat = JoeUtils.createDateFormatterFromEditString(format);
        }

        return dateTimeFormat;
    }

    public String getDrivers() {
        if (drivers == null)
            drivers = getConfigValue("Drivers");

        return drivers;
    }
}