// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ .
// ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌·
// ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀
// ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌
// ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀
// Magicbane Emulator Project © 2013 - 2022
// www.magicbane.com
package engine.objects;
import engine.gameManager.DbManager;
import engine.job.JobScheduler;
import engine.jobs.BasicScheduledJob;
import engine.server.MBServerStatics;
import org.pmw.tinylog.Logger;
import java.io.InputStream;
import java.io.Reader;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.*;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.concurrent.ConcurrentHashMap;
/**
* A thread-safe sharing implementation of {@link PreparedStatement}.
*
* All of the methods from the PreparedStatement interface simply check to see
* that the PreparedStatement is active, and call the corresponding method on
* that PreparedStatement.
*
* @author Burfo
* @see PreparedStatement
*
**/
public class PreparedStatementShared implements PreparedStatement {
private static final ConcurrentHashMap> statementList = new ConcurrentHashMap<>(MBServerStatics.CHM_INIT_CAP, MBServerStatics.CHM_LOAD, MBServerStatics.CHM_THREAD_LOW);
private static final ArrayList statementListDelegated = new ArrayList<>();
private static final String ExceptionMessage = "PreparedStatementShared object " + "was accessed after being released.";
private static boolean debuggingIsOn;
private PreparedStatement ps = null;
private int sqlHash;
private String sql;
private long delegatedTime;
//debugging variables
private StackTraceElement[] stackTrace;
private DebugParam[] variables;
private String filteredSql;
private class DebugParam {
private Object debugObject;
private boolean valueAssigned;
public DebugParam(Object debugObject){
this.debugObject = debugObject;
valueAssigned = true;
}
public Object getDebugObject(){
return debugObject;
}
public boolean isValueAssigned(){
return valueAssigned;
}
}
@Override
public boolean isCloseOnCompletion() {
return true;
}
@Override
public void closeOnCompletion() {
Logger.warn( "Prepared Statement Closed");
}
/**
* Generates a new PreparedStatementShared based on the specified sql.
*
* @param sql
* Query string to generate the PreparedStatement
* @throws SQLException
**/
public PreparedStatementShared(String sql) throws SQLException {
this.sqlHash = sql.hashCode();
this.sql = sql;
this.delegatedTime = System.currentTimeMillis();
this.ps = getFromPool(sql, sqlHash);
if (this.ps == null) {
this.ps = createNew(sql, sqlHash);
}
if (debuggingIsOn) {
//see if there are any '?' in the statement that are not bind variables
//and filter them out.
boolean isString = false;
char[] sqlString = this.sql.toCharArray();
for (int i = 0; i < sqlString.length; i++){
if (sqlString[i] == '\'')
isString = !isString;
//substitute the ? with an unprintable character if is in a string
if (sqlString[i] == '?' && isString)
sqlString[i] = '\u0007';
}
this.filteredSql = new String(sqlString);
//find out how many variables are present in statement.
int count = 0;
int index = -1;
while ((index = filteredSql.indexOf('?',index+1)) != -1){
count++;
}
//create variables array with size equal to count of variables
this.variables = new DebugParam[count];
this.stackTrace = Thread.currentThread().getStackTrace();
} else {
this.stackTrace = null;
this.variables = null;
this.filteredSql = null;
}
synchronized (statementListDelegated) {
statementListDelegated.add(this);
}
}
private static PreparedStatement getFromPool(String sql, int sqlHash) throws SQLException {
PreparedStatement ps = null;
if (statementList.containsKey(sqlHash)) {
LinkedList list = statementList.get(sqlHash);
if (list == null) { // Shouldn't happen b/c no keys are ever removed
throw new AssertionError("list cannot be null.");
}
boolean success = false;
synchronized (list) {
do {
ps = list.pollFirst();
if (ps == null) {
break;
}
if (ps.isClosed()) { // should rarely happen
Logger.warn("A closed PreparedStatement was removed "
+ "from AbstractGameObject statementList. " + "SQL: " + sql);
} else {
success = true;
}
} while (!success);
}
if (ps != null) {
if (MBServerStatics.DB_DEBUGGING_ON_BY_DEFAULT) {
Logger.info("Found cached PreparedStatement for SQL hash: " + sqlHash
+ " SQL String: " + sql);
}
}
}
return ps;
}
private static PreparedStatement createNew(String sql, int sqlHash) throws SQLException {
statementList.putIfAbsent(sqlHash, new LinkedList<>());
return DbManager.prepareStatement(sql);
}
/**
* Releases the use of a PreparedStatementShared that was generated by
* {@link AbstractGameObject#prepareStatement}, making it available for use
* by another query.
*
* Do not utilize or modify the object after calling this method.
*
* Example:
*
*
* @code
* PreparedStatementShared ps = prepareStatement(...);
* ps.executeUpdate();
* ps.release();
* ps = null;}
*
**/
public void release() {
if (this.ps == null) {
return;
} // nothing to release
if (statementListDelegated.contains(this)) {
synchronized (statementListDelegated) {
statementListDelegated.remove(this);
}
try {
if (this.ps.isClosed()) {
return;
}
this.ps.clearParameters();
this.variables = null;
} catch (SQLException ignore) {
}
// add back to pool
LinkedList list = statementList.get(this.sqlHash);
if (list == null) {
return;
}
synchronized (list) {
list.add(this.ps);
}
}
// clear values from this object so caller cannot use it after it has
// been released
this.ps = null;
this.sqlHash = 0;
this.sql = "";
this.delegatedTime = 0;
this.stackTrace = null;
}
/**
* Determines if the object is in a usable state.
*
* @return True if the object is in a useable state.
**/
public boolean isUsable() {
if (ps == null) {
return false;
}
try {
if (ps.isClosed()) {
return false;
}
} catch (SQLException e) {
return false;
}
return true;
}
private String getTraceInfo() {
if (stackTrace == null) {
return "";
}
if (stackTrace.length > 3) {
return stackTrace[3].getClassName() + '.' + stackTrace[3].getMethodName();
} else if (stackTrace.length == 0) {
return "";
} else {
return stackTrace[stackTrace.length - 1].getClassName() + '.' + stackTrace[stackTrace.length - 1].getMethodName();
}
}
public static void submitPreparedStatementsCleaningJob() {
JobScheduler.getInstance().scheduleJob(new BasicScheduledJob("cleanUnreleasedStatements", PreparedStatementShared.class), 1000 * 60 * 2); // 2
// minutes
}
public static void cleanUnreleasedStatements() {
long now = System.currentTimeMillis();
long timeLimit = 120000; // 2 minutes
synchronized (statementListDelegated) {
Iterator iterator = statementListDelegated.iterator();
while (iterator.hasNext()) {
PreparedStatementShared pss = iterator.next();
if ((pss.delegatedTime + timeLimit) >= now) {
continue;
}
iterator.remove();
Logger.warn("Forcefully released after being held for > 2 minutes." + " SQL STRING: \""
+ pss.sql + "\" METHOD: " + pss.getTraceInfo());
}
}
submitPreparedStatementsCleaningJob(); // resubmit
}
@Override
public boolean equals(Object obj) {
if (ps == null || obj == null) {
return false;
}
if (obj instanceof PreparedStatementShared) {
return this.ps.equals(((PreparedStatementShared) obj).ps);
}
if (obj instanceof PreparedStatement) {
return this.ps.equals(obj);
}
return false;
}
@Override
public String toString(){
if (!debuggingIsOn || variables == null) {
return "SQL: " + this.sql + " (enable DB debugging for more data)";
}
String out;
out = "SQL: [" + this.sql + "] ";
out += "VARIABLES[count=" + variables.length + "]: ";
for (int i=0; i variables.length){
throw new SQLException("Parameter index of " + parameterIndex +
" exceeds actual parameter count of " + this.variables.length);
}
this.variables[parameterIndex-1] = new DebugParam(obj);
}
private void logExceptionAndRethrow(SQLException e) throws SQLException {
Logger.error("SQL operation failed: (" +
e.getMessage() + ") " + this.toString(), e);
throw e;
}
@Override
public void clearParameters() throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
this.ps.clearParameters();
for (int i=0; i"));
this.ps.setAsciiStream(parameterIndex, x);
}
@Override
public void setAsciiStream(int parameterIndex, InputStream x, int length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (x==null?"NULL":""));
this.ps.setBinaryStream(parameterIndex, x);
}
@Override
public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (x==null?"NULL":""));
this.ps.setBlob(parameterIndex, x);
}
@Override
public void setBlob(int parameterIndex, InputStream x, long length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (x==null?"NULL":""));
this.ps.setCharacterStream(parameterIndex, reader);
}
@Override
public void setCharacterStream(int parameterIndex, Reader reader, int length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (reader==null?"NULL":""));
this.ps.setClob(parameterIndex, reader);
}
@Override
public void setClob(int parameterIndex, Reader reader, long length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (reader==null?"NULL":""));
this.ps.setNCharacterStream(parameterIndex, value);
}
@Override
public void setNCharacterStream(int parameterIndex, Reader value, long length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (value==null?"NULL":""));
this.ps.setNClob(parameterIndex, value);
}
@Override
public void setNClob(int parameterIndex, Reader reader) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (reader==null?"NULL":""));
this.ps.setNClob(parameterIndex, reader);
}
@Override
public void setNClob(int parameterIndex, Reader reader, long length) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
saveObject(parameterIndex, (reader==null?"NULL":" iface) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
return this.ps.isWrapperFor(iface);
}
@Override
public T unwrap(Class iface) throws SQLException {
if (this.ps == null) {
throw new SQLException(ExceptionMessage);
}
return this.ps.unwrap(iface);
}
}