/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * $Header:$ */ package org.apache.beehive.controls.system.jdbc.parser; import org.apache.beehive.controls.api.ControlException; import org.apache.beehive.controls.api.context.ControlBeanContext; import org.apache.beehive.controls.system.jdbc.JdbcControl; import org.apache.beehive.controls.system.jdbc.TypeMappingsFactory; import javax.sql.RowSet; import java.io.Serializable; import java.lang.reflect.Method; import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; import java.sql.Statement; import java.sql.Types; import java.util.Calendar; /** * Represents a fully parsed SQL statement. SqlStatements can be used to generated a java.sql.PreparedStatement. */ public final class SqlStatement extends SqlFragmentContainer implements Serializable { private static final TypeMappingsFactory _tmf = TypeMappingsFactory.getInstance(); private boolean _callableStatement = false; private boolean _cacheableStatement = true; // // set from SQL annotation element values // private boolean _batchUpdate; private boolean _getGeneratedKeys; private String[] _genKeyColumnNames; private int _fetchSize; private int _maxArray; private int _maxRows; private int[] _genKeyColumnIndexes; private JdbcControl.ScrollType _scrollType; private JdbcControl.FetchDirection _fetchDirection; private JdbcControl.HoldabilityType _holdability; /** * Create a new SqlStatement. */ SqlStatement() { super(); } /** * Append a SqlFragment to the end of this statement. * * @param frag SqlFragment to append. */ void addChild(SqlFragment frag) { super.addChild(frag); if (frag.isDynamicFragment()) { _cacheableStatement = false; } } /** * Can the PreparedStatement generated by this class be cached? * * @return true if this statement can be cached by the SqlParser. */ boolean isCacheable() { return _cacheableStatement; } /** * Does this statement generate a callable or prepared statement? * * @return true if this statement generates callable statement. */ public boolean isCallableStatement() { return _callableStatement; } /** * Does this statement do a batch update? * * @return true if this statement should be executed as a batch update. */ public boolean isBatchUpdate() { return _batchUpdate; } /** * Does this statement return generatedKeys? * * @return true if getGeneratedKeys set to true. */ public boolean getsGeneratedKeys() { return _getGeneratedKeys; } /** * Generates the PreparedStatement the SQL statement. * * @param context ControlBeanContext instance. * @param connection Connection to database. * @param calendar Calendar instance which can be used to resolve date/time values. * @param method Method the SQL is associated with. * @param arguments Method parameters. * @return The PreparedStatement generated by this statement. * @throws SQLException If PreparedStatement cannot be created. */ public PreparedStatement createPreparedStatement(ControlBeanContext context, Connection connection, Calendar calendar, Method method, Object[] arguments) throws SQLException { PreparedStatement preparedStatement = null; loadSQLAnnotationStatmentOptions(context, method); checkJdbcSupport(connection.getMetaData()); _callableStatement = setCallableStatement(arguments); try { final String sql = getPreparedStatementText(context, method, arguments); // // is this a request for generatedKeys ? // if (_getGeneratedKeys) { if (_callableStatement) { throw new ControlException("getGeneratedKeys not supported for CallableStatements"); } if (_genKeyColumnNames.length > 0) { preparedStatement = connection.prepareStatement(sql, _genKeyColumnNames); } else if (_genKeyColumnIndexes.length > 0) { preparedStatement = connection.prepareStatement(sql, _genKeyColumnIndexes); } else { preparedStatement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS); } } else { if (_holdability == JdbcControl.HoldabilityType.DRIVER_DEFAULT) { if (_scrollType == JdbcControl.ScrollType.DRIVER_DEFAULT) { preparedStatement = (_callableStatement) ? connection.prepareCall(sql) : connection.prepareStatement(sql); } else { preparedStatement = (_callableStatement) ? connection.prepareCall(sql, _scrollType.getType(), _scrollType.getConcurrencyType()) : connection.prepareStatement(sql, _scrollType.getType(), _scrollType.getConcurrencyType()); } } else { preparedStatement = (_callableStatement) ? connection.prepareCall(sql, _scrollType.getType(), _scrollType.getConcurrencyType(), _holdability.getHoldability()) : connection.prepareStatement(sql, _scrollType.getType(), _scrollType.getConcurrencyType(), _holdability.getHoldability()); } } // // If the method argument is of type SQLParameter, treat this statement as a CallableStatement, // if (_callableStatement) { for (SqlFragment sf : _children) { if (sf.hasParamValue()) { throw new ControlException("Cannot use parameter substution and SQLParameter array in the same method."); } } JdbcControl.SQLParameter[] params = (JdbcControl.SQLParameter[]) arguments[0]; if (params == null) { return preparedStatement; } for (int i = 0; i < params.length; i++) { JdbcControl.SQLParameter p = params[i]; if (p.dir != JdbcControl.SQLParameter.OUT) { Object value = params[i].value; setPreparedStatementParameter(preparedStatement, i + 1, value, params[i].type, calendar); } if (p.dir != JdbcControl.SQLParameter.IN) { ((CallableStatement) preparedStatement).registerOutParameter(i + 1, params[i].type); } } // // special handling for batch updates // } else if (_batchUpdate) { doBatchUpdate(preparedStatement, arguments, calendar); // // standard case, not a batch or callable // } else { int pIndex = 1; for (SqlFragment sf : _children) { if (sf.hasParamValue()) { Object values[] = sf.getParameterValues(context, method, arguments); for (Object value : values) { setPreparedStatementParameter(preparedStatement, pIndex++, value, sf.getParamSqlDataType(), calendar); } } } } } catch (SQLException e) { if (preparedStatement != null) preparedStatement.close(); throw e; } preparedStatement.setFetchDirection(_fetchDirection.getDirection()); preparedStatement.setFetchSize(_fetchSize); preparedStatement.setMaxRows(computeMaxRows(method)); return preparedStatement; } /** * Generates the PreparedStatement the SQL statement. * * @param context ControlBeanContext instance. * @param connection Connection to database. * @param method Method the SQL is associated with. * @param arguments Method parameters. * @return The PreparedStatement generated by this statement. */ public String createPreparedStatementString(ControlBeanContext context, Connection connection, Method method, Object[] arguments) { final boolean callableStatement = setCallableStatement(arguments); StringBuilder sqlString = new StringBuilder(getPreparedStatementText(context, method, arguments)); if (callableStatement) { JdbcControl.SQLParameter[] params = (JdbcControl.SQLParameter[]) arguments[0]; if (params == null) { return sqlString.toString(); } sqlString.append(" Params: {"); for (int i = 0; i < params.length; i++) { if (i > 0) { sqlString.append(params[i].value.toString()); } } sqlString.append("}"); } else if (_batchUpdate) { sqlString.append(" Params: batch update."); } else { sqlString.append(" Params: {"); boolean first = true; for (SqlFragment sf : _children) { if (sf.hasParamValue()) { Object values[] = sf.getParameterValues(context, method, arguments); for (Object value : values) { if (!first) sqlString.append(", "); else first = false; sqlString.append(value); } } } sqlString.append("}"); } return sqlString.toString(); } // /////////////////////////////////////////////////// PRIVATE METHODS /////////////////////////////////////////// /** * Sets the specified parameter in the prepared statement. * * @param ps A PreparedStatement. * @param i index of parameter to set. * @param value value of the parameter. * @param sqlType SQL type of value. * @param cal A calendar instance used to resolve date/time values. * @throws SQLException If the parameter cannot be set. */ private void setPreparedStatementParameter(PreparedStatement ps, int i, Object value, int sqlType, Calendar cal) throws SQLException { if (sqlType == Types.NULL) { sqlType = _tmf.getSqlType(value); } if (value == null) { ps.setNull(i, Types.NULL == sqlType ? Types.VARCHAR : sqlType); return; } switch (sqlType) { case Types.VARCHAR: if (!(value instanceof String)) value = value.toString(); break; case Types.BOOLEAN: if (value instanceof Boolean) { ps.setBoolean(i, ((Boolean) value).booleanValue()); return; } break; case Types.TIMESTAMP: if (value instanceof java.util.Calendar) { Calendar calValue = (Calendar) value; // @todo: validate it is correct to comment out call to deprectated method // if (cal == null) { // /* NOTE: drivers are inconsistent in their handling of setTimestamp(i,date,cal) // * so we won't use that, unless the user calls setCalendar(). // * I'm going with the theory that it makes sense to store // * the time relative to the Calendar's timezone rather than // * the system timezone otherwise, using a Calendar would be a no-op. // */ // value = new java._sql.Timestamp(calValue.get(Calendar.YEAR) - 1900, // calValue.get(Calendar.MONTH), // calValue.get(Calendar.DATE), // calValue.get(Calendar.HOUR_OF_DAY), // calValue.get(Calendar.MINUTE), // calValue.get(Calendar.SECOND), // calValue.get(Calendar.MILLISECOND)); // } else { value = new java.sql.Timestamp(calValue.getTimeInMillis()); // } } else if (java.util.Date.class.equals(value.getClass())) { // some drivers don't like java.util.Date value = new java.sql.Timestamp(((java.util.Date) value).getTime()); } if (value instanceof java.sql.Timestamp) { if (cal == null) ps.setTimestamp(i, (java.sql.Timestamp) value); else ps.setTimestamp(i, (java.sql.Timestamp) value, cal); return; } break; case Types.DATE: if (value instanceof java.util.Calendar) { /* NOTE: see note above Calendar cal = (Calendar)value; value = new java._sql.Date(cal.getTimeInMillis()); ps.setDate(i, (java._sql.Date)value, cal); return; */ Calendar calValue = (Calendar) value; // @todo: validate that commenting out deprected method is correct behavior // if (cal == null) { // value = new java._sql.Date(calValue.get(Calendar.YEAR - 1900), // calValue.get(Calendar.MONTH), // calValue.get(Calendar.DATE)); // } else { value = new java.sql.Date(calValue.getTimeInMillis()); // } } else if (value.getClass() == java.util.Date.class) { // some drivers don't like java.util.Date value = new java.sql.Date(((java.util.Date) value).getTime()); } if (value instanceof java.sql.Date) { if (cal == null) { ps.setDate(i, (java.sql.Date) value); } else { ps.setDate(i, (java.sql.Date) value, cal); } return; } break; case Types.TIME: if (value instanceof java.sql.Time) { if (cal == null) { ps.setTime(i, (java.sql.Time) value); } else { ps.setTime(i, (java.sql.Time) value, cal); } return; } break; } if (sqlType == Types.NULL) { ps.setObject(i, value); } else { ps.setObject(i, value, sqlType); } } /** * Determine if this SQL will generate a callable or prepared statement. * * @param args The method's argument list which this SQL annotation was assocatied with. * @return true if this statement will generated a CallableStatement */ private boolean setCallableStatement(Object[] args) { // CallableStatement vs. PreparedStatement if (args != null && args.length == 1 && args[0] != null) { Class argClass = args[0].getClass(); if (argClass.isArray() && JdbcControl.SQLParameter.class.isAssignableFrom(argClass.getComponentType())) { return true; } } return false; } /** * Build a prepared statement for a batch update. * * @param ps The PreparedStatement object. * @param args The parameter list of the jdbccontrol method. * @param cal A Calendar instance used to resolve date/time values. * @throws SQLException If a batch update cannot be performed. */ private void doBatchUpdate(PreparedStatement ps, Object[] args, Calendar cal) throws SQLException { final int[] sqlTypes = new int[args.length]; final Object[] objArrays = new Object[args.length]; // build an array of type values and object arrays for (int i = 0; i < args.length; i++) { sqlTypes[i] = _tmf.getSqlType(args[i].getClass().getComponentType()); objArrays[i] = TypeMappingsFactory.toObjectArray(args[i]); } final int rowCount = ((Object[]) objArrays[0]).length; for (int i = 0; i < rowCount; i++) { for (int j = 0; j < args.length; j++) { setPreparedStatementParameter(ps, j + 1, ((Object[]) objArrays[j])[i], sqlTypes[j], cal); } ps.addBatch(); } } /** * Load element values from the SQL annotation which apply to Statements. * * @param context ControlBeanContext instance. * @param method Annotated method. */ private void loadSQLAnnotationStatmentOptions(ControlBeanContext context, Method method) { final JdbcControl.SQL methodSQL = (JdbcControl.SQL) context.getMethodPropertySet(method, JdbcControl.SQL.class); _batchUpdate = methodSQL.batchUpdate(); _getGeneratedKeys = methodSQL.getGeneratedKeys(); _genKeyColumnNames = methodSQL.generatedKeyColumnNames(); _genKeyColumnIndexes = methodSQL.generatedKeyColumnIndexes(); _scrollType = methodSQL.scrollableResultSet(); _fetchDirection = methodSQL.fetchDirection(); _fetchSize = methodSQL.fetchSize(); _maxRows = methodSQL.maxRows(); _maxArray = methodSQL.arrayMaxLength(); _holdability = methodSQL.resultSetHoldabilityOverride(); } /** * Checks that all statement options specified in annotation are supported by the database. * * @param metaData * @throws SQLException */ private void checkJdbcSupport(DatabaseMetaData metaData) throws SQLException { if (_getGeneratedKeys && !metaData.supportsGetGeneratedKeys()) { throw new ControlException("The database does not support getGeneratedKeys."); } if (_batchUpdate && !metaData.supportsBatchUpdates()) { throw new ControlException("The database does not support batchUpdates."); } if (_scrollType != JdbcControl.ScrollType.DRIVER_DEFAULT && !metaData.supportsResultSetConcurrency(_scrollType.getType(), _scrollType.getConcurrencyType())) { throw new ControlException("The database does not support the ResultSet concurrecy type: " + _scrollType.toString()); } if (_holdability != JdbcControl.HoldabilityType.DRIVER_DEFAULT && !metaData.supportsResultSetHoldability(_holdability.getHoldability())) { throw new ControlException("The database does not support the ResultSet holdability type: " + _holdability.toString()); } } /** * The much maligned method for computing the maximum number of ResultSet rows this statement should return. * The values of maxRows and arrayMaxLength are enforced at compile-time by the JdbcControlChecker to be the * following: MAXROWS_ALL <= maxRows, 0 < arrayMaxLength * * @param method The annotated method. * @return max number of resultSet rows to return from the query. */ private int computeMaxRows(Method method) { Class returnType = method.getReturnType(); final boolean isArray = returnType.isArray(); final boolean isRowSet = returnType.equals(RowSet.class); int maxSet = _maxRows; if (isArray && _maxArray != JdbcControl.MAXROWS_ALL) { maxSet = _maxRows == JdbcControl.MAXROWS_ALL ? _maxArray + 1 : Math.min(_maxArray + 1, _maxRows); } else if (isRowSet && _maxRows > 0) { maxSet = _maxRows + 1; } return maxSet; } }