Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.quinsoft.zeidon.dbhandler.JdbcHandler Maven / Gradle / Ivy
/**
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 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|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;
}
}