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