// • ▌ ▄ ·. ▄▄▄· ▄▄ • ▪ ▄▄· ▄▄▄▄· ▄▄▄· ▐▄▄▄ ▄▄▄ . // ·██ ▐███▪▐█ ▀█ ▐█ ▀ ▪██ ▐█ ▌▪▐█ ▀█▪▐█ ▀█ •█▌ ▐█▐▌· // ▐█ ▌▐▌▐█·▄█▀▀█ ▄█ ▀█▄▐█·██ ▄▄▐█▀▀█▄▄█▀▀█ ▐█▐ ▐▌▐▀▀▀ // ██ ██▌▐█▌▐█ ▪▐▌▐█▄▪▐█▐█▌▐███▌██▄▪▐█▐█ ▪▐▌██▐ █▌▐█▄▄▌ // ▀▀ █▪▀▀▀ ▀ ▀ ·▀▀▀▀ ▀▀▀·▀▀▀ ·▀▀▀▀ ▀ ▀ ▀▀ █▪ ▀▀▀ // 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; /** * 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); } 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 } public static void enableDebugging() { debuggingIsOn = true; Logger.info("Database debugging has been enabled."); } public static void disableDebugging() { debuggingIsOn = false; Logger.info("Database debugging has been disabled."); } @Override public boolean isCloseOnCompletion() { return true; } @Override public void closeOnCompletion() { Logger.warn("Prepared Statement Closed"); } /** * 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(); } } @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; i++) { out += "[" + (i + 1) + "]: "; DebugParam dp = variables[i]; if (dp == null || !dp.isValueAssigned()) { out += "{MISSING} "; continue; } Object dpObj = dp.getDebugObject(); out += dpObj.toString() + ' '; } return out; } @Override public void addBatch() throws SQLException { if (this.ps == null) { throw new SQLException(); } this.ps.addBatch(); } private void saveObject(int parameterIndex, Object obj) throws SQLException { if (!debuggingIsOn || this.variables == null) { return; } if (parameterIndex > 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.variables.length; i++) { this.variables[i] = null; } } @Override public boolean execute() throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } if (debuggingIsOn || MBServerStatics.ENABLE_EXECUTION_TIME_WARNING) { long startTime = System.currentTimeMillis(); boolean rs = false; try { rs = this.ps.execute(); } catch (SQLException e) { logExceptionAndRethrow(e); } if ((startTime + MBServerStatics.DB_EXECUTION_WARNING_TIME_MS) < System.currentTimeMillis()) Logger.warn("The following statement took " + (System.currentTimeMillis() - startTime) + " millis to execute: " + this.sql); return rs; } return this.ps.execute(); } @Override public ResultSet executeQuery() throws SQLException, SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } if (debuggingIsOn || MBServerStatics.ENABLE_QUERY_TIME_WARNING) { long startTime = System.currentTimeMillis(); ResultSet rs = null; try { rs = this.ps.executeQuery(); } catch (SQLException e) { logExceptionAndRethrow(e); } if ((startTime + MBServerStatics.DB_QUERY_WARNING_TIME_MS) < System.currentTimeMillis()) Logger.warn("The following query took " + (System.currentTimeMillis() - startTime) + " millis to execute: " + this.sql); return rs; } return this.ps.executeQuery(); } @Override public int executeUpdate() throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } if (debuggingIsOn || MBServerStatics.ENABLE_UPDATE_TIME_WARNING) { long startTime = System.currentTimeMillis(); int rs = 0; try { rs = this.ps.executeUpdate(); } catch (SQLException e) { logExceptionAndRethrow(e); } if ((startTime + MBServerStatics.DB_UPDATE_WARNING_TIME_MS) < System.currentTimeMillis()) Logger.warn("The following update took " + (System.currentTimeMillis() - startTime) + " millis to execute: " + this.sql); return rs; } return this.ps.executeUpdate(); } @Override public ResultSetMetaData getMetaData() throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } return this.ps.getMetaData(); } @Override public ParameterMetaData getParameterMetaData() throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } return this.ps.getParameterMetaData(); } @Override public void setArray(int parameterIndex, Array x) throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } this.saveObject(parameterIndex, x); this.ps.setArray(parameterIndex, x); } @Override public void setAsciiStream(int parameterIndex, InputStream x) throws SQLException { if (this.ps == null) { throw new SQLException(ExceptionMessage); } saveObject(parameterIndex, (x == null ? "NULL" : "")); 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); } 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; } } }