/*
* Copyright (C) 2000 - 2008 TagServlet Ltd
*
* This file is part of Open BlueDragon (OpenBD) CFML Server Engine.
*
* OpenBD is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* Free Software Foundation,version 3.
*
* OpenBD is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with OpenBD. If not, see http://www.gnu.org/licenses/
*
* Additional permission under GNU GPL version 3 section 7
*
* If you modify this Program, or any covered work, by linking or combining
* it with any of the JARS listed in the README.txt (or a modified version of
* (that library), containing parts covered by the terms of that JAR, the
* licensors of this Program grant you additional permission to convey the
* resulting work.
* README.txt @ http://www.openbluedragon.org/license/README.txt
*
* http://www.openbluedragon.org/
*/
package com.naryx.tagfusion.cfm.application;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Reader;
import java.io.Serializable;
import java.io.StringReader;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Map;
import javax.servlet.http.Cookie;
import com.nary.util.FastMap;
import com.naryx.tagfusion.cfm.cookie.cfCookieData;
import com.naryx.tagfusion.cfm.engine.cfCatchData;
import com.naryx.tagfusion.cfm.engine.cfData;
import com.naryx.tagfusion.cfm.engine.cfDateData;
import com.naryx.tagfusion.cfm.engine.cfEngine;
import com.naryx.tagfusion.cfm.engine.cfNumberData;
import com.naryx.tagfusion.cfm.engine.cfSession;
import com.naryx.tagfusion.cfm.engine.cfStringData;
import com.naryx.tagfusion.cfm.engine.cfStructData;
import com.naryx.tagfusion.cfm.engine.cfmRunTimeException;
import com.naryx.tagfusion.cfm.engine.dataNotSupportedException;
import com.naryx.tagfusion.cfm.engine.variableStore;
import com.naryx.tagfusion.cfm.sql.cfDataSource;
/**
* This class enscapsulates all the [client] scope management
*
* Note that the implementation of this has changed so this class is further
* complicated by maintaining backwards compatibility for 6.1 client data
*
* IMPORTANT! for backwards-compatibility of client data, we must use
* com.nary.util.HashMap instead of FastMap.
*/
public class cfClientSessionData extends cfStructData implements Serializable {
private static final long serialVersionUID = 1;
private static final int DATABASE = 1;
private static final int COOKIE = 2;
private static final int INVALID = 3;
// This is the SQL for the old client data table
private static String SQL_SELECT = "SELECT CFDATA FROM BDCLIENTDATA WHERE CFID=?";
private static String SQL_DELETE = "DELETE FROM BDCLIENTDATA WHERE CFID=?";
private static String SQL_INSERT = "INSERT INTO BDCLIENTDATA (CFID,CFDATA) VALUES (?,?)";
private static String SQL_UPDATE = "UPDATE BDCLIENTDATA SET CFDATA=? WHERE CFID=?";
// This is the SQL for the new client data tables
private static String SQL_SELECT_GLOBAL;
private static String SQL_INSERT_GLOBAL;
private static String SQL_UPDATE_GLOBAL;
private static String[] SQL_CREATE_GLOBAL;
private static String SQL_SELECT_DATA;
private static String SQL_INSERT_DATA;
private static String SQL_UPDATE_DATA;
private static String[] SQL_CREATE_DATA;
private static String DATA_TABLE_NAME;
private static String GLOBAL_TABLE_NAME;
private static String DATA_COLUMN_NAME;
private static String GLOBAL_COOKIE_NAME;
private static String CLIENT_COOKIE_NAME;
// For fixed-width CHAR(n) columns we're going to use "LIKE" instead of "="
// when doing
// selects or inserts (or, eventually, deletes). This was discovered while
// testing CF5
// compatible client data on Oracle 8; to minimize the impact of these
// changes, we're
// only going to use LIKE when running in CF5 compatibility mode. We may
// eventually want
// to change this to always use LIKE.
private static String COMPARISON_OP;
private static final String LIKE = "LIKE";
static {
// Use different table names for ColdFusion compatibility. Use LIKE for CF5
// compatibility.
if (cfApplicationManager.cf5ClientData) {
DATA_TABLE_NAME = "CDATA";
GLOBAL_TABLE_NAME = "CGLOBAL";
DATA_COLUMN_NAME = "DATA";
GLOBAL_COOKIE_NAME = "CFGLOBALS";
CLIENT_COOKIE_NAME = "CFCLIENT_";
COMPARISON_OP = LIKE;
} else {
DATA_TABLE_NAME = "BDDATA";
GLOBAL_TABLE_NAME = "BDGLOBAL";
DATA_COLUMN_NAME = "CFDATA";
GLOBAL_COOKIE_NAME = "bdglobals";
CLIENT_COOKIE_NAME = "bdclient_";
COMPARISON_OP = "=";
}
// Initialize the SQL statements needed for the global table
SQL_SELECT_GLOBAL = "SELECT " + DATA_COLUMN_NAME + " FROM " + GLOBAL_TABLE_NAME + " WHERE CFID " + COMPARISON_OP + " ?";
SQL_INSERT_GLOBAL = "INSERT INTO " + GLOBAL_TABLE_NAME + " (" + DATA_COLUMN_NAME + ",LVISIT,CFID) VALUES (?,?,?)";
SQL_UPDATE_GLOBAL = "UPDATE " + GLOBAL_TABLE_NAME + " SET " + DATA_COLUMN_NAME + "=?, LVISIT=? WHERE CFID " + COMPARISON_OP + " ?";
// don't create tables when in ColdFusion-compatible mode
SQL_CREATE_GLOBAL = new String[] {
// Microsoft Access (ASCII/UNICODE)
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48), CFDATA MEMO, LVISIT DATETIME, CONSTRAINT PK_BDGLOBAL PRIMARY KEY (CFID) )",
// PointBase (NOTE: must come before Oracle entry otherwise it will be
// used.)
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) PRIMARY KEY, CFDATA CLOB(2G), LVISIT TIMESTAMP )",
// Oracle (ASCII)
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) PRIMARY KEY, CFDATA CLOB, LVISIT DATE )",
// Microsoft SQL Server (ASCII), MySQL, Sybase
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) PRIMARY KEY, CFDATA TEXT, LVISIT DATETIME )",
// PostgreSQL
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) PRIMARY KEY, CFDATA TEXT, LVISIT TIMESTAMP )",
// DB2
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) NOT NULL PRIMARY KEY, CFDATA LONG VARCHAR, LVISIT TIMESTAMP )",
// Informix
"CREATE TABLE " + GLOBAL_TABLE_NAME + " ( CFID VARCHAR(48) NOT NULL PRIMARY KEY, CFDATA LONG VARCHAR, LVISIT datetime year to fraction(5) )" };
// Initialize the SQL statements needed for the data table
SQL_SELECT_DATA = "SELECT " + DATA_COLUMN_NAME + " FROM " + DATA_TABLE_NAME + " WHERE CFID " + COMPARISON_OP + " ? AND APP " + COMPARISON_OP + " ?";
SQL_INSERT_DATA = "INSERT INTO " + DATA_TABLE_NAME + " (" + DATA_COLUMN_NAME + ",CFID,APP) VALUES (?,?,?)";
SQL_UPDATE_DATA = "UPDATE " + DATA_TABLE_NAME + " SET " + DATA_COLUMN_NAME + "=? WHERE CFID " + COMPARISON_OP + " ? AND APP " + COMPARISON_OP + " ?";
// don't create tables when in ColdFusion-compatible mode
SQL_CREATE_DATA = new String[] {
// Microsoft Access (ASCII/UNICODE)
"CREATE TABLE " + DATA_TABLE_NAME + " ( CFID VARCHAR(48), APP VARCHAR(64), CFDATA MEMO, CONSTRAINT PK_BDDATA PRIMARY KEY (CFID,APP) )",
// PointBase (NOTE: must come before Oracle entry otherwise it will be
// used.)
"CREATE TABLE " + DATA_TABLE_NAME + " ( CFID VARCHAR(48), APP VARCHAR(64), CFDATA CLOB(2G), PRIMARY KEY (CFID,APP) )",
// Oracle (ASCII)
"CREATE TABLE " + DATA_TABLE_NAME + " ( CFID VARCHAR(48), APP VARCHAR(64), CFDATA CLOB, PRIMARY KEY (CFID,APP) )",
// Microsoft SQL Server (ASCII), MySQL, PostgreSQL, Sybase
"CREATE TABLE " + DATA_TABLE_NAME + " ( CFID VARCHAR(48) NOT NULL, APP VARCHAR(64) NOT NULL, CFDATA TEXT, PRIMARY KEY (CFID,APP) )",
// DB2
"CREATE TABLE " + DATA_TABLE_NAME + " ( CFID VARCHAR(48) NOT NULL, APP VARCHAR(64) NOT NULL, CFDATA LONG VARCHAR, PRIMARY KEY (CFID,APP) )" };
}
// ---------------------------------------------
String appName;
private String CFID, CFTOKEN;
private int clientStorageType;
private String dataSource;
private cfDataSource sqldataSource;
private boolean bLoadedFromTable = false;
// these indicate whether the global and app-specific data has been loaded in
// from the db
private boolean loadedGlobalFromTable = false;
private boolean loadedAppFromTable = false;
private boolean usingNewDB = false;
// indicates whether old or new client db tables are used
private cfSession session;
private boolean clientStart = false;
private boolean isModified = false; // has client data been modified this
// request?
public boolean isClientStart() {
return clientStart;
}
// ---------------------------------------------
public cfClientSessionData(cfSession _Session, sessionUtility global, String _clientStorageType, String _appName, boolean _updateAccessData) throws cfmRunTimeException {
super(null); // null because we're going to use setHashData()
// --[ Setup the Client data
CFID = global.CFID;
CFTOKEN = global.CFTOKEN;
session = _Session;
appName = _appName;
// Oracle doesn't allow primary key fields to be assigned an empty string
// so we need to set the unnamed app to a different value
if ((appName == null) || (appName.length() == 0))
appName = (cfApplicationManager.cf5ClientData ? " " : "BD_Unnamed_App");
if (cfApplicationManager.cf5ClientData) {
appName = appName.toUpperCase();
}
// --[ Determine the type of storage we are using for this one
if (_clientStorageType == null || _clientStorageType.length() == 0 || _clientStorageType.equalsIgnoreCase("COOKIE")) {
// --[ If the storage type is a cookie, we will have to hold the page back
// until all the processing is complete
clientStorageType = COOKIE;
loadCookie(_Session);
} else if (_clientStorageType.equalsIgnoreCase("REGISTRY")) {
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_APPLICATION);
catchData.setDetail("As of BlueDragon 7.0, the registry can no longer be used to store client data.");
catchData.setMessage("Cannot use the REGISTRY to store client data");
throw new cfmRunTimeException(catchData);
} else {
clientStorageType = DATABASE;
dataSource = _clientStorageType;
loadDB(_Session);
}
// --[ Something has gone wrong, and we need to reset the scope
if (clientStorageType == INVALID) {
setHashData(new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE));
initialize();
}
// --[ Update the client scope with data
if (_updateAccessData)
updateAccessData();
}
// ----------------------------------------------------
public void setData(cfData _key, cfData _data) throws cfmRunTimeException {
flushCheck();
if (cfData.isSimpleValue(_data)) {
this.isModified = true;
super.setData(_key, _data);
}
}
public void setData(String _key, cfData _data) {
if (cfData.isSimpleValue(_data)) {
this.isModified = true;
super.setData(_key, _data);
}
}
public void deleteData(String _key) throws cfmRunTimeException {
if (this.containsKey(_key)) {
this.isModified = true;
super.deleteData(_key);
}
}
public void clear() {
this.isModified = true;
super.clear();
}
private void flushCheck() throws cfmRunTimeException {
if (clientStorageType == COOKIE && session.isFlushed()) {
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_TEMPLATE);
catchData.setDetail("When CLIENT variables are stored as cookies you cannot set a CLIENT variable after the page has been flushed");
catchData.setMessage("Cannot set CLIENT variable");
throw new cfmRunTimeException(catchData);
}
}
// -----------------------------------------------------
// req'd by cfFlush so as cookies are set if the client data is saved
// as cookies.
public void flush(cfSession _Session) {
if (clientStorageType == COOKIE) {
saveCookie(_Session);
}
}
public void close(cfSession _Session) throws cfmRunTimeException {
getHashData().put("lastvisit", new cfDateData(System.currentTimeMillis()));
switch (clientStorageType) {
case COOKIE:
saveCookie(_Session);
break;
case DATABASE:
if (!cfApplicationManager.clientGlobalUpdateDisabled || this.isModified) {
saveDB();
}
break;
default:
throw new IllegalStateException("illegal client storage type - " + clientStorageType);
}
}
private void initialize() {
Map<String, cfData> hashdata = getHashData();
hashdata.put("hitcount", new cfNumberData(0));
hashdata.put("cfid", new cfStringData(CFID));
hashdata.put("cftoken", new cfStringData(CFTOKEN));
hashdata.put("timecreated", new cfDateData(System.currentTimeMillis()));
hashdata.put("lastvisit", new cfDateData(System.currentTimeMillis()));
hashdata.put("urltoken", new cfStringData("CFID=" + CFID + "&CFTOKEN=" + CFTOKEN));
clientStart = true;
}
private void updateAccessData() throws dataNotSupportedException {
Map<String, cfData> hashdata = getHashData();
if (hashdata.containsKey("hitcount")) {
cfNumberData hitcount = ((cfData) hashdata.get("hitcount")).getNumber();
if (hitcount != null) {
hitcount.add(1);
hashdata.put("hitcount", hitcount);
} else {
hashdata.put("hitcount", new cfNumberData(1));
}
} else {
hashdata.put("hitcount", new cfNumberData(1));
}
if (!hashdata.containsKey("lastvisit")) {
hashdata.put("lastvisit", new cfDateData(System.currentTimeMillis()));
} else {
cfDateData lastvisit = ((cfData) hashdata.get("lastvisit")).getDateData();
if (lastvisit == null) {
hashdata.put("lastvisit", new cfDateData(System.currentTimeMillis()));
}
}
}
public String getNonReadOnlyList() {
String list = "";
Map<String, cfData> hashdata = getHashData();
synchronized (hashdata) {
Iterator<String> iter = hashdata.keySet().iterator();
while (iter.hasNext()) {
String K = iter.next();
if (!K.equalsIgnoreCase("hitcount") && !K.equalsIgnoreCase("cfid") && !K.equalsIgnoreCase("cftoken") && !K.equalsIgnoreCase("timecreated") && !K.equalsIgnoreCase("lastvisit") && !K.equalsIgnoreCase("urltoken"))
list += K + ",";
}
}
if (list.length() > 0)
list = list.substring(0, list.length() - 1);
return list;
}
// -------------------------------------------------------------------
// --[ Routines to save/load to the database
// -------------------------------------------------------------------
private void loadDB(cfSession _Session) throws cfmRunTimeException {
// -----------------------------------------------------------------
// --[ Attempt to locate the datasource, throw an error if not found
try {
sqldataSource = new cfDataSource(dataSource, _Session);
} catch (cfmRunTimeException RFE) {
RFE.getCatchData().setExtendedInfo(cfEngine.getMessage("cfapplication.clientBadDataSource"));
throw RFE;
}
// if the BDCLIENTDATA table exists then we want to use the old method
// otherwise look to BDGLOBAL/BDDATA for client data creating the tables if
// required
if (cfApplicationManager.cf5ClientData || !loadDBOld()) {
loadDBNew();
usingNewDB = true;
}
// --[ The hashdata may not have been initialised, so lets make sure it
// lives!
Map<String, cfData> hashdata = getHashData();
if (hashdata == null) {
setHashData(new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE));
hashdata = getHashData();
}
if (hashdata.size() == 0)
initialize();
}
private boolean loadDBOld() {
// --[ Attempt to load the table. if it's not there then don't create it
if (!loadData(sqldataSource)) {
return false;
} else {
return true;
}
}
private void loadDBNew() throws cfmRunTimeException {
if (!loadDataNew(sqldataSource)) {
if (cfApplicationManager.cf5ClientData) { // don't create tables when in
// ColdFusion-compatible mode
clientStorageType = INVALID;
throw newInvalidDatasourceException("Failed to load data");
}
if (!createTable(sqldataSource)) {
clientStorageType = INVALID;
throw newInvalidDatasourceException("Failed to create table");
}
// try again after creating tables
if (!loadDataNew(sqldataSource)) {
clientStorageType = INVALID;
throw newInvalidDatasourceException("Failed to load data");
}
}
}
private cfmRunTimeException newInvalidDatasourceException(String message) {
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_EXPRESSION);
catchData.setDetail("Check that the datasource '" + sqldataSource.getDataSourceName() + "' exists and the necessary write permissions are configured.");
catchData.setMessage("Invalid client storage datasource (" + message + ")");
return new cfmRunTimeException(catchData);
}
private void saveDB() throws cfmRunTimeException {
if (usingNewDB) {
saveDBNew();
} else {
saveDBOld();
}
}
private void saveDBNew() throws cfmRunTimeException {
String key = CFID + (cfApplicationManager.cf5ClientData ? ":" : "-") + CFTOKEN;
Connection con = null;
// INSERT or UPDATE global data?
Map<String, cfData> globalData = getGlobalData();
try {
con = sqldataSource.getConnection();
PreparedStatement statmt = null;
// --[ select the SQL string
if (loadedGlobalFromTable) { // UPDATE since the data is already in the
// table
statmt = con.prepareStatement(SQL_UPDATE_GLOBAL);
} else { // otherwise entry not already there so INSERT
statmt = con.prepareStatement(SQL_INSERT_GLOBAL);
}
// --[ set the parameters. Note the 2 SQL statements list the params in
// the same order
String tmp = encodeData(globalData);
statmt.setAsciiStream(1, new ByteArrayInputStream(tmp.getBytes()), tmp.length());
statmt.setTimestamp(2, new java.sql.Timestamp(((cfData) globalData.get("lastvisit")).getDateData().getLong()));
if (loadedGlobalFromTable && (COMPARISON_OP == LIKE)) {
statmt.setString(3, key + "%");
} else {
statmt.setString(3, key);
}
statmt.executeUpdate();
statmt.close();
if (!con.getAutoCommit()) {
cfEngine.log("CLIENT data update: auto-commit disabled for " + sqldataSource.getDataSourceName());
con.commit();
}
} catch (SQLException E) {
cfEngine.log("Error updating CLIENT data for " + CFID + "-" + CFTOKEN + ": " + E);
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_APPLICATION);
catchData.setDetail("Database error: " + E.getMessage());
catchData.setMessage("Error occurred when attempting to save client data to database");
throw new cfmRunTimeException(catchData);
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException ignore) {
}
}
}
// INSERT or UPDATE app-specific data?
Map<String, cfData> appData = getAppData();
try {
con = sqldataSource.getConnection();
PreparedStatement statmt = null;
// --[ select the SQL string
if (loadedAppFromTable) { // UPDATE since the data is already in the table
statmt = con.prepareStatement(SQL_UPDATE_DATA);
} else { // otherwise entry not already there so INSERT
statmt = con.prepareStatement(SQL_INSERT_DATA);
}
// --[ set the parameters. Note the 2 SQL statements list the params in
// the same order
String tmp;
tmp = encodeData(appData);
statmt.setAsciiStream(1, new ByteArrayInputStream(tmp.getBytes()), tmp.length());
if (loadedAppFromTable && (COMPARISON_OP == LIKE)) {
statmt.setString(2, key + "%");
statmt.setString(3, appName + "%");
} else {
statmt.setString(2, key);
statmt.setString(3, appName);
}
statmt.executeUpdate();
statmt.close();
if (!con.getAutoCommit()) {
cfEngine.log("CLIENT data update: auto-commit disabled for " + sqldataSource.getDataSourceName());
con.commit();
}
} catch (SQLException E) {
cfEngine.log("Error updating CLIENT data for " + CFID + "-" + CFTOKEN + ": " + E);
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_APPLICATION);
catchData.setDetail("Database error: " + E.getMessage());
catchData.setMessage("Error occurred when attempting to save client data to database");
throw new cfmRunTimeException(catchData);
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException ignore) {
}
}
}
}
private void saveDBOld() throws cfmRunTimeException {
Connection Con = null;
// --[ Insert the new one
if (bLoadedFromTable) {
try {
Con = sqldataSource.getConnection();
PreparedStatement Statmt = Con.prepareStatement(SQL_UPDATE);
Statmt.setString(2, CFID + "-" + CFTOKEN);
String tmp = encode(getHashData());
Statmt.setAsciiStream(1, new ByteArrayInputStream(tmp.getBytes()), tmp.length());
Statmt.executeUpdate();
Statmt.close();
if (!Con.getAutoCommit()) {
cfEngine.log("CLIENT data update: auto-commit disabled for " + sqldataSource.getDataSourceName());
Con.commit();
}
} catch (SQLException E) {
cfEngine.log("Error updating CLIENT data for " + CFID + "-" + CFTOKEN + ": " + E);
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_APPLICATION);
catchData.setDetail("Database error: " + E.getMessage());
catchData.setMessage("Error occurred when attempting to save client data to database");
throw new cfmRunTimeException(catchData);
} finally {
if (Con != null) {
try {
Con.close();
} catch (SQLException ignore) {
}
}
}
} else {
try {
Con = sqldataSource.getConnection();
PreparedStatement Statmt = Con.prepareStatement(SQL_INSERT);
Statmt.setString(1, CFID + "-" + CFTOKEN);
String tmp = encode(getHashData());
Statmt.setAsciiStream(2, new ByteArrayInputStream(tmp.getBytes()), tmp.length());
Statmt.executeUpdate();
Statmt.close();
if (!Con.getAutoCommit()) {
cfEngine.log("CLIENT data insert: auto-commit disabled for " + sqldataSource.getDataSourceName());
Con.commit();
}
} catch (Exception E) {
cfEngine.log("Error inserting CLIENT data for " + CFID + "-" + CFTOKEN + ": " + E);
cfCatchData catchData = new cfCatchData();
catchData.setType(cfCatchData.TYPE_APPLICATION);
catchData.setDetail("Database error: " + E.getMessage());
catchData.setMessage("Error occurred when attempting to save client data to database");
throw new cfmRunTimeException(catchData);
} finally {
if (Con != null) {
try {
Con.close();
} catch (SQLException ignore) {
}
}
}
}
}
// ---------------------------------------------------------
// creates the tables BDGLOBAL/BDDATA if they don't already exist at the
// specified datasource
private static boolean createTable(cfDataSource _sqldataSource) {
Connection Con = null;
try {
Con = _sqldataSource.getConnection();
boolean bdGlobalExists = com.nary.db.metaDatabase.tableExist(Con, _sqldataSource.getCatalog(), GLOBAL_TABLE_NAME);
boolean bdDataExists = com.nary.db.metaDatabase.tableExist(Con, _sqldataSource.getCatalog(), DATA_TABLE_NAME);
// If both tables already exist then return true
if (bdGlobalExists && bdDataExists)
return true;
if (!bdGlobalExists) {
// --[ The table doesn't exist, now we need to attempt to create it
boolean bFound = false;
for (int x = 0; x < SQL_CREATE_GLOBAL.length; x++) {
if (com.nary.db.metaDatabase.createTable(Con, SQL_CREATE_GLOBAL[x])) {
bFound = true;
break;
} else {
cfEngine.log("-] Application.Client.FailedToCreateTable:[" + SQL_CREATE_GLOBAL[x] + "]");
}
}
if (!bFound) { // if there's an error return now, else continue
return bFound;
}
}
// check also that the BDDATA table exists. We could assume that it exists
// if BDGLOBAL
// exists but best to be tolerant
if (!bdDataExists) {
// --[ The table doesn't exist, now we need to attempt to create it
boolean bFound = false;
for (int x = 0; x < SQL_CREATE_DATA.length; x++) {
if (com.nary.db.metaDatabase.createTable(Con, SQL_CREATE_DATA[x])) {
bFound = true;
break;
} else {
cfEngine.log("-] Application.Client.FailedToCreateTable:[" + SQL_CREATE_DATA[x] + "]");
}
}
return bFound;
}
} catch (Exception E) {
cfEngine.log("-] Application.Client.FailedToGetConnection:[" + E + "]");
} finally {
if (Con != null) {
try {
Con.close();
} catch (SQLException ignore) {
}
}
}
return false;
}
private boolean loadData(cfDataSource _sqldataSource) {
Connection Con = null;
ResultSet Res;
PreparedStatement Statmt;
boolean bDeleteClientRow = false;
try {
Con = _sqldataSource.getConnection();
Statmt = Con.prepareStatement(SQL_SELECT);
Statmt.setString(1, CFID + "-" + CFTOKEN);
Res = Statmt.executeQuery();
if (Res.next()) {
Map<String, cfData> hashdata = decode(Res.getString(1));
if (hashdata != null) {
setHashData(hashdata);
bLoadedFromTable = true;
} else {
cfEngine.log("Deleting bad CLIENT data for: " + CFID + "-" + CFTOKEN);
bDeleteClientRow = true;
// the data in the table is bad--so delete it
}
}
// --[ Close off the database connections
Res.close();
Statmt.close();
// --[ Since the connections are closed off, we can delete any bad data
// sitting in the table
if (bDeleteClientRow)
deleteClientData(Con);
return true;
} catch (Exception E) {
// this exception is expected when the BDCLIENTDATA table hasn't been
// created yet
// CFID+"-"+CFTOKEN + ": " + E );
} finally {
if (Con != null) {
try {
Con.close();
} catch (SQLException ignore) {
}
}
}
return false;
}
private boolean loadDataNew(cfDataSource _sqldataSource) {
Connection Con = null;
ResultSet Res = null;
PreparedStatement Statmt = null;
// first load global data
try {
String key = CFID + (cfApplicationManager.cf5ClientData ? ":" : "-") + CFTOKEN;
Con = _sqldataSource.getConnection();
Statmt = Con.prepareStatement(SQL_SELECT_GLOBAL);
if (COMPARISON_OP == LIKE) {
Statmt.setString(1, key + "%");
} else {
Statmt.setString(1, key);
}
Res = Statmt.executeQuery();
Map<String, cfData> globalData = null;
Map<String, cfData> appData = null;
if (Res.next()) {
loadedGlobalFromTable = true;
globalData = decodeData(Res.getString(1));
if (cfApplicationManager.cf5ClientData) {
globalData.put("URLTOKEN", new cfStringData("CFID=" + CFID + "&CFTOKEN=" + CFTOKEN));
}
}
// --[ Close off the database connections in preparation for next query
Res.close();
Statmt.close();
// --[ Now load the app-specific data
Statmt = Con.prepareStatement(SQL_SELECT_DATA);
if (COMPARISON_OP == LIKE) {
Statmt.setString(1, key + "%");
Statmt.setString(2, appName + "%");
} else {
Statmt.setString(1, key);
Statmt.setString(2, appName);
}
Res = Statmt.executeQuery();
if (Res.next()) {
loadedAppFromTable = true;
appData = decodeData(Res.getString(1));
}
// --[ Close off the database connections
Res.close();
Statmt.close();
Map<String, cfData> hashdata = new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE);
if (globalData != null) {
hashdata.putAll(globalData);
}
if (appData != null) {
hashdata.putAll(appData);
}
setHashData(hashdata);
return true;
} catch (Exception E) {
// this exception is expected when the BDCLIENTDATA table hasn't been
// created yet
// CFID+"-"+CFTOKEN + ": " + E );
// If an exception was thrown then make sure we close the resultset and
// statement
if (Res != null) {
try {
Res.close();
} catch (java.sql.SQLException e1) {
}
}
if (Statmt != null) {
try {
Statmt.close();
} catch (java.sql.SQLException e1) {
}
}
} finally {
if (Con != null) {
try {
Con.close();
} catch (SQLException ignore) {
}
}
}
return false;
}
private String encodeData(Map<String, cfData> globalData) throws dataNotSupportedException {
if (cfApplicationManager.cf5ClientData) {
return encodeColdFusionData(globalData);
}
return encode(globalData);
}
private Map<String, cfData> decodeData(String data) throws IOException {
if (cfApplicationManager.cf5ClientData) {
return decodeColdFusionData(data);
}
return decode(data);
}
// method for deleting client data (old version)
private void deleteClientData(Connection Con) {
PreparedStatement Statmt = null;
try {
Statmt = Con.prepareStatement(SQL_DELETE);
Statmt.setString(1, CFID + "-" + CFTOKEN);
Statmt.executeUpdate();
} catch (Exception e) {
cfEngine.log("Error deleting CLIENT data for " + CFID + "-" + CFTOKEN + ": " + e);
} finally {
try {
if (Statmt != null)
Statmt.close();
} catch (Exception ignoreException2) {
}
}
}
// -------------------------------------------------------------------
// --[ Routines to save/load to COOKIE
// -------------------------------------------------------------------
private void saveCookie(cfSession _Session) {
Map<String, cfData> globalData = getGlobalData();
// take a copy of the client scope removing all the global variables leaving
// us with the app specific variables
Map<String, cfData> appData = getAppData();
try {
cfCookieData cookieHolder = (cfCookieData) _Session.getQualifiedData(variableStore.COOKIE_SCOPE);
String encodedGlobalData = encodeData(globalData);
// The Netscape cookie spec. limits the cookie name and value to 4k so log
// a warning if they are longer than that.
int cookieLen = GLOBAL_COOKIE_NAME.length() + encodedGlobalData.length();
if (cookieLen > 4096)
cfEngine.log("-] WARNING: the client global data cookie is greater than 4k so some browsers may not accept it.(len=" + cookieLen + ")");
Cookie globCookie = new Cookie(GLOBAL_COOKIE_NAME, encodedGlobalData);
globCookie.setMaxAge(9 * 365 * 24 * 60 * 60);
globCookie.setPath("/");
cookieHolder.setData(globCookie, true);
String encodedAppData = encodeData(appData);
// The Netscape cookie spec. limits the cookie name and value to 4k so log
// a warning if they are longer than that; we want to remove spaces from
// the cookie name
String cookieName = (CLIENT_COOKIE_NAME + appName).trim();
cookieName = cookieName.replace(' ', '_');
cookieLen = cookieName.length() + encodedGlobalData.length();
if (cookieLen > 4096)
cfEngine.log("-] WARNING: the client application data cookie for " + appName + " is greater than 4k so some browsers may not accept it.(len=" + cookieLen + ")");
Cookie appCookie = new Cookie(cookieName, encodedAppData);
appCookie.setMaxAge(9 * 365 * 24 * 60 * 60);
appCookie.setPath("/");
cookieHolder.setData(appCookie, true);
} catch (cfmRunTimeException e) {
}
}
private void loadCookie(cfSession _Session) {
// CFGLOBALS - urltoken, lastvisit, timecreated, hitcount, cftoken, cfid
Map<String, cfData> hashdata = new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE);
try {
cfData cookieData = _Session.getQualifiedData(variableStore.COOKIE_SCOPE);
// load global data
Map<String, cfData> globData = null;
if (cookieData != null) {
cfData data = cookieData.getData(GLOBAL_COOKIE_NAME);
if (data != null) {
globData = decodeData(data.getString());
}
}
// if there's no client data found in the cookie scope
if (globData == null) {
setHashData(new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE));
initialize();
} else {
// add global data to scope
Iterator<String> keys = globData.keySet().iterator();
while (keys.hasNext()) {
String nextKey = keys.next();
hashdata.put(nextKey, globData.get(nextKey));
}
// load app-specific client data
Map<String, cfData> appData = null;
if (cookieData != null) {
cfData data = cookieData.getData((CLIENT_COOKIE_NAME + appName.replace(' ', '_')).trim());
if (data != null)
appData = decodeData(data.getString());
}
if (appData != null) {
keys = appData.keySet().iterator();
while (keys.hasNext()) {
String nextKey = keys.next();
hashdata.put(nextKey, appData.get(nextKey));
}
}
setHashData(hashdata);
}
} catch (IOException ie) {
setHashData(new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE));
initialize();
} catch (cfmRunTimeException E) {
setHashData(new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE));
initialize();
}
}
// -------------------------------------------------------------------
// --[ Routines for encoding and decoding the string data
// -------------------------------------------------------------------
// returns a map with all the application specific data i.e. variables such as
// hitcount are ommitted
private Map<String, cfData> getAppData() {
Map<String, cfData> appData = new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE);
appData.putAll(getHashData());
appData.remove("urltoken");
appData.remove("lastvisit");
appData.remove("timecreated");
appData.remove("hitcount");
appData.remove("cftoken");
appData.remove("cfid");
return appData;
}
private Map<String, cfData> getGlobalData() {
Map<String, cfData> hashdata = getHashData();
Map<String, cfData> globalData = new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE);
cfData nextItem;
if ((nextItem = hashdata.get("hitcount")) != null) {
globalData.put("hitcount", nextItem);
} else {
globalData.put("hitcount", new cfNumberData(1));
}
if ((nextItem = hashdata.get("lastvisit")) != null) {
globalData.put("lastvisit", nextItem);
} else {
globalData.put("lastvisit", new cfDateData(System.currentTimeMillis()));
}
if ((nextItem = hashdata.get("timecreated")) != null) {
globalData.put("timecreated", nextItem);
} else {
globalData.put("timecreated", new cfDateData(System.currentTimeMillis()));
}
if ((nextItem = hashdata.get("urltoken")) != null) {
globalData.put("urltoken", nextItem);
} else {
globalData.put("urltoken", new cfStringData("CFID=" + CFID + "&CFTOKEN=" + CFTOKEN));
}
if ((nextItem = hashdata.get("cftoken")) != null) {
globalData.put("cftoken", nextItem);
} else {
globalData.put("cftoken", new cfStringData(CFTOKEN));
}
if ((nextItem = hashdata.get("cfid")) != null) {
globalData.put("cfid", nextItem);
} else {
globalData.put("cfid", new cfStringData(CFID));
}
return globalData;
}
private static String encode(Map<String, cfData> hashdata) {
try {
ByteArrayOutputStream FS = new ByteArrayOutputStream();
ObjectOutputStream OS = new ObjectOutputStream(FS);
OS.writeObject(hashdata);
OS.close();
return new String(com.nary.net.Base64.base64Encode(FS.toByteArray()));
} catch (Exception E) {
cfEngine.log("Error encoding CLIENT data: " + E);
return "";
}
}
private static Map<String, cfData> decode(String data64) {
if (data64 == null)
return null;
ObjectInputStream IS = null;
try {
ByteArrayInputStream FS = new ByteArrayInputStream(com.nary.net.Base64.base64Decode(data64.getBytes()));
IS = new ObjectInputStream(FS);
return (Map<String, cfData>) IS.readObject();
} catch (Exception E) {
cfEngine.log("Error decoding CLIENT data: " + E);
return null;
} finally {
try {
if (IS != null)
IS.close();
} catch (IOException ignore) {
}
}
}
/**
* ColdFusion (CF5 and CFMX) encode client data as a sequence of name=value
* pairs delimited by hash symbols (#); for example:
*
* name1=value1#name2=value2#name3=value3
*
* A double-hash (##) is not treated as a delimiter, but as part of the value;
* the value associated with "name2" is "value2##contains##hashes":
*
* name1=value1#name2=value2##contains##hashes#name3=value3
*
* Further, CFMX seems to escape equals signs (=) embeded within values by
* preceding them with a hash, though CF5 does not, and CFMX can properly read
* client data written by CF5; so escaping embedded equals seems optional. For
* example, the value associated with "name2" is "color=red" on both CF5 and
* CFMX:
*
* name1=value1#name2=color=red#name3=value3
*
* On CFMX with optional escaping it would appear like this:
*
* name1=value1#name2=color#=red#name3=value3
*
* Therefore, our implementation, below, has the ability to read escaped
* equals signs, but does not escape them when writing out client data.
*/
private static Map<String, cfData> decodeColdFusionData(String data) throws IOException {
if (data == null) {
return null;
}
Map<String, cfData> dataMap = new FastMap<String, cfData>(FastMap.CASE_INSENSITIVE);
StringBuilder sb = new StringBuilder();
Reader reader = new BufferedReader(new StringReader(data));
String name = null;
int ch;
while ((ch = reader.read()) != -1) {
// prior to reading name, = is terminator for name, after that it's part
// of the value
if ((name == null) && (ch == '=')) {
name = sb.toString();
sb.setLength(0);
} else if (ch == '#') { // look at the next char for escaped # or =
int next = reader.read();
if (next == '#') { // write out both # chars
sb.append((char) ch);
sb.append((char) next);
} else if (next == '=') { // discard the # and write out the =
sb.append((char) next);
} else { // # is value terminator
dataMap.put(name, new cfStringData(sb.toString()));
if (next == -1) { // we're done
break;
} else { // reset for next name=value pair
sb.setLength(0);
sb.append((char) next);
name = null;
}
}
} else {
sb.append((char) ch);
}
}
return dataMap;
}
private static String encodeColdFusionData(Map<String, cfData> data) throws dataNotSupportedException {
StringBuilder sb = new StringBuilder();
Iterator<String> iter = data.keySet().iterator();
while (iter.hasNext()) {
// CF5 writes names out in uppercase; CFMX in lowercase
String name = iter.next().toUpperCase();
sb.append(name);
sb.append('=');
sb.append(((cfData) data.get(name)).getString());
sb.append('#');
}
return sb.toString();
}
}