/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2001 Michael Powers This library 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 2.1 of the License, or (at your option) any later version. This library 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 this library; if not, see http://www.gnu.org */ package net.wotonomy.jdbcadaptor; import java.math.BigDecimal; import java.sql.Blob; import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import net.wotonomy.access.EOAdaptorChannel; import net.wotonomy.access.EOAttribute; import net.wotonomy.access.EOEntity; import net.wotonomy.access.EOGeneralAdaptorException; import net.wotonomy.access.EOSQLExpression; import net.wotonomy.access.EOStoredProcedure; import net.wotonomy.control.EOFetchSpecification; import net.wotonomy.control.EOQualifier; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSData; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSKeyValueCoding; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.NSTimestamp; /** * Concrete implementation of EOAdaptorChannel for use with JDBC. * * @author ezamudio@nasoft.com * @author $Author: cgruber $ * @version $Revision: 903 $ */ public class JDBCChannel extends EOAdaptorChannel { protected boolean _fetchInProgress; protected ResultSet _resultSet; protected Statement _statement; protected NSArray _attsToFetch; protected NSArray _resultAttributes; protected boolean _transactionWasOpen; protected NSDictionary _spReturnValues; protected int _resultCount; /** * Creates a new JDBCChannel. * * @param context The JDBCContext this channel belongs to. */ public JDBCChannel(JDBCContext context) { super(context); } protected JDBCContext _context() { return (JDBCContext) adaptorContext(); } /* * Sets the attributes to be fetched from the database. * * @see net.wotonomy.access.EOAdaptorChannel#setAttributesToFetch(net.wotonomy. * foundation.NSArray) */ public void setAttributesToFetch(NSArray atts) { _attsToFetch = atts; } /* * Returns an array with the attributes that will be fetched. * * @see net.wotonomy.access.EOAdaptorChannel#attributesToFetch() */ public NSArray attributesToFetch() { return _attsToFetch; } /* * Cancels the fetch, rolling back the transaction. * * @see net.wotonomy.access.EOAdaptorChannel#cancelFetch() */ public void cancelFetch() { if (_statement == null || _resultSet == null) return; try { _resultSet.close(); _statement.cancel(); } catch (SQLException ex) { throw new JDBCAdaptorException("Cannot cancel fetch in database.", ex); } } /* * Closes the jdbc channel. * * @see net.wotonomy.access.EOAdaptorChannel#closeChannel() */ public void closeChannel() { if (_statement == null) return; try { _statement.close(); } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to close the channel.", ex); } } /* * If the fetch was done with an array of EOAttributes, returns that same array; * otherwise it creates an array of EOAttributes based on the column names that * will be fetched. * * @see net.wotonomy.access.EOAdaptorChannel#describeResults() */ public NSArray describeResults() { if (_resultSet == null || !_fetchInProgress) throw new EOGeneralAdaptorException("Cannot describe results without a result set."); if (_resultAttributes == null) { try { ResultSetMetaData _rsmeta = _resultSet.getMetaData(); EOAttribute[] attarr = new EOAttribute[_rsmeta.getColumnCount()]; for (int i = 1; i <= attarr.length; i++) { EOAttribute a = new EOAttribute(); a.setName("Attribute " + (i)); a.setColumnName(_rsmeta.getColumnName(i)); a.setClassName(_rsmeta.getColumnClassName(i)); a.setExternalType(_rsmeta.getColumnTypeName(i)); a.setPrecision(_rsmeta.getPrecision(i)); a.setScale(_rsmeta.getScale(i)); a.setAllowsNull(_rsmeta.isNullable(i) == ResultSetMetaData.columnNullable); a.setWidth(_rsmeta.getColumnDisplaySize(i)); a.setReadOnly(_rsmeta.isReadOnly(i)); attarr[i - 1] = a; } _resultAttributes = new NSArray(attarr); } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to get the result set metadata.", ex); } } return _resultAttributes; } /* * Deletes from the database the rows described by the qualifier, in the * specified entity. * * @see net.wotonomy.access.EOAdaptorChannel#deleteRowsDescribedByQualifier(net. * wotonomy.control.EOQualifier, net.wotonomy.access.EOEntity) */ public int deleteRowsDescribedByQualifier(EOQualifier q, EOEntity entity) { EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); exp.prepareDeleteExpressionForQualifier(q); evaluateExpression(exp); return _resultCount; } /* * Creates a java.sql.Statement object and executes it. If there is an open * transaction, the statement is executed inside it; otherwise a transaction is * started, the statement executed, and the transaction is committed. * * @see * net.wotonomy.access.EOAdaptorChannel#evaluateExpression(net.wotonomy.access. * EOSQLExpression) */ public void evaluateExpression(EOSQLExpression sql) { if (!isOpen()) throw new EOGeneralAdaptorException("Attempt to evaluate expression without opening the channel first."); try { _statement = _context().connection().createStatement(); } catch (SQLException ex) { throw new JDBCAdaptorException("Cannot create java.sql.Statement", ex); } _resultSet = null; boolean isQuery = false; String text = sql.statement(); try { // run an executeUpdate with these prefixes if (text.startsWith("INSERT") || text.startsWith("DELETE") || text.startsWith("UPDATE")) { conditionalBeginTransaction(); _resultCount = _statement.executeUpdate(text); conditionalCommitTransaction(); return; } else if (text.startsWith("SELECT")) { // run an executeQuery with SELECT if (_resultCount > 0) _statement.setMaxRows(_resultCount); _resultSet = _statement.executeQuery(text); _fetchInProgress = true; return; } else { // just plain execute conditionalBeginTransaction(); isQuery = _statement.execute(text); } } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to execute expression '" + text + "'", ex); } try { if (isQuery) { if (_resultCount > 0) _statement.setMaxRows(_resultCount); _resultSet = _statement.getResultSet(); } else { _resultCount = _statement.getUpdateCount(); conditionalCommitTransaction(); } } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to get the result set.", ex); } } /* * Executes a stored procedure with the specified parameters. Any results that * the procedure returns should be obtained by calling * returnValuesForLastStoredProcedureInvocation. * * @see * net.wotonomy.access.EOAdaptorChannel#executeStoredProcedure(net.wotonomy. * access.EOStoredProcedure, net.wotonomy.foundation.NSDictionary) */ public void executeStoredProcedure(EOStoredProcedure proc, NSDictionary values) { if (!isOpen()) throw new EOGeneralAdaptorException("Attempt to execute a stored procedure on a closed channel."); conditionalBeginTransaction(); try { // Assemble the procedure call StringBuffer buf = new StringBuffer("{ call "); buf.append(proc.externalName()); NSArray args = proc.arguments(); if (args != null && args.count() > 0) { buf.append("["); for (int i = 0; i < args.count(); i++) { EOAttribute a = (EOAttribute) args.objectAtIndex(i); if (a.parameterDirection() != EOAttribute.OutParameter) { buf.append('?'); buf.append(", "); } } buf.delete(buf.length() - 2, buf.length()); buf.append("]"); } buf.append(" }"); // get the callable statement CallableStatement sp = _context().connection().prepareCall(buf.toString()); if (args != null && args.count() > 0) { int pos = 1; // set the in and inOut parameters for (int i = 0; i < args.count(); i++) { EOAttribute a = (EOAttribute) args.objectAtIndex(i); if (a.parameterDirection() != EOAttribute.OutParameter) { Object val = values.objectForKey(a.name()); if (val == NSKeyValueCoding.NullValue) sp.setNull(pos, 0); // TODO: check sql type if (val instanceof String) sp.setString(pos, (String) val); else if (val instanceof BigDecimal) sp.setBigDecimal(pos, (BigDecimal) val); else if (val instanceof NSTimestamp) sp.setTimestamp(pos, (NSTimestamp) val); else if (val instanceof NSData) sp.setBytes(pos, ((NSData) val).bytes()); else if (val instanceof Integer) sp.setInt(pos, ((Integer) val).intValue()); else if (val instanceof Long) sp.setLong(pos, ((Long) val).longValue()); else sp.setObject(pos, val); pos++; } } } // run the procedure sp.execute(); // get the return values if (args != null && args.count() > 0) { int pos = 1; NSMutableDictionary retvals = new NSMutableDictionary(); for (int i = 0; i < args.count(); i++) { EOAttribute a = (EOAttribute) args.objectAtIndex(i); if (a.parameterDirection() != EOAttribute.InParameter) { Object val = sp.getObject(pos); if (val == null) retvals.setObjectForKey(NSKeyValueCoding.NullValue, a.name()); else if (val instanceof Blob) { try { retvals.setObjectForKey(new NSData(((Blob) val).getBinaryStream(), 1024), a.name()); } catch (java.io.IOException ex) { // what should I do here? retvals.setObjectForKey(NSData.EmptyData, a.name()); } } else retvals.setObjectForKey(val, a.name()); pos++; } } _spReturnValues = retvals; } } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to execute stored procedure.", ex); } conditionalCommitTransaction(); } /* * Fetches one row from the database * * @see net.wotonomy.access.EOAdaptorChannel#fetchRow() */ public NSMutableDictionary fetchRow() { if (_resultSet == null) { return null; } if (attributesToFetch() == null) throw new EOGeneralAdaptorException("Attempt to fetchRow without setting attributes to fetch first."); try { // If the current result set ends, there may be another one if (!_resultSet.next()) { _resultSet.close(); _resultAttributes = null; _fetchInProgress = _statement.getMoreResults(); if (_fetchInProgress) _resultSet = _statement.getResultSet(); return null; } } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to fetch row.", ex); } // Assemble the dictionary NSMutableDictionary dict = new NSMutableDictionary(attributesToFetch().count()); try { for (int i = 0; i < attributesToFetch().count(); i++) { EOAttribute a = (EOAttribute) attributesToFetch().objectAtIndex(i); Object o = _resultSet.getObject(i + 1); if (o == null) o = NSKeyValueCoding.NullValue; dict.setObjectForKey(o, a.name()); } } catch (SQLException ex) { throw new JDBCAdaptorException("While trying to create row.", ex); } return dict; } /* * Inserts a row into a table in the database. * * @see net.wotonomy.access.EOAdaptorChannel#insertRow(net.wotonomy.foundation. * NSDictionary, net.wotonomy.access.EOEntity) */ public void insertRow(NSDictionary row, EOEntity entity) { EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); exp.prepareInsertExpressionWithRow(row); evaluateExpression(exp); } /* * Indicates if a fetch is in progress; that is, if a SELECT statement was * executed and there are still rows to be fetched. * * @see net.wotonomy.access.EOAdaptorChannel#isFetchInProgress() */ public boolean isFetchInProgress() { return _fetchInProgress; } /* * Indicates if the channel is open. * * @see net.wotonomy.access.EOAdaptorChannel#isOpen() */ public boolean isOpen() { boolean open = (_context().connection() != null); try { open = open || !_context().connection().isClosed(); } catch (SQLException ex) { open = false; } return open; } /* * Opens the channel. If the adaptor context has not yet made a connection to * the database, this forces the context to connect. * * @see net.wotonomy.access.EOAdaptorChannel#openChannel() */ public void openChannel() { try { if (_context().connection() == null || _context().connection().isClosed()) _context().connect(); } catch (SQLException ex) { throw new JDBCAdaptorException("Cannot open connection to database.", ex); } } /* * Returns the values obtained from the last stored procedure executed. * * @see net.wotonomy.access.EOAdaptorChannel# * returnValuesForLastStoredProcedureInvocation() */ public NSDictionary returnValuesForLastStoredProcedureInvocation() { return _spReturnValues; } /* * Creates a SELECT expression and executes it. If the attribute array is null, * then the result's metadata is used to dynamically create an array of * attributes. * * @see * net.wotonomy.access.EOAdaptorChannel#selectAttributes(net.wotonomy.foundation * .NSArray, net.wotonomy.control.EOFetchSpecification, boolean, * net.wotonomy.access.EOEntity) */ public void selectAttributes(NSArray atts, EOFetchSpecification fspec, boolean lock, EOEntity entity) { _resultAttributes = atts; EOSQLExpression expr = adaptorContext().adaptor().expressionFactory().createExpression(entity); _fetchInProgress = true; expr.prepareSelectExpressionWithAttributes(atts, lock, fspec); // for now we store the fetch limit here if (fspec != null) _resultCount = fspec.fetchLimit(); evaluateExpression(expr); } /* * Creates and executes an UPDATE statement. * * @see * net.wotonomy.access.EOAdaptorChannel#updateValuesInRowsDescribedByQualifier( * net.wotonomy.foundation.NSDictionary, net.wotonomy.control.EOQualifier, * net.wotonomy.access.EOEntity) */ public int updateValuesInRowsDescribedByQualifier(NSDictionary row, EOQualifier q, EOEntity entity) { EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity); exp.prepareUpdateExpressionWithRow(row, q); evaluateExpression(exp); return _resultCount; } protected void conditionalBeginTransaction() { _transactionWasOpen = adaptorContext().hasOpenTransaction(); if (!_transactionWasOpen) adaptorContext().beginTransaction(); } protected void conditionalCommitTransaction() { if (!_transactionWasOpen) adaptorContext().commitTransaction(); _transactionWasOpen = false; } } /* * $Log$ Revision 1.2 2006/02/18 22:59:22 cgruber make it compile with maven * dependencies and add a cvsignore. * * Revision 1.1 2006/02/16 13:22:23 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.3 2003/08/14 02:15:11 chochos added lots of comments * * Revision 1.2 2003/08/13 20:45:20 chochos small fixes in evaluateExpression, * which has been successfully tested with a SELECT statement. * * Revision 1.1 2003/08/13 20:12:48 chochos a subclass of EOAdaptorChannel to be * used with JDBC. * */