package org.ovirt.engine.core.dal.dbbroker;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.ovirt.engine.core.compat.Guid;
import org.ovirt.engine.core.utils.SerializationFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
public final class BatchProcedureExecutionConnectionCallback implements ConnectionCallback<Object> {
private static final Logger log = LoggerFactory.getLogger(BatchProcedureExecutionConnectionCallback.class);
private static ConcurrentMap<String, StoredProcedureMetaData> storedProceduresMap =
new ConcurrentHashMap<>();
private final String procName;
private final List<MapSqlParameterSource> executions;
private final SimpleJdbcCallsHandler handler;
private final DbEngineDialect dbEngineDialect;
public BatchProcedureExecutionConnectionCallback(SimpleJdbcCallsHandler handler,
String procName,
List<MapSqlParameterSource> executions) {
this.handler = handler;
this.procName = procName;
this.executions = executions;
this.dbEngineDialect = handler.getDialect();
}
@Override
public Object doInConnection(Connection con) throws SQLException,
DataAccessException {
log.debug("Executing batch for procedure " + procName);
StoredProcedureMetaData procMetaData = getStoredProcedureMetaData(
procName, con);
try (CallableStatement stmt = con.prepareCall(procMetaData.getSqlCommand())) {
for (MapSqlParameterSource execution : executions) {
mapParams(stmt, execution,
procMetaData.getParamatersMetaData());
stmt.addBatch();
}
stmt.executeBatch();
log.debug("Executed batch");
} catch (SQLException e) {
log.error("Can't execute batch: {}", e.getMessage());
log.debug("Exception", e);
if (e.getNextException() != null) {
log.error("Can't execute batch. Next exception is: {}",
e.getNextException().getMessage());
log.debug("Exception", e.getNextException());
}
throw e;
}
return null;
}
private StoredProcedureMetaData getStoredProcedureMetaData(
String procName, Connection con) throws SQLException {
if (!storedProceduresMap.containsKey(procName)) {
StoredProcedureMetaData procMetaData = new StoredProcedureMetaData();
fillProcMetaData(procName, con, procMetaData);
storedProceduresMap.putIfAbsent(procName, procMetaData);
}
return storedProceduresMap.get(procName);
}
private void fillProcMetaData(String procName, Connection con,
StoredProcedureMetaData procMetaData) throws SQLException {
SimpleJdbcCall call = handler.getCall(procName,
handler.createCallForModification(procName));
Map<String, SqlCallParameter> paramOrder = new HashMap<>();
String procNameFromDB = null;
String procSchemaFromDB = null;
StringBuilder params = new StringBuilder();
try (ResultSet rs2 = con.getMetaData().getProcedureColumns(null,
null, call.getProcedureName().toLowerCase(), "%")) {
int internalCounter = 1;
while (rs2.next()) {
ProcData procData = fillProcData(rs2, internalCounter);
if (procData != null) {
++internalCounter;
paramOrder.put(procData.getColName(), new SqlCallParameter(
procData.getOrdinal(), procData.getColName(), procData.getDataType()));
procNameFromDB = procData.getProcName();
procSchemaFromDB = procData.getSchemaName();
params.append("CAST (? AS ").append(procData.getDbTypeName())
.append("),");
}
}
if (params.length() > 0) {
params.deleteCharAt(params.length() - 1);
}
} catch (SQLException e) {
log.error("Can't get procedure '{}' meta data: {}", procName, e.getMessage());
log.debug("Exception", e);
}
procMetaData.setSqlCommand(handler.getDialect().createSqlCallCommand(
procSchemaFromDB, procNameFromDB, params.toString()));
procMetaData.setDbName(procNameFromDB);
procMetaData.setParamatersMetaData(paramOrder);
procMetaData.setSchemaName(procSchemaFromDB);
}
private ProcData fillProcData(ResultSet rs, int internalCounter) throws SQLException {
String colName = rs.getString("COLUMN_NAME");
if (colName.equalsIgnoreCase("returnValue")) {
return null;
}
ProcData retValue = new ProcData();
retValue.setColName(colName);
try {
retValue.setOrdinal(rs.getInt("ORDINAL_POSITION"));
} catch (SQLException e) {
// TODO: Delete when moving to Postgres Driver 9.1
// For some reason, some postgres drivers don't
// provide ORDINAL_POSITION
retValue.setOrdinal(internalCounter);
}
retValue.setDataType(rs.getInt("DATA_TYPE"));
retValue.setDbTypeName(rs.getString("TYPE_NAME"));
retValue.setProcName(rs.getString("PROCEDURE_NAME"));
retValue.setSchemaName(rs.getString("PROCEDURE_SCHEM"));
return retValue;
}
private void mapParams(PreparedStatement stmt,
MapSqlParameterSource paramSource,
Map<String, SqlCallParameter> paramOrder)
throws SQLException {
Map<String, Object> values = paramSource.getValues();
for (Map.Entry<String, SqlCallParameter> paramOrderEntry : paramOrder.entrySet()) {
String paramName = paramOrderEntry.getKey();
Object value = values.get(paramName);
if (value == null && paramName.startsWith(dbEngineDialect.getParamNamePrefix())) {
value = values.get(paramName.substring(dbEngineDialect.getParamNamePrefix().length()));
}
SqlCallParameter sqlParam = paramOrderEntry.getValue();
if (value != null) {
if (value.getClass().isEnum()) {
try {
Method method = value.getClass().getMethod("getValue");
value = method.invoke(value);
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
log.error("Error mapping enum type '{}': {}", value, ex.getMessage());
log.debug("Exception", ex);
}
}
if (value instanceof Guid) {
value = value.toString();
}
if (sqlParam.getDataType() == Types.TIMESTAMP) {
value = new Timestamp(((Date) value).getTime());
}
if (value instanceof Map) {
value = SerializationFactory.getSerializer().serialize(value);
}
} else {
if (sqlParam.getDataType() == Types.BOOLEAN || sqlParam.getDataType() == Types.BIT) {
value = false;
}
}
int ordinal = sqlParam.getOrdinal();
try {
stmt.setObject(ordinal, value);
} catch (Exception e) {
log.error("Can't map '{}' of type '{}' to type '{}', mapping to null value for parameter '{}'.",
value,
value != null ? value.getClass().getName() : null,
sqlParam.getDataType(),
sqlParam.getName());
stmt.setObject(ordinal, null);
}
}
log.debug("Mapped params: {}", values.keySet());
}
private static class ProcData {
private String colName;
private int ordinal;
private int dataType;
private String procName;
private String schemaName;
private String dbTypeName;
public String getColName() {
return colName;
}
public void setColName(String colName) {
this.colName = colName;
}
public int getOrdinal() {
return ordinal;
}
public void setOrdinal(int ordinal) {
this.ordinal = ordinal;
}
public int getDataType() {
return dataType;
}
public void setDataType(int dataType) {
this.dataType = dataType;
}
public String getProcName() {
return procName;
}
public void setProcName(String procName) {
this.procName = procName;
}
public String getSchemaName() {
return schemaName;
}
public void setSchemaName(String schemaName) {
this.schemaName = schemaName;
}
public String getDbTypeName() {
return dbTypeName;
}
public void setDbTypeName(String dbTypeName) {
this.dbTypeName = dbTypeName;
}
}
}