001: /*
002: * Licensed to the Apache Software Foundation (ASF) under one or more
003: * contributor license agreements. See the NOTICE file distributed with
004: * this work for additional information regarding copyright ownership.
005: * The ASF licenses this file to You under the Apache License, Version 2.0
006: * (the "License"); you may not use this file except in compliance with
007: * the License. You may obtain a copy of the License at
008: *
009: * http://www.apache.org/licenses/LICENSE-2.0
010: *
011: * Unless required by applicable law or agreed to in writing, software
012: * distributed under the License is distributed on an "AS IS" BASIS,
013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014: * See the License for the specific language governing permissions and
015: * limitations under the License.
016: *
017: * $Header:$
018: */
019:
020: package org.apache.beehive.controls.system.jdbc.parser;
021:
022: import org.apache.beehive.controls.api.ControlException;
023: import org.apache.beehive.controls.api.context.ControlBeanContext;
024: import org.apache.beehive.controls.system.jdbc.JdbcControl;
025: import org.apache.beehive.controls.system.jdbc.TypeMappingsFactory;
026:
027: import javax.sql.RowSet;
028: import java.io.Serializable;
029: import java.lang.reflect.Method;
030: import java.sql.CallableStatement;
031: import java.sql.Connection;
032: import java.sql.DatabaseMetaData;
033: import java.sql.PreparedStatement;
034: import java.sql.SQLException;
035: import java.sql.Statement;
036: import java.sql.Types;
037: import java.util.Calendar;
038:
039: /**
040: * Represents a fully parsed SQL statement. SqlStatements can be used to generated a java.sql.PreparedStatement.
041: */
042: public final class SqlStatement extends SqlFragmentContainer implements
043: Serializable {
044:
045: private static final TypeMappingsFactory _tmf = TypeMappingsFactory
046: .getInstance();
047: private boolean _callableStatement = false;
048: private boolean _cacheableStatement = true;
049:
050: //
051: // set from SQL annotation element values
052: //
053: private boolean _batchUpdate;
054: private boolean _getGeneratedKeys;
055: private String[] _genKeyColumnNames;
056: private int _fetchSize;
057: private int _maxArray;
058: private int _maxRows;
059: private int[] _genKeyColumnIndexes;
060: private JdbcControl.ScrollType _scrollType;
061: private JdbcControl.FetchDirection _fetchDirection;
062: private JdbcControl.HoldabilityType _holdability;
063:
064: /**
065: * Create a new SqlStatement.
066: */
067: SqlStatement() {
068: super ();
069: }
070:
071: /**
072: * Append a SqlFragment to the end of this statement.
073: *
074: * @param frag SqlFragment to append.
075: */
076: void addChild(SqlFragment frag) {
077: super .addChild(frag);
078:
079: if (frag.isDynamicFragment()) {
080: _cacheableStatement = false;
081: }
082: }
083:
084: /**
085: * Can the PreparedStatement generated by this class be cached?
086: *
087: * @return true if this statement can be cached by the SqlParser.
088: */
089: boolean isCacheable() {
090: return _cacheableStatement;
091: }
092:
093: /**
094: * Does this statement generate a callable or prepared statement?
095: *
096: * @return true if this statement generates callable statement.
097: */
098: public boolean isCallableStatement() {
099: return _callableStatement;
100: }
101:
102: /**
103: * Does this statement do a batch update?
104: *
105: * @return true if this statement should be executed as a batch update.
106: */
107: public boolean isBatchUpdate() {
108: return _batchUpdate;
109: }
110:
111: /**
112: * Does this statement return generatedKeys?
113: *
114: * @return true if getGeneratedKeys set to true.
115: */
116: public boolean getsGeneratedKeys() {
117: return _getGeneratedKeys;
118: }
119:
120: /**
121: * Generates the PreparedStatement the SQL statement.
122: *
123: * @param context ControlBeanContext instance.
124: * @param connection Connection to database.
125: * @param calendar Calendar instance which can be used to resolve date/time values.
126: * @param method Method the SQL is associated with.
127: * @param arguments Method parameters.
128: * @return The PreparedStatement generated by this statement.
129: * @throws SQLException If PreparedStatement cannot be created.
130: */
131: public PreparedStatement createPreparedStatement(
132: ControlBeanContext context, Connection connection,
133: Calendar calendar, Method method, Object[] arguments)
134: throws SQLException {
135:
136: PreparedStatement preparedStatement = null;
137: loadSQLAnnotationStatmentOptions(context, method);
138: checkJdbcSupport(connection.getMetaData());
139:
140: _callableStatement = setCallableStatement(arguments);
141:
142: try {
143: final String sql = getPreparedStatementText(context,
144: method, arguments);
145:
146: //
147: // is this a request for generatedKeys ?
148: //
149: if (_getGeneratedKeys) {
150:
151: if (_callableStatement) {
152: throw new ControlException(
153: "getGeneratedKeys not supported for CallableStatements");
154: }
155:
156: if (_genKeyColumnNames.length > 0) {
157: preparedStatement = connection.prepareStatement(
158: sql, _genKeyColumnNames);
159: } else if (_genKeyColumnIndexes.length > 0) {
160: preparedStatement = connection.prepareStatement(
161: sql, _genKeyColumnIndexes);
162: } else {
163: preparedStatement = connection.prepareStatement(
164: sql, Statement.RETURN_GENERATED_KEYS);
165: }
166:
167: } else {
168:
169: if (_holdability == JdbcControl.HoldabilityType.DRIVER_DEFAULT) {
170: if (_scrollType == JdbcControl.ScrollType.DRIVER_DEFAULT) {
171: preparedStatement = (_callableStatement) ? connection
172: .prepareCall(sql)
173: : connection.prepareStatement(sql);
174: } else {
175: preparedStatement = (_callableStatement) ? connection
176: .prepareCall(sql,
177: _scrollType.getType(),
178: _scrollType
179: .getConcurrencyType())
180: : connection.prepareStatement(sql,
181: _scrollType.getType(),
182: _scrollType
183: .getConcurrencyType());
184: }
185: } else {
186: preparedStatement = (_callableStatement) ? connection
187: .prepareCall(sql, _scrollType.getType(),
188: _scrollType.getConcurrencyType(),
189: _holdability.getHoldability())
190: : connection.prepareStatement(sql,
191: _scrollType.getType(), _scrollType
192: .getConcurrencyType(),
193: _holdability.getHoldability());
194: }
195: }
196:
197: //
198: // If the method argument is of type SQLParameter, treat this statement as a CallableStatement,
199: //
200: if (_callableStatement) {
201: for (SqlFragment sf : _children) {
202: if (sf.hasParamValue()) {
203: throw new ControlException(
204: "Cannot use parameter substution and SQLParameter array in the same method.");
205: }
206: }
207: JdbcControl.SQLParameter[] params = (JdbcControl.SQLParameter[]) arguments[0];
208: if (params == null) {
209: return preparedStatement;
210: }
211: for (int i = 0; i < params.length; i++) {
212: JdbcControl.SQLParameter p = params[i];
213: if (p.dir != JdbcControl.SQLParameter.OUT) {
214: Object value = params[i].value;
215: setPreparedStatementParameter(
216: preparedStatement, i + 1, value,
217: params[i].type, calendar);
218: }
219:
220: if (p.dir != JdbcControl.SQLParameter.IN) {
221: ((CallableStatement) preparedStatement)
222: .registerOutParameter(i + 1,
223: params[i].type);
224: }
225: }
226:
227: //
228: // special handling for batch updates
229: //
230: } else if (_batchUpdate) {
231: doBatchUpdate(preparedStatement, arguments, calendar);
232:
233: //
234: // standard case, not a batch or callable
235: //
236: } else {
237: int pIndex = 1;
238: for (SqlFragment sf : _children) {
239: if (sf.hasParamValue()) {
240: Object values[] = sf.getParameterValues(
241: context, method, arguments);
242: for (Object value : values) {
243: setPreparedStatementParameter(
244: preparedStatement, pIndex++, value,
245: sf.getParamSqlDataType(), calendar);
246: }
247: }
248: }
249: }
250: } catch (SQLException e) {
251: if (preparedStatement != null)
252: preparedStatement.close();
253: throw e;
254: }
255:
256: preparedStatement.setFetchDirection(_fetchDirection
257: .getDirection());
258: preparedStatement.setFetchSize(_fetchSize);
259: preparedStatement.setMaxRows(computeMaxRows(method));
260:
261: return preparedStatement;
262: }
263:
264: /**
265: * Generates the PreparedStatement the SQL statement.
266: *
267: * @param context ControlBeanContext instance.
268: * @param connection Connection to database.
269: * @param method Method the SQL is associated with.
270: * @param arguments Method parameters.
271: * @return The PreparedStatement generated by this statement.
272: */
273: public String createPreparedStatementString(
274: ControlBeanContext context, Connection connection,
275: Method method, Object[] arguments) {
276:
277: final boolean callableStatement = setCallableStatement(arguments);
278: StringBuilder sqlString = new StringBuilder(
279: getPreparedStatementText(context, method, arguments));
280:
281: if (callableStatement) {
282: JdbcControl.SQLParameter[] params = (JdbcControl.SQLParameter[]) arguments[0];
283: if (params == null) {
284: return sqlString.toString();
285: }
286:
287: sqlString.append(" Params: {");
288: for (int i = 0; i < params.length; i++) {
289: if (i > 0) {
290: sqlString.append(params[i].value.toString());
291: }
292: }
293: sqlString.append("}");
294:
295: } else if (_batchUpdate) {
296: sqlString.append(" Params: batch update.");
297:
298: } else {
299: sqlString.append(" Params: {");
300: boolean first = true;
301: for (SqlFragment sf : _children) {
302: if (sf.hasParamValue()) {
303: Object values[] = sf.getParameterValues(context,
304: method, arguments);
305: for (Object value : values) {
306:
307: if (!first)
308: sqlString.append(", ");
309: else
310: first = false;
311: sqlString.append(value);
312: }
313: }
314: }
315: sqlString.append("}");
316: }
317: return sqlString.toString();
318: }
319:
320: // /////////////////////////////////////////////////// PRIVATE METHODS ///////////////////////////////////////////
321:
322: /**
323: * Sets the specified parameter in the prepared statement.
324: *
325: * @param ps A PreparedStatement.
326: * @param i index of parameter to set.
327: * @param value value of the parameter.
328: * @param sqlType SQL type of value.
329: * @param cal A calendar instance used to resolve date/time values.
330: * @throws SQLException If the parameter cannot be set.
331: */
332: private void setPreparedStatementParameter(PreparedStatement ps,
333: int i, Object value, int sqlType, Calendar cal)
334: throws SQLException {
335:
336: if (sqlType == Types.NULL) {
337: sqlType = _tmf.getSqlType(value);
338: }
339:
340: if (value == null) {
341: ps.setNull(i, Types.NULL == sqlType ? Types.VARCHAR
342: : sqlType);
343: return;
344: }
345:
346: switch (sqlType) {
347:
348: case Types.VARCHAR:
349: if (!(value instanceof String))
350: value = value.toString();
351: break;
352:
353: case Types.BOOLEAN:
354: if (value instanceof Boolean) {
355: ps.setBoolean(i, ((Boolean) value).booleanValue());
356: return;
357: }
358: break;
359:
360: case Types.TIMESTAMP:
361: if (value instanceof java.util.Calendar) {
362: Calendar calValue = (Calendar) value;
363:
364: // @todo: validate it is correct to comment out call to deprectated method
365: // if (cal == null) {
366: // /* NOTE: drivers are inconsistent in their handling of setTimestamp(i,date,cal)
367: // * so we won't use that, unless the user calls setCalendar().
368: // * I'm going with the theory that it makes sense to store
369: // * the time relative to the Calendar's timezone rather than
370: // * the system timezone otherwise, using a Calendar would be a no-op.
371: // */
372: // value = new java._sql.Timestamp(calValue.get(Calendar.YEAR) - 1900,
373: // calValue.get(Calendar.MONTH),
374: // calValue.get(Calendar.DATE),
375: // calValue.get(Calendar.HOUR_OF_DAY),
376: // calValue.get(Calendar.MINUTE),
377: // calValue.get(Calendar.SECOND),
378: // calValue.get(Calendar.MILLISECOND));
379: // } else {
380: value = new java.sql.Timestamp(calValue
381: .getTimeInMillis());
382: // }
383: } else if (java.util.Date.class.equals(value.getClass())) {
384: // some drivers don't like java.util.Date
385: value = new java.sql.Timestamp(((java.util.Date) value)
386: .getTime());
387: }
388:
389: if (value instanceof java.sql.Timestamp) {
390: if (cal == null)
391: ps.setTimestamp(i, (java.sql.Timestamp) value);
392: else
393: ps.setTimestamp(i, (java.sql.Timestamp) value, cal);
394: return;
395: }
396: break;
397:
398: case Types.DATE:
399: if (value instanceof java.util.Calendar) {
400: /* NOTE: see note above
401: Calendar cal = (Calendar)value;
402: value = new java._sql.Date(cal.getTimeInMillis());
403: ps.setDate(i, (java._sql.Date)value, cal);
404: return;
405: */
406: Calendar calValue = (Calendar) value;
407:
408: // @todo: validate that commenting out deprected method is correct behavior
409: // if (cal == null) {
410: // value = new java._sql.Date(calValue.get(Calendar.YEAR - 1900),
411: // calValue.get(Calendar.MONTH),
412: // calValue.get(Calendar.DATE));
413: // } else {
414: value = new java.sql.Date(calValue.getTimeInMillis());
415: // }
416: } else if (value.getClass() == java.util.Date.class) {
417: // some drivers don't like java.util.Date
418: value = new java.sql.Date(((java.util.Date) value)
419: .getTime());
420: }
421:
422: if (value instanceof java.sql.Date) {
423: if (cal == null) {
424: ps.setDate(i, (java.sql.Date) value);
425: } else {
426: ps.setDate(i, (java.sql.Date) value, cal);
427: }
428: return;
429: }
430: break;
431:
432: case Types.TIME:
433: if (value instanceof java.sql.Time) {
434: if (cal == null) {
435: ps.setTime(i, (java.sql.Time) value);
436: } else {
437: ps.setTime(i, (java.sql.Time) value, cal);
438: }
439: return;
440: }
441: break;
442: }
443:
444: if (sqlType == Types.NULL) {
445: ps.setObject(i, value);
446: } else {
447: ps.setObject(i, value, sqlType);
448: }
449: }
450:
451: /**
452: * Determine if this SQL will generate a callable or prepared statement.
453: *
454: * @param args The method's argument list which this SQL annotation was assocatied with.
455: * @return true if this statement will generated a CallableStatement
456: */
457: private boolean setCallableStatement(Object[] args) {
458:
459: // CallableStatement vs. PreparedStatement
460: if (args != null && args.length == 1 && args[0] != null) {
461: Class argClass = args[0].getClass();
462: if (argClass.isArray()
463: && JdbcControl.SQLParameter.class
464: .isAssignableFrom(argClass
465: .getComponentType())) {
466: return true;
467: }
468: }
469: return false;
470: }
471:
472: /**
473: * Build a prepared statement for a batch update.
474: *
475: * @param ps The PreparedStatement object.
476: * @param args The parameter list of the jdbccontrol method.
477: * @param cal A Calendar instance used to resolve date/time values.
478: * @throws SQLException If a batch update cannot be performed.
479: */
480: private void doBatchUpdate(PreparedStatement ps, Object[] args,
481: Calendar cal) throws SQLException {
482:
483: final int[] sqlTypes = new int[args.length];
484: final Object[] objArrays = new Object[args.length];
485:
486: // build an array of type values and object arrays
487: for (int i = 0; i < args.length; i++) {
488: sqlTypes[i] = _tmf.getSqlType(args[i].getClass()
489: .getComponentType());
490: objArrays[i] = TypeMappingsFactory.toObjectArray(args[i]);
491: }
492:
493: final int rowCount = ((Object[]) objArrays[0]).length;
494: for (int i = 0; i < rowCount; i++) {
495: for (int j = 0; j < args.length; j++) {
496: setPreparedStatementParameter(ps, j + 1,
497: ((Object[]) objArrays[j])[i], sqlTypes[j], cal);
498: }
499: ps.addBatch();
500: }
501: }
502:
503: /**
504: * Load element values from the SQL annotation which apply to Statements.
505: *
506: * @param context ControlBeanContext instance.
507: * @param method Annotated method.
508: */
509: private void loadSQLAnnotationStatmentOptions(
510: ControlBeanContext context, Method method) {
511:
512: final JdbcControl.SQL methodSQL = (JdbcControl.SQL) context
513: .getMethodPropertySet(method, JdbcControl.SQL.class);
514:
515: _batchUpdate = methodSQL.batchUpdate();
516: _getGeneratedKeys = methodSQL.getGeneratedKeys();
517: _genKeyColumnNames = methodSQL.generatedKeyColumnNames();
518: _genKeyColumnIndexes = methodSQL.generatedKeyColumnIndexes();
519: _scrollType = methodSQL.scrollableResultSet();
520: _fetchDirection = methodSQL.fetchDirection();
521: _fetchSize = methodSQL.fetchSize();
522: _maxRows = methodSQL.maxRows();
523: _maxArray = methodSQL.arrayMaxLength();
524:
525: _holdability = methodSQL.resultSetHoldabilityOverride();
526: }
527:
528: /**
529: * Checks that all statement options specified in annotation are supported by the database.
530: *
531: * @param metaData
532: * @throws SQLException
533: */
534: private void checkJdbcSupport(DatabaseMetaData metaData)
535: throws SQLException {
536:
537: if (_getGeneratedKeys && !metaData.supportsGetGeneratedKeys()) {
538: throw new ControlException(
539: "The database does not support getGeneratedKeys.");
540: }
541:
542: if (_batchUpdate && !metaData.supportsBatchUpdates()) {
543: throw new ControlException(
544: "The database does not support batchUpdates.");
545: }
546:
547: if (_scrollType != JdbcControl.ScrollType.DRIVER_DEFAULT
548: && !metaData.supportsResultSetConcurrency(_scrollType
549: .getType(), _scrollType.getConcurrencyType())) {
550: throw new ControlException(
551: "The database does not support the ResultSet concurrecy type: "
552: + _scrollType.toString());
553: }
554:
555: if (_holdability != JdbcControl.HoldabilityType.DRIVER_DEFAULT
556: && !metaData.supportsResultSetHoldability(_holdability
557: .getHoldability())) {
558: throw new ControlException(
559: "The database does not support the ResultSet holdability type: "
560: + _holdability.toString());
561: }
562: }
563:
564: /**
565: * The much maligned method for computing the maximum number of ResultSet rows this statement should return.
566: * The values of maxRows and arrayMaxLength are enforced at compile-time by the JdbcControlChecker to be the
567: * following: MAXROWS_ALL <= maxRows, 0 < arrayMaxLength
568: *
569: * @param method The annotated method.
570: * @return max number of resultSet rows to return from the query.
571: */
572: private int computeMaxRows(Method method) {
573:
574: Class returnType = method.getReturnType();
575:
576: final boolean isArray = returnType.isArray();
577: final boolean isRowSet = returnType.equals(RowSet.class);
578:
579: int maxSet = _maxRows;
580: if (isArray && _maxArray != JdbcControl.MAXROWS_ALL) {
581: maxSet = _maxRows == JdbcControl.MAXROWS_ALL ? _maxArray + 1
582: : Math.min(_maxArray + 1, _maxRows);
583: } else if (isRowSet && _maxRows > 0) {
584: maxSet = _maxRows + 1;
585: }
586:
587: return maxSet;
588: }
589: }
|