package er.extensions.jdbc; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.util.Iterator; import com.webobjects.eoaccess.EOAdaptor; import com.webobjects.eoaccess.EOAdaptorChannel; import com.webobjects.eoaccess.EOAdaptorContext; import com.webobjects.eoaccess.EOAttribute; import com.webobjects.eoaccess.EOEntity; import com.webobjects.eoaccess.EOGeneralAdaptorException; import com.webobjects.eoaccess.EOSQLExpression; import com.webobjects.eoaccess.EOStoredProcedure; import com.webobjects.eocontrol.EOFetchSpecification; import com.webobjects.eocontrol.EOQualifier; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSDictionary; import com.webobjects.foundation.NSForwardException; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation._NSUtilities; import com.webobjects.jdbcadaptor.ERXJDBCColumn; import com.webobjects.jdbcadaptor.JDBCAdaptor; import com.webobjects.jdbcadaptor.JDBCAdaptorException; import com.webobjects.jdbcadaptor.JDBCChannel; import com.webobjects.jdbcadaptor.JDBCContext; import com.webobjects.jdbcadaptor.JDBCPlugIn; import er.extensions.eof.ERXAdaptorOperationWrapper; import er.extensions.foundation.ERXKeyValueCodingUtilities; import er.extensions.foundation.ERXPatcher; import er.extensions.foundation.ERXProperties; import er.extensions.foundation.ERXSystem; import er.extensions.foundation.ERXValueUtilities; /** * Subclass of the JDBC adaptor and accompanying classes that supports * connection pooling and posting of adaptor operation notifications. Will get * patched into the runtime via the usual class name magic if the property * <code>er.extensions.ERXJDBCAdaptor.className</code> is set to this class's * name or another subclass of JDBCAdaptor. The connection pooling will be * enabled if the system property * <code>er.extensions.ERXJDBCAdaptor.useConnectionBroker</code> is set. * * @author ak * */ public class ERXJDBCAdaptor extends JDBCAdaptor { public static interface ConnectionBroker { public void freeConnection(Connection conn); public Connection getConnection(); } public static final String USE_CONNECTION_BROKER_KEY = "er.extensions.ERXJDBCAdaptor.useConnectionBroker"; public static final String CLASS_NAME_KEY = "er.extensions.ERXJDBCAdaptor.className"; private static Boolean switchReadWrite = null; private static Boolean useConnectionBroker = null; static boolean switchReadWrite() { if (switchReadWrite == null) { switchReadWrite = "false".equals(ERXSystem.getProperty("er.extensions.ERXJDBCAdaptor.switchReadWrite", "false")) ? Boolean.FALSE : Boolean.TRUE; } return switchReadWrite.booleanValue(); } /** * Returns whether the connection broker is active. * * @return <code>true</code> if connection broker is active */ public static boolean useConnectionBroker() { if (useConnectionBroker == null) { useConnectionBroker = ERXProperties.booleanForKeyWithDefault(USE_CONNECTION_BROKER_KEY, false) ? Boolean.TRUE : Boolean.FALSE; } return useConnectionBroker.booleanValue(); } public static void registerJDBCAdaptor() { String className = ERXProperties.stringForKey(CLASS_NAME_KEY); if (className != null) { Class c = ERXPatcher.classForName(className); if (c == null) { throw new IllegalStateException("Can't find class: " + className); } ERXPatcher.setClassForName(c, JDBCAdaptor.class.getName()); } } /** * Channel subclass to support notification posting. * * @author ak */ public static class Channel extends JDBCChannel { public static final String COLUMN_CLASS_NAME_KEY = "er.extensions.ERXJDBCAdaptor.columnClassName"; private static Class columnClass; /** * The class of the JDBCColumn. It must subclass ERXJDBCColumn and provide * implementations for the same two constructors as ERXJDBCColumn. It is set * using the property <code>er.extensions.ERXJDBCAdaptor.columnClassName</code> * If no value is set, then the default class is ERXJDBCColumn. * * @return The ERXJDBCColumn subclass */ public static Class columnClass() { if(columnClass == null) { String className = ERXProperties.stringForKey(COLUMN_CLASS_NAME_KEY); if(className != null && className.length() > 0) { columnClass = _NSUtilities.classWithName(className); } else { columnClass = ERXJDBCColumn.class; } } return columnClass; } public static ERXJDBCColumn newERXJDBCColumn(Channel channel) { try { Constructor<? extends ERXJDBCColumn> cstr = columnClass().getDeclaredConstructor(Channel.class); return cstr.newInstance(channel); } catch(Exception e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } public static ERXJDBCColumn newERXJDBCColumn(EOAttribute attribute, JDBCChannel channel, int column, ResultSet rs) { try { Constructor<? extends ERXJDBCColumn> cstr = columnClass().getDeclaredConstructor(EOAttribute.class, JDBCChannel.class, Integer.TYPE, ResultSet.class); return cstr.newInstance(attribute, channel, column, rs); } catch(Exception e) { throw NSForwardException._runtimeExceptionForThrowable(e); } } public Channel(JDBCContext jdbccontext) { super(jdbccontext); try { Field field = JDBCChannel.class.getDeclaredField("_inputColumn"); field.setAccessible(true); field.set(this, newERXJDBCColumn(this)); } catch (Exception e) { System.err.println(e); e.printStackTrace(); System.exit(1); } } @Override public void setAttributesToFetch(NSArray<EOAttribute> attributes) { _attributes = attributes; int j; if (_attributes == null || (j = _attributes.count()) == 0) return; ERXJDBCColumn columns[] = new ERXJDBCColumn[j]; for (int i = 0; i < j; i++) columns[i] = newERXJDBCColumn(_attributes.objectAtIndex(i), this, i + 1, _resultSet); _selectedColumns = new NSArray(columns); } private boolean setReadOnly(boolean mode) { boolean old = false; if (switchReadWrite()) { try { Connection connection = ((JDBCContext) adaptorContext()).connection(); if (connection != null) { old = connection.isReadOnly(); connection.setReadOnly(mode); } else { throw new EOGeneralAdaptorException("Can't switch connection mode to " + mode + ", the connection is null"); } } catch (java.sql.SQLException e) { throw new EOGeneralAdaptorException("Can't switch connection mode to " + mode, new NSDictionary(e, "originalException")); } } return old; } /** * Overridden to switch the connection to read-only while selecting. */ @Override public void selectAttributes(NSArray array, EOFetchSpecification fetchspecification, boolean lock, EOEntity entity) { boolean mode = setReadOnly(!lock); super.selectAttributes(array, fetchspecification, lock, entity); setReadOnly(mode); } /** * Overridden to post a notification when the operations were performed. */ @Override public void performAdaptorOperations(NSArray ops) { super.performAdaptorOperations(ops); ERXAdaptorOperationWrapper.adaptorOperationsDidPerform(ops); } private JDBCPlugIn _plugIn() { JDBCAdaptor jdbcadaptor = (JDBCAdaptor) adaptorContext().adaptor(); return jdbcadaptor.plugIn(); } private static NSMutableDictionary<String, NSMutableArray> pkCache = new NSMutableDictionary<>(); private int defaultBatchSize = ERXProperties.intForKeyWithDefault("er.extensions.ERXPrimaryKeyBatchSize", -1); /** * Batch-fetches new primary keys. Set the property * <code> er.extensions.ERXPrimaryKeyBatchSize</code> to a number * larger than 0. Also, you can fine-tune the size by adding a key * <code>ERXPrimaryKeyBatchSize</code> to your model or entity user * info. */ @Override public NSArray primaryKeysForNewRowsWithEntity(int cnt, EOEntity entity) { if (defaultBatchSize > 0) { synchronized (pkCache) { String key = entity.primaryKeyRootName(); NSMutableArray pks = pkCache.objectForKey(key); if (pks == null) { pks = new NSMutableArray(); pkCache.setObjectForKey(pks, key); } if (pks.count() < cnt) { Object batchSize = (entity.userInfo() != null ? entity.userInfo().objectForKey("ERXPrimaryKeyBatchSize") : null); if (batchSize == null) { batchSize = (entity.model().userInfo() != null ? entity.model().userInfo().objectForKey("ERXPrimaryKeyBatchSize") : null); } if (batchSize == null) { batchSize = ERXProperties.stringForKey("er.extensions.ERXPrimaryKeyBatchSize"); } int size = defaultBatchSize; if (batchSize != null) { size = ERXValueUtilities.intValue(batchSize); } pks.addObjectsFromArray(_plugIn().newPrimaryKeys(size + cnt, entity, this)); } NSMutableArray batch = new NSMutableArray(); for (Iterator iterator = pks.iterator(); iterator.hasNext() && --cnt >= 0;) { Object pk = iterator.next(); batch.addObject(pk); iterator.remove(); } return batch; } } return _plugIn().newPrimaryKeys(cnt, entity, this); } private void cleanup() { Boolean value = (Boolean) ERXKeyValueCodingUtilities.privateValueForKey(this, "_beganTransaction"); if (value) { try { _context.rollbackTransaction(); } catch (JDBCAdaptorException ex) { ERXKeyValueCodingUtilities.takePrivateValueForKey(this, Boolean.FALSE, "_beganTransaction"); throw ex; } } } /** * Overridden to clean up after a transaction fails. */ @Override public void evaluateExpression(EOSQLExpression eosqlexpression) { try { super.evaluateExpression(eosqlexpression); } catch (JDBCAdaptorException ex) { cleanup(); throw ex; } } /** * Overridden to clean up after a transaction fails. */ @Override public void executeStoredProcedure(EOStoredProcedure eostoredprocedure, NSDictionary nsdictionary) { try { super.executeStoredProcedure(eostoredprocedure, nsdictionary); } catch (JDBCAdaptorException ex) { cleanup(); throw ex; } } /** * Overridden to clean up after a transaction fails. */ @Override public int deleteRowsDescribedByQualifier(EOQualifier eoqualifier, EOEntity eoentity) { try { return super.deleteRowsDescribedByQualifier(eoqualifier, eoentity); } catch (JDBCAdaptorException ex) { cleanup(); throw ex; } } /** * Overridden to clea up after a transaction fails. */ @Override public int updateValuesInRowsDescribedByQualifier(NSDictionary nsdictionary, EOQualifier eoqualifier, EOEntity eoentity) { try { return super.updateValuesInRowsDescribedByQualifier(nsdictionary, eoqualifier, eoentity); } catch (JDBCAdaptorException ex) { cleanup(); throw ex; } } } /** * Context subclass that uses connection pooling. * * @author ak */ public static class Context extends JDBCContext { public static final String IGNORE_JNDI_CONFIGURATION_KEY = "er.extensions.ERXJDBCAdaptor.ignoreJNDIConfiguration"; public Context(EOAdaptor eoadaptor) { super(eoadaptor); } /** * In servlet context, when not using JNDI to obtain the database channel, you will get annoying error messages like * <em>javax.naming.NameNotFoundException: Name "comp/env/jdbc" not found in context</em>. * * Set the property <code>er.extensions.ERXJDBCAdaptor.ignoreJNDIConfiguration</code> to true in order to suppress * this messages. * * @throws JDBCAdaptorException */ @Override public void setupJndiConfiguration() throws JDBCAdaptorException { if(!ERXProperties.booleanForKeyWithDefault(IGNORE_JNDI_CONFIGURATION_KEY, false)) { super.setupJndiConfiguration(); } } private void freeConnection() { if (useConnectionBroker()) { if (_jdbcConnection != null) { ((ERXJDBCAdaptor) adaptor()).freeConnection(_jdbcConnection); _jdbcConnection = null; } } } /** * Re-implemented to fix: http://www.mail-archive.com/dspace-tech@lists.sourceforge.net/msg06063.html. * We could also use the delegate, but where would be the fun in that? */ @Override public void rollbackTransaction() { if (!hasOpenTransaction()) { return; } if (((Number)ERXKeyValueCodingUtilities.privateValueForKey(this, "_fetchesInProgress")).intValue() > 0) { throw new JDBCAdaptorException("Cannot rollbackTransaction() while a fetch is in progress", null); } if (_delegateRespondsTo_shouldRollback && !_delegate.booleanPerform("adaptorContextShouldRollback", this)) return; try { if (_connectionSupportTransaction) { // AK: only roll back if the connection isn't closed. if(!_jdbcConnection.isClosed()) { _jdbcConnection.rollback(); } } } catch (SQLException sqlexception) { throw new JDBCAdaptorException(sqlexception); } transactionDidRollback(); if (_delegateRespondsTo_didRollback) { _delegate.perform("adaptorContextDidRollback", this); } } private void checkoutConnection() { if (useConnectionBroker()) { if (_jdbcConnection == null) { _jdbcConnection = ((ERXJDBCAdaptor) adaptor()).checkoutConnection(); } } } @Override public boolean connect() throws JDBCAdaptorException { boolean connected = false; if (useConnectionBroker()) { checkoutConnection(); connected = _jdbcConnection != null; } else { connected = super.connect(); } return connected; } protected JDBCChannel createJDBCChannel() { return new Channel(this); } @Override protected JDBCChannel _cachedAdaptorChannel() { if (_cachedChannel == null) { _cachedChannel = createJDBCChannel(); } return _cachedChannel; } @Override public EOAdaptorChannel createAdaptorChannel() { if (_cachedChannel != null) { JDBCChannel jdbcchannel = _cachedChannel; _cachedChannel = null; return jdbcchannel; } return createJDBCChannel(); } @Override public void disconnect() throws JDBCAdaptorException { freeConnection(); super.disconnect(); } @Override public void beginTransaction() { checkoutConnection(); super.beginTransaction(); } @Override public void transactionDidCommit() { super.transactionDidCommit(); freeConnection(); } @Override public void transactionDidRollback() { super.transactionDidRollback(); freeConnection(); } } public ERXJDBCAdaptor(String name) { super(name); } @Override protected JDBCContext _cachedAdaptorContext() { if (_cachedContext == null) { _cachedContext = createJDBCContext(); } return _cachedContext; } @Override protected NSDictionary jdbcInfo() { boolean closeCachedContext = (_cachedContext == null && _jdbcInfo == null); NSDictionary jdbcInfo = super.jdbcInfo(); if (closeCachedContext && _cachedContext != null) { _cachedContext.disconnect(); _cachedContext = null; } return jdbcInfo; } @Override protected NSDictionary typeInfo() { boolean closeCachedContext = (_cachedContext == null && _jdbcInfo == null); NSDictionary typeInfo = super.typeInfo(); if (closeCachedContext && _cachedContext != null) { _cachedContext.disconnect(); _cachedContext = null; } return typeInfo; } public Context createJDBCContext() { Context context = new Context(this); return context; } @Override public EOAdaptorContext createAdaptorContext() { EOAdaptorContext context; if (_cachedContext != null) { context = _cachedContext; _cachedContext = null; } else { context = createJDBCContext(); } return context; } protected Connection checkoutConnection() { Connection c = connectionBroker().getConnection(); return c; } private ConnectionBroker connectionBroker() { return ERXJDBCConnectionBroker.connectionBrokerForAdaptor(this); } protected void freeConnection(Connection connection) { connectionBroker().freeConnection(connection); } }