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