All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.quinsoft.zeidon.dbhandler.JdbcHandler Maven / Gradle / Ivy

There is a newer version: 2.2.0
Show newest version
/**
    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 .

    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 java.util.regex.Matcher;
import java.util.regex.Pattern;

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

    protected void initializeBasicDataSource( BasicDataSource dataSource,
                                              Task task,
                                              Application application)
    {
        // By default do nothing but give other handlers a chance to do something.
    }

    @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 loadedObjects )
    {
        // If we're using OpenSQL we'll assume the first column is the unique identifier.
        if ( stmt.usesOpenSql )
            return getSqlObject( rs, 1, null, loadedObjects ).toString();

        List values = new ArrayList();

        // 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 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 loadedObjects )
    {
        // If we're using OpenSQL we'll assume the first column is the unique identifier.
        if ( stmt.usesOpenSql )
            return getSqlObject( rs, 1, null, loadedObjects ).toString();

        KeyStringBuilder builder = new KeyStringBuilder();

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

        List 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 loadedEntities = new HashMap();
            IntegerLinkedHashMap entityCounts = new IntegerLinkedHashMap();
            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 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 loadedEntities,
                                 IntegerLinkedHashMap 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 loadedObjects = new HashMap(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.
                        if ( selectAllInstances( entityDef ) )
                        {
                            EntityDef parent = entityDef.getParent();
                            RelRecord relRecord = dataRecord.getRelRecord();
                            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 ) + "";
    }

    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();
                    ResultSet rs2 = ps.getGeneratedKeys();
                    try
                    {
                        while ( rs2.next() )
                        {
                            Object i = rs2.getObject( 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();
                rs.next();
                return rs.getInt( 1 );
            }
            else
            {
                ps.execute();
            }
        }
        catch ( SQLException e )
        {
            throw new ZeidonDbException( view, e ).appendMessage( generateErrorMessageWithBoundAttributes( sql, entityDef, stmt ) );
        }
        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 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 static final Pattern JDBC_FINDER = Pattern.compile( "^jdbc:([a-zA-Z]*):.*" );

        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.
                Matcher matcher = JDBC_FINDER.matcher( url );
                if ( matcher.matches() )
                {
                    String driverName = matcher.group( 1 );
                    switch ( driverName )
                    {
                        case "mysql":
                            driver = "com.mysql.jdbc.Driver";
                            break;

                        case "odbc":
                            driver = "sun.jdbc.odbc.JdbcOdbcDriver";
                            break;

                        case "postgresql":
                            driver = "org.postgresql.Driver";
                            break;

                        case "sqlite":
                            driver = "org.sqlite.JDBC";
                            break;

                        case "sqlserver":
                            driver = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
                            break;

                        default:
                            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" );
                    handler.initializeBasicDataSource( pool, task, application );
                    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 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" ) )
                    {
                        translator = new SqliteJdbcTranslator( task, this );
                        return translator;
                    }
                }

                translator = new StandardJdbcTranslator( task, this );
                return translator;
            }

            try
            {
                ObjectEngine oe = task.getObjectEngine();
                ClassLoader classLoader = oe.getClassLoader( transName );
                Class translatorClass;
                translatorClass = (Class) classLoader.loadClass( transName );
                Constructor 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|yyyy-MM-dd 00:00:00";

            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;
    }
}