]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/c-apps/oils_qstore.c
1ff05a33e7fcf958914d75f825fdcec80640cb42
[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         StoredQ*    query;
27         jsonObject* bind_map;
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( osrfMethodContext* ctx, StoredQ* query, jsonObject* bind_map );
39 static void free_cached_query( char* key, void* data );
40 static void userDataFree( void* blob );
41 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token );
42
43 /**
44         @brief Disconnect from the database.
45
46         This function is called when the server drone is about to terminate.
47 */
48 void osrfAppChildExit() {
49         osrfLogDebug( OSRF_LOG_MARK, "Child is exiting, disconnecting from database..." );
50
51         if ( dbhandle ) {
52                 dbi_conn_query( dbhandle, "ROLLBACK;" );
53                 dbi_conn_close( dbhandle );
54                 dbhandle = NULL;
55         }
56 }
57
58 /**
59         @brief Initialize the application.
60         @return Zero if successful, or non-zero if not.
61
62         Load the IDL file into an internal data structure for future reference.  Each non-virtual
63         class in the IDL corresponds to a table or view in the database, or to a subquery defined
64         in the IDL.  Ignore all virtual tables and virtual fields.
65
66         Register the functions for remote procedure calls.
67
68         This function is called when the registering the application, and is executed by the
69         listener before spawning the drones.
70 */
71 int osrfAppInitialize() {
72
73         osrfLogInfo( OSRF_LOG_MARK, "Initializing the QStore Server..." );
74         osrfLogInfo( OSRF_LOG_MARK, "Finding XML file..." );
75
76         if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
77                 return 1; /* return non-zero to indicate error */
78
79         growing_buffer* method_name = buffer_init( 64 );
80
81         OSRF_BUFFER_ADD( method_name, modulename );
82         OSRF_BUFFER_ADD( method_name, ".prepare" );
83         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
84                         "doPrepare", "", 1, 0 );
85
86         buffer_reset( method_name );
87         OSRF_BUFFER_ADD( method_name, modulename );
88         OSRF_BUFFER_ADD( method_name, ".bind_param" );
89         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
90                         "doBindParam", "", 2, 0 );
91
92         buffer_reset( method_name );
93         OSRF_BUFFER_ADD( method_name, modulename );
94         OSRF_BUFFER_ADD( method_name, ".execute" );
95         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
96                         "doExecute", "", 1, OSRF_METHOD_STREAMING );
97
98         buffer_reset( method_name );
99         OSRF_BUFFER_ADD( method_name, modulename );
100         OSRF_BUFFER_ADD( method_name, ".sql" );
101         osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
102                         "doSql", "", 1, OSRF_METHOD_STREAMING );
103
104         return 0;
105 }
106
107 /**
108         @brief Initialize a server drone.
109         @return Zero if successful, -1 if not.
110
111         Connect to the database.  For each non-virtual class in the IDL, execute a dummy "SELECT * "
112         query to get the datatype of each column.  Record the datatypes in the loaded IDL.
113
114         This function is called by a server drone shortly after it is spawned by the listener.
115 */
116 int osrfAppChildInit() {
117
118         osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
119         dbi_initialize( NULL );
120         osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
121
122         char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", modulename );
123         char* user   = osrf_settings_host_value( "/apps/%s/app_settings/database/user", modulename );
124         char* host   = osrf_settings_host_value( "/apps/%s/app_settings/database/host", modulename );
125         char* port   = osrf_settings_host_value( "/apps/%s/app_settings/database/port", modulename );
126         char* db     = osrf_settings_host_value( "/apps/%s/app_settings/database/db", modulename );
127         char* pw     = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", modulename );
128
129         osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
130         dbhandle = dbi_conn_new( driver );
131
132         if( !dbhandle ) {
133                 osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
134                 return -1;
135         }
136         osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
137
138         osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database.  host=%s, "
139                         "port=%s, user=%s, db=%s", modulename, host, port, user, db );
140
141         if( host ) dbi_conn_set_option( dbhandle, "host", host );
142         if( port ) dbi_conn_set_option_numeric( dbhandle, "port", atoi( port ) );
143         if( user ) dbi_conn_set_option( dbhandle, "username", user );
144         if( pw )   dbi_conn_set_option( dbhandle, "password", pw );
145         if( db )   dbi_conn_set_option( dbhandle, "dbname", db );
146
147         free( user );
148         free( host );
149         free( port );
150         free( db );
151         free( pw );
152
153         const char* err;
154         if( dbi_conn_connect( dbhandle ) < 0 ) {
155                 sleep( 1 );
156                 if( dbi_conn_connect( dbhandle ) < 0 ) {
157                         dbi_conn_error( dbhandle, &err );
158                         osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s", err );
159                         return -1;
160                 }
161         }
162
163         oilsSetDBConnection( dbhandle );
164         osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", modulename );
165
166         // Add datatypes from database to the fields in the IDL
167         if( oilsExtendIDL() ) {
168                 osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
169                 return -1;
170         }
171         else
172                 return 0;
173 }
174
175 /**
176         @brief Load a specified query from the database query tables.
177         @param ctx Pointer to the current method context.
178         @return Zero if successful, or -1 if not.
179
180         Method parameters:
181         - query id (key of query.stored_query table)
182
183         Returns: a character string serving as a token for future references to the query.
184
185         NB: the method return type is temporary.  Eventually this method will return both a token
186         and a list of bind variables.
187 */
188 int doPrepare( osrfMethodContext* ctx ) {
189         if(osrfMethodVerifyContext( ctx )) {
190                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
191                 return -1;
192         }
193
194         // Get the query id from a method parameter
195         const jsonObject* query_id_obj = jsonObjectGetIndex( ctx->params, 0 );
196         if( query_id_obj->type != JSON_NUMBER ) {
197                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
198                         ctx->request, "Invalid parameter; query id must be a number" );
199                 return -1;
200         }
201
202         int query_id = atoi( jsonObjectGetString( query_id_obj ));
203         if( query_id <= 0 ) {
204                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
205                         ctx->request, "Invalid parameter: query id must be greater than zero" );
206                 return -1;
207         }
208
209         osrfLogInfo( OSRF_LOG_MARK, "Building query for id # %d", query_id );
210
211         // To do: prepare query
212         StoredQ* query = NULL;
213         jsonObject* bind_map = NULL;
214         const char* token = save_query( ctx, query, bind_map );
215
216         osrfLogInfo( OSRF_LOG_MARK, "Token for query id # %d is \"%s\"", query_id, token );
217
218         osrfAppRespondComplete( ctx, jsonNewObject( token ));
219         return 0;
220 }
221
222 int doBindParam( osrfMethodContext* ctx ) {
223         if(osrfMethodVerifyContext( ctx )) {
224                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
225                 return -1;
226         }
227
228         // Get the query token from a method parameter
229         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
230         if( token_obj->type != JSON_STRING ) {
231                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
232                         ctx->request, "Invalid parameter; query token must be a string" );
233                 return -1;
234         }
235         const char* token = jsonObjectGetString( token_obj );
236
237         // Look up the query token in the session-level userData
238         CachedQuery* query = search_token( ctx, token );
239         if( !query ) {
240                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
241                                                           ctx->request, "Invalid query token" );
242                 return -1;
243         }
244
245         osrfLogInfo( OSRF_LOG_MARK, "Binding parameter(s) for token %s", token );
246
247         osrfAppRespondComplete( ctx, jsonNewObject( "build method not yet implemented" ));
248         return 0;
249 }
250
251 int doExecute( osrfMethodContext* ctx ) {
252         if(osrfMethodVerifyContext( ctx )) {
253                 osrfLogError( OSRF_LOG_MARK,  "Invalid method context" );
254                 return -1;
255         }
256
257         // Get the query token
258         const jsonObject* token_obj = jsonObjectGetIndex( ctx->params, 0 );
259         if( token_obj->type != JSON_STRING ) {
260                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
261                         ctx->request, "Invalid parameter; query token must be a string" );
262                 return -1;
263         }
264         const char* token = jsonObjectGetString( token_obj );
265
266         // Look up the query token in the session-level userData
267         CachedQuery* query = search_token( ctx, token );
268         if( !query ) {
269                 osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
270                         ctx->request, "Invalid query token" );
271                 return -1;
272         }
273
274         osrfLogInfo( OSRF_LOG_MARK, "Executing query for token \"%s\"", token );
275
276         osrfAppRespondComplete( ctx, jsonNewObject( "execute method not yet implemented" ));
277         return 0;
278 }
279
280 int doSql( 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, "Returning SQL for token \"%s\"", token );
304
305         osrfAppRespondComplete( ctx, jsonNewObject( "sql method not yet implemented" ));
306         return 0;
307 }
308
309 static const char* save_query( osrfMethodContext* ctx, StoredQ* query, jsonObject* bind_map ) {
310
311         CachedQuery* cached_query = safe_malloc( sizeof( CachedQuery ));
312         cached_query->query       = query;
313         cached_query->bind_map    = bind_map;
314
315         // Get the cache.  If we don't have one yet, make one.
316         osrfHash* cache = ctx->session->userData;
317         if( !cache ) {
318                 cache = osrfNewHash();
319                 osrfHashSetCallback( cache, free_cached_query );
320                 ctx->session->userData = cache;
321                 ctx->session->userDataFree = userDataFree;  // arrange to free it at end of session
322         }
323
324         // Create a token string to be used as a key
325         static unsigned int token_count = 0;
326         char* token = va_list_to_string(
327                 "%u_%ld_%ld", ++token_count, (long) time( NULL ), (long) getpid() );
328
329         osrfHashSet( cache, cached_query, token );
330         return token;
331 }
332
333 /**
334         @brief Free a CachedQuery
335         @param Pointer to the CachedQuery to be freed.
336 */
337 static void free_cached_query( char* key, void* data ) {
338         if( data ) {
339                 CachedQuery* cached_query = data;
340                 //storedQFree( cached_query->query );
341                 if( cached_query->bind_map )
342                         jsonObjectFree( cached_query->bind_map );
343         }
344 }
345
346 /**
347         @brief Callback for freeing session-level userData.
348         @param blob Opaque pointer t userData.
349 */
350 static void userDataFree( void* blob ) {
351         osrfHashFree( (osrfHash*) blob );
352 }
353
354 /**
355         @brief Search for the cached query corresponding to a given token.
356         @param ctx Pointer to the current method context.
357         @param token Token string from a previous call to the prepare method.
358         @return A pointer to the cached query, if found, or NULL if not.
359 */
360 static CachedQuery* search_token( osrfMethodContext* ctx, const char* token ) {
361         if( ctx && ctx->session->userData && token ) {
362                 osrfHash* cache = ctx->session->userData;
363                 return osrfHashGet( cache, token );
364         } else
365                 return NULL;
366 }