]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
Implement the .finish method for the qstore server.
[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
289         osrfAppRespondComplete( ctx, jsonNewObject( "execute method not yet implemented" ));
290         return 0;
291 }
292
293 int doSql( osrfMethodContext* ctx ) {
294         if(osrfMethodVerifyContext( ctx )) {
295                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
296                 return -1;
297         }
298
299         // Get the query token
300         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
301         if( token_obj->type != JSON_STRING ) {
302                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
303                         ctx->request, "Invalid parameter; query token must be a string" );
304                 return -1;
305         }
306         const char* token = jsonObjectGetString( token_obj );
307
308         // Look up the query token in the session-level userData
309         CachedQuery* query = search_token( ctx, token );
310         if( !query ) {
311                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
312                         ctx->request, "Invalid query token" );
313                 return -1;
314         }
315
316         osrfLogInfo( OSRF_LOG_MARK, "Returning SQL for token \"%s\"", token );
317         if( query->state->error ) {
318                 osrfLogWarning( OSRF_LOG_MARK, "No valid prepared query available for query id # %d",
319                         query->query->id );
320                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
321                         ctx->request, "No valid prepared query available" );
322                 return -1;
323         } else if( buildSQL( query->state, query->query )) {
324                 osrfLogWarning( OSRF_LOG_MARK, "Unable to build SQL statement for query id # %d",
325                         query->query->id );
326                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
327                         ctx->request, "Unable to build SQL statement" );
328                 return -1;
329         }
330
331         osrfAppRespondComplete( ctx, jsonNewObject( OSRF_BUFFER_C_STR( query->state->sql )));
332         return 0;
333 }
334
335 int doFinish( osrfMethodContext* ctx ) {
336         if(osrfMethodVerifyContext( ctx )) {
337                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
338                 return -1;
339         }
340
341         // Get the query token.
342         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
343         if( token_obj->type != JSON_STRING ) {
344                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
345                                                           ctx->request, "Invalid parameter; query token must be a string" );
346                 return -1;
347         }
348         const char* token = jsonObjectGetString( token_obj );
349
350         // Delete the corresponding entry from the cache.  If there is no cache, or no such entry,
351         // just ignore the problem and report success.
352         osrfHash* cache = ctx->session->userData;
353         if( cache )
354                 osrfHashRemove( cache, token );
355
356         osrfAppRespondComplete( ctx, NULL );
357         return 0;
358 }
359
360 /**
361         @brief Save a query in session-level userData for reference in future method calls.
362         @param ctx Pointer to the current method context.
363         @param state Pointer to the state of the query.
364         @param query Pointer to the abstract representation of the query.
365         @return Pointer to an identifying token to be returned to the client.
366 */
367 static const char* save_query( 
368         osrfMethodContext* ctx, BuildSQLState* state, StoredQ* query ) {
369
370         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
371         cached_query->state       = state;
372         cached_query->query       = query;
373
374         // Get the cache.  If we don't have one yet, make one.
375         osrfHash* cache = ctx->session->userData;
376         if( !cache ) {
377                 cache = osrfNewHash();
378                 osrfHashSetCallback( cache, free_cached_query );
379                 ctx->session->userData = cache;
380                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
381         }
382
383         // Create a token string to be used as a key
384         static unsigned int token_count = 0;
385         char* token = va_list_to_string(
386                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
387
388         osrfHashSet( cache, cached_query, token );
389         return token;
390 }
391
392 /**
393         @brief Free a CachedQuery
394         @param Pointer to the CachedQuery to be freed.
395 */
396 static void free_cached_query( char* key, void* data ) {
397         if( data ) {
398                 CachedQuery* cached_query = data;
399                 buildSQLStateFree( cached_query->state );
400                 storedQFree( cached_query->query );
401         }
402 }
403
404 /**
405         @brief Callback for freeing session-level userData.
406         @param blob Opaque pointer t userData.
407 */
408 static void userDataFree( void* blob ) {
409         osrfHashFree( (osrfHash*) blob );
410 }
411
412 /**
413         @brief Search for the cached query corresponding to a given token.
414         @param ctx Pointer to the current method context.
415         @param token Token string from a previous call to the prepare method.
416         @return A pointer to the cached query, if found, or NULL if not.
417 */
418 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
419         if( ctx && ctx->session->userData && token ) {
420                 osrfHash* cache = ctx->session->userData;
421                 return osrfHashGet( cache, token );
422         } else
423                 return NULL;
424 }