Allow reduction of isolation level via config.
[shibboleth/sp.git] / odbc-store / odbc-store.cpp
1 /*
2  *  Copyright 2001-2007 Internet2
3  * 
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 /**
18  * odbc-store.cpp
19  *
20  * Storage Service using ODBC
21  */
22
23 #if defined (_MSC_VER) || defined(__BORLANDC__)
24 # include "config_win32.h"
25 #else
26 # include "config.h"
27 #endif
28
29 #ifdef WIN32
30 # define _CRT_NONSTDC_NO_DEPRECATE 1
31 # define _CRT_SECURE_NO_DEPRECATE 1
32 #endif
33
34 #ifdef WIN32
35 # define ODBCSTORE_EXPORTS __declspec(dllexport)
36 #else
37 # define ODBCSTORE_EXPORTS
38 #endif
39
40 #include <xercesc/util/XMLUniDefs.hpp>
41 #include <xmltooling/logging.h>
42 #include <xmltooling/XMLToolingConfig.h>
43 #include <xmltooling/util/NDC.h>
44 #include <xmltooling/util/StorageService.h>
45 #include <xmltooling/util/Threads.h>
46 #include <xmltooling/util/XMLHelper.h>
47
48 #include <sql.h>
49 #include <sqlext.h>
50
51 using namespace xmltooling::logging;
52 using namespace xmltooling;
53 using namespace xercesc;
54 using namespace std;
55
56 #define PLUGIN_VER_MAJOR 1
57 #define PLUGIN_VER_MINOR 0
58
59 #define LONGDATA_BUFLEN 16384
60
61 #define COLSIZE_CONTEXT 255
62 #define COLSIZE_ID 255
63 #define COLSIZE_STRING_VALUE 255
64
65 #define STRING_TABLE "strings"
66 #define TEXT_TABLE "texts"
67
68 /* table definitions
69 CREATE TABLE version (
70     major tinyint NOT NULL,
71     minor tinyint NOT NULL
72     )
73
74 CREATE TABLE strings (
75     context varchar(255) not null,
76     id varchar(255) not null,
77     expires datetime not null,
78     version smallint not null,
79     value varchar(255) not null,
80     PRIMARY KEY (context, id)
81     )
82
83 CREATE TABLE texts (
84     context varchar(255) not null,
85     id varchar(255) not null,
86     expires datetime not null,
87     version smallint not null,
88     value text not null,
89     PRIMARY KEY (context, id)
90     )
91 */
92
93 namespace {
94     static const XMLCh cleanupInterval[] =  UNICODE_LITERAL_15(c,l,e,a,n,u,p,I,n,t,e,r,v,a,l);
95     static const XMLCh isolationLevel[] =   UNICODE_LITERAL_14(i,s,o,l,a,t,i,o,n,L,e,v,e,l);
96     static const XMLCh ConnectionString[] = UNICODE_LITERAL_16(C,o,n,n,e,c,t,i,o,n,S,t,r,i,n,g);
97
98     // RAII for ODBC handles
99     struct ODBCConn {
100         ODBCConn(SQLHDBC conn) : handle(conn), autoCommit(true) {}
101         ~ODBCConn() {
102             SQLRETURN sr = SQL_SUCCESS;
103             if (!autoCommit)
104                 sr = SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, (SQLPOINTER)SQL_AUTOCOMMIT_ON, NULL);
105             SQLDisconnect(handle);
106             SQLFreeHandle(SQL_HANDLE_DBC,handle);
107             if (!SQL_SUCCEEDED(sr))
108                 throw IOException("Failed to commit connection and return to auto-commit mode.");
109         }
110         operator SQLHDBC() {return handle;}
111         SQLHDBC handle;
112         bool autoCommit;
113     };
114
115     class ODBCStorageService : public StorageService
116     {
117     public:
118         ODBCStorageService(const DOMElement* e);
119         virtual ~ODBCStorageService();
120
121         bool createString(const char* context, const char* key, const char* value, time_t expiration) {
122             return createRow(STRING_TABLE, context, key, value, expiration);
123         }
124         int readString(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) {
125             return readRow(STRING_TABLE, context, key, pvalue, pexpiration, version, false);
126         }
127         int updateString(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) {
128             return updateRow(STRING_TABLE, context, key, value, expiration, version);
129         }
130         bool deleteString(const char* context, const char* key) {
131             return deleteRow(STRING_TABLE, context, key);
132         }
133
134         bool createText(const char* context, const char* key, const char* value, time_t expiration) {
135             return createRow(TEXT_TABLE, context, key, value, expiration);
136         }
137         int readText(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) {
138             return readRow(TEXT_TABLE, context, key, pvalue, pexpiration, version, true);
139         }
140         int updateText(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) {
141             return updateRow(TEXT_TABLE, context, key, value, expiration, version);
142         }
143         bool deleteText(const char* context, const char* key) {
144             return deleteRow(TEXT_TABLE, context, key);
145         }
146
147         void reap(const char* context) {
148             reap(STRING_TABLE, context);
149             reap(TEXT_TABLE, context);
150         }
151
152         void updateContext(const char* context, time_t expiration) {
153             updateContext(STRING_TABLE, context, expiration);
154             updateContext(TEXT_TABLE, context, expiration);
155         }
156
157         void deleteContext(const char* context) {
158             deleteContext(STRING_TABLE, context);
159             deleteContext(TEXT_TABLE, context);
160         }
161          
162
163     private:
164         bool createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration);
165         int readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text);
166         int updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version);
167         bool deleteRow(const char *table, const char* context, const char* key);
168
169         void reap(const char* table, const char* context);
170         void updateContext(const char* table, const char* context, time_t expiration);
171         void deleteContext(const char* table, const char* context);
172
173         SQLHDBC getHDBC();
174         SQLHSTMT getHSTMT(SQLHDBC);
175         pair<int,int> getVersion(SQLHDBC);
176         bool log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor=NULL);
177
178         static void* cleanup_fn(void*); 
179         void cleanup();
180
181         Category& m_log;
182         int m_cleanupInterval;
183         CondWait* shutdown_wait;
184         Thread* cleanup_thread;
185         bool shutdown;
186
187         SQLHENV m_henv;
188         string m_connstring;
189         long m_isolation;
190     };
191
192     StorageService* ODBCStorageServiceFactory(const DOMElement* const & e)
193     {
194         return new ODBCStorageService(e);
195     }
196
197     // convert SQL timestamp to time_t 
198     time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires)
199     {
200         time_t ret;
201         struct tm t;
202         t.tm_sec=expires.second;
203         t.tm_min=expires.minute;
204         t.tm_hour=expires.hour;
205         t.tm_mday=expires.day;
206         t.tm_mon=expires.month-1;
207         t.tm_year=expires.year-1900;
208         t.tm_isdst=0;
209 #if defined(HAVE_TIMEGM)
210         ret = timegm(&t);
211 #else
212         ret = mktime(&t) - timezone;
213 #endif
214         return (ret);
215     }
216
217     // conver time_t to SQL string
218     void timestampFromTime(time_t t, char* ret)
219     {
220 #ifdef HAVE_GMTIME_R
221         struct tm res;
222         struct tm* ptime=gmtime_r(&t,&res);
223 #else
224         struct tm* ptime=gmtime(&t);
225 #endif
226         strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime);
227     }
228
229     // make a string safe for SQL command
230     // result to be free'd only if it isn't the input
231     static char *makeSafeSQL(const char *src)
232     {
233        int ns = 0;
234        int nc = 0;
235        char *s;
236     
237        // see if any conversion needed
238        for (s=(char*)src; *s; nc++,s++) if (*s=='\'') ns++;
239        if (ns==0) return ((char*)src);
240     
241        char *safe = new char[(nc+2*ns+1)];
242        for (s=safe; *src; src++) {
243            if (*src=='\'') *s++ = '\'';
244            *s++ = (char)*src;
245        }
246        *s = '\0';
247        return (safe);
248     }
249
250     void freeSafeSQL(char *safe, const char *src)
251     {
252         if (safe!=src)
253             delete[](safe);
254     }
255 };
256
257 ODBCStorageService::ODBCStorageService(const DOMElement* e) : m_log(Category::getInstance("XMLTooling.StorageService")),
258    m_cleanupInterval(900), shutdown_wait(NULL), cleanup_thread(NULL), shutdown(false), m_henv(SQL_NULL_HANDLE), m_isolation(SQL_TXN_SERIALIZABLE)
259 {
260 #ifdef _DEBUG
261     xmltooling::NDC ndc("ODBCStorageService");
262 #endif
263
264     const XMLCh* tag=e ? e->getAttributeNS(NULL,cleanupInterval) : NULL;
265     if (tag && *tag)
266         m_cleanupInterval = XMLString::parseInt(tag);
267     if (!m_cleanupInterval)
268         m_cleanupInterval = 900;
269
270     auto_ptr_char iso(e ? e->getAttributeNS(NULL,isolationLevel) : NULL);
271     if (iso.get() && *iso.get()) {
272         if (!strcmp(iso.get(),"SERIALIZABLE"))
273             m_isolation = SQL_TXN_SERIALIZABLE;
274         else if (!strcmp(iso.get(),"REPEATABLE_READ"))
275             m_isolation = SQL_TXN_REPEATABLE_READ;
276         else if (!strcmp(iso.get(),"READ_COMMITTED"))
277             m_isolation = SQL_TXN_READ_COMMITTED;
278         else if (!strcmp(iso.get(),"READ_UNCOMMITTED"))
279             m_isolation = SQL_TXN_READ_UNCOMMITTED;
280         else
281             throw XMLToolingException("Unknown transaction isolationLevel property.");
282     }
283
284     if (m_henv == SQL_NULL_HANDLE) {
285         // Enable connection pooling.
286         SQLSetEnvAttr(SQL_NULL_HANDLE, SQL_ATTR_CONNECTION_POOLING, (void*)SQL_CP_ONE_PER_HENV, 0);
287
288         // Allocate the environment.
289         if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_henv)))
290             throw XMLToolingException("ODBC failed to initialize.");
291
292         // Specify ODBC 3.x
293         SQLSetEnvAttr(m_henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
294
295         m_log.info("ODBC initialized");
296     }
297
298     // Grab connection string from the configuration.
299     e = e ? XMLHelper::getFirstChildElement(e,ConnectionString) : NULL;
300     if (!e || !e->hasChildNodes()) {
301         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
302         throw XMLToolingException("ODBC StorageService requires ConnectionString element in configuration.");
303     }
304     auto_ptr_char arg(e->getFirstChild()->getNodeValue());
305     m_connstring=arg.get();
306
307     // Connect and check version.
308     ODBCConn conn(getHDBC());
309     pair<int,int> v=getVersion(conn);
310
311     // Make sure we've got the right version.
312     if (v.first != PLUGIN_VER_MAJOR) {
313         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
314         m_log.crit("unknown database version: %d.%d", v.first, v.second);
315         throw XMLToolingException("Unknown database version for ODBC StorageService.");
316     }
317
318     // Initialize the cleanup thread
319     shutdown_wait = CondWait::create();
320     cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
321 }
322
323 ODBCStorageService::~ODBCStorageService()
324 {
325     shutdown = true;
326     shutdown_wait->signal();
327     cleanup_thread->join(NULL);
328     delete shutdown_wait;
329     if (m_henv != SQL_NULL_HANDLE)
330         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
331 }
332
333 bool ODBCStorageService::log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor)
334 {
335     SQLSMALLINT  i = 0;
336     SQLINTEGER   native;
337     SQLCHAR      state[7];
338     SQLCHAR      text[256];
339     SQLSMALLINT  len;
340     SQLRETURN    ret;
341
342     bool res = false;
343     do {
344         ret = SQLGetDiagRec(htype, handle, ++i, state, &native, text, sizeof(text), &len);
345         if (SQL_SUCCEEDED(ret)) {
346             m_log.error("ODBC Error: %s:%ld:%ld:%s", state, i, native, text);
347             if (checkfor && !strcmp(checkfor, (const char*)state))
348                 res = true;
349         }
350     } while(SQL_SUCCEEDED(ret));
351     return res;
352 }
353
354 SQLHDBC ODBCStorageService::getHDBC()
355 {
356 #ifdef _DEBUG
357     xmltooling::NDC ndc("getHDBC");
358 #endif
359
360     // Get a handle.
361     SQLHDBC handle;
362     SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_DBC, m_henv, &handle);
363     if (!SQL_SUCCEEDED(sr)) {
364         m_log.error("failed to allocate connection handle");
365         log_error(m_henv, SQL_HANDLE_ENV);
366         throw IOException("ODBC StorageService failed to allocate a connection handle.");
367     }
368
369     sr=SQLDriverConnect(handle,NULL,(SQLCHAR*)m_connstring.c_str(),m_connstring.length(),NULL,0,NULL,SQL_DRIVER_NOPROMPT);
370     if (!SQL_SUCCEEDED(sr)) {
371         m_log.error("failed to connect to database");
372         log_error(handle, SQL_HANDLE_DBC);
373         throw IOException("ODBC StorageService failed to connect to database.");
374     }
375
376     sr = SQLSetConnectAttr(handle, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)m_isolation, NULL);
377     if (!SQL_SUCCEEDED(sr))
378         throw IOException("ODBC StorageService failed to set transaction isolation level.");
379
380     return handle;
381 }
382
383 SQLHSTMT ODBCStorageService::getHSTMT(SQLHDBC conn)
384 {
385     SQLHSTMT hstmt;
386     SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt);
387     if (!SQL_SUCCEEDED(sr)) {
388         m_log.error("failed to allocate statement handle");
389         log_error(conn, SQL_HANDLE_DBC);
390         throw IOException("ODBC StorageService failed to allocate a statement handle.");
391     }
392     return hstmt;
393 }
394
395 pair<int,int> ODBCStorageService::getVersion(SQLHDBC conn)
396 {
397     // Grab the version number from the database.
398     SQLHSTMT stmt = getHSTMT(conn);
399     
400     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)"SELECT major,minor FROM version", SQL_NTS);
401     if (!SQL_SUCCEEDED(sr)) {
402         m_log.error("failed to read version from database");
403         log_error(stmt, SQL_HANDLE_STMT);
404         throw IOException("ODBC StorageService failed to read version from database.");
405     }
406
407     SQLINTEGER major;
408     SQLINTEGER minor;
409     SQLBindCol(stmt,1,SQL_C_SLONG,&major,0,NULL);
410     SQLBindCol(stmt,2,SQL_C_SLONG,&minor,0,NULL);
411
412     if ((sr=SQLFetch(stmt)) != SQL_NO_DATA)
413         return pair<int,int>(major,minor);
414
415     m_log.error("no rows returned in version query");
416     throw IOException("ODBC StorageService failed to read version from database.");
417 }
418
419 bool ODBCStorageService::createRow(const char* table, const char* context, const char* key, const char* value, time_t expiration)
420 {
421 #ifdef _DEBUG
422     xmltooling::NDC ndc("createRow");
423 #endif
424
425     char timebuf[32];
426     timestampFromTime(expiration, timebuf);
427
428     // Get statement handle.
429     ODBCConn conn(getHDBC());
430     SQLHSTMT stmt = getHSTMT(conn);
431
432     // Prepare and exectute insert statement.
433     //char *scontext = makeSafeSQL(context);
434     //char *skey = makeSafeSQL(key);
435     //char *svalue = makeSafeSQL(value);
436     string q  = string("INSERT INTO ") + table + " VALUES (?,?," + timebuf + ",1,?)";
437
438     SQLRETURN sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
439     if (!SQL_SUCCEEDED(sr)) {
440         m_log.error("SQLPrepare failed (t=%s, c=%s, k=%s)", table, context, key);
441         log_error(stmt, SQL_HANDLE_STMT);
442         throw IOException("ODBC StorageService failed to insert record.");
443     }
444     m_log.debug("SQLPrepare succeded. SQL: %s", q.c_str());
445
446     SQLINTEGER b_ind = SQL_NTS;
447     sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(context), &b_ind);
448     if (!SQL_SUCCEEDED(sr)) {
449         m_log.error("SQLBindParam failed (context = %s)", context);
450         log_error(stmt, SQL_HANDLE_STMT);
451         throw IOException("ODBC StorageService failed to insert record.");
452     }
453     m_log.debug("SQLBindParam succeded (context = %s)", context);
454
455     sr = SQLBindParam(stmt, 2, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(key), &b_ind);
456     if (!SQL_SUCCEEDED(sr)) {
457         m_log.error("SQLBindParam failed (key = %s)", key);
458         log_error(stmt, SQL_HANDLE_STMT);
459         throw IOException("ODBC StorageService failed to insert record.");
460     }
461     m_log.debug("SQLBindParam succeded (key = %s)", key);
462
463     if (strcmp(table, TEXT_TABLE)==0)
464         sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast<char*>(value), &b_ind);
465     else
466         sr = SQLBindParam(stmt, 3, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(value), &b_ind);
467     if (!SQL_SUCCEEDED(sr)) {
468         m_log.error("SQLBindParam failed (value = %s)", value);
469         log_error(stmt, SQL_HANDLE_STMT);
470         throw IOException("ODBC StorageService failed to insert record.");
471     }
472     m_log.debug("SQLBindParam succeded (value = %s)", value);
473     
474     //freeSafeSQL(scontext, context);
475     //freeSafeSQL(skey, key);
476     //freeSafeSQL(svalue, value);
477     //m_log.debug("SQL: %s", q.c_str());
478
479     sr=SQLExecute(stmt);
480     if (!SQL_SUCCEEDED(sr)) {
481         m_log.error("insert record failed (t=%s, c=%s, k=%s)", table, context, key);
482         if (log_error(stmt, SQL_HANDLE_STMT, "23000"))
483             return false;   // supposedly integrity violation?
484         throw IOException("ODBC StorageService failed to insert record.");
485     }
486
487     m_log.debug("SQLExecute of insert succeeded");
488     return true;
489 }
490
491 int ODBCStorageService::readRow(
492     const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text
493     )
494 {
495 #ifdef _DEBUG
496     xmltooling::NDC ndc("readRow");
497 #endif
498
499     // Get statement handle.
500     ODBCConn conn(getHDBC());
501     SQLHSTMT stmt = getHSTMT(conn);
502
503     // Prepare and exectute select statement.
504     char timebuf[32];
505     timestampFromTime(time(NULL), timebuf);
506     char *scontext = makeSafeSQL(context);
507     char *skey = makeSafeSQL(key);
508     ostringstream q;
509     q << "SELECT version";
510     if (pexpiration)
511         q << ",expires";
512     if (pvalue)
513         q << ",CASE version WHEN " << version << " THEN NULL ELSE value END";
514     q << " FROM " << table << " WHERE context='" << scontext << "' AND id='" << skey << "' AND expires > " << timebuf;
515     freeSafeSQL(scontext, context);
516     freeSafeSQL(skey, key);
517     if (m_log.isDebugEnabled())
518         m_log.debug("SQL: %s", q.str().c_str());
519
520     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.str().c_str(), SQL_NTS);
521     if (!SQL_SUCCEEDED(sr)) {
522         m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
523         log_error(stmt, SQL_HANDLE_STMT);
524         throw IOException("ODBC StorageService search failed.");
525     }
526
527     SQLSMALLINT ver;
528     SQL_TIMESTAMP_STRUCT expiration;
529
530     SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL);
531     if (pexpiration)
532         SQLBindCol(stmt,2,SQL_C_TYPE_TIMESTAMP,&expiration,0,NULL);
533
534     if ((sr=SQLFetch(stmt)) == SQL_NO_DATA)
535         return 0;
536
537     if (pexpiration)
538         *pexpiration = timeFromTimestamp(expiration);
539
540     if (version == ver)
541         return version; // nothing's changed, so just echo back the version
542
543     if (pvalue) {
544         SQLINTEGER len;
545         SQLCHAR buf[LONGDATA_BUFLEN];
546         while ((sr=SQLGetData(stmt,pexpiration ? 3 : 2,SQL_C_CHAR,buf,sizeof(buf),&len)) != SQL_NO_DATA) {
547             if (!SQL_SUCCEEDED(sr)) {
548                 m_log.error("error while reading text field from result set");
549                 log_error(stmt, SQL_HANDLE_STMT);
550                 throw IOException("ODBC StorageService search failed to read data from result set.");
551             }
552             pvalue->append((char*)buf);
553         }
554     }
555     
556     return ver;
557 }
558
559 int ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version)
560 {
561 #ifdef _DEBUG
562     xmltooling::NDC ndc("updateRow");
563 #endif
564
565     if (!value && !expiration)
566         throw IOException("ODBC StorageService given invalid update instructions.");
567
568     // Get statement handle. Disable auto-commit mode to wrap select + update.
569     ODBCConn conn(getHDBC());
570     SQLRETURN sr = SQLSetConnectAttr(conn, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, NULL);
571     if (!SQL_SUCCEEDED(sr))
572         throw IOException("ODBC StorageService failed to disable auto-commit mode.");
573     conn.autoCommit = false;
574     SQLHSTMT stmt = getHSTMT(conn);
575
576     // First, fetch the current version for later, which also ensures the record still exists.
577     char timebuf[32];
578     timestampFromTime(time(NULL), timebuf);
579     char *scontext = makeSafeSQL(context);
580     char *skey = makeSafeSQL(key);
581     string q("SELECT version FROM ");
582     q = q + table + " WHERE context='" + scontext + "' AND id='" + skey + "' AND expires > " + timebuf;
583
584     m_log.debug("SQL: %s", q.c_str());
585
586     sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
587     if (!SQL_SUCCEEDED(sr)) {
588         freeSafeSQL(scontext, context);
589         freeSafeSQL(skey, key);
590         m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
591         log_error(stmt, SQL_HANDLE_STMT);
592         throw IOException("ODBC StorageService search failed.");
593     }
594
595     SQLSMALLINT ver;
596     SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL);
597     if ((sr=SQLFetch(stmt)) == SQL_NO_DATA) {
598         freeSafeSQL(scontext, context);
599         freeSafeSQL(skey, key);
600         return 0;
601     }
602
603     // Check version?
604     if (version > 0 && version != ver) {
605         freeSafeSQL(scontext, context);
606         freeSafeSQL(skey, key);
607         return -1;
608     }
609
610     SQLFreeHandle(SQL_HANDLE_STMT, stmt);
611     stmt = getHSTMT(conn);
612
613     // Prepare and exectute update statement.
614     q = string("UPDATE ") + table + " SET ";
615
616     if (value)
617         q = q + "value=?, version=version+1";
618
619     if (expiration) {
620         timestampFromTime(expiration, timebuf);
621         if (value)
622             q += ',';
623         q = q + "expires = " + timebuf;
624     }
625
626     q = q + " WHERE context='" + scontext + "' AND id='" + skey + "'";
627     freeSafeSQL(scontext, context);
628     freeSafeSQL(skey, key);
629
630     sr = SQLPrepare(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
631     if (!SQL_SUCCEEDED(sr)) {
632         m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key);
633         log_error(stmt, SQL_HANDLE_STMT);
634         throw IOException("ODBC StorageService failed to update record.");
635     }
636     m_log.debug("SQLPrepare succeded. SQL: %s", q.c_str());
637
638     SQLINTEGER b_ind = SQL_NTS;
639     if (value) {
640         if (strcmp(table, TEXT_TABLE)==0)
641             sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_LONGVARCHAR, strlen(value), 0, const_cast<char*>(value), &b_ind);
642         else
643             sr = SQLBindParam(stmt, 1, SQL_C_CHAR, SQL_VARCHAR, 255, 0, const_cast<char*>(value), &b_ind);
644         if (!SQL_SUCCEEDED(sr)) {
645             m_log.error("SQLBindParam failed (context = %s)", context);
646             log_error(stmt, SQL_HANDLE_STMT);
647             throw IOException("ODBC StorageService failed to update record.");
648         }
649         m_log.debug("SQLBindParam succeded (context = %s)", context);
650     }
651
652     sr=SQLExecute(stmt);
653     if (sr==SQL_NO_DATA)
654         return 0;   // went missing?
655     else if (!SQL_SUCCEEDED(sr)) {
656         m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key);
657         log_error(stmt, SQL_HANDLE_STMT);
658         throw IOException("ODBC StorageService failed to update record.");
659     }
660
661     m_log.debug("SQLExecute of update succeeded");
662     return ver + 1;
663 }
664
665 bool ODBCStorageService::deleteRow(const char *table, const char *context, const char* key)
666 {
667 #ifdef _DEBUG
668     xmltooling::NDC ndc("deleteRow");
669 #endif
670
671     // Get statement handle.
672     ODBCConn conn(getHDBC());
673     SQLHSTMT stmt = getHSTMT(conn);
674
675     // Prepare and execute delete statement.
676     char *scontext = makeSafeSQL(context);
677     char *skey = makeSafeSQL(key);
678     string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "' AND id='" + skey + "'";
679     freeSafeSQL(scontext, context);
680     freeSafeSQL(skey, key);
681     m_log.debug("SQL: %s", q.c_str());
682
683     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
684      if (sr==SQL_NO_DATA)
685         return false;
686     else if (!SQL_SUCCEEDED(sr)) {
687         m_log.error("error deleting record (t=%s, c=%s, k=%s)", table, context, key);
688         log_error(stmt, SQL_HANDLE_STMT);
689         throw IOException("ODBC StorageService failed to delete record.");
690     }
691
692     return true;
693 }
694
695
696 void ODBCStorageService::cleanup()
697 {
698 #ifdef _DEBUG
699     xmltooling::NDC ndc("cleanup");
700 #endif
701
702     Mutex* mutex = Mutex::create();
703
704     mutex->lock();
705
706     m_log.info("cleanup thread started... running every %d secs", m_cleanupInterval);
707
708     while (!shutdown) {
709         shutdown_wait->timedwait(mutex, m_cleanupInterval);
710         if (shutdown)
711             break;
712         try {
713             reap(NULL);
714         }
715         catch (exception& ex) {
716             m_log.error("cleanup thread swallowed exception: %s", ex.what());
717         }
718     }
719
720     m_log.info("cleanup thread exiting...");
721
722     mutex->unlock();
723     delete mutex;
724     Thread::exit(NULL);
725 }
726
727 void* ODBCStorageService::cleanup_fn(void* cache_p)
728 {
729   ODBCStorageService* cache = (ODBCStorageService*)cache_p;
730
731 #ifndef WIN32
732   // First, let's block all signals
733   Thread::mask_all_signals();
734 #endif
735
736   // Now run the cleanup process.
737   cache->cleanup();
738   return NULL;
739 }
740
741 void ODBCStorageService::updateContext(const char *table, const char* context, time_t expiration)
742 {
743 #ifdef _DEBUG
744     xmltooling::NDC ndc("updateContext");
745 #endif
746
747     // Get statement handle.
748     ODBCConn conn(getHDBC());
749     SQLHSTMT stmt = getHSTMT(conn);
750
751     char timebuf[32];
752     timestampFromTime(expiration, timebuf);
753
754     char nowbuf[32];
755     timestampFromTime(time(NULL), nowbuf);
756
757     char *scontext = makeSafeSQL(context);
758     string q("UPDATE ");
759     q = q + table + " SET expires = " + timebuf + " WHERE context='" + scontext + "' AND expires > " + nowbuf;
760     freeSafeSQL(scontext, context);
761
762     m_log.debug("SQL: %s", q.c_str());
763
764     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
765     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
766         m_log.error("error updating records (t=%s, c=%s)", table, context ? context : "all");
767         log_error(stmt, SQL_HANDLE_STMT);
768         throw IOException("ODBC StorageService failed to update context expiration.");
769     }
770 }
771
772 void ODBCStorageService::reap(const char *table, const char* context)
773 {
774 #ifdef _DEBUG
775     xmltooling::NDC ndc("reap");
776 #endif
777
778     // Get statement handle.
779     ODBCConn conn(getHDBC());
780     SQLHSTMT stmt = getHSTMT(conn);
781
782     // Prepare and execute delete statement.
783     char nowbuf[32];
784     timestampFromTime(time(NULL), nowbuf);
785     string q;
786     if (context) {
787         char *scontext = makeSafeSQL(context);
788         q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "' AND expires <= " + nowbuf;
789         freeSafeSQL(scontext, context);
790     }
791     else {
792         q = string("DELETE FROM ") + table + " WHERE expires <= " + nowbuf;
793     }
794     m_log.debug("SQL: %s", q.c_str());
795
796     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
797     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
798         m_log.error("error expiring records (t=%s, c=%s)", table, context ? context : "all");
799         log_error(stmt, SQL_HANDLE_STMT);
800         throw IOException("ODBC StorageService failed to purge expired records.");
801     }
802 }
803
804 void ODBCStorageService::deleteContext(const char *table, const char* context)
805 {
806 #ifdef _DEBUG
807     xmltooling::NDC ndc("deleteContext");
808 #endif
809
810     // Get statement handle.
811     ODBCConn conn(getHDBC());
812     SQLHSTMT stmt = getHSTMT(conn);
813
814     // Prepare and execute delete statement.
815     char *scontext = makeSafeSQL(context);
816     string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "'";
817     freeSafeSQL(scontext, context);
818     m_log.debug("SQL: %s", q.c_str());
819
820     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
821     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
822         m_log.error("error deleting context (t=%s, c=%s)", table, context);
823         log_error(stmt, SQL_HANDLE_STMT);
824         throw IOException("ODBC StorageService failed to delete context.");
825     }
826 }
827
828 extern "C" int ODBCSTORE_EXPORTS xmltooling_extension_init(void*)
829 {
830     // Register this SS type
831     XMLToolingConfig::getConfig().StorageServiceManager.registerFactory("ODBC", ODBCStorageServiceFactory);
832     return 0;
833 }
834
835 extern "C" void ODBCSTORE_EXPORTS xmltooling_extension_term()
836 {
837     XMLToolingConfig::getConfig().StorageServiceManager.deregisterFactory("ODBC");
838 }