JdbcMapper/beehive-jdbc-control/src/main/java/org/apache/beehive/controls/system/jdbc/parser/SqlStatement.java

524 lines
21 KiB
Java

/*
* 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;
}
}