Java tutorial
/** 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; } }