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