X-Git-Url: http://www.project-moonshot.org/gitweb/?p=shibboleth%2Fsp.git;a=blobdiff_plain;f=odbc-store%2Fodbc-store.cpp;h=45f684e78a3511323250f46697c0c17949590ae1;hp=8f61b064fb15100b6ce90ea67611704c35ebb63d;hb=41a4ab7a19274d18e207e3ed0933e6058ff559f6;hpb=fb2ed1c221e7f0176c8864d59a54740ae8846195 diff --git a/odbc-store/odbc-store.cpp b/odbc-store/odbc-store.cpp index 8f61b06..45f684e 100644 --- a/odbc-store/odbc-store.cpp +++ b/odbc-store/odbc-store.cpp @@ -14,13 +14,12 @@ * limitations under the License. */ -/* - * odbc-store.cpp - Storage service using ODBC +/** + * odbc-store.cpp * - * $Id$ + * Storage Service using ODBC */ -// eventually we might be able to support autoconf via cygwin... #if defined (_MSC_VER) || defined(__BORLANDC__) # include "config_win32.h" #else @@ -30,131 +29,257 @@ #ifdef WIN32 # define _CRT_NONSTDC_NO_DEPRECATE 1 # define _CRT_SECURE_NO_DEPRECATE 1 -# define NOMINMAX -# define SHIBODBC_EXPORTS __declspec(dllexport) +#endif + +#ifdef WIN32 +# define ODBCSTORE_EXPORTS __declspec(dllexport) #else -# define SHIBODBC_EXPORTS +# define ODBCSTORE_EXPORTS #endif -#include -#include -#include +#include +#include +#include #include +#include #include - -#include -#include -#include +#include #include #include -#ifdef HAVE_LIBDMALLOCXX -#include -#endif - -using namespace shibsp; -using namespace shibtarget; -using namespace opensaml::saml2md; -using namespace saml; +using namespace xmltooling::logging; using namespace xmltooling; -using namespace log4cpp; +using namespace xercesc; using namespace std; -#define PLUGIN_VER_MAJOR 3 +#define PLUGIN_VER_MAJOR 1 #define PLUGIN_VER_MINOR 0 -#define COLSIZE_KEY 64 -#define COLSIZE_CONTEXT 256 -#define COLSIZE_STRING_VALUE 256 - - -/* tables definitions - not used here */ - -#define STRING_TABLE "STRING_STORE" - -#define STRING_TABLE \ - "CREATE TABLE STRING_TABLE ( "\ - "context VARCHAR( COLSIZE_CONTEXT ), " \ - "key VARCHAR( COLSIZE_KEY ), " \ - "value VARCHAR( COLSIZE_STRING_VALUE ), " \ - "expires TIMESTAMP, " - "PRIMARY KEY (context, key), " - "INDEX (context))" +#define LONGDATA_BUFLEN 16384 + +#define COLSIZE_CONTEXT 255 +#define COLSIZE_ID 255 +#define COLSIZE_STRING_VALUE 255 + +#define STRING_TABLE "strings" +#define TEXT_TABLE "texts" + +/* table definitions +CREATE TABLE version ( + major tinyint NOT NULL, + minor tinyint NOT NULL + ) + +CREATE TABLE strings ( + context varchar(255) not null, + id varchar(255) not null, + expires datetime not null, + version smallint not null, + value varchar(255) not null, + PRIMARY KEY (context, id) + ) + +CREATE TABLE texts ( + context varchar(255) not null, + id varchar(255) not null, + expires datetime not null, + version smallint not null, + value text not null, + PRIMARY KEY (context, id) + ) +*/ + +namespace { + static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l); + static const XMLCh isolationLevel[] = UNICODE_LITERAL_14(i,s,o,l,a,t,i,o,n,L,e,v,e,l); + static const XMLCh ConnectionString[] = UNICODE_LITERAL_16(C,o,n,n,e,c,t,i,o,n,S,t,r,i,n,g); + + // RAII for ODBC handles + struct ODBCConn { + ODBCConn(SQLHDBC conn) : handle(conn), autoCommit(true) {} + ~ODBCConn() { + SQLRETURN sr = SQL_SUCCESS; + if (!autoCommit) + sr = SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, NULL); + SQLDisconnect(handle); + SQLFreeHandle(SQL_HANDLE_DBC,handle); + if (!SQL_SUCCEEDED(sr)) + throw IOException("Failed to commit connection and return to auto-commit mode."); + } + operator SQLHDBC() {return handle;} + SQLHDBC handle; + bool autoCommit; + }; + class ODBCStorageService : public StorageService + { + public: + ODBCStorageService(const DOMElement* e); + virtual ~ODBCStorageService(); -#define TEXT_TABLE "TEXT_STORE" + bool createString(const char* context, const char* key, const char* value, time_t expiration) { + return createRow(STRING_TABLE, context, key, value, expiration); + } + int readString(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) { + return readRow(STRING_TABLE, context, key, pvalue, pexpiration, version, false); + } + int updateString(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) { + return updateRow(STRING_TABLE, context, key, value, expiration, version); + } + bool deleteString(const char* context, const char* key) { + return deleteRow(STRING_TABLE, context, key); + } -#define TEXT_TABLE \ - "CREATE TABLE TEXT_TABLE ( "\ - "context VARCHAR( COLSIZE_CONTEXT ), " \ - "key VARCHAR( COLSIZE_KEY ), " \ - "value LONG TEXT, " \ - "expires TIMESTAMP, " - "PRIMARY KEY (context, key), " - "INDEX (context))" + bool createText(const char* context, const char* key, const char* value, time_t expiration) { + return createRow(TEXT_TABLE, context, key, value, expiration); + } + int readText(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) { + return readRow(TEXT_TABLE, context, key, pvalue, pexpiration, version, true); + } + int updateText(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) { + return updateRow(TEXT_TABLE, context, key, value, expiration, version); + } + bool deleteText(const char* context, const char* key) { + return deleteRow(TEXT_TABLE, context, key); + } + void reap(const char* context) { + reap(STRING_TABLE, context); + reap(TEXT_TABLE, context); + } + void updateContext(const char* context, time_t expiration) { + updateContext(STRING_TABLE, context, expiration); + updateContext(TEXT_TABLE, context, expiration); + } + void deleteContext(const char* context) { + deleteContext(STRING_TABLE, context); + deleteContext(TEXT_TABLE, context); + } + -static const XMLCh ConnectionString[] = -{ chLatin_C, chLatin_o, chLatin_n, chLatin_n, chLatin_e, chLatin_c, chLatin_t, chLatin_i, chLatin_o, chLatin_n, - chLatin_S, chLatin_t, chLatin_r, chLatin_i, chLatin_n, chLatin_g, chNull -}; -static const XMLCh cleanupInterval[] = -{ chLatin_c, chLatin_l, chLatin_e, chLatin_a, chLatin_n, chLatin_u, chLatin_p, - chLatin_I, chLatin_n, chLatin_t, chLatin_e, chLatin_r, chLatin_v, chLatin_a, chLatin_l, chNull -}; -static const XMLCh cacheTimeout[] = -{ chLatin_c, chLatin_a, chLatin_c, chLatin_h, chLatin_e, chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull }; -static const XMLCh odbcTimeout[] = -{ chLatin_o, chLatin_d, chLatin_b, chLatin_c, chLatin_T, chLatin_i, chLatin_m, chLatin_e, chLatin_o, chLatin_u, chLatin_t, chNull }; -static const XMLCh storeAttributes[] = -{ chLatin_s, chLatin_t, chLatin_o, chLatin_r, chLatin_e, chLatin_A, chLatin_t, chLatin_t, chLatin_r, chLatin_i, chLatin_b, chLatin_u, chLatin_t, chLatin_e, chLatin_s, chNull }; + private: + bool createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration); + int readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text); + int updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version); + bool deleteRow(const char *table, const char* context, const char* key); -static const XMLCh cleanupInterval[] = UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l); + void reap(const char* table, const char* context); + void updateContext(const char* table, const char* context, time_t expiration); + void deleteContext(const char* table, const char* context); + SQLHDBC getHDBC(); + SQLHSTMT getHSTMT(SQLHDBC); + pair getVersion(SQLHDBC); + bool log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor=NULL); -// ODBC tools + static void* cleanup_fn(void*); + void cleanup(); -struct ODBCConn { - ODBCConn(SQLHDBC conn) : handle(conn) {} - ~ODBCConn() {SQLFreeHandle(SQL_HANDLE_DBC,handle);} - operator SQLHDBC() {return handle;} - SQLHDBC handle; -}; + Category& m_log; + int m_cleanupInterval; + CondWait* shutdown_wait; + Thread* cleanup_thread; + bool shutdown; -class ODBCBase : public virtual saml::IPlugIn -{ -public: - ODBCBase(const DOMElement* e); - virtual ~ODBCBase(); + SQLHENV m_henv; + string m_connstring; + long m_isolation; + }; - SQLHDBC getHDBC(); + StorageService* ODBCStorageServiceFactory(const DOMElement* const & e) + { + return new ODBCStorageService(e); + } - Category* log; + // convert SQL timestamp to time_t + time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires) + { + time_t ret; + struct tm t; + t.tm_sec=expires.second; + t.tm_min=expires.minute; + t.tm_hour=expires.hour; + t.tm_mday=expires.day; + t.tm_mon=expires.month-1; + t.tm_year=expires.year-1900; + t.tm_isdst=0; +#if defined(HAVE_TIMEGM) + ret = timegm(&t); +#else + ret = mktime(&t) - timezone; +#endif + return (ret); + } -protected: - const DOMElement* m_root; // can only use this during initialization - string m_connstring; + // conver time_t to SQL string + void timestampFromTime(time_t t, char* ret) + { +#ifdef HAVE_GMTIME_R + struct tm res; + struct tm* ptime=gmtime_r(&t,&res); +#else + struct tm* ptime=gmtime(&t); +#endif + strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime); + } - static SQLHENV m_henv; // single handle for both plugins - bool m_bInitializedODBC; // tracks which class handled the process - static const char* p_connstring; + // make a string safe for SQL command + // result to be free'd only if it isn't the input + static char *makeSafeSQL(const char *src) + { + int ns = 0; + int nc = 0; + char *s; + + // see if any conversion needed + for (s=(char*)src; *s; nc++,s++) if (*s=='\'') ns++; + if (ns==0) return ((char*)src); + + char *safe = new char[(nc+2*ns+1)]; + for (s=safe; *src; src++) { + if (*src=='\'') *s++ = '\''; + *s++ = (char)*src; + } + *s = '\0'; + return (safe); + } - pair getVersion(SQLHDBC); - void log_error(SQLHANDLE handle, SQLSMALLINT htype); + void freeSafeSQL(char *safe, const char *src) + { + if (safe!=src) + delete[](safe); + } }; -SQLHENV ODBCBase::m_henv = SQL_NULL_HANDLE; -const char* ODBCBase::p_connstring = NULL; - -ODBCBase::ODBCBase(const DOMElement* e) : m_root(e), m_bInitializedODBC(false) +ODBCStorageService::ODBCStorageService(const DOMElement* e) : m_log(Category::getInstance("XMLTooling.StorageService")), + m_cleanupInterval(900), shutdown_wait(NULL), cleanup_thread(NULL), shutdown(false), m_henv(SQL_NULL_HANDLE), m_isolation(SQL_TXN_SERIALIZABLE) { #ifdef _DEBUG - xmltooling::NDC ndc("ODBCBase"); + xmltooling::NDC ndc("ODBCStorageService"); #endif - log = &(Category::getInstance("shibtarget.ODBC")); + + const XMLCh* tag=e ? e->getAttributeNS(NULL,cleanupInterval) : NULL; + if (tag && *tag) + m_cleanupInterval = XMLString::parseInt(tag); + if (!m_cleanupInterval) + m_cleanupInterval = 900; + + auto_ptr_char iso(e ? e->getAttributeNS(NULL,isolationLevel) : NULL); + if (iso.get() && *iso.get()) { + if (!strcmp(iso.get(),"SERIALIZABLE")) + m_isolation = SQL_TXN_SERIALIZABLE; + else if (!strcmp(iso.get(),"REPEATABLE_READ")) + m_isolation = SQL_TXN_REPEATABLE_READ; + else if (!strcmp(iso.get(),"READ_COMMITTED")) + m_isolation = SQL_TXN_READ_COMMITTED; + else if (!strcmp(iso.get(),"READ_UNCOMMITTED")) + m_isolation = SQL_TXN_READ_UNCOMMITTED; + else + throw XMLToolingException("Unknown transaction isolationLevel property."); + } if (m_henv == SQL_NULL_HANDLE) { // Enable connection pooling. @@ -162,54 +287,50 @@ ODBCBase::ODBCBase(const DOMElement* e) : m_root(e), m_bInitializedODBC(false) // Allocate the environment. if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_henv))) - throw ConfigurationException("ODBC failed to initialize."); + throw XMLToolingException("ODBC failed to initialize."); // Specify ODBC 3.x SQLSetEnvAttr(m_henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0); - log->info("ODBC initialized"); - m_bInitializedODBC = true; + m_log.info("ODBC initialized"); } // Grab connection string from the configuration. - e=XMLHelper::getFirstChildElement(e,ConnectionString); + e = e ? XMLHelper::getFirstChildElement(e,ConnectionString) : NULL; if (!e || !e->hasChildNodes()) { - if (!p_connstring) { - this->~ODBCBase(); - throw ConfigurationException("ODBC cache requires ConnectionString element in configuration."); - } - m_connstring=p_connstring; - } - else { - xmltooling::auto_ptr_char arg(e->getFirstChild()->getNodeValue()); - m_connstring=arg.get(); - p_connstring=m_connstring.c_str(); + SQLFreeHandle(SQL_HANDLE_ENV, m_henv); + throw XMLToolingException("ODBC StorageService requires ConnectionString element in configuration."); } + auto_ptr_char arg(e->getFirstChild()->getNodeValue()); + m_connstring=arg.get(); // Connect and check version. - SQLHDBC conn=getHDBC(); + ODBCConn conn(getHDBC()); pair v=getVersion(conn); - SQLFreeHandle(SQL_HANDLE_DBC,conn); // Make sure we've got the right version. if (v.first != PLUGIN_VER_MAJOR) { - this->~ODBCBase(); - log->crit("unknown database version: %d.%d", v.first, v.second); - throw SAMLException("Unknown cache database version."); + SQLFreeHandle(SQL_HANDLE_ENV, m_henv); + m_log.crit("unknown database version: %d.%d", v.first, v.second); + throw XMLToolingException("Unknown database version for ODBC StorageService."); } + + // Initialize the cleanup thread + shutdown_wait = CondWait::create(); + cleanup_thread = Thread::create(&cleanup_fn, (void*)this); } -ODBCBase::~ODBCBase() +ODBCStorageService::~ODBCStorageService() { - //delete m_mysql; - if (m_bInitializedODBC) - SQLFreeHandle(SQL_HANDLE_ENV,m_henv); - m_bInitializedODBC=false; - m_henv = SQL_NULL_HANDLE; - p_connstring=NULL; + shutdown = true; + shutdown_wait->signal(); + cleanup_thread->join(NULL); + delete shutdown_wait; + if (m_henv != SQL_NULL_HANDLE) + SQLFreeHandle(SQL_HANDLE_ENV, m_henv); } -void ODBCBase::log_error(SQLHANDLE handle, SQLSMALLINT htype) +bool ODBCStorageService::log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor) { SQLSMALLINT i = 0; SQLINTEGER native; @@ -218,216 +339,84 @@ void ODBCBase::log_error(SQLHANDLE handle, SQLSMALLINT htype) SQLSMALLINT len; SQLRETURN ret; + bool res = false; do { ret = SQLGetDiagRec(htype, handle, ++i, state, &native, text, sizeof(text), &len); - if (SQL_SUCCEEDED(ret)) - log->error("ODBC Error: %s:%ld:%ld:%s", state, i, native, text); + if (SQL_SUCCEEDED(ret)) { + m_log.error("ODBC Error: %s:%ld:%ld:%s", state, i, native, text); + if (checkfor && !strcmp(checkfor, (const char*)state)) + res = true; + } } while(SQL_SUCCEEDED(ret)); + return res; } -SQLHDBC ODBCBase::getHDBC() +SQLHDBC ODBCStorageService::getHDBC() { #ifdef _DEBUG - xmltooling::NDC ndc("getMYSQL"); + xmltooling::NDC ndc("getHDBC"); #endif // Get a handle. SQLHDBC handle; SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_DBC, m_henv, &handle); if (!SQL_SUCCEEDED(sr)) { - log->error("failed to allocate connection handle"); + m_log.error("failed to allocate connection handle"); log_error(m_henv, SQL_HANDLE_ENV); - throw SAMLException("ODBCBase::getHDBC failed to allocate connection handle"); + throw IOException("ODBC StorageService failed to allocate a connection handle."); } sr=SQLDriverConnect(handle,NULL,(SQLCHAR*)m_connstring.c_str(),m_connstring.length(),NULL,0,NULL,SQL_DRIVER_NOPROMPT); if (!SQL_SUCCEEDED(sr)) { - log->error("failed to connect to database"); + m_log.error("failed to connect to database"); log_error(handle, SQL_HANDLE_DBC); - throw SAMLException("ODBCBase::getHDBC failed to connect to database"); + throw IOException("ODBC StorageService failed to connect to database."); } + sr = SQLSetConnectAttr(handle, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)m_isolation, NULL); + if (!SQL_SUCCEEDED(sr)) + throw IOException("ODBC StorageService failed to set transaction isolation level."); + return handle; } -pair ODBCBase::getVersion(SQLHDBC conn) +SQLHSTMT ODBCStorageService::getHSTMT(SQLHDBC conn) { - // Grab the version number from the database. SQLHSTMT hstmt; - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); - - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)"SELECT major,minor FROM version", SQL_NTS); + SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); if (!SQL_SUCCEEDED(sr)) { - log->error("failed to read version from database"); - log_error(hstmt, SQL_HANDLE_STMT); - throw SAMLException("ODBCBase::getVersion failed to read version from database"); + m_log.error("failed to allocate statement handle"); + log_error(conn, SQL_HANDLE_DBC); + throw IOException("ODBC StorageService failed to allocate a statement handle."); } - - SQLINTEGER major; - SQLINTEGER minor; - SQLBindCol(hstmt,1,SQL_C_SLONG,&major,0,NULL); - SQLBindCol(hstmt,2,SQL_C_SLONG,&minor,0,NULL); - - if ((sr=SQLFetch(hstmt)) != SQL_NO_DATA) { - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return pair(major,minor); - } - - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - log->error("no rows returned in version query"); - throw SAMLException("ODBCBase::getVersion failed to read version from database"); + return hstmt; } - -// ------------------------------------------------------------ - -// ODBC Storage Service class - -class ODBCStorageService : public ODBCBase, public StorageService -{ - string stringTable = STRING_TABLE; - string textTable = TEXT_TABLE; - -public: - ODBCStorageService(const DOMElement* e); - virtual ~ODBCStorageService(); - - void createString(const char* context, const char* key, const char* value, time_t expiration) { - return createRow(string_table, context, key, value, expiration); - } - bool readString(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL) { - return readRow(string_table, context, key, value, expiration, COLSIZE_STRING_VALUE); - } - bool updateString(const char* context, const char* key, const char* value=NULL, time_t expiration=0) { - return updateRow(string_table, context, key, value, expiration); - } - bool deleteString(const char* context, const char* key) { - return deleteRow(string_table, context, key, value, expiration); - } - - void createText(const char* context, const char* key, const char* value, time_t expiration) { - return createRow(text_table, context, key, value, expiration); - } - bool readText(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL) { - return readRow(text_table, context, key, value, expiration, 0); - } - bool updateText(const char* context, const char* key, const char* value=NULL, time_t expiration=0) { - return updateRow(text_table, context, key, value, expiration); - } - bool deleteText(const char* context, const char* key) { - return deleteRow(text_table, context, key, value, expiration); - } - - void reap(const char* context) { - reap(string_table, context); - reap(text_table, context); - } - void deleteContext(const char* context) { - deleteCtx(string_table, context); - deleteCtx(text_table, context); - } - - -private: - - void createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration); - bool readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int maxsize); - bool updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration); - bool deleteRow(const char *table, const char* context, const char* key); - - void reapRows(const char* table, const char* context); - void deleteCtx(const char* table, const char* context); - - xmltooling::CondWait* shutdown_wait; - bool shutdown; - xmltooling::Thread* cleanup_thread; - - static void* cleanup_fcn(void*); - void cleanup(); - - CondWait* shutdown_wait; - Thread* cleanup_thread; - bool shutdown; - int m_cleanupInterval; - Category& log; - - StorageService* ODBCStorageServiceFactory(const DOMElement* const & e) - { - return new ODBCStorageService(e); - } - - // convert SQL timestamp to time_t - time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires) - { - time_t ret; - struct tm t; - t.tm_sec=expires.second; - t.tm_min=expires.minute; - t.tm_hour=expires.hour; - t.tm_mday=expires.day; - t.tm_mon=expires.month-1; - t.tm_year=expires.year-1900; - t.tm_isdst=0; -#if defined(HAVE_TIMEGM) - ret = timegm(&t); -#else - ret = mktime(&t) - timezone; -#endif - return (ret); - } - - // conver time_t to SQL string - void timestampFromTime(time_t t, char &ret) - { -#ifdef HAVE_GMTIME_R - struct tm res; - struct tm* ptime=gmtime_r(&created,&res); -#else - struct tm* ptime=gmtime(&created); -#endif - strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime); - } - -}; - -// class constructor - -ODBCStorageService::ODBCStorageService(const DOMElement* e): - ODBCBase(e), - shutdown(false), - m_cleanupInterval(0) - +pair ODBCStorageService::getVersion(SQLHDBC conn) { -#ifdef _DEBUG - xmltooling::NDC ndc("ODBCStorageService"); -#endif - log = &(Category::getInstance("shibtarget.StorageService.ODBC")); - - const XMLCh* tag=e ? e->getAttributeNS(NULL,cleanupInterval) : NULL; - if (tag && *tag) { - m_cleanupInterval = XMLString::parseInt(tag); + // Grab the version number from the database. + SQLHSTMT stmt = getHSTMT(conn); + + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)"SELECT major,minor FROM version", SQL_NTS); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("failed to read version from database"); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to read version from database."); } - if (!m_cleanupInterval) m_cleanupInterval=300; - - contextLock = Mutex::create(); - shutdown_wait = CondWait::create(); - // Initialize the cleanup thread - cleanup_thread = Thread::create(&cleanup_fcn, (void*)this); -} + SQLINTEGER major; + SQLINTEGER minor; + SQLBindCol(stmt,1,SQL_C_SLONG,&major,0,NULL); + SQLBindCol(stmt,2,SQL_C_SLONG,&minor,0,NULL); -ODBCStorageService::~ODBCStorageService() -{ - shutdown = true; - shutdown_wait->signal(); - cleanup_thread->join(NULL); + if ((sr=SQLFetch(stmt)) != SQL_NO_DATA) + return pair(major,minor); - delete shutdown_wait; + m_log.error("no rows returned in version query"); + throw IOException("ODBC StorageService failed to read version from database."); } -// create - -HRESULT ODBCStorageService::createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration) +bool ODBCStorageService::createRow(const char* table, const char* context, const char* key, const char* value, time_t expiration) { #ifdef _DEBUG xmltooling::NDC ndc("createRow"); @@ -437,207 +426,305 @@ HRESULT ODBCStorageService::createRow(const char *table, const char* context, co timestampFromTime(expiration, timebuf); // Get statement handle. - SQLHSTMT hstmt; ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLHSTMT stmt = getHSTMT(conn); // Prepare and exectute insert statement. - string q = string("INSERT ") + table + " VALUES ('" + context + "','" + key + "','" + value + "'," + timebuf + "')"; - log->debug("SQL: %s", q.str()); + //char *scontext = makeSafeSQL(context); + //char *skey = makeSafeSQL(key); + //char *svalue = makeSafeSQL(value); + string q = string("INSERT INTO ") + table + " VALUES (?,?," + timebuf + ",1,?)"; - HRESULT hr=NOERROR; - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)q.str().c_str(), SQL_NTS); + SQLRETURN sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); if (!SQL_SUCCEEDED(sr)) { - log->error("insert record failed (t=%s, c=%s, k=%s", table, context, key); - log_error(hstmt, SQL_HANDLE_STMT); - hr=E_FAIL; + m_log.error("SQLPrepare failed (t=%s, c=%s, k=%s)", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to insert record."); } + m_log.debug("SQLPrepare succeded. SQL: %s", q.c_str()); - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return hr; -} + SQLINTEGER b_ind = SQL_NTS; + sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast(context), &b_ind); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("SQLBindParam failed (context = %s)", context); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to insert record."); + } + m_log.debug("SQLBindParam succeded (context = %s)", context); + + sr = SQLBindParam(stmt, 2, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast(key), &b_ind); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("SQLBindParam failed (key = %s)", key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to insert record."); + } + m_log.debug("SQLBindParam succeded (key = %s)", key); + + if (strcmp(table, TEXT_TABLE)==0) + sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast(value), &b_ind); + else + sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast(value), &b_ind); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("SQLBindParam failed (value = %s)", value); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to insert record."); + } + m_log.debug("SQLBindParam succeded (value = %s)", value); + + //freeSafeSQL(scontext, context); + //freeSafeSQL(skey, key); + //freeSafeSQL(svalue, value); + //m_log.debug("SQL: %s", q.c_str()); -// read + sr=SQLExecute(stmt); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("insert record failed (t=%s, c=%s, k=%s)", table, context, key); + if (log_error(stmt, SQL_HANDLE_STMT, "23000")) + return false; // supposedly integrity violation? + throw IOException("ODBC StorageService failed to insert record."); + } -HRESULT ODBCStorageService::readRow(const char *table, const char* context, const char* key, string& pvalue, time_t& pexpiration, int maxsize) + m_log.debug("SQLExecute of insert succeeded"); + return true; +} + +int ODBCStorageService::readRow( + const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text + ) { #ifdef _DEBUG xmltooling::NDC ndc("readRow"); #endif - SQLCHAR *tvalue = NULL; - SQL_TIMESTAMP_STRUCT expires; - time_t exp; - // Get statement handle. - SQLHSTMT hstmt; ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLHSTMT stmt = getHSTMT(conn); // Prepare and exectute select statement. - string q = string("SELECT expires,value FROM ") + table + - " WHERE context='" + context + "' AND key='" + key + "'"; - log->debug("SQL: %s", q.str()); - - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)q.c_str(), SQL_NTS); + char timebuf[32]; + timestampFromTime(time(NULL), timebuf); + char *scontext = makeSafeSQL(context); + char *skey = makeSafeSQL(key); + ostringstream q; + q << "SELECT version"; + if (pexpiration) + q << ",expires"; + if (pvalue) + q << ",CASE version WHEN " << version << " THEN NULL ELSE value END"; + q << " FROM " << table << " WHERE context='" << scontext << "' AND id='" << skey << "' AND expires > " << timebuf; + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); + if (m_log.isDebugEnabled()) + m_log.debug("SQL: %s", q.str().c_str()); + + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.str().c_str(), SQL_NTS); if (!SQL_SUCCEEDED(sr)) { - log->error("error searching for (t=%s, c=%s, k=%s)", table, context, key); - log_error(hstmt, SQL_HANDLE_STMT); - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return E_FAIL; + m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService search failed."); } - // retrieve data - SQLBindCol(hstmt,1,SQL_C_TYPE_TIMESTAMP,&expires,0,NULL); - // SQLBindCol(hstmt,1,SQL_C_CHAR,value,sizeof(value),NULL); + SQLSMALLINT ver; + SQL_TIMESTAMP_STRUCT expiration; - if ((sr=SQLFetch(hstmt)) == SQL_NO_DATA) { - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return S_FALSE; - } + SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL); + if (pexpiration) + SQLBindCol(stmt,2,SQL_C_TYPE_TIMESTAMP,&expiration,0,NULL); - // expire time from bound col - exp = timeFromTimestamp(expires); - if (time(NULL)>ezp) { - log->debug(".. expired"); - return false; - } - if (pexpiration) pexpiration = exp; + if ((sr=SQLFetch(stmt)) == SQL_NO_DATA) + return 0; - // value by getdata + if (pexpiration) + *pexpiration = timeFromTimestamp(expiration); - // see how much text there is - if (maxsize==0) { - SQLINTEGER nch; - SQLCHAR tv[12]; - sr = SQLGetData(hstmt, 2, SQL_C_CHAR, tvp, BUFSIZE_TEXT_BLOCK, &nch); - if (sr==SQL_SUCCESS || sr==SQL_SUCCESS_WITH_INFO) { - maxsize = nch; - } - } + if (version == ver) + return version; // nothing's changed, so just echo back the version - tvalue = (SQLCHAR*) malloc(maxsize+1); - sr = SQLGetData(hstmt, 2, SQL_C_CHAR, tvalue, maxsize, &nch); - if (sr!=SQL_SUCCESS) { - log->error("error retriving value for (t=%s, c=%s, k=%s)", table, context, key); - log_error(hstmt, SQL_HANDLE_STMT); - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return E_FAIL; + if (pvalue) { + SQLINTEGER len; + SQLCHAR buf[LONGDATA_BUFLEN]; + while ((sr=SQLGetData(stmt,pexpiration ? 3 : 2,SQL_C_CHAR,buf,sizeof(buf),&len)) != SQL_NO_DATA) { + if (!SQL_SUCCEEDED(sr)) { + m_log.error("error while reading text field from result set"); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService search failed to read data from result set."); + } + pvalue->append((char*)buf); } } - pvalue = string(tvalue); - free(tvalue); - - log->debug(".. value found"); - - return sr; + + return ver; } - -// update - -HRESULT ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration) +int ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version) { #ifdef _DEBUG xmltooling::NDC ndc("updateRow"); #endif - char timebuf[32]; - timestampFromTime(expiration, timebuf); + if (!value && !expiration) + throw IOException("ODBC StorageService given invalid update instructions."); - // Get statement handle. - SQLHSTMT hstmt; + // Get statement handle. Disable auto-commit mode to wrap select + update. ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLRETURN sr = SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, NULL); + if (!SQL_SUCCEEDED(sr)) + throw IOException("ODBC StorageService failed to disable auto-commit mode."); + conn.autoCommit = false; + SQLHSTMT stmt = getHSTMT(conn); + + // First, fetch the current version for later, which also ensures the record still exists. + char timebuf[32]; + timestampFromTime(time(NULL), timebuf); + char *scontext = makeSafeSQL(context); + char *skey = makeSafeSQL(key); + string q("SELECT version FROM "); + q = q + table + " WHERE context='" + scontext + "' AND id='" + skey + "' AND expires > " + timebuf; + + m_log.debug("SQL: %s", q.c_str()); + + sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); + if (!SQL_SUCCEEDED(sr)) { + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); + m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService search failed."); + } + + SQLSMALLINT ver; + SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL); + if ((sr=SQLFetch(stmt)) == SQL_NO_DATA) { + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); + return 0; + } + + // Check version? + if (version > 0 && version != ver) { + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); + return -1; + } + + SQLFreeHandle(SQL_HANDLE_STMT, stmt); + stmt = getHSTMT(conn); // Prepare and exectute update statement. + q = string("UPDATE ") + table + " SET "; + + if (value) + q = q + "value=?, version=version+1"; - string expstr = ""; - if (expiration) expstr = string(",expires = '") + timebuf + "' "; + if (expiration) { + timestampFromTime(expiration, timebuf); + if (value) + q += ','; + q = q + "expires = " + timebuf; + } - string q = string("UPDATE ") + table + " SET value='" + value + "'" + expstr + - " WHERE context='" + context + "' AND key='" + key + "' AND expires > NOW()"; - log->debug("SQL: %s", q.str()); + q = q + " WHERE context='" + scontext + "' AND id='" + skey + "'"; + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); - HRESULT hr=NOERROR; - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)q.str().c_str(), SQL_NTS); + sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); if (!SQL_SUCCEEDED(sr)) { - log->error("update record failed (t=%s, c=%s, k=%s", table, context, key); - log_error(hstmt, SQL_HANDLE_STMT); - hr=E_FAIL; + m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to update record."); + } + m_log.debug("SQLPrepare succeded. SQL: %s", q.c_str()); + + SQLINTEGER b_ind = SQL_NTS; + if (value) { + if (strcmp(table, TEXT_TABLE)==0) + sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast(value), &b_ind); + else + sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast(value), &b_ind); + if (!SQL_SUCCEEDED(sr)) { + m_log.error("SQLBindParam failed (context = %s)", context); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to update record."); + } + m_log.debug("SQLBindParam succeded (context = %s)", context); } - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return hr; -} - + sr=SQLExecute(stmt); + if (sr==SQL_NO_DATA) + return 0; // went missing? + else if (!SQL_SUCCEEDED(sr)) { + m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to update record."); + } -// delete + m_log.debug("SQLExecute of update succeeded"); + return ver + 1; +} -HRESULT ODBCStorageService::deleteRow(const char *table, const char *context, const char* key) +bool ODBCStorageService::deleteRow(const char *table, const char *context, const char* key) { #ifdef _DEBUG xmltooling::NDC ndc("deleteRow"); #endif // Get statement handle. - SQLHSTMT hstmt; ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLHSTMT stmt = getHSTMT(conn); // Prepare and execute delete statement. - string q = string("DELETE FROM ") + table + " WHERE context='" + context + "' AND key='" + key + "'"; - log->debug("SQL: %s", q.str()); - - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)q.c_str(), SQL_NTS); - - HRESULT hr=NOERROR; - if (sr==SQL_NO_DATA) - hr=S_FALSE; + char *scontext = makeSafeSQL(context); + char *skey = makeSafeSQL(key); + string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "' AND id='" + skey + "'"; + freeSafeSQL(scontext, context); + freeSafeSQL(skey, key); + m_log.debug("SQL: %s", q.c_str()); + + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); + if (sr==SQL_NO_DATA) + return false; else if (!SQL_SUCCEEDED(sr)) { - log->error("error deleting record (t=%s, c=%s, k=%s)", table, context, key); - log_error(hstmt, SQL_HANDLE_STMT); - hr=E_FAIL; + m_log.error("error deleting record (t=%s, c=%s, k=%s)", table, context, key); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to delete record."); } - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); - return hr; + return true; } -// cleanup - delete expired entries - void ODBCStorageService::cleanup() { #ifdef _DEBUG xmltooling::NDC ndc("cleanup"); #endif - Mutex* mutex = xmltooling::Mutex::create(); - - int rerun_timer = 0; - int timeout_life = 0; + Mutex* mutex = Mutex::create(); mutex->lock(); - log->info("cleanup thread started... running every %d secs", m_cleanupInterval); + m_log.info("cleanup thread started... running every %d secs", m_cleanupInterval); while (!shutdown) { shutdown_wait->timedwait(mutex, m_cleanupInterval); - - if (shutdown) break; - - reap(NULL); + if (shutdown) + break; + try { + reap(NULL); + } + catch (exception& ex) { + m_log.error("cleanup thread swallowed exception: %s", ex.what()); + } } - log->info("cleanup thread exiting..."); + m_log.info("cleanup thread exiting..."); mutex->unlock(); delete mutex; - xmltooling::Thread::exit(NULL); + Thread::exit(NULL); } -void* ODBCStorageService::cleanup_fcn(void* cache_p) +void* ODBCStorageService::cleanup_fn(void* cache_p) { ODBCStorageService* cache = (ODBCStorageService*)cache_p; @@ -651,72 +738,101 @@ void* ODBCStorageService::cleanup_fcn(void* cache_p) return NULL; } +void ODBCStorageService::updateContext(const char *table, const char* context, time_t expiration) +{ +#ifdef _DEBUG + xmltooling::NDC ndc("updateContext"); +#endif + + // Get statement handle. + ODBCConn conn(getHDBC()); + SQLHSTMT stmt = getHSTMT(conn); -// remove expired entries for a context + char timebuf[32]; + timestampFromTime(expiration, timebuf); + + char nowbuf[32]; + timestampFromTime(time(NULL), nowbuf); + + char *scontext = makeSafeSQL(context); + string q("UPDATE "); + q = q + table + " SET expires = " + timebuf + " WHERE context='" + scontext + "' AND expires > " + nowbuf; + freeSafeSQL(scontext, context); + + m_log.debug("SQL: %s", q.c_str()); + + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); + if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) { + m_log.error("error updating records (t=%s, c=%s)", table, context ? context : "all"); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to update context expiration."); + } +} -void ODBCStorageService::reapRows(const char *table, const char* context) +void ODBCStorageService::reap(const char *table, const char* context) { #ifdef _DEBUG - xmltooling::NDC ndc("reapRows"); + xmltooling::NDC ndc("reap"); #endif // Get statement handle. - SQLHSTMT hstmt; ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLHSTMT stmt = getHSTMT(conn); // Prepare and execute delete statement. + char nowbuf[32]; + timestampFromTime(time(NULL), nowbuf); string q; if (context) { - q = string("DELETE FROM ") + table + " WHERE context='" + context + "' AND expireserror("error expiring records (t=%s, c=%s)", table, context?context:"null"); - log_error(hstmt, SQL_HANDLE_STMT); - hr=E_FAIL; + else { + q = string("DELETE FROM ") + table + " WHERE expires <= " + nowbuf; } + m_log.debug("SQL: %s", q.c_str()); - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); + if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) { + m_log.error("error expiring records (t=%s, c=%s)", table, context ? context : "all"); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to purge expired records."); + } } - - -// remove all entries for a context - -void ODBCStorageService::deleteCtx(const char *table, const char* context) +void ODBCStorageService::deleteContext(const char *table, const char* context) { #ifdef _DEBUG - xmltooling::NDC ndc("deleteCtx"); + xmltooling::NDC ndc("deleteContext"); #endif // Get statement handle. - SQLHSTMT hstmt; ODBCConn conn(getHDBC()); - SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt); + SQLHSTMT stmt = getHSTMT(conn); // Prepare and execute delete statement. - string q = string("DELETE FROM ") + table + " WHERE context='" + context + "'"; - log->debug("SQL: %s", q.str()); + char *scontext = makeSafeSQL(context); + string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "'"; + freeSafeSQL(scontext, context); + m_log.debug("SQL: %s", q.c_str()); - SQLRETURN sr=SQLExecDirect(hstmt, (SQLCHAR*)q.c_str(), SQL_NTS); - - HRESULT hr=NOERROR; - if (sr==SQL_NO_DATA) - hr=S_FALSE; - else if (!SQL_SUCCEEDED(sr)) { - log->error("error deleting context (t=%s, c=%s)", table, context); - log_error(hstmt, SQL_HANDLE_STMT); - hr=E_FAIL; + SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS); + if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) { + m_log.error("error deleting context (t=%s, c=%s)", table, context); + log_error(stmt, SQL_HANDLE_STMT); + throw IOException("ODBC StorageService failed to delete context."); } +} - SQLFreeHandle(SQL_HANDLE_STMT,hstmt); +extern "C" int ODBCSTORE_EXPORTS xmltooling_extension_init(void*) +{ + // Register this SS type + XMLToolingConfig::getConfig().StorageServiceManager.registerFactory("ODBC", ODBCStorageServiceFactory); + return 0; +} + +extern "C" void ODBCSTORE_EXPORTS xmltooling_extension_term() +{ + XMLToolingConfig::getConfig().StorageServiceManager.deregisterFactory("ODBC"); }