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