]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
Create a reusable function for connecting to the database.
[working/Evergreen.git] / Open-ILS / src / c-apps / oils_qstore.c
1 /**
2         @file oils_qstore.c
3         @brief As a server, perform database queries as defined in the database itself.
4 */
5
6 #include <stdlib.h>
7 #include <string.h>
8 #include <ctype.h>
9 #include <dbi/dbi.h>
10 #include "opensrf/utils.h"
11 #include "opensrf/log.h"
12 #include "opensrf/osrf_json.h"
13 #include "opensrf/osrf_application.h"
14 #include "openils/oils_utils.h"
15 #include "openils/oils_sql.h"
16 #include "openils/oils_buildq.h"
17
18 /**
19         @brief Information about a previously prepared query.
20
21         We store an osrfHash of CachedQueries in the userData area of the application session,
22         keyed on query token.  That way we can fetch what a previous call to the prepare method
23         has prepared.
24 */
25 typedef struct {
26         BuildSQLState* state;
27         StoredQ*       query;
28 } CachedQuery;
29
30 static dbi_conn dbhandle; /* our db connection */
31
32 static const char modulename[] = "open-ils.qstore";
33
34 int doPrepare( osrfMethodContext* ctx );
35 int doExecute( osrfMethodContext* ctx );
36 int doSql( osrfMethodContext* ctx );
37
38 static const char* save_query(
39         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query );
40 static void free_cached_query( char* key, void* data );
41 static void userDataFree( void* blob );
42 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token );
43
44 /**
45         @brief Disconnect from the database.
46
47         This function is called when the server drone is about to terminate.
48 */
49 void osrfAppChildExit() {
50         osrfLogDebug( OSRF_LOG_MARK, "Child is exiting, disconnecting from database..." );
51
52         if ( dbhandle ) {
53                 dbi_conn_query( dbhandle, "ROLLBACK;" );
54                 dbi_conn_close( dbhandle );
55                 dbhandle = NULL;
56         }
57 }
58
59 /**
60         @brief Initialize the application.
61         @return Zero if successful, or non-zero if not.
62
63         Load the IDL file into an internal data structure for future reference.  Each non-virtual
64         class in the IDL corresponds to a table or view in the database, or to a subquery defined
65         in the IDL.  Ignore all virtual tables and virtual fields.
66
67         Register the functions for remote procedure calls.
68
69         This function is called when the registering the application, and is executed by the
70         listener before spawning the drones.
71 */
72 int osrfAppInitialize() {
73
74         osrfLogInfo( OSRF_LOG_MARK, "Initializing the QStore Server..." );
75         osrfLogInfo( OSRF_LOG_MARK, "Finding XML file..." );
76
77         if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
78                 return 1; /* return non-zero to indicate error */
79
80         growing_buffer* method_name = buffer_init( 64 );
81
82         OSRF_BUFFER_ADD( method_name, modulename );
83         OSRF_BUFFER_ADD( method_name, ".prepare" );
84         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
85                         "doPrepare", "", 1, 0 );
86
87         buffer_reset( method_name );
88         OSRF_BUFFER_ADD( method_name, modulename );
89         OSRF_BUFFER_ADD( method_name, ".bind_param" );
90         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
91                         "doBindParam", "", 2, 0 );
92
93         buffer_reset( method_name );
94         OSRF_BUFFER_ADD( method_name, modulename );
95         OSRF_BUFFER_ADD( method_name, ".execute" );
96         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
97                         "doExecute", "", 1, OSRF_METHOD_STREAMING );
98
99         buffer_reset( method_name );
100         OSRF_BUFFER_ADD( method_name, modulename );
101         OSRF_BUFFER_ADD( method_name, ".sql" );
102         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
103                         "doSql", "", 1, OSRF_METHOD_STREAMING );
104
105         buffer_reset( method_name );
106         OSRF_BUFFER_ADD( method_name, modulename );
107         OSRF_BUFFER_ADD( method_name, ".finish" );
108         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
109                         "doFinish", "", 1, 0 );
110
111         buffer_reset( method_name );
112         OSRF_BUFFER_ADD( method_name, modulename );
113         OSRF_BUFFER_ADD( method_name, ".messages" );
114         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
115                         "doMessages", "", 1, 0 );
116
117         return 0;
118 }
119
120 /**
121         @brief Initialize a server drone.
122         @return Zero if successful, -1 if not.
123
124         Connect to the database.  For each non-virtual class in the IDL, execute a dummy "SELECT * "
125         query to get the datatype of each column.  Record the datatypes in the loaded IDL.
126
127         This function is called by a server drone shortly after it is spawned by the listener.
128 */
129 int osrfAppChildInit( void ) {
130
131         dbhandle = oilsConnectDB( modulename );
132         if( !dbhandle )
133                 return -1;
134         else {
135                 oilsSetDBConnection( dbhandle );
136                 osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
137
138                 // Apply datatypes from database to the fields in the IDL
139                 //if( oilsExtendIDL() ) {
140                 //      osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
141                 //      return -1;
142                 //}
143                 //else
144                 return 0;
145         }
146 }
147
148 /**
149         @brief Load a specified query from the database query tables.
150         @param ctx Pointer to the current method context.
151         @return Zero if successful, or -1 if not.
152
153         Method parameters:
154         - query id (key of query.stored_query table)
155
156         Returns: a character string serving as a token for future references to the query.
157
158         NB: the method return type is temporary.  Eventually this method will return both a token
159         and a list of bind variables.
160 */
161 int doPrepare( osrfMethodContext* ctx ) {
162         if(osrfMethodVerifyContext( ctx )) {
163                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
164                 return -1;
165         }
166
167         // Get the query id from a method parameter
168         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
169         if( query_id_obj->type != JSON_NUMBER ) {
170                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
171                         ctx->request, "Invalid parameter; query id must be a number" );
172                 return -1;
173         }
174
175         int query_id = atoi( jsonObjectGetString( query_id_obj ));
176         if( query_id <= 0 ) {
177                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
178                         ctx->request, "Invalid parameter: query id must be greater than zero" );
179                 return -1;
180         }
181
182         osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
183
184         BuildSQLState* state = buildSQLStateNew( dbhandle );
185         StoredQ* query = getStoredQuery( state, query_id );
186         if( state->error ) {
187                 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
188                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
189                         ctx->request, "Unable to load stored query" );
190                 return -1;
191         }
192
193         const char* token = save_query( ctx, state, query );
194
195         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
196
197         osrfAppRespondComplete( ctx, jsonNewObject( token ));
198         return 0;
199 }
200
201 int doBindParam( osrfMethodContext* ctx ) {
202         if(osrfMethodVerifyContext( ctx )) {
203                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
204                 return -1;
205         }
206
207         // Get the query token from a method parameter
208         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
209         if( token_obj->type != JSON_STRING ) {
210                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
211                         ctx->request, "Invalid parameter; query token must be a string" );
212                 return -1;
213         }
214         const char* token = jsonObjectGetString( token_obj );
215
216         // Look up the query token in the session-level userData
217         CachedQuery* query = search_token( ctx, token );
218         if( !query ) {
219                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
220                                                           ctx->request, "Invalid query token" );
221                 return -1;
222         }
223
224         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
225
226         osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
227         return 0;
228 }
229
230 /**
231         @brief Execute an SQL query and return a result set.
232         @param ctx Pointer to the current method context.
233         @return Zero if successful, or -1 if not.
234
235         Method parameters:
236         - query token, as previously returned by the .prepare method.
237
238         Returns: A series of responses, each of them a row represented as an array of column values.
239 */
240 int doExecute( osrfMethodContext* ctx ) {
241         if(osrfMethodVerifyContext( ctx )) {
242                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
243                 return -1;
244         }
245
246         // Get the query token
247         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
248         if( token_obj->type != JSON_STRING ) {
249                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
250                         ctx->request, "Invalid parameter; query token must be a string" );
251                 return -1;
252         }
253         const char* token = jsonObjectGetString( token_obj );
254
255         // Look up the query token in the session-level userData
256         CachedQuery* query = search_token( ctx, token );
257         if( !query ) {
258                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
259                         ctx->request, "Invalid query token" );
260                 return -1;
261         }
262
263         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
264         if( query->state->error ) {
265                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
266                         "No valid prepared query available for query id # %d", query->query->id ));
267                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
268                                                           ctx->request, "No valid prepared query available" );
269                 return -1;
270         } else if( buildSQL( query->state, query->query )) {
271                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
272                         "Unable to build SQL statement for query id # %d", query->query->id ));
273                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
274                         ctx->request, "Unable to build SQL statement" );
275                 return -1;
276         }
277
278         jsonObject* row = oilsFirstRow( query->state );
279         while( row ) {
280                 osrfAppRespond( ctx, row );
281                 row = oilsNextRow( query->state );
282         }
283
284         if( query->state->error ) {
285                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
286                         "Unable to execute SQL statement for query id # %d", query->query->id ));
287                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
288                         ctx->request, "Unable to execute SQL statement" );
289                 return -1;
290         }
291
292         osrfAppRespondComplete( ctx, NULL );
293         return 0;
294 }
295
296 /**
297         @brief Construct an SQL query, but without executing it.
298         @param ctx Pointer to the current method context.
299         @return Zero if successful, or -1 if not.
300
301         Method parameters:
302         - query token, as previously returned by the .prepare method.
303
304         Returns: A string containing an SQL query..
305 */
306 int doSql( osrfMethodContext* ctx ) {
307         if(osrfMethodVerifyContext( ctx )) {
308                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
309                 return -1;
310         }
311
312         // Get the query token
313         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
314         if( token_obj->type != JSON_STRING ) {
315                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
316                         ctx->request, "Invalid parameter; query token must be a string" );
317                 return -1;
318         }
319         const char* token = jsonObjectGetString( token_obj );
320
321         // Look up the query token in the session-level userData
322         CachedQuery* query = search_token( ctx, token );
323         if( !query ) {
324                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
325                         ctx->request, "Invalid query token" );
326                 return -1;
327         }
328
329         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
330         if( query->state->error ) {
331                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
332                         "No valid prepared query available for query id # %d", query->query->id ));
333                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
334                         ctx->request, "No valid prepared query available" );
335                 return -1;
336         } else if( buildSQL( query->state, query->query )) {
337                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
338                         "Unable to build SQL statement for query id # %d", query->query->id ));
339                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
340                         ctx->request, "Unable to build SQL statement" );
341                 return -1;
342         }
343
344         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
345         return 0;
346 }
347
348 /**
349         @brief Return a list of previously generated error messages for a specified query.
350         @param ctx Pointer to the current method context.
351         @return Zero if successful, or -1 if not.
352
353         Method parameters:
354         - query token, as previously returned by the .prepare method.
355
356         Returns: A (possibly empty) array of strings, each one an error message generated during
357         previous operations in connection with the specified query.
358 */
359 int doMessages( osrfMethodContext* ctx ) {
360         if(osrfMethodVerifyContext( ctx )) {
361                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
362                 return -1;
363         }
364
365         // Get the query token from a method parameter
366         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
367         if( token_obj->type != JSON_STRING ) {
368                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
369                         ctx->request, "Invalid parameter; query token must be a string" );
370                 return -1;
371         }
372         const char* token = jsonObjectGetString( token_obj );
373
374         // Look up the query token in the session-level userData
375         CachedQuery* query = search_token( ctx, token );
376         if( !query ) {
377                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
378                         ctx->request, "Invalid query token" );
379                 return -1;
380         }
381
382         osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
383
384         jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
385         const osrfStringArray* error_msgs = query->state->error_msgs;
386         int i;
387         for( i = 0; i < error_msgs->size; ++i ) {
388                 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
389                 jsonObjectPush( msgs, msg );
390         }
391
392         osrfAppRespondComplete( ctx, msgs );
393         return 0;
394 }
395
396 /**
397         @brief Discard a previously stored query, as identified by a token.
398         @param ctx Pointer to the current method context.
399         @return Zero if successful, or -1 if not.
400
401         Method parameters:
402         - query token, as previously returned by the .prepare method.
403
404         Returns: Nothing.
405 */
406 int doFinish( osrfMethodContext* ctx ) {
407         if(osrfMethodVerifyContext( ctx )) {
408                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
409                 return -1;
410         }
411
412         // Get the query token.
413         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
414         if( token_obj->type != JSON_STRING ) {
415                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
416                                                           ctx->request, "Invalid parameter; query token must be a string" );
417                 return -1;
418         }
419         const char* token = jsonObjectGetString( token_obj );
420
421         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
422         // just ignore the problem and report success.
423         osrfHash* cache = ctx->session->userData;
424         if( cache )
425                 osrfHashRemove( cache, token );
426
427         osrfAppRespondComplete( ctx, NULL );
428         return 0;
429 }
430
431 /**
432         @brief Save a query in session-level userData for reference in future method calls.
433         @param ctx Pointer to the current method context.
434         @param state Pointer to the state of the query.
435         @param query Pointer to the abstract representation of the query.
436         @return Pointer to an identifying token to be returned to the client.
437 */
438 static const char* save_query(
439         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
440
441         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
442         cached_query->state       = state;
443         cached_query->query       = query;
444
445         // Get the cache.  If we don't have one yet, make one.
446         osrfHash* cache = ctx->session->userData;
447         if( !cache ) {
448                 cache = osrfNewHash();
449                 osrfHashSetCallback( cache, free_cached_query );
450                 ctx->session->userData = cache;
451                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
452         }
453
454         // Create a token string to be used as a key
455         static unsigned int token_count = 0;
456         char* token = va_list_to_string(
457                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
458
459         osrfHashSet( cache, cached_query, token );
460         return token;
461 }
462
463 /**
464         @brief Free a CachedQuery
465         @param Pointer to the CachedQuery to be freed.
466 */
467 static void free_cached_query( char* key, void* data ) {
468         if( data ) {
469                 CachedQuery* cached_query = data;
470                 buildSQLStateFree( cached_query->state );
471                 storedQFree( cached_query->query );
472         }
473 }
474
475 /**
476         @brief Callback for freeing session-level userData.
477         @param blob Opaque pointer t userData.
478 */
479 static void userDataFree( void* blob ) {
480         osrfHashFree( (osrfHash*) blob );
481 }
482
483 /**
484         @brief Search for the cached query corresponding to a given token.
485         @param ctx Pointer to the current method context.
486         @param token Token string from a previous call to the prepare method.
487         @return A pointer to the cached query, if found, or NULL if not.
488 */
489 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
490         if( ctx && ctx->session->userData && token ) {
491                 osrfHash* cache = ctx->session->userData;
492                 return osrfHashGet( cache, token );
493         } else
494                 return NULL;
495 }