View Javadoc

1   /*
2    Wotonomy: OpenStep design patterns for pure Java applications.
3    Copyright (C) 2001 Michael Powers
4   
5    This library is free software; you can redistribute it and/or
6    modify it under the terms of the GNU Lesser General Public
7    License as published by the Free Software Foundation; either
8    version 2.1 of the License, or (at your option) any later version.
9   
10   This library is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13   Lesser General Public License for more details.
14  
15   You should have received a copy of the GNU Lesser General Public
16   License along with this library; if not, see http://www.gnu.org
17   */
18  package net.wotonomy.jdbcadaptor;
19  
20  import java.math.BigDecimal;
21  import java.sql.Blob;
22  import java.sql.CallableStatement;
23  import java.sql.ResultSet;
24  import java.sql.ResultSetMetaData;
25  import java.sql.SQLException;
26  import java.sql.Statement;
27  
28  import net.wotonomy.access.EOAdaptorChannel;
29  import net.wotonomy.access.EOAttribute;
30  import net.wotonomy.access.EOEntity;
31  import net.wotonomy.access.EOGeneralAdaptorException;
32  import net.wotonomy.access.EOSQLExpression;
33  import net.wotonomy.access.EOStoredProcedure;
34  import net.wotonomy.control.EOFetchSpecification;
35  import net.wotonomy.control.EOQualifier;
36  import net.wotonomy.foundation.NSArray;
37  import net.wotonomy.foundation.NSData;
38  import net.wotonomy.foundation.NSDictionary;
39  import net.wotonomy.foundation.NSKeyValueCoding;
40  import net.wotonomy.foundation.NSMutableDictionary;
41  import net.wotonomy.foundation.NSTimestamp;
42  
43  /***
44  * Concrete implementation of EOAdaptorChannel for use with JDBC.
45  *
46  * @author ezamudio@nasoft.com
47  * @author $Author: cgruber $
48  * @version $Revision: 903 $
49  */
50  public class JDBCChannel extends EOAdaptorChannel {
51  
52  	protected boolean _fetchInProgress;
53  	protected ResultSet _resultSet;
54  	protected Statement _statement;
55  	protected NSArray _attsToFetch;
56  	protected NSArray _resultAttributes;
57  	protected boolean _transactionWasOpen;
58  	protected NSDictionary _spReturnValues;
59  	protected int _resultCount;
60  
61  	/***
62  	 * Creates a new JDBCChannel.
63  	 * @param context The JDBCContext this channel belongs to.
64  	 */
65  	public JDBCChannel(JDBCContext context) {
66  		super(context);
67  	}
68  
69  	protected JDBCContext _context() {
70  		return (JDBCContext)adaptorContext();
71  	}
72  
73  	/* Sets the attributes to be fetched from the database.
74  	 * @see net.wotonomy.access.EOAdaptorChannel#setAttributesToFetch(net.wotonomy.foundation.NSArray)
75  	 */
76  	public void setAttributesToFetch(NSArray atts) {
77  		_attsToFetch = atts;
78  	}
79  
80  	/* Returns an array with the attributes that will be fetched.
81  	 * @see net.wotonomy.access.EOAdaptorChannel#attributesToFetch()
82  	 */
83  	public NSArray attributesToFetch() {
84  		return _attsToFetch;
85  	}
86  
87  	/* Cancels the fetch, rolling back the transaction.
88  	 * @see net.wotonomy.access.EOAdaptorChannel#cancelFetch()
89  	 */
90  	public void cancelFetch() {
91  		if (_statement == null || _resultSet == null)
92  			return;
93  		try {
94  			_resultSet.close();
95  			_statement.cancel();
96  		} catch (SQLException ex) {
97  			throw new JDBCAdaptorException("Cannot cancel fetch in database.", ex);
98  		}
99  	}
100 
101 	/* Closes the jdbc channel.
102 	 * @see net.wotonomy.access.EOAdaptorChannel#closeChannel()
103 	 */
104 	public void closeChannel() {
105 		if (_statement == null)
106 			return;
107 		try {
108 			_statement.close();
109 		} catch (SQLException ex) {
110 			throw new JDBCAdaptorException("While trying to close the channel.", ex);
111 		}
112 	}
113 
114 	/* If the fetch was done with an array of EOAttributes, returns
115 	 * that same array; otherwise it creates an array of EOAttributes
116 	 * based on the column names that will be fetched.
117 	 * @see net.wotonomy.access.EOAdaptorChannel#describeResults()
118 	 */
119 	public NSArray describeResults() {
120 		if (_resultSet == null || !_fetchInProgress)
121 			throw new EOGeneralAdaptorException("Cannot describe results without a result set.");
122 		if (_resultAttributes == null) {
123 			try {
124 				ResultSetMetaData _rsmeta = _resultSet.getMetaData();
125 				EOAttribute[] attarr = new EOAttribute[_rsmeta.getColumnCount()];
126 				for (int i = 1; i <= attarr.length; i++) {
127 					EOAttribute a = new EOAttribute();
128 					a.setName("Attribute " + (i));
129 					a.setColumnName(_rsmeta.getColumnName(i));
130 					a.setClassName(_rsmeta.getColumnClassName(i));
131 					a.setExternalType(_rsmeta.getColumnTypeName(i));
132 					a.setPrecision(_rsmeta.getPrecision(i));
133 					a.setScale(_rsmeta.getScale(i));
134 					a.setAllowsNull(_rsmeta.isNullable(i) == ResultSetMetaData.columnNullable);
135 					a.setWidth(_rsmeta.getColumnDisplaySize(i));
136 					a.setReadOnly(_rsmeta.isReadOnly(i));
137 					attarr[i-1] = a;
138 				}
139 				_resultAttributes = new NSArray(attarr);
140 			} catch (SQLException ex) {
141 				throw new JDBCAdaptorException("While trying to get the result set metadata.", ex);
142 			}
143 		}
144 		return _resultAttributes;
145 	}
146 
147 	/* Deletes from the database the rows described by the qualifier,
148 	 * in the specified entity.
149 	 * @see net.wotonomy.access.EOAdaptorChannel#deleteRowsDescribedByQualifier(net.wotonomy.control.EOQualifier, net.wotonomy.access.EOEntity)
150 	 */
151 	public int deleteRowsDescribedByQualifier(EOQualifier q, EOEntity entity) {
152 		EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity);
153 		exp.prepareDeleteExpressionForQualifier(q);
154 		evaluateExpression(exp);
155 		return _resultCount;
156 	}
157 
158 	/* Creates a java.sql.Statement object and executes it.
159 	 * If there is an open transaction, the statement is executed inside it;
160 	 * otherwise a transaction is started, the statement executed, and
161 	 * the transaction is committed.
162 	 * @see net.wotonomy.access.EOAdaptorChannel#evaluateExpression(net.wotonomy.access.EOSQLExpression)
163 	 */
164 	public void evaluateExpression(EOSQLExpression sql) {
165 		if (!isOpen())
166 			throw new EOGeneralAdaptorException("Attempt to evaluate expression without opening the channel first.");
167 		try {
168 			_statement = _context().connection().createStatement();
169 		} catch (SQLException ex) {
170 			throw new JDBCAdaptorException("Cannot create java.sql.Statement", ex);
171 		}
172 		_resultSet = null;
173 		boolean isQuery = false;
174 		String text = sql.statement();
175 		try {
176 			//run an executeUpdate with these prefixes
177 			if (text.startsWith("INSERT") || text.startsWith("DELETE") || text.startsWith("UPDATE")) {
178 				conditionalBeginTransaction();
179 				_resultCount = _statement.executeUpdate(text);
180 				conditionalCommitTransaction();
181 				return;
182 			} else if (text.startsWith("SELECT")) {
183 				//run an executeQuery with SELECT
184 				if (_resultCount > 0)
185 					_statement.setMaxRows(_resultCount);
186 				_resultSet = _statement.executeQuery(text);
187 				_fetchInProgress = true;
188 				return;
189 			} else { //just plain execute
190 				conditionalBeginTransaction();
191 				isQuery = _statement.execute(text);
192 			}
193 		} catch (SQLException ex) {
194 			throw new JDBCAdaptorException("While trying to execute expression '" + text + "'", ex);
195 		}
196 		try {
197 			if (isQuery) {
198 				if (_resultCount > 0)
199 					_statement.setMaxRows(_resultCount);
200 				_resultSet = _statement.getResultSet();
201 			} else {
202 				_resultCount = _statement.getUpdateCount();
203 				conditionalCommitTransaction();
204 			}
205 		} catch (SQLException ex) {
206 			throw new JDBCAdaptorException("While trying to get the result set.", ex);
207 		}
208 	}
209 
210 	/* Executes a stored procedure with the specified parameters.
211 	 * Any results that the procedure returns should be obtained
212 	 * by calling returnValuesForLastStoredProcedureInvocation.
213 	 * @see net.wotonomy.access.EOAdaptorChannel#executeStoredProcedure(net.wotonomy.access.EOStoredProcedure, net.wotonomy.foundation.NSDictionary)
214 	 */
215 	public void executeStoredProcedure(
216 			EOStoredProcedure proc, NSDictionary values) {
217 		if (!isOpen())
218 			throw new EOGeneralAdaptorException("Attempt to execute a stored procedure on a closed channel.");
219 		conditionalBeginTransaction();
220 		try {
221 			//Assemble the procedure call
222 			StringBuffer buf = new StringBuffer("{ call ");
223 			buf.append(proc.externalName());
224 			NSArray args = proc.arguments();
225 			if (args != null && args.count() > 0) {
226 				buf.append("[");
227 				for (int i = 0; i < args.count(); i++) {
228 					EOAttribute a = (EOAttribute)args.objectAtIndex(i);
229 					if (a.parameterDirection() != EOAttribute.OutParameter) {
230 						buf.append('?');
231 						buf.append(", ");
232 					}
233 				}
234 				buf.delete(buf.length()-2, buf.length());
235 				buf.append("]");
236 			}
237 			buf.append(" }");
238 			//get the callable statement
239 			CallableStatement sp = _context().connection().prepareCall(buf.toString());
240 			if (args != null && args.count() > 0) {
241 				int pos = 1;
242 				//set the in and inOut parameters
243 				for (int i = 0; i < args.count(); i++) {
244 					EOAttribute a = (EOAttribute)args.objectAtIndex(i);
245 					if (a.parameterDirection() != EOAttribute.OutParameter) {
246 						Object val = values.objectForKey(a.name());
247 						if (val == NSKeyValueCoding.NullValue)
248 							sp.setNull(pos, 0); //TODO: check sql type
249 						if (val instanceof String)
250 							sp.setString(pos, (String)val);
251 						else if (val instanceof BigDecimal)
252 							sp.setBigDecimal(pos, (BigDecimal)val);
253 						else if (val instanceof NSTimestamp)
254 							sp.setTimestamp(pos, (NSTimestamp)val);
255 						else if (val instanceof NSData)
256 							sp.setBytes(pos, ((NSData)val).bytes());
257 						else if (val instanceof Integer)
258 							sp.setInt(pos, ((Integer)val).intValue());
259 						else if (val instanceof Long)
260 							sp.setLong(pos, ((Long)val).longValue());
261 						else
262 							sp.setObject(pos, val);
263 						pos++;
264 					}
265 				}
266 			}
267 			//run the procedure
268 			sp.execute();
269 			//get the return values
270 			if (args != null && args.count() > 0) {
271 				int pos = 1;
272 				NSMutableDictionary retvals = new NSMutableDictionary();
273 				for (int i = 0; i < args.count(); i++) {
274 					EOAttribute a = (EOAttribute)args.objectAtIndex(i);
275 					if (a.parameterDirection() != EOAttribute.InParameter) {
276 						Object val = sp.getObject(pos);
277 						if (val == null)
278 							retvals.setObjectForKey(NSKeyValueCoding.NullValue, a.name());
279 						else if (val instanceof Blob) {
280 							try {
281 								retvals.setObjectForKey(new NSData(((Blob)val).getBinaryStream(), 1024), a.name());
282 							} catch (java.io.IOException ex) {
283 								//what should I do here?
284 								retvals.setObjectForKey(NSData.EmptyData, a.name());
285 							}
286 						} else
287 							retvals.setObjectForKey(val, a.name());
288 						pos++;
289 					}
290 				}
291 				_spReturnValues = retvals;
292 			}
293 		} catch (SQLException ex) {
294 			throw new JDBCAdaptorException("While trying to execute stored procedure.", ex);
295 		}
296 		conditionalCommitTransaction();
297 	}
298 
299 	/* Fetches one row from the database
300 	 * @see net.wotonomy.access.EOAdaptorChannel#fetchRow()
301 	 */
302 	public NSMutableDictionary fetchRow() {
303 		if (_resultSet == null) {
304 			return null;
305 		}
306 		if (attributesToFetch() == null)
307 			throw new EOGeneralAdaptorException("Attempt to fetchRow without setting attributes to fetch first.");
308 		try {
309 			//If the current result set ends, there may be another one
310 			if (!_resultSet.next()) {
311 				_resultSet.close();
312 				_resultAttributes = null;
313 				_fetchInProgress = _statement.getMoreResults();
314 				if (_fetchInProgress)
315 					_resultSet = _statement.getResultSet();
316 				return null;
317 			}
318 		} catch (SQLException ex) {
319 			throw new JDBCAdaptorException("While trying to fetch row.", ex);
320 		}
321 
322 		//Assemble the dictionary
323 		NSMutableDictionary dict = new NSMutableDictionary(attributesToFetch().count());
324 		try {
325 			for (int i = 0; i < attributesToFetch().count(); i++) {
326 				EOAttribute a = (EOAttribute)attributesToFetch().objectAtIndex(i);
327 				Object o = _resultSet.getObject(i+1);
328 				if (o == null)
329 					o = NSKeyValueCoding.NullValue;
330 				dict.setObjectForKey(o, a.name());
331 			}
332 		} catch (SQLException ex) {
333 			throw new JDBCAdaptorException("While trying to create row.", ex);
334 		}
335 		return dict;
336 	}
337 
338 	/* Inserts a row into a table in the database.
339 	 * @see net.wotonomy.access.EOAdaptorChannel#insertRow(net.wotonomy.foundation.NSDictionary, net.wotonomy.access.EOEntity)
340 	 */
341 	public void insertRow(NSDictionary row, EOEntity entity) {
342 		EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity);
343 		exp.prepareInsertExpressionWithRow(row);
344 		evaluateExpression(exp);
345 	}
346 
347 	/* Indicates if a fetch is in progress; that is, if a SELECT statement
348 	 * was executed and there are still rows to be fetched.
349 	 * @see net.wotonomy.access.EOAdaptorChannel#isFetchInProgress()
350 	 */
351 	public boolean isFetchInProgress() {
352 		return _fetchInProgress;
353 	}
354 
355 	/* Indicates if the channel is open.
356 	 * @see net.wotonomy.access.EOAdaptorChannel#isOpen()
357 	 */
358 	public boolean isOpen() {
359 		boolean open = (_context().connection() != null);
360 		try {
361 			open = open || !_context().connection().isClosed();
362 		} catch (SQLException ex) {
363 			open = false;
364 		}
365 		return open;
366 	}
367 
368 	/* Opens the channel. If the adaptor context has not yet made
369 	 * a connection to the database, this forces the context to
370 	 * connect.
371 	 * @see net.wotonomy.access.EOAdaptorChannel#openChannel()
372 	 */
373 	public void openChannel() {
374 		try {
375 			if (_context().connection() == null || _context().connection().isClosed())
376 				_context().connect();
377 		} catch (SQLException ex) {
378 			throw new JDBCAdaptorException("Cannot open connection to database.", ex);
379 		}
380 	}
381 
382 	/* Returns the values obtained from the last stored procedure executed.
383 	 * @see net.wotonomy.access.EOAdaptorChannel#returnValuesForLastStoredProcedureInvocation()
384 	 */
385 	public NSDictionary returnValuesForLastStoredProcedureInvocation() {
386 		return _spReturnValues;
387 	}
388 
389 	/* Creates a SELECT expression and executes it. If the attribute array is null,
390 	 * then the result's metadata is used to dynamically create an array
391 	 * of attributes.
392 	 * @see net.wotonomy.access.EOAdaptorChannel#selectAttributes(net.wotonomy.foundation.NSArray, net.wotonomy.control.EOFetchSpecification, boolean, net.wotonomy.access.EOEntity)
393 	 */
394 	public void selectAttributes(
395 			NSArray atts, EOFetchSpecification fspec,
396 			boolean lock, EOEntity entity) {
397 		_resultAttributes = atts;
398 		EOSQLExpression expr = adaptorContext().adaptor().expressionFactory().createExpression(entity);
399 		_fetchInProgress = true;
400 		expr.prepareSelectExpressionWithAttributes(atts, lock, fspec);
401 		//for now we store the fetch limit here
402 		if (fspec != null)
403 			_resultCount = fspec.fetchLimit();
404 		evaluateExpression(expr);
405 	}
406 
407 	/* Creates and executes an UPDATE statement.
408 	 * @see net.wotonomy.access.EOAdaptorChannel#updateValuesInRowsDescribedByQualifier(net.wotonomy.foundation.NSDictionary, net.wotonomy.control.EOQualifier, net.wotonomy.access.EOEntity)
409 	 */
410 	public int updateValuesInRowsDescribedByQualifier(
411 			NSDictionary row, EOQualifier q, EOEntity entity) {
412 		EOSQLExpression exp = adaptorContext().adaptor().expressionFactory().createExpression(entity);
413 		exp.prepareUpdateExpressionWithRow(row, q);
414 		evaluateExpression(exp);
415 		return _resultCount;
416 	}
417 
418 	protected void conditionalBeginTransaction() {
419 		_transactionWasOpen = adaptorContext().hasOpenTransaction();
420 		if (!_transactionWasOpen)
421 			adaptorContext().beginTransaction();
422 	}
423 
424 	protected void conditionalCommitTransaction() {
425 		if (!_transactionWasOpen)
426 			adaptorContext().commitTransaction();
427 		_transactionWasOpen = false;
428 	}
429 
430 }
431 /*
432 * $Log$
433 * Revision 1.2  2006/02/18 22:59:22  cgruber
434 * make it compile with maven dependencies and add a cvsignore.
435 *
436 * Revision 1.1  2006/02/16 13:22:23  cgruber
437 * Check in all sources in eclipse-friendly maven-enabled packages.
438 *
439 * Revision 1.3  2003/08/14 02:15:11  chochos
440 * added lots of comments
441 *
442 * Revision 1.2  2003/08/13 20:45:20  chochos
443 * small fixes in evaluateExpression, which has been successfully tested with a SELECT statement.
444 *
445 * Revision 1.1  2003/08/13 20:12:48  chochos
446 * a subclass of EOAdaptorChannel to be used with JDBC.
447 *
448 */
449