1. Implement a messages method to return a list of previously
[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() {
130
131         osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
132         dbi_initialize( NULL );
133         osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
134
135         char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", modulename );
136         char* user   = osrf_settings_host_value( "/apps/%s/app_settings/database/user", modulename );
137         char* host   = osrf_settings_host_value( "/apps/%s/app_settings/database/host", modulename );
138         char* port   = osrf_settings_host_value( "/apps/%s/app_settings/database/port", modulename );
139         char* db     = osrf_settings_host_value( "/apps/%s/app_settings/database/db", modulename );
140         char* pw     = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", modulename );
141
142         osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
143         dbhandle = dbi_conn_new( driver );
144
145         if( !dbhandle ) {
146                 osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
147                 return -1;
148         }
149         osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
150
151         osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database.  host=%s, "
152                         "port=%s, user=%s, db=%s", modulename, host, port, user, db );
153
154         if( host ) dbi_conn_set_option( dbhandle, "host", host );
155         if( port ) dbi_conn_set_option_numeric( dbhandle, "port", atoi( port ));
156         if( user ) dbi_conn_set_option( dbhandle, "username", user );
157         if( pw )   dbi_conn_set_option( dbhandle, "password", pw );
158         if( db )   dbi_conn_set_option( dbhandle, "dbname", db );
159
160         free( user );
161         free( host );
162         free( port );
163         free( db );
164         free( pw );
165
166         const char* err;
167         if( dbi_conn_connect( dbhandle ) < 0 ) {
168                 sleep( 1 );
169                 if( dbi_conn_connect( dbhandle ) < 0 ) {
170                         dbi_conn_error( dbhandle, &err );
171                         osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s", err );
172                         return -1;
173                 }
174         }
175
176         oilsSetDBConnection( dbhandle );
177         osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
178
179         // Add datatypes from database to the fields in the IDL
180         //if( oilsExtendIDL() ) {
181         //      osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
182         //      return -1;
183         //}
184         //else
185                 return 0;
186 }
187
188 /**
189         @brief Load a specified query from the database query tables.
190         @param ctx Pointer to the current method context.
191         @return Zero if successful, or -1 if not.
192
193         Method parameters:
194         - query id (key of query.stored_query table)
195
196         Returns: a character string serving as a token for future references to the query.
197
198         NB: the method return type is temporary.  Eventually this method will return both a token
199         and a list of bind variables.
200 */
201 int doPrepare( osrfMethodContext* ctx ) {
202         if(osrfMethodVerifyContext( ctx )) {
203                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
204                 return -1;
205         }
206
207         // Get the query id from a method parameter
208         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
209         if( query_id_obj->type != JSON_NUMBER ) {
210                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
211                         ctx->request, "Invalid parameter; query id must be a number" );
212                 return -1;
213         }
214
215         int query_id = atoi( jsonObjectGetString( query_id_obj ));
216         if( query_id <= 0 ) {
217                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
218                         ctx->request, "Invalid parameter: query id must be greater than zero" );
219                 return -1;
220         }
221
222         osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
223
224         BuildSQLState* state = buildSQLStateNew( dbhandle );
225         StoredQ* query = getStoredQuery( state, query_id );
226         if( state->error ) {
227                 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
228                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
229                         ctx->request, "Unable to load stored query" );
230                 return -1;
231         }
232
233         const char* token = save_query( ctx, state, query );
234
235         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
236
237         osrfAppRespondComplete( ctx, jsonNewObject( token ));
238         return 0;
239 }
240
241 int doBindParam( osrfMethodContext* ctx ) {
242         if(osrfMethodVerifyContext( ctx )) {
243                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
244                 return -1;
245         }
246
247         // Get the query token from a method parameter
248         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
249         if( token_obj->type != JSON_STRING ) {
250                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
251                         ctx->request, "Invalid parameter; query token must be a string" );
252                 return -1;
253         }
254         const char* token = jsonObjectGetString( token_obj );
255
256         // Look up the query token in the session-level userData
257         CachedQuery* query = search_token( ctx, token );
258         if( !query ) {
259                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
260                                                           ctx->request, "Invalid query token" );
261                 return -1;
262         }
263
264         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
265
266         osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
267         return 0;
268 }
269
270 /**
271         @brief Execute an SQL query and return a result set.
272         @param ctx Pointer to the current method context.
273         @return Zero if successful, or -1 if not.
274
275         Method parameters:
276         - query token, as previously returned by the .prepare method.
277
278         Returns: A series of responses, each of them a row represented as an array of column values.
279 */
280 int doExecute( osrfMethodContext* ctx ) {
281         if(osrfMethodVerifyContext( ctx )) {
282                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
283                 return -1;
284         }
285
286         // Get the query token
287         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
288         if( token_obj->type != JSON_STRING ) {
289                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
290                         ctx->request, "Invalid parameter; query token must be a string" );
291                 return -1;
292         }
293         const char* token = jsonObjectGetString( token_obj );
294
295         // Look up the query token in the session-level userData
296         CachedQuery* query = search_token( ctx, token );
297         if( !query ) {
298                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
299                         ctx->request, "Invalid query token" );
300                 return -1;
301         }
302
303         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
304         if( query->state->error ) {
305                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
306                         "No valid prepared query available for query id # %d", query->query->id ));
307                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
308                                                           ctx->request, "No valid prepared query available" );
309                 return -1;
310         } else if( buildSQL( query->state, query->query )) {
311                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
312                         "Unable to build SQL statement for query id # %d", query->query->id ));
313                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
314                         ctx->request, "Unable to build SQL statement" );
315                 return -1;
316         }
317
318         jsonObject* row = oilsFirstRow( query->state );
319         while( row ) {
320                 osrfAppRespond( ctx, row );
321                 row = oilsNextRow( query->state );
322         }
323
324         if( query->state->error ) {
325                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
326                         "Unable to execute SQL statement for query id # %d", query->query->id ));
327                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
328                         ctx->request, "Unable to execute SQL statement" );
329                 return -1;
330         }
331
332         osrfAppRespondComplete( ctx, NULL );
333         return 0;
334 }
335
336 /**
337         @brief Construct an SQL query, but without executing it.
338         @param ctx Pointer to the current method context.
339         @return Zero if successful, or -1 if not.
340
341         Method parameters:
342         - query token, as previously returned by the .prepare method.
343
344         Returns: A string containing an SQL query..
345 */
346 int doSql( osrfMethodContext* ctx ) {
347         if(osrfMethodVerifyContext( ctx )) {
348                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
349                 return -1;
350         }
351
352         // Get the query token
353         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
354         if( token_obj->type != JSON_STRING ) {
355                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
356                         ctx->request, "Invalid parameter; query token must be a string" );
357                 return -1;
358         }
359         const char* token = jsonObjectGetString( token_obj );
360
361         // Look up the query token in the session-level userData
362         CachedQuery* query = search_token( ctx, token );
363         if( !query ) {
364                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
365                         ctx->request, "Invalid query token" );
366                 return -1;
367         }
368
369         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
370         if( query->state->error ) {
371                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
372                         "No valid prepared query available for query id # %d", query->query->id ));
373                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
374                         ctx->request, "No valid prepared query available" );
375                 return -1;
376         } else if( buildSQL( query->state, query->query )) {
377                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
378                         "Unable to build SQL statement for query id # %d", query->query->id ));
379                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
380                         ctx->request, "Unable to build SQL statement" );
381                 return -1;
382         }
383
384         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
385         return 0;
386 }
387
388 /**
389         @brief Return a list of previously generated error messages for a specified query.
390         @param ctx Pointer to the current method context.
391         @return Zero if successful, or -1 if not.
392
393         Method parameters:
394         - query token, as previously returned by the .prepare method.
395
396         Returns: A (possibly empty) array of strings, each one an error message generated during
397         previous operations in connection with the specified query.
398 */
399 int doMessages( osrfMethodContext* ctx ) {
400         if(osrfMethodVerifyContext( ctx )) {
401                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
402                 return -1;
403         }
404
405         // Get the query token from a method parameter
406         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
407         if( token_obj->type != JSON_STRING ) {
408                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
409                         ctx->request, "Invalid parameter; query token must be a string" );
410                 return -1;
411         }
412         const char* token = jsonObjectGetString( token_obj );
413
414         // Look up the query token in the session-level userData
415         CachedQuery* query = search_token( ctx, token );
416         if( !query ) {
417                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
418                         ctx->request, "Invalid query token" );
419                 return -1;
420         }
421
422         osrfLogInfo( OSRF_LOG_MARK, "Returning messages for token %s", token );
423
424         jsonObject* msgs = jsonNewObjectType( JSON_ARRAY );
425         const osrfStringArray* error_msgs = query->state->error_msgs;
426         int i;
427         for( i = 0; i < error_msgs->size; ++i ) {
428                 jsonObject* msg = jsonNewObject( osrfStringArrayGetString( error_msgs, i ));
429                 jsonObjectPush( msgs, msg );
430         }
431
432         osrfAppRespondComplete( ctx, msgs );
433         return 0;
434 }
435
436 /**
437         @brief Discard a previously stored query, as identified by a token.
438         @param ctx Pointer to the current method context.
439         @return Zero if successful, or -1 if not.
440
441         Method parameters:
442         - query token, as previously returned by the .prepare method.
443
444         Returns: Nothing.
445 */
446 int doFinish( osrfMethodContext* ctx ) {
447         if(osrfMethodVerifyContext( ctx )) {
448                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
449                 return -1;
450         }
451
452         // Get the query token.
453         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
454         if( token_obj->type != JSON_STRING ) {
455                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
456                                                           ctx->request, "Invalid parameter; query token must be a string" );
457                 return -1;
458         }
459         const char* token = jsonObjectGetString( token_obj );
460
461         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
462         // just ignore the problem and report success.
463         osrfHash* cache = ctx->session->userData;
464         if( cache )
465                 osrfHashRemove( cache, token );
466
467         osrfAppRespondComplete( ctx, NULL );
468         return 0;
469 }
470
471 /**
472         @brief Save a query in session-level userData for reference in future method calls.
473         @param ctx Pointer to the current method context.
474         @param state Pointer to the state of the query.
475         @param query Pointer to the abstract representation of the query.
476         @return Pointer to an identifying token to be returned to the client.
477 */
478 static const char* save_query(
479         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
480
481         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
482         cached_query->state       = state;
483         cached_query->query       = query;
484
485         // Get the cache.  If we don't have one yet, make one.
486         osrfHash* cache = ctx->session->userData;
487         if( !cache ) {
488                 cache = osrfNewHash();
489                 osrfHashSetCallback( cache, free_cached_query );
490                 ctx->session->userData = cache;
491                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
492         }
493
494         // Create a token string to be used as a key
495         static unsigned int token_count = 0;
496         char* token = va_list_to_string(
497                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
498
499         osrfHashSet( cache, cached_query, token );
500         return token;
501 }
502
503 /**
504         @brief Free a CachedQuery
505         @param Pointer to the CachedQuery to be freed.
506 */
507 static void free_cached_query( char* key, void* data ) {
508         if( data ) {
509                 CachedQuery* cached_query = data;
510                 buildSQLStateFree( cached_query->state );
511                 storedQFree( cached_query->query );
512         }
513 }
514
515 /**
516         @brief Callback for freeing session-level userData.
517         @param blob Opaque pointer t userData.
518 */
519 static void userDataFree( void* blob ) {
520         osrfHashFree( (osrfHash*) blob );
521 }
522
523 /**
524         @brief Search for the cached query corresponding to a given token.
525         @param ctx Pointer to the current method context.
526         @param token Token string from a previous call to the prepare method.
527         @return A pointer to the cached query, if found, or NULL if not.
528 */
529 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
530         if( ctx && ctx->session->userData && token ) {
531                 osrfHash* cache = ctx->session->userData;
532                 return osrfHashGet( cache, token );
533         } else
534                 return NULL;
535 }