5985b324debbe247e7fbea62f5965fe36af82bdd
[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 ConnectionString[] = UNICODE_LITERAL_16(C,o,n,n,e,c,t,i,o,n,S,t,r,i,n,g);
96
97     // RAII for ODBC handles
98     struct ODBCConn {
99         ODBCConn(SQLHDBC conn) : handle(conn) {}
100         ~ODBCConn() {
101             SQLRETURN sr = SQLEndTran(SQL_HANDLE_DBC, handle, SQL_COMMIT);
102             SQLDisconnect(handle);
103             SQLFreeHandle(SQL_HANDLE_DBC,handle);
104             if (!SQL_SUCCEEDED(sr))
105                 throw IOException("Failed to commit connection.");
106         }
107         operator SQLHDBC() {return handle;}
108         SQLHDBC handle;
109     };
110
111     struct ODBCStatement {
112         ODBCStatement(SQLHSTMT statement) : handle(statement) {}
113         ~ODBCStatement() {SQLFreeHandle(SQL_HANDLE_STMT,handle);}
114         operator SQLHSTMT() {return handle;}
115         SQLHSTMT handle;
116     };
117
118     class ODBCStorageService : public StorageService
119     {
120     public:
121         ODBCStorageService(const DOMElement* e);
122         virtual ~ODBCStorageService();
123
124         bool createString(const char* context, const char* key, const char* value, time_t expiration) {
125             return createRow(STRING_TABLE, context, key, value, expiration);
126         }
127         int readString(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) {
128             return readRow(STRING_TABLE, context, key, pvalue, pexpiration, version, false);
129         }
130         int updateString(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) {
131             return updateRow(STRING_TABLE, context, key, value, expiration, version);
132         }
133         bool deleteString(const char* context, const char* key) {
134             return deleteRow(STRING_TABLE, context, key);
135         }
136
137         bool createText(const char* context, const char* key, const char* value, time_t expiration) {
138             return createRow(TEXT_TABLE, context, key, value, expiration);
139         }
140         int readText(const char* context, const char* key, string* pvalue=NULL, time_t* pexpiration=NULL, int version=0) {
141             return readRow(TEXT_TABLE, context, key, pvalue, pexpiration, version, true);
142         }
143         int updateText(const char* context, const char* key, const char* value=NULL, time_t expiration=0, int version=0) {
144             return updateRow(TEXT_TABLE, context, key, value, expiration, version);
145         }
146         bool deleteText(const char* context, const char* key) {
147             return deleteRow(TEXT_TABLE, context, key);
148         }
149
150         void reap(const char* context) {
151             reap(STRING_TABLE, context);
152             reap(TEXT_TABLE, context);
153         }
154
155         void updateContext(const char* context, time_t expiration) {
156             updateContext(STRING_TABLE, context, expiration);
157             updateContext(TEXT_TABLE, context, expiration);
158         }
159
160         void deleteContext(const char* context) {
161             deleteContext(STRING_TABLE, context);
162             deleteContext(TEXT_TABLE, context);
163         }
164          
165
166     private:
167         bool createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration);
168         int readRow(const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text);
169         int updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version);
170         bool deleteRow(const char *table, const char* context, const char* key);
171
172         void reap(const char* table, const char* context);
173         void updateContext(const char* table, const char* context, time_t expiration);
174         void deleteContext(const char* table, const char* context);
175
176         SQLHDBC getHDBC();
177         SQLHSTMT getHSTMT(SQLHDBC);
178         pair<int,int> getVersion(SQLHDBC);
179         bool log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor=NULL);
180
181         static void* cleanup_fn(void*); 
182         void cleanup();
183
184         Category& m_log;
185         int m_cleanupInterval;
186         CondWait* shutdown_wait;
187         Thread* cleanup_thread;
188         bool shutdown;
189
190         SQLHENV m_henv;
191         string m_connstring;
192     };
193
194     StorageService* ODBCStorageServiceFactory(const DOMElement* const & e)
195     {
196         return new ODBCStorageService(e);
197     }
198
199     // convert SQL timestamp to time_t 
200     time_t timeFromTimestamp(SQL_TIMESTAMP_STRUCT expires)
201     {
202         time_t ret;
203         struct tm t;
204         t.tm_sec=expires.second;
205         t.tm_min=expires.minute;
206         t.tm_hour=expires.hour;
207         t.tm_mday=expires.day;
208         t.tm_mon=expires.month-1;
209         t.tm_year=expires.year-1900;
210         t.tm_isdst=0;
211 #if defined(HAVE_TIMEGM)
212         ret = timegm(&t);
213 #else
214         ret = mktime(&t) - timezone;
215 #endif
216         return (ret);
217     }
218
219     // conver time_t to SQL string
220     void timestampFromTime(time_t t, char* ret)
221     {
222 #ifdef HAVE_GMTIME_R
223         struct tm res;
224         struct tm* ptime=gmtime_r(&t,&res);
225 #else
226         struct tm* ptime=gmtime(&t);
227 #endif
228         strftime(ret,32,"{ts '%Y-%m-%d %H:%M:%S'}",ptime);
229     }
230
231     // make a string safe for SQL command
232     // result to be free'd only if it isn't the input
233     static char *makeSafeSQL(const char *src)
234     {
235        int ns = 0;
236        int nc = 0;
237        char *s;
238     
239        // see if any conversion needed
240        for (s=(char*)src; *s; nc++,s++) if (*s=='\'') ns++;
241        if (ns==0) return ((char*)src);
242     
243        char *safe = new char[(nc+2*ns+1)];
244        for (s=safe; *src; src++) {
245            if (*src=='\'') *s++ = '\'';
246            *s++ = (char)*src;
247        }
248        *s = '\0';
249        return (safe);
250     }
251
252     void freeSafeSQL(char *safe, const char *src)
253     {
254         if (safe!=src)
255             delete[](safe);
256     }
257 };
258
259 ODBCStorageService::ODBCStorageService(const DOMElement* e) : m_log(Category::getInstance("XMLTooling.StorageService")),
260    m_cleanupInterval(900), shutdown_wait(NULL), cleanup_thread(NULL), shutdown(false), m_henv(SQL_NULL_HANDLE)
261 {
262 #ifdef _DEBUG
263     xmltooling::NDC ndc("ODBCStorageService");
264 #endif
265
266     const XMLCh* tag=e ? e->getAttributeNS(NULL,cleanupInterval) : NULL;
267     if (tag && *tag)
268         m_cleanupInterval = XMLString::parseInt(tag);
269     if (!m_cleanupInterval)
270         m_cleanupInterval = 900;
271
272     if (m_henv == SQL_NULL_HANDLE) {
273         // Enable connection pooling.
274         SQLSetEnvAttr(SQL_NULL_HANDLE, SQL_ATTR_CONNECTION_POOLING, (void*)SQL_CP_ONE_PER_HENV, 0);
275
276         // Allocate the environment.
277         if (!SQL_SUCCEEDED(SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &m_henv)))
278             throw XMLToolingException("ODBC failed to initialize.");
279
280         // Specify ODBC 3.x
281         SQLSetEnvAttr(m_henv, SQL_ATTR_ODBC_VERSION, (void*)SQL_OV_ODBC3, 0);
282
283         m_log.info("ODBC initialized");
284     }
285
286     // Grab connection string from the configuration.
287     e = e ? XMLHelper::getFirstChildElement(e,ConnectionString) : NULL;
288     if (!e || !e->hasChildNodes()) {
289         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
290         throw XMLToolingException("ODBC StorageService requires ConnectionString element in configuration.");
291     }
292     auto_ptr_char arg(e->getFirstChild()->getNodeValue());
293     m_connstring=arg.get();
294
295     // Connect and check version.
296     ODBCConn conn(getHDBC());
297     pair<int,int> v=getVersion(conn);
298
299     // Make sure we've got the right version.
300     if (v.first != PLUGIN_VER_MAJOR) {
301         SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
302         m_log.crit("unknown database version: %d.%d", v.first, v.second);
303         throw XMLToolingException("Unknown database version for ODBC StorageService.");
304     }
305
306     // Initialize the cleanup thread
307     shutdown_wait = CondWait::create();
308     cleanup_thread = Thread::create(&cleanup_fn, (void*)this);
309 }
310
311 ODBCStorageService::~ODBCStorageService()
312 {
313     shutdown = true;
314     shutdown_wait->signal();
315     cleanup_thread->join(NULL);
316     delete shutdown_wait;
317     SQLFreeHandle(SQL_HANDLE_ENV, m_henv);
318 }
319
320 bool ODBCStorageService::log_error(SQLHANDLE handle, SQLSMALLINT htype, const char* checkfor)
321 {
322     SQLSMALLINT  i = 0;
323     SQLINTEGER   native;
324     SQLCHAR      state[7];
325     SQLCHAR      text[256];
326     SQLSMALLINT  len;
327     SQLRETURN    ret;
328
329     bool res = false;
330     do {
331         ret = SQLGetDiagRec(htype, handle, ++i, state, &native, text, sizeof(text), &len);
332         if (SQL_SUCCEEDED(ret)) {
333             m_log.error("ODBC Error: %s:%ld:%ld:%s", state, i, native, text);
334             if (checkfor && !strcmp(checkfor, (const char*)state))
335                 res = true;
336         }
337     } while(SQL_SUCCEEDED(ret));
338     return res;
339 }
340
341 SQLHDBC ODBCStorageService::getHDBC()
342 {
343 #ifdef _DEBUG
344     xmltooling::NDC ndc("getHDBC");
345 #endif
346
347     // Get a handle.
348     SQLHDBC handle;
349     SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_DBC, m_henv, &handle);
350     if (!SQL_SUCCEEDED(sr)) {
351         m_log.error("failed to allocate connection handle");
352         log_error(m_henv, SQL_HANDLE_ENV);
353         throw IOException("ODBC StorageService failed to allocate a connection handle.");
354     }
355
356     sr=SQLDriverConnect(handle,NULL,(SQLCHAR*)m_connstring.c_str(),m_connstring.length(),NULL,0,NULL,SQL_DRIVER_NOPROMPT);
357     if (!SQL_SUCCEEDED(sr)) {
358         m_log.error("failed to connect to database");
359         log_error(handle, SQL_HANDLE_DBC);
360         throw IOException("ODBC StorageService failed to connect to database.");
361     }
362
363     sr = SQLSetConnectAttr(handle, SQL_ATTR_AUTOCOMMIT, SQL_AUTOCOMMIT_OFF, NULL);
364     if (!SQL_SUCCEEDED(sr))
365         throw IOException("ODBC StorageService failed to disable auto-commit mode.");
366     sr = SQLSetConnectAttr(handle, SQL_ATTR_TXN_ISOLATION, (SQLPOINTER)SQL_TXN_SERIALIZABLE, NULL);
367     if (!SQL_SUCCEEDED(sr))
368         throw IOException("ODBC StorageService failed to enable transaction isolation.");
369
370     return handle;
371 }
372
373 SQLHSTMT ODBCStorageService::getHSTMT(SQLHDBC conn)
374 {
375     SQLHSTMT hstmt;
376     SQLRETURN sr=SQLAllocHandle(SQL_HANDLE_STMT,conn,&hstmt);
377     if (!SQL_SUCCEEDED(sr)) {
378         m_log.error("failed to allocate statement handle");
379         log_error(conn, SQL_HANDLE_DBC);
380         throw IOException("ODBC StorageService failed to allocate a statement handle.");
381     }
382     return hstmt;
383 }
384
385 pair<int,int> ODBCStorageService::getVersion(SQLHDBC conn)
386 {
387     // Grab the version number from the database.
388     ODBCStatement stmt(getHSTMT(conn));
389     
390     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)"SELECT major,minor FROM version", SQL_NTS);
391     if (!SQL_SUCCEEDED(sr)) {
392         m_log.error("failed to read version from database");
393         log_error(stmt, SQL_HANDLE_STMT);
394         throw IOException("ODBC StorageService failed to read version from database.");
395     }
396
397     SQLINTEGER major;
398     SQLINTEGER minor;
399     SQLBindCol(stmt,1,SQL_C_SLONG,&major,0,NULL);
400     SQLBindCol(stmt,2,SQL_C_SLONG,&minor,0,NULL);
401
402     if ((sr=SQLFetch(stmt)) != SQL_NO_DATA)
403         return pair<int,int>(major,minor);
404
405     m_log.error("no rows returned in version query");
406     throw IOException("ODBC StorageService failed to read version from database.");
407 }
408
409 bool ODBCStorageService::createRow(const char *table, const char* context, const char* key, const char* value, time_t expiration)
410 {
411 #ifdef _DEBUG
412     xmltooling::NDC ndc("createRow");
413 #endif
414
415     char timebuf[32];
416     timestampFromTime(expiration, timebuf);
417
418     // Get statement handle.
419     ODBCConn conn(getHDBC());
420     ODBCStatement stmt(getHSTMT(conn));
421
422     // Prepare and exectute insert statement.
423     char *scontext = makeSafeSQL(context);
424     char *skey = makeSafeSQL(key);
425     char *svalue = makeSafeSQL(value);
426     string q  = string("INSERT INTO ") + table + " VALUES ('" + scontext + "','" + skey + "'," + timebuf + ",1,'" + svalue + "')";
427     freeSafeSQL(scontext, context);
428     freeSafeSQL(skey, key);
429     freeSafeSQL(svalue, value);
430     m_log.debug("SQL: %s", q.c_str());
431
432     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
433     if (!SQL_SUCCEEDED(sr)) {
434         m_log.error("insert record failed (t=%s, c=%s, k=%s)", table, context, key);
435         if (log_error(stmt, SQL_HANDLE_STMT, "23000"))
436             return false;   // supposedly integrity violation?
437         throw IOException("ODBC StorageService failed to insert record.");
438     }
439     return true;
440 }
441
442 int ODBCStorageService::readRow(
443     const char *table, const char* context, const char* key, string* pvalue, time_t* pexpiration, int version, bool text
444     )
445 {
446 #ifdef _DEBUG
447     xmltooling::NDC ndc("readRow");
448 #endif
449
450     // Get statement handle.
451     ODBCConn conn(getHDBC());
452     ODBCStatement stmt(getHSTMT(conn));
453
454     // Prepare and exectute select statement.
455     char timebuf[32];
456     timestampFromTime(time(NULL), timebuf);
457     char *scontext = makeSafeSQL(context);
458     char *skey = makeSafeSQL(key);
459     ostringstream q;
460     q << "SELECT version";
461     if (pexpiration)
462         q << ",expires";
463     if (pvalue)
464         q << ",CASE version WHEN " << version << " THEN NULL ELSE value END";
465     q << " FROM " << table << " WHERE context='" << scontext << "' AND id='" << skey << "' AND expires > " << timebuf;
466     freeSafeSQL(scontext, context);
467     freeSafeSQL(skey, key);
468     if (m_log.isDebugEnabled())
469         m_log.debug("SQL: %s", q.str().c_str());
470
471     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.str().c_str(), SQL_NTS);
472     if (!SQL_SUCCEEDED(sr)) {
473         m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
474         log_error(stmt, SQL_HANDLE_STMT);
475         throw IOException("ODBC StorageService search failed.");
476     }
477
478     SQLSMALLINT ver;
479     SQL_TIMESTAMP_STRUCT expiration;
480
481     SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL);
482     if (pexpiration)
483         SQLBindCol(stmt,2,SQL_C_TYPE_TIMESTAMP,&expiration,0,NULL);
484
485     if ((sr=SQLFetch(stmt)) == SQL_NO_DATA)
486         return 0;
487
488     if (pexpiration)
489         *pexpiration = timeFromTimestamp(expiration);
490
491     if (version == ver)
492         return version; // nothing's changed, so just echo back the version
493
494     if (pvalue) {
495         SQLINTEGER len;
496         SQLCHAR buf[LONGDATA_BUFLEN];
497         while ((sr=SQLGetData(stmt,pexpiration ? 3 : 2,SQL_C_CHAR,buf,sizeof(buf),&len)) != SQL_NO_DATA) {
498             if (!SQL_SUCCEEDED(sr)) {
499                 m_log.error("error while reading text field from result set");
500                 log_error(stmt, SQL_HANDLE_STMT);
501                 throw IOException("ODBC StorageService search failed to read data from result set.");
502             }
503             pvalue->append((char*)buf);
504         }
505     }
506     
507     return ver;
508 }
509
510 int ODBCStorageService::updateRow(const char *table, const char* context, const char* key, const char* value, time_t expiration, int version)
511 {
512 #ifdef _DEBUG
513     xmltooling::NDC ndc("updateRow");
514 #endif
515
516     if (!value && !expiration)
517         throw IOException("ODBC StorageService given invalid update instructions.");
518
519     // Get statement handle.
520     ODBCConn conn(getHDBC());
521     ODBCStatement stmt(getHSTMT(conn));
522
523     // First, fetch the current version for later, which also ensures the record still exists.
524     char timebuf[32];
525     timestampFromTime(time(NULL), timebuf);
526     char *scontext = makeSafeSQL(context);
527     char *skey = makeSafeSQL(key);
528     string q("SELECT version FROM ");
529     q = q + table + " WHERE context='" + scontext + "' AND id='" + key + "' AND expires > " + timebuf;
530
531     m_log.debug("SQL: %s", q.c_str());
532
533     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
534     if (!SQL_SUCCEEDED(sr)) {
535         freeSafeSQL(scontext, context);
536         freeSafeSQL(skey, key);
537         m_log.error("error searching for (t=%s, c=%s, k=%s)", table, context, key);
538         log_error(stmt, SQL_HANDLE_STMT);
539         throw IOException("ODBC StorageService search failed.");
540     }
541
542     SQLSMALLINT ver;
543     SQLBindCol(stmt,1,SQL_C_SSHORT,&ver,0,NULL);
544     if ((sr=SQLFetch(stmt)) == SQL_NO_DATA) {
545         freeSafeSQL(scontext, context);
546         freeSafeSQL(skey, key);
547         return 0;
548     }
549
550     // Check version?
551     if (version > 0 && version != ver) {
552         freeSafeSQL(scontext, context);
553         freeSafeSQL(skey, key);
554         return -1;
555     }
556
557     // Prepare and exectute update statement.
558     q = string("UPDATE ") + table + " SET ";
559
560     if (value) {
561         char *svalue = makeSafeSQL(value);
562         q = q + "value='" + svalue + "'" + ",version=version+1";
563         freeSafeSQL(svalue, value);
564     }
565
566     if (expiration) {
567         timestampFromTime(expiration, timebuf);
568         if (value)
569             q += ',';
570         q = q + "expires = " + timebuf;
571     }
572
573     q = q + " WHERE context='" + scontext + "' AND id='" + key + "'";
574     freeSafeSQL(scontext, context);
575     freeSafeSQL(skey, key);
576
577     m_log.debug("SQL: %s", q.c_str());
578     sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
579     if (sr==SQL_NO_DATA)
580         return 0;   // went missing?
581     else if (!SQL_SUCCEEDED(sr)) {
582         m_log.error("update of record failed (t=%s, c=%s, k=%s", table, context, key);
583         log_error(stmt, SQL_HANDLE_STMT);
584         throw IOException("ODBC StorageService failed to update record.");
585     }
586
587     return ver + 1;
588 }
589
590 bool ODBCStorageService::deleteRow(const char *table, const char *context, const char* key)
591 {
592 #ifdef _DEBUG
593     xmltooling::NDC ndc("deleteRow");
594 #endif
595
596     // Get statement handle.
597     ODBCConn conn(getHDBC());
598     ODBCStatement stmt(getHSTMT(conn));
599
600     // Prepare and execute delete statement.
601     char *scontext = makeSafeSQL(context);
602     char *skey = makeSafeSQL(key);
603     string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "' AND id='" + skey + "'";
604     freeSafeSQL(scontext, context);
605     freeSafeSQL(skey, key);
606     m_log.debug("SQL: %s", q.c_str());
607
608     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
609      if (sr==SQL_NO_DATA)
610         return false;
611     else if (!SQL_SUCCEEDED(sr)) {
612         m_log.error("error deleting record (t=%s, c=%s, k=%s)", table, context, key);
613         log_error(stmt, SQL_HANDLE_STMT);
614         throw IOException("ODBC StorageService failed to delete record.");
615     }
616
617     return true;
618 }
619
620
621 void ODBCStorageService::cleanup()
622 {
623 #ifdef _DEBUG
624     xmltooling::NDC ndc("cleanup");
625 #endif
626
627     Mutex* mutex = Mutex::create();
628
629     mutex->lock();
630
631     m_log.info("cleanup thread started... running every %d secs", m_cleanupInterval);
632
633     while (!shutdown) {
634         shutdown_wait->timedwait(mutex, m_cleanupInterval);
635         if (shutdown)
636             break;
637         try {
638             reap(NULL);
639         }
640         catch (exception& ex) {
641             m_log.error("cleanup thread swallowed exception: %s", ex.what());
642         }
643     }
644
645     m_log.info("cleanup thread exiting...");
646
647     mutex->unlock();
648     delete mutex;
649     Thread::exit(NULL);
650 }
651
652 void* ODBCStorageService::cleanup_fn(void* cache_p)
653 {
654   ODBCStorageService* cache = (ODBCStorageService*)cache_p;
655
656 #ifndef WIN32
657   // First, let's block all signals
658   Thread::mask_all_signals();
659 #endif
660
661   // Now run the cleanup process.
662   cache->cleanup();
663   return NULL;
664 }
665
666 void ODBCStorageService::updateContext(const char *table, const char* context, time_t expiration)
667 {
668 #ifdef _DEBUG
669     xmltooling::NDC ndc("updateContext");
670 #endif
671
672     // Get statement handle.
673     ODBCConn conn(getHDBC());
674     ODBCStatement stmt(getHSTMT(conn));
675
676     char timebuf[32];
677     timestampFromTime(expiration, timebuf);
678
679     char nowbuf[32];
680     timestampFromTime(time(NULL), nowbuf);
681
682     char *scontext = makeSafeSQL(context);
683     string q("UPDATE ");
684     q = q + table + " SET expires = " + timebuf + " WHERE context='" + scontext + "' AND expires > " + nowbuf;
685     freeSafeSQL(scontext, context);
686
687     m_log.debug("SQL: %s", q.c_str());
688
689     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
690     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
691         m_log.error("error updating records (t=%s, c=%s)", table, context ? context : "all");
692         log_error(stmt, SQL_HANDLE_STMT);
693         throw IOException("ODBC StorageService failed to update context expiration.");
694     }
695 }
696
697 void ODBCStorageService::reap(const char *table, const char* context)
698 {
699 #ifdef _DEBUG
700     xmltooling::NDC ndc("reap");
701 #endif
702
703     // Get statement handle.
704     ODBCConn conn(getHDBC());
705     ODBCStatement stmt(getHSTMT(conn));
706
707     // Prepare and execute delete statement.
708     char nowbuf[32];
709     timestampFromTime(time(NULL), nowbuf);
710     string q;
711     if (context) {
712         char *scontext = makeSafeSQL(context);
713         q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "' AND expires <= " + nowbuf;
714         freeSafeSQL(scontext, context);
715     }
716     else {
717         q = string("DELETE FROM ") + table + " WHERE expires <= " + nowbuf;
718     }
719     m_log.debug("SQL: %s", q.c_str());
720
721     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
722     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
723         m_log.error("error expiring records (t=%s, c=%s)", table, context ? context : "all");
724         log_error(stmt, SQL_HANDLE_STMT);
725         throw IOException("ODBC StorageService failed to purge expired records.");
726     }
727 }
728
729 void ODBCStorageService::deleteContext(const char *table, const char* context)
730 {
731 #ifdef _DEBUG
732     xmltooling::NDC ndc("deleteContext");
733 #endif
734
735     // Get statement handle.
736     ODBCConn conn(getHDBC());
737     ODBCStatement stmt(getHSTMT(conn));
738
739     // Prepare and execute delete statement.
740     char *scontext = makeSafeSQL(context);
741     string q = string("DELETE FROM ") + table + " WHERE context='" + scontext + "'";
742     freeSafeSQL(scontext, context);
743     m_log.debug("SQL: %s", q.c_str());
744
745     SQLRETURN sr=SQLExecDirect(stmt, (SQLCHAR*)q.c_str(), SQL_NTS);
746     if ((sr!=SQL_NO_DATA) && !SQL_SUCCEEDED(sr)) {
747         m_log.error("error deleting context (t=%s, c=%s)", table, context);
748         log_error(stmt, SQL_HANDLE_STMT);
749         throw IOException("ODBC StorageService failed to delete context.");
750     }
751 }
752
753 extern "C" int ODBCSTORE_EXPORTS xmltooling_extension_init(void*)
754 {
755     // Register this SS type
756     XMLToolingConfig::getConfig().StorageServiceManager.registerFactory("ODBC", ODBCStorageServiceFactory);
757     return 0;
758 }
759
760 extern "C" void ODBCSTORE_EXPORTS xmltooling_extension_term()
761 {
762     XMLToolingConfig::getConfig().StorageServiceManager.deregisterFactory("ODBC");
763 }