]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
dbf74f662978a744ce5d4699de46aea422554d85
[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         return 0;
112 }
113
114 /**
115         @brief Initialize a server drone.
116         @return Zero if successful, -1 if not.
117
118         Connect to the database.  For each non-virtual class in the IDL, execute a dummy "SELECT * "
119         query to get the datatype of each column.  Record the datatypes in the loaded IDL.
120
121         This function is called by a server drone shortly after it is spawned by the listener.
122 */
123 int osrfAppChildInit() {
124
125         osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
126         dbi_initialize( NULL );
127         osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
128
129         char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", modulename );
130         char* user   = osrf_settings_host_value( "/apps/%s/app_settings/database/user", modulename );
131         char* host   = osrf_settings_host_value( "/apps/%s/app_settings/database/host", modulename );
132         char* port   = osrf_settings_host_value( "/apps/%s/app_settings/database/port", modulename );
133         char* db     = osrf_settings_host_value( "/apps/%s/app_settings/database/db", modulename );
134         char* pw     = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", modulename );
135
136         osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
137         dbhandle = dbi_conn_new( driver );
138
139         if( !dbhandle ) {
140                 osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
141                 return -1;
142         }
143         osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
144
145         osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database.  host=%s, "
146                         "port=%s, user=%s, db=%s", modulename, host, port, user, db );
147
148         if( host ) dbi_conn_set_option( dbhandle, "host", host );
149         if( port ) dbi_conn_set_option_numeric( dbhandle, "port", atoi( port ) );
150         if( user ) dbi_conn_set_option( dbhandle, "username", user );
151         if( pw )   dbi_conn_set_option( dbhandle, "password", pw );
152         if( db )   dbi_conn_set_option( dbhandle, "dbname", db );
153
154         free( user );
155         free( host );
156         free( port );
157         free( db );
158         free( pw );
159
160         const char* err;
161         if( dbi_conn_connect( dbhandle ) < 0 ) {
162                 sleep( 1 );
163                 if( dbi_conn_connect( dbhandle ) < 0 ) {
164                         dbi_conn_error( dbhandle, &err );
165                         osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s", err );
166                         return -1;
167                 }
168         }
169
170         oilsSetDBConnection( dbhandle );
171         osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
172
173         // Add datatypes from database to the fields in the IDL
174         if( oilsExtendIDL() ) {
175                 osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
176                 return -1;
177         }
178         else
179                 return 0;
180 }
181
182 /**
183         @brief Load a specified query from the database query tables.
184         @param ctx Pointer to the current method context.
185         @return Zero if successful, or -1 if not.
186
187         Method parameters:
188         - query id (key of query.stored_query table)
189
190         Returns: a character string serving as a token for future references to the query.
191
192         NB: the method return type is temporary.  Eventually this method will return both a token
193         and a list of bind variables.
194 */
195 int doPrepare( osrfMethodContext* ctx ) {
196         if(osrfMethodVerifyContext( ctx )) {
197                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
198                 return -1;
199         }
200
201         // Get the query id from a method parameter
202         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
203         if( query_id_obj->type != JSON_NUMBER ) {
204                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
205                         ctx->request, "Invalid parameter; query id must be a number" );
206                 return -1;
207         }
208
209         int query_id = atoi( jsonObjectGetString( query_id_obj ));
210         if( query_id <= 0 ) {
211                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
212                         ctx->request, "Invalid parameter: query id must be greater than zero" );
213                 return -1;
214         }
215
216         osrfLogInfo( OSRF_LOG_MARK, "Loading query for id # %d", query_id );
217
218         BuildSQLState* state = buildSQLStateNew( dbhandle );
219         StoredQ* query = getStoredQuery( state, query_id );
220         if( state->error ) {
221                 osrfLogWarning( OSRF_LOG_MARK, "Unable to load stored query # %d", query_id );
222                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
223                         ctx->request, "Unable to load stored query" );
224                 return -1;
225         }
226
227         const char* token = save_query( ctx, state, query );
228
229         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
230
231         osrfAppRespondComplete( ctx, jsonNewObject( token ));
232         return 0;
233 }
234
235 int doBindParam( osrfMethodContext* ctx ) {
236         if(osrfMethodVerifyContext( ctx )) {
237                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
238                 return -1;
239         }
240
241         // Get the query token from a method parameter
242         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
243         if( token_obj->type != JSON_STRING ) {
244                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
245                         ctx->request, "Invalid parameter; query token must be a string" );
246                 return -1;
247         }
248         const char* token = jsonObjectGetString( token_obj );
249
250         // Look up the query token in the session-level userData
251         CachedQuery* query = search_token( ctx, token );
252         if( !query ) {
253                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
254                                                           ctx->request, "Invalid query token" );
255                 return -1;
256         }
257
258         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
259
260         osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
261         return 0;
262 }
263
264 int doExecute( osrfMethodContext* ctx ) {
265         if(osrfMethodVerifyContext( ctx )) {
266                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
267                 return -1;
268         }
269
270         // Get the query token
271         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
272         if( token_obj->type != JSON_STRING ) {
273                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
274                         ctx->request, "Invalid parameter; query token must be a string" );
275                 return -1;
276         }
277         const char* token = jsonObjectGetString( token_obj );
278
279         // Look up the query token in the session-level userData
280         CachedQuery* query = search_token( ctx, token );
281         if( !query ) {
282                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
283                         ctx->request, "Invalid query token" );
284                 return -1;
285         }
286
287         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
288         if( query->state->error ) {
289                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
290                         "No valid prepared query available for query id # %d", query->query->id ));
291                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
292                                                           ctx->request, "No valid prepared query available" );
293                 return -1;
294         } else if( buildSQL( query->state, query->query )) {
295                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
296                         "Unable to build SQL statement for query id # %d", query->query->id ));
297                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
298                         ctx->request, "Unable to build SQL statement" );
299                 return -1;
300         }
301
302         jsonObject* row = oilsFirstRow( query->state );
303         while( row ) {
304                 osrfAppRespond( ctx, row );
305                 row = oilsNextRow( query->state );
306         }
307
308         if( query->state->error ) {
309                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
310                         "Unable to execute SQL statement for query id # %d", query->query->id ));
311                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
312                         ctx->request, "Unable to execute SQL statement" );
313                 return -1;
314         }
315
316         osrfAppRespondComplete( ctx, NULL );
317         return 0;
318 }
319
320 int doSql( osrfMethodContext* ctx ) {
321         if(osrfMethodVerifyContext( ctx )) {
322                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
323                 return -1;
324         }
325
326         // Get the query token
327         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
328         if( token_obj->type != JSON_STRING ) {
329                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
330                         ctx->request, "Invalid parameter; query token must be a string" );
331                 return -1;
332         }
333         const char* token = jsonObjectGetString( token_obj );
334
335         // Look up the query token in the session-level userData
336         CachedQuery* query = search_token( ctx, token );
337         if( !query ) {
338                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
339                         ctx->request, "Invalid query token" );
340                 return -1;
341         }
342
343         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
344         if( query->state->error ) {
345                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
346                         "No valid prepared query available for query id # %d", query->query->id ));
347                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
348                         ctx->request, "No valid prepared query available" );
349                 return -1;
350         } else if( buildSQL( query->state, query->query )) {
351                 osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( query->state,
352                         "Unable to build SQL statement for query id # %d", query->query->id ));
353                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
354                         ctx->request, "Unable to build SQL statement" );
355                 return -1;
356         }
357
358         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
359         return 0;
360 }
361
362 /**
363         @brief Discard a previously stored query, as identified by a token.
364         @param ctx Pointer to the current method context.
365         @return Zero if successful, or -1 if not.
366
367         Method parameters:
368         - query token, as previously returned by the .prepare method.
369
370         Returns: Nothing.
371 */
372 int doFinish( osrfMethodContext* ctx ) {
373         if(osrfMethodVerifyContext( ctx )) {
374                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
375                 return -1;
376         }
377
378         // Get the query token.
379         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
380         if( token_obj->type != JSON_STRING ) {
381                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
382                                                           ctx->request, "Invalid parameter; query token must be a string" );
383                 return -1;
384         }
385         const char* token = jsonObjectGetString( token_obj );
386
387         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
388         // just ignore the problem and report success.
389         osrfHash* cache = ctx->session->userData;
390         if( cache )
391                 osrfHashRemove( cache, token );
392
393         osrfAppRespondComplete( ctx, NULL );
394         return 0;
395 }
396
397 /**
398         @brief Save a query in session-level userData for reference in future method calls.
399         @param ctx Pointer to the current method context.
400         @param state Pointer to the state of the query.
401         @param query Pointer to the abstract representation of the query.
402         @return Pointer to an identifying token to be returned to the client.
403 */
404 static const char* save_query( 
405         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
406
407         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
408         cached_query->state       = state;
409         cached_query->query       = query;
410
411         // Get the cache.  If we don't have one yet, make one.
412         osrfHash* cache = ctx->session->userData;
413         if( !cache ) {
414                 cache = osrfNewHash();
415                 osrfHashSetCallback( cache, free_cached_query );
416                 ctx->session->userData = cache;
417                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
418         }
419
420         // Create a token string to be used as a key
421         static unsigned int token_count = 0;
422         char* token = va_list_to_string(
423                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
424
425         osrfHashSet( cache, cached_query, token );
426         return token;
427 }
428
429 /**
430         @brief Free a CachedQuery
431         @param Pointer to the CachedQuery to be freed.
432 */
433 static void free_cached_query( char* key, void* data ) {
434         if( data ) {
435                 CachedQuery* cached_query = data;
436                 buildSQLStateFree( cached_query->state );
437                 storedQFree( cached_query->query );
438         }
439 }
440
441 /**
442         @brief Callback for freeing session-level userData.
443         @param blob Opaque pointer t userData.
444 */
445 static void userDataFree( void* blob ) {
446         osrfHashFree( (osrfHash*) blob );
447 }
448
449 /**
450         @brief Search for the cached query corresponding to a given token.
451         @param ctx Pointer to the current method context.
452         @param token Token string from a previous call to the prepare method.
453         @return A pointer to the cached query, if found, or NULL if not.
454 */
455 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
456         if( ctx && ctx->session->userData && token ) {
457                 osrfHash* cache = ctx->session->userData;
458                 return osrfHashGet( cache, token );
459         } else
460                 return NULL;
461 }