--- /dev/null
+#-----------------------------------------------------------
+# Makefile.am for Open-ILS/src/c-apps
+# Author: Kevin Beswick (kevinbeswick00@gmail.com)
+# Process this file with automake to generate Makefile.in
+#-----------------------------------------------------------
+
+AM_CFLAGS = $(DEF_CFLAGS) -DOSRF_LOG_PARAMS -I@top_srcdir@/include/
+AM_LDFLAGS = $(DEF_LDFLAGS) -L$(DBI_LIBS) -lopensrf
+
+bin_PROGRAMS = dump_idl idlval test_json_query
+dump_idl_SOURCES = dump_idl.c
+dump_idl_LDFLAGS = $(AM_LDFLAGS) -loils_idl
+dump_idl_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+idlval_SOURCES = idlval.c oils_sql.c
+idlval_CFLAGS = $(AM_CFLAGS)
+idlval_LDFLAGS = $(AM_LDFLAGS) -loils_idl -loils_utils
+idlval_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+test_json_query_SOURCES = test_json_query.c oils_sql.c
+test_json_query_CFLAGS = $(AM_CFLAGS)
+test_json_query_LDFLAGS = $(AM_LDFLAGS) -loils_idl -loils_utils
+test_json_query_DEPENDENCIES = liboils_idl.la liboils_utils.la
+
+lib_LTLIBRARIES = liboils_idl.la liboils_utils.la oils_cstore.la oils_pcrud.la sharestuff_auth.la
+
+liboils_idl_la_SOURCES = oils_idl-core.c
+liboils_idl_la_LDFLAGS = -version-info 2:0:0
+
+liboils_utils_la_SOURCES = oils_utils.c oils_event.c
+liboils_utils_la_LDFLAGS = -loils_idl -version-info 2:0:0
+
+oils_cstore_la_SOURCES = oils_cstore.c oils_sql.c
+oils_cstore_la_LDFLAGS = $(AM_LDFLAGS) -ldbi -ldbdpgsql -loils_utils -module -version-info 1:0:0
+oils_cstore_la_DEPENDENCIES = liboils_utils.la
+
+oils_pcrud_la_SOURCES = oils_pcrud.c oils_sql.c
+oils_pcrud_la_LDFLAGS = $(AM_LDFLAGS) -ldbi -ldbdpgsql -loils_utils -module -version-info 1:0:0
+oils_pcrud_la_DEPENDENCIES = liboils_utils.la
+
+sharestuff_auth_la_SOURCES = sharestuff_auth.c
+sharestuff_auth_la_LDFLAGS = -module -loils_utils -version-info 1:0:0
+sharestuff_auth_la_DEPENDENCIES = liboils_utils.la
+
+
--- /dev/null
+/**
+ @file buildSQL.c
+ @brief Translate an abstract representation of a query into an SQL statement.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/string_array.h"
+#include "opensrf/osrf_hash.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_sql.h"
+#include "openils/oils_buildq.h"
+
+static void build_Query( BuildSQLState* state, const StoredQ* query );
+static void buildCombo( BuildSQLState* state, const StoredQ* query, const char* type_str );
+static void buildSelect( BuildSQLState* state, const StoredQ* query );
+static void buildFrom( BuildSQLState* state, const FromRelation* core_from );
+static void buildJoin( BuildSQLState* state, const FromRelation* join );
+static void buildSelectList( BuildSQLState* state, const SelectItem* item );
+static void buildGroupBy( BuildSQLState* state, const SelectItem* sel_list );
+static void buildOrderBy( BuildSQLState* state, const OrderItem* ord_list );
+static void buildCase( BuildSQLState* state, const Expression* expr );
+static void buildExpression( BuildSQLState* state, const Expression* expr );
+
+static void buildFunction( BuildSQLState* state, const Expression* exp );
+static int subexp_count( const Expression* expr );
+static void buildTypicalFunction( BuildSQLState* state, const Expression* expr );
+static void buildExtract( BuildSQLState* state, const Expression* expr );
+
+static void buildSeries( BuildSQLState* state, const Expression* subexp_list, const char* op );
+static void buildBindVar( BuildSQLState* state, const BindVar* bind );
+static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj );
+
+static void add_newline( BuildSQLState* state );
+static inline void incr_indent( BuildSQLState* state );
+static inline void decr_indent( BuildSQLState* state );
+
+/**
+ @brief Create a jsonObject representing the current list of bind variables.
+ @param bindvar_list Pointer to the bindvar_list member of a BuildSQLState.
+ @return Pointer to the newly created jsonObject.
+
+ The returned jsonObject is a (possibly empty) JSON_HASH, keyed on the names of the bind
+ variables. The data for each is another level of JSON_HASH with a fixed set of tags:
+ - "label"
+ - "type"
+ - "description"
+ - "default_value" (as a jsonObject)
+ - "actual_value" (as a jsonObject)
+
+ Any non-existent values are represented as JSON_NULLs.
+
+ The calling code is responsible for freeing the returned jsonOjbect by calling
+ jsonObjectFree().
+*/
+jsonObject* oilsBindVarList( osrfHash* bindvar_list ) {
+ jsonObject* list = jsonNewObjectType( JSON_HASH );
+
+ if( bindvar_list && osrfHashGetCount( bindvar_list )) {
+ // Traverse our internal list of bind variables
+ BindVar* bind = NULL;
+ osrfHashIterator* iter = osrfNewHashIterator( bindvar_list );
+ while(( bind = osrfHashIteratorNext( iter ))) {
+ // Create an hash to represent the bind variable
+ jsonObject* bind_obj = jsonNewObjectType( JSON_HASH );
+
+ // Add an entry for each attribute
+ jsonObject* attr = jsonNewObject( bind->label );
+ jsonObjectSetKey( bind_obj, "label", attr );
+
+ const char* type = NULL;
+ switch( bind->type ) {
+ case BIND_STR :
+ type = "string";
+ break;
+ case BIND_NUM :
+ type = "number";
+ break;
+ case BIND_STR_LIST :
+ type = "string_list";
+ break;
+ case BIND_NUM_LIST :
+ type = "number_list";
+ break;
+ default :
+ type = "(invalid)";
+ break;
+ }
+ attr = jsonNewObject( type );
+ jsonObjectSetKey( bind_obj, "type", attr );
+
+ attr = jsonNewObject( bind->description );
+ jsonObjectSetKey( bind_obj, "description", attr );
+
+ if( bind->default_value ) {
+ attr = jsonObjectClone( bind->default_value );
+ jsonObjectSetKey( bind_obj, "default_value", attr );
+ }
+
+ if( bind->actual_value ) {
+ attr = jsonObjectClone( bind->actual_value );
+ jsonObjectSetKey( bind_obj, "actual_value", attr );
+ }
+
+ // Add the bind variable to the list
+ jsonObjectSetKey( list, osrfHashIteratorKey( iter ), bind_obj );
+ }
+ osrfHashIteratorFree( iter );
+ }
+
+ return list;
+}
+
+/**
+ @brief Apply values to bind variables, overriding the defaults, if any.
+ @param state Pointer to the query-building context.
+ @param bindings A JSON_HASH of values.
+ @return 0 if successful, or 1 if not.
+
+ The @a bindings parameter must be a JSON_HASH. The keys are the names of bind variables.
+ The values are the corresponding values for the variables.
+*/
+int oilsApplyBindValues( BuildSQLState* state, const jsonObject* bindings ) {
+ if( !state ) {
+ osrfLogError( OSRF_LOG_MARK, "NULL pointer to state" );
+ return 1;
+ } else if( !bindings ) {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: No pointer to bindings" ));
+ return 1;
+ } else if( bindings->type != JSON_HASH ) {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: bindings parameter is not a JSON_HASH" ));
+ return 1;
+ }
+
+ int rc = 0;
+ jsonObject* value = NULL;
+ jsonIterator* iter = jsonNewIterator( bindings );
+ while(( value = jsonIteratorNext( iter ))) {
+ const char* var_name = iter->key;
+ BindVar* bind = osrfHashGet( state->bindvar_list, var_name );
+ if( bind ) {
+ // Apply or replace the value for the specified variable
+ if( bind->actual_value )
+ jsonObjectFree( bind->actual_value );
+ bind->actual_value = jsonObjectClone( value );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Can't assign value to bind variable \"%s\": no such variable", var_name ));
+ rc = 1;
+ }
+ }
+ jsonIteratorFree( iter );
+
+ return rc;
+}
+
+/**
+ @brief Build an SQL query.
+ @param state Pointer to the query-building context.
+ @param query Pointer to the query to be built.
+ @return Zero if successful, or 1 if not.
+
+ Clear the output buffer, call build_Query() to do the work, and add a closing semicolon.
+*/
+int buildSQL( BuildSQLState* state, const StoredQ* query ) {
+ state->error = 0;
+ buffer_reset( state->sql );
+ state->indent = 0;
+ build_Query( state, query );
+ if( ! state->error ) {
+ // Remove the trailing space, if there is one, and add a semicolon.
+ char c = buffer_chomp( state->sql );
+ if( c != ' ' )
+ buffer_add_char( state->sql, c ); // oops, not a space; put it back
+ buffer_add( state->sql, ";\n" );
+ }
+ return state->error;
+}
+
+/**
+ @brief Build an SQL query, appending it to what has been built so far.
+ @param state Pointer to the query-building context.
+ @param query Pointer to the query to be built.
+
+ Look at the query type and branch to the corresponding routine.
+*/
+static void build_Query( BuildSQLState* state, const StoredQ* query ) {
+ if( buffer_length( state->sql ))
+ add_newline( state );
+
+ switch( query->type ) {
+ case QT_SELECT :
+ buildSelect( state, query );
+ break;
+ case QT_UNION :
+ buildCombo( state, query, "UNION" );
+ break;
+ case QT_INTERSECT :
+ buildCombo( state, query, "INTERSECT" );
+ break;
+ case QT_EXCEPT :
+ buildCombo( state, query, "EXCEPT" );
+ break;
+ default :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: invalid query type %d in query # %d",
+ query->type, query->id ));
+ state->error = 1;
+ break;
+ }
+}
+
+/**
+ @brief Build a UNION, INTERSECT, or EXCEPT query.
+ @param state Pointer to the query-building context.
+ @param query Pointer to the query to be built.
+ @param type_str The query type, as a string.
+*/
+static void buildCombo( BuildSQLState* state, const StoredQ* query, const char* type_str ) {
+
+ QSeq* seq = query->child_list;
+ if( !seq ) {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: No child queries within %s query # %d",
+ type_str, query->id ));
+ state->error = 1;
+ return;
+ }
+
+ // Traverse the list of child queries
+ while( seq ) {
+ build_Query( state, seq->child_query );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build child query # %d within %s query %d",
+ seq->child_query->id, type_str, query->id );
+ return;
+ }
+ seq = seq->next;
+ if( seq ) {
+ add_newline( state );
+ buffer_add( state->sql, type_str );
+ buffer_add_char( state->sql, ' ' );
+ if( query->use_all )
+ buffer_add( state->sql, "ALL " );
+ }
+ }
+
+ return;
+}
+
+/**
+ @brief Build a SELECT statement.
+ @param state Pointer to the query-building context.
+ @param query Pointer to the StoredQ structure that represents the query.
+*/
+static void buildSelect( BuildSQLState* state, const StoredQ* query ) {
+
+ FromRelation* from_clause = query->from_clause;
+ if( !from_clause ) {
+ sqlAddMsg( state, "SELECT has no FROM clause in query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+
+ // Get SELECT list
+ buffer_add( state->sql, "SELECT" );
+ incr_indent( state );
+ buildSelectList( state, query->select_list );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build SELECT list for query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+ decr_indent( state );
+
+ // Build FROM clause, if there is one
+ if( query->from_clause ) {
+ buildFrom( state, query->from_clause );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build FROM clause for query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+ }
+
+ // Build WHERE clause, if there is one
+ if( query->where_clause ) {
+ add_newline( state );
+ buffer_add( state->sql, "WHERE" );
+ incr_indent( state );
+ add_newline( state );
+ buildExpression( state, query->where_clause );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build WHERE clause for query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+ decr_indent( state );
+ }
+
+ // Build GROUP BY clause, if there is one
+ buildGroupBy( state, query->select_list );
+
+ // Build HAVING clause, if there is one
+ if( query->having_clause ) {
+ add_newline( state );
+ buffer_add( state->sql, "HAVING" );
+ incr_indent( state );
+ add_newline( state );
+ buildExpression( state, query->having_clause );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build HAVING clause for query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+ decr_indent( state );
+ }
+
+ // Build ORDER BY clause, if there is one
+ if( query->order_by_list ) {
+ buildOrderBy( state, query->order_by_list );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build ORDER BY clause for query # %d", query->id );
+ state->error = 1;
+ return;
+ }
+ }
+
+ // Build LIMIT clause, if there is one
+ if( query->limit_count ) {
+ add_newline( state );
+ buffer_add( state->sql, "LIMIT " );
+ buildExpression( state, query->limit_count );
+ }
+
+ // Build OFFSET clause, if there is one
+ if( query->offset_count ) {
+ add_newline( state );
+ buffer_add( state->sql, "OFFSET " );
+ buildExpression( state, query->offset_count );
+ }
+
+ state->error = 0;
+}
+
+/**
+ @brief Build a FROM clause.
+ @param Pointer to the query-building context.
+ @param Pointer to the StoredQ query to which the FROM clause belongs.
+*/
+static void buildFrom( BuildSQLState* state, const FromRelation* core_from ) {
+
+ add_newline( state );
+ buffer_add( state->sql, "FROM" );
+ incr_indent( state );
+ add_newline( state );
+
+ switch( core_from->type ) {
+ case FRT_RELATION : {
+ char* relation = core_from->table_name;
+ if( !relation ) {
+ if( !core_from->class_name ) {
+ sqlAddMsg( state, "No relation specified for core relation # %d",
+ core_from->id );
+ state->error = 1;
+ return;
+ }
+
+ // Look up table name, view name, or source_definition in the IDL
+ osrfHash* class_hash = osrfHashGet( oilsIDL(), core_from->class_name );
+ relation = oilsGetRelation( class_hash );
+ }
+
+ // Add table or view
+ buffer_add( state->sql, relation );
+ if( !core_from->table_name )
+ free( relation ); // In this case we strdup'd it, must free it
+ break;
+ }
+ case FRT_SUBQUERY :
+ buffer_add_char( state->sql, '(' );
+ incr_indent( state );
+ build_Query( state, core_from->subquery );
+ decr_indent( state );
+ add_newline( state );
+ buffer_add_char( state->sql, ')' );
+ break;
+ case FRT_FUNCTION :
+ buildFunction( state, core_from->function_call );
+ if ( state->error ) {
+ sqlAddMsg( state,
+ "Unable to include function call # %d in FROM relation # %d",
+ core_from->function_call->id, core_from->id );
+ return;
+ }
+ break;
+ default :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: Invalid type # %d in FROM relation # %d",
+ core_from->type, core_from->id ));
+ state->error = 1;
+ return;
+ }
+
+ // Add a table alias, if possible
+ if( core_from->table_alias ) {
+ buffer_add( state->sql, " AS \"" );
+ buffer_add( state->sql, core_from->table_alias );
+ buffer_add( state->sql, "\" " );
+ }
+ else if( core_from->class_name ) {
+ buffer_add( state->sql, " AS \"" );
+ buffer_add( state->sql, core_from->class_name );
+ buffer_add( state->sql, "\" " );
+ } else
+ buffer_add_char( state->sql, ' ' );
+
+ incr_indent( state );
+ FromRelation* join = core_from->join_list;
+ while( join ) {
+ buildJoin( state, join );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build JOIN clause(s) for relation # %d",
+ core_from->id );
+ break;
+ } else
+ join = join->next;
+ }
+ decr_indent( state );
+ decr_indent( state );
+}
+
+/**
+ @brief Add a JOIN clause.
+ @param state Pointer to the query-building context.
+ @param join Pointer to the FromRelation representing the JOIN to be added.
+*/
+static void buildJoin( BuildSQLState* state, const FromRelation* join ) {
+ add_newline( state );
+ switch( join->join_type ) {
+ case JT_NONE :
+ sqlAddMsg( state, "Non-join relation # %d in JOIN clause", join->id );
+ state->error = 1;
+ return;
+ case JT_INNER :
+ buffer_add( state->sql, "INNER JOIN " );
+ break;
+ case JT_LEFT:
+ buffer_add( state->sql, "LEFT JOIN " );
+ break;
+ case JT_RIGHT:
+ buffer_add( state->sql, "RIGHT JOIN " );
+ break;
+ case JT_FULL:
+ buffer_add( state->sql, "FULL JOIN " );
+ break;
+ default :
+ sqlAddMsg( state, "Unrecognized join type in relation # %d", join->id );
+ state->error = 1;
+ return;
+ }
+
+ switch( join->type ) {
+ case FRT_RELATION :
+ // Sanity check
+ if( !join->table_name || ! *join->table_name ) {
+ sqlAddMsg( state, "No relation designated for relation # %d", join->id );
+ state->error = 1;
+ return;
+ }
+ buffer_add( state->sql, join->table_name );
+ break;
+ case FRT_SUBQUERY :
+ // Sanity check
+ if( !join->subquery ) {
+ sqlAddMsg( state, "Subquery expected, not found for relation # %d", join->id );
+ state->error = 1;
+ return;
+ } else if( !join->table_alias ) {
+ sqlAddMsg( state, "No table alias for subquery in FROM relation # %d",
+ join->id );
+ state->error = 1;
+ return;
+ }
+ buffer_add_char( state->sql, '(' );
+ incr_indent( state );
+ build_Query( state, join->subquery );
+ decr_indent( state );
+ add_newline( state );
+ buffer_add_char( state->sql, ')' );
+ break;
+ case FRT_FUNCTION :
+ if( !join->table_name || ! *join->table_name ) {
+ sqlAddMsg( state, "Joins to functions not yet supported in relation # %d",
+ join->id );
+ state->error = 1;
+ return;
+ }
+ break;
+ }
+
+ const char* effective_alias = join->table_alias;
+ if( !effective_alias )
+ effective_alias = join->class_name;
+
+ if( effective_alias ) {
+ buffer_add( state->sql, " AS \"" );
+ buffer_add( state->sql, effective_alias );
+ buffer_add_char( state->sql, '\"' );
+ }
+
+ if( join->on_clause ) {
+ incr_indent( state );
+ add_newline( state );
+ buffer_add( state->sql, "ON " );
+ buildExpression( state, join->on_clause );
+ decr_indent( state );
+ }
+
+ FromRelation* subjoin = join->join_list;
+ while( subjoin ) {
+ buildJoin( state, subjoin );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build JOIN clause(s) for relation # %d", join->id );
+ break;
+ } else
+ subjoin = subjoin->next;
+ }
+}
+
+/**
+ @brief Build a SELECT list.
+ @param state Pointer to the query-building context.
+ @param item Pointer to the first in a linked list of SELECT items.
+*/
+static void buildSelectList( BuildSQLState* state, const SelectItem* item ) {
+
+ int first = 1;
+ while( item ) {
+ if( !first )
+ buffer_add_char( state->sql, ',' );
+ add_newline( state );
+ buildExpression( state, item->expression );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build an expression for SELECT item # %d", item->id );
+ state->error = 1;
+ break;
+ }
+
+ if( item->column_alias ) {
+ buffer_add( state->sql, " AS \"" );
+ buffer_add( state->sql, item->column_alias );
+ buffer_add_char( state->sql, '\"' );
+ }
+ first = 0;
+ item = item->next;
+ };
+ buffer_add_char( state->sql, ' ' );
+}
+
+/**
+ @brief Add a GROUP BY clause, if there is one, to the current query.
+ @param state Pointer to the query-building context.
+ @param sel_list Pointer to the first node in a linked list of SelectItems
+
+ We reference the GROUP BY items by number, not by repeating the expressions.
+*/
+static void buildGroupBy( BuildSQLState* state, const SelectItem* sel_list ) {
+ int seq = 0; // Sequence number of current SelectItem
+ int first = 1; // Boolean: true for the first GROUPed BY item
+ while( sel_list ) {
+ ++seq;
+
+ if( sel_list->grouped_by ) {
+ if( first ) {
+ add_newline( state );
+ buffer_add( state->sql, "GROUP BY " );
+ first = 0;
+ }
+ else
+ buffer_add( state->sql, ", " );
+
+ buffer_fadd( state->sql, "%d", seq );
+ }
+
+ sel_list = sel_list->next;
+ }
+}
+
+/**
+ @brief Add an ORDER BY clause to the current query.
+ @param state Pointer to the query-building context.
+ @param ord_list Pointer to the first node in a linked list of OrderItems.
+*/
+static void buildOrderBy( BuildSQLState* state, const OrderItem* ord_list ) {
+ add_newline( state );
+ buffer_add( state->sql, "ORDER BY" );
+ incr_indent( state );
+
+ int first = 1; // boolean
+ while( ord_list ) {
+ if( first )
+ first = 0;
+ else
+ buffer_add_char( state->sql, ',' );
+ add_newline( state );
+ buildExpression( state, ord_list->expression );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to add ORDER BY expression # %d", ord_list->id );
+ return;
+ }
+
+ ord_list = ord_list->next;
+ }
+
+ decr_indent( state );
+ return;
+}
+
+/**
+ @brief Build an arbitrary expression.
+ @param state Pointer to the query-building context.
+ @param expr Pointer to the Expression representing the expression to be built.
+*/
+static void buildExpression( BuildSQLState* state, const Expression* expr ) {
+ if( !expr ) {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: NULL pointer to Expression" ));
+ state->error = 1;
+ return;
+ }
+
+ if( expr->parenthesize )
+ buffer_add_char( state->sql, '(' );
+
+ switch( expr->type ) {
+ case EXP_BETWEEN :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ buildExpression( state, expr->left_operand );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit left operand in BETWEEN expression # %d",
+ expr->id );
+ break;
+ }
+
+ buffer_add( state->sql, " BETWEEN " );
+
+ buildExpression( state, expr->subexp_list );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit lower limit in BETWEEN expression # %d",
+ expr->id );
+ break;
+ }
+
+ buffer_add( state->sql, " AND " );
+
+ buildExpression( state, expr->subexp_list->next );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit upper limit in BETWEEN expression # %d",
+ expr->id );
+ break;
+ }
+
+ break;
+ case EXP_BIND :
+ if( !expr->bind ) { // Sanity check
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: no variable for bind variable expression" ));
+ state->error = 1;
+ } else
+ buildBindVar( state, expr->bind );
+ break;
+ case EXP_BOOL :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ if( expr->literal ) {
+ buffer_add( state->sql, expr->literal );
+ buffer_add_char( state->sql, ' ' );
+ } else
+ buffer_add( state->sql, "FALSE " );
+ break;
+ case EXP_CASE :
+ buildCase( state, expr );
+ if( state->error )
+ sqlAddMsg( state, "Unable to build CASE expression # %d", expr->id );
+
+ break;
+ case EXP_CAST : // Type cast
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ buffer_add( state->sql, "CAST (" );
+ buildExpression( state, expr->left_operand );
+ if( state->error )
+ sqlAddMsg( state, "Unable to build left operand for CAST expression # %d",
+ expr->id );
+ else {
+ buffer_add( state->sql, " AS " );
+ if( expr->cast_type && expr->cast_type->datatype_name ) {
+ buffer_add( state->sql, expr->cast_type->datatype_name );
+ buffer_add_char( state->sql, ')' );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "No datatype available for CAST expression # %d", expr->id ));
+ state->error = 1;
+ }
+ }
+ break;
+ case EXP_COLUMN : // Table column
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ if( expr->table_alias ) {
+ buffer_add_char( state->sql, '\"' );
+ buffer_add( state->sql, expr->table_alias );
+ buffer_add( state->sql, "\"." );
+ }
+ if( expr->column_name ) {
+ buffer_add( state->sql, expr->column_name );
+ } else {
+ buffer_add_char( state->sql, '*' );
+ }
+ break;
+ case EXP_EXIST :
+ if( !expr->subquery ) {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "No subquery found for EXIST expression # %d", expr->id ));
+ state->error = 1;
+ } else {
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ buffer_add( state->sql, "EXISTS (" );
+ incr_indent( state );
+ build_Query( state, expr->subquery );
+ decr_indent( state );
+ add_newline( state );
+ buffer_add_char( state->sql, ')' );
+ }
+ break;
+ case EXP_FUNCTION :
+ buildFunction( state, expr );
+ break;
+ case EXP_IN :
+ if( expr->left_operand ) {
+ buildExpression( state, expr->left_operand );
+ if( !state->error ) {
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+ buffer_add( state->sql, " IN (" );
+
+ if( expr->subquery ) {
+ incr_indent( state );
+ build_Query( state, expr->subquery );
+ if( state->error )
+ sqlAddMsg( state, "Unable to build subquery for IN condition" );
+ else {
+ decr_indent( state );
+ add_newline( state );
+ buffer_add_char( state->sql, ')' );
+ }
+ } else {
+ buildSeries( state, expr->subexp_list, NULL );
+ if( state->error )
+ sqlAddMsg( state, "Unable to build IN list" );
+ else
+ buffer_add_char( state->sql, ')' );
+ }
+ }
+ }
+ break;
+ case EXP_ISNULL :
+ if( expr->left_operand ) {
+ buildExpression( state, expr->left_operand );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit left operand in IS NULL expression # %d",
+ expr->id );
+ break;
+ }
+ }
+
+ if( expr->negate )
+ buffer_add( state->sql, " IS NOT NULL" );
+ else
+ buffer_add( state->sql, " IS NULL" );
+ break;
+ case EXP_NULL :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ buffer_add( state->sql, "NULL" );
+ break;
+ case EXP_NUMBER : // Numeric literal
+ if( !expr->literal ) {
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: No numeric value in string expression # %d", expr->id ));
+ state->error = 1;
+ } else {
+ buffer_add( state->sql, expr->literal );
+ }
+ break;
+ case EXP_OPERATOR :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT (" );
+
+ if( expr->left_operand ) {
+ buildExpression( state, expr->left_operand );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit left operand in expression # %d",
+ expr->id );
+ break;
+ }
+ }
+ buffer_add_char( state->sql, ' ' );
+ buffer_add( state->sql, expr->op );
+ buffer_add_char( state->sql, ' ' );
+ if( expr->right_operand ) {
+ buildExpression( state, expr->right_operand );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to emit right operand in expression # %d",
+ expr->id );
+ break;
+ }
+ }
+
+ if( expr->negate )
+ buffer_add_char( state->sql, ')' );
+
+ break;
+ case EXP_SERIES :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT (" );
+
+ buildSeries( state, expr->subexp_list, expr->op );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build series expression using operator \"%s\"",
+ expr->op ? expr->op : "," );
+ }
+ if( expr->negate )
+ buffer_add_char( state->sql, ')' );
+
+ break;
+ case EXP_STRING : // String literal
+ if( !expr->literal ) {
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: No string value in string expression # %d", expr->id ));
+ state->error = 1;
+ } else {
+ char* str = strdup( expr->literal );
+ dbi_conn_quote_string( state->dbhandle, &str );
+ if( str ) {
+ buffer_add( state->sql, str );
+ free( str );
+ } else {
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Unable to format string literal \"%s\" for expression # %d",
+ expr->literal, expr->id ));
+ state->error = 1;
+ }
+ }
+ break;
+ case EXP_SUBQUERY :
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ if( expr->subquery ) {
+ buffer_add_char( state->sql, '(' );
+ incr_indent( state );
+ build_Query( state, expr->subquery );
+ decr_indent( state );
+ add_newline( state );
+ buffer_add_char( state->sql, ')' );
+ } else {
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: No subquery in subquery expression # %d", expr->id ));
+ state->error = 1;
+ }
+ break;
+ }
+
+ if( expr->parenthesize )
+ buffer_add_char( state->sql, ')' );
+}
+
+/**
+ @brief Build a CASE expression.
+ @param state Pointer to the query-building context.
+ @param exp Pointer to an Expression representing a CASE expression.
+*/
+static void buildCase( BuildSQLState* state, const Expression* expr ) {
+ // Sanity checks
+ if( ! expr->left_operand ) {
+ sqlAddMsg( state, "CASE expression # %d has no left operand", expr->id );
+ state->error = 1;
+ return;
+ } else if( ! expr->branch_list ) {
+ sqlAddMsg( state, "CASE expression # %d has no branches", expr->id );
+ state->error = 1;
+ return;
+ }
+
+ if( expr->negate )
+ buffer_add( state->sql, "NOT (" );
+
+ // left_operand is the expression on which we shall branch
+ buffer_add( state->sql, "CASE " );
+ buildExpression( state, expr->left_operand );
+ if( state->error ) {
+ sqlAddMsg( state, "Unable to build operand of CASE expression # %d", expr->id );
+ return;
+ }
+
+ incr_indent( state );
+
+ // Emit each branch in turn
+ CaseBranch* branch = expr->branch_list;
+ while( branch ) {
+ add_newline( state );
+
+ if( branch->condition ) {
+ // Emit a WHEN condition
+ buffer_add( state->sql, "WHEN " );
+ buildExpression( state, branch->condition );
+ incr_indent( state );
+ add_newline( state );
+ buffer_add( state->sql, "THEN " );
+ } else {
+ // Emit ELSE
+ buffer_add( state->sql, "ELSE " );
+ incr_indent( state );
+ add_newline( state );
+ }
+
+ // Emit the THEN expression
+ buildExpression( state, branch->result );
+ decr_indent( state );
+
+ branch = branch->next;
+ }
+
+ decr_indent( state );
+ add_newline( state );
+ buffer_add( state->sql, "END" );
+
+ if( expr->negate )
+ buffer_add( state->sql, ")" );
+}
+
+/**
+ @brief Build a function call, with a subfield if specified.
+ @param state Pointer to the query-building context.
+ @param exp Pointer to an Expression representing a function call.
+*/
+static void buildFunction( BuildSQLState* state, const Expression* expr ) {
+ if( expr->negate )
+ buffer_add( state->sql, "NOT " );
+
+ // If a subfield is specified, the function call
+ // needs an extra layer of parentheses
+ if( expr->column_name )
+ buffer_add_char( state->sql, '(' );
+
+ // First, check for some specific functions with peculiar syntax, and treat them
+ // as special exceptions. We rely on the input side to ensure that the function
+ // name is available.
+ if( !strcasecmp( expr->function_name, "EXTRACT" ))
+ buildExtract( state, expr );
+ else if( !strcasecmp( expr->function_name, "CURRENT_DATE" ) && ! expr->subexp_list )
+ buffer_add( state->sql, "CURRENT_DATE " );
+ else if( !strcasecmp( expr->function_name, "CURRENT_TIME" ) && ! expr->subexp_list )
+ buffer_add( state->sql, "CURRENT_TIME " );
+ else if( !strcasecmp( expr->function_name, "CURRENT_TIMESTAMP" ) && ! expr->subexp_list )
+ buffer_add( state->sql, "CURRENT_TIMESTAMP " );
+ else if( !strcasecmp( expr->function_name, "LOCALTIME" ) && ! expr->subexp_list )
+ buffer_add( state->sql, "LOCALTIME " );
+ else if( !strcasecmp( expr->function_name, "LOCALTIMESTAMP" ) && ! expr->subexp_list )
+ buffer_add( state->sql, "LOCALTIMESTAMP " );
+ else if( !strcasecmp( expr->function_name, "TRIM" )) {
+ int arg_count = subexp_count( expr );
+
+ if( (arg_count != 2 && arg_count != 3 ) || expr->subexp_list->type != EXP_STRING )
+ buildTypicalFunction( state, expr );
+ else {
+ sqlAddMsg( state,
+ "TRIM function not supported in expr # %d; use ltrim() and/or rtrim()",
+ expr->id );
+ state->error = 1;
+ return;
+ }
+ } else
+ buildTypicalFunction( state, expr ); // Not a special exception.
+
+ if( expr->column_name ) {
+ // Add the name of the subfield
+ buffer_add( state->sql, ").\"" );
+ buffer_add( state->sql, expr->column_name );
+ buffer_add_char( state->sql, '\"' );
+ }
+}
+
+/**
+ @brief Count the number of subexpressions attached to a given Expression.
+ @param expr Pointer to the Expression whose subexpressions are to be counted.
+ @return The number of subexpressions.
+*/
+static int subexp_count( const Expression* expr ) {
+ if( !expr )
+ return 0;
+
+ int count = 0;
+ const Expression* sub = expr->subexp_list;
+ while( sub ) {
+ ++count;
+ sub = sub->next;
+ }
+ return count;
+}
+
+/**
+ @brief Build an ordinary function call, i.e. one with no special syntax,
+ @param state Pointer to the query-building context.
+ @param exp Pointer to an Expression representing a function call.
+
+ Emit the parameters as a comma-separated list of expressions.
+*/
+static void buildTypicalFunction( BuildSQLState* state, const Expression* expr ) {
+ buffer_add( state->sql, expr->function_name );
+ buffer_add_char( state->sql, '(' );
+
+ // Add the parameters, if any
+ buildSeries( state, expr->subexp_list, NULL );
+
+ buffer_add_char( state->sql, ')' );
+}
+
+/**
+ @brief Build a call to the EXTRACT function, with its peculiar syntax.
+ @param state Pointer to the query-building context.
+ @param exp Pointer to an Expression representing an EXTRACT call.
+
+ If there are not exactly two parameters, or if the first parameter is not a string,
+ then assume it is an ordinary function overloading on the same name. We don't try to
+ check the type of the second parameter. Hence it is possible for a legitimately
+ overloaded function to be uncallable.
+
+ The first parameter of EXTRACT() must be one of a short list of names for some fragment
+ of a date or time. Here we accept that parameter in the form of a string. We don't
+ surround it with quotes in the output, although PostgreSQL wouldn't mind if we did.
+*/
+static void buildExtract( BuildSQLState* state, const Expression* expr ) {
+
+ const Expression* arg = expr->subexp_list;
+
+ // See if this is the special form of EXTRACT(), so far as we can tell
+ if( subexp_count( expr ) != 2 || arg->type != EXP_STRING ) {
+ buildTypicalFunction( state, expr );
+ return;
+ } else {
+ // check the first argument against a list of valid values
+ if( strcasecmp( arg->literal, "century" )
+ && strcasecmp( arg->literal, "day" )
+ && strcasecmp( arg->literal, "decade" )
+ && strcasecmp( arg->literal, "dow" )
+ && strcasecmp( arg->literal, "doy" )
+ && strcasecmp( arg->literal, "epoch" )
+ && strcasecmp( arg->literal, "hour" )
+ && strcasecmp( arg->literal, "isodow" )
+ && strcasecmp( arg->literal, "isoyear" )
+ && strcasecmp( arg->literal, "microseconds" )
+ && strcasecmp( arg->literal, "millennium" )
+ && strcasecmp( arg->literal, "milliseconds" )
+ && strcasecmp( arg->literal, "minute" )
+ && strcasecmp( arg->literal, "month" )
+ && strcasecmp( arg->literal, "quarter" )
+ && strcasecmp( arg->literal, "second" )
+ && strcasecmp( arg->literal, "timezone" )
+ && strcasecmp( arg->literal, "timezone_hour" )
+ && strcasecmp( arg->literal, "timezone_minute" )
+ && strcasecmp( arg->literal, "week" )
+ && strcasecmp( arg->literal, "year" )) {
+ // This *could* be an ordinary function, overloading on the name. However it's
+ // more likely that the user misspelled one of the names expected by EXTRACT().
+ sqlAddMsg( state,
+ "Invalid name \"%s\" as EXTRACT argument in expression # %d",
+ expr->literal, expr->id );
+ state->error = 1;
+ }
+ }
+
+ buffer_add( state->sql, "EXTRACT(" );
+ buffer_add( state->sql, arg->literal );
+ buffer_add( state->sql, " FROM " );
+
+ arg = arg->next;
+ if( !arg ) {
+ sqlAddMsg( state,
+ "Only one argument supplied to EXTRACT function in expression # %d", expr->id );
+ state->error = 1;
+ return;
+ }
+
+ // The second parameter must be of type timestamp, time, or interval. We don't have
+ // a good way of checking it here, so we rely on PostgreSQL to complain if necessary.
+ buildExpression( state, arg );
+ buffer_add_char( state->sql, ')' );
+}
+
+/**
+ @brief Build a series of expressions separated by a specified operator, or by commas.
+ @param state Pointer to the query-building context.
+ @param subexp_list Pointer to the first Expression in a linked list.
+ @param op Pointer to the operator, or NULL for commas.
+
+ If the operator is AND or OR (in upper, lower, or mixed case), the second and all
+ subsequent operators will begin on a new line.
+*/
+static void buildSeries( BuildSQLState* state, const Expression* subexp_list, const char* op ) {
+
+ if( !subexp_list)
+ return; // List is empty
+
+ int comma = 0; // Boolean; true if separator is a comma
+ int newline_needed = 0; // Boolean; true if operator is AND or OR
+
+ if( !op ) {
+ op = ",";
+ comma = 1;
+ } else if( !strcmp( op, "," ))
+ comma = 1;
+ else if( !strcasecmp( op, "AND" ) || !strcasecmp( op, "OR" ))
+ newline_needed = 1;
+
+ int first = 1; // Boolean; true for first item in list
+ while( subexp_list ) {
+ if( first )
+ first = 0; // No separator needed yet
+ else {
+ // Insert a separator
+ if( comma )
+ buffer_add( state->sql, ", " );
+ else {
+ if( newline_needed )
+ add_newline( state );
+ else
+ buffer_add_char( state->sql, ' ' );
+
+ buffer_add( state->sql, op );
+ buffer_add_char( state->sql, ' ' );
+ }
+ }
+
+ buildExpression( state, subexp_list );
+ subexp_list = subexp_list->next;
+ }
+}
+
+/**
+ @brief Add the value of a bind variable to an SQL statement.
+ @param state Pointer to the query-building context.
+ @param bind Pointer to the bind variable whose value is to be added to the SQL.
+
+ The value may be a null, a scalar, or an array of nulls and/or scalars, depending on
+ the type of the bind variable.
+*/
+static void buildBindVar( BuildSQLState* state, const BindVar* bind ) {
+
+ // Decide where to get the value, if any
+ const jsonObject* value = NULL;
+ if( bind->actual_value )
+ value = bind->actual_value;
+ else if( bind->default_value ) {
+ if( state->defaults_usable )
+ value = bind->default_value;
+ else {
+ sqlAddMsg( state, "No confirmed value available for bind variable \"%s\"",
+ bind->name );
+ state->error = 1;
+ return;
+ }
+ } else if( state->values_required ) {
+ sqlAddMsg( state, "No value available for bind variable \"%s\"", bind->name );
+ state->error = 1;
+ return;
+ } else {
+ // No value available, and that's okay. Emit the name of the bind variable.
+ buffer_add_char( state->sql, ':' );
+ buffer_add( state->sql, bind->name );
+ return;
+ }
+
+ // If we get to this point, we know that a value is available. Carry on.
+
+ int numeric = 0; // Boolean
+ if( BIND_NUM == bind->type || BIND_NUM_LIST == bind->type )
+ numeric = 1;
+
+ // Emit the value
+ switch( bind->type ) {
+ case BIND_STR :
+ case BIND_NUM :
+ buildScalar( state, numeric, value );
+ break;
+ case BIND_STR_LIST :
+ case BIND_NUM_LIST :
+ if( JSON_ARRAY == value->type ) {
+ // Iterate over array, emit each value
+ int first = 1; // Boolean
+ unsigned long max = value->size;
+ unsigned long i = 0;
+ while( i < max ) {
+ if( first )
+ first = 0;
+ else
+ buffer_add( state->sql, ", " );
+
+ buildScalar( state, numeric, jsonObjectGetIndex( value, i ));
+ ++i;
+ }
+ } else {
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Invalid value for bind variable; expected a list of values" ));
+ state->error = 1;
+ }
+ break;
+ default :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: invalid type for bind variable" ));
+ state->error = 1;
+ break;
+ }
+
+ if( state->error )
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Unable to emit value of bind variable \"%s\"", bind->name ));
+}
+
+/**
+ @brief Add a number or quoted string to an SQL statement.
+ @param state Pointer to the query-building context.
+ @param numeric Boolean; true if the value is expected to be a number
+ @param obj Pointer to the jsonObject whose value is to be added to the SQL.
+*/
+static void buildScalar( BuildSQLState* state, int numeric, const jsonObject* obj ) {
+ switch( obj->type ) {
+ case JSON_HASH :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: hash value for bind variable" ));
+ state->error = 1;
+ break;
+ case JSON_ARRAY :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: array value for bind variable" ));
+ state->error = 1;
+ break;
+ case JSON_STRING :
+ if( numeric ) {
+ sqlAddMsg( state,
+ "Invalid value for bind variable: expected a string, found a number" );
+ state->error = 1;
+ } else {
+ char* str = jsonObjectToSimpleString( obj );
+ dbi_conn_quote_string( state->dbhandle, &str );
+ if( str ) {
+ buffer_add( state->sql, str );
+ free( str );
+ } else {
+ osrfLogWarning( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Unable to format string literal \"%s\" for bind variable",
+ jsonObjectGetString( obj )));
+ state->error = 1;
+ }
+ }
+ break;
+ case JSON_NUMBER :
+ if( numeric ) {
+ buffer_add( state->sql, jsonObjectGetString( obj ));
+ } else {
+ sqlAddMsg( state,
+ "Invalid value for bind variable: expected a number, found a string" );
+ state->error = 1;
+ }
+ break;
+ case JSON_NULL :
+ buffer_add( state->sql, "NULL" );
+ break;
+ case JSON_BOOL :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: boolean value for bind variable" ));
+ state->error = 1;
+ break;
+ default :
+ osrfLogError( OSRF_LOG_MARK, sqlAddMsg( state,
+ "Internal error: corrupted value for bind variable" ));
+ state->error = 1;
+ break;
+ }
+}
+
+/**
+ @brief Start a new line in the output, with the current level of indentation.
+ @param state Pointer to the query-building context.
+*/
+static void add_newline( BuildSQLState* state ) {
+ buffer_add_char( state->sql, '\n' );
+
+ // Add indentation
+ static const char blanks[] = " "; // 32 blanks
+ static const size_t maxlen = sizeof( blanks ) - 1;
+ const int blanks_per_level = 3;
+ int n = state->indent * blanks_per_level;
+ while( n > 0 ) {
+ size_t len = n >= maxlen ? maxlen : n;
+ buffer_add_n( state->sql, blanks, len );
+ n -= len;
+ }
+}
+
+/**
+ @brief Increase the degree of indentation.
+ @param state Pointer to the query-building context.
+*/
+static inline void incr_indent( BuildSQLState* state ) {
+ ++state->indent;
+}
+
+/**
+ @brief Reduce the degree of indentation.
+ @param state Pointer to the query-building context.
+*/
+static inline void decr_indent( BuildSQLState* state ) {
+ if( state->indent )
+ --state->indent;
+}
--- /dev/null
+/*
+* C Implementation: dump_idl
+*
+* Description:
+*
+*
+* Author: Scott McKellar <scott@esilibrary.com>, (C) 2009
+*
+* Copyright: See COPYING file that comes with this distribution
+*
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <opensrf/string_array.h>
+#include <opensrf/osrf_hash.h>
+#include <openils/oils_idl.h>
+
+static void dump_idl( osrfHash* IDLHash );
+static void dump_class( osrfHash* class_hash, const char* class_name );
+static void dump_fields( osrfHash* field_hash );
+static void dump_one_field( osrfHash* field_hash, const char* field_name );
+static void dump_links( osrfHash* links_hash );
+static void dump_one_link( osrfHash* link_hash, const char* link_name );
+static void dump_permacrud( osrfHash* pcrud_hash );
+static void dump_action( osrfHash* action_hash, const char* action_name );
+static void dump_foreign_context( osrfHash* fc_hash );
+static void dump_fc_class( osrfHash* fc_class_hash, const char* class_name );
+static void dump_string_array(
+ osrfStringArray* sarr, const char* name, const char* indent );
+
+int main( int argc, char* argv[] ) {
+ int rc = 0;
+
+ // Suppress informational messages
+ osrfLogSetLevel( OSRF_LOG_WARNING );
+
+ // Get name of IDL file, if specified on command line
+ const char* IDL_filename = NULL;
+ int filename_expected = 0; // boolean
+ int i;
+ for( i = 1; i < argc; ++i ) {
+ const char* arg = argv[ i ];
+ printf( "%s\n", arg );
+ if( filename_expected ) {
+ IDL_filename = arg;
+ filename_expected = 0;
+ } else {
+ if( '-' == arg[ 0 ] && 'f' == arg[1] ) {
+ if( IDL_filename ) {
+ fprintf( stderr, "Only one IDL file may be specified\n" );
+ return 1;
+ } else {
+ if( arg[ 2 ] )
+ IDL_filename = arg + 2;
+ else
+ filename_expected = 1;
+ }
+ }
+ else
+ break;
+ }
+ }
+
+ if( filename_expected ) {
+ fprintf( stderr, "IDL filename expected on command line, not found\n" );
+ return 1;
+ }
+
+ // No filename? Look in the environment
+ if( !IDL_filename )
+ IDL_filename = getenv( "OILS_IDL_FILENAME" );
+
+ // Still no filename? Apply a default
+ if( !IDL_filename )
+ IDL_filename = "/openils/conf/fm_IDL.xml";
+
+ printf( "IDL filename: %s\n", IDL_filename );
+
+ osrfHash* IDL = oilsIDLInit( IDL_filename );
+ if( NULL == IDL ) {
+ fputs( "Failed to build IDL\n", stderr );
+ rc = 1;
+ }
+
+ if( i >= argc )
+ // No classes specified? Dump them all
+ dump_idl( IDL );
+ else do {
+ // Dump the requested classes
+ dump_class( osrfHashGet( IDL, argv[ i ] ), argv[ i ] );
+ ++i;
+ } while( i < argc );
+
+ return rc;
+}
+
+static void dump_idl( osrfHash* IDLHash ) {
+ if( NULL == IDLHash )
+ return;
+
+ if( 0 == osrfHashGetCount( IDLHash ) )
+ return;
+
+ osrfHashIterator* iter = osrfNewHashIterator( IDLHash );
+ osrfHash* class_hash = NULL;
+
+ // Dump each class
+ for( ;; ) {
+ class_hash = osrfHashIteratorNext( iter );
+ if( class_hash )
+ dump_class( class_hash, osrfHashIteratorKey( iter ) );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_class( osrfHash* class_hash, const char* class_name )
+{
+ if( !class_hash || !class_name )
+ return;
+
+ if( 0 == osrfHashGetCount( class_hash ) )
+ return;
+
+ printf( "Class %s\n", class_name );
+ const char* indent = " ";
+
+ osrfHashIterator* iter = osrfNewHashIterator( class_hash );
+
+ // Dump each attribute, etc. of the class hash
+ for( ;; ) {
+ void* class_attr = osrfHashIteratorNext( iter );
+ if( class_attr ) {
+ const char* attr_name = osrfHashIteratorKey( iter );
+ if( !strcmp( attr_name, "classname" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "fieldmapper" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "tablename" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "restrict_primary" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "virtual" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "controller" ) )
+ dump_string_array( (osrfStringArray*) class_attr, attr_name, indent );
+ else if( !strcmp( attr_name, "fields" ) )
+ dump_fields( (osrfHash*) class_attr );
+ else if( !strcmp( attr_name, "links" ) )
+ dump_links( (osrfHash*) class_attr );
+ else if( !strcmp( attr_name, "primarykey" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "sequence" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) class_attr );
+ else if( !strcmp( attr_name, "permacrud" ) )
+ dump_permacrud( (osrfHash*) class_attr );
+ else if( !strcmp( attr_name, "source_definition" ) )
+ printf( "%s%s:\n%s\n", indent, attr_name, (char*) class_attr );
+ else
+ printf( "%s%s (unknown)\n", indent, attr_name );
+ } else
+ break;
+ }
+}
+
+static void dump_fields( osrfHash* fields_hash ) {
+ if( NULL == fields_hash )
+ return;
+
+ if( 0 == osrfHashGetCount( fields_hash ) )
+ return;
+
+ fputs( " fields\n", stdout );
+
+ osrfHashIterator* iter = osrfNewHashIterator( fields_hash );
+ osrfHash* fields_attr = NULL;
+
+ // Dump each field
+ for( ;; ) {
+ fields_attr = osrfHashIteratorNext( iter );
+ if( fields_attr )
+ dump_one_field( fields_attr, osrfHashIteratorKey( iter ) );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_one_field( osrfHash* field_hash, const char* field_name ) {
+ if( !field_hash || !field_name )
+ return;
+
+ if( 0 == osrfHashGetCount( field_hash ) )
+ return;
+
+ printf( " %s\n", field_name );
+
+ osrfHashIterator* iter = osrfNewHashIterator( field_hash );
+ const char* field_attr = NULL;
+ const char* indent = " ";
+
+ // Dump each field attribute
+ for( ;; ) {
+ field_attr = osrfHashIteratorNext( iter );
+ if( field_attr )
+ printf( "%s%s: %s\n", indent, osrfHashIteratorKey( iter ), field_attr );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_links( osrfHash* links_hash ) {
+ if( NULL == links_hash )
+ return;
+
+ if( 0 == osrfHashGetCount( links_hash ) )
+ return;
+
+ fputs( " links\n", stdout );
+
+ osrfHashIterator* iter = osrfNewHashIterator( links_hash );
+ osrfHash* links_attr = NULL;
+
+ // Dump each link
+ for( ;; ) {
+ links_attr = osrfHashIteratorNext( iter );
+ if( links_attr )
+ dump_one_link( links_attr, osrfHashIteratorKey( iter ) );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_one_link( osrfHash* link_hash, const char* link_name ) {
+ if( !link_hash || !link_name )
+ return;
+
+ if( 0 == osrfHashGetCount( link_hash ) )
+ return;
+
+ printf( " %s\n", link_name );
+
+ osrfHashIterator* iter = osrfNewHashIterator( link_hash );
+ const void* link_attr = NULL;
+ const char* indent = " ";
+
+ // Dump each link attribute
+ for( ;; ) {
+ link_attr = osrfHashIteratorNext( iter );
+ if( link_attr ) {
+ const char* link_attr_name = osrfHashIteratorKey( iter );
+ if( !strcmp( link_attr_name, "reltype" ) )
+ printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+ else if( !strcmp( link_attr_name, "key" ) )
+ printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+ else if( !strcmp( link_attr_name, "class" ) )
+ printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+ else if( !strcmp( link_attr_name, "map" ) )
+ dump_string_array( (osrfStringArray*) link_attr, link_attr_name, indent );
+ else if( !strcmp( link_attr_name, "field" ) )
+ printf( "%s%s: %s\n", indent, link_attr_name, (char*) link_attr );
+ else
+ printf( "%s%s (unknown)\n", indent, link_attr_name );
+ } else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_permacrud( osrfHash* pcrud_hash ) {
+ if( NULL == pcrud_hash )
+ return;
+
+ if( 0 == osrfHashGetCount( pcrud_hash ) )
+ return;
+
+ fputs( " permacrud\n", stdout );
+
+ osrfHashIterator* iter = osrfNewHashIterator( pcrud_hash );
+ osrfHash* pcrud_attr = NULL;
+
+ // Dump each action
+ for( ;; ) {
+ pcrud_attr = osrfHashIteratorNext( iter );
+ if( pcrud_attr )
+ dump_action( pcrud_attr, osrfHashIteratorKey( iter ) );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_action( osrfHash* action_hash, const char* action_name ) {
+ if( !action_hash || !action_name )
+ return;
+
+ if( 0 == osrfHashGetCount( action_hash ) )
+ return;
+
+ printf( " %s\n", action_name );
+
+ osrfHashIterator* iter = osrfNewHashIterator( action_hash );
+ void* action_attr = NULL;
+ const char* indent = " ";
+
+ // Dump each attribute of the action
+ for( ;; ) {
+ action_attr = osrfHashIteratorNext( iter );
+ if( action_attr ) {
+ const char* attr_name = osrfHashIteratorKey( iter );
+ if( !strcmp( attr_name, "permission" ) )
+ dump_string_array( action_attr, attr_name, indent );
+ else if( !strcmp( attr_name, "global_required" ) )
+ printf( "%s%s: %s\n", indent, attr_name, (char*) action_attr );
+ else if( !strcmp( attr_name, "local_context" ) )
+ dump_string_array( action_attr, attr_name, indent );
+ else if( !strcmp( attr_name, "foreign_context" ) )
+ dump_foreign_context( action_attr );
+ else
+ printf( "%s%s (unknown)\n", indent, attr_name );
+ } else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_foreign_context( osrfHash* fc_hash ) {
+ if( !fc_hash )
+ return;
+
+ if( 0 == osrfHashGetCount( fc_hash ) )
+ return;
+
+ fputs( " foreign_context\n", stdout );
+
+ osrfHashIterator* iter = osrfNewHashIterator( fc_hash );
+ osrfHash* fc_attr = NULL;
+
+ // Dump each foreign context attribute
+ for( ;; ) {
+ fc_attr = osrfHashIteratorNext( iter );
+ if( fc_attr )
+ dump_fc_class( (osrfHash*) fc_attr, osrfHashIteratorKey( iter ) );
+ else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_fc_class( osrfHash* fc_class_hash, const char* class_name )
+{
+ if( ! fc_class_hash )
+ return;
+
+ if( 0 == osrfHashGetCount( fc_class_hash ) )
+ return;
+
+ printf( " %s\n", class_name );
+
+ osrfHashIterator* iter = osrfNewHashIterator( fc_class_hash );
+ void* fc_class_attr = NULL;
+ const char* indent = " ";
+
+ // Dump each foreign context attribute
+ for( ;; ) {
+ fc_class_attr = osrfHashIteratorNext( iter );
+ if( fc_class_attr ) {
+ const char* fc_class_attr_name = osrfHashIteratorKey( iter );
+ if( !strcmp( fc_class_attr_name, "field" ) )
+ printf( "%s%s: %s\n", indent, fc_class_attr_name, (const char*) fc_class_attr );
+ else if( !strcmp( fc_class_attr_name, "fkey" ) )
+ printf( "%s%s: %s\n", indent, fc_class_attr_name, (const char*) fc_class_attr );
+ else if( !strcmp( fc_class_attr_name, "jump" ) )
+ dump_string_array( (osrfStringArray*) fc_class_attr, fc_class_attr_name, indent );
+ else if( !strcmp( fc_class_attr_name, "context" ) )
+ dump_string_array( (osrfStringArray*) fc_class_attr, fc_class_attr_name, indent );
+ else
+ printf( "%s%s\n", indent, fc_class_attr_name );
+ } else
+ break;
+ }
+
+ osrfHashIteratorFree( iter );
+}
+
+static void dump_string_array(
+ osrfStringArray* sarr, const char* name, const char* indent ) {
+ if( !sarr || !name || !indent )
+ return;
+
+ int size = sarr->size;
+
+ // Ignore an empty array
+ if( 0 == size )
+ return;
+
+ printf( "%s%s (string array)\n", indent, name );
+
+ int i;
+ for( i = 0; i < size; ++i )
+ printf( "%s\t%s\n", indent, osrfStringArrayGetString( sarr, i ) );
+}
--- /dev/null
+/**
+ @file idlval.c
+ @brief Validator for IDL files.
+*/
+
+/*
+Copyright (C) 2009 Georgia Public Library Service
+Scott McKellar <scott@esilibrary.com>
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <libxml/globals.h>
+#include <libxml/xmlerror.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/debugXML.h>
+#include <libxml/xmlmemory.h>
+
+#include <opensrf/utils.h>
+#include <opensrf/osrf_hash.h>
+
+/* Represents the command line */
+struct Opts {
+ int new_argc;
+ char ** new_argv;
+
+ char * idl_file_name;
+ int idl_file_name_found;
+ int warning;
+};
+typedef struct Opts Opts;
+
+/* datatype attribute of <field> element */
+typedef enum {
+ DT_NONE,
+ DT_BOOL,
+ DT_FLOAT,
+ DT_ID,
+ DT_INT,
+ DT_INTERVAL,
+ DT_LINK,
+ DT_MONEY,
+ DT_NUMBER,
+ DT_ORG_UNIT,
+ DT_TEXT,
+ DT_TIMESTAMP,
+ DT_INVALID
+} Datatype;
+
+/* Represents a <Field> aggregate */
+struct Field_struct {
+ struct Field_struct* next;
+ xmlChar* name;
+ int is_virtual; // boolean
+ xmlChar* label;
+ Datatype datatype;
+};
+typedef struct Field_struct Field;
+
+/* reltype attribute of <link> element */
+typedef enum {
+ RT_NONE,
+ RT_HAS_A,
+ RT_MIGHT_HAVE,
+ RT_HAS_MANY,
+ RT_INVALID
+} Reltype;
+
+/* Represents a <link> element */
+struct Link_struct {
+ struct Link_struct* next;
+ xmlChar* field;
+ Reltype reltype;
+ xmlChar* key;
+ xmlChar* classref;
+};
+typedef struct Link_struct Link;
+
+/* Represents a <class> aggregate */
+typedef struct {
+ xmlNodePtr node;
+ int loaded; // boolean
+ int is_virtual; // boolean
+ xmlChar* primary; // name of primary key column
+ Field* fields; // linked list
+ Link* links; // linked list
+} Class;
+
+static int get_Opts( int argc, char * argv[], Opts * pOpts );;
+static int val_idl( void );
+static int cross_validate_classes( Class* class, const char* id );
+static int cross_validate_linkage( Class* class, const char*id, Link* link );
+static int val_class( Class* class, const char* id );
+static int val_class_attributes( Class* class, const char* id );
+static int check_labels( const Class* class, const char* id );
+static int val_fields_attributes( Class* class, const char* id, xmlNodePtr fields );
+static int val_links_to_fields( const Class* class, const char* id );
+static int compareFieldAndLink( const Class* class, const char* id,
+ const Field* field, const Link* link );
+static int val_fields_to_links( const Class* class, const char* id );
+static const Field* searchFieldByName( const Class* class, const xmlChar* field_name );
+static int val_fields( Class* class, const char* id, xmlNodePtr fields );
+static int val_one_field( Class* class, const char* id, xmlNodePtr field );
+static Datatype translate_datatype( const xmlChar* value );
+static int val_links( Class* class, const char* id, xmlNodePtr links );
+static int val_one_link( Class* class, const char* id, xmlNodePtr link );
+static Reltype translate_reltype( const xmlChar* value );
+static int scan_idl( xmlDocPtr doc );
+static int register_class( xmlNodePtr child );
+static int addField( Class* class, const char* id, Field* new_field );
+static int addLink( Class* class, const char* id, Link* new_link );
+static Class* newClass( xmlNodePtr node );
+static void freeClass( char* key, void* p );
+static Field* newField( xmlChar* name );
+static void freeField( Field* field );
+static Link* newLink( xmlChar* field );
+static void freeLink( Link* link );
+
+/* Stores an in-memory representation of the IDL */
+static osrfHash* classes = NULL;
+
+static int warn = 0; // boolean; true if -w present on command line
+
+int main( int argc, char* argv[] ) {
+
+ // Examine command line
+ Opts opts;
+ if( get_Opts( argc, argv, &opts ) )
+ return 1;
+
+ const char* IDL_filename = NULL;
+ if( opts.idl_file_name_found )
+ IDL_filename = opts.idl_file_name;
+ else {
+ IDL_filename = getenv( "OILS_IDL_FILENAME" );
+ if( ! IDL_filename )
+ IDL_filename = "/openils/conf/fm_IDL.xml";
+ }
+
+ if( opts.warning )
+ warn = 1;
+
+ int rc = 0;
+
+ xmlLineNumbersDefault(1);
+ xmlDocPtr doc = xmlReadFile( IDL_filename, NULL, XML_PARSE_XINCLUDE );
+ if ( ! doc ) {
+ fprintf( stderr, "Could not load or parse the IDL XML file %s\n", IDL_filename );
+ rc = 1;
+ } else {
+ printf( "Validating: %s\n", IDL_filename );
+ classes = osrfNewHash();
+ osrfHashSetCallback( classes, freeClass );
+
+ // Load the IDL
+ if( scan_idl( doc ) )
+ rc = 1;
+
+ if( opts.new_argc < 2 ) {
+
+ // No classes specified: validate all classes
+ if( val_idl() )
+ rc = 1;
+ } else {
+
+ // Validate one or more specified classes
+ int i = 1;
+ while( i < opts.new_argc ) {
+ const char* classname = opts.new_argv[ i ];
+ Class* class = osrfHashGet( classes, classname );
+ if( ! class ) {
+ printf( "Class \"%s\" does not exist\n", classname );
+ rc = 1;
+ } else {
+ // Validate the class in isolation
+ if( val_class( class, classname ) )
+ rc = 1;
+ // Cross-validate with linked classes
+ if( cross_validate_classes( class, classname ) )
+ rc = 1;
+ }
+ ++i;
+ }
+ }
+ osrfHashFree( classes );
+ xmlFreeDoc( doc );
+ }
+
+ return rc;
+}
+
+/**
+ @brief Examine the command line
+ @param argc Number of entries in argv[]
+ @param argv Array of pointers to command line strings
+ @param pOpts Pointer to structure to be populated
+ @return 0 upon success, or 1 if the command line is invalid
+*/
+static int get_Opts( int argc, char * argv[], Opts * pOpts ) {
+ int rc = 0; /* return code */
+ int opt;
+
+ /* Define valid option characters */
+
+ const char optstring[] = ":f:w";
+
+ /* Initialize members of struct */
+
+ pOpts->new_argc = 0;
+ pOpts->new_argv = NULL;
+
+ pOpts->idl_file_name_found = 0;
+ pOpts->idl_file_name = NULL;
+ pOpts->warning = 0;
+
+ /* Suppress error messages from getopt() */
+
+ opterr = 0;
+
+ /* Examine command line options */
+
+ while( ( opt = getopt( argc, argv, optstring ) ) != -1 ) {
+ switch( opt ) {
+ case 'f' : /* Get idl_file_name */
+ if( pOpts->idl_file_name_found ) {
+ fprintf( stderr, "Only one occurrence of -f option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->idl_file_name_found = 1;
+
+ pOpts->idl_file_name = optarg;
+ break;
+ case 'w' : /* Get warning */
+ pOpts->warning = 1;
+ break;
+ case ':' : /* Missing argument */
+ fprintf( stderr, "Required argument missing on -%c option\n",
+ (char) optopt );
+ rc = 1;
+ break;
+ case '?' : /* Invalid option */
+ fprintf( stderr, "Invalid option '-%c' on command line\n",
+ (char) optopt );
+ rc = 1;
+ break;
+ default : /* Programmer error */
+ fprintf( stderr, "Internal error: unexpected value '-%c'"
+ "for optopt", (char) optopt );
+ rc = 1;
+ break;
+ } /* end switch */
+ } /* end while */
+
+ if( optind > argc ) {
+ /* This should never happen! */
+
+ fprintf( stderr, "Program error: found more arguments than expected\n" );
+ rc = 1;
+ } else {
+ /* Calculate new_argcv and new_argc to reflect */
+ /* the number of arguments consumed */
+
+ pOpts->new_argc = argc - optind + 1;
+ pOpts->new_argv = argv + optind - 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Validate all classes.
+ @return 1 if errors found, or 0 if not.
+
+ Traverse the class list and validate each class in turn.
+*/
+static int val_idl( void ) {
+ int rc = 0;
+ osrfHashIterator* itr = osrfNewHashIterator( classes );
+ Class* class = NULL;
+
+ // For each class
+ while( (class = osrfHashIteratorNext( itr )) ) {
+ const char* id = osrfHashIteratorKey( itr );
+ if( val_class( class, id ) ) // validate class separately
+ rc = 1;
+ if( cross_validate_classes( class, id ) ) // cross-validate with linked classes
+ rc = 1;
+ }
+
+ osrfHashIteratorFree( itr );
+ return rc;
+}
+
+/**
+ @brief Make sure that every linkage appropriately matches the linked class.
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @return 1 if errors found, or 0 if not.
+*/
+static int cross_validate_classes( Class* class, const char* id ) {
+ int rc = 0;
+ Link* link = class->links;
+ while( link ) {
+ if( cross_validate_linkage( class, id, link ) )
+ rc = 1;
+ link = link->next;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Make sure that a linkage appropriately matches the linked class.
+ @param class Pointer to the current class.
+ @param id Class id.
+ @param link Pointer to the link being validated.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - The linked class must exist.
+ - The field to which the linkage points must exist.
+ - If the linked class has a corresponding link back to the current class, then exactly
+ one end of the linkage must have a reltype of "has_many".
+
+ It is not an error if the linkage is not reciprocated.
+*/
+static int cross_validate_linkage( Class* class, const char*id, Link* link ) {
+ int rc = 0;
+ Class* other_class = osrfHashGet( classes, (char*) link->classref );
+ if( ! other_class ) {
+ printf( "In class \"%s\": class \"%s\", referenced by \"%s\" field, does not exist\n",
+ id, (char*) link->classref, (char*) link->field );
+ rc = 1;
+ } else {
+ // Make sure the other class is loaded before we look at it further
+ if( val_class( other_class, (char*) link->classref ) )
+ rc = 1;
+
+ // Now see if the other class links back to this one
+ Link* other_link = other_class->links;
+ while( other_link ) {
+ if( !strcmp( id, (char*) other_link->classref ) // class to class
+ && !strcmp( (char*) link->key, (char*) other_link->field ) // key to field
+ && !strcmp( (char*) link->field, (char*) other_link->key ) ) { // field to key
+ break;
+ }
+ other_link = other_link->next;
+ }
+
+ if( ! other_link ) {
+ // Link is not reciprocated? That's okay, as long as
+ // the referenced field exists in the referenced class.
+ if( !searchFieldByName( other_class, link->key ) ) {
+ printf( "In class \"%s\": field \"%s\" links to field \"%s\" of class \"%s\", "
+ "but that field doesn't exist\n", id, (char*) link->field,
+ (char*) link->key, (char*) link->classref );
+ rc = 1;
+ }
+ } else {
+ // The link is reciprocated. Make sure that exactly one of the links
+ // has a reltype of "has_many"
+ int many_count = 0;
+ if( RT_HAS_MANY == link->reltype )
+ ++many_count;
+ if( RT_HAS_MANY == other_link->reltype )
+ ++many_count;
+
+ if( 0 == many_count ) {
+ printf( "Classes \"%s\" and \"%s\" link to each other, but neither has a reltype "
+ "of \"has_many\"\n", id, (char*) link->classref );
+ rc = 1;
+ } else if( 2 == many_count ) {
+ printf( "Classes \"%s\" and \"%s\" link to each other, but both have a reltype "
+ "of \"has_many\"\n", id, (char*) link->classref );
+ rc = 1;
+ }
+ }
+ }
+
+ return rc;
+}
+
+/**
+ @brief Validate a single class.
+ @param id Class id.
+ @param class Pointer to the XML node for the class element.
+ @return 1 if errors found, or 0 if not.
+
+ We have already validated the id.
+
+ Rules:
+ - Allowed elements are "fields", "links", "permacrud", and "source_definition".
+ - None of these elements may occur more than once in the same class.
+ - The "fields" element is required.
+ - No text allowed, other than white space.
+ - Comments are allowed (and ignored).
+*/
+static int val_class( Class* class, const char* id ) {
+ if( !class )
+ return 1;
+ else if( class->loaded )
+ return 0; // We've already validated this one locally
+
+ int rc = 0;
+
+ if( val_class_attributes( class, id ) )
+ rc = 1;
+
+ xmlNodePtr fields = NULL;
+ xmlNodePtr links = NULL;
+ xmlNodePtr permacrud = NULL;
+ xmlNodePtr src_def = NULL;
+
+ // Examine every child element of the <class> element.
+ xmlNodePtr child = class->node->children;
+ while( child ) {
+ const char* child_name = (char*) child->name;
+ if( xmlNodeIsText( child ) ) {
+ if( ! xmlIsBlankNode( child ) ) {
+ // Found unexpected text. After removing leading and
+ // trailing white space, complain about it.
+ xmlChar* content = xmlNodeGetContent( child );
+
+ xmlChar* begin = content;
+ while( *begin && isspace( *begin ) )
+ ++begin;
+ if( *begin ) {
+ xmlChar* end = begin + strlen( (char*) begin ) - 1;
+ while( (isspace( *end ) ) )
+ --end;
+ end[ 1 ] = '\0';
+ }
+
+ printf( "Unexpected text in class \"%s\": \"%s\"\n", id,
+ (char*) begin );
+ xmlFree( content );
+ }
+ } else if( !strcmp( child_name, "fields" ) ) {
+ if( fields ) {
+ printf( "Multiple <fields> elements in class \"%s\"\n", id );
+ rc = 1;
+ } else {
+ fields = child;
+ // Identify the primary key, if any
+ class->primary = xmlGetProp( fields, (xmlChar*) "primary" );
+ if( val_fields( class, id, fields ) )
+ rc = 1;
+ }
+ } else if( !strcmp( child_name, "links" ) ) {
+ if( links ) {
+ printf( "Multiple <links> elements in class \"%s\"\n", id );
+ rc = 1;
+ } else {
+ links = child;
+ if( val_links( class, id, links ) )
+ rc = 1;
+ }
+ } else if( !strcmp( child_name, "permacrud" ) ) {
+ if( permacrud ) {
+ printf( "Multiple <permacrud> elements in class \"%s\"\n", id );
+ rc = 1;
+ } else {
+ permacrud = child;
+ }
+ } else if( !strcmp( child_name, "source_definition" ) ) {
+ if( src_def ) {
+ printf( "Multiple <source_definition> elements in class \"%s\"\n", id );
+ rc = 1;
+ } else {
+ // To do: verify that there is nothing in <source_definition> except text and
+ // comments, and that the text is non-empty.
+ src_def = child;
+ }
+ } else if( !strcmp( child_name, "comment" ) )
+ ; // ignore comment
+ else {
+ printf( "Line %ld: Unexpected <%s> element in class \"%s\"\n",
+ xmlGetLineNo( child ), child_name, id );
+ rc = 1;
+ }
+ child = child->next;
+ }
+
+ if( fields ) {
+ if( check_labels( class, id ) )
+ rc = 1;
+ if( val_fields_attributes( class, id, fields ) )
+ rc = 1;
+ } else {
+ printf( "No <fields> element in class \"%s\"\n", id );
+ rc = 1;
+ }
+
+ if( val_links_to_fields( class, id ) )
+ rc = 1;
+
+ if( val_fields_to_links( class, id ) )
+ rc = 1;
+
+ class->loaded = 1;
+ return rc;
+}
+
+/**
+ @brief Validate the class attributes.
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @return if errors found, or 0 if not.
+
+ Rules:
+ - Only the following attributes are valid: controller, core, field_safe, field_mapper,
+ id, label, readonly, restrict_primary, tablename, and virtual.
+ - The controller and fieldmapper attributes are required (as is the id attribute, but
+ that's checked elsewhere).
+ - Every attribute value must be non-empty.
+ - The values of attributes core, field_safe, reaadonly, and virtual must be either
+ "true" or "false".
+ - A virtual class must not have a tablename attribute.
+*/
+static int val_class_attributes( Class* class, const char* id ) {
+ int rc = 0;
+
+ int controller_found = 0; // boolean
+ int fieldmapper_found = 0; // boolean
+ int tablename_found = 0; // boolean
+
+ xmlAttrPtr attr = class->node->properties;
+ while( attr ) {
+ const char* attr_name = (char*) attr->name;
+ if( !strcmp( (char*) attr_name, "id" ) ) {
+ ; // ignore; we already grabbed this one
+ } else if( !strcmp( (char*) attr_name, "controller" ) ) {
+ controller_found = 1;
+ xmlChar* value = xmlGetProp( class->node, (xmlChar*) "controller" );
+ if( '\0' == *value ) {
+ printf( "Line %ld: Value of controller attribute is empty in class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+ xmlFree( value );
+ } else if( !strcmp( (char*) attr_name, "fieldmapper" ) ) {
+ fieldmapper_found = 1;
+ xmlChar* value = xmlGetProp( class->node, (xmlChar*) "fieldmapper" );
+ if( '\0' == *value ) {
+ printf( "Line %ld: Value of fieldmapper attribute is empty in class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+ xmlFree( value );
+ } else if( !strcmp( (char*) attr_name, "label" ) ) {
+ xmlChar* value = xmlGetProp( class->node, (xmlChar*) "label" );
+ if( '\0' == *value ) {
+ printf( "Line %ld: Value of label attribute is empty in class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+ xmlFree( value );
+ } else if( !strcmp( (char*) attr_name, "tablename" ) ) {
+ tablename_found = 1;
+ xmlChar* value = xmlGetProp( class->node, (xmlChar*) "tablename" );
+ if( '\0' == *value ) {
+ printf( "Line %ld: Value of tablename attribute is empty in class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+ xmlFree( value );
+ } else if( !strcmp( (char*) attr_name, "virtual" ) ) {
+ xmlChar* virtual_str = xmlGetProp( class->node, (xmlChar*) "virtual" );
+ if( virtual_str ) {
+ if( !strcmp( (char*) virtual_str, "true" ) ) {
+ class->is_virtual = 1;
+ } else if( strcmp( (char*) virtual_str, "false" ) ) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for virtual attribute of class\"%s\"\n",
+ xmlGetLineNo( class->node ), (char*) virtual_str, id );
+ rc = 1;
+ }
+ xmlFree( virtual_str );
+ }
+ } else if( !strcmp( (char*) attr_name, "readonly" ) ) {
+ xmlChar* readonly = xmlGetProp( class->node, (xmlChar*) "readonly" );
+ if( readonly ) {
+ if( strcmp( (char*) readonly, "true" )
+ && strcmp( (char*) readonly, "false" ) ) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for readonly attribute of class\"%s\"\n",
+ xmlGetLineNo( class->node ), (char*) readonly, id );
+ rc = 1;
+ }
+ xmlFree( readonly );
+ }
+ } else if( !strcmp( (char*) attr_name, "restrict_primary" ) ) {
+ xmlChar* value = xmlGetProp( class->node, (xmlChar*) "restrict_primary" );
+ if( '\0' == *value ) {
+ printf( "Line %ld: Value of restrict_primary attribute is empty in class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+ xmlFree( value );
+ } else if( !strcmp( (char*) attr_name, "core" ) ) {
+ xmlChar* core = xmlGetProp( class->node, (xmlChar*) "core" );
+ if( core ) {
+ if( strcmp( (char*) core, "true" )
+ && strcmp( (char*) core, "false" ) ) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for core attribute of class\"%s\"\n",
+ xmlGetLineNo( class->node ), (char*) core, id );
+ rc = 1;
+ }
+ xmlFree( core );
+ }
+ } else if( !strcmp( (char*) attr_name, "field_safe" ) ) {
+ xmlChar* field_safe = xmlGetProp( class->node, (xmlChar*) "field_safe" );
+ if( field_safe ) {
+ if( strcmp( (char*) field_safe, "true" )
+ && strcmp( (char*) field_safe, "false" ) ) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for field_safe attribute of class\"%s\"\n",
+ xmlGetLineNo( class->node ), (char*) field_safe, id );
+ rc = 1;
+ }
+ xmlFree( field_safe );
+ }
+ } else {
+ printf( "Line %ld: Unrecognized class attribute \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( class->node ), attr_name, id );
+ rc = 1;
+ }
+ attr = attr->next;
+ } // end while
+
+ if( ! controller_found ) {
+ printf( "Line %ld: No controller attribute for class \"%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+
+ if( ! fieldmapper_found ) {
+ printf( "Line %ld: No fieldmapper attribute for class \"\%s\"\n",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+
+ if( class->is_virtual && tablename_found ) {
+ printf( "Line %ld: Virtual class \"%s\" shouldn't have a tablename",
+ xmlGetLineNo( class->node ), id );
+ rc = 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Determine whether fields are either all labeled or all unlabeled.
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @return 1 if errors found, or 0 if not.
+
+ Rule:
+ - The fields for a given class must either all be labeled or all unlabeled.
+
+ For purposes of this validation, a field is considered labeled even if the label is an
+ empty string. Empty labels are reported elsewhere.
+*/
+static int check_labels( const Class* class, const char* id ) {
+ int rc = 0;
+
+ int label_found = 0; // boolean
+ int unlabel_found = 0; // boolean
+
+ Field* field = class->fields;
+ while( field ) {
+ if( field->label )
+ label_found = 1;
+ else
+ unlabel_found = 1;
+ field = field->next;
+ }
+
+ if( label_found && unlabel_found ) {
+ printf( "Class \"%s\" has a mixture of labeled and unlabeled fields\n", id );
+ rc = 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Validate the fields attributes.
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @param fields Pointer to the XML node for the fields element.
+ @return if errors found, or 0 if not.
+
+ Rules:
+ - The only valid attributes for the fields element are "primary" and "sequence".
+ - Neither attribute may have an empty string for a value.
+ - If there is a sequence attribute, there must also be a primary attribute.
+ - If there is a primary attribute, the field identified must exist.
+ - If the primary key field has the datatype "id", there must be a sequence attribute.
+ - If the datatype of the primary key is not "id" or "int", there must @em not be a
+ sequence attribute.
+*/
+static int val_fields_attributes( Class* class, const char* id, xmlNodePtr fields ) {
+ int rc = 0;
+
+ xmlChar* sequence = NULL;
+ xmlChar* primary = NULL;
+
+ // Traverse the attributes
+ xmlAttrPtr attr = fields->properties;
+ while( attr ) {
+ const char* attr_name = (char*) attr->name;
+ if( !strcmp( attr_name, "primary" ) ) {
+ primary = xmlGetProp( fields, (xmlChar*) "primary" );
+ if( '\0' == primary[0] ) {
+ printf(
+ "Line %ld: value of primary attribute is an empty string for class \"%s\"\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ }
+ } else if( !strcmp( attr_name, "sequence" )) {
+ sequence = xmlGetProp( fields, (xmlChar*) "sequence" );
+ if( '\0' == sequence[0] ) {
+ printf(
+ "Line %ld: value of sequence attribute is an empty string for class \"%s\"\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ } else if( !strchr( (const char*) sequence, '.' )) {
+ printf(
+ "Line %ld: name of sequence for class \"%s\" is not qualified by schema\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ }
+ } else {
+ printf( "Line %ld: Unexpected fields attribute \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( fields ), attr_name, id );
+ rc = 1;
+ }
+
+ attr = attr->next;
+ }
+
+ if( sequence && ! primary ) {
+ printf( "Line %ld: class \"%s\" has a sequence identified but no primary key\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ }
+
+ if( primary ) {
+ // look for the primary key
+ Field* field = class->fields;
+ while( field ) {
+ if( !strcmp( (char*) field->name, (char*) primary ) )
+ break;
+ field = field->next;
+ }
+ if( !field ) {
+ printf( "Primary key field \"%s\" does not exist for class \"%s\"\n",
+ (char*) primary, id );
+ rc = 1;
+ } else if( DT_ID == field->datatype && ! sequence && ! class->is_virtual ) {
+ printf(
+ "Line %ld: Primary key is an id; class \"%s\" may need a sequence attribute\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ } else if( DT_ID != field->datatype
+ && DT_INT != field->datatype
+ && DT_ORG_UNIT != field->datatype
+ && sequence ) {
+ printf(
+ "Line %ld: Datatype of key for class \"%s\" does not allow a sequence attribute\n",
+ xmlGetLineNo( fields ), id );
+ rc = 1;
+ }
+ }
+
+ xmlFree( primary );
+ xmlFree( sequence );
+ return rc;
+}
+
+/**
+ @brief Verify that every Link has a matching Field for a given Class.
+ @param class Pointer to the current class.
+ @param id Class id.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - For every link element, there must be a matching field element in the same class.
+ - If the link's reltype is "has_many", the field must be a virtual field of type link
+ or org_unit.
+ - If the link's reltype is "has_a" or "might_have", the field must be a non-virtual link
+ of type link or org_unit.
+*/
+static int val_links_to_fields( const Class* class, const char* id ) {
+ if( !class )
+ return 1;
+
+ int rc = 0;
+
+ const Link* link = class->links;
+ while( link ) {
+ if( link->field && *link->field ) {
+ const Field* field = searchFieldByName( class, link->field );
+ if( field ) {
+ if( compareFieldAndLink( class, id, field, link ) )
+ rc = 1;
+ } else {
+ printf( "\"%s\" class has no <field> corresponding to <link> for \"%s\"\n",
+ id, (char*) link->field );
+ rc = 1;
+ }
+ }
+ link = link->next;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Compare matching field and link elements to see if they are compatible
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @param field Pointer to the Field to be compared to the Link.
+ @param link Pointer to the Link to be compared to the Field.
+ @return 0 if they are compatible, or 1 if not.
+
+ Rules:
+ - If the reltype is "has_many", the field must be virtual.
+ - If a field corresponds to a link, and is not the primary key, then it must have a
+ datatype "link" or "org_unit".
+ - If the datatype is "org_unit", the linkage must be to the class "aou".
+
+ Warnings:
+ - If the reltype is "has_a" or "might_have", the the field should probably @em not
+ be virtual, but there are legitimate exceptions.
+ - If the linkage is to the class "aou", then the datatype should probably be "org_unit".
+*/
+static int compareFieldAndLink( const Class* class, const char* id,
+ const Field* field, const Link* link ) {
+ int rc = 0;
+
+ Datatype datatype = field->datatype;
+ const char* classref = (const char*) link->classref;
+
+ // Validate the virtuality of the field
+ if( RT_HAS_A == link->reltype || RT_MIGHT_HAVE == link->reltype ) {
+ if( warn && field->is_virtual ) {
+ // This is the child class; field should usually be non-virtual,
+ // but there are legitimate exceptions.
+ printf( "WARNING: In class \"%s\": field \"%s\" is tied to a \"has_a\" or "
+ "\"might_have\" link; perhaps should not be virtual\n",
+ id, (char*) field->name );
+ }
+ } else if ( RT_HAS_MANY == link->reltype ) {
+ if( ! field->is_virtual ) {
+ printf( "In class \"%s\": field \"%s\" is tied to a \"has_many\" link "
+ "and therefore should be virtual\n", id, (char*) field->name );
+ rc = 1;
+ }
+ }
+
+ // Validate the datatype of the field
+ if( class->primary && !strcmp( (char*) class->primary, (char*) field->name ) ) {
+ ; // For the primary key field, the datatype can be anything
+ } else if( DT_NONE == datatype || DT_INVALID == datatype ) {
+ printf( "In class \"%s\": \"%s\" field should have a datatype for linkage\n",
+ id, (char*) field->name );
+ rc = 1;
+ } else if( DT_ORG_UNIT == datatype ) {
+ if( strcmp( classref, "aou" ) ) {
+ printf( "In class \"%s\": \"%s\" field should have a datatype "
+ "\"link\", not \"org_unit\"\n", id, field->name );
+ rc = 1;
+ }
+ } else if( DT_LINK == datatype ) {
+ if( warn && !strcmp( classref, "aou" ) ) {
+ printf( "WARNING: In class \"%s\", field \"%s\": Consider changing datatype "
+ "to \"org_unit\"\n", id, (char*) field->name );
+ }
+ } else {
+ // Datatype should be "link", or maybe "org_unit"
+ if( !strcmp( classref, "aou" ) ) {
+ printf( "In class \"%s\": \"%s\" field should have a datatype "
+ "\"org_unit\" or \"link\"\n",
+ id, (char*) field->name );
+ rc = 1;
+ } else {
+ printf( "In class \"%s\": \"%s\" field should have a datatype \"link\"\n",
+ id, (char*) field->name );
+ rc = 1;
+ }
+ }
+
+ return rc;
+}
+
+/**
+ @brief See if every linked field has a counterpart in the links aggregate.
+ @param class Pointer to the current class.
+ @param id Class id.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - If a field has a datatype of "link" or "org_unit, there must be a corresponding
+ entry in the links aggregate.
+*/
+static int val_fields_to_links( const Class* class, const char* id ) {
+ int rc = 0;
+ const Field* field = class->fields;
+ while( field ) {
+ if( DT_LINK != field->datatype && DT_ORG_UNIT != field->datatype ) {
+ field = field->next;
+ continue; // not a link? skip it
+ }
+ // See if there's a matching entry in the <links> aggregate
+ const Link* link = class->links;
+ while( link ) {
+ if( !strcmp( (char*) field->name, (char*) link->field ) )
+ break;
+ link = link->next;
+ }
+
+ if( !link ) {
+ if( !strcmp( (char*) field->name, "id" ) && !strcmp( id, "aou" ) ) {
+ // Special exception: primary key of "aou" is of
+ // datatype "org_unit", but it's not a foreign key.
+ ;
+ } else {
+ printf( "In class \"%s\": Linked field \"%s\" has no matching <link>\n",
+ id, (char*) field->name );
+ rc = 1;
+ }
+ }
+ field = field->next;
+ }
+ return rc;
+}
+
+/**
+ @brief Search a given Class for a Field with a given name.
+ @param class Pointer to the class in which to search.
+ @param field_name The field name for which to search.
+ @return Pointer to the Field if found, or NULL if not.
+*/
+static const Field* searchFieldByName( const Class* class, const xmlChar* field_name ) {
+ if( ! class || ! field_name || ! *field_name )
+ return NULL;
+
+ const char* name = (const char*) field_name;
+ const Field* field = class->fields;
+ while( field ) {
+ if( field->name && !strcmp( (char*) field->name, name ) )
+ return field;
+ field = field->next;
+ }
+
+ return NULL;
+}
+
+/**
+ @brief Validate a fields element.
+ @param class Pointer to the current Class.
+ @param id Id of the current Class.
+ @param fields Pointer to the XML node for the fields element.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - There must be at least one field element.
+ - No other elements are allowed.
+ - Text is not allowed, other than white space.
+ - Comments are allowed (and ignored).
+*/
+static int val_fields( Class* class, const char* id, xmlNodePtr fields ) {
+ int rc = 0;
+ int field_found = 0; // boolean
+
+ xmlNodePtr child = fields->children;
+ while( child ) {
+ const char* child_name = (char*) child->name;
+ if( xmlNodeIsText( child ) ) {
+ if( ! xmlIsBlankNode( child ) ) {
+ // Found unexpected text. After removing leading and
+ // trailing white space, complain about it.
+ xmlChar* content = xmlNodeGetContent( child );
+
+ xmlChar* begin = content;
+ while( *begin && isspace( *begin ) )
+ ++begin;
+ if( *begin ) {
+ xmlChar* end = begin + strlen( (char*) begin ) - 1;
+ while( (isspace( *end ) ) )
+ --end;
+ end[ 1 ] = '\0';
+ }
+
+ printf( "Unexpected text in <fields> element of class \"%s\": \"%s\"\n", id,
+ (char*) begin );
+ xmlFree( content );
+ }
+ } else if( ! strcmp( child_name, "field" ) ) {
+ field_found = 1;
+ if( val_one_field( class, id, child ) )
+ rc = 1;
+ } else if( !strcmp( child_name, "comment" ) )
+ ; // ignore comment
+ else {
+ printf( "Line %ld: Unexpected <%s> element in <fields> of class \"%s\"\n",
+ xmlGetLineNo( child ), child_name, id );
+ rc = 1;
+ }
+ child = child->next;
+ }
+
+ if( !field_found ) {
+ printf( "No <field> element in class \"%s\"\n", id );
+ rc = 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Validate a field element within a fields element.
+ @param class Pointer to the current Class.
+ @param id Class id.
+ @param field Pointer to the XML node for the field element.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - attribute names are limited to: "name", "virtual", "label", "datatype", "array_position",
+ "selector", "i18n", "primitive".
+ - "name" attribute is required.
+ - label attribute, if present, must have a non-empty value.
+ - virtual and i18n attributes, if present, must have a value of "true" or "false".
+ - if the datatype attribute is present, its value must be one of: "bool", "float", "id",
+ "int", "interval", "link", "money", "number", "org_unit", "text", "timestamp".
+
+ Warnings:
+ - A non-virtual field should have a datatype attribute.
+ - Attribute "array_position" is deprecated.
+*/
+static int val_one_field( Class* class, const char* id, xmlNodePtr field ) {
+ int rc = 0;
+ xmlChar* label = NULL;
+ xmlChar* field_name = NULL;
+ int is_virtual = 0;
+ Datatype datatype = DT_NONE;
+
+ // Traverse the attributes
+ xmlAttrPtr attr = field->properties;
+ while( attr ) {
+ const char* attr_name = (char*) attr->name;
+ if( !strcmp( attr_name, "name" ) ) {
+ field_name = xmlGetProp( field, (xmlChar*) "name" );
+ } else if( !strcmp( attr_name, "virtual" ) ) {
+ xmlChar* virt = xmlGetProp( field, (xmlChar*) "virtual" );
+ if( !strcmp( (char*) virt, "true" ) )
+ is_virtual = 1;
+ else if( strcmp( (char*) virt, "false" ) ) {
+ printf( "Line %ld: Invalid value for virtual attribute: \"%s\"\n",
+ xmlGetLineNo( field ), (char*) virt );
+ rc = 1;
+ }
+ xmlFree( virt );
+ // To do: verify that the namespace is oils_persist
+ } else if( !strcmp( attr_name, "label" ) ) {
+ label = xmlGetProp( field, (xmlChar*) "label" );
+ if( '\0' == *label ) {
+ printf( "Line %ld: Empty value for label attribute for class \"%s\"\n",
+ xmlGetLineNo( field ), id );
+ xmlFree( label );
+ label = NULL;
+ rc = 1;
+ }
+ // To do: verify that the namespace is reporter
+ } else if( !strcmp( attr_name, "datatype" ) ) {
+ xmlChar* dt_str = xmlGetProp( field, (xmlChar*) "datatype" );
+ datatype = translate_datatype( dt_str );
+ if( DT_INVALID == datatype ) {
+ printf( "Line %ld: Invalid datatype \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), (char*) dt_str, id );
+ rc = 1;
+ }
+ xmlFree( dt_str );
+ // To do: make sure that the namespace is reporter
+ } else if( !strcmp( attr_name, "array_position" ) ) {
+ printf( "Line %ld: WARNING: Deprecated array_position attribute "
+ "for field \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), ((char*) field_name ? : ""), id );
+ } else if( !strcmp( attr_name, "selector" ) ) {
+ ; // Ignore for now
+ } else if( !strcmp( attr_name, "i18n" ) ) {
+ xmlChar* i18n = xmlGetProp( field, (xmlChar*) "i18n" );
+ if( strcmp( (char*) i18n, "true" ) && strcmp( (char*) i18n, "false" ) ) {
+ printf( "Line %ld: Invalid value for i18n attribute: \"%s\"\n",
+ xmlGetLineNo( field ), (char*) i18n );
+ rc = 1;
+ }
+ xmlFree( i18n );
+ // To do: verify that the namespace is oils_persist
+ } else if( !strcmp( attr_name, "primitive" ) ) {
+ xmlChar* primitive = xmlGetProp( field, (xmlChar*) "primitive" );
+ if( strcmp( (char*) primitive, "string" ) && strcmp( (char*) primitive, "number" ) ) {
+ printf( "Line %ld: Invalid value for primitive attribute: \"%s\"\n",
+ xmlGetLineNo( field ), (char*) primitive );
+ rc = 1;
+ }
+ xmlFree( primitive );
+ } else if( !strcmp( attr_name, "validate" )) {
+ xmlChar* validate = xmlGetProp( field, (xmlChar*) "validate" );
+ if( !*validate ) {
+ // Value should be a regular expression to define a validation rule
+ printf( "Line %ld: Empty value for \"validate\" attribute "
+ "for field \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), (char*) field_name ? : "", id );
+ rc = 1;
+ }
+ xmlFree( validate );
+ // To do: verify that the namespace is oils_obj
+ } else if( !strcmp( attr_name, "required" )) {
+ xmlChar* required = xmlGetProp( field, (xmlChar*) "required" );
+ if( strcmp( (char*) required, "true" ) && strcmp( (char*) required, "false" )) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for \"required\" attribute "
+ "for field \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), (char*) required,
+ (char*) field_name ? : "", id );
+ rc = 1;
+ }
+ xmlFree( required );
+ // To do: verify that the namespace is oils_obj
+ } else {
+ printf( "Line %ld: Unexpected field attribute \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), attr_name, id );
+ rc = 1;
+ }
+
+ attr = attr->next;
+ }
+
+ if( warn && (!is_virtual) && DT_NONE == datatype ) {
+ printf( "Line %ld: WARNING: No datatype attribute for field \"%s\" in class \"%s\"\n",
+ xmlGetLineNo( field ), ((char*) field_name ? : ""), id );
+ }
+
+ if( ! field_name ) {
+ printf( "Line %ld: No name attribute for <field> element in class \"%s\"\n",
+ xmlGetLineNo( field ), id );
+ rc = 1;
+ } else if( '\0' == *field_name ) {
+ printf( "Line %ld: Field name is empty for <field> element in class \"%s\"\n",
+ xmlGetLineNo( field ), id );
+ rc = 1;
+ } else {
+ // Add to the class's field list
+ Field* new_field = newField( field_name );
+ new_field->is_virtual = is_virtual;
+ new_field->label = label;
+ new_field->datatype = datatype;
+ if( addField( class, id, new_field ) )
+ rc = 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Translate a datatype string into a Dataype (an enum).
+ @param value The value of a datatype attribute.
+ @return The datatype in the form of an enum.
+*/
+static Datatype translate_datatype( const xmlChar* value ) {
+ const char* val = (const char*) value;
+ Datatype type;
+
+ if( !value || !*value )
+ type = DT_NONE;
+ else if( !strcmp( val, "bool" ) )
+ type = DT_BOOL;
+ else if( !strcmp( val, "float" ) )
+ type = DT_FLOAT;
+ else if( !strcmp( val, "id" ) )
+ type = DT_ID;
+ else if( !strcmp( val, "int" ) )
+ type = DT_INT;
+ else if( !strcmp( val, "interval" ) )
+ type = DT_INTERVAL;
+ else if( !strcmp( val, "link" ) )
+ type = DT_LINK;
+ else if( !strcmp( val, "money" ) )
+ type = DT_MONEY;
+ else if( !strcmp( val, "number" ) )
+ type = DT_NUMBER;
+ else if( !strcmp( val, "org_unit" ) )
+ type = DT_ORG_UNIT;
+ else if( !strcmp( val, "text" ) )
+ type = DT_TEXT;
+ else if( !strcmp( val, "timestamp" ) )
+ type = DT_TIMESTAMP;
+ else
+ type = DT_INVALID;
+
+ return type;
+}
+
+/**
+ @brief Validate a links element.
+ @param class Pointer to the current Class.
+ @param id Id of the current Class.
+ @param links Pointer to the XML node for the links element.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - No elements other than "link" are allowed.
+ - Text is not allowed, other than white space.
+ - Comments are allowed (and ignored).
+
+ Warnings:
+ - There is usually at least one link element.
+*/
+static int val_links( Class* class, const char* id, xmlNodePtr links ) {
+ int rc = 0;
+ int link_found = 0; // boolean
+
+ xmlNodePtr child = links->children;
+ while( child ) {
+ const char* child_name = (char*) child->name;
+ if( xmlNodeIsText( child ) ) {
+ if( ! xmlIsBlankNode( child ) ) {
+ // Found unexpected text. After removing leading and
+ // trailing white space, complain about it.
+ xmlChar* content = xmlNodeGetContent( child );
+
+ xmlChar* begin = content;
+ while( *begin && isspace( *begin ) )
+ ++begin;
+ if( *begin ) {
+ xmlChar* end = begin + strlen( (char*) begin ) - 1;
+ while( (isspace( *end ) ) )
+ --end;
+ end[ 1 ] = '\0';
+ }
+
+ printf( "Unexpected text in <links> element of class \"%s\": \"%s\"\n", id,
+ (char*) begin );
+ xmlFree( content );
+ }
+ } else if( ! strcmp( child_name, "link" ) ) {
+ link_found = 1;
+ if( val_one_link( class, id, child ) )
+ rc = 1;
+ } else if( !strcmp( child_name, "comment" ) )
+ ; // ignore comment
+ else {
+ printf( "Line %ld: Unexpected <%s> element in <link> of class \"%s\"\n",
+ xmlGetLineNo( child ), child_name, id );
+ rc = 1;
+ }
+ child = child->next;
+ }
+
+ if( warn && !link_found ) {
+ printf( "WARNING: No <link> element in class \"%s\"\n", id );
+ }
+
+ return rc;
+}
+
+/**
+ @brief Validate one link element.
+ @param class Pointer to the current Class.
+ @param id Id of the current Class.
+ @param link Pointer to the XML node for the link element.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - The only allowed attributes are "field", "reltype", "key", "map", and "class".
+ - Except for map, every attribute is required.
+ - Except for map, every attribute must have a non-empty value.
+ - The value of the reltype attribute must be one of "has_a", "might_have", or "has_many".
+*/
+static int val_one_link( Class* class, const char* id, xmlNodePtr link ) {
+ int rc = 0;
+ xmlChar* field_name = NULL;
+ Reltype reltype = RT_NONE;
+ xmlChar* key = NULL;
+ xmlChar* classref = NULL;
+
+ // Traverse the attributes
+ xmlAttrPtr attr = link->properties;
+ while( attr ) {
+ const char* attr_name = (const char*) attr->name;
+ if( !strcmp( attr_name, "field" ) ) {
+ field_name = xmlGetProp( link, (xmlChar*) "field" );
+ } else if (!strcmp( attr_name, "reltype" ) ) {
+ ;
+ xmlChar* rt = xmlGetProp( link, (xmlChar*) "reltype" );
+ if( *rt ) {
+ reltype = translate_reltype( rt );
+ if( RT_INVALID == reltype ) {
+ printf(
+ "Line %ld: Invalid value \"%s\" for reltype attribute in class \"%s\"\n",
+ xmlGetLineNo( link ), (char*) rt, id );
+ rc = 1;
+ }
+ } else {
+ printf( "Line %ld: Empty value for reltype attribute in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ }
+ xmlFree( rt );
+ } else if (!strcmp( attr_name, "key" ) ) {
+ key = xmlGetProp( link, (xmlChar*) "key" );
+ } else if (!strcmp( attr_name, "map" ) ) {
+ ; // ignore for now
+ } else if (!strcmp( attr_name, "class" ) ) {
+ classref = xmlGetProp( link, (xmlChar*) "class" );
+ } else {
+ printf( "Line %ld: Unexpected attribute %s in links element of class \"%s\"\n",
+ xmlGetLineNo( link ), attr_name, id );
+ rc = 1;
+ }
+ attr = attr->next;
+ }
+
+ if( !field_name ) {
+ printf( "Line %ld: No field attribute found in <link> in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( '\0' == *field_name ) {
+ printf( "Line %ld: Field name is empty for <link> element in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( !reltype ) {
+ printf( "Line %ld: No reltype attribute found in <link> in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( !key ) {
+ printf( "Line %ld: No key attribute found in <link> in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( '\0' == *key ) {
+ printf( "Line %ld: key attribute is empty for <link> element in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( !classref ) {
+ printf( "Line %ld: No class attribute found in <link> in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else if( '\0' == *classref ) {
+ printf( "Line %ld: class attribute is empty for <link> element in class \"%s\"\n",
+ xmlGetLineNo( link ), id );
+ rc = 1;
+ } else {
+ // Add to Link list
+ Link* new_link = newLink( field_name );
+ new_link->reltype = reltype;
+ new_link->key = key;
+ new_link->classref = classref;
+ if( addLink( class, id, new_link ) )
+ rc = 1;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Translate an attribute value into a Reltype (an enum).
+ @param value The value of a reltype attribute.
+ @return The value of the attribute translated into the enum Reltype.
+*/
+static Reltype translate_reltype( const xmlChar* value ) {
+ const char* val = (char*) value;
+ Reltype reltype;
+
+ if( !val || !*val )
+ reltype = RT_NONE;
+ else if( !strcmp( val, "has_a" ) )
+ reltype = RT_HAS_A;
+ else if( !strcmp( val, "might_have" ) )
+ reltype = RT_MIGHT_HAVE;
+ else if( !strcmp( val, "has_many" ) )
+ reltype = RT_HAS_MANY;
+ else
+ reltype = RT_INVALID;
+
+ return reltype;
+}
+
+/**
+ @brief Build a list of classes, while checking for several errors.
+ @param doc Pointer to the xmlDoc loaded from the IDL.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - Every child element of the root must be of the element "class".
+ - No text is allowed, other than white space, between classes.
+ - Comments are allowed (and ignored) between classes.
+*/
+static int scan_idl( xmlDocPtr doc ) {
+ int rc = 0;
+
+ xmlNodePtr child = xmlDocGetRootElement( doc )->children;
+ while( child ) {
+ char* child_name = (char*) child->name;
+ if( xmlNodeIsText( child ) ) {
+ if( ! xmlIsBlankNode( child ) ) {
+ // Found unexpected text. After removing leading and
+ // trailing white space, complain about it.
+ xmlChar* content = xmlNodeGetContent( child );
+
+ xmlChar* begin = content;
+ while( *begin && isspace( *begin ) )
+ ++begin;
+ if( *begin ) {
+ xmlChar* end = begin + strlen( (char*) begin ) - 1;
+ while( (isspace( *end ) ) )
+ --end;
+ end[ 1 ] = '\0';
+ }
+
+ printf( "Unexpected text between class elements: \"%s\"\n",
+ (char*) begin );
+ xmlFree( content );
+ }
+ } else if( !strcmp( child_name, "class" ) ) {
+ if( register_class( child ) )
+ rc = 1;
+ } else if( !strcmp( child_name, "comment" ) )
+ ; // ignore comment
+ else {
+ printf( "Line %ld: Unexpected <%s> element under root\n",
+ xmlGetLineNo( child ), child_name );
+ rc = 1;
+ }
+
+ child = child->next;
+ }
+ return rc;
+}
+
+/**
+ @brief Register a class.
+ @param class Pointer to the class node.
+ @return 1 if errors found, or 0 if not.
+
+ Rules:
+ - Every class element must have an "id" attribute.
+ - A class id must not be an empty string.
+ - Every class id must be unique.
+
+ Warnings:
+ - A class id normally consists entirely of lower case letters, digits and underscores.
+ - A class id longer than 12 characters is suspiciously long.
+*/
+static int register_class( xmlNodePtr class ) {
+ int rc = 0;
+ xmlChar* id = xmlGetProp( class, (xmlChar*) "id" );
+
+ if( ! id ) {
+ printf( "Line %ld: Class has no \"id\" attribute\n", xmlGetLineNo( class ) );
+ rc = 1;
+ } else if( ! *id ) {
+ printf( "Line %ld: Class id is an empty string\n", xmlGetLineNo( class ) );
+ rc = 1;
+ } else {
+
+ // In principle a class id could contain any arbitrary characters, but in practice
+ // anything but lower case, digits, and underscores is probably a mistake.
+ const xmlChar* p = id;
+ while( *p ) {
+ if( islower( *p ) || isdigit( *p ) || '_' == *p )
+ ++p;
+ else if( warn ) {
+ printf( "Line %ld: WARNING: Dubious class id \"%s\"; not all lower case, "
+ "digits, and underscores\n", xmlGetLineNo( class ), (char*) id );
+ break;
+ }
+ }
+
+ // Warn about a suspiciously long id
+ if( warn && strlen( (char*) id ) > 12 ) {
+ printf( "Line %ld: WARNING: Class id is unusually long: \"%s\"\n",
+ xmlGetLineNo( class ), (char*) id );
+ }
+
+ // Add the classname to the list of classes. If the size of
+ // the list doesn't change, then we must have a duplicate.
+ Class* entry = newClass( class );
+ unsigned long class_count = osrfHashGetCount( classes );
+ osrfHashSet( classes, entry, (char*) id );
+ if( osrfHashGetCount( classes ) == class_count ) {
+ printf( "Line %ld: Duplicate class name \"%s\"\n",
+ xmlGetLineNo( class ), (char*) id );
+ rc = 1;
+ }
+ xmlFree( id );
+ }
+ return rc;
+}
+
+/**
+ @brief Add a field to a class's field list (unless the id collides with an earlier entry).
+ @param class Pointer to the current class.
+ @param id The class id.
+ @param new_field Pointer to the Field to be added.
+ @return 0 if successful, or 1 if not (probably due to a duplicate key).
+
+ If the id collides with a previous entry, we free the new Field instead of adding it
+ to the list. If the label collides with a previous entry, we complain, but we go
+ ahead and add the Field to the list.
+
+ RULES:
+ - Each field name should be unique within the fields element.
+ - Each label should be unique within the fields element.
+*/
+static int addField( Class* class, const char* id, Field* new_field ) {
+ if( ! class || ! new_field )
+ return 1;
+
+ int rc = 0;
+ int dup_name = 0;
+
+ // See if the class has any other fields with the same name or label.
+ const Field* old_field = class->fields;
+ while( old_field ) {
+
+ // Compare the ids
+ if( !strcmp( (char*) old_field->name, (char*) new_field->name ) ) {
+ printf( "Duplicate field name \"%s\" in class \"%s\"\n",
+ (char*) new_field->name, id );
+ dup_name = 1;
+ rc = 1;
+ break;
+ }
+
+ // Compare the labels. if they're both non-empty
+ if( old_field->label && *old_field->label
+ && new_field->label && *new_field->label
+ && !strcmp( (char*) old_field->label, (char*) new_field->label )) {
+ printf( "Duplicate labels \"%s\" in class \"%s\"\n",
+ (char*) old_field->label, id );
+ rc = 1;
+ }
+
+ old_field = old_field->next;
+ }
+
+ if( dup_name ) {
+ free( new_field );
+ } else {
+ new_field->next = class->fields;
+ class->fields = new_field;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Add a Link to the Link list of a specified Class (unless it's a duplicate).
+ @param class Pointer to the Class to whose list to add the Link.
+ @param id Class id.
+ @param new_link Pointer to the Link to be added.
+ @return 0 if successful, or 1 if not (probably due to a duplicate).
+
+ If there's already a Link in the list with the same field name, free the new Link
+ instead of adding it.
+*/
+static int addLink( Class* class, const char* id, Link* new_link ) {
+ if( ! class || ! new_link )
+ return 1;
+
+ int rc = 0;
+ int dup_name = 0;
+
+ // See if the class has any other links with the same field
+ const Link* old_link = class->links;
+ while( old_link ) {
+
+ if( !strcmp( (char*) old_link->field, (char*) new_link->field ) ) {
+ printf( "Duplicate field name \"%s\" in links of class \"%s\"\n",
+ (char*) old_link->field, id );
+ rc = 1;
+ dup_name = 1;
+ break;
+ }
+
+ old_link = old_link->next;
+ }
+
+ if( dup_name ) {
+ freeLink( new_link );
+ } else {
+ // Add to the linked list
+ new_link->next = class->links;
+ class->links = new_link;
+ }
+
+ return rc;
+}
+
+/**
+ @brief Create and initialize a new Class.
+ @param node Pointer to the XML node for a class element.
+ @return Pointer to the newly created Class.
+
+ The calling code is responsible for freeing the Class by calling freeClass(). In practice
+ this happens automagically when we free the osrfHash classes.
+*/
+static Class* newClass( xmlNodePtr node ) {
+ Class* class = safe_malloc( sizeof( Class ) );
+ class->node = node;
+ class->loaded = 0;
+ class->is_virtual = 0;
+ xmlFree( class->primary );
+ class->fields = NULL;
+ class->links = NULL;
+ return class;
+}
+
+/**
+ @brief Free a Class and everything it owns.
+ @param key The class id (not used).
+ @param p A pointer to the Class to be freed, cast to a void pointer.
+
+ This function is designed to be a freeItem callback for an osrfHash.
+*/
+static void freeClass( char* key, void* p ) {
+ Class* class = p;
+
+ // Free the linked list of Fields
+ Field* next_field = NULL;
+ Field* field = class->fields;
+ while( field ) {
+ next_field = field->next;
+ freeField( field );
+ field = next_field;
+ }
+
+ // Free the linked list of Links
+ Link* next_link = NULL;
+ Link* link = class->links;
+ while( link ) {
+ next_link = link->next;
+ freeLink( link );
+ link = next_link;
+ }
+
+ free( class );
+}
+
+/**
+ @brief Allocate and initialize a Field.
+ @param name Field name.
+ @return Pointer to a new Field.
+
+ It is the responsibility of the caller to free the Field by calling freeField().
+*/
+static Field* newField( xmlChar* name ) {
+ Field* field = safe_malloc( sizeof( Field ) );
+ field->next = NULL;
+ field->name = name;
+ field->is_virtual = 0;
+ field->label = NULL;
+ field->datatype = DT_NONE;
+ return field;
+}
+
+/**
+ @brief Free a Field and everything in it.
+ @param field Pointer to the Field to be freed.
+*/
+static void freeField( Field* field ) {
+ if( field ) {
+ xmlFree( field->name );
+ if( field->label )
+ xmlFree( field->label );
+ free( field );
+ }
+}
+
+/**
+ @brief Allocate and initialize a Link.
+ @param field Field name.
+ @return Pointer to a new Link.
+
+ It is the responsibility of the caller to free the Link by calling freeLink().
+*/
+static Link* newLink( xmlChar* field ) {
+ Link* link = safe_malloc( sizeof( Link ) );
+ link->next = NULL;
+ link->field = field;
+ link->reltype = RT_NONE;
+ link->key = NULL;
+ link->classref = NULL;
+ return link;
+}
+
+/**
+ @brief Free a Link and everything it owns.
+ @param link Pointer to the Link to be freed.
+*/
+static void freeLink( Link* link ) {
+ if( link ) {
+ xmlFree( link->field );
+ xmlFree( link->key );
+ xmlFree( link->classref );
+ free( link );
+ }
+}
--- /dev/null
+/**
+ @file oils_cstore.c
+ @brief As a server, perform database operations at the request of clients.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static const int enforce_pcrud = 0; // Boolean
+static const char modulename[] = "open-ils.cstore";
+
+/**
+ @brief Disconnect from the database.
+
+ This function is called when the server drone is about to terminate.
+*/
+void osrfAppChildExit( void ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Child is exiting, disconnecting from database...");
+
+ int same = 0;
+ if (writehandle == dbhandle)
+ same = 1;
+
+ if (writehandle) {
+ dbi_conn_query(writehandle, "ROLLBACK;");
+ dbi_conn_close(writehandle);
+ writehandle = NULL;
+ }
+ if (dbhandle && !same)
+ dbi_conn_close(dbhandle);
+
+ // XXX add cleanup of readHandles whenever that gets used
+
+ return;
+}
+
+/**
+ @brief Initialize the application.
+ @return Zero if successful, or non-zero if not.
+
+ Load the IDL file into an internal data structure for future reference. Each non-virtual
+ class in the IDL corresponds to a table or view in the database, or to a subquery defined
+ in the IDL. Ignore all virtual tables and virtual fields.
+
+ Register a number of methods, some of them general-purpose and others specific for
+ particular classes.
+
+ The name of the application is given by the MODULENAME macro, whose value depends on
+ conditional compilation. The method names also incorporate MODULENAME, followed by a
+ dot, as a prefix.
+
+ The general-purpose methods are as follows (minus their MODULENAME prefixes):
+
+ - json_query
+ - transaction.begin
+ - transaction.commit
+ - transaction.rollback
+ - savepoint.set
+ - savepoint.release
+ - savepoint.rollback
+ - set_audit_info
+
+ For each non-virtual class, create up to eight class-specific methods:
+
+ - create (not for readonly classes)
+ - retrieve
+ - update (not for readonly classes)
+ - delete (not for readonly classes
+ - search (atomic and non-atomic versions)
+ - id_list (atomic and non-atomic versions)
+
+ The full method names follow the pattern "MODULENAME.direct.XXX.method_type", where XXX
+ is the fieldmapper name from the IDL, with every run of one or more consecutive colons
+ replaced by a period. In addition, the names of atomic methods have a suffix of ".atomic".
+
+ This function is called when the registering the application, and is executed by the
+ listener before spawning the drones.
+*/
+int osrfAppInitialize( void ) {
+
+ osrfLogInfo(OSRF_LOG_MARK, "Initializing the CStore Server...");
+ osrfLogInfo(OSRF_LOG_MARK, "Finding XML file...");
+
+ // Load the IDL into memory
+ if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
+ return 1; /* return non-zero to indicate error */
+
+ // Open the database temporarily. Look up the datatypes of all
+ // the non-virtual fields and record them with the IDL data.
+ dbi_conn handle = oilsConnectDB( modulename );
+ if( !handle )
+ return -1;
+ else if( oilsExtendIDL( handle )) {
+ osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
+ return -1;
+ }
+ dbi_conn_close( handle );
+
+ // Get the maximum flesh depth from the settings
+ char* md = osrf_settings_host_value(
+ "/apps/%s/app_settings/max_query_recursion", modulename );
+ int max_flesh_depth = 100;
+ if( md )
+ max_flesh_depth = atoi( md );
+ if( max_flesh_depth < 0 )
+ max_flesh_depth = 1;
+ else if( max_flesh_depth > 1000 )
+ max_flesh_depth = 1000;
+
+ oilsSetSQLOptions( modulename, enforce_pcrud, max_flesh_depth );
+
+ // Now register all the methods
+ growing_buffer* method_name = buffer_init(64);
+
+ // Generic search thingy
+ buffer_add( method_name, modulename );
+ buffer_add( method_name, ".json_query" );
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+ "doJSONSearch", "", 1, OSRF_METHOD_STREAMING );
+
+ // Next we register all the transaction and savepoint methods
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.begin");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+ "beginTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.commit");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "commitTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.rollback");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "rollbackTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.set");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "setSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.release");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "releaseSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.rollback");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "rollbackSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".set_audit_info");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "setAuditInfo", "", 3, 0 );
+
+ static const char* global_method[] = {
+ "create",
+ "retrieve",
+ "update",
+ "delete",
+ "search",
+ "id_list"
+ };
+ const int global_method_count
+ = sizeof( global_method ) / sizeof ( global_method[0] );
+
+ unsigned long class_count = osrfHashGetCount( oilsIDL() );
+ osrfLogDebug(OSRF_LOG_MARK, "%lu classes loaded", class_count );
+ osrfLogDebug(OSRF_LOG_MARK,
+ "At most %lu methods will be generated",
+ (unsigned long) (class_count * global_method_count) );
+
+ osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+ osrfHash* idlClass = NULL;
+
+ // For each class in the IDL...
+ while( (idlClass = osrfHashIteratorNext( class_itr ) ) ) {
+
+ const char* classname = osrfHashIteratorKey( class_itr );
+ osrfLogInfo(OSRF_LOG_MARK, "Generating class methods for %s", classname);
+
+ if (!osrfStringArrayContains( osrfHashGet(idlClass, "controller"), modulename )) {
+ osrfLogInfo(OSRF_LOG_MARK, "%s is not listed as a controller for %s, moving on",
+ modulename, classname);
+ continue;
+ }
+
+ if ( str_is_true( osrfHashGet(idlClass, "virtual") ) ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+ continue;
+ }
+
+ // Look up some other attributes of the current class
+ const char* idlClass_fieldmapper = osrfHashGet(idlClass, "fieldmapper");
+ if( !idlClass_fieldmapper ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Skipping class \"%s\"; no fieldmapper in IDL",
+ classname );
+ continue;
+ }
+
+ const char* readonly = osrfHashGet(idlClass, "readonly");
+
+ int i;
+ for( i = 0; i < global_method_count; ++i ) { // for each global method
+ const char* method_type = global_method[ i ];
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Using files to build %s class methods for %s", method_type, classname);
+
+ // No create, update, or delete methods for a readonly class
+ if ( str_is_true( readonly )
+ && ( *method_type == 'c' || *method_type == 'u' || *method_type == 'd') )
+ continue;
+
+ buffer_reset( method_name );
+
+ // Build the method name: MODULENAME.MODULENAME.direct.XXX.method_type
+ // where XXX is the fieldmapper name from the IDL, with every run of
+ // one or more consecutive colons replaced by a period.
+ char* st_tmp = NULL;
+ char* part = NULL;
+ char* _fm = strdup( idlClass_fieldmapper );
+ part = strtok_r(_fm, ":", &st_tmp);
+
+ buffer_fadd(method_name, "%s.direct.%s", modulename, part);
+
+ while ((part = strtok_r(NULL, ":", &st_tmp))) {
+ OSRF_BUFFER_ADD_CHAR(method_name, '.');
+ OSRF_BUFFER_ADD(method_name, part);
+ }
+ OSRF_BUFFER_ADD_CHAR(method_name, '.');
+ OSRF_BUFFER_ADD(method_name, method_type);
+ free(_fm);
+
+ // For an id_list or search method we specify the OSRF_METHOD_STREAMING option.
+ // The consequence is that we implicitly create an atomic method in addition to
+ // the usual non-atomic method.
+ int flags = 0;
+ if (*method_type == 'i' || *method_type == 's') { // id_list or search
+ flags = flags | OSRF_METHOD_STREAMING;
+ }
+
+ osrfHash* method_meta = osrfNewHash();
+ osrfHashSet( method_meta, idlClass, "class");
+ osrfHashSet( method_meta, buffer_data( method_name ), "methodname" );
+ osrfHashSet( method_meta, strdup(method_type), "methodtype" );
+
+ // Register the method, with a pointer to an osrfHash to tell the method
+ // its name, type, and class.
+ osrfAppRegisterExtendedMethod(
+ modulename,
+ OSRF_BUFFER_C_STR( method_name ),
+ "dispatchCRUDMethod",
+ "",
+ 1,
+ flags,
+ (void*)method_meta
+ );
+
+ } // end for each global method
+ } // end for each class in IDL
+
+ buffer_free( method_name );
+ osrfHashIteratorFree( class_itr );
+
+ return 0;
+}
+
+/**
+ @brief Initialize a server drone.
+ @return Zero if successful, -1 if not.
+
+ Connect to the database.
+
+ This function is called by a server drone shortly after it is spawned by the listener.
+*/
+int osrfAppChildInit( void ) {
+
+ writehandle = oilsConnectDB( modulename );
+ if( !writehandle )
+ return -1;
+
+ oilsSetDBConnection( writehandle );
+
+ return 0;
+}
+
+/**
+ @brief Implement the class-specific methods.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Branch on the method type: create, retrieve, update, delete, search, or id_list.
+
+ The method parameters and the type of value returned to the client depend on the method
+ type.
+*/
+int dispatchCRUDMethod( osrfMethodContext* ctx ) {
+
+ // Get the method type, then can branch on it
+ osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+ const char* methodtype = osrfHashGet( method_meta, "methodtype" );
+
+ if( !strcmp( methodtype, "create" ))
+ return doCreate( ctx );
+ else if( !strcmp(methodtype, "retrieve" ))
+ return doRetrieve( ctx );
+ else if( !strcmp(methodtype, "update" ))
+ return doUpdate( ctx );
+ else if( !strcmp(methodtype, "delete" ))
+ return doDelete( ctx );
+ else if( !strcmp(methodtype, "search" ))
+ return doSearch( ctx );
+ else if( !strcmp(methodtype, "id_list" ))
+ return doIdList( ctx );
+ else {
+ osrfAppRespondComplete( ctx, NULL ); // should be unreachable...
+ return 0;
+ }
+}
--- /dev/null
+#include "openils/oils_event.h"
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include "opensrf/osrf_settings.h"
+
+const char default_lang[] = "en-US";
+
+static void _oilsEventParseEvents();
+static const char* lookup_desc( const char* lang, const char* code );
+
+// The following two osrfHashes are created when we
+// create the first osrfEvent, and are never freed.
+
+/**
+ @brief Lookup store mapping event names to event numbers.
+
+ - Key: textcode from the events config file.
+ - Data: numeric code (as a string) from the events config file.
+*/
+static osrfHash* _oilsEventEvents = NULL;
+
+/**
+ @brief Lookup store mapping event numbers to descriptive text.
+
+ - Key: numeric code (as a string) of the event.
+ - Data: another layer of lookup, as follows:
+ - Key: numeric code (as a string) of the event.
+ - Data: text message describing the event.
+*/
+static osrfHash* _oilsEventDescriptions = NULL;
+
+/**
+ @brief Allocate and initialize a new oilsEvent.
+ @param file The name of the source file where oilsNewEvent is called.
+ @param line The line number in the source code where oilsNewEvent is called.
+ @param event A name or label for the event.
+ @return Pointer to the newly allocated oilsEvent.
+
+ The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+ The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent( const char* file, int line, const char* event ) {
+ if(!event) return NULL;
+
+ osrfLogInfo(OSRF_LOG_MARK, "Creating new event: %s", event);
+
+ if(!_oilsEventEvents)
+ _oilsEventParseEvents();
+
+ oilsEvent* evt = safe_malloc( sizeof(oilsEvent) );
+ evt->event = strdup(event);
+ evt->perm = NULL;
+ evt->permloc = -1;
+ evt->payload = NULL;
+ evt->json = NULL;
+
+ if(file)
+ evt->file = strdup(file);
+ else
+ evt->file = strdup( "" );
+
+ evt->line = line;
+ return evt;
+}
+
+/**
+ @brief Allocate and initialize a new oilsEvent with a payload.
+ @param file The name of the source file where oilsNewEvent is called.
+ @param line The line number in the source code where oilsNewEvent is called.
+ @param event A name or label for the event.
+ @param payload The payload, of which a copy will be incorporated into the oilsEvent.
+ @return Pointer to the newly allocated oilsEvent.
+
+ The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+ The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent2( const char* file, int line, const char* event,
+ const jsonObject* payload ) {
+ oilsEvent* evt = oilsNewEvent(file, line, event);
+
+ if(payload)
+ evt->payload = jsonObjectClone(payload);
+
+ return evt;
+}
+
+/**
+ @brief Create a new oilsEvent with a permission and a permission location.
+ @param file The name of the source file where oilsNewEvent is called.
+ @param line The line number in the source code where oilsNewEvent is called.
+ @param event A name or label for the event.
+ @param perm The permission name.
+ @param permloc The permission location.
+ @return Pointer to the newly allocated oilsEvent.
+
+ The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+ The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent3( const char* file, int line, const char* event,
+ const char* perm, int permloc ) {
+ oilsEvent* evt = oilsNewEvent(file, line, event);
+ if(perm) {
+ evt->perm = strdup(perm);
+ evt->permloc = permloc;
+ }
+ return evt;
+}
+
+/**
+ @brief Create a new oilsEvent with a permission and a permission location.
+ @param file The name of the source file where oilsNewEvent is called.
+ @param line The line number in the source code where oilsNewEvent is called.
+ @param event A name or label for the event.
+ @param perm The permission name.
+ @param permloc The permission location.
+ @param payload Pointer to the payload.
+ @return Pointer to the newly allocated oilsEvent.
+
+ The first two parameters are normally passed as the OSRF_LOG_MARK macro.
+
+ The calling code is responsible for freeing the oilsEvent by calling oilsEventFree().
+*/
+oilsEvent* oilsNewEvent4( const char* file, int line, const char* event,
+ const char* perm, int permloc, const jsonObject* payload ) {
+ oilsEvent* evt = oilsNewEvent3( file, line, event, perm, permloc );
+
+ if(payload)
+ evt->payload = jsonObjectClone(payload);
+
+ return evt;
+}
+
+/**
+ @brief Set the permission and permission location of an oilsEvent.
+ @param event Pointer the oilsEvent whose permission and permission location are to be set.
+ @param perm The permission name.
+ @param permloc The permission location.
+*/
+void oilsEventSetPermission( oilsEvent* event, const char* perm, int permloc ) {
+ if(!(event && perm)) return;
+
+ if(event->perm)
+ free(event->perm);
+
+ event->perm = strdup(perm);
+ event->permloc = permloc;
+}
+
+/**
+ @brief Install a payload in an oilsEvent.
+ @param event The oilsEvent in which the payload is to be installed.
+ @param payload The payload, a copy of which will be installed in the oilsEvent.
+
+ If @a payload is NULL, install a JSON_NULL as the payload.
+*/
+void oilsEventSetPayload( oilsEvent* event, const jsonObject* payload ) {
+ if(!(event && payload)) return;
+
+ if(event->payload)
+ jsonObjectFree(event->payload);
+
+ event->payload = jsonObjectClone(payload);
+}
+
+/**
+ @brief Free an OilsEvent.
+ @param event Pointer to the oilsEvent to be freed.
+*/
+void oilsEventFree( oilsEvent* event ) {
+ if(!event) return;
+ free(event->event);
+ free(event->perm);
+ free(event->file);
+
+ // If present, the jsonObject to which event->json will include a pointer to
+ // event->payload. Hence we must avoid trying to free the payload twice.
+ if(event->json)
+ jsonObjectFree(event->json);
+ else
+ jsonObjectFree(event->payload);
+
+ free(event);
+}
+
+/**
+ @brief Package the contents of an oilsEvent into a jsonObject.
+ @param event Pointer to the oilsEvent whose contents are to be packaged.
+ @return Pointer to the newly created jsonObject if successful, or NULL if not.
+
+ The jsonObject will include a textual description of the event, as loaded from the
+ events file. Although the events file may include text in multiple languages,
+ oilsEventToJSON() uses only those marked as "en-US".
+
+ A pointer to the resulting jsonObject will be stored in the oilsEvent. Hence the calling
+ code should @em not free the returned jsonObject directly. It will be freed by
+ oilsEventFree().
+*/
+jsonObject* oilsEventToJSON( oilsEvent* event ) {
+ if(!event) return NULL;
+
+ char* code = osrfHashGet( _oilsEventEvents, event->event );
+ if(!code) {
+ osrfLogError(OSRF_LOG_MARK, "No such event name: %s", event->event );
+ return NULL;
+ }
+
+ // Look up the text message corresponding the code, preferably in the right language.
+ const char* lang = osrf_message_get_last_locale();
+ const char* desc = lookup_desc( lang, code );
+ if( !desc && strcmp( lang, default_lang ) ) // No luck?
+ desc = lookup_desc( default_lang, code ); // Try the default language
+
+ if( !desc )
+ desc = ""; // Not found? Default to an empty string.
+
+ jsonObject* json = jsonNewObject(NULL);
+ jsonObjectSetKey( json, "ilsevent", jsonNewNumberObject(atoi(code)) );
+ jsonObjectSetKey( json, "textcode", jsonNewObject(event->event) );
+ jsonObjectSetKey( json, "desc", jsonNewObject(desc) );
+ jsonObjectSetKey( json, "pid", jsonNewNumberObject(getpid()) );
+
+ char buf[256] = "";
+ snprintf(buf, sizeof(buf), "%s:%d", event->file, event->line);
+ jsonObjectSetKey( json, "stacktrace", jsonNewObject(buf) );
+
+ if(event->perm)
+ jsonObjectSetKey( json, "ilsperm", jsonNewObject(event->perm) );
+
+ if(event->permloc != -1)
+ jsonObjectSetKey( json, "ilspermloc", jsonNewNumberObject(event->permloc) );
+
+ if(event->payload)
+ jsonObjectSetKey( json, "payload", event->payload );
+
+ if(event->json)
+ jsonObjectFree(event->json);
+
+ event->json = json;
+ return json;
+}
+
+/**
+ @brief Lookup up the descriptive text, in a given language, for a given event code.
+ @param lang The language (a.k.a. locale) of the desired message.
+ @param code The numeric code for the event, as a string.
+ return The corresponding descriptive text if found, or NULL if not.
+
+ The lookup has two stages. First we look up the language, and then within that
+ language we look up the code.
+*/
+static const char* lookup_desc( const char* lang, const char* code ) {
+ // Search for the right language
+ const char* desc = NULL;
+ osrfHash* lang_hash = osrfHashGet( _oilsEventDescriptions, lang );
+ if( lang_hash ) {
+ // Within that language, search for the right message
+ osrfLogDebug( OSRF_LOG_MARK, "Loaded event lang hash for %s", lang );
+ desc = osrfHashGet( lang_hash, code );
+ }
+
+ if( desc )
+ osrfLogDebug( OSRF_LOG_MARK, "Found event description %s", desc );
+ else
+ osrfLogDebug( OSRF_LOG_MARK, "Event description not found for code %s", code );
+
+ return desc;
+}
+
+/**
+ @brief Parse and load the events file.
+
+ Get the name of the events file from previously loaded settings. Open it and load
+ it into an xmlDoc. Based on the contents of the xmlDoc, build two osrfHashes: one to
+ map event names to event numbers, and another to map event numbers to descriptive
+ text messages (actually one such hash for each supported language).
+*/
+static void _oilsEventParseEvents() {
+
+ char* xml = osrf_settings_host_value("/ils_events");
+
+ if(!xml) {
+ osrfLogError(OSRF_LOG_MARK, "Unable to find ILS Events file: %s", xml);
+ return;
+ }
+
+ xmlDocPtr doc = xmlParseFile(xml);
+ free(xml);
+ int success = 0;
+ _oilsEventEvents = osrfNewHash();
+ _oilsEventDescriptions = osrfNewHash();
+
+ if( doc ) {
+ xmlNodePtr root = xmlDocGetRootElement(doc);
+ if( root ) {
+ xmlNodePtr child = root->children;
+ while( child ) {
+ if( !strcmp((char*) child->name, "event") ) {
+ xmlChar* code = xmlGetProp( child, BAD_CAST "code");
+ xmlChar* textcode = xmlGetProp( child, BAD_CAST "textcode");
+ if( code && textcode ) {
+ osrfHashSet( _oilsEventEvents, code, (char*) textcode );
+ success = 1;
+ }
+
+ /* here we collect all of the <desc> nodes on the event
+ * element and store them based on the xml:lang attribute
+ */
+ xmlNodePtr desc = child->children;
+ while(desc) {
+ if( !strcmp((char*) desc->name, "desc") ) {
+ xmlChar* lang = xmlGetProp( desc, BAD_CAST "lang");
+ if(lang) {
+ osrfLogInternal( OSRF_LOG_MARK,
+ "Loaded event lang: %s", (char*) lang );
+ osrfHash* langHash = osrfHashGet(
+ _oilsEventDescriptions, (char*) lang);
+ if(!langHash) {
+ langHash = osrfNewHash();
+ osrfHashSet(_oilsEventDescriptions, langHash, (char*) lang);
+ }
+ char* content;
+ if( desc->children
+ && (content = (char*) desc->children->content) ) {
+ osrfLogInternal( OSRF_LOG_MARK,
+ "Loaded event desc: %s", content);
+ osrfHashSet( langHash, content, (char*) code );
+ }
+ }
+ }
+ desc = desc->next;
+ }
+ }
+ child = child->next;
+ }
+ }
+ }
+
+ if(!success)
+ osrfLogError(OSRF_LOG_MARK, " ! Unable to parse events file: %s", xml );
+}
--- /dev/null
+#include "openils/oils_idl.h"
+/*
+ * vim:noet:ts=4:
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <libxml/globals.h>
+#include <libxml/xmlerror.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/debugXML.h>
+#include <libxml/xmlmemory.h>
+
+#define PERSIST_NS "http://open-ils.org/spec/opensrf/IDL/persistence/v1"
+#define OBJECT_NS "http://open-ils.org/spec/opensrf/IDL/objects/v1"
+#define BASE_NS "http://opensrf.org/spec/IDL/base/v1"
+#define REPORTER_NS "http://open-ils.org/spec/opensrf/IDL/reporter/v1"
+#define PERM_NS "http://open-ils.org/spec/opensrf/IDL/permacrud/v1"
+
+static xmlDocPtr idlDoc = NULL; // parse and store the IDL here
+
+/* parse and store the IDL here */
+static osrfHash* idlHash;
+
+static void add_std_fld( osrfHash* fields_hash, const char* field_name, unsigned pos );
+osrfHash* oilsIDL(void) { return idlHash; }
+osrfHash* oilsIDLInit( const char* idl_filename ) {
+
+ if (idlHash) return idlHash;
+
+ char* prop_str = NULL;
+
+ idlHash = osrfNewHash();
+ osrfHash* class_def_hash = NULL;
+
+ osrfLogInfo(OSRF_LOG_MARK, "Parsing the IDL XML...");
+ idlDoc = xmlReadFile( idl_filename, NULL, XML_PARSE_XINCLUDE );
+
+ if (!idlDoc) {
+ osrfLogError(OSRF_LOG_MARK, "Could not load or parse the IDL XML file!");
+ return NULL;
+ }
+
+ osrfLogDebug(OSRF_LOG_MARK, "Initializing the Fieldmapper IDL...");
+
+ xmlNodePtr docRoot = xmlDocGetRootElement(idlDoc);
+ xmlNodePtr kid = docRoot->children;
+ while (kid) {
+ if (!strcmp( (char*)kid->name, "class" )) {
+
+ class_def_hash = osrfNewHash();
+ char* current_class_name = (char*) xmlGetProp(kid, BAD_CAST "id");
+
+ osrfHashSet( class_def_hash, current_class_name, "classname" );
+ osrfHashSet( class_def_hash, xmlGetNsProp(kid, BAD_CAST "fieldmapper", BAD_CAST OBJECT_NS), "fieldmapper" );
+ osrfHashSet( class_def_hash, xmlGetNsProp(kid, BAD_CAST "readonly", BAD_CAST PERSIST_NS), "readonly" );
+
+ osrfHashSet( idlHash, class_def_hash, current_class_name );
+
+ if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "tablename", BAD_CAST PERSIST_NS))) {
+ osrfLogDebug(OSRF_LOG_MARK, "Using table '%s' for class %s", prop_str, current_class_name );
+ osrfHashSet(
+ class_def_hash,
+ prop_str,
+ "tablename"
+ );
+ }
+
+ if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "restrict_primary", BAD_CAST PERSIST_NS))) {
+ osrfLogDebug(OSRF_LOG_MARK, "Delete restriction policy set at '%s' for pkey of class %s", prop_str, current_class_name );
+ osrfHashSet(
+ class_def_hash,
+ prop_str,
+ "restrict_primary"
+ );
+ }
+
+ if ((prop_str = (char*)xmlGetNsProp(kid, BAD_CAST "virtual", BAD_CAST PERSIST_NS))) {
+ osrfHashSet(
+ class_def_hash,
+ prop_str,
+ "virtual"
+ );
+ }
+
+ // Tokenize controller attribute into an osrfStringArray
+ prop_str = (char*) xmlGetProp(kid, BAD_CAST "controller");
+ if( prop_str )
+ osrfLogDebug(OSRF_LOG_MARK, "Controller list is %s", prop_str );
+ osrfStringArray* controller = osrfStringArrayTokenize( prop_str, ' ' );
+ xmlFree( prop_str );
+ osrfHashSet( class_def_hash, controller, "controller");
+
+ osrfHash* current_links_hash = osrfNewHash();
+ osrfHash* current_fields_hash = osrfNewHash();
+
+ osrfHashSet( class_def_hash, current_fields_hash, "fields" );
+ osrfHashSet( class_def_hash, current_links_hash, "links" );
+
+ xmlNodePtr _cur = kid->children;
+
+ while (_cur) {
+
+ if (!strcmp( (char*)_cur->name, "fields" )) {
+
+ if( (prop_str = (char*)xmlGetNsProp(_cur, BAD_CAST "primary", BAD_CAST PERSIST_NS)) ) {
+ osrfHashSet(
+ class_def_hash,
+ prop_str,
+ "primarykey"
+ );
+ }
+
+ if( (prop_str = (char*)xmlGetNsProp(_cur, BAD_CAST "sequence", BAD_CAST PERSIST_NS)) ) {
+ osrfHashSet(
+ class_def_hash,
+ prop_str,
+ "sequence"
+ );
+ }
+
+ unsigned int array_pos = 0;
+ char array_pos_buf[ 7 ]; // For up to 1,000,000 fields per class
+
+ xmlNodePtr _f = _cur->children;
+ while(_f) {
+ if (strcmp( (char*)_f->name, "field" )) {
+ _f = _f->next;
+ continue;
+ }
+
+ // Get the field name. If it's one of the three standard
+ // fields that we always generate, ignore it.
+ char* field_name = (char*)xmlGetProp(_f, BAD_CAST "name");
+ if( field_name ) {
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Found field %s for class %s", field_name, current_class_name );
+ if( !strcmp( field_name, "isnew" )
+ || !strcmp( field_name, "ischanged" )
+ || !strcmp( field_name, "isdeleted" ) ) {
+ free( field_name );
+ _f = _f->next;
+ continue;
+ }
+ } else {
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Found field with no name for class %s", current_class_name );
+ _f = _f->next;
+ continue;
+ }
+
+ osrfHash* field_def_hash = osrfNewHash();
+
+ // Insert array_position
+ snprintf( array_pos_buf, sizeof( array_pos_buf ), "%u", array_pos++ );
+ osrfHashSet( field_def_hash, strdup( array_pos_buf ), "array_position" );
+
+ // Tokenize suppress_controller attribute into an osrfStringArray
+ if( (prop_str = (char*)xmlGetProp(_f, BAD_CAST "suppress_controller")) ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Controller suppression list is %s", prop_str );
+ osrfStringArray* controller = osrfStringArrayTokenize( prop_str, ' ' );
+ osrfHashSet( field_def_hash, controller, "suppress_controller");
+ }
+
+ if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "i18n", BAD_CAST PERSIST_NS)) ) {
+ osrfHashSet(
+ field_def_hash,
+ prop_str,
+ "i18n"
+ );
+ }
+
+ if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "virtual", BAD_CAST PERSIST_NS)) ) {
+ osrfHashSet(
+ field_def_hash,
+ prop_str,
+ "virtual"
+ );
+ } else { // default to virtual
+ osrfHashSet(
+ field_def_hash,
+ "false",
+ "virtual"
+ );
+ }
+
+ if( (prop_str = (char*)xmlGetNsProp(_f, BAD_CAST "primitive", BAD_CAST PERSIST_NS)) ) {
+ osrfHashSet(
+ field_def_hash,
+ prop_str,
+ "primitive"
+ );
+ }
+
+ osrfHashSet( field_def_hash, field_name, "name" );
+ osrfHashSet(
+ current_fields_hash,
+ field_def_hash,
+ field_name
+ );
+ _f = _f->next;
+ }
+
+ // Create three standard, stereotyped virtual fields for every class
+ add_std_fld( current_fields_hash, "isnew", array_pos++ );
+ add_std_fld( current_fields_hash, "ischanged", array_pos++ );
+ add_std_fld( current_fields_hash, "isdeleted", array_pos );
+
+ }
+
+ if (!strcmp( (char*)_cur->name, "links" )) {
+ xmlNodePtr _l = _cur->children;
+
+ while(_l) {
+ if (strcmp( (char*)_l->name, "link" )) {
+ _l = _l->next;
+ continue;
+ }
+
+ osrfHash* link_def_hash = osrfNewHash();
+
+ if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "reltype")) ) {
+ osrfHashSet(
+ link_def_hash,
+ prop_str,
+ "reltype"
+ );
+ osrfLogDebug(OSRF_LOG_MARK, "Adding link with reltype %s", prop_str );
+ } else
+ osrfLogDebug(OSRF_LOG_MARK, "Adding link with no reltype" );
+
+ if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "key")) ) {
+ osrfHashSet(
+ link_def_hash,
+ prop_str,
+ "key"
+ );
+ osrfLogDebug(OSRF_LOG_MARK, "Link fkey is %s", prop_str );
+ } else
+ osrfLogDebug(OSRF_LOG_MARK, "Link with no fkey" );
+
+ if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "class")) ) {
+ osrfHashSet(
+ link_def_hash,
+ prop_str,
+ "class"
+ );
+ osrfLogDebug(OSRF_LOG_MARK, "Link fclass is %s", prop_str );
+ } else
+ osrfLogDebug(OSRF_LOG_MARK, "Link with no fclass" );
+
+ // Tokenize map attribute into an osrfStringArray
+ prop_str = (char*) xmlGetProp(_l, BAD_CAST "map");
+ if( prop_str )
+ osrfLogDebug(OSRF_LOG_MARK, "Link mapping list is %s", prop_str );
+ osrfStringArray* map = osrfStringArrayTokenize( prop_str, ' ' );
+ osrfHashSet( link_def_hash, map, "map");
+ xmlFree( prop_str );
+
+ if( (prop_str = (char*)xmlGetProp(_l, BAD_CAST "field")) ) {
+ osrfHashSet(
+ link_def_hash,
+ prop_str,
+ "field"
+ );
+ osrfLogDebug(OSRF_LOG_MARK, "Link fclass is %s", prop_str );
+ } else
+ osrfLogDebug(OSRF_LOG_MARK, "Link with no fclass" );
+
+ osrfHashSet(
+ current_links_hash,
+ link_def_hash,
+ prop_str
+ );
+
+ _l = _l->next;
+ }
+ }
+/**** Structure of permacrud in memory ****
+
+{ create :
+ { permission : [ x, y, z ],
+ global_required : "true", -- anything else, or missing, is false
+ local_context : [ f1, f2 ],
+ foreign_context : { class1 : { fkey : local_class_key, field : class1_field, context : [ a, b, c ] }, ...}
+ },
+ retrieve : null, -- no perm check, or structure similar to the others
+ update : -- like create
+ ...
+ delete : -- like create
+ ...
+}
+
+**** Structure of permacrud in memory ****/
+
+ if (!strcmp( (char*)_cur->name, "permacrud" )) {
+ osrfHash* pcrud = osrfNewHash();
+ osrfHashSet( class_def_hash, pcrud, "permacrud" );
+ xmlNodePtr _l = _cur->children;
+
+ while(_l) {
+ if (strcmp( (char*)_l->name, "actions" )) {
+ _l = _l->next;
+ continue;
+ }
+
+ xmlNodePtr _a = _l->children;
+
+ while(_a) {
+ const char* action_name = (const char*) _a->name;
+ if (
+ strcmp( action_name, "create" ) &&
+ strcmp( action_name, "retrieve" ) &&
+ strcmp( action_name, "update" ) &&
+ strcmp( action_name, "delete" )
+ ) {
+ _a = _a->next;
+ continue;
+ }
+
+ osrfLogDebug(OSRF_LOG_MARK, "Found Permacrud action %s for class %s",
+ action_name, current_class_name );
+
+ osrfHash* action_def_hash = osrfNewHash();
+ osrfHashSet( pcrud, action_def_hash, action_name );
+
+ // Tokenize permission attribute into an osrfStringArray
+ prop_str = (char*) xmlGetProp(_a, BAD_CAST "permission");
+ if( prop_str )
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Permacrud permission list is %s", prop_str );
+ osrfStringArray* map = osrfStringArrayTokenize( prop_str, ' ' );
+ osrfHashSet( action_def_hash, map, "permission");
+ xmlFree( prop_str );
+
+ osrfHashSet( action_def_hash,
+ (char*)xmlGetNoNsProp(_a, BAD_CAST "global_required"), "global_required");
+
+ // Tokenize context_field attribute into an osrfStringArray
+ prop_str = (char*) xmlGetProp(_a, BAD_CAST "context_field");
+ if( prop_str )
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Permacrud context_field list is %s", prop_str );
+ map = osrfStringArrayTokenize( prop_str, ' ' );
+ osrfHashSet( action_def_hash, map, "local_context");
+ xmlFree( prop_str );
+
+ osrfHash* foreign_context = osrfNewHash();
+ osrfHashSet( action_def_hash, foreign_context, "foreign_context");
+
+ xmlNodePtr _f = _a->children;
+
+ while(_f) {
+ if ( strcmp( (char*)_f->name, "context" ) ) {
+ _f = _f->next;
+ continue;
+ }
+
+ if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "link")) ) {
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Permacrud context link definition is %s", prop_str );
+
+ osrfHash* _tmp_fcontext = osrfNewHash();
+
+ // Store pointers to elements already stored
+ // from the <link> aggregate
+ osrfHash* _flink = osrfHashGet( current_links_hash, prop_str );
+ osrfHashSet( _tmp_fcontext, osrfHashGet(_flink, "field"), "fkey" );
+ osrfHashSet( _tmp_fcontext, osrfHashGet(_flink, "key"), "field" );
+ xmlFree( prop_str );
+
+ if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "jump")) )
+ osrfHashSet( _tmp_fcontext, osrfStringArrayTokenize( prop_str, '.' ), "jump" );
+ xmlFree( prop_str );
+
+ // Tokenize field attribute into an osrfStringArray
+ char * field_list = (char*) xmlGetProp(_f, BAD_CAST "field");
+ if( field_list )
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Permacrud foreign context field list is %s", field_list );
+ map = osrfStringArrayTokenize( field_list, ' ' );
+ osrfHashSet( _tmp_fcontext, map, "context");
+ xmlFree( field_list );
+
+ // Insert the new hash into a hash attached to the parent node
+ osrfHashSet( foreign_context, _tmp_fcontext, osrfHashGet( _flink, "class" ) );
+
+ } else {
+
+ if( (prop_str = (char*)xmlGetNoNsProp(_f, BAD_CAST "field") )) {
+ char* map_list = prop_str;
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Permacrud local context field list is %s", prop_str );
+
+ if (strlen( map_list ) > 0) {
+ char* st_tmp = NULL;
+ char* _map_class = strtok_r(map_list, " ", &st_tmp);
+ osrfStringArrayAdd(
+ osrfHashGet( action_def_hash, "local_context"), _map_class);
+
+ while ((_map_class = strtok_r(NULL, " ", &st_tmp))) {
+ osrfStringArrayAdd(
+ osrfHashGet( action_def_hash, "local_context"), _map_class);
+ }
+ }
+ xmlFree(map_list);
+ }
+
+ }
+ _f = _f->next;
+ }
+ _a = _a->next;
+ }
+ _l = _l->next;
+ }
+ }
+
+ if (!strcmp( (char*)_cur->name, "source_definition" )) {
+ char* content_str;
+ if( (content_str = (char*)xmlNodeGetContent(_cur)) ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Using source definition '%s' for class %s",
+ content_str, current_class_name );
+ osrfHashSet(
+ class_def_hash,
+ content_str,
+ "source_definition"
+ );
+ }
+
+ }
+
+ _cur = _cur->next;
+ } // end while
+ }
+
+ kid = kid->next;
+ } // end while
+
+ osrfLogInfo(OSRF_LOG_MARK, "...IDL XML parsed");
+
+ return idlHash;
+}
+
+// Adds a standard virtual field to a fields hash
+static void add_std_fld( osrfHash* fields_hash, const char* field_name, unsigned pos ) {
+ char array_pos_buf[ 7 ];
+ osrfHash* std_fld_hash = osrfNewHash();
+
+ snprintf( array_pos_buf, sizeof( array_pos_buf ), "%u", pos );
+ osrfHashSet( std_fld_hash, strdup( array_pos_buf ), "array_position" );
+ osrfHashSet( std_fld_hash, "true", "virtual" );
+ osrfHashSet( std_fld_hash, strdup( field_name ), "name" );
+ osrfHashSet( fields_hash, std_fld_hash, field_name );
+}
+
+
+osrfHash* oilsIDLFindPath( const char* path, ... ) {
+ if(!path || strlen(path) < 1) return NULL;
+
+ osrfHash* obj = idlHash;
+
+ VA_LIST_TO_STRING(path);
+ char* buf = VA_BUF;
+
+ char* token = NULL;
+ char* t = buf;
+ char* tt;
+
+ token = strtok_r(t, "/", &tt);
+ if(!token) return NULL;
+
+ do {
+ obj = osrfHashGet(obj, token);
+ } while( (token = strtok_r(NULL, "/", &tt)) && obj);
+
+ return obj;
+}
+
+static osrfHash* findClassDef( const char* classname ) {
+ if( !classname || !idlHash )
+ return NULL;
+ else
+ return osrfHashGet( idlHash, classname );
+}
+
+osrfHash* oilsIDL_links( const char* classname ) {
+ osrfHash* classdef = findClassDef( classname );
+ if( classdef )
+ return osrfHashGet( classdef, "links" );
+ else
+ return NULL;
+}
+
+osrfHash* oilsIDL_fields( const char* classname ) {
+ osrfHash* classdef = findClassDef( classname );
+ if( classdef )
+ return osrfHashGet( classdef, "fields" );
+ else
+ return NULL;
+}
+
+int oilsIDL_classIsFieldmapper ( const char* classname ) {
+ if( findClassDef( classname ) )
+ return 1;
+ else
+ return 0;
+}
+
+// For a given class: return the array_position associated with a
+// specified field. (or -1 if it doesn't exist)
+int oilsIDL_ntop (const char* classname, const char* fieldname) {
+ osrfHash* fields_hash = oilsIDL_fields( classname );
+ if( !fields_hash )
+ return -1; // No such class, or no fields for it
+
+ osrfHash* field_def_hash = osrfHashGet( fields_hash, fieldname );
+ if( !field_def_hash )
+ return -1; // No such field
+
+ const char* pos_attr = osrfHashGet( field_def_hash, "array_position" );
+ if( !pos_attr )
+ return -1; // No array_position attribute
+
+ return atoi( pos_attr ); // Return position as int
+}
+
+// For a given class: return a copy of the name of the field
+// at a specified array_position (or NULL if there is none)
+char * oilsIDL_pton (const char* classname, int pos) {
+ osrfHash* fields_hash = oilsIDL_fields( classname );
+ if( !fields_hash )
+ return NULL; // No such class, or no fields for it
+
+ char* ret = NULL;
+ osrfHash* field_def_hash = NULL;
+ osrfHashIterator* iter = osrfNewHashIterator( fields_hash );
+
+ while ( ( field_def_hash = osrfHashIteratorNext( iter ) ) ) {
+ if ( atoi( osrfHashGet( field_def_hash, "array_position" ) ) == pos ) {
+ ret = strdup( osrfHashIteratorKey( iter ) );
+ break;
+ }
+ }
+
+ osrfHashIteratorFree( iter );
+
+ return ret;
+}
+
--- /dev/null
+/**
+ @file oils_pcrud.c
+ @brief As a server, perform database operations at the request of clients.
+
+ This server is similar to the cstore and reporter-store servers,
+ except that it enforces a permissions scheme.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static const int enforce_pcrud = 1; // Boolean
+static const char modulename[] = "open-ils.pcrud";
+
+/**
+ @brief Disconnect from the database.
+
+ This function is called when the server drone is about to terminate.
+*/
+void osrfAppChildExit( void ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Child is exiting, disconnecting from database...");
+
+ int same = 0;
+ if (writehandle == dbhandle)
+ same = 1;
+
+ if (writehandle) {
+ dbi_conn_query(writehandle, "ROLLBACK;");
+ dbi_conn_close(writehandle);
+ writehandle = NULL;
+ }
+ if (dbhandle && !same)
+ dbi_conn_close(dbhandle);
+
+ // XXX add cleanup of readHandles whenever that gets used
+
+ return;
+}
+
+/**
+ @brief Initialize the application.
+ @return Zero if successful, or non-zero if not.
+
+ Load the IDL file into an internal data structure for future reference. Each non-virtual
+ class in the IDL corresponds to a table or view in the database, or to a subquery defined
+ in the IDL. Ignore all virtual tables and virtual fields.
+
+ Register a number of methods, some of them general-purpose and others specific for
+ particular classes.
+
+ The name of the application is given by the MODULENAME macro, whose value depends on
+ conditional compilation. The method names also incorporate MODULENAME, followed by a
+ dot, as a prefix. Some methods are registered or not registered depending on whether
+ the IDL includes permacrud entries for the class and method.
+
+ The general-purpose methods are as follows (minus their MODULENAME prefixes):
+
+ - transaction.begin
+ - transaction.commit
+ - transaction.rollback
+ - savepoint.set
+ - savepoint.release
+ - savepoint.rollback
+ - set_audit_info
+
+ For each non-virtual class, create up to eight class-specific methods:
+
+ - create (not for readonly classes)
+ - retrieve
+ - update (not for readonly classes)
+ - delete (not for readonly classes
+ - search (atomic and non-atomic versions)
+ - id_list (atomic and non-atomic versions)
+
+ The full method names follow the pattern "MODULENAME.method_type.classname".
+ In addition, the names of atomic methods have a suffix of ".atomic".
+
+ This function is called when the registering the application, and is executed by the
+ listener before spawning the drones.
+*/
+int osrfAppInitialize( void ) {
+
+ osrfLogInfo(OSRF_LOG_MARK, "Initializing the PCRUD Server...");
+ osrfLogInfo(OSRF_LOG_MARK, "Finding XML file...");
+
+ if ( !oilsIDLInit( osrf_settings_host_value( "/IDL" )))
+ return 1; /* return non-zero to indicate error */
+
+ // Open the database temporarily. Look up the datatypes of all
+ // the non-virtual fields and record them with the IDL data.
+ dbi_conn handle = oilsConnectDB( modulename );
+ if( !handle )
+ return -1;
+ else if( oilsExtendIDL( handle )) {
+ osrfLogError( OSRF_LOG_MARK, "Error extending the IDL" );
+ return -1;
+ }
+ dbi_conn_close( handle );
+
+ // Get the maximum flesh depth from the settings
+ char* md = osrf_settings_host_value(
+ "/apps/%s/app_settings/max_query_recursion", modulename );
+ int max_flesh_depth = 100;
+ if( md )
+ max_flesh_depth = atoi( md );
+ if( max_flesh_depth < 0 )
+ max_flesh_depth = 1;
+ else if( max_flesh_depth > 1000 )
+ max_flesh_depth = 1000;
+
+ oilsSetSQLOptions( modulename, enforce_pcrud, max_flesh_depth );
+
+ // Now register all the methods
+ growing_buffer* method_name = buffer_init(64);
+
+ // first we register all the transaction and savepoint methods
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.begin");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR( method_name ),
+ "beginTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.commit");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "commitTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".transaction.rollback");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "rollbackTransaction", "", 0, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.set");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "setSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.release");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "releaseSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".savepoint.rollback");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "rollbackSavepoint", "", 1, 0 );
+
+ buffer_reset(method_name);
+ OSRF_BUFFER_ADD(method_name, modulename );
+ OSRF_BUFFER_ADD(method_name, ".set_audit_info");
+ osrfAppRegisterMethod( modulename, OSRF_BUFFER_C_STR(method_name),
+ "setAuditInfo", "", 3, 0 );
+
+ static const char* global_method[] = {
+ "create",
+ "retrieve",
+ "update",
+ "delete",
+ "search",
+ "id_list"
+ };
+ const int global_method_count
+ = sizeof( global_method ) / sizeof ( global_method[0] );
+
+ unsigned long class_count = osrfHashGetCount( oilsIDL() );
+ osrfLogDebug(OSRF_LOG_MARK, "%lu classes loaded", class_count );
+ osrfLogDebug(OSRF_LOG_MARK,
+ "At most %lu methods will be generated",
+ (unsigned long) (class_count * global_method_count) );
+
+ osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+ osrfHash* idlClass = NULL;
+
+ // For each class in the IDL...
+ while( (idlClass = osrfHashIteratorNext( class_itr ) ) ) {
+
+ const char* classname = osrfHashIteratorKey( class_itr );
+ osrfLogInfo(OSRF_LOG_MARK, "Generating class methods for %s", classname);
+
+ if (!osrfStringArrayContains( osrfHashGet(idlClass, "controller"), modulename )) {
+ osrfLogInfo(OSRF_LOG_MARK, "%s is not listed as a controller for %s, moving on",
+ modulename, classname);
+ continue;
+ }
+
+ if ( str_is_true( osrfHashGet(idlClass, "virtual") ) ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+ continue;
+ }
+
+ // Look up some other attributes of the current class
+ const char* idlClass_fieldmapper = osrfHashGet(idlClass, "fieldmapper");
+ if( !idlClass_fieldmapper ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Skipping class \"%s\"; no fieldmapper in IDL",
+ classname );
+ continue;
+ }
+
+ osrfHash* idlClass_permacrud = NULL;
+
+ // Ignore classes with no permacrud section
+ idlClass_permacrud = osrfHashGet( idlClass, "permacrud" );
+ if( !idlClass_permacrud ) {
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Skipping class \"%s\"; no permacrud in IDL", classname );
+ continue;
+ }
+
+ const char* readonly = osrfHashGet( idlClass, "readonly" );
+
+ int i;
+ for( i = 0; i < global_method_count; ++i ) { // for each global method
+ const char* method_type = global_method[ i ];
+ osrfLogDebug(OSRF_LOG_MARK,
+ "Using files to build %s class methods for %s", method_type, classname);
+
+ // Treat "id_list" or "search" as forms of "retrieve"
+ const char* tmp_method = method_type;
+ if ( *tmp_method == 'i' || *tmp_method == 's') { // "id_list" or "search"
+ tmp_method = "retrieve";
+ }
+ // Skip this method if there is no permacrud entry for it
+ if (!osrfHashGet( idlClass_permacrud, tmp_method ))
+ continue;
+
+ // No create, update, or delete methods for a readonly class
+ if ( str_is_true( readonly )
+ && ( *method_type == 'c' || *method_type == 'u' || *method_type == 'd') )
+ continue;
+
+ buffer_reset( method_name );
+
+ // Build the method name: MODULENAME.method_type.classname
+ buffer_fadd(method_name, "%s.%s.%s", modulename, method_type, classname);
+
+ // For an id_list or search method we specify the OSRF_METHOD_STREAMING option.
+ // The consequence is that we implicitly create an atomic method in addition to
+ // the usual non-atomic method.
+ int flags = 0;
+ if (*method_type == 'i' || *method_type == 's') { // id_list or search
+ flags = flags | OSRF_METHOD_STREAMING;
+ }
+
+ osrfHash* method_meta = osrfNewHash();
+ osrfHashSet( method_meta, idlClass, "class");
+ osrfHashSet( method_meta, buffer_data( method_name ), "methodname" );
+ osrfHashSet( method_meta, strdup(method_type), "methodtype" );
+
+ // Register the method, with a pointer to an osrfHash to tell the method
+ // its name, type, and class.
+ osrfAppRegisterExtendedMethod(
+ modulename,
+ OSRF_BUFFER_C_STR( method_name ),
+ "dispatchCRUDMethod",
+ "",
+ 1,
+ flags,
+ (void*)method_meta
+ );
+
+ } // end for each global method
+ } // end for each class in IDL
+
+ buffer_free( method_name );
+ osrfHashIteratorFree( class_itr );
+
+ return 0;
+}
+
+/**
+ @brief Initialize a server drone.
+ @return Zero if successful, -1 if not.
+
+ Connect to the database.
+
+ This function is called by a server drone shortly after it is spawned by the listener.
+*/
+int osrfAppChildInit( void ) {
+
+ writehandle = oilsConnectDB( modulename );
+ if( !writehandle )
+ return -1;
+
+ oilsSetDBConnection( writehandle );
+
+ return 0;
+}
+
+/**
+ @brief Implement the class-specific methods.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Branch on the method type: create, retrieve, update, delete, search, or id_list.
+
+ The method parameters and the type of value returned to the client depend on the method
+ type. However, for all PCRUD methods, the first method parameter is an authkey.
+*/
+int dispatchCRUDMethod( osrfMethodContext* ctx ) {
+
+ // Get the method type, then can branch on it
+ osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+ const char* methodtype = osrfHashGet( method_meta, "methodtype" );
+
+ if( !strcmp( methodtype, "create" ))
+ return doCreate( ctx );
+ else if( !strcmp(methodtype, "retrieve" ))
+ return doRetrieve( ctx );
+ else if( !strcmp(methodtype, "update" ))
+ return doUpdate( ctx );
+ else if( !strcmp(methodtype, "delete" ))
+ return doDelete( ctx );
+ else if( !strcmp(methodtype, "search" ))
+ return doSearch( ctx );
+ else if( !strcmp(methodtype, "id_list" ))
+ return doIdList( ctx );
+ else {
+ osrfAppRespondComplete( ctx, NULL ); // should be unreachable...
+ return 0;
+ }
+}
--- /dev/null
+/**
+ @file oils_sql.c
+ @brief Utility routines for translating JSON into SQL.
+*/
+
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <ctype.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/log.h"
+#include "opensrf/osrf_application.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_sql.h"
+
+// The next four macros are OR'd together as needed to form a set
+// of bitflags. SUBCOMBO enables an extra pair of parentheses when
+// nesting one UNION, INTERSECT or EXCEPT inside another.
+// SUBSELECT tells us we're in a subquery, so don't add the
+// terminal semicolon yet.
+#define SUBCOMBO 8
+#define SUBSELECT 4
+#define DISABLE_I18N 2
+#define SELECT_DISTINCT 1
+
+#define AND_OP_JOIN 0
+#define OR_OP_JOIN 1
+
+struct ClassInfoStruct;
+typedef struct ClassInfoStruct ClassInfo;
+
+#define ALIAS_STORE_SIZE 16
+#define CLASS_NAME_STORE_SIZE 16
+
+struct ClassInfoStruct {
+ char* alias;
+ char* class_name;
+ char* source_def;
+ osrfHash* class_def; // Points into IDL
+ osrfHash* fields; // Points into IDL
+ osrfHash* links; // Points into IDL
+
+ // The remaining members are private and internal. Client code should not
+ // access them directly.
+
+ ClassInfo* next; // Supports linked list of joined classes
+ int in_use; // boolean
+
+ // We usually store the alias and class name in the following arrays, and
+ // point the corresponding pointers at them. When the string is too big
+ // for the array (which will probably never happen in practice), we strdup it.
+
+ char alias_store[ ALIAS_STORE_SIZE + 1 ];
+ char class_name_store[ CLASS_NAME_STORE_SIZE + 1 ];
+};
+
+struct QueryFrameStruct;
+typedef struct QueryFrameStruct QueryFrame;
+
+struct QueryFrameStruct {
+ ClassInfo core;
+ ClassInfo* join_list; // linked list of classes joined to the core class
+ QueryFrame* next; // implements stack as linked list
+ int in_use; // boolean
+};
+
+static int timeout_needs_resetting;
+static time_t time_next_reset;
+
+static int verifyObjectClass ( osrfMethodContext*, const jsonObject* );
+
+static void setXactId( osrfMethodContext* ctx );
+static inline const char* getXactId( osrfMethodContext* ctx );
+static inline void clearXactId( osrfMethodContext* ctx );
+
+static jsonObject* doFieldmapperSearch ( osrfMethodContext* ctx, osrfHash* class_meta,
+ jsonObject* where_hash, jsonObject* query_hash, int* err );
+static jsonObject* oilsMakeFieldmapperFromResult( dbi_result, osrfHash* );
+static jsonObject* oilsMakeJSONFromResult( dbi_result );
+
+static char* searchSimplePredicate ( const char* op, const char* class_alias,
+ osrfHash* field, const jsonObject* node );
+static char* searchFunctionPredicate ( const char*, osrfHash*, const jsonObject*, const char* );
+static char* searchFieldTransform ( const char*, osrfHash*, const jsonObject* );
+static char* searchFieldTransformPredicate ( const ClassInfo*, osrfHash*, const jsonObject*,
+ const char* );
+static char* searchBETWEENPredicate ( const char*, osrfHash*, const jsonObject* );
+static char* searchINPredicate ( const char*, osrfHash*,
+ jsonObject*, const char*, osrfMethodContext* );
+static char* searchPredicate ( const ClassInfo*, osrfHash*, jsonObject*, osrfMethodContext* );
+static char* searchJOIN ( const jsonObject*, const ClassInfo* left_info );
+static char* searchWHERE ( const jsonObject* search_hash, const ClassInfo*, int, osrfMethodContext* );
+static char* buildSELECT( const jsonObject*, jsonObject* rest_of_query,
+ osrfHash* meta, osrfMethodContext* ctx );
+static char* buildOrderByFromArray( osrfMethodContext* ctx, const jsonObject* order_array );
+
+char* buildQuery( osrfMethodContext* ctx, jsonObject* query, int flags );
+
+char* SELECT ( osrfMethodContext*, jsonObject*, const jsonObject*, const jsonObject*,
+ const jsonObject*, const jsonObject*, const jsonObject*, const jsonObject*, int );
+
+static osrfStringArray* getPermLocationCache( osrfMethodContext*, const char* );
+static void setPermLocationCache( osrfMethodContext*, const char*, osrfStringArray* );
+
+void userDataFree( void* );
+static void sessionDataFree( char*, void* );
+static void pcacheFree( char*, void* );
+static int obj_is_true( const jsonObject* obj );
+static const char* json_type( int code );
+static const char* get_primitive( osrfHash* field );
+static const char* get_datatype( osrfHash* field );
+static void pop_query_frame( void );
+static void push_query_frame( void );
+static int add_query_core( const char* alias, const char* class_name );
+static inline ClassInfo* search_alias( const char* target );
+static ClassInfo* search_all_alias( const char* target );
+static ClassInfo* add_joined_class( const char* alias, const char* classname );
+static void clear_query_stack( void );
+
+static const jsonObject* verifyUserPCRUD( osrfMethodContext* );
+static int verifyObjectPCRUD( osrfMethodContext*, osrfHash*, const jsonObject*, int );
+static const char* org_tree_root( osrfMethodContext* ctx );
+static jsonObject* single_hash( const char* key, const char* value );
+
+static int child_initialized = 0; /* boolean */
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+// The following points to the top of a stack of QueryFrames. It's a little
+// confusing because the top level of the query is at the bottom of the stack.
+static QueryFrame* curr_query = NULL;
+
+static dbi_conn writehandle; /* our MASTER db connection */
+static dbi_conn dbhandle; /* our CURRENT db connection */
+//static osrfHash * readHandles;
+
+static int max_flesh_depth = 100;
+
+static int perm_at_threshold = 5;
+static int enforce_pcrud = 0; // Boolean
+static char* modulename = NULL;
+
+int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id);
+
+static char* _sanitize_savepoint_name( const char* sp );
+
+/**
+ @brief Connect to the database.
+ @return A database connection if successful, or NULL if not.
+*/
+dbi_conn oilsConnectDB( const char* mod_name ) {
+
+ osrfLogDebug( OSRF_LOG_MARK, "Attempting to initialize libdbi..." );
+ if( dbi_initialize( NULL ) == -1 ) {
+ osrfLogError( OSRF_LOG_MARK, "Unable to initialize libdbi" );
+ return NULL;
+ } else
+ osrfLogDebug( OSRF_LOG_MARK, "... libdbi initialized." );
+
+ char* driver = osrf_settings_host_value( "/apps/%s/app_settings/driver", mod_name );
+ char* user = osrf_settings_host_value( "/apps/%s/app_settings/database/user", mod_name );
+ char* host = osrf_settings_host_value( "/apps/%s/app_settings/database/host", mod_name );
+ char* port = osrf_settings_host_value( "/apps/%s/app_settings/database/port", mod_name );
+ char* db = osrf_settings_host_value( "/apps/%s/app_settings/database/db", mod_name );
+ char* pw = osrf_settings_host_value( "/apps/%s/app_settings/database/pw", mod_name );
+
+ osrfLogDebug( OSRF_LOG_MARK, "Attempting to load the database driver [%s]...", driver );
+ dbi_conn handle = dbi_conn_new( driver );
+
+ if( !handle ) {
+ osrfLogError( OSRF_LOG_MARK, "Error loading database driver [%s]", driver );
+ return NULL;
+ }
+ osrfLogDebug( OSRF_LOG_MARK, "Database driver [%s] seems OK", driver );
+
+ osrfLogInfo(OSRF_LOG_MARK, "%s connecting to database. host=%s, "
+ "port=%s, user=%s, db=%s", mod_name, host, port, user, db );
+
+ if( host ) dbi_conn_set_option( handle, "host", host );
+ if( port ) dbi_conn_set_option_numeric( handle, "port", atoi( port ));
+ if( user ) dbi_conn_set_option( handle, "username", user );
+ if( pw ) dbi_conn_set_option( handle, "password", pw );
+ if( db ) dbi_conn_set_option( handle, "dbname", db );
+
+ free( user );
+ free( host );
+ free( port );
+ free( db );
+ free( pw );
+
+ if( dbi_conn_connect( handle ) < 0 ) {
+ sleep( 1 );
+ if( dbi_conn_connect( handle ) < 0 ) {
+ const char* msg;
+ dbi_conn_error( handle, &msg );
+ osrfLogError( OSRF_LOG_MARK, "Error connecting to database: %s",
+ msg ? msg : "(No description available)" );
+ return NULL;
+ }
+ }
+
+ osrfLogInfo( OSRF_LOG_MARK, "%s successfully connected to the database", mod_name );
+
+ return handle;
+}
+
+/**
+ @brief Select some options.
+ @param module_name: Name of the server.
+ @param do_pcrud: Boolean. True if we are to enforce PCRUD permissions.
+
+ This source file is used (at this writing) to implement three different servers:
+ - open-ils.reporter-store
+ - open-ils.pcrud
+ - open-ils.cstore
+
+ These servers behave mostly the same, but they implement different combinations of
+ methods, and open-ils.pcrud enforces a permissions scheme that the other two don't.
+
+ Here we use the server name in messages to identify which kind of server issued them.
+ We use do_crud as a boolean to control whether or not to enforce the permissions scheme.
+*/
+void oilsSetSQLOptions( const char* module_name, int do_pcrud, int flesh_depth ) {
+ if( !module_name )
+ module_name = "open-ils.cstore"; // bulletproofing with a default
+
+ if( modulename )
+ free( modulename );
+
+ modulename = strdup( module_name );
+ enforce_pcrud = do_pcrud;
+ max_flesh_depth = flesh_depth;
+}
+
+/**
+ @brief Install a database connection.
+ @param conn Pointer to a database connection.
+
+ In some contexts, @a conn may merely provide a driver so that we can process strings
+ properly, without providing an open database connection.
+*/
+void oilsSetDBConnection( dbi_conn conn ) {
+ dbhandle = writehandle = conn;
+}
+
+/**
+ @brief Determine whether a database connection is alive.
+ @param handle Handle for a database connection.
+ @return 1 if the connection is alive, or zero if it isn't.
+*/
+int oilsIsDBConnected( dbi_conn handle ) {
+ // Do an innocuous SELECT. If it succeeds, the database connection is still good.
+ dbi_result result = dbi_conn_query( handle, "SELECT 1;" );
+ if( result ) {
+ dbi_result_free( result );
+ return 1;
+ } else {
+ // This is a terrible, horrible, no good, very bad kludge.
+ // Sometimes the SELECT 1 query fails, not because the database connection is dead,
+ // but because (due to a previous error) the database is ignoring all commands,
+ // even innocuous SELECTs, until the current transaction is rolled back. The only
+ // known way to detect this condition via the dbi library is by looking at the error
+ // message. This approach will break if the language or wording of the message ever
+ // changes.
+ // Note: the dbi_conn_ping function purports to determine whether the database
+ // connection is live, but at this writing this function is unreliable and useless.
+ static const char* ok_msg = "ERROR: current transaction is aborted, commands "
+ "ignored until end of transaction block\n";
+ const char* msg;
+ dbi_conn_error( handle, &msg );
+ if( strcmp( msg, ok_msg )) {
+ osrfLogError( OSRF_LOG_MARK, "Database connection isn't working" );
+ return 0;
+ } else
+ return 1; // ignoring SELECT due to previous error; that's okay
+ }
+}
+
+/**
+ @brief Get a table name, view name, or subquery for use in a FROM clause.
+ @param class Pointer to the IDL class entry.
+ @return A table name, a view name, or a subquery in parentheses.
+
+ In some cases the IDL defines a class, not with a table name or a view name, but with
+ a SELECT statement, which may be used as a subquery.
+*/
+char* oilsGetRelation( osrfHash* classdef ) {
+
+ char* source_def = NULL;
+ const char* tabledef = osrfHashGet( classdef, "tablename" );
+
+ if( tabledef ) {
+ source_def = strdup( tabledef ); // Return the name of a table or view
+ } else {
+ tabledef = osrfHashGet( classdef, "source_definition" );
+ if( tabledef ) {
+ // Return a subquery, enclosed in parentheses
+ source_def = safe_malloc( strlen( tabledef ) + 3 );
+ source_def[ 0 ] = '(';
+ strcpy( source_def + 1, tabledef );
+ strcat( source_def, ")" );
+ } else {
+ // Not found: return an error
+ const char* classname = osrfHashGet( classdef, "classname" );
+ if( !classname )
+ classname = "???";
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR No tablename or source_definition for class \"%s\"",
+ modulename,
+ classname
+ );
+ }
+ }
+
+ return source_def;
+}
+
+/**
+ @brief Add datatypes from the database to the fields in the IDL.
+ @param handle Handle for a database connection
+ @return Zero if successful, or 1 upon error.
+
+ For each relevant class in the IDL: ask the database for the datatype of every field.
+ In particular, determine which fields are text fields and which fields are numeric
+ fields, so that we know whether to enclose their values in quotes.
+*/
+int oilsExtendIDL( dbi_conn handle ) {
+ osrfHashIterator* class_itr = osrfNewHashIterator( oilsIDL() );
+ osrfHash* class = NULL;
+ growing_buffer* query_buf = buffer_init( 64 );
+ int results_found = 0; // boolean
+
+ // For each class in the IDL...
+ while( (class = osrfHashIteratorNext( class_itr ) ) ) {
+ const char* classname = osrfHashIteratorKey( class_itr );
+ osrfHash* fields = osrfHashGet( class, "fields" );
+
+ // If the class is virtual, ignore it
+ if( str_is_true( osrfHashGet(class, "virtual") ) ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Class %s is virtual, skipping", classname );
+ continue;
+ }
+
+ char* tabledef = oilsGetRelation( class );
+ if( !tabledef )
+ continue; // No such relation -- a query of it would be doomed to failure
+
+ buffer_reset( query_buf );
+ buffer_fadd( query_buf, "SELECT * FROM %s AS x WHERE 1=0;", tabledef );
+
+ free(tabledef );
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s Investigatory SQL = %s",
+ modulename, OSRF_BUFFER_C_STR( query_buf ) );
+
+ dbi_result result = dbi_conn_query( handle, OSRF_BUFFER_C_STR( query_buf ) );
+ if( result ) {
+
+ results_found = 1;
+ int columnIndex = 1;
+ const char* columnName;
+ while( (columnName = dbi_result_get_field_name(result, columnIndex)) ) {
+
+ osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...",
+ columnName );
+
+ /* fetch the fieldmapper index */
+ osrfHash* _f = osrfHashGet(fields, columnName);
+ if( _f ) {
+
+ osrfLogDebug(OSRF_LOG_MARK, "Found [%s] in IDL hash...", columnName);
+
+ /* determine the field type and storage attributes */
+
+ switch( dbi_result_get_field_type_idx( result, columnIndex )) {
+
+ case DBI_TYPE_INTEGER : {
+
+ if( !osrfHashGet(_f, "primitive") )
+ osrfHashSet(_f, "number", "primitive");
+
+ int attr = dbi_result_get_field_attribs_idx( result, columnIndex );
+ if( attr & DBI_INTEGER_SIZE8 )
+ osrfHashSet( _f, "INT8", "datatype" );
+ else
+ osrfHashSet( _f, "INT", "datatype" );
+ break;
+ }
+ case DBI_TYPE_DECIMAL :
+ if( !osrfHashGet( _f, "primitive" ))
+ osrfHashSet( _f, "number", "primitive" );
+
+ osrfHashSet( _f, "NUMERIC", "datatype" );
+ break;
+
+ case DBI_TYPE_STRING :
+ if( !osrfHashGet( _f, "primitive" ))
+ osrfHashSet( _f, "string", "primitive" );
+
+ osrfHashSet( _f,"TEXT", "datatype" );
+ break;
+
+ case DBI_TYPE_DATETIME :
+ if( !osrfHashGet( _f, "primitive" ))
+ osrfHashSet( _f, "string", "primitive" );
+
+ osrfHashSet( _f, "TIMESTAMP", "datatype" );
+ break;
+
+ case DBI_TYPE_BINARY :
+ if( !osrfHashGet( _f, "primitive" ))
+ osrfHashSet( _f, "string", "primitive" );
+
+ osrfHashSet( _f, "BYTEA", "datatype" );
+ }
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Setting [%s] to primitive [%s] and datatype [%s]...",
+ columnName,
+ osrfHashGet( _f, "primitive" ),
+ osrfHashGet( _f, "datatype" )
+ );
+ }
+ ++columnIndex;
+ } // end while loop for traversing columns of result
+ dbi_result_free( result );
+ } else {
+ const char* msg;
+ int errnum = dbi_conn_error( handle, &msg );
+ osrfLogDebug( OSRF_LOG_MARK, "No data found for class [%s]: %d, %s", classname,
+ errnum, msg ? msg : "(No description available)" );
+ // We don't check the database connection here. It's routine to get failures at
+ // this point; we routinely try to query tables that don't exist, because they
+ // are defined in the IDL but not in the database.
+ }
+ } // end for each class in IDL
+
+ buffer_free( query_buf );
+ osrfHashIteratorFree( class_itr );
+ child_initialized = 1;
+
+ if( !results_found ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "No results found for any class -- bad database connection?" );
+ return 1;
+ } else if( ! oilsIsDBConnected( handle )) {
+ osrfLogError( OSRF_LOG_MARK,
+ "Unable to extend IDL: database connection isn't working" );
+ return 1;
+ }
+ else
+ return 0;
+}
+
+/**
+ @brief Free an osrfHash that stores a transaction ID.
+ @param blob A pointer to the osrfHash to be freed, cast to a void pointer.
+
+ This function is a callback, to be called by the application session when it ends.
+ The application session stores the osrfHash via an opaque pointer.
+
+ If the osrfHash contains an entry for the key "xact_id", it means that an
+ uncommitted transaction is pending. Roll it back.
+*/
+void userDataFree( void* blob ) {
+ osrfHash* hash = (osrfHash*) blob;
+ if( osrfHashGet( hash, "xact_id" ) && writehandle ) {
+ if( !dbi_conn_query( writehandle, "ROLLBACK;" )) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogWarning( OSRF_LOG_MARK, "Unable to perform rollback: %d %s",
+ errnum, msg ? msg : "(No description available)" );
+ };
+ }
+ if( writehandle ) {
+ if( !dbi_conn_query( writehandle, "SELECT auditor.clear_audit_info();" ) ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogWarning( OSRF_LOG_MARK, "Unable to perform audit info clearing: %d %s",
+ errnum, msg ? msg : "(No description available)" );
+ }
+ }
+
+ osrfHashFree( hash );
+}
+
+/**
+ @name Managing session data
+ @brief Maintain data stored via the userData pointer of the application session.
+
+ Currently, session-level data is stored in an osrfHash. Other arrangements are
+ possible, and some would be more efficient. The application session calls a
+ callback function to free userData before terminating.
+
+ Currently, the only data we store at the session level is the transaction id. By this
+ means we can ensure that any pending transactions are rolled back before the application
+ session terminates.
+*/
+/*@{*/
+
+/**
+ @brief Free an item in the application session's userData.
+ @param key The name of a key for an osrfHash.
+ @param item An opaque pointer to the item associated with the key.
+
+ We store an osrfHash as userData with the application session, and arrange (by
+ installing userDataFree() as a different callback) for the session to free that
+ osrfHash before terminating.
+
+ This function is a callback for freeing items in the osrfHash. Currently we store
+ two things:
+ - Transaction id of a pending transaction; a character string. Key: "xact_id".
+ - Authkey; a character string. Key: "authkey".
+ - User object from the authentication server; a jsonObject. Key: "user_login".
+
+ If we ever store anything else in userData, we will need to revisit this function so
+ that it will free whatever else needs freeing.
+*/
+static void sessionDataFree( char* key, void* item ) {
+ if( !strcmp( key, "xact_id" ) || !strcmp( key, "authkey" ) || !strncmp( key, "rs_size_", 8) )
+ free( item );
+ else if( !strcmp( key, "user_login" ) )
+ jsonObjectFree( (jsonObject*) item );
+ else if( !strcmp( key, "pcache" ) )
+ osrfHashFree( (osrfHash*) item );
+}
+
+static void pcacheFree( char* key, void* item ) {
+ osrfStringArrayFree( (osrfStringArray*) item );
+}
+
+/**
+ @brief Initialize session cache.
+ @param ctx Pointer to the method context.
+
+ Create a cache for the session by making the session's userData member point
+ to an osrfHash instance.
+*/
+static osrfHash* initSessionCache( osrfMethodContext* ctx ) {
+ ctx->session->userData = osrfNewHash();
+ osrfHashSetCallback( (osrfHash*) ctx->session->userData, &sessionDataFree );
+ ctx->session->userDataFree = &userDataFree;
+ return ctx->session->userData;
+}
+
+/**
+ @brief Save a transaction id.
+ @param ctx Pointer to the method context.
+
+ Save the session_id of the current application session as a transaction id.
+*/
+static void setXactId( osrfMethodContext* ctx ) {
+ if( ctx && ctx->session ) {
+ osrfAppSession* session = ctx->session;
+
+ osrfHash* cache = session->userData;
+
+ // If the session doesn't already have a hash, create one. Make sure
+ // that the application session frees the hash when it terminates.
+ if( NULL == cache )
+ cache = initSessionCache( ctx );
+
+ // Save the transaction id in the hash, with the key "xact_id"
+ osrfHashSet( cache, strdup( session->session_id ), "xact_id" );
+ }
+}
+
+/**
+ @brief Get the transaction ID for the current transaction, if any.
+ @param ctx Pointer to the method context.
+ @return Pointer to the transaction ID.
+
+ The return value points to an internal buffer, and will become invalid upon issuing
+ a commit or rollback.
+*/
+static inline const char* getXactId( osrfMethodContext* ctx ) {
+ if( ctx && ctx->session && ctx->session->userData )
+ return osrfHashGet( (osrfHash*) ctx->session->userData, "xact_id" );
+ else
+ return NULL;
+}
+
+/**
+ @brief Clear the current transaction id.
+ @param ctx Pointer to the method context.
+*/
+static inline void clearXactId( osrfMethodContext* ctx ) {
+ if( ctx && ctx->session && ctx->session->userData )
+ osrfHashRemove( ctx->session->userData, "xact_id" );
+}
+/*@}*/
+
+/**
+ @brief Stash the location for a particular perm in the sessionData cache
+ @param ctx Pointer to the method context.
+ @param perm Name of the permission we're looking at
+ @param array StringArray of perm location ids
+*/
+static void setPermLocationCache( osrfMethodContext* ctx, const char* perm, osrfStringArray* locations ) {
+ if( ctx && ctx->session ) {
+ osrfAppSession* session = ctx->session;
+
+ osrfHash* cache = session->userData;
+
+ // If the session doesn't already have a hash, create one. Make sure
+ // that the application session frees the hash when it terminates.
+ if( NULL == cache )
+ cache = initSessionCache( ctx );
+
+ osrfHash* pcache = osrfHashGet(cache, "pcache");
+
+ if( NULL == pcache ) {
+ pcache = osrfNewHash();
+ osrfHashSetCallback( pcache, &pcacheFree );
+ osrfHashSet( cache, pcache, "pcache" );
+ }
+
+ if( perm && locations )
+ osrfHashSet( pcache, locations, strdup(perm) );
+ }
+}
+
+/**
+ @brief Grab stashed location for a particular perm in the sessionData cache
+ @param ctx Pointer to the method context.
+ @param perm Name of the permission we're looking at
+*/
+static osrfStringArray* getPermLocationCache( osrfMethodContext* ctx, const char* perm ) {
+ if( ctx && ctx->session ) {
+ osrfAppSession* session = ctx->session;
+ osrfHash* cache = session->userData;
+ if( cache ) {
+ osrfHash* pcache = osrfHashGet(cache, "pcache");
+ if( pcache ) {
+ return osrfHashGet( pcache, perm );
+ }
+ }
+ }
+
+ return NULL;
+}
+
+/**
+ @brief Save the user's login in the userData for the current application session.
+ @param ctx Pointer to the method context.
+ @param user_login Pointer to the user login object to be cached (we cache the original,
+ not a copy of it).
+
+ If @a user_login is NULL, remove the user login if one is already cached.
+*/
+static void setUserLogin( osrfMethodContext* ctx, jsonObject* user_login ) {
+ if( ctx && ctx->session ) {
+ osrfAppSession* session = ctx->session;
+
+ osrfHash* cache = session->userData;
+
+ // If the session doesn't already have a hash, create one. Make sure
+ // that the application session frees the hash when it terminates.
+ if( NULL == cache )
+ cache = initSessionCache( ctx );
+
+ if( user_login )
+ osrfHashSet( cache, user_login, "user_login" );
+ else
+ osrfHashRemove( cache, "user_login" );
+ }
+}
+
+/**
+ @brief Get the user login object for the current application session, if any.
+ @param ctx Pointer to the method context.
+ @return Pointer to the user login object if found; otherwise NULL.
+
+ The user login object was returned from the authentication server, and then cached so
+ we don't have to call the authentication server again for the same user.
+*/
+static const jsonObject* getUserLogin( osrfMethodContext* ctx ) {
+ if( ctx && ctx->session && ctx->session->userData )
+ return osrfHashGet( (osrfHash*) ctx->session->userData, "user_login" );
+ else
+ return NULL;
+}
+
+/**
+ @brief Save a copy of an authkey in the userData of the current application session.
+ @param ctx Pointer to the method context.
+ @param authkey The authkey to be saved.
+
+ If @a authkey is NULL, remove the authkey if one is already cached.
+*/
+static void setAuthkey( osrfMethodContext* ctx, const char* authkey ) {
+ if( ctx && ctx->session && authkey ) {
+ osrfAppSession* session = ctx->session;
+ osrfHash* cache = session->userData;
+
+ // If the session doesn't already have a hash, create one. Make sure
+ // that the application session frees the hash when it terminates.
+ if( NULL == cache )
+ cache = initSessionCache( ctx );
+
+ // Save the transaction id in the hash, with the key "xact_id"
+ if( authkey && *authkey )
+ osrfHashSet( cache, strdup( authkey ), "authkey" );
+ else
+ osrfHashRemove( cache, "authkey" );
+ }
+}
+
+/**
+ @brief Reset the login timeout.
+ @param authkey The authentication key for the current login session.
+ @param now The current time.
+ @return Zero if successful, or 1 if not.
+
+ Tell the authentication server to reset the timeout so that the login session won't
+ expire for a while longer.
+
+ We could dispense with the @a now parameter by calling time(). But we just called
+ time() in order to decide whether to reset the timeout, so we might as well reuse
+ the result instead of calling time() again.
+*/
+static int reset_timeout( const char* authkey, time_t now ) {
+ jsonObject* auth_object = jsonNewObject( authkey );
+
+ // Ask the authentication server to reset the timeout. It returns an event
+ // indicating success or failure.
+ jsonObject* result = oilsUtilsQuickReq( "open-ils.auth",
+ "open-ils.auth.session.reset_timeout", auth_object );
+ jsonObjectFree( auth_object );
+
+ if( !result || result->type != JSON_HASH ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "Unexpected object type receieved from open-ils.auth.session.reset_timeout" );
+ jsonObjectFree( result );
+ return 1; // Not the right sort of object returned
+ }
+
+ const jsonObject* ilsevent = jsonObjectGetKeyConst( result, "ilsevent" );
+ if( !ilsevent || ilsevent->type != JSON_NUMBER ) {
+ osrfLogError( OSRF_LOG_MARK, "ilsevent is absent or malformed" );
+ jsonObjectFree( result );
+ return 1; // Return code from method not available
+ }
+
+ if( jsonObjectGetNumber( ilsevent ) != 0.0 ) {
+ const char* desc = jsonObjectGetString( jsonObjectGetKeyConst( result, "desc" ));
+ if( !desc )
+ desc = "(No reason available)"; // failsafe; shouldn't happen
+ osrfLogInfo( OSRF_LOG_MARK, "Failure to reset timeout: %s", desc );
+ jsonObjectFree( result );
+ return 1;
+ }
+
+ // Revise our local proxy for the timeout deadline
+ // by a smallish fraction of the timeout interval
+ const char* timeout = jsonObjectGetString( jsonObjectGetKeyConst( result, "payload" ));
+ if( !timeout )
+ timeout = "1"; // failsafe; shouldn't happen
+ time_next_reset = now + atoi( timeout ) / 15;
+
+ jsonObjectFree( result );
+ return 0; // Successfully reset timeout
+}
+
+/**
+ @brief Get the authkey string for the current application session, if any.
+ @param ctx Pointer to the method context.
+ @return Pointer to the cached authkey if found; otherwise NULL.
+
+ If present, the authkey string was cached from a previous method call.
+*/
+static const char* getAuthkey( osrfMethodContext* ctx ) {
+ if( ctx && ctx->session && ctx->session->userData ) {
+ const char* authkey = osrfHashGet( (osrfHash*) ctx->session->userData, "authkey" );
+ // LFW recent changes mean the userData hash gets set up earlier, but
+ // doesn't necessarily have an authkey yet
+ if (!authkey)
+ return NULL;
+
+ // Possibly reset the authentication timeout to keep the login alive. We do so
+ // no more than once per method call, and not at all if it has been only a short
+ // time since the last reset.
+
+ // Here we reset explicitly, if at all. We also implicitly reset the timeout
+ // whenever we call the "open-ils.auth.session.retrieve" method.
+ if( timeout_needs_resetting ) {
+ time_t now = time( NULL );
+ if( now >= time_next_reset && reset_timeout( authkey, now ) )
+ authkey = NULL; // timeout has apparently expired already
+ }
+
+ timeout_needs_resetting = 0;
+ return authkey;
+ }
+ else
+ return NULL;
+}
+
+/**
+ @brief Implement the transaction.begin method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 upon error.
+
+ Start a transaction. Save a transaction ID for future reference.
+
+ Method parameters:
+ - authkey (PCRUD only)
+
+ Return to client: Transaction ID
+*/
+int beginTransaction( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud ) {
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ dbi_result result = dbi_conn_query( writehandle, "START TRANSACTION;" );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError( OSRF_LOG_MARK, "%s: Error starting transaction: %d %s",
+ modulename, errnum, msg ? msg : "(No description available)" );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error starting transaction" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ setXactId( ctx );
+ jsonObject* ret = jsonNewObject( getXactId( ctx ) );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the savepoint.set method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a SAVEPOINT to the database server.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - savepoint name
+
+ Return to client: Savepoint name
+*/
+int setSavepoint( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ int spNamePos = 0;
+ if( enforce_pcrud ) {
+ spNamePos = 1;
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ // Verify that a transaction is pending
+ const char* trans_id = getXactId( ctx );
+ if( NULL == trans_id ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for savepoints"
+ );
+ return -1;
+ }
+
+ // Get the savepoint name from the method params
+ const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+ if (!spName) {
+ osrfLogWarning(OSRF_LOG_MARK, "savepoint.set called with no name");
+ return -1;
+ }
+
+ char *safeSpName = _sanitize_savepoint_name( spName );
+
+ dbi_result result = dbi_conn_queryf( writehandle, "SAVEPOINT \"%s\";", safeSpName );
+ free( safeSpName );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Error creating savepoint %s in transaction %s: %d %s",
+ modulename,
+ spName,
+ trans_id,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error creating savepoint" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ jsonObject* ret = jsonNewObject( spName );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the savepoint.release method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a RELEASE SAVEPOINT to the database server.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - savepoint name
+
+ Return to client: Savepoint name
+*/
+int releaseSavepoint( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ int spNamePos = 0;
+ if( enforce_pcrud ) {
+ spNamePos = 1;
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ // Verify that a transaction is pending
+ const char* trans_id = getXactId( ctx );
+ if( NULL == trans_id ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for savepoints"
+ );
+ return -1;
+ }
+
+ // Get the savepoint name from the method params
+ const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+ if (!spName) {
+ osrfLogWarning(OSRF_LOG_MARK, "savepoint.release called with no name");
+ return -1;
+ }
+
+ char *safeSpName = _sanitize_savepoint_name( spName );
+
+ dbi_result result = dbi_conn_queryf( writehandle, "RELEASE SAVEPOINT \"%s\";", safeSpName );
+ free( safeSpName );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Error releasing savepoint %s in transaction %s: %d %s",
+ modulename,
+ spName,
+ trans_id,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error releasing savepoint" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ jsonObject* ret = jsonNewObject( spName );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the savepoint.rollback method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a ROLLBACK TO SAVEPOINT to the database server.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - savepoint name
+
+ Return to client: Savepoint name
+*/
+int rollbackSavepoint( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ int spNamePos = 0;
+ if( enforce_pcrud ) {
+ spNamePos = 1;
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ // Verify that a transaction is pending
+ const char* trans_id = getXactId( ctx );
+ if( NULL == trans_id ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for savepoints"
+ );
+ return -1;
+ }
+
+ // Get the savepoint name from the method params
+ const char* spName = jsonObjectGetString( jsonObjectGetIndex(ctx->params, spNamePos) );
+
+ if (!spName) {
+ osrfLogWarning(OSRF_LOG_MARK, "savepoint.rollback called with no name");
+ return -1;
+ }
+
+ char *safeSpName = _sanitize_savepoint_name( spName );
+
+ dbi_result result = dbi_conn_queryf( writehandle, "ROLLBACK TO SAVEPOINT \"%s\";", safeSpName );
+ free( safeSpName );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Error rolling back savepoint %s in transaction %s: %d %s",
+ modulename,
+ spName,
+ trans_id,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error rolling back savepoint" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ jsonObject* ret = jsonNewObject( spName );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the transaction.commit method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a COMMIT to the database server.
+
+ Method parameters:
+ - authkey (PCRUD only)
+
+ Return to client: Transaction ID.
+*/
+int commitTransaction( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud ) {
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ // Verify that a transaction is pending
+ const char* trans_id = getXactId( ctx );
+ if( NULL == trans_id ) {
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "No active transaction to commit" );
+ return -1;
+ }
+
+ dbi_result result = dbi_conn_query( writehandle, "COMMIT;" );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError( OSRF_LOG_MARK, "%s: Error committing transaction: %d %s",
+ modulename, errnum, msg ? msg : "(No description available)" );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error committing transaction" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ jsonObject* ret = jsonNewObject( trans_id );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ clearXactId( ctx );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the transaction.rollback method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a ROLLBACK to the database server.
+
+ Method parameters:
+ - authkey (PCRUD only)
+
+ Return to client: Transaction ID
+*/
+int rollbackTransaction( osrfMethodContext* ctx ) {
+ if( osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud ) {
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ }
+
+ // Verify that a transaction is pending
+ const char* trans_id = getXactId( ctx );
+ if( NULL == trans_id ) {
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "No active transaction to roll back" );
+ return -1;
+ }
+
+ dbi_result result = dbi_conn_query( writehandle, "ROLLBACK;" );
+ if( !result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError( OSRF_LOG_MARK, "%s: Error rolling back transaction: %d %s",
+ modulename, errnum, msg ? msg : "(No description available)" );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error rolling back transaction" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ jsonObject* ret = jsonNewObject( trans_id );
+ osrfAppRespondComplete( ctx, ret );
+ jsonObjectFree( ret );
+ clearXactId( ctx );
+ return 0;
+ }
+}
+
+/**
+ @brief Implement the "search" method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - WHERE clause, as jsonObject
+ - Other SQL clause(s), as a JSON_HASH: joins, SELECT list, LIMIT, etc.
+
+ Return to client: rows of the specified class that satisfy a specified WHERE clause.
+ Optionally flesh linked fields.
+*/
+int doSearch( osrfMethodContext* ctx ) {
+ if( osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ jsonObject* where_clause;
+ jsonObject* rest_of_query;
+
+ if( enforce_pcrud ) {
+ where_clause = jsonObjectGetIndex( ctx->params, 1 );
+ rest_of_query = jsonObjectGetIndex( ctx->params, 2 );
+ } else {
+ where_clause = jsonObjectGetIndex( ctx->params, 0 );
+ rest_of_query = jsonObjectGetIndex( ctx->params, 1 );
+ }
+
+ if( !where_clause ) {
+ osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
+ return -1;
+ }
+
+ // Get the class metadata
+ osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+ osrfHash* class_meta = osrfHashGet( method_meta, "class" );
+
+ // Do the query
+ int err = 0;
+ jsonObject* obj = doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
+ if( err ) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // doFieldmapperSearch() now takes care of our responding for us
+// // Return each row to the client
+// jsonObject* cur = 0;
+// unsigned long res_idx = 0;
+//
+// while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
+// // We used to discard based on perms here, but now that's
+// // inside doFieldmapperSearch()
+// osrfAppRespond( ctx, cur );
+// }
+
+ jsonObjectFree( obj );
+
+ osrfAppRespondComplete( ctx, NULL );
+ return 0;
+}
+
+/**
+ @brief Implement the "id_list" method.
+ @param ctx Pointer to the method context.
+ @param err Pointer through which to return an error code.
+ @return Zero if successful, or -1 if not.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - WHERE clause, as jsonObject
+ - Other SQL clause(s), as a JSON_HASH: joins, LIMIT, etc.
+
+ Return to client: The primary key values for all rows of the relevant class that
+ satisfy a specified WHERE clause.
+
+ This method relies on the assumption that every class has a primary key consisting of
+ a single column.
+*/
+int doIdList( osrfMethodContext* ctx ) {
+ if( osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ jsonObject* where_clause;
+ jsonObject* rest_of_query;
+
+ // We use the where clause without change. But we need to massage the rest of the
+ // query, so we work with a copy of it instead of modifying the original.
+
+ if( enforce_pcrud ) {
+ where_clause = jsonObjectGetIndex( ctx->params, 1 );
+ rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 2 ) );
+ } else {
+ where_clause = jsonObjectGetIndex( ctx->params, 0 );
+ rest_of_query = jsonObjectClone( jsonObjectGetIndex( ctx->params, 1 ) );
+ }
+
+ if( !where_clause ) {
+ osrfLogError( OSRF_LOG_MARK, "No WHERE clause parameter supplied" );
+ return -1;
+ }
+
+ // Eliminate certain SQL clauses, if present.
+ if( rest_of_query ) {
+ jsonObjectRemoveKey( rest_of_query, "select" );
+ jsonObjectRemoveKey( rest_of_query, "no_i18n" );
+ jsonObjectRemoveKey( rest_of_query, "flesh" );
+ jsonObjectRemoveKey( rest_of_query, "flesh_fields" );
+ } else {
+ rest_of_query = jsonNewObjectType( JSON_HASH );
+ }
+
+ jsonObjectSetKey( rest_of_query, "no_i18n", jsonNewBoolObject( 1 ) );
+
+ // Get the class metadata
+ osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+ osrfHash* class_meta = osrfHashGet( method_meta, "class" );
+
+ // Build a SELECT list containing just the primary key,
+ // i.e. like { "classname":["keyname"] }
+ jsonObject* col_list_obj = jsonNewObjectType( JSON_ARRAY );
+
+ // Load array with name of primary key
+ jsonObjectPush( col_list_obj, jsonNewObject( osrfHashGet( class_meta, "primarykey" ) ) );
+ jsonObject* select_clause = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( select_clause, osrfHashGet( class_meta, "classname" ), col_list_obj );
+
+ jsonObjectSetKey( rest_of_query, "select", select_clause );
+
+ // Do the query
+ int err = 0;
+ jsonObject* obj =
+ doFieldmapperSearch( ctx, class_meta, where_clause, rest_of_query, &err );
+
+ jsonObjectFree( rest_of_query );
+ if( err ) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // Return each primary key value to the client
+ jsonObject* cur;
+ unsigned long res_idx = 0;
+ while((cur = jsonObjectGetIndex( obj, res_idx++ ) )) {
+ // We used to discard based on perms here, but now that's
+ // inside doFieldmapperSearch()
+ osrfAppRespond( ctx,
+ oilsFMGetObject( cur, osrfHashGet( class_meta, "primarykey" ) ) );
+ }
+
+ jsonObjectFree( obj );
+ osrfAppRespondComplete( ctx, NULL );
+ return 0;
+}
+
+/**
+ @brief Verify that we have a valid class reference.
+ @param ctx Pointer to the method context.
+ @param param Pointer to the method parameters.
+ @return 1 if the class reference is valid, or zero if it isn't.
+
+ The class of the method params must match the class to which the method id devoted.
+ For PCRUD there are additional restrictions.
+*/
+static int verifyObjectClass ( osrfMethodContext* ctx, const jsonObject* param ) {
+
+ osrfHash* method_meta = (osrfHash*) ctx->method->userData;
+ osrfHash* class = osrfHashGet( method_meta, "class" );
+
+ // Compare the method's class to the parameters' class
+ if( !param->classname || (strcmp( osrfHashGet(class, "classname"), param->classname ))) {
+
+ // Oops -- they don't match. Complain.
+ growing_buffer* msg = buffer_init( 128 );
+ buffer_fadd(
+ msg,
+ "%s: %s method for type %s was passed a %s",
+ modulename,
+ osrfHashGet( method_meta, "methodtype" ),
+ osrfHashGet( class, "classname" ),
+ param->classname ? param->classname : "(null)"
+ );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_BADREQUEST, "osrfMethodException",
+ ctx->request, m );
+ free( m );
+
+ return 0;
+ }
+
+ if( enforce_pcrud )
+ return verifyObjectPCRUD( ctx, class, param, 1 );
+ else
+ return 1;
+}
+
+/**
+ @brief (PCRUD only) Verify that the user is properly logged in.
+ @param ctx Pointer to the method context.
+ @return If the user is logged in, a pointer to the user object from the authentication
+ server; otherwise NULL.
+*/
+static const jsonObject* verifyUserPCRUD( osrfMethodContext* ctx ) {
+
+ // Get the authkey (the first method parameter)
+ const char* auth = jsonObjectGetString( jsonObjectGetIndex( ctx->params, 0 ) );
+
+ // See if we have the same authkey, and a user object,
+ // locally cached from a previous call
+ const char* cached_authkey = getAuthkey( ctx );
+ if( cached_authkey && !strcmp( cached_authkey, auth ) ) {
+ const jsonObject* cached_user = getUserLogin( ctx );
+ if( cached_user )
+ return cached_user;
+ }
+
+ // We have no matching authentication data in the cache. Authenticate from scratch.
+ jsonObject* auth_object = jsonNewObject( auth );
+
+ // Fetch the user object from the authentication server
+ jsonObject* user = oilsUtilsQuickReq( "open-ils.auth", "open-ils.auth.session.retrieve",
+ auth_object );
+ jsonObjectFree( auth_object );
+
+ if( !user->classname || strcmp(user->classname, "au" )) {
+
+ growing_buffer* msg = buffer_init( 128 );
+ buffer_fadd(
+ msg,
+ "%s: permacrud received a bad auth token: %s",
+ modulename,
+ auth
+ );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_UNAUTHORIZED, "osrfMethodException",
+ ctx->request, m );
+
+ free( m );
+ jsonObjectFree( user );
+ user = NULL;
+ } else if( writeAuditInfo( ctx, oilsFMGetStringConst( user, "id" ), oilsFMGetStringConst( user, "wsid" ) ) ) {
+ // Failed to set audit information - But note that write_audit_info already set error information.
+ jsonObjectFree( user );
+ user = NULL;
+ }
+
+ setUserLogin( ctx, user );
+ setAuthkey( ctx, auth );
+
+ // Allow ourselves up to a second before we have to reset the login timeout.
+ // It would be nice to use some fraction of the timeout interval enforced by the
+ // authentication server, but that value is not readily available at this point.
+ // Instead, we use a conservative default interval.
+ time_next_reset = time( NULL ) + 1;
+
+ return user;
+}
+
+/**
+ @brief For PCRUD: Determine whether the current user may access the current row.
+ @param ctx Pointer to the method context.
+ @param class Same as ctx->method->userData's item for key "class" except when called in recursive doFieldmapperSearch
+ @param obj Pointer to the row being potentially accessed.
+ @return 1 if access is permitted, or 0 if it isn't.
+
+ The @a obj parameter points to a JSON_HASH of column values, keyed on column name.
+*/
+static int verifyObjectPCRUD ( osrfMethodContext* ctx, osrfHash *class, const jsonObject* obj, int rs_size ) {
+
+ dbhandle = writehandle;
+
+ // Figure out what class and method are involved
+ osrfHash* method_metadata = (osrfHash*) ctx->method->userData;
+ const char* method_type = osrfHashGet( method_metadata, "methodtype" );
+
+ if (!rs_size) {
+ int *rs_size_from_hash = osrfHashGetFmt( (osrfHash *) ctx->session->userData, "rs_size_req_%d", ctx->request );
+ if (rs_size_from_hash) {
+ rs_size = *rs_size_from_hash;
+ osrfLogDebug(OSRF_LOG_MARK, "used rs_size from request-scoped hash: %d", rs_size);
+ }
+ }
+
+ // Set fetch to 1 in all cases except for inserts, meaning that for local or foreign
+ // contexts we will do another lookup of the current row, even if we already have a
+ // previously fetched row image, because the row image in hand may not include the
+ // foreign key(s) that we need.
+
+ // This is a quick fix with a bludgeon. There are ways to avoid the extra lookup,
+ // but they aren't implemented yet.
+
+ int fetch = 0;
+ if( *method_type == 's' || *method_type == 'i' ) {
+ method_type = "retrieve"; // search and id_list are equivalent to retrieve for this
+ fetch = 1;
+ } else if( *method_type == 'u' || *method_type == 'd' ) {
+ fetch = 1; // MUST go to the db for the object for update and delete
+ }
+
+ // Get the appropriate permacrud entry from the IDL, depending on method type
+ osrfHash* pcrud = osrfHashGet( osrfHashGet( class, "permacrud" ), method_type );
+ if( !pcrud ) {
+ // No permacrud for this method type on this class
+
+ growing_buffer* msg = buffer_init( 128 );
+ buffer_fadd(
+ msg,
+ "%s: %s on class %s has no permacrud IDL entry",
+ modulename,
+ osrfHashGet( method_metadata, "methodtype" ),
+ osrfHashGet( class, "classname" )
+ );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_FORBIDDEN,
+ "osrfMethodException", ctx->request, m );
+
+ free( m );
+
+ return 0;
+ }
+
+ // Get the user id, and make sure the user is logged in
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return 0; // Not logged in? No access.
+
+ int userid = atoi( oilsFMGetStringConst( user, "id" ) );
+
+ // Get a list of permissions from the permacrud entry.
+ osrfStringArray* permission = osrfHashGet( pcrud, "permission" );
+ if( permission->size == 0 ) {
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "No permissions required for this action (class %s), passing through",
+ osrfHashGet(class, "classname")
+ );
+ return 1;
+ }
+
+ // Build a list of org units that own the row. This is fairly convoluted because there
+ // are several different ways that an org unit may own the row, as defined by the
+ // permacrud entry.
+
+ // Local context means that the row includes a foreign key pointing to actor.org_unit,
+ // identifying an owning org_unit..
+ osrfStringArray* local_context = osrfHashGet( pcrud, "local_context" );
+
+ // Foreign context adds a layer of indirection. The row points to some other row that
+ // an org unit may own. The "jump" attribute, if present, adds another layer of
+ // indirection.
+ osrfHash* foreign_context = osrfHashGet( pcrud, "foreign_context" );
+
+ // The following string array stores the list of org units. (We don't have a thingie
+ // for storing lists of integers, so we fake it with a list of strings.)
+ osrfStringArray* context_org_array = osrfNewStringArray( 1 );
+
+ int err = 0;
+ const char* pkey_value = NULL;
+ if( str_is_true( osrfHashGet(pcrud, "global_required") ) ) {
+ // If the global_required attribute is present and true, then the only owning
+ // org unit is the root org unit, i.e. the one with no parent.
+ osrfLogDebug( OSRF_LOG_MARK,
+ "global-level permissions required, fetching top of the org tree" );
+
+ // no need to check perms for org tree root retrieval
+ osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+ // check for perm at top of org tree
+ const char* org_tree_root_id = org_tree_root( ctx );
+ osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+ if( org_tree_root_id ) {
+ osrfStringArrayAdd( context_org_array, org_tree_root_id );
+ osrfLogDebug( OSRF_LOG_MARK, "top of the org tree is %s", org_tree_root_id );
+ } else {
+ osrfStringArrayFree( context_org_array );
+ return 0;
+ }
+
+ } else {
+ // If the global_required attribute is absent or false, then we look for
+ // local and/or foreign context. In order to find the relevant foreign
+ // keys, we must either read the relevant row from the database, or look at
+ // the image of the row that we already have in memory.
+
+ // Even if we have an image of the row in memory, that image may not include the
+ // foreign key column(s) that we need. So whenever possible, we do a fresh read
+ // of the row to make sure that we have what we need.
+
+ osrfLogDebug( OSRF_LOG_MARK, "global-level permissions not required, "
+ "fetching context org ids" );
+ const char* pkey = osrfHashGet( class, "primarykey" );
+ jsonObject *param = NULL;
+
+ if( !pkey ) {
+ // There is no primary key, so we can't do a fresh lookup. Use the row
+ // image that we already have. If it doesn't have everything we need, too bad.
+ fetch = 0;
+ param = jsonObjectClone( obj );
+ osrfLogDebug( OSRF_LOG_MARK, "No primary key; using clone of object" );
+ } else if( obj->classname ) {
+ pkey_value = oilsFMGetStringConst( obj, pkey );
+ if( !fetch )
+ param = jsonObjectClone( obj );
+ osrfLogDebug( OSRF_LOG_MARK, "Object supplied, using primary key value of %s",
+ pkey_value );
+ } else {
+ pkey_value = jsonObjectGetString( obj );
+ fetch = 1;
+ osrfLogDebug( OSRF_LOG_MARK, "Object not supplied, using primary key value "
+ "of %s and retrieving from the database", pkey_value );
+ }
+
+ if( fetch ) {
+ // Fetch the row so that we can look at the foreign key(s)
+ osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+ jsonObject* _tmp_params = single_hash( pkey, pkey_value );
+ jsonObject* _list = doFieldmapperSearch( ctx, class, _tmp_params, NULL, &err );
+ jsonObjectFree( _tmp_params );
+ osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+ param = jsonObjectExtractIndex( _list, 0 );
+ jsonObjectFree( _list );
+ }
+
+ if( !param ) {
+ // The row doesn't exist. Complain, and deny access.
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Object not found in the database with primary key %s of %s",
+ pkey, pkey_value );
+
+ growing_buffer* msg = buffer_init( 128 );
+ buffer_fadd(
+ msg,
+ "%s: no object found with primary key %s of %s",
+ modulename,
+ pkey,
+ pkey_value
+ );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ m
+ );
+
+ free( m );
+ return 0;
+ }
+
+ if( local_context && local_context->size > 0 ) {
+ // The IDL provides a list of column names for the foreign keys denoting
+ // local context, i.e. columns identifying owing org units directly. Look up
+ // the value of each one, and if it isn't null, add it to the list of org units.
+ osrfLogDebug( OSRF_LOG_MARK, "%d class-local context field(s) specified",
+ local_context->size );
+ int i = 0;
+ const char* lcontext = NULL;
+ while ( (lcontext = osrfStringArrayGetString(local_context, i++)) ) {
+ const char* fkey_value = oilsFMGetStringConst( param, lcontext );
+ if( fkey_value ) { // if not null
+ osrfStringArrayAdd( context_org_array, fkey_value );
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "adding class-local field %s (value: %s) to the context org list",
+ lcontext,
+ osrfStringArrayGetString( context_org_array, context_org_array->size - 1 )
+ );
+ }
+ }
+ }
+
+ if( foreign_context ) {
+ unsigned long class_count = osrfHashGetCount( foreign_context );
+ osrfLogDebug( OSRF_LOG_MARK, "%d foreign context classes(s) specified", class_count );
+
+ if( class_count > 0 ) {
+
+ // The IDL provides a list of foreign key columns pointing to rows that
+ // an org unit may own. Follow each link, identify the owning org unit,
+ // and add it to the list.
+ osrfHash* fcontext = NULL;
+ osrfHashIterator* class_itr = osrfNewHashIterator( foreign_context );
+ while( (fcontext = osrfHashIteratorNext( class_itr )) ) {
+ // For each class to which a foreign key points:
+ const char* class_name = osrfHashIteratorKey( class_itr );
+ osrfHash* fcontext = osrfHashGet( foreign_context, class_name );
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "%d foreign context fields(s) specified for class %s",
+ ((osrfStringArray*)osrfHashGet(fcontext,"context"))->size,
+ class_name
+ );
+
+ // Get the name of the key field in the foreign table
+ const char* foreign_pkey = osrfHashGet( fcontext, "field" );
+
+ // Get the value of the foreign key pointing to the foreign table
+ char* foreign_pkey_value =
+ oilsFMGetString( param, osrfHashGet( fcontext, "fkey" ));
+ if( !foreign_pkey_value )
+ continue; // Foreign key value is null; skip it
+
+ // Look up the row to which the foreign key points
+ jsonObject* _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
+
+ osrfHashSet((osrfHash*) ctx->session->userData, "1", "inside_verify");
+ jsonObject* _list = doFieldmapperSearch(
+ ctx, osrfHashGet( oilsIDL(), class_name ), _tmp_params, NULL, &err );
+ osrfHashSet((osrfHash*) ctx->session->userData, "0", "inside_verify");
+
+ jsonObject* _fparam = NULL;
+ if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
+ _fparam = jsonObjectExtractIndex( _list, 0 );
+
+ jsonObjectFree( _tmp_params );
+ jsonObjectFree( _list );
+
+ // At this point _fparam either points to the row identified by the
+ // foreign key, or it's NULL (no such row found).
+
+ osrfStringArray* jump_list = osrfHashGet( fcontext, "jump" );
+
+ const char* bad_class = NULL; // For noting failed lookups
+ if( ! _fparam )
+ bad_class = class_name; // Referenced row not found
+ else if( jump_list ) {
+ // Follow a chain of rows, linked by foreign keys, to find an owner
+ const char* flink = NULL;
+ int k = 0;
+ while ( (flink = osrfStringArrayGetString(jump_list, k++)) && _fparam ) {
+ // For each entry in the jump list. Each entry (i.e. flink) is
+ // the name of a foreign key column in the current row.
+
+ // From the IDL, get the linkage information for the next jump
+ osrfHash* foreign_link_hash =
+ oilsIDLFindPath( "/%s/links/%s", _fparam->classname, flink );
+
+ // Get the class metadata for the class
+ // to which the foreign key points
+ osrfHash* foreign_class_meta = osrfHashGet( oilsIDL(),
+ osrfHashGet( foreign_link_hash, "class" ));
+
+ // Get the name of the referenced key of that class
+ foreign_pkey = osrfHashGet( foreign_link_hash, "key" );
+
+ // Get the value of the foreign key pointing to that class
+ free( foreign_pkey_value );
+ foreign_pkey_value = oilsFMGetString( _fparam, flink );
+ if( !foreign_pkey_value )
+ break; // Foreign key is null; quit looking
+
+ // Build a WHERE clause for the lookup
+ _tmp_params = single_hash( foreign_pkey, foreign_pkey_value );
+
+ // Do the lookup
+ _list = doFieldmapperSearch( ctx, foreign_class_meta,
+ _tmp_params, NULL, &err );
+
+ // Get the resulting row
+ jsonObjectFree( _fparam );
+ if( _list && JSON_ARRAY == _list->type && _list->size > 0 )
+ _fparam = jsonObjectExtractIndex( _list, 0 );
+ else {
+ // Referenced row not found
+ _fparam = NULL;
+ bad_class = osrfHashGet( foreign_link_hash, "class" );
+ }
+
+ jsonObjectFree( _tmp_params );
+ jsonObjectFree( _list );
+ }
+ }
+
+ if( bad_class ) {
+
+ // We had a foreign key pointing to such-and-such a row, but then
+ // we couldn't fetch that row. The data in the database are in an
+ // inconsistent state; the database itself may even be corrupted.
+ growing_buffer* msg = buffer_init( 128 );
+ buffer_fadd(
+ msg,
+ "%s: no object of class %s found with primary key %s of %s",
+ modulename,
+ bad_class,
+ foreign_pkey,
+ foreign_pkey_value ? foreign_pkey_value : "(null)"
+ );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ m
+ );
+
+ free( m );
+ osrfHashIteratorFree( class_itr );
+ free( foreign_pkey_value );
+ jsonObjectFree( param );
+
+ return 0;
+ }
+
+ free( foreign_pkey_value );
+
+ if( _fparam ) {
+ // Examine each context column of the foreign row,
+ // and add its value to the list of org units.
+ int j = 0;
+ const char* foreign_field = NULL;
+ osrfStringArray* ctx_array = osrfHashGet( fcontext, "context" );
+ while ( (foreign_field = osrfStringArrayGetString( ctx_array, j++ )) ) {
+ osrfStringArrayAdd( context_org_array,
+ oilsFMGetStringConst( _fparam, foreign_field ));
+ osrfLogDebug( OSRF_LOG_MARK,
+ "adding foreign class %s field %s (value: %s) "
+ "to the context org list",
+ class_name,
+ foreign_field,
+ osrfStringArrayGetString(
+ context_org_array, context_org_array->size - 1 )
+ );
+ }
+
+ jsonObjectFree( _fparam );
+ }
+ }
+
+ osrfHashIteratorFree( class_itr );
+ }
+ }
+
+ jsonObjectFree( param );
+ }
+
+ const char* context_org = NULL;
+ const char* perm = NULL;
+ int OK = 0;
+
+ // For every combination of permission and context org unit: call a stored procedure
+ // to determine if the user has this permission in the context of this org unit.
+ // If the answer is yes at any point, then we're done, and the user has permission.
+ // In other words permissions are additive.
+ int i = 0;
+ while( (perm = osrfStringArrayGetString(permission, i++)) ) {
+ dbi_result result;
+
+ osrfStringArray* pcache = NULL;
+ if (rs_size > perm_at_threshold) { // grab and cache locations of user perms
+ pcache = getPermLocationCache(ctx, perm);
+
+ if (!pcache) {
+ pcache = osrfNewStringArray(0);
+
+ result = dbi_conn_queryf(
+ writehandle,
+ "SELECT permission.usr_has_perm_at_all(%d, '%s') AS at;",
+ userid,
+ perm
+ );
+
+ if( result ) {
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Received a result for permission [%s] for user %d",
+ perm,
+ userid
+ );
+
+ if( dbi_result_first_row( result )) {
+ do {
+ jsonObject* return_val = oilsMakeJSONFromResult( result );
+ osrfStringArrayAdd( pcache, jsonObjectGetString( jsonObjectGetKeyConst( return_val, "at" ) ) );
+ jsonObjectFree( return_val );
+ } while( dbi_result_next_row( result ));
+
+ setPermLocationCache(ctx, perm, pcache);
+ }
+
+ dbi_result_free( result );
+ }
+ }
+ }
+
+ int j = 0;
+ while( (context_org = osrfStringArrayGetString( context_org_array, j++ )) ) {
+
+ if (rs_size > perm_at_threshold) {
+ if (osrfStringArrayContains( pcache, context_org )) {
+ OK = 1;
+ break;
+ }
+ }
+
+ if( pkey_value ) {
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Checking object permission [%s] for user %d "
+ "on object %s (class %s) at org %d",
+ perm,
+ userid,
+ pkey_value,
+ osrfHashGet( class, "classname" ),
+ atoi( context_org )
+ );
+
+ result = dbi_conn_queryf(
+ writehandle,
+ "SELECT permission.usr_has_object_perm(%d, '%s', '%s', '%s', %d) AS has_perm;",
+ userid,
+ perm,
+ osrfHashGet( class, "classname" ),
+ pkey_value,
+ atoi( context_org )
+ );
+
+ if( result ) {
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Received a result for object permission [%s] "
+ "for user %d on object %s (class %s) at org %d",
+ perm,
+ userid,
+ pkey_value,
+ osrfHashGet( class, "classname" ),
+ atoi( context_org )
+ );
+
+ if( dbi_result_first_row( result )) {
+ jsonObject* return_val = oilsMakeJSONFromResult( result );
+ const char* has_perm = jsonObjectGetString(
+ jsonObjectGetKeyConst( return_val, "has_perm" ));
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Status of object permission [%s] for user %d "
+ "on object %s (class %s) at org %d is %s",
+ perm,
+ userid,
+ pkey_value,
+ osrfHashGet(class, "classname"),
+ atoi(context_org),
+ has_perm
+ );
+
+ if( *has_perm == 't' )
+ OK = 1;
+ jsonObjectFree( return_val );
+ }
+
+ dbi_result_free( result );
+ if( OK )
+ break;
+ } else {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogWarning( OSRF_LOG_MARK,
+ "Unable to call check object permissions: %d, %s",
+ errnum, msg ? msg : "(No description available)" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ }
+ }
+
+ if (rs_size > perm_at_threshold) break;
+
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Checking non-object permission [%s] for user %d at org %d",
+ perm, userid, atoi(context_org) );
+ result = dbi_conn_queryf(
+ writehandle,
+ "SELECT permission.usr_has_perm(%d, '%s', %d) AS has_perm;",
+ userid,
+ perm,
+ atoi( context_org )
+ );
+
+ if( result ) {
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Received a result for permission [%s] for user %d at org %d",
+ perm, userid, atoi( context_org ));
+ if( dbi_result_first_row( result )) {
+ jsonObject* return_val = oilsMakeJSONFromResult( result );
+ const char* has_perm = jsonObjectGetString(
+ jsonObjectGetKeyConst( return_val, "has_perm" ));
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Status of permission [%s] for user %d at org %d is [%s]",
+ perm, userid, atoi( context_org ), has_perm );
+ if( *has_perm == 't' )
+ OK = 1;
+ jsonObjectFree( return_val );
+ }
+
+ dbi_result_free( result );
+ if( OK )
+ break;
+ } else {
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogWarning( OSRF_LOG_MARK, "Unable to call user object permissions: %d, %s",
+ errnum, msg ? msg : "(No description available)" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ }
+
+ }
+
+ if( OK )
+ break;
+ }
+
+ osrfStringArrayFree( context_org_array );
+
+ return OK;
+}
+
+/**
+ @brief Look up the root of the org_unit tree.
+ @param ctx Pointer to the method context.
+ @return The id of the root org unit, as a character string.
+
+ Query actor.org_unit where parent_ou is null, and return the id as a string.
+
+ This function assumes that there is only one root org unit, i.e. that we
+ have a single tree, not a forest.
+
+ The calling code is responsible for freeing the returned string.
+*/
+static const char* org_tree_root( osrfMethodContext* ctx ) {
+
+ static char cached_root_id[ 32 ] = ""; // extravagantly large buffer
+ static time_t last_lookup_time = 0;
+ time_t current_time = time( NULL );
+
+ if( cached_root_id[ 0 ] && ( current_time - last_lookup_time < 3600 ) ) {
+ // We successfully looked this up less than an hour ago.
+ // It's not likely to have changed since then.
+ return strdup( cached_root_id );
+ }
+ last_lookup_time = current_time;
+
+ int err = 0;
+ jsonObject* where_clause = single_hash( "parent_ou", NULL );
+ jsonObject* result = doFieldmapperSearch(
+ ctx, osrfHashGet( oilsIDL(), "aou" ), where_clause, NULL, &err );
+ jsonObjectFree( where_clause );
+
+ jsonObject* tree_top = jsonObjectGetIndex( result, 0 );
+
+ if( !tree_top ) {
+ jsonObjectFree( result );
+
+ growing_buffer* msg = buffer_init( 128 );
+ OSRF_BUFFER_ADD( msg, modulename );
+ OSRF_BUFFER_ADD( msg,
+ ": Internal error, could not find the top of the org tree (parent_ou = NULL)" );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus( ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR, "osrfMethodException", ctx->request, m );
+ free( m );
+
+ cached_root_id[ 0 ] = '\0';
+ return NULL;
+ }
+
+ const char* root_org_unit_id = oilsFMGetStringConst( tree_top, "id" );
+ osrfLogDebug( OSRF_LOG_MARK, "Top of the org tree is %s", root_org_unit_id );
+
+ strcpy( cached_root_id, root_org_unit_id );
+ jsonObjectFree( result );
+ return cached_root_id;
+}
+
+/**
+ @brief Create a JSON_HASH with a single key/value pair.
+ @param key The key of the key/value pair.
+ @param value the value of the key/value pair.
+ @return Pointer to a newly created jsonObject of type JSON_HASH.
+
+ The value of the key/value is either a string or (if @a value is NULL) a null.
+*/
+static jsonObject* single_hash( const char* key, const char* value ) {
+ // Sanity check
+ if( ! key ) key = "";
+
+ jsonObject* hash = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( hash, key, jsonNewObject( value ) );
+ return hash;
+}
+
+
+int doCreate( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+ jsonObject* target = NULL;
+ jsonObject* options = NULL;
+
+ if( enforce_pcrud ) {
+ target = jsonObjectGetIndex( ctx->params, 1 );
+ options = jsonObjectGetIndex( ctx->params, 2 );
+ } else {
+ target = jsonObjectGetIndex( ctx->params, 0 );
+ options = jsonObjectGetIndex( ctx->params, 1 );
+ }
+
+ if( !verifyObjectClass( ctx, target )) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "Object seems to be of the correct type" );
+
+ const char* trans_id = getXactId( ctx );
+ if( !trans_id ) {
+ osrfLogError( OSRF_LOG_MARK, "No active transaction -- required for CREATE" );
+
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for CREATE"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // The following test is harmless but redundant. If a class is
+ // readonly, we don't register a create method for it.
+ if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "Cannot INSERT readonly class"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // Set the last_xact_id
+ int index = oilsIDL_ntop( target->classname, "last_xact_id" );
+ if( index > -1 ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Setting last_xact_id to %s on %s at position %d",
+ trans_id, target->classname, index);
+ jsonObjectSetIndex( target, index, jsonNewObject( trans_id ));
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "There is a transaction running..." );
+
+ dbhandle = writehandle;
+
+ osrfHash* fields = osrfHashGet( meta, "fields" );
+ char* pkey = osrfHashGet( meta, "primarykey" );
+ char* seq = osrfHashGet( meta, "sequence" );
+
+ growing_buffer* table_buf = buffer_init( 128 );
+ growing_buffer* col_buf = buffer_init( 128 );
+ growing_buffer* val_buf = buffer_init( 128 );
+
+ OSRF_BUFFER_ADD( table_buf, "INSERT INTO " );
+ OSRF_BUFFER_ADD( table_buf, osrfHashGet( meta, "tablename" ));
+ OSRF_BUFFER_ADD_CHAR( col_buf, '(' );
+ buffer_add( val_buf,"VALUES (" );
+
+
+ int first = 1;
+ osrfHash* field = NULL;
+ osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+ while( (field = osrfHashIteratorNext( field_itr ) ) ) {
+
+ const char* field_name = osrfHashIteratorKey( field_itr );
+
+ if( str_is_true( osrfHashGet( field, "virtual" ) ) )
+ continue;
+
+ const jsonObject* field_object = oilsFMGetObject( target, field_name );
+
+ char* value;
+ if( field_object && field_object->classname ) {
+ value = oilsFMGetString(
+ field_object,
+ (char*)oilsIDLFindPath( "/%s/primarykey", field_object->classname )
+ );
+ } else if( field_object && JSON_BOOL == field_object->type ) {
+ if( jsonBoolIsTrue( field_object ) )
+ value = strdup( "t" );
+ else
+ value = strdup( "f" );
+ } else {
+ value = jsonObjectToSimpleString( field_object );
+ }
+
+ if( first ) {
+ first = 0;
+ } else {
+ OSRF_BUFFER_ADD_CHAR( col_buf, ',' );
+ OSRF_BUFFER_ADD_CHAR( val_buf, ',' );
+ }
+
+ buffer_add( col_buf, field_name );
+
+ if( !field_object || field_object->type == JSON_NULL ) {
+ buffer_add( val_buf, "DEFAULT" );
+
+ } else if( !strcmp( get_primitive( field ), "number" )) {
+ const char* numtype = get_datatype( field );
+ if( !strcmp( numtype, "INT8" )) {
+ buffer_fadd( val_buf, "%lld", atoll( value ));
+
+ } else if( !strcmp( numtype, "INT" )) {
+ buffer_fadd( val_buf, "%d", atoi( value ));
+
+ } else if( !strcmp( numtype, "NUMERIC" )) {
+ buffer_fadd( val_buf, "%f", atof( value ));
+ }
+ } else {
+ if( dbi_conn_quote_string( writehandle, &value )) {
+ OSRF_BUFFER_ADD( val_buf, value );
+
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]", modulename, value );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Error quoting string -- please see the error log for more details"
+ );
+ free( value );
+ buffer_free( table_buf );
+ buffer_free( col_buf );
+ buffer_free( val_buf );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+ }
+
+ free( value );
+ }
+
+ osrfHashIteratorFree( field_itr );
+
+ OSRF_BUFFER_ADD_CHAR( col_buf, ')' );
+ OSRF_BUFFER_ADD_CHAR( val_buf, ')' );
+
+ char* table_str = buffer_release( table_buf );
+ char* col_str = buffer_release( col_buf );
+ char* val_str = buffer_release( val_buf );
+ growing_buffer* sql = buffer_init( 128 );
+ buffer_fadd( sql, "%s %s %s;", table_str, col_str, val_str );
+ free( table_str );
+ free( col_str );
+ free( val_str );
+
+ char* query = buffer_release( sql );
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s: Insert SQL [%s]", modulename, query );
+
+ jsonObject* obj = NULL;
+ int rc = 0;
+
+ dbi_result result = dbi_conn_query( writehandle, query );
+ if( !result ) {
+ obj = jsonNewObject( NULL );
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR inserting %s object using query [%s]: %d %s",
+ modulename,
+ osrfHashGet(meta, "fieldmapper"),
+ query,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "INSERT error -- please see the error log for more details"
+ );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ rc = -1;
+ } else {
+ dbi_result_free( result );
+
+ char* id = oilsFMGetString( target, pkey );
+ if( !id ) {
+ unsigned long long new_id = dbi_conn_sequence_last( writehandle, seq );
+ growing_buffer* _id = buffer_init( 10 );
+ buffer_fadd( _id, "%lld", new_id );
+ id = buffer_release( _id );
+ }
+
+ // Find quietness specification, if present
+ const char* quiet_str = NULL;
+ if( options ) {
+ const jsonObject* quiet_obj = jsonObjectGetKeyConst( options, "quiet" );
+ if( quiet_obj )
+ quiet_str = jsonObjectGetString( quiet_obj );
+ }
+
+ if( str_is_true( quiet_str )) { // if quietness is specified
+ obj = jsonNewObject( id );
+ }
+ else {
+
+ // Fetch the row that we just inserted, so that we can return it to the client
+ jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( where_clause, pkey, jsonNewObject( id ));
+
+ int err = 0;
+ jsonObject* list = doFieldmapperSearch( ctx, meta, where_clause, NULL, &err );
+ if( err )
+ rc = -1;
+ else
+ obj = jsonObjectClone( jsonObjectGetIndex( list, 0 ));
+
+ jsonObjectFree( list );
+ jsonObjectFree( where_clause );
+ }
+
+ free( id );
+ }
+
+ free( query );
+ osrfAppRespondComplete( ctx, obj );
+ jsonObjectFree( obj );
+ return rc;
+}
+
+/**
+ @brief Implement the retrieve method.
+ @param ctx Pointer to the method context.
+ @param err Pointer through which to return an error code.
+ @return If successful, a pointer to the result to be returned to the client;
+ otherwise NULL.
+
+ From the method's class, fetch a row with a specified value in the primary key. This
+ method relies on the database design convention that a primary key consists of a single
+ column.
+
+ Method parameters:
+ - authkey (PCRUD only)
+ - value of the primary key for the desired row, for building the WHERE clause
+ - a JSON_HASH containing any other SQL clauses: select, join, etc.
+
+ Return to client: One row from the query.
+*/
+int doRetrieve( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ int id_pos = 0;
+ int order_pos = 1;
+
+ if( enforce_pcrud ) {
+ id_pos = 1;
+ order_pos = 2;
+ }
+
+ // Get the class metadata
+ osrfHash* class_def = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+ // Get the value of the primary key, from a method parameter
+ const jsonObject* id_obj = jsonObjectGetIndex( ctx->params, id_pos );
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "%s retrieving %s object with primary key value of %s",
+ modulename,
+ osrfHashGet( class_def, "fieldmapper" ),
+ jsonObjectGetString( id_obj )
+ );
+
+ // Build a WHERE clause based on the key value
+ jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey(
+ where_clause,
+ osrfHashGet( class_def, "primarykey" ), // name of key column
+ jsonObjectClone( id_obj ) // value of key column
+ );
+
+ jsonObject* rest_of_query = jsonObjectGetIndex( ctx->params, order_pos );
+
+ // Do the query
+ int err = 0;
+ jsonObject* list = doFieldmapperSearch( ctx, class_def, where_clause, rest_of_query, &err );
+
+ jsonObjectFree( where_clause );
+ if( err ) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ jsonObject* obj = jsonObjectExtractIndex( list, 0 );
+ jsonObjectFree( list );
+
+ if( enforce_pcrud ) {
+ // no result, skip this entirely
+ if(NULL != obj && !verifyObjectPCRUD( ctx, class_def, obj, 1 )) {
+ jsonObjectFree( obj );
+
+ growing_buffer* msg = buffer_init( 128 );
+ OSRF_BUFFER_ADD( msg, modulename );
+ OSRF_BUFFER_ADD( msg, ": Insufficient permissions to retrieve object" );
+
+ char* m = buffer_release( msg );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_NOTALLOWED, "osrfMethodException",
+ ctx->request, m );
+ free( m );
+
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+ }
+
+ // doFieldmapperSearch() now does the responding for us
+ //osrfAppRespondComplete( ctx, obj );
+ osrfAppRespondComplete( ctx, NULL );
+
+ jsonObjectFree( obj );
+ return 0;
+}
+
+/**
+ @brief Translate a numeric value to a string representation for the database.
+ @param field Pointer to the IDL field definition.
+ @param value Pointer to a jsonObject holding the value of a field.
+ @return Pointer to a newly allocated string.
+
+ The input object is typically a JSON_NUMBER, but it may be a JSON_STRING as long as
+ its contents are numeric. A non-numeric string is likely to result in invalid SQL,
+ or (what is worse) valid SQL that is wrong.
+
+ If the datatype of the receiving field is not numeric, wrap the value in quotes.
+
+ The calling code is responsible for freeing the resulting string by calling free().
+*/
+static char* jsonNumberToDBString( osrfHash* field, const jsonObject* value ) {
+ growing_buffer* val_buf = buffer_init( 32 );
+ const char* numtype = get_datatype( field );
+
+ // For historical reasons the following contains cruft that could be cleaned up.
+ if( !strncmp( numtype, "INT", 3 ) ) {
+ if( value->type == JSON_NUMBER )
+ //buffer_fadd( val_buf, "%ld", (long)jsonObjectGetNumber(value) );
+ buffer_fadd( val_buf, jsonObjectGetString( value ) );
+ else {
+ buffer_fadd( val_buf, jsonObjectGetString( value ) );
+ }
+
+ } else if( !strcmp( numtype, "NUMERIC" )) {
+ if( value->type == JSON_NUMBER )
+ buffer_fadd( val_buf, jsonObjectGetString( value ));
+ else {
+ buffer_fadd( val_buf, jsonObjectGetString( value ));
+ }
+
+ } else {
+ // Presumably this was really intended to be a string, so quote it
+ char* str = jsonObjectToSimpleString( value );
+ if( dbi_conn_quote_string( dbhandle, &str )) {
+ OSRF_BUFFER_ADD( val_buf, str );
+ free( str );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]", modulename, str );
+ free( str );
+ buffer_free( val_buf );
+ return NULL;
+ }
+ }
+
+ return buffer_release( val_buf );
+}
+
+static char* searchINPredicate( const char* class_alias, osrfHash* field,
+ jsonObject* node, const char* op, osrfMethodContext* ctx ) {
+ growing_buffer* sql_buf = buffer_init( 32 );
+
+ buffer_fadd(
+ sql_buf,
+ "\"%s\".%s ",
+ class_alias,
+ osrfHashGet( field, "name" )
+ );
+
+ if( !op ) {
+ buffer_add( sql_buf, "IN (" );
+ } else if( !strcasecmp( op,"not in" )) {
+ buffer_add( sql_buf, "NOT IN (" );
+ } else {
+ buffer_add( sql_buf, "IN (" );
+ }
+
+ if( node->type == JSON_HASH ) {
+ // subquery predicate
+ char* subpred = buildQuery( ctx, node, SUBSELECT );
+ if( ! subpred ) {
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_add( sql_buf, subpred );
+ free( subpred );
+
+ } else if( node->type == JSON_ARRAY ) {
+ // literal value list
+ int in_item_index = 0;
+ int in_item_first = 1;
+ const jsonObject* in_item;
+ while( (in_item = jsonObjectGetIndex( node, in_item_index++ )) ) {
+
+ if( in_item_first )
+ in_item_first = 0;
+ else
+ buffer_add( sql_buf, ", " );
+
+ // Sanity check
+ if( in_item->type != JSON_STRING && in_item->type != JSON_NUMBER ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Expected string or number within IN list; found %s",
+ modulename, json_type( in_item->type ) );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ // Append the literal value -- quoted if not a number
+ if( JSON_NUMBER == in_item->type ) {
+ char* val = jsonNumberToDBString( field, in_item );
+ OSRF_BUFFER_ADD( sql_buf, val );
+ free( val );
+
+ } else if( !strcmp( get_primitive( field ), "number" )) {
+ char* val = jsonNumberToDBString( field, in_item );
+ OSRF_BUFFER_ADD( sql_buf, val );
+ free( val );
+
+ } else {
+ char* key_string = jsonObjectToSimpleString( in_item );
+ if( dbi_conn_quote_string( dbhandle, &key_string )) {
+ OSRF_BUFFER_ADD( sql_buf, key_string );
+ free( key_string );
+ } else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error quoting key string [%s]", modulename, key_string );
+ free( key_string );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+ }
+ }
+
+ if( in_item_first ) {
+ osrfLogError(OSRF_LOG_MARK, "%s: Empty IN list", modulename );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Expected object or array for IN clause; found %s",
+ modulename, json_type( node->type ));
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ OSRF_BUFFER_ADD_CHAR( sql_buf, ')' );
+
+ return buffer_release( sql_buf );
+}
+
+// Receive a JSON_ARRAY representing a function call. The first
+// entry in the array is the function name. The rest are parameters.
+static char* searchValueTransform( const jsonObject* array ) {
+
+ if( array->size < 1 ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Empty array for value transform", modulename );
+ return NULL;
+ }
+
+ // Get the function name
+ jsonObject* func_item = jsonObjectGetIndex( array, 0 );
+ if( func_item->type != JSON_STRING ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error: expected function name, found %s",
+ modulename, json_type( func_item->type ));
+ return NULL;
+ }
+
+ growing_buffer* sql_buf = buffer_init( 32 );
+
+ OSRF_BUFFER_ADD( sql_buf, jsonObjectGetString( func_item ) );
+ OSRF_BUFFER_ADD( sql_buf, "( " );
+
+ // Get the parameters
+ int func_item_index = 1; // We already grabbed the zeroth entry
+ while( (func_item = jsonObjectGetIndex( array, func_item_index++ )) ) {
+
+ // Add a separator comma, if we need one
+ if( func_item_index > 2 )
+ buffer_add( sql_buf, ", " );
+
+ // Add the current parameter
+ if( func_item->type == JSON_NULL ) {
+ buffer_add( sql_buf, "NULL" );
+ } else {
+ if( func_item->type == JSON_BOOL ) {
+ if( jsonBoolIsTrue(func_item) ) {
+ buffer_add( sql_buf, "TRUE" );
+ } else {
+ buffer_add( sql_buf, "FALSE" );
+ }
+ } else {
+ char* val = jsonObjectToSimpleString( func_item );
+ if( dbi_conn_quote_string( dbhandle, &val )) {
+ OSRF_BUFFER_ADD( sql_buf, val );
+ free( val );
+ } else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error quoting key string [%s]", modulename, val );
+ buffer_free( sql_buf );
+ free( val );
+ return NULL;
+ }
+ }
+ }
+ }
+
+ buffer_add( sql_buf, " )" );
+
+ return buffer_release( sql_buf );
+}
+
+static char* searchFunctionPredicate( const char* class_alias, osrfHash* field,
+ const jsonObject* node, const char* op ) {
+
+ if( ! is_good_operator( op ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Invalid operator [%s]", modulename, op );
+ return NULL;
+ }
+
+ char* val = searchValueTransform( node );
+ if( !val )
+ return NULL;
+
+ growing_buffer* sql_buf = buffer_init( 32 );
+ buffer_fadd(
+ sql_buf,
+ "\"%s\".%s %s %s",
+ class_alias,
+ osrfHashGet( field, "name" ),
+ op,
+ val
+ );
+
+ free( val );
+
+ return buffer_release( sql_buf );
+}
+
+// class_alias is a class name or other table alias
+// field is a field definition as stored in the IDL
+// node comes from the method parameter, and may represent an entry in the SELECT list
+static char* searchFieldTransform( const char* class_alias, osrfHash* field,
+ const jsonObject* node ) {
+ growing_buffer* sql_buf = buffer_init( 32 );
+
+ const char* field_transform = jsonObjectGetString(
+ jsonObjectGetKeyConst( node, "transform" ) );
+ const char* transform_subcolumn = jsonObjectGetString(
+ jsonObjectGetKeyConst( node, "result_field" ) );
+
+ if( transform_subcolumn ) {
+ if( ! is_identifier( transform_subcolumn ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Invalid subfield name: \"%s\"\n",
+ modulename, transform_subcolumn );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+ OSRF_BUFFER_ADD_CHAR( sql_buf, '(' ); // enclose transform in parentheses
+ }
+
+ if( field_transform ) {
+
+ if( ! is_identifier( field_transform ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Expected function name, found \"%s\"\n",
+ modulename, field_transform );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ if( obj_is_true( jsonObjectGetKeyConst( node, "distinct" ) ) ) {
+ buffer_fadd( sql_buf, "%s(DISTINCT \"%s\".%s",
+ field_transform, class_alias, osrfHashGet( field, "name" ));
+ } else {
+ buffer_fadd( sql_buf, "%s(\"%s\".%s",
+ field_transform, class_alias, osrfHashGet( field, "name" ));
+ }
+
+ const jsonObject* array = jsonObjectGetKeyConst( node, "params" );
+
+ if( array ) {
+ if( array->type != JSON_ARRAY ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Expected JSON_ARRAY for function params; found %s",
+ modulename, json_type( array->type ) );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+ int func_item_index = 0;
+ jsonObject* func_item;
+ while( (func_item = jsonObjectGetIndex( array, func_item_index++ ))) {
+
+ char* val = jsonObjectToSimpleString( func_item );
+
+ if( !val ) {
+ buffer_add( sql_buf, ",NULL" );
+ } else if( dbi_conn_quote_string( dbhandle, &val )) {
+ OSRF_BUFFER_ADD_CHAR( sql_buf, ',' );
+ OSRF_BUFFER_ADD( sql_buf, val );
+ } else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error quoting key string [%s]", modulename, val );
+ free( val );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+ free( val );
+ }
+ }
+
+ buffer_add( sql_buf, " )" );
+
+ } else {
+ buffer_fadd( sql_buf, "\"%s\".%s", class_alias, osrfHashGet( field, "name" ));
+ }
+
+ if( transform_subcolumn )
+ buffer_fadd( sql_buf, ").\"%s\"", transform_subcolumn );
+
+ return buffer_release( sql_buf );
+}
+
+static char* searchFieldTransformPredicate( const ClassInfo* class_info, osrfHash* field,
+ const jsonObject* node, const char* op ) {
+
+ if( ! is_good_operator( op ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error: Invalid operator %s", modulename, op );
+ return NULL;
+ }
+
+ char* field_transform = searchFieldTransform( class_info->alias, field, node );
+ if( ! field_transform )
+ return NULL;
+ char* value = NULL;
+ int extra_parens = 0; // boolean
+
+ const jsonObject* value_obj = jsonObjectGetKeyConst( node, "value" );
+ if( ! value_obj ) {
+ value = searchWHERE( node, class_info, AND_OP_JOIN, NULL );
+ if( !value ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error building condition for field transform",
+ modulename );
+ free( field_transform );
+ return NULL;
+ }
+ extra_parens = 1;
+ } else if( value_obj->type == JSON_ARRAY ) {
+ value = searchValueTransform( value_obj );
+ if( !value ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error building value transform for field transform", modulename );
+ free( field_transform );
+ return NULL;
+ }
+ } else if( value_obj->type == JSON_HASH ) {
+ value = searchWHERE( value_obj, class_info, AND_OP_JOIN, NULL );
+ if( !value ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error building predicate for field transform",
+ modulename );
+ free( field_transform );
+ return NULL;
+ }
+ extra_parens = 1;
+ } else if( value_obj->type == JSON_NUMBER ) {
+ value = jsonNumberToDBString( field, value_obj );
+ } else if( value_obj->type == JSON_NULL ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error building predicate for field transform: null value", modulename );
+ free( field_transform );
+ return NULL;
+ } else if( value_obj->type == JSON_BOOL ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Error building predicate for field transform: boolean value", modulename );
+ free( field_transform );
+ return NULL;
+ } else {
+ if( !strcmp( get_primitive( field ), "number") ) {
+ value = jsonNumberToDBString( field, value_obj );
+ } else {
+ value = jsonObjectToSimpleString( value_obj );
+ if( !dbi_conn_quote_string( dbhandle, &value )) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]",
+ modulename, value );
+ free( value );
+ free( field_transform );
+ return NULL;
+ }
+ }
+ }
+
+ const char* left_parens = "";
+ const char* right_parens = "";
+
+ if( extra_parens ) {
+ left_parens = "(";
+ right_parens = ")";
+ }
+
+ const char* right_percent = "";
+ const char* real_op = op;
+
+ if( !strcasecmp( op, "startwith") ) {
+ real_op = "like";
+ right_percent = "|| '%'";
+ }
+
+ growing_buffer* sql_buf = buffer_init( 32 );
+
+ buffer_fadd(
+ sql_buf,
+ "%s%s %s %s %s%s %s%s",
+ left_parens,
+ field_transform,
+ real_op,
+ left_parens,
+ value,
+ right_percent,
+ right_parens,
+ right_parens
+ );
+
+ free( value );
+ free( field_transform );
+
+ return buffer_release( sql_buf );
+}
+
+static char* searchSimplePredicate( const char* op, const char* class_alias,
+ osrfHash* field, const jsonObject* node ) {
+
+ if( ! is_good_operator( op ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Invalid operator [%s]", modulename, op );
+ return NULL;
+ }
+
+ char* val = NULL;
+
+ // Get the value to which we are comparing the specified column
+ if( node->type != JSON_NULL ) {
+ if( node->type == JSON_NUMBER ) {
+ val = jsonNumberToDBString( field, node );
+ } else if( !strcmp( get_primitive( field ), "number" ) ) {
+ val = jsonNumberToDBString( field, node );
+ } else {
+ val = jsonObjectToSimpleString( node );
+ }
+ }
+
+ if( val ) {
+ if( JSON_NUMBER != node->type && strcmp( get_primitive( field ), "number") ) {
+ // Value is not numeric; enclose it in quotes
+ if( !dbi_conn_quote_string( dbhandle, &val ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key string [%s]",
+ modulename, val );
+ free( val );
+ return NULL;
+ }
+ }
+ } else {
+ // Compare to a null value
+ val = strdup( "NULL" );
+ if( strcmp( op, "=" ))
+ op = "IS NOT";
+ else
+ op = "IS";
+ }
+
+ growing_buffer* sql_buf = buffer_init( 32 );
+ buffer_fadd( sql_buf, "\"%s\".%s %s %s", class_alias, osrfHashGet(field, "name"), op, val );
+ char* pred = buffer_release( sql_buf );
+
+ free( val );
+
+ return pred;
+}
+
+static char* searchBETWEENPredicate( const char* class_alias,
+ osrfHash* field, const jsonObject* node ) {
+
+ const jsonObject* x_node = jsonObjectGetIndex( node, 0 );
+ const jsonObject* y_node = jsonObjectGetIndex( node, 1 );
+
+ if( NULL == y_node ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Not enough operands for BETWEEN operator", modulename );
+ return NULL;
+ }
+ else if( NULL != jsonObjectGetIndex( node, 2 ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Too many operands for BETWEEN operator", modulename );
+ return NULL;
+ }
+
+ char* x_string;
+ char* y_string;
+
+ if( !strcmp( get_primitive( field ), "number") ) {
+ x_string = jsonNumberToDBString( field, x_node );
+ y_string = jsonNumberToDBString( field, y_node );
+
+ } else {
+ x_string = jsonObjectToSimpleString( x_node );
+ y_string = jsonObjectToSimpleString( y_node );
+ if( !(dbi_conn_quote_string( dbhandle, &x_string )
+ && dbi_conn_quote_string( dbhandle, &y_string )) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting key strings [%s] and [%s]",
+ modulename, x_string, y_string );
+ free( x_string );
+ free( y_string );
+ return NULL;
+ }
+ }
+
+ growing_buffer* sql_buf = buffer_init( 32 );
+ buffer_fadd( sql_buf, "\"%s\".%s BETWEEN %s AND %s",
+ class_alias, osrfHashGet( field, "name" ), x_string, y_string );
+ free( x_string );
+ free( y_string );
+
+ return buffer_release( sql_buf );
+}
+
+static char* searchPredicate( const ClassInfo* class_info, osrfHash* field,
+ jsonObject* node, osrfMethodContext* ctx ) {
+
+ char* pred = NULL;
+ if( node->type == JSON_ARRAY ) { // equality IN search
+ pred = searchINPredicate( class_info->alias, field, node, NULL, ctx );
+ } else if( node->type == JSON_HASH ) { // other search
+ jsonIterator* pred_itr = jsonNewIterator( node );
+ if( !jsonIteratorHasNext( pred_itr ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Empty predicate for field \"%s\"",
+ modulename, osrfHashGet(field, "name" ));
+ } else {
+ jsonObject* pred_node = jsonIteratorNext( pred_itr );
+
+ // Verify that there are no additional predicates
+ if( jsonIteratorHasNext( pred_itr ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Multiple predicates for field \"%s\"",
+ modulename, osrfHashGet(field, "name" ));
+ } else if( !(strcasecmp( pred_itr->key,"between" )) )
+ pred = searchBETWEENPredicate( class_info->alias, field, pred_node );
+ else if( !(strcasecmp( pred_itr->key,"in" ))
+ || !(strcasecmp( pred_itr->key,"not in" )) )
+ pred = searchINPredicate(
+ class_info->alias, field, pred_node, pred_itr->key, ctx );
+ else if( pred_node->type == JSON_ARRAY )
+ pred = searchFunctionPredicate(
+ class_info->alias, field, pred_node, pred_itr->key );
+ else if( pred_node->type == JSON_HASH )
+ pred = searchFieldTransformPredicate(
+ class_info, field, pred_node, pred_itr->key );
+ else
+ pred = searchSimplePredicate( pred_itr->key, class_info->alias, field, pred_node );
+ }
+ jsonIteratorFree( pred_itr );
+
+ } else if( node->type == JSON_NULL ) { // IS NULL search
+ growing_buffer* _p = buffer_init( 64 );
+ buffer_fadd(
+ _p,
+ "\"%s\".%s IS NULL",
+ class_info->alias,
+ osrfHashGet( field, "name" )
+ );
+ pred = buffer_release( _p );
+ } else { // equality search
+ pred = searchSimplePredicate( "=", class_info->alias, field, node );
+ }
+
+ return pred;
+
+}
+
+
+/*
+
+join : {
+ acn : {
+ field : record,
+ fkey : id
+ type : left
+ filter_op : or
+ filter : { ... },
+ join : {
+ acp : {
+ field : call_number,
+ fkey : id,
+ filter : { ... },
+ },
+ },
+ },
+ mrd : {
+ field : record,
+ type : inner
+ fkey : id,
+ filter : { ... },
+ }
+}
+
+*/
+
+static char* searchJOIN( const jsonObject* join_hash, const ClassInfo* left_info ) {
+
+ const jsonObject* working_hash;
+ jsonObject* freeable_hash = NULL;
+
+ if( join_hash->type == JSON_HASH ) {
+ working_hash = join_hash;
+ } else if( join_hash->type == JSON_STRING ) {
+ // turn it into a JSON_HASH by creating a wrapper
+ // around a copy of the original
+ const char* _tmp = jsonObjectGetString( join_hash );
+ freeable_hash = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( freeable_hash, _tmp, NULL );
+ working_hash = freeable_hash;
+ } else {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed; expected JSON object type not found",
+ modulename
+ );
+ return NULL;
+ }
+
+ growing_buffer* join_buf = buffer_init( 128 );
+ const char* leftclass = left_info->class_name;
+
+ jsonObject* snode = NULL;
+ jsonIterator* search_itr = jsonNewIterator( working_hash );
+
+ while ( (snode = jsonIteratorNext( search_itr )) ) {
+ const char* right_alias = search_itr->key;
+ const char* class =
+ jsonObjectGetString( jsonObjectGetKeyConst( snode, "class" ) );
+ if( ! class )
+ class = right_alias;
+
+ const ClassInfo* right_info = add_joined_class( right_alias, class );
+ if( !right_info ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed. Class \"%s\" not resolved in IDL",
+ modulename,
+ search_itr->key
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ return NULL;
+ }
+ osrfHash* links = right_info->links;
+ const char* table = right_info->source_def;
+
+ const char* fkey = jsonObjectGetString( jsonObjectGetKeyConst( snode, "fkey" ) );
+ const char* field = jsonObjectGetString( jsonObjectGetKeyConst( snode, "field" ) );
+
+ if( field && !fkey ) {
+ // Look up the corresponding join column in the IDL.
+ // The link must be defined in the child table,
+ // and point to the right parent table.
+ osrfHash* idl_link = (osrfHash*) osrfHashGet( links, field );
+ const char* reltype = NULL;
+ const char* other_class = NULL;
+ reltype = osrfHashGet( idl_link, "reltype" );
+ if( reltype && strcmp( reltype, "has_many" ) )
+ other_class = osrfHashGet( idl_link, "class" );
+ if( other_class && !strcmp( other_class, leftclass ) )
+ fkey = osrfHashGet( idl_link, "key" );
+ if( !fkey ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed. No link defined from %s.%s to %s",
+ modulename,
+ class,
+ field,
+ leftclass
+ );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ jsonIteratorFree( search_itr );
+ return NULL;
+ }
+
+ } else if( !field && fkey ) {
+ // Look up the corresponding join column in the IDL.
+ // The link must be defined in the child table,
+ // and point to the right parent table.
+ osrfHash* left_links = left_info->links;
+ osrfHash* idl_link = (osrfHash*) osrfHashGet( left_links, fkey );
+ const char* reltype = NULL;
+ const char* other_class = NULL;
+ reltype = osrfHashGet( idl_link, "reltype" );
+ if( reltype && strcmp( reltype, "has_many" ) )
+ other_class = osrfHashGet( idl_link, "class" );
+ if( other_class && !strcmp( other_class, class ) )
+ field = osrfHashGet( idl_link, "key" );
+ if( !field ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed. No link defined from %s.%s to %s",
+ modulename,
+ leftclass,
+ fkey,
+ class
+ );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ jsonIteratorFree( search_itr );
+ return NULL;
+ }
+
+ } else if( !field && !fkey ) {
+ osrfHash* left_links = left_info->links;
+
+ // For each link defined for the left class:
+ // see if the link references the joined class
+ osrfHashIterator* itr = osrfNewHashIterator( left_links );
+ osrfHash* curr_link = NULL;
+ while( (curr_link = osrfHashIteratorNext( itr ) ) ) {
+ const char* other_class = osrfHashGet( curr_link, "class" );
+ if( other_class && !strcmp( other_class, class ) ) {
+
+ // In the IDL, the parent class doesn't always know then names of the child
+ // columns that are pointing to it, so don't use that end of the link
+ const char* reltype = osrfHashGet( curr_link, "reltype" );
+ if( reltype && strcmp( reltype, "has_many" ) ) {
+ // Found a link between the classes
+ fkey = osrfHashIteratorKey( itr );
+ field = osrfHashGet( curr_link, "key" );
+ break;
+ }
+ }
+ }
+ osrfHashIteratorFree( itr );
+
+ if( !field || !fkey ) {
+ // Do another such search, with the classes reversed
+
+ // For each link defined for the joined class:
+ // see if the link references the left class
+ osrfHashIterator* itr = osrfNewHashIterator( links );
+ osrfHash* curr_link = NULL;
+ while( (curr_link = osrfHashIteratorNext( itr ) ) ) {
+ const char* other_class = osrfHashGet( curr_link, "class" );
+ if( other_class && !strcmp( other_class, leftclass ) ) {
+
+ // In the IDL, the parent class doesn't know then names of the child
+ // columns that are pointing to it, so don't use that end of the link
+ const char* reltype = osrfHashGet( curr_link, "reltype" );
+ if( reltype && strcmp( reltype, "has_many" ) ) {
+ // Found a link between the classes
+ field = osrfHashIteratorKey( itr );
+ fkey = osrfHashGet( curr_link, "key" );
+ break;
+ }
+ }
+ }
+ osrfHashIteratorFree( itr );
+ }
+
+ if( !field || !fkey ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed. No link defined between %s and %s",
+ modulename,
+ leftclass,
+ class
+ );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ jsonIteratorFree( search_itr );
+ return NULL;
+ }
+ }
+
+ const char* type = jsonObjectGetString( jsonObjectGetKeyConst( snode, "type" ) );
+ if( type ) {
+ if( !strcasecmp( type,"left" )) {
+ buffer_add( join_buf, " LEFT JOIN" );
+ } else if( !strcasecmp( type,"right" )) {
+ buffer_add( join_buf, " RIGHT JOIN" );
+ } else if( !strcasecmp( type,"full" )) {
+ buffer_add( join_buf, " FULL JOIN" );
+ } else {
+ buffer_add( join_buf, " INNER JOIN" );
+ }
+ } else {
+ buffer_add( join_buf, " INNER JOIN" );
+ }
+
+ buffer_fadd( join_buf, " %s AS \"%s\" ON ( \"%s\".%s = \"%s\".%s",
+ table, right_alias, right_alias, field, left_info->alias, fkey );
+
+ // Add any other join conditions as specified by "filter"
+ const jsonObject* filter = jsonObjectGetKeyConst( snode, "filter" );
+ if( filter ) {
+ const char* filter_op = jsonObjectGetString(
+ jsonObjectGetKeyConst( snode, "filter_op" ) );
+ if( filter_op && !strcasecmp( "or",filter_op )) {
+ buffer_add( join_buf, " OR " );
+ } else {
+ buffer_add( join_buf, " AND " );
+ }
+
+ char* jpred = searchWHERE( filter, right_info, AND_OP_JOIN, NULL );
+ if( jpred ) {
+ OSRF_BUFFER_ADD_CHAR( join_buf, ' ' );
+ OSRF_BUFFER_ADD( join_buf, jpred );
+ free( jpred );
+ } else {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: JOIN failed. Invalid conditional expression.",
+ modulename
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ return NULL;
+ }
+ }
+
+ buffer_add( join_buf, " ) " );
+
+ // Recursively add a nested join, if one is present
+ const jsonObject* join_filter = jsonObjectGetKeyConst( snode, "join" );
+ if( join_filter ) {
+ char* jpred = searchJOIN( join_filter, right_info );
+ if( jpred ) {
+ OSRF_BUFFER_ADD_CHAR( join_buf, ' ' );
+ OSRF_BUFFER_ADD( join_buf, jpred );
+ free( jpred );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Invalid nested join.", modulename );
+ jsonIteratorFree( search_itr );
+ buffer_free( join_buf );
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ return NULL;
+ }
+ }
+ }
+
+ if( freeable_hash )
+ jsonObjectFree( freeable_hash );
+ jsonIteratorFree( search_itr );
+
+ return buffer_release( join_buf );
+}
+
+/*
+
+{ +class : { -or|-and : { field : { op : value }, ... } ... }, ... }
+{ +class : { -or|-and : [ { field : { op : value }, ... }, ...] ... }, ... }
+[ { +class : { -or|-and : [ { field : { op : value }, ... }, ...] ... }, ... }, ... ]
+
+Generate code to express a set of conditions, as for a WHERE clause. Parameters:
+
+search_hash is the JSON expression of the conditions.
+meta is the class definition from the IDL, for the relevant table.
+opjoin_type indicates whether multiple conditions, if present, should be
+ connected by AND or OR.
+osrfMethodContext is loaded with all sorts of stuff, but all we do with it here is
+ to pass it to other functions -- and all they do with it is to use the session
+ and request members to send error messages back to the client.
+
+*/
+
+static char* searchWHERE( const jsonObject* search_hash, const ClassInfo* class_info,
+ int opjoin_type, osrfMethodContext* ctx ) {
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "%s: Entering searchWHERE; search_hash addr = %p, meta addr = %p, "
+ "opjoin_type = %d, ctx addr = %p",
+ modulename,
+ search_hash,
+ class_info->class_def,
+ opjoin_type,
+ ctx
+ );
+
+ growing_buffer* sql_buf = buffer_init( 128 );
+
+ jsonObject* node = NULL;
+
+ int first = 1;
+ if( search_hash->type == JSON_ARRAY ) {
+ if( 0 == search_hash->size ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid predicate structure: empty JSON array",
+ modulename
+ );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ unsigned long i = 0;
+ while(( node = jsonObjectGetIndex( search_hash, i++ ) )) {
+ if( first ) {
+ first = 0;
+ } else {
+ if( opjoin_type == OR_OP_JOIN )
+ buffer_add( sql_buf, " OR " );
+ else
+ buffer_add( sql_buf, " AND " );
+ }
+
+ char* subpred = searchWHERE( node, class_info, opjoin_type, ctx );
+ if( ! subpred ) {
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "( %s )", subpred );
+ free( subpred );
+ }
+
+ } else if( search_hash->type == JSON_HASH ) {
+ osrfLogDebug( OSRF_LOG_MARK,
+ "%s: In WHERE clause, condition type is JSON_HASH", modulename );
+ jsonIterator* search_itr = jsonNewIterator( search_hash );
+ if( !jsonIteratorHasNext( search_itr ) ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid predicate structure: empty JSON object",
+ modulename
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ while( (node = jsonIteratorNext( search_itr )) ) {
+
+ if( first ) {
+ first = 0;
+ } else {
+ if( opjoin_type == OR_OP_JOIN )
+ buffer_add( sql_buf, " OR " );
+ else
+ buffer_add( sql_buf, " AND " );
+ }
+
+ if( '+' == search_itr->key[ 0 ] ) {
+
+ // This plus sign prefixes a class name or other table alias;
+ // make sure the table alias is in scope
+ ClassInfo* alias_info = search_all_alias( search_itr->key + 1 );
+ if( ! alias_info ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid table alias \"%s\" in WHERE clause",
+ modulename,
+ search_itr->key + 1
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ if( node->type == JSON_STRING ) {
+ // It's the name of a column; make sure it belongs to the class
+ const char* fieldname = jsonObjectGetString( node );
+ if( ! osrfHashGet( alias_info->fields, fieldname ) ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid column name \"%s\" in WHERE clause "
+ "for table alias \"%s\"",
+ modulename,
+ fieldname,
+ alias_info->alias
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, " \"%s\".%s ", alias_info->alias, fieldname );
+ } else {
+ // It's something more complicated
+ char* subpred = searchWHERE( node, alias_info, AND_OP_JOIN, ctx );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "( %s )", subpred );
+ free( subpred );
+ }
+ } else if( '-' == search_itr->key[ 0 ] ) {
+ if( !strcasecmp( "-or", search_itr->key )) {
+ char* subpred = searchWHERE( node, class_info, OR_OP_JOIN, ctx );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "( %s )", subpred );
+ free( subpred );
+ } else if( !strcasecmp( "-and", search_itr->key )) {
+ char* subpred = searchWHERE( node, class_info, AND_OP_JOIN, ctx );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "( %s )", subpred );
+ free( subpred );
+ } else if( !strcasecmp("-not",search_itr->key) ) {
+ char* subpred = searchWHERE( node, class_info, AND_OP_JOIN, ctx );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, " NOT ( %s )", subpred );
+ free( subpred );
+ } else if( !strcasecmp( "-exists", search_itr->key )) {
+ char* subpred = buildQuery( ctx, node, SUBSELECT );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "EXISTS ( %s )", subpred );
+ free( subpred );
+ } else if( !strcasecmp("-not-exists", search_itr->key )) {
+ char* subpred = buildQuery( ctx, node, SUBSELECT );
+ if( ! subpred ) {
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ buffer_fadd( sql_buf, "NOT EXISTS ( %s )", subpred );
+ free( subpred );
+ } else { // Invalid "minus" operator
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid operator \"%s\" in WHERE clause",
+ modulename,
+ search_itr->key
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ } else {
+
+ const char* class = class_info->class_name;
+ osrfHash* fields = class_info->fields;
+ osrfHash* field = osrfHashGet( fields, search_itr->key );
+
+ if( !field ) {
+ const char* table = class_info->source_def;
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Attempt to reference non-existent column \"%s\" on %s (%s)",
+ modulename,
+ search_itr->key,
+ table ? table : "?",
+ class ? class : "?"
+ );
+ jsonIteratorFree( search_itr );
+ buffer_free( sql_buf );
+ return NULL;
+ }
+
+ char* subpred = searchPredicate( class_info, field, node, ctx );
+ if( ! subpred ) {
+ buffer_free( sql_buf );
+ jsonIteratorFree( search_itr );
+ return NULL;
+ }
+
+ buffer_add( sql_buf, subpred );
+ free( subpred );
+ }
+ }
+ jsonIteratorFree( search_itr );
+
+ } else {
+ // ERROR ... only hash and array allowed at this level
+ char* predicate_string = jsonObjectToJSON( search_hash );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Invalid predicate structure: %s",
+ modulename,
+ predicate_string
+ );
+ buffer_free( sql_buf );
+ free( predicate_string );
+ return NULL;
+ }
+
+ return buffer_release( sql_buf );
+}
+
+/* Build a JSON_ARRAY of field names for a given table alias
+*/
+static jsonObject* defaultSelectList( const char* table_alias ) {
+
+ if( ! table_alias )
+ table_alias = "";
+
+ ClassInfo* class_info = search_all_alias( table_alias );
+ if( ! class_info ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Can't build default SELECT clause for \"%s\"; no such table alias",
+ modulename,
+ table_alias
+ );
+ return NULL;
+ }
+
+ jsonObject* array = jsonNewObjectType( JSON_ARRAY );
+ osrfHash* field_def = NULL;
+ osrfHashIterator* field_itr = osrfNewHashIterator( class_info->fields );
+ while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+ const char* field_name = osrfHashIteratorKey( field_itr );
+ if( ! str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ jsonObjectPush( array, jsonNewObject( field_name ) );
+ }
+ }
+ osrfHashIteratorFree( field_itr );
+
+ return array;
+}
+
+// Translate a jsonObject into a UNION, INTERSECT, or EXCEPT query.
+// The jsonObject must be a JSON_HASH with an single entry for "union",
+// "intersect", or "except". The data associated with this key must be an
+// array of hashes, each hash being a query.
+// Also allowed but currently ignored: entries for "order_by" and "alias".
+static char* doCombo( osrfMethodContext* ctx, jsonObject* combo, int flags ) {
+ // Sanity check
+ if( ! combo || combo->type != JSON_HASH )
+ return NULL; // should be impossible; validated by caller
+
+ const jsonObject* query_array = NULL; // array of subordinate queries
+ const char* op = NULL; // name of operator, e.g. UNION
+ const char* alias = NULL; // alias for the query (needed for ORDER BY)
+ int op_count = 0; // for detecting conflicting operators
+ int excepting = 0; // boolean
+ int all = 0; // boolean
+ jsonObject* order_obj = NULL;
+
+ // Identify the elements in the hash
+ jsonIterator* query_itr = jsonNewIterator( combo );
+ jsonObject* curr_obj = NULL;
+ while( (curr_obj = jsonIteratorNext( query_itr ) ) ) {
+ if( ! strcmp( "union", query_itr->key ) ) {
+ ++op_count;
+ op = " UNION ";
+ query_array = curr_obj;
+ } else if( ! strcmp( "intersect", query_itr->key ) ) {
+ ++op_count;
+ op = " INTERSECT ";
+ query_array = curr_obj;
+ } else if( ! strcmp( "except", query_itr->key ) ) {
+ ++op_count;
+ op = " EXCEPT ";
+ excepting = 1;
+ query_array = curr_obj;
+ } else if( ! strcmp( "order_by", query_itr->key ) ) {
+ osrfLogWarning(
+ OSRF_LOG_MARK,
+ "%s: ORDER BY not supported for UNION, INTERSECT, or EXCEPT",
+ modulename
+ );
+ order_obj = curr_obj;
+ } else if( ! strcmp( "alias", query_itr->key ) ) {
+ if( curr_obj->type != JSON_STRING ) {
+ jsonIteratorFree( query_itr );
+ return NULL;
+ }
+ alias = jsonObjectGetString( curr_obj );
+ } else if( ! strcmp( "all", query_itr->key ) ) {
+ if( obj_is_true( curr_obj ) )
+ all = 1;
+ } else {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed query; unexpected entry in query object"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Unexpected entry for \"%s\" in%squery",
+ modulename,
+ query_itr->key,
+ op
+ );
+ jsonIteratorFree( query_itr );
+ return NULL;
+ }
+ }
+ jsonIteratorFree( query_itr );
+
+ // More sanity checks
+ if( ! query_array ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Expected UNION, INTERSECT, or EXCEPT operator not found"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Expected UNION, INTERSECT, or EXCEPT operator not found",
+ modulename
+ );
+ return NULL; // should be impossible...
+ } else if( op_count > 1 ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Found more than one of UNION, INTERSECT, and EXCEPT in same query"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Found more than one of UNION, INTERSECT, and EXCEPT in same query",
+ modulename
+ );
+ return NULL;
+ } if( query_array->type != JSON_ARRAY ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed query: expected array of queries under UNION, INTERSECT or EXCEPT"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Expected JSON_ARRAY of queries for%soperator; found %s",
+ modulename,
+ op,
+ json_type( query_array->type )
+ );
+ return NULL;
+ } if( query_array->size < 2 ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "UNION, INTERSECT or EXCEPT requires multiple queries as operands"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s:%srequires multiple queries as operands",
+ modulename,
+ op
+ );
+ return NULL;
+ } else if( excepting && query_array->size > 2 ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "EXCEPT operator has too many queries as operands"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s:EXCEPT operator has too many queries as operands",
+ modulename
+ );
+ return NULL;
+ } else if( order_obj && ! alias ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "ORDER BY requires an alias for a UNION, INTERSECT, or EXCEPT"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s:ORDER BY requires an alias for a UNION, INTERSECT, or EXCEPT",
+ modulename
+ );
+ return NULL;
+ }
+
+ // So far so good. Now build the SQL.
+ growing_buffer* sql = buffer_init( 256 );
+
+ // If we nested inside another UNION, INTERSECT, or EXCEPT,
+ // Add a layer of parentheses
+ if( flags & SUBCOMBO )
+ OSRF_BUFFER_ADD( sql, "( " );
+
+ // Traverse the query array. Each entry should be a hash.
+ int first = 1; // boolean
+ int i = 0;
+ jsonObject* query = NULL;
+ while( (query = jsonObjectGetIndex( query_array, i++ )) ) {
+ if( query->type != JSON_HASH ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed query under UNION, INTERSECT or EXCEPT"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Malformed query under%s -- expected JSON_HASH, found %s",
+ modulename,
+ op,
+ json_type( query->type )
+ );
+ buffer_free( sql );
+ return NULL;
+ }
+
+ if( first )
+ first = 0;
+ else {
+ OSRF_BUFFER_ADD( sql, op );
+ if( all )
+ OSRF_BUFFER_ADD( sql, "ALL " );
+ }
+
+ char* query_str = buildQuery( ctx, query, SUBSELECT | SUBCOMBO );
+ if( ! query_str ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Error building query under%s",
+ modulename,
+ op
+ );
+ buffer_free( sql );
+ return NULL;
+ }
+
+ OSRF_BUFFER_ADD( sql, query_str );
+ }
+
+ if( flags & SUBCOMBO )
+ OSRF_BUFFER_ADD_CHAR( sql, ')' );
+
+ if( !(flags & SUBSELECT) )
+ OSRF_BUFFER_ADD_CHAR( sql, ';' );
+
+ return buffer_release( sql );
+}
+
+// Translate a jsonObject into a SELECT, UNION, INTERSECT, or EXCEPT query.
+// The jsonObject must be a JSON_HASH with an entry for "from", "union", "intersect",
+// or "except" to indicate the type of query.
+char* buildQuery( osrfMethodContext* ctx, jsonObject* query, int flags ) {
+ // Sanity checks
+ if( ! query ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed query; no query object"
+ );
+ osrfLogError( OSRF_LOG_MARK, "%s: Null pointer to query object", modulename );
+ return NULL;
+ } else if( query->type != JSON_HASH ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed query object"
+ );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Query object is %s instead of JSON_HASH",
+ modulename,
+ json_type( query->type )
+ );
+ return NULL;
+ }
+
+ // Determine what kind of query it purports to be, and dispatch accordingly.
+ if( jsonObjectGetKeyConst( query, "union" ) ||
+ jsonObjectGetKeyConst( query, "intersect" ) ||
+ jsonObjectGetKeyConst( query, "except" )) {
+ return doCombo( ctx, query, flags );
+ } else {
+ // It is presumably a SELECT query
+
+ // Push a node onto the stack for the current query. Every level of
+ // subquery gets its own QueryFrame on the Stack.
+ push_query_frame();
+
+ // Build an SQL SELECT statement
+ char* sql = SELECT(
+ ctx,
+ jsonObjectGetKey( query, "select" ),
+ jsonObjectGetKeyConst( query, "from" ),
+ jsonObjectGetKeyConst( query, "where" ),
+ jsonObjectGetKeyConst( query, "having" ),
+ jsonObjectGetKeyConst( query, "order_by" ),
+ jsonObjectGetKeyConst( query, "limit" ),
+ jsonObjectGetKeyConst( query, "offset" ),
+ flags
+ );
+ pop_query_frame();
+ return sql;
+ }
+}
+
+char* SELECT (
+ /* method context */ osrfMethodContext* ctx,
+
+ /* SELECT */ jsonObject* selhash,
+ /* FROM */ const jsonObject* join_hash,
+ /* WHERE */ const jsonObject* search_hash,
+ /* HAVING */ const jsonObject* having_hash,
+ /* ORDER BY */ const jsonObject* order_hash,
+ /* LIMIT */ const jsonObject* limit,
+ /* OFFSET */ const jsonObject* offset,
+ /* flags */ int flags
+) {
+ const char* locale = osrf_message_get_last_locale();
+
+ // general tmp objects
+ const jsonObject* tmp_const;
+ jsonObject* selclass = NULL;
+ jsonObject* snode = NULL;
+ jsonObject* onode = NULL;
+
+ char* string = NULL;
+ int from_function = 0;
+ int first = 1;
+ int gfirst = 1;
+ //int hfirst = 1;
+
+ osrfLogDebug(OSRF_LOG_MARK, "cstore SELECT locale: %s", locale ? locale : "(none)" );
+
+ // punt if there's no FROM clause
+ if( !join_hash || ( join_hash->type == JSON_HASH && !join_hash->size )) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: FROM clause is missing or empty",
+ modulename
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "FROM clause is missing or empty in JSON query"
+ );
+ return NULL;
+ }
+
+ // the core search class
+ const char* core_class = NULL;
+
+ // get the core class -- the only key of the top level FROM clause, or a string
+ if( join_hash->type == JSON_HASH ) {
+ jsonIterator* tmp_itr = jsonNewIterator( join_hash );
+ snode = jsonIteratorNext( tmp_itr );
+
+ // Populate the current QueryFrame with information
+ // about the core class
+ if( add_query_core( NULL, tmp_itr->key ) ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to look up core class"
+ );
+ return NULL;
+ }
+ core_class = curr_query->core.class_name;
+ join_hash = snode;
+
+ jsonObject* extra = jsonIteratorNext( tmp_itr );
+
+ jsonIteratorFree( tmp_itr );
+ snode = NULL;
+
+ // There shouldn't be more than one entry in join_hash
+ if( extra ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Malformed FROM clause: extra entry in JSON_HASH",
+ modulename
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed FROM clause in JSON query"
+ );
+ return NULL; // Malformed join_hash; extra entry
+ }
+ } else if( join_hash->type == JSON_ARRAY ) {
+ // We're selecting from a function, not from a table
+ from_function = 1;
+ core_class = jsonObjectGetString( jsonObjectGetIndex( join_hash, 0 ));
+ selhash = NULL;
+
+ } else if( join_hash->type == JSON_STRING ) {
+ // Populate the current QueryFrame with information
+ // about the core class
+ core_class = jsonObjectGetString( join_hash );
+ join_hash = NULL;
+ if( add_query_core( NULL, core_class ) ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to look up core class"
+ );
+ return NULL;
+ }
+ }
+ else {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: FROM clause is unexpected JSON type: %s",
+ modulename,
+ json_type( join_hash->type )
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Ill-formed FROM clause in JSON query"
+ );
+ return NULL;
+ }
+
+ // Build the join clause, if any, while filling out the list
+ // of joined classes in the current QueryFrame.
+ char* join_clause = NULL;
+ if( join_hash && ! from_function ) {
+
+ join_clause = searchJOIN( join_hash, &curr_query->core );
+ if( ! join_clause ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to construct JOIN clause(s)"
+ );
+ return NULL;
+ }
+ }
+
+ // For in case we don't get a select list
+ jsonObject* defaultselhash = NULL;
+
+ // if there is no select list, build a default select list ...
+ if( !selhash && !from_function ) {
+ jsonObject* default_list = defaultSelectList( core_class );
+ if( ! default_list ) {
+ if( ctx ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to build default SELECT clause in JSON query"
+ );
+ free( join_clause );
+ return NULL;
+ }
+ }
+
+ selhash = defaultselhash = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( selhash, core_class, default_list );
+ }
+
+ // The SELECT clause can be encoded only by a hash
+ if( !from_function && selhash->type != JSON_HASH ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Expected JSON_HASH for SELECT clause; found %s",
+ modulename,
+ json_type( selhash->type )
+ );
+
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed SELECT clause in JSON query"
+ );
+ free( join_clause );
+ return NULL;
+ }
+
+ // If you see a null or wild card specifier for the core class, or an
+ // empty array, replace it with a default SELECT list
+ tmp_const = jsonObjectGetKeyConst( selhash, core_class );
+ if( tmp_const ) {
+ int default_needed = 0; // boolean
+ if( JSON_STRING == tmp_const->type
+ && !strcmp( "*", jsonObjectGetString( tmp_const ) ))
+ default_needed = 1;
+ else if( JSON_NULL == tmp_const->type )
+ default_needed = 1;
+
+ if( default_needed ) {
+ // Build a default SELECT list
+ jsonObject* default_list = defaultSelectList( core_class );
+ if( ! default_list ) {
+ if( ctx ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Can't build default SELECT clause in JSON query"
+ );
+ free( join_clause );
+ return NULL;
+ }
+ }
+
+ jsonObjectSetKey( selhash, core_class, default_list );
+ }
+ }
+
+ // temp buffers for the SELECT list and GROUP BY clause
+ growing_buffer* select_buf = buffer_init( 128 );
+ growing_buffer* group_buf = buffer_init( 128 );
+
+ int aggregate_found = 0; // boolean
+
+ // Build a select list
+ if( from_function ) // From a function we select everything
+ OSRF_BUFFER_ADD_CHAR( select_buf, '*' );
+ else {
+
+ // Build the SELECT list as SQL
+ int sel_pos = 1;
+ first = 1;
+ gfirst = 1;
+ jsonIterator* selclass_itr = jsonNewIterator( selhash );
+ while ( (selclass = jsonIteratorNext( selclass_itr )) ) { // For each class
+
+ const char* cname = selclass_itr->key;
+
+ // Make sure the target relation is in the FROM clause.
+
+ // At this point join_hash is a step down from the join_hash we
+ // received as a parameter. If the original was a JSON_STRING,
+ // then json_hash is now NULL. If the original was a JSON_HASH,
+ // then json_hash is now the first (and only) entry in it,
+ // denoting the core class. We've already excluded the
+ // possibility that the original was a JSON_ARRAY, because in
+ // that case from_function would be non-NULL, and we wouldn't
+ // be here.
+
+ // If the current table alias isn't in scope, bail out
+ ClassInfo* class_info = search_alias( cname );
+ if( ! class_info ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: SELECT clause references class not in FROM clause: \"%s\"",
+ modulename,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected class not in FROM clause in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ if( selclass->type != JSON_ARRAY ) {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Malformed SELECT list for class \"%s\"; not an array",
+ modulename,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected class not in FROM clause in JSON query"
+ );
+
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ // Look up some attributes of the current class
+ osrfHash* idlClass = class_info->class_def;
+ osrfHash* class_field_set = class_info->fields;
+ const char* class_pkey = osrfHashGet( idlClass, "primarykey" );
+ const char* class_tname = osrfHashGet( idlClass, "tablename" );
+
+ if( 0 == selclass->size ) {
+ osrfLogWarning(
+ OSRF_LOG_MARK,
+ "%s: No columns selected from \"%s\"",
+ modulename,
+ cname
+ );
+ }
+
+ // stitch together the column list for the current table alias...
+ unsigned long field_idx = 0;
+ jsonObject* selfield = NULL;
+ while(( selfield = jsonObjectGetIndex( selclass, field_idx++ ) )) {
+
+ // If we need a separator comma, add one
+ if( first ) {
+ first = 0;
+ } else {
+ OSRF_BUFFER_ADD_CHAR( select_buf, ',' );
+ }
+
+ // if the field specification is a string, add it to the list
+ if( selfield->type == JSON_STRING ) {
+
+ // Look up the field in the IDL
+ const char* col_name = jsonObjectGetString( selfield );
+ osrfHash* field_def;
+
+ if (!osrfStringArrayContains(
+ osrfHashGet(
+ osrfHashGet( class_field_set, col_name ),
+ "suppress_controller"),
+ modulename
+ ))
+ field_def = osrfHashGet( class_field_set, col_name );
+
+ if( !field_def ) {
+ // No such field in current class
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Selected column \"%s\" not defined in IDL for class \"%s\"",
+ modulename,
+ col_name,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected column not defined in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ // Virtual field not allowed
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Selected column \"%s\" for class \"%s\" is virtual",
+ modulename,
+ col_name,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected column may not be virtual in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ if( locale ) {
+ const char* i18n;
+ if( flags & DISABLE_I18N )
+ i18n = NULL;
+ else
+ i18n = osrfHashGet( field_def, "i18n" );
+
+ if( str_is_true( i18n ) ) {
+ buffer_fadd( select_buf, " oils_i18n_xlate('%s', '%s', '%s', "
+ "'%s', \"%s\".%s::TEXT, '%s') AS \"%s\"",
+ class_tname, cname, col_name, class_pkey,
+ cname, class_pkey, locale, col_name );
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+ cname, col_name, col_name );
+ }
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+ cname, col_name, col_name );
+ }
+
+ // ... but it could be an object, in which case we check for a Field Transform
+ } else if( selfield->type == JSON_HASH ) {
+
+ const char* col_name = jsonObjectGetString(
+ jsonObjectGetKeyConst( selfield, "column" ) );
+
+ // Get the field definition from the IDL
+ osrfHash* field_def;
+ if (!osrfStringArrayContains(
+ osrfHashGet(
+ osrfHashGet( class_field_set, col_name ),
+ "suppress_controller"),
+ modulename
+ ))
+ field_def = osrfHashGet( class_field_set, col_name );
+
+
+ if( !field_def ) {
+ // No such field in current class
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Selected column \"%s\" is not defined in IDL for class \"%s\"",
+ modulename,
+ col_name,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected column is not defined in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ } else if( str_is_true( osrfHashGet( field_def, "virtual" ))) {
+ // No such field in current class
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Selected column \"%s\" is virtual for class \"%s\"",
+ modulename,
+ col_name,
+ cname
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Selected column is virtual in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ // Decide what to use as a column alias
+ const char* _alias;
+ if((tmp_const = jsonObjectGetKeyConst( selfield, "alias" ))) {
+ _alias = jsonObjectGetString( tmp_const );
+ } else if((tmp_const = jsonObjectGetKeyConst( selfield, "result_field" ))) { // Use result_field name as the alias
+ _alias = jsonObjectGetString( tmp_const );
+ } else { // Use field name as the alias
+ _alias = col_name;
+ }
+
+ if( jsonObjectGetKeyConst( selfield, "transform" )) {
+ char* transform_str = searchFieldTransform(
+ class_info->alias, field_def, selfield );
+ if( transform_str ) {
+ buffer_fadd( select_buf, " %s AS \"%s\"", transform_str, _alias );
+ free( transform_str );
+ } else {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to generate transform function in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+ } else {
+
+ if( locale ) {
+ const char* i18n;
+ if( flags & DISABLE_I18N )
+ i18n = NULL;
+ else
+ i18n = osrfHashGet( field_def, "i18n" );
+
+ if( str_is_true( i18n ) ) {
+ buffer_fadd( select_buf,
+ " oils_i18n_xlate('%s', '%s', '%s', '%s', "
+ "\"%s\".%s::TEXT, '%s') AS \"%s\"",
+ class_tname, cname, col_name, class_pkey, cname,
+ class_pkey, locale, _alias );
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+ cname, col_name, _alias );
+ }
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s AS \"%s\"",
+ cname, col_name, _alias );
+ }
+ }
+ }
+ else {
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Selected item is unexpected JSON type: %s",
+ modulename,
+ json_type( selfield->type )
+ );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Ill-formed SELECT item in JSON query"
+ );
+ jsonIteratorFree( selclass_itr );
+ buffer_free( select_buf );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ const jsonObject* agg_obj = jsonObjectGetKeyConst( selfield, "aggregate" );
+ if( obj_is_true( agg_obj ) )
+ aggregate_found = 1;
+ else {
+ // Append a comma (except for the first one)
+ // and add the column to a GROUP BY clause
+ if( gfirst )
+ gfirst = 0;
+ else
+ OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+
+ buffer_fadd( group_buf, " %d", sel_pos );
+ }
+
+#if 0
+ if (is_agg->size || (flags & SELECT_DISTINCT)) {
+
+ const jsonObject* aggregate_obj = jsonObjectGetKeyConst( elfield, "aggregate");
+ if ( ! obj_is_true( aggregate_obj ) ) {
+ if (gfirst) {
+ gfirst = 0;
+ } else {
+ OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+ }
+
+ buffer_fadd(group_buf, " %d", sel_pos);
+
+ /*
+ } else if (is_agg = jsonObjectGetKeyConst( selfield, "having" )) {
+ if (gfirst) {
+ gfirst = 0;
+ } else {
+ OSRF_BUFFER_ADD_CHAR( group_buf, ',' );
+ }
+
+ _column = searchFieldTransform(class_info->alias, field, selfield);
+ OSRF_BUFFER_ADD_CHAR(group_buf, ' ');
+ OSRF_BUFFER_ADD(group_buf, _column);
+ _column = searchFieldTransform(class_info->alias, field, selfield);
+ */
+ }
+ }
+#endif
+
+ sel_pos++;
+ } // end while -- iterating across SELECT columns
+
+ } // end while -- iterating across classes
+
+ jsonIteratorFree( selclass_itr );
+ }
+
+ char* col_list = buffer_release( select_buf );
+
+ // Make sure the SELECT list isn't empty. This can happen, for example,
+ // if we try to build a default SELECT clause from a non-core table.
+
+ if( ! *col_list ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: SELECT clause is empty", modulename );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "SELECT list is empty"
+ );
+ free( col_list );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ char* table = NULL;
+ if( from_function )
+ table = searchValueTransform( join_hash );
+ else
+ table = strdup( curr_query->core.source_def );
+
+ if( !table ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to identify table for core class"
+ );
+ free( col_list );
+ buffer_free( group_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ free( join_clause );
+ return NULL;
+ }
+
+ // Put it all together
+ growing_buffer* sql_buf = buffer_init( 128 );
+ buffer_fadd(sql_buf, "SELECT %s FROM %s AS \"%s\" ", col_list, table, core_class );
+ free( col_list );
+ free( table );
+
+ // Append the join clause, if any
+ if( join_clause ) {
+ buffer_add(sql_buf, join_clause );
+ free( join_clause );
+ }
+
+ char* order_by_list = NULL;
+ char* having_buf = NULL;
+
+ if( !from_function ) {
+
+ // Build a WHERE clause, if there is one
+ if( search_hash ) {
+ buffer_add( sql_buf, " WHERE " );
+
+ // and it's on the WHERE clause
+ char* pred = searchWHERE( search_hash, &curr_query->core, AND_OP_JOIN, ctx );
+ if( ! pred ) {
+ if( ctx ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in WHERE predicate -- see error log for more details"
+ );
+ }
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+
+ buffer_add( sql_buf, pred );
+ free( pred );
+ }
+
+ // Build a HAVING clause, if there is one
+ if( having_hash ) {
+
+ // and it's on the the WHERE clause
+ having_buf = searchWHERE( having_hash, &curr_query->core, AND_OP_JOIN, ctx );
+
+ if( ! having_buf ) {
+ if( ctx ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in HAVING predicate -- see error log for more details"
+ );
+ }
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+ }
+
+ // Build an ORDER BY clause, if there is one
+ if( NULL == order_hash )
+ ; // No ORDER BY? do nothing
+ else if( JSON_ARRAY == order_hash->type ) {
+ order_by_list = buildOrderByFromArray( ctx, order_hash );
+ if( !order_by_list ) {
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+ } else if( JSON_HASH == order_hash->type ) {
+ // This hash is keyed on class alias. Each class has either
+ // an array of field names or a hash keyed on field name.
+ growing_buffer* order_buf = NULL; // to collect ORDER BY list
+ jsonIterator* class_itr = jsonNewIterator( order_hash );
+ while( (snode = jsonIteratorNext( class_itr )) ) {
+
+ ClassInfo* order_class_info = search_alias( class_itr->key );
+ if( ! order_class_info ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Invalid class \"%s\" referenced in ORDER BY clause",
+ modulename, class_itr->key );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Invalid class referenced in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+
+ osrfHash* field_list_def = order_class_info->fields;
+
+ if( snode->type == JSON_HASH ) {
+
+ // Hash is keyed on field names from the current class. For each field
+ // there is another layer of hash to define the sorting details, if any,
+ // or a string to indicate direction of sorting.
+ jsonIterator* order_itr = jsonNewIterator( snode );
+ while( (onode = jsonIteratorNext( order_itr )) ) {
+
+ osrfHash* field_def = osrfHashGet( field_list_def, order_itr->key );
+ if( !field_def ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Invalid field \"%s\" in ORDER BY clause",
+ modulename, order_itr->key );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Invalid field in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( order_itr );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Virtual field \"%s\" in ORDER BY clause",
+ modulename, order_itr->key );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Virtual field in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( order_itr );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+
+ const char* direction = NULL;
+ if( onode->type == JSON_HASH ) {
+ if( jsonObjectGetKeyConst( onode, "transform" ) ) {
+ string = searchFieldTransform(
+ class_itr->key,
+ osrfHashGet( field_list_def, order_itr->key ),
+ onode
+ );
+ if( ! string ) {
+ if( ctx ) osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( order_itr );
+ jsonIteratorFree( class_itr );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( order_buf);
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+ } else {
+ growing_buffer* field_buf = buffer_init( 16 );
+ buffer_fadd( field_buf, "\"%s\".%s",
+ class_itr->key, order_itr->key );
+ string = buffer_release( field_buf );
+ }
+
+ if( (tmp_const = jsonObjectGetKeyConst( onode, "direction" )) ) {
+ const char* dir = jsonObjectGetString( tmp_const );
+ if(!strncasecmp( dir, "d", 1 )) {
+ direction = " DESC";
+ } else {
+ direction = " ASC";
+ }
+ }
+
+ } else if( JSON_NULL == onode->type || JSON_ARRAY == onode->type ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Expected JSON_STRING in ORDER BY clause; found %s",
+ modulename, json_type( onode->type ) );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed ORDER BY clause -- see error log for more details"
+ );
+ jsonIteratorFree( order_itr );
+ jsonIteratorFree( class_itr );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( order_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+
+ } else {
+ string = strdup( order_itr->key );
+ const char* dir = jsonObjectGetString( onode );
+ if( !strncasecmp( dir, "d", 1 )) {
+ direction = " DESC";
+ } else {
+ direction = " ASC";
+ }
+ }
+
+ if( order_buf )
+ OSRF_BUFFER_ADD( order_buf, ", " );
+ else
+ order_buf = buffer_init( 128 );
+
+ OSRF_BUFFER_ADD( order_buf, string );
+ free( string );
+
+ if( direction ) {
+ OSRF_BUFFER_ADD( order_buf, direction );
+ }
+
+ } // end while
+ jsonIteratorFree( order_itr );
+
+ } else if( snode->type == JSON_ARRAY ) {
+
+ // Array is a list of fields from the current class
+ unsigned long order_idx = 0;
+ while(( onode = jsonObjectGetIndex( snode, order_idx++ ) )) {
+
+ const char* _f = jsonObjectGetString( onode );
+
+ osrfHash* field_def = osrfHashGet( field_list_def, _f );
+ if( !field_def ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Invalid field \"%s\" in ORDER BY clause",
+ modulename, _f );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Invalid field in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Virtual field \"%s\" in ORDER BY clause",
+ modulename, _f );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Virtual field in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+
+ if( order_buf )
+ OSRF_BUFFER_ADD( order_buf, ", " );
+ else
+ order_buf = buffer_init( 128 );
+
+ buffer_fadd( order_buf, "\"%s\".%s", class_itr->key, _f );
+
+ } // end while
+
+ // IT'S THE OOOOOOOOOOOLD STYLE!
+ } else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Possible SQL injection attempt; direct order by is not allowed",
+ modulename );
+ if(ctx) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error -- see error log for more details"
+ );
+ }
+
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( order_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ jsonIteratorFree( class_itr );
+ return NULL;
+ }
+ } // end while
+ jsonIteratorFree( class_itr );
+ if( order_buf )
+ order_by_list = buffer_release( order_buf );
+ } else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Malformed ORDER BY clause; expected JSON_HASH or JSON_ARRAY, found %s",
+ modulename, json_type( order_hash->type ) );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed ORDER BY clause -- see error log for more details"
+ );
+ free( having_buf );
+ buffer_free( group_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+ }
+
+ string = buffer_release( group_buf );
+
+ if( *string && ( aggregate_found || (flags & SELECT_DISTINCT) ) ) {
+ OSRF_BUFFER_ADD( sql_buf, " GROUP BY " );
+ OSRF_BUFFER_ADD( sql_buf, string );
+ }
+
+ free( string );
+
+ if( having_buf && *having_buf ) {
+ OSRF_BUFFER_ADD( sql_buf, " HAVING " );
+ OSRF_BUFFER_ADD( sql_buf, having_buf );
+ free( having_buf );
+ }
+
+ if( order_by_list ) {
+
+ if( *order_by_list ) {
+ OSRF_BUFFER_ADD( sql_buf, " ORDER BY " );
+ OSRF_BUFFER_ADD( sql_buf, order_by_list );
+ }
+
+ free( order_by_list );
+ }
+
+ if( limit ){
+ const char* str = jsonObjectGetString( limit );
+ if (str) { // limit could be JSON_NULL, etc.
+ buffer_fadd( sql_buf, " LIMIT %d", atoi( str ));
+ }
+ }
+
+ if( offset ) {
+ const char* str = jsonObjectGetString( offset );
+ if (str) {
+ buffer_fadd( sql_buf, " OFFSET %d", atoi( str ));
+ }
+ }
+
+ if( !(flags & SUBSELECT) )
+ OSRF_BUFFER_ADD_CHAR( sql_buf, ';' );
+
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+
+ return buffer_release( sql_buf );
+
+} // end of SELECT()
+
+/**
+ @brief Build a list of ORDER BY expressions.
+ @param ctx Pointer to the method context.
+ @param order_array Pointer to a JSON_ARRAY of field specifications.
+ @return Pointer to a string containing a comma-separated list of ORDER BY expressions.
+ Each expression may be either a column reference or a function call whose first parameter
+ is a column reference.
+
+ Each entry in @a order_array must be a JSON_HASH with values for "class" and "field".
+ It may optionally include entries for "direction" and/or "transform".
+
+ The calling code is responsible for freeing the returned string.
+*/
+static char* buildOrderByFromArray( osrfMethodContext* ctx, const jsonObject* order_array ) {
+ if( ! order_array ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Logic error: NULL pointer for ORDER BY clause",
+ modulename );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Logic error: ORDER BY clause expected, not found; "
+ "see error log for more details"
+ );
+ return NULL;
+ } else if( order_array->type != JSON_ARRAY ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Logic error: Expected JSON_ARRAY for ORDER BY clause, not found", modulename );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Logic error: Unexpected format for ORDER BY clause; see error log for more details" );
+ return NULL;
+ }
+
+ growing_buffer* order_buf = buffer_init( 128 );
+ int first = 1; // boolean
+ int order_idx = 0;
+ jsonObject* order_spec;
+ while( (order_spec = jsonObjectGetIndex( order_array, order_idx++ ))) {
+
+ if( JSON_HASH != order_spec->type ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Malformed field specification in ORDER BY clause; "
+ "expected JSON_HASH, found %s",
+ modulename, json_type( order_spec->type ) );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed ORDER BY clause -- see error log for more details"
+ );
+ buffer_free( order_buf );
+ return NULL;
+ }
+
+ const char* class_alias =
+ jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "class" ));
+ const char* field =
+ jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "field" ));
+
+ jsonObject* compare_to = jsonObjectGetKeyConst( order_spec, "compare" );
+
+ if( !field || !class_alias ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Missing class or field name in field specification of ORDER BY clause",
+ modulename );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Malformed ORDER BY clause -- see error log for more details"
+ );
+ buffer_free( order_buf );
+ return NULL;
+ }
+
+ const ClassInfo* order_class_info = search_alias( class_alias );
+ if( ! order_class_info ) {
+ osrfLogInternal( OSRF_LOG_MARK, "%s: ORDER BY clause references class \"%s\" "
+ "not in FROM clause, skipping it", modulename, class_alias );
+ continue;
+ }
+
+ // Add a separating comma, except at the beginning
+ if( first )
+ first = 0;
+ else
+ OSRF_BUFFER_ADD( order_buf, ", " );
+
+ osrfHash* field_def = osrfHashGet( order_class_info->fields, field );
+ if( !field_def ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s: Invalid field \"%s\".%s referenced in ORDER BY clause",
+ modulename, class_alias, field );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Invalid field referenced in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ free( order_buf );
+ return NULL;
+ } else if( str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ osrfLogError( OSRF_LOG_MARK, "%s: Virtual field \"%s\" in ORDER BY clause",
+ modulename, field );
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Virtual field in ORDER BY clause -- see error log for more details"
+ );
+ buffer_free( order_buf );
+ return NULL;
+ }
+
+ if( jsonObjectGetKeyConst( order_spec, "transform" )) {
+ char* transform_str = searchFieldTransform( class_alias, field_def, order_spec );
+ if( ! transform_str ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ buffer_free( order_buf );
+ return NULL;
+ }
+
+ OSRF_BUFFER_ADD( order_buf, transform_str );
+ free( transform_str );
+ } else if( compare_to ) {
+ char* compare_str = searchPredicate( order_class_info, field_def, compare_to, ctx );
+ if( ! compare_str ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ buffer_free( order_buf );
+ return NULL;
+ }
+
+ buffer_fadd( order_buf, "(%s)", compare_str );
+ free( compare_str );
+ }
+ else
+ buffer_fadd( order_buf, "\"%s\".%s", class_alias, field );
+
+ const char* direction =
+ jsonObjectGetString( jsonObjectGetKeyConst( order_spec, "direction" ) );
+ if( direction ) {
+ if( direction[ 0 ] && ( 'd' == direction[ 0 ] || 'D' == direction[ 0 ] ) )
+ OSRF_BUFFER_ADD( order_buf, " DESC" );
+ else
+ OSRF_BUFFER_ADD( order_buf, " ASC" );
+ }
+ }
+
+ return buffer_release( order_buf );
+}
+
+/**
+ @brief Build a SELECT statement.
+ @param search_hash Pointer to a JSON_HASH or JSON_ARRAY encoding the WHERE clause.
+ @param rest_of_query Pointer to a JSON_HASH containing any other SQL clauses.
+ @param meta Pointer to the class metadata for the core class.
+ @param ctx Pointer to the method context.
+ @return Pointer to a character string containing the WHERE clause; or NULL upon error.
+
+ Within the rest_of_query hash, the meaningful keys are "join", "select", "no_i18n",
+ "order_by", "limit", and "offset".
+
+ The SELECT statements built here are distinct from those built for the json_query method.
+*/
+static char* buildSELECT ( const jsonObject* search_hash, jsonObject* rest_of_query,
+ osrfHash* meta, osrfMethodContext* ctx ) {
+
+ const char* locale = osrf_message_get_last_locale();
+
+ osrfHash* fields = osrfHashGet( meta, "fields" );
+ const char* core_class = osrfHashGet( meta, "classname" );
+
+ const jsonObject* join_hash = jsonObjectGetKeyConst( rest_of_query, "join" );
+
+ jsonObject* selhash = NULL;
+ jsonObject* defaultselhash = NULL;
+
+ growing_buffer* sql_buf = buffer_init( 128 );
+ growing_buffer* select_buf = buffer_init( 128 );
+
+ if( !(selhash = jsonObjectGetKey( rest_of_query, "select" )) ) {
+ defaultselhash = jsonNewObjectType( JSON_HASH );
+ selhash = defaultselhash;
+ }
+
+ // If there's no SELECT list for the core class, build one
+ if( !jsonObjectGetKeyConst( selhash, core_class ) ) {
+ jsonObject* field_list = jsonNewObjectType( JSON_ARRAY );
+
+ // Add every non-virtual field to the field list
+ osrfHash* field_def = NULL;
+ osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+ while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+ if( ! str_is_true( osrfHashGet( field_def, "virtual" ) ) ) {
+ const char* field = osrfHashIteratorKey( field_itr );
+ jsonObjectPush( field_list, jsonNewObject( field ) );
+ }
+ }
+ osrfHashIteratorFree( field_itr );
+ jsonObjectSetKey( selhash, core_class, field_list );
+ }
+
+ // Build a list of columns for the SELECT clause
+ int first = 1;
+ const jsonObject* snode = NULL;
+ jsonIterator* class_itr = jsonNewIterator( selhash );
+ while( (snode = jsonIteratorNext( class_itr )) ) { // For each class
+
+ // If the class isn't in the IDL, ignore it
+ const char* cname = class_itr->key;
+ osrfHash* idlClass = osrfHashGet( oilsIDL(), cname );
+ if( !idlClass )
+ continue;
+
+ // If the class isn't the core class, and isn't in the JOIN clause, ignore it
+ if( strcmp( core_class, class_itr->key )) {
+ if( !join_hash )
+ continue;
+
+ jsonObject* found = jsonObjectFindPath( join_hash, "//%s", class_itr->key );
+ if( !found->size ) {
+ jsonObjectFree( found );
+ continue;
+ }
+
+ jsonObjectFree( found );
+ }
+
+ const jsonObject* node = NULL;
+ jsonIterator* select_itr = jsonNewIterator( snode );
+ while( (node = jsonIteratorNext( select_itr )) ) {
+ const char* item_str = jsonObjectGetString( node );
+ osrfHash* field = osrfHashGet( osrfHashGet( idlClass, "fields" ), item_str );
+ char* fname = osrfHashGet( field, "name" );
+
+ if( !field )
+ continue;
+
+ if (osrfStringArrayContains( osrfHashGet(field, "suppress_controller"), modulename ))
+ continue;
+
+ if( first ) {
+ first = 0;
+ } else {
+ OSRF_BUFFER_ADD_CHAR( select_buf, ',' );
+ }
+
+ if( locale ) {
+ const char* i18n;
+ const jsonObject* no_i18n_obj = jsonObjectGetKeyConst( rest_of_query, "no_i18n" );
+ if( obj_is_true( no_i18n_obj ) ) // Suppress internationalization?
+ i18n = NULL;
+ else
+ i18n = osrfHashGet( field, "i18n" );
+
+ if( str_is_true( i18n ) ) {
+ char* pkey = osrfHashGet( idlClass, "primarykey" );
+ char* tname = osrfHashGet( idlClass, "tablename" );
+
+ buffer_fadd( select_buf, " oils_i18n_xlate('%s', '%s', '%s', "
+ "'%s', \"%s\".%s::TEXT, '%s') AS \"%s\"",
+ tname, cname, fname, pkey, cname, pkey, locale, fname );
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s", cname, fname );
+ }
+ } else {
+ buffer_fadd( select_buf, " \"%s\".%s", cname, fname );
+ }
+ }
+
+ jsonIteratorFree( select_itr );
+ }
+
+ jsonIteratorFree( class_itr );
+
+ char* col_list = buffer_release( select_buf );
+ char* table = oilsGetRelation( meta );
+ if( !table )
+ table = strdup( "(null)" );
+
+ buffer_fadd( sql_buf, "SELECT %s FROM %s AS \"%s\"", col_list, table, core_class );
+ free( col_list );
+ free( table );
+
+ // Clear the query stack (as a fail-safe precaution against possible
+ // leftover garbage); then push the first query frame onto the stack.
+ clear_query_stack();
+ push_query_frame();
+ if( add_query_core( NULL, core_class ) ) {
+ if( ctx )
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Unable to build query frame for core class"
+ );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ return NULL;
+ }
+
+ // Add the JOIN clauses, if any
+ if( join_hash ) {
+ char* join_clause = searchJOIN( join_hash, &curr_query->core );
+ OSRF_BUFFER_ADD_CHAR( sql_buf, ' ' );
+ OSRF_BUFFER_ADD( sql_buf, join_clause );
+ free( join_clause );
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s pre-predicate SQL = %s",
+ modulename, OSRF_BUFFER_C_STR( sql_buf ));
+
+ OSRF_BUFFER_ADD( sql_buf, " WHERE " );
+
+ // Add the conditions in the WHERE clause
+ char* pred = searchWHERE( search_hash, &curr_query->core, AND_OP_JOIN, ctx );
+ if( !pred ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error -- see error log for more details"
+ );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ clear_query_stack();
+ return NULL;
+ } else {
+ buffer_add( sql_buf, pred );
+ free( pred );
+ }
+
+ // Add the ORDER BY, LIMIT, and/or OFFSET clauses, if present
+ if( rest_of_query ) {
+ const jsonObject* order_by = NULL;
+ if( ( order_by = jsonObjectGetKeyConst( rest_of_query, "order_by" )) ){
+
+ char* order_by_list = NULL;
+
+ if( JSON_ARRAY == order_by->type ) {
+ order_by_list = buildOrderByFromArray( ctx, order_by );
+ if( !order_by_list ) {
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ clear_query_stack();
+ return NULL;
+ }
+ } else if( JSON_HASH == order_by->type ) {
+ // We expect order_by to be a JSON_HASH keyed on class names. Traverse it
+ // and build a list of ORDER BY expressions.
+ growing_buffer* order_buf = buffer_init( 128 );
+ first = 1;
+ jsonIterator* class_itr = jsonNewIterator( order_by );
+ while( (snode = jsonIteratorNext( class_itr )) ) { // For each class:
+
+ ClassInfo* order_class_info = search_alias( class_itr->key );
+ if( ! order_class_info )
+ continue; // class not referenced by FROM clause? Ignore it.
+
+ if( JSON_HASH == snode->type ) {
+
+ // If the data for the current class is a JSON_HASH, then it is
+ // keyed on field name.
+
+ const jsonObject* onode = NULL;
+ jsonIterator* order_itr = jsonNewIterator( snode );
+ while( (onode = jsonIteratorNext( order_itr )) ) { // For each field
+
+ osrfHash* field_def = osrfHashGet(
+ order_class_info->fields, order_itr->key );
+ if( !field_def )
+ continue; // Field not defined in IDL? Ignore it.
+ if( str_is_true( osrfHashGet( field_def, "virtual")))
+ continue; // Field is virtual? Ignore it.
+
+ char* field_str = NULL;
+ char* direction = NULL;
+ if( onode->type == JSON_HASH ) {
+ if( jsonObjectGetKeyConst( onode, "transform" ) ) {
+ field_str = searchFieldTransform(
+ class_itr->key, field_def, onode );
+ if( ! field_str ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error in ORDER BY clause -- "
+ "see error log for more details"
+ );
+ jsonIteratorFree( order_itr );
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ clear_query_stack();
+ return NULL;
+ }
+ } else {
+ growing_buffer* field_buf = buffer_init( 16 );
+ buffer_fadd( field_buf, "\"%s\".%s",
+ class_itr->key, order_itr->key );
+ field_str = buffer_release( field_buf );
+ }
+
+ if( ( order_by = jsonObjectGetKeyConst( onode, "direction" )) ) {
+ const char* dir = jsonObjectGetString( order_by );
+ if(!strncasecmp( dir, "d", 1 )) {
+ direction = " DESC";
+ }
+ }
+ } else {
+ field_str = strdup( order_itr->key );
+ const char* dir = jsonObjectGetString( onode );
+ if( !strncasecmp( dir, "d", 1 )) {
+ direction = " DESC";
+ } else {
+ direction = " ASC";
+ }
+ }
+
+ if( first ) {
+ first = 0;
+ } else {
+ buffer_add( order_buf, ", " );
+ }
+
+ buffer_add( order_buf, field_str );
+ free( field_str );
+
+ if( direction ) {
+ buffer_add( order_buf, direction );
+ }
+ } // end while; looping over ORDER BY expressions
+
+ jsonIteratorFree( order_itr );
+
+ } else if( JSON_STRING == snode->type ) {
+ // We expect a comma-separated list of sort fields.
+ const char* str = jsonObjectGetString( snode );
+ if( strchr( str, ';' )) {
+ // No semicolons allowed. It is theoretically possible for a
+ // legitimate semicolon to occur within quotes, but it's not likely
+ // to occur in practice in the context of an ORDER BY list.
+ osrfLogError( OSRF_LOG_MARK, "%s: Possible attempt at SOL injection -- "
+ "semicolon found in ORDER BY list: \"%s\"", modulename, str );
+ if( ctx ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Possible attempt at SOL injection -- "
+ "semicolon found in ORDER BY list"
+ );
+ }
+ jsonIteratorFree( class_itr );
+ buffer_free( order_buf );
+ buffer_free( sql_buf );
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ clear_query_stack();
+ return NULL;
+ }
+ buffer_add( order_buf, str );
+ break;
+ }
+
+ } // end while; looping over order_by classes
+
+ jsonIteratorFree( class_itr );
+ order_by_list = buffer_release( order_buf );
+
+ } else {
+ osrfLogWarning( OSRF_LOG_MARK,
+ "\"order_by\" object in a query is not a JSON_HASH or JSON_ARRAY;"
+ "no ORDER BY generated" );
+ }
+
+ if( order_by_list && *order_by_list ) {
+ OSRF_BUFFER_ADD( sql_buf, " ORDER BY " );
+ OSRF_BUFFER_ADD( sql_buf, order_by_list );
+ }
+
+ free( order_by_list );
+ }
+
+ const jsonObject* limit = jsonObjectGetKeyConst( rest_of_query, "limit" );
+ if( limit ) {
+ const char* str = jsonObjectGetString( limit );
+ if (str) {
+ buffer_fadd(
+ sql_buf,
+ " LIMIT %d",
+ atoi(str)
+ );
+ }
+ }
+
+ const jsonObject* offset = jsonObjectGetKeyConst( rest_of_query, "offset" );
+ if( offset ) {
+ const char* str = jsonObjectGetString( offset );
+ if (str) {
+ buffer_fadd(
+ sql_buf,
+ " OFFSET %d",
+ atoi( str )
+ );
+ }
+ }
+ }
+
+ if( defaultselhash )
+ jsonObjectFree( defaultselhash );
+ clear_query_stack();
+
+ OSRF_BUFFER_ADD_CHAR( sql_buf, ';' );
+ return buffer_release( sql_buf );
+}
+
+int doJSONSearch ( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "Received query request" );
+
+ int err = 0;
+
+ jsonObject* hash = jsonObjectGetIndex( ctx->params, 0 );
+
+ int flags = 0;
+
+ if( obj_is_true( jsonObjectGetKeyConst( hash, "distinct" )))
+ flags |= SELECT_DISTINCT;
+
+ if( obj_is_true( jsonObjectGetKeyConst( hash, "no_i18n" )))
+ flags |= DISABLE_I18N;
+
+ osrfLogDebug( OSRF_LOG_MARK, "Building SQL ..." );
+ clear_query_stack(); // a possibly needless precaution
+ char* sql = buildQuery( ctx, hash, flags );
+ clear_query_stack();
+
+ if( !sql ) {
+ err = -1;
+ return err;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s SQL = %s", modulename, sql );
+
+ // XXX for now...
+ dbhandle = writehandle;
+
+ dbi_result result = dbi_conn_query( dbhandle, sql );
+
+ if( result ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Query returned with no errors" );
+
+ if( dbi_result_first_row( result )) {
+ /* JSONify the result */
+ osrfLogDebug( OSRF_LOG_MARK, "Query returned at least one row" );
+
+ do {
+ jsonObject* return_val = oilsMakeJSONFromResult( result );
+ osrfAppRespond( ctx, return_val );
+ jsonObjectFree( return_val );
+ } while( dbi_result_next_row( result ));
+
+ } else {
+ osrfLogDebug( OSRF_LOG_MARK, "%s returned no results for query %s", modulename, sql );
+ }
+
+ osrfAppRespondComplete( ctx, NULL );
+
+ /* clean up the query */
+ dbi_result_free( result );
+
+ } else {
+ err = -1;
+ const char* msg;
+ int errnum = dbi_conn_error( dbhandle, &msg );
+ osrfLogError( OSRF_LOG_MARK, "%s: Error with query [%s]: %d %s",
+ modulename, sql, errnum, msg ? msg : "(No description available)" );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error -- see error log for more details"
+ );
+ if( !oilsIsDBConnected( dbhandle ))
+ osrfAppSessionPanic( ctx->session );
+ }
+
+ free( sql );
+ return err;
+}
+
+// The last parameter, err, is used to report an error condition by updating an int owned by
+// the calling code.
+
+// In case of an error, we set *err to -1. If there is no error, *err is left unchanged.
+// It is the responsibility of the calling code to initialize *err before the
+// call, so that it will be able to make sense of the result.
+
+// Note also that we return NULL if and only if we set *err to -1. So the err parameter is
+// redundant anyway.
+static jsonObject* doFieldmapperSearch( osrfMethodContext* ctx, osrfHash* class_meta,
+ jsonObject* where_hash, jsonObject* query_hash, int* err ) {
+
+ // XXX for now...
+ dbhandle = writehandle;
+
+ char* core_class = osrfHashGet( class_meta, "classname" );
+ osrfLogDebug( OSRF_LOG_MARK, "entering doFieldmapperSearch() with core_class %s", core_class );
+
+ char* pkey = osrfHashGet( class_meta, "primarykey" );
+
+ if (!ctx->session->userData)
+ (void) initSessionCache( ctx );
+
+ char *methodtype = osrfHashGet( (osrfHash *) ctx->method->userData, "methodtype" );
+ char *inside_verify = osrfHashGet( (osrfHash*) ctx->session->userData, "inside_verify" );
+ int need_to_verify = (inside_verify ? !atoi(inside_verify) : 1);
+
+ int i_respond_directly = 0;
+ int flesh_depth = 0;
+
+ char* sql = buildSELECT( where_hash, query_hash, class_meta, ctx );
+ if( !sql ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Problem building query, returning NULL" );
+ *err = -1;
+ return NULL;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s SQL = %s", modulename, sql );
+
+ dbi_result result = dbi_conn_query( dbhandle, sql );
+ if( NULL == result ) {
+ const char* msg;
+ int errnum = dbi_conn_error( dbhandle, &msg );
+ osrfLogError(OSRF_LOG_MARK, "%s: Error retrieving %s with query [%s]: %d %s",
+ modulename, osrfHashGet( class_meta, "fieldmapper" ), sql, errnum,
+ msg ? msg : "(No description available)" );
+ if( !oilsIsDBConnected( dbhandle ))
+ osrfAppSessionPanic( ctx->session );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Severe query error -- see error log for more details"
+ );
+ *err = -1;
+ free( sql );
+ return NULL;
+
+ } else {
+ osrfLogDebug( OSRF_LOG_MARK, "Query returned with no errors" );
+ }
+
+ jsonObject* res_list = jsonNewObjectType( JSON_ARRAY );
+ jsonObject* row_obj = NULL;
+
+ // The following two steps are for verifyObjectPCRUD()'s benefit.
+ // 1. get the flesh depth
+ const jsonObject* _tmp = jsonObjectGetKeyConst( query_hash, "flesh" );
+ if( _tmp ) {
+ flesh_depth = (int) jsonObjectGetNumber( _tmp );
+ if( flesh_depth == -1 || flesh_depth > max_flesh_depth )
+ flesh_depth = max_flesh_depth;
+ }
+
+ // 2. figure out one consistent rs_size for verifyObjectPCRUD to use
+ // over the whole life of this request. This means if we've already set
+ // up a rs_size_req_%d, do nothing.
+ // a. Incidentally, we can also use this opportunity to set i_respond_directly
+ int *rs_size = osrfHashGetFmt( (osrfHash *) ctx->session->userData, "rs_size_req_%d", ctx->request );
+ if( !rs_size ) { // pointer null, so value not set in hash
+ // i_respond_directly can only be true at the /top/ of a recursive search, if even that.
+ i_respond_directly = ( *methodtype == 'r' || *methodtype == 'i' || *methodtype == 's' );
+
+ rs_size = (int *) safe_malloc( sizeof(int) ); // will be freed by sessionDataFree()
+ unsigned long long result_count = dbi_result_get_numrows( result );
+ *rs_size = (int) result_count * (flesh_depth + 1); // yes, we could lose some bits, but come on
+ osrfHashSet( (osrfHash *) ctx->session->userData, rs_size, "rs_size_req_%d", ctx->request );
+ }
+
+ if( dbi_result_first_row( result )) {
+
+ // Convert each row to a JSON_ARRAY of column values, and enclose those objects
+ // in a JSON_ARRAY of rows. If two or more rows have the same key value, then
+ // eliminate the duplicates.
+ osrfLogDebug( OSRF_LOG_MARK, "Query returned at least one row" );
+ osrfHash* dedup = osrfNewHash();
+ do {
+ row_obj = oilsMakeFieldmapperFromResult( result, class_meta );
+ char* pkey_val = oilsFMGetString( row_obj, pkey );
+ if( osrfHashGet( dedup, pkey_val ) ) {
+ jsonObjectFree( row_obj );
+ free( pkey_val );
+ } else {
+ if( !enforce_pcrud || !need_to_verify ||
+ verifyObjectPCRUD( ctx, class_meta, row_obj, 0 /* means check user data for rs_size */ )) {
+ osrfHashSet( dedup, pkey_val, pkey_val );
+ jsonObjectPush( res_list, row_obj );
+ }
+ }
+ } while( dbi_result_next_row( result ));
+ osrfHashFree( dedup );
+
+ } else {
+ osrfLogDebug( OSRF_LOG_MARK, "%s returned no results for query %s",
+ modulename, sql );
+ }
+
+ /* clean up the query */
+ dbi_result_free( result );
+ free( sql );
+
+ // If we're asked to flesh, and there's anything to flesh, then flesh it
+ // (formerly we would skip fleshing if in pcrud mode, but now we support
+ // fleshing even in PCRUD).
+ if( res_list->size ) {
+ jsonObject* temp_blob; // We need a non-zero flesh depth, and a list of fields to flesh
+ jsonObject* flesh_fields;
+ jsonObject* flesh_blob = NULL;
+ osrfStringArray* link_fields = NULL;
+ osrfHash* links = NULL;
+ int want_flesh = 0;
+
+ if( query_hash ) {
+ temp_blob = jsonObjectGetKey( query_hash, "flesh_fields" );
+ if( temp_blob && flesh_depth > 0 ) {
+
+ flesh_blob = jsonObjectClone( temp_blob );
+ flesh_fields = jsonObjectGetKey( flesh_blob, core_class );
+
+ links = osrfHashGet( class_meta, "links" );
+
+ // Make an osrfStringArray of the names of fields to be fleshed
+ if( flesh_fields ) {
+ if( flesh_fields->size == 1 ) {
+ const char* _t = jsonObjectGetString(
+ jsonObjectGetIndex( flesh_fields, 0 ) );
+ if( !strcmp( _t, "*" ))
+ link_fields = osrfHashKeys( links );
+ }
+
+ if( !link_fields ) {
+ jsonObject* _f;
+ link_fields = osrfNewStringArray( 1 );
+ jsonIterator* _i = jsonNewIterator( flesh_fields );
+ while ((_f = jsonIteratorNext( _i ))) {
+ osrfStringArrayAdd( link_fields, jsonObjectGetString( _f ) );
+ }
+ jsonIteratorFree( _i );
+ }
+ }
+ want_flesh = link_fields ? 1 : 0;
+ }
+ }
+
+ osrfHash* fields = osrfHashGet( class_meta, "fields" );
+
+ // Iterate over the JSON_ARRAY of rows
+ jsonObject* cur;
+ unsigned long res_idx = 0;
+ while((cur = jsonObjectGetIndex( res_list, res_idx++ ) )) {
+
+ int i = 0;
+ const char* link_field;
+
+ // Iterate over the list of fleshable fields
+ if ( want_flesh ) {
+ while( (link_field = osrfStringArrayGetString(link_fields, i++)) ) {
+
+ osrfLogDebug( OSRF_LOG_MARK, "Starting to flesh %s", link_field );
+
+ osrfHash* kid_link = osrfHashGet( links, link_field );
+ if( !kid_link )
+ continue; // Not a link field; skip it
+
+ osrfHash* field = osrfHashGet( fields, link_field );
+ if( !field )
+ continue; // Not a field at all; skip it (IDL is ill-formed)
+
+ osrfHash* kid_idl = osrfHashGet( oilsIDL(),
+ osrfHashGet( kid_link, "class" ));
+ if( !kid_idl )
+ continue; // The class it links to doesn't exist; skip it
+
+ const char* reltype = osrfHashGet( kid_link, "reltype" );
+ if( !reltype )
+ continue; // No reltype; skip it (IDL is ill-formed)
+
+ osrfHash* value_field = field;
+
+ if( !strcmp( reltype, "has_many" )
+ || !strcmp( reltype, "might_have" ) ) { // has_many or might_have
+ value_field = osrfHashGet(
+ fields, osrfHashGet( class_meta, "primarykey" ) );
+ }
+
+ int kid_has_controller = osrfStringArrayContains( osrfHashGet(kid_idl, "controller"), modulename );
+ // fleshing pcrud case: we require the controller in need_to_verify mode
+ if ( !kid_has_controller && enforce_pcrud && need_to_verify ) {
+ osrfLogInfo( OSRF_LOG_MARK, "%s is not listed as a controller for %s; moving on", modulename, core_class );
+
+ jsonObjectSetIndex(
+ cur,
+ (unsigned long) atoi( osrfHashGet(field, "array_position") ),
+ jsonNewObjectType(
+ !strcmp( reltype, "has_many" ) ? JSON_ARRAY : JSON_NULL
+ )
+ );
+ continue;
+ }
+
+ osrfStringArray* link_map = osrfHashGet( kid_link, "map" );
+
+ if( link_map->size > 0 ) {
+ jsonObject* _kid_key = jsonNewObjectType( JSON_ARRAY );
+ jsonObjectPush(
+ _kid_key,
+ jsonNewObject( osrfStringArrayGetString( link_map, 0 ) )
+ );
+
+ jsonObjectSetKey(
+ flesh_blob,
+ osrfHashGet( kid_link, "class" ),
+ _kid_key
+ );
+ };
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "Link field: %s, remote class: %s, fkey: %s, reltype: %s",
+ osrfHashGet( kid_link, "field" ),
+ osrfHashGet( kid_link, "class" ),
+ osrfHashGet( kid_link, "key" ),
+ osrfHashGet( kid_link, "reltype" )
+ );
+
+ const char* search_key = jsonObjectGetString(
+ jsonObjectGetIndex( cur,
+ atoi( osrfHashGet( value_field, "array_position" ) )
+ )
+ );
+
+ if( !search_key ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Nothing to search for!" );
+ continue;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "Creating param objects..." );
+
+ // construct WHERE clause
+ jsonObject* where_clause = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey(
+ where_clause,
+ osrfHashGet( kid_link, "key" ),
+ jsonNewObject( search_key )
+ );
+
+ // construct the rest of the query, mostly
+ // by copying pieces of the previous level of query
+ jsonObject* rest_of_query = jsonNewObjectType( JSON_HASH );
+ jsonObjectSetKey( rest_of_query, "flesh",
+ jsonNewNumberObject( flesh_depth - 1 + link_map->size )
+ );
+
+ if( flesh_blob )
+ jsonObjectSetKey( rest_of_query, "flesh_fields",
+ jsonObjectClone( flesh_blob ));
+
+ if( jsonObjectGetKeyConst( query_hash, "order_by" )) {
+ jsonObjectSetKey( rest_of_query, "order_by",
+ jsonObjectClone( jsonObjectGetKeyConst( query_hash, "order_by" ))
+ );
+ }
+
+ if( jsonObjectGetKeyConst( query_hash, "select" )) {
+ jsonObjectSetKey( rest_of_query, "select",
+ jsonObjectClone( jsonObjectGetKeyConst( query_hash, "select" ))
+ );
+ }
+
+ // do the query, recursively, to expand the fleshable field
+ jsonObject* kids = doFieldmapperSearch( ctx, kid_idl,
+ where_clause, rest_of_query, err );
+
+ jsonObjectFree( where_clause );
+ jsonObjectFree( rest_of_query );
+
+ if( *err ) {
+ osrfStringArrayFree( link_fields );
+ jsonObjectFree( res_list );
+ jsonObjectFree( flesh_blob );
+ return NULL;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "Search for %s return %d linked objects",
+ osrfHashGet( kid_link, "class" ), kids->size );
+
+ // Traverse the result set
+ jsonObject* X = NULL;
+ if( link_map->size > 0 && kids->size > 0 ) {
+ X = kids;
+ kids = jsonNewObjectType( JSON_ARRAY );
+
+ jsonObject* _k_node;
+ unsigned long res_idx = 0;
+ while((_k_node = jsonObjectGetIndex( X, res_idx++ ) )) {
+ jsonObjectPush(
+ kids,
+ jsonObjectClone(
+ jsonObjectGetIndex(
+ _k_node,
+ (unsigned long) atoi(
+ osrfHashGet(
+ osrfHashGet(
+ osrfHashGet(
+ osrfHashGet(
+ oilsIDL(),
+ osrfHashGet( kid_link, "class" )
+ ),
+ "fields"
+ ),
+ osrfStringArrayGetString( link_map, 0 )
+ ),
+ "array_position"
+ )
+ )
+ )
+ )
+ );
+ } // end while loop traversing X
+ }
+
+ if (kids->size > 0) {
+
+ if(( !strcmp( osrfHashGet( kid_link, "reltype" ), "has_a" )
+ || !strcmp( osrfHashGet( kid_link, "reltype" ), "might_have" ))
+ ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Storing fleshed objects in %s",
+ osrfHashGet( kid_link, "field" ));
+ jsonObjectSetIndex(
+ cur,
+ (unsigned long) atoi( osrfHashGet( field, "array_position" ) ),
+ jsonObjectClone( jsonObjectGetIndex( kids, 0 ))
+ );
+ }
+ }
+
+ if( !strcmp( osrfHashGet( kid_link, "reltype" ), "has_many" )) {
+ // has_many
+ osrfLogDebug( OSRF_LOG_MARK, "Storing fleshed objects in %s",
+ osrfHashGet( kid_link, "field" ) );
+ jsonObjectSetIndex(
+ cur,
+ (unsigned long) atoi( osrfHashGet( field, "array_position" ) ),
+ jsonObjectClone( kids )
+ );
+ }
+
+ if( X ) {
+ jsonObjectFree( kids );
+ kids = X;
+ }
+
+ jsonObjectFree( kids );
+
+ osrfLogDebug( OSRF_LOG_MARK, "Fleshing of %s complete",
+ osrfHashGet( kid_link, "field" ) );
+ osrfLogDebug( OSRF_LOG_MARK, "%s", jsonObjectToJSON( cur ));
+
+ } // end while loop traversing list of fleshable fields
+ }
+
+ if( i_respond_directly ) {
+ if ( *methodtype == 'i' ) {
+ osrfAppRespond( ctx,
+ oilsFMGetObject( cur, osrfHashGet( class_meta, "primarykey" ) ) );
+ } else {
+ osrfAppRespond( ctx, cur );
+ }
+ }
+ } // end while loop traversing res_list
+ jsonObjectFree( flesh_blob );
+ osrfStringArrayFree( link_fields );
+ }
+
+ if( i_respond_directly ) {
+ jsonObjectFree( res_list );
+ return jsonNewObjectType( JSON_ARRAY );
+ } else {
+ return res_list;
+ }
+}
+
+
+int doUpdate( osrfMethodContext* ctx ) {
+ if( osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+ jsonObject* target = NULL;
+ if( enforce_pcrud )
+ target = jsonObjectGetIndex( ctx->params, 1 );
+ else
+ target = jsonObjectGetIndex( ctx->params, 0 );
+
+ if(!verifyObjectClass( ctx, target )) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ if( getXactId( ctx ) == NULL ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for UPDATE"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // The following test is harmless but redundant. If a class is
+ // readonly, we don't register an update method for it.
+ if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "Cannot UPDATE readonly class"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ const char* trans_id = getXactId( ctx );
+
+ // Set the last_xact_id
+ int index = oilsIDL_ntop( target->classname, "last_xact_id" );
+ if( index > -1 ) {
+ osrfLogDebug( OSRF_LOG_MARK, "Setting last_xact_id to %s on %s at position %d",
+ trans_id, target->classname, index );
+ jsonObjectSetIndex( target, index, jsonNewObject( trans_id ));
+ }
+
+ char* pkey = osrfHashGet( meta, "primarykey" );
+ osrfHash* fields = osrfHashGet( meta, "fields" );
+
+ char* id = oilsFMGetString( target, pkey );
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "%s updating %s object with %s = %s",
+ modulename,
+ osrfHashGet( meta, "fieldmapper" ),
+ pkey,
+ id
+ );
+
+ dbhandle = writehandle;
+ growing_buffer* sql = buffer_init( 128 );
+ buffer_fadd( sql,"UPDATE %s SET", osrfHashGet( meta, "tablename" ));
+
+ int first = 1;
+ osrfHash* field_def = NULL;
+ osrfHashIterator* field_itr = osrfNewHashIterator( fields );
+ while( ( field_def = osrfHashIteratorNext( field_itr ) ) ) {
+
+ // Skip virtual fields, and the primary key
+ if( str_is_true( osrfHashGet( field_def, "virtual") ) )
+ continue;
+
+ if (osrfStringArrayContains( osrfHashGet(field_def, "suppress_controller"), modulename ))
+ continue;
+
+
+ const char* field_name = osrfHashIteratorKey( field_itr );
+ if( ! strcmp( field_name, pkey ) )
+ continue;
+
+ const jsonObject* field_object = oilsFMGetObject( target, field_name );
+
+ int value_is_numeric = 0; // boolean
+ char* value;
+ if( field_object && field_object->classname ) {
+ value = oilsFMGetString(
+ field_object,
+ (char*) oilsIDLFindPath( "/%s/primarykey", field_object->classname )
+ );
+ } else if( field_object && JSON_BOOL == field_object->type ) {
+ if( jsonBoolIsTrue( field_object ) )
+ value = strdup( "t" );
+ else
+ value = strdup( "f" );
+ } else {
+ value = jsonObjectToSimpleString( field_object );
+ if( field_object && JSON_NUMBER == field_object->type )
+ value_is_numeric = 1;
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "Updating %s object with %s = %s",
+ osrfHashGet( meta, "fieldmapper" ), field_name, value);
+
+ if( !field_object || field_object->type == JSON_NULL ) {
+ if( !( !( strcmp( osrfHashGet( meta, "classname" ), "au" ) )
+ && !( strcmp( field_name, "passwd" ) )) ) { // arg at the special case!
+ if( first )
+ first = 0;
+ else
+ OSRF_BUFFER_ADD_CHAR( sql, ',' );
+ buffer_fadd( sql, " %s = NULL", field_name );
+ }
+
+ } else if( value_is_numeric || !strcmp( get_primitive( field_def ), "number") ) {
+ if( first )
+ first = 0;
+ else
+ OSRF_BUFFER_ADD_CHAR( sql, ',' );
+
+ const char* numtype = get_datatype( field_def );
+ if( !strncmp( numtype, "INT", 3 ) ) {
+ buffer_fadd( sql, " %s = %ld", field_name, atol( value ) );
+ } else if( !strcmp( numtype, "NUMERIC" ) ) {
+ buffer_fadd( sql, " %s = %f", field_name, atof( value ) );
+ } else {
+ // Must really be intended as a string, so quote it
+ if( dbi_conn_quote_string( dbhandle, &value )) {
+ buffer_fadd( sql, " %s = %s", field_name, value );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]",
+ modulename, value );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Error quoting string -- please see the error log for more details"
+ );
+ free( value );
+ free( id );
+ osrfHashIteratorFree( field_itr );
+ buffer_free( sql );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+ }
+
+ osrfLogDebug( OSRF_LOG_MARK, "%s is of type %s", field_name, numtype );
+
+ } else {
+ if( dbi_conn_quote_string( dbhandle, &value ) ) {
+ if( first )
+ first = 0;
+ else
+ OSRF_BUFFER_ADD_CHAR( sql, ',' );
+ buffer_fadd( sql, " %s = %s", field_name, value );
+ } else {
+ osrfLogError( OSRF_LOG_MARK, "%s: Error quoting string [%s]", modulename, value );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Error quoting string -- please see the error log for more details"
+ );
+ free( value );
+ free( id );
+ osrfHashIteratorFree( field_itr );
+ buffer_free( sql );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+ }
+
+ free( value );
+
+ } // end while
+
+ osrfHashIteratorFree( field_itr );
+
+ jsonObject* obj = jsonNewObject( id );
+
+ if( strcmp( get_primitive( osrfHashGet( osrfHashGet(meta, "fields"), pkey )), "number" ))
+ dbi_conn_quote_string( dbhandle, &id );
+
+ buffer_fadd( sql, " WHERE %s = %s;", pkey, id );
+
+ char* query = buffer_release( sql );
+ osrfLogDebug( OSRF_LOG_MARK, "%s: Update SQL [%s]", modulename, query );
+
+ dbi_result result = dbi_conn_query( dbhandle, query );
+ free( query );
+
+ int rc = 0;
+ if( !result ) {
+ jsonObjectFree( obj );
+ obj = jsonNewObject( NULL );
+ const char* msg;
+ int errnum = dbi_conn_error( dbhandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR updating %s object with %s = %s: %d %s",
+ modulename,
+ osrfHashGet( meta, "fieldmapper" ),
+ pkey,
+ id,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Error in updating a row -- please see the error log for more details"
+ );
+ if( !oilsIsDBConnected( dbhandle ))
+ osrfAppSessionPanic( ctx->session );
+ rc = -1;
+ } else
+ dbi_result_free( result );
+
+ free( id );
+ osrfAppRespondComplete( ctx, obj );
+ jsonObjectFree( obj );
+ return rc;
+}
+
+int doDelete( osrfMethodContext* ctx ) {
+ if( osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ if( enforce_pcrud )
+ timeout_needs_resetting = 1;
+
+ osrfHash* meta = osrfHashGet( (osrfHash*) ctx->method->userData, "class" );
+
+ if( getXactId( ctx ) == NULL ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "No active transaction -- required for DELETE"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ // The following test is harmless but redundant. If a class is
+ // readonly, we don't register a delete method for it.
+ if( str_is_true( osrfHashGet( meta, "readonly" ) ) ) {
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_BADREQUEST,
+ "osrfMethodException",
+ ctx->request,
+ "Cannot DELETE readonly class"
+ );
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ dbhandle = writehandle;
+
+ char* pkey = osrfHashGet( meta, "primarykey" );
+
+ int _obj_pos = 0;
+ if( enforce_pcrud )
+ _obj_pos = 1;
+
+ char* id;
+ if( jsonObjectGetIndex( ctx->params, _obj_pos )->classname ) {
+ if( !verifyObjectClass( ctx, jsonObjectGetIndex( ctx->params, _obj_pos ))) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+
+ id = oilsFMGetString( jsonObjectGetIndex(ctx->params, _obj_pos), pkey );
+ } else {
+ if( enforce_pcrud && !verifyObjectPCRUD( ctx, meta, NULL, 1 )) {
+ osrfAppRespondComplete( ctx, NULL );
+ return -1;
+ }
+ id = jsonObjectToSimpleString( jsonObjectGetIndex( ctx->params, _obj_pos ));
+ }
+
+ osrfLogDebug(
+ OSRF_LOG_MARK,
+ "%s deleting %s object with %s = %s",
+ modulename,
+ osrfHashGet( meta, "fieldmapper" ),
+ pkey,
+ id
+ );
+
+ jsonObject* obj = jsonNewObject( id );
+
+ if( strcmp( get_primitive( osrfHashGet( osrfHashGet(meta, "fields"), pkey ) ), "number" ) )
+ dbi_conn_quote_string( writehandle, &id );
+
+ dbi_result result = dbi_conn_queryf( writehandle, "DELETE FROM %s WHERE %s = %s;",
+ osrfHashGet( meta, "tablename" ), pkey, id );
+
+ int rc = 0;
+ if( !result ) {
+ rc = -1;
+ jsonObjectFree( obj );
+ obj = jsonNewObject( NULL );
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR deleting %s object with %s = %s: %d %s",
+ modulename,
+ osrfHashGet( meta, "fieldmapper" ),
+ pkey,
+ id,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus(
+ ctx->session,
+ OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException",
+ ctx->request,
+ "Error in deleting a row -- please see the error log for more details"
+ );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ } else
+ dbi_result_free( result );
+
+ free( id );
+
+ osrfAppRespondComplete( ctx, obj );
+ jsonObjectFree( obj );
+ return rc;
+}
+
+/**
+ @brief Translate a row returned from the database into a jsonObject of type JSON_ARRAY.
+ @param result An iterator for a result set; we only look at the current row.
+ @param @meta Pointer to the class metadata for the core class.
+ @return Pointer to the resulting jsonObject if successful; otherwise NULL.
+
+ If a column is not defined in the IDL, or if it has no array_position defined for it in
+ the IDL, or if it is defined as virtual, ignore it.
+
+ Otherwise, translate the column value into a jsonObject of type JSON_NULL, JSON_NUMBER,
+ or JSON_STRING. Then insert this jsonObject into the JSON_ARRAY according to its
+ array_position in the IDL.
+
+ A field defined in the IDL but not represented in the returned row will leave a hole
+ in the JSON_ARRAY. In effect it will be treated as a null value.
+
+ In the resulting JSON_ARRAY, the field values appear in the sequence defined by the IDL,
+ regardless of their sequence in the SELECT statement. The JSON_ARRAY is assigned the
+ classname corresponding to the @a meta argument.
+
+ The calling code is responsible for freeing the the resulting jsonObject by calling
+ jsonObjectFree().
+*/
+static jsonObject* oilsMakeFieldmapperFromResult( dbi_result result, osrfHash* meta) {
+ if( !( result && meta )) return NULL;
+
+ jsonObject* object = jsonNewObjectType( JSON_ARRAY );
+ jsonObjectSetClass( object, osrfHashGet( meta, "classname" ));
+ osrfLogInternal( OSRF_LOG_MARK, "Setting object class to %s ", object->classname );
+
+ osrfHash* fields = osrfHashGet( meta, "fields" );
+
+ int columnIndex = 1;
+ const char* columnName;
+
+ /* cycle through the columns in the row returned from the database */
+ while( (columnName = dbi_result_get_field_name( result, columnIndex )) ) {
+
+ osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...", (char*) columnName );
+
+ int fmIndex = -1; // Will be set to the IDL's sequence number for this field
+
+ /* determine the field type and storage attributes */
+ unsigned short type = dbi_result_get_field_type_idx( result, columnIndex );
+ int attr = dbi_result_get_field_attribs_idx( result, columnIndex );
+
+ // Fetch the IDL's sequence number for the field. If the field isn't in the IDL,
+ // or if it has no sequence number there, or if it's virtual, skip it.
+ osrfHash* _f = osrfHashGet( fields, (char*) columnName );
+ if( _f ) {
+
+ if( str_is_true( osrfHashGet( _f, "virtual" )))
+ continue; // skip this column: IDL says it's virtual
+
+ const char* pos = (char*) osrfHashGet( _f, "array_position" );
+ if( !pos ) // IDL has no sequence number for it. This shouldn't happen,
+ continue; // since we assign sequence numbers dynamically as we load the IDL.
+
+ fmIndex = atoi( pos );
+ osrfLogInternal( OSRF_LOG_MARK, "... Found column at position [%s]...", pos );
+ } else {
+ continue; // This field is not defined in the IDL
+ }
+
+ // Stuff the column value into a slot in the JSON_ARRAY, indexed according to the
+ // sequence number from the IDL (which is likely to be different from the sequence
+ // of columns in the SELECT clause).
+ if( dbi_result_field_is_null_idx( result, columnIndex )) {
+ jsonObjectSetIndex( object, fmIndex, jsonNewObject( NULL ));
+ } else {
+
+ switch( type ) {
+
+ case DBI_TYPE_INTEGER :
+
+ if( attr & DBI_INTEGER_SIZE8 )
+ jsonObjectSetIndex( object, fmIndex,
+ jsonNewNumberObject(
+ dbi_result_get_longlong_idx( result, columnIndex )));
+ else
+ jsonObjectSetIndex( object, fmIndex,
+ jsonNewNumberObject( dbi_result_get_int_idx( result, columnIndex )));
+
+ break;
+
+ case DBI_TYPE_DECIMAL :
+ jsonObjectSetIndex( object, fmIndex,
+ jsonNewNumberObject( dbi_result_get_double_idx(result, columnIndex )));
+ break;
+
+ case DBI_TYPE_STRING :
+
+ jsonObjectSetIndex(
+ object,
+ fmIndex,
+ jsonNewObject( dbi_result_get_string_idx( result, columnIndex ))
+ );
+
+ break;
+
+ case DBI_TYPE_DATETIME : {
+
+ char dt_string[ 256 ] = "";
+ struct tm gmdt;
+
+ // Fetch the date column as a time_t
+ time_t _tmp_dt = dbi_result_get_datetime_idx( result, columnIndex );
+
+ // Translate the time_t to a human-readable string
+ if( !( attr & DBI_DATETIME_DATE )) {
+ gmtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%T", &gmdt );
+ } else if( !( attr & DBI_DATETIME_TIME )) {
+ localtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%d", &gmdt );
+ } else {
+ localtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%dT%T%z", &gmdt );
+ }
+
+ jsonObjectSetIndex( object, fmIndex, jsonNewObject( dt_string ));
+
+ break;
+ }
+ case DBI_TYPE_BINARY :
+ osrfLogError( OSRF_LOG_MARK,
+ "Can't do binary at column %s : index %d", columnName, columnIndex );
+ } // End switch
+ }
+ ++columnIndex;
+ } // End while
+
+ return object;
+}
+
+static jsonObject* oilsMakeJSONFromResult( dbi_result result ) {
+ if( !result ) return NULL;
+
+ jsonObject* object = jsonNewObject( NULL );
+
+ time_t _tmp_dt;
+ char dt_string[ 256 ];
+ struct tm gmdt;
+
+ int fmIndex;
+ int columnIndex = 1;
+ int attr;
+ unsigned short type;
+ const char* columnName;
+
+ /* cycle through the column list */
+ while(( columnName = dbi_result_get_field_name( result, columnIndex ))) {
+
+ osrfLogInternal( OSRF_LOG_MARK, "Looking for column named [%s]...", (char*) columnName );
+
+ fmIndex = -1; // reset the position
+
+ /* determine the field type and storage attributes */
+ type = dbi_result_get_field_type_idx( result, columnIndex );
+ attr = dbi_result_get_field_attribs_idx( result, columnIndex );
+
+ if( dbi_result_field_is_null_idx( result, columnIndex )) {
+ jsonObjectSetKey( object, columnName, jsonNewObject( NULL ));
+ } else {
+
+ switch( type ) {
+
+ case DBI_TYPE_INTEGER :
+
+ if( attr & DBI_INTEGER_SIZE8 )
+ jsonObjectSetKey( object, columnName,
+ jsonNewNumberObject( dbi_result_get_longlong_idx(
+ result, columnIndex )) );
+ else
+ jsonObjectSetKey( object, columnName, jsonNewNumberObject(
+ dbi_result_get_int_idx( result, columnIndex )) );
+ break;
+
+ case DBI_TYPE_DECIMAL :
+ jsonObjectSetKey( object, columnName, jsonNewNumberObject(
+ dbi_result_get_double_idx( result, columnIndex )) );
+ break;
+
+ case DBI_TYPE_STRING :
+ jsonObjectSetKey( object, columnName,
+ jsonNewObject( dbi_result_get_string_idx( result, columnIndex )));
+ break;
+
+ case DBI_TYPE_DATETIME :
+
+ memset( dt_string, '\0', sizeof( dt_string ));
+ memset( &gmdt, '\0', sizeof( gmdt ));
+
+ _tmp_dt = dbi_result_get_datetime_idx( result, columnIndex );
+
+ if( !( attr & DBI_DATETIME_DATE )) {
+ gmtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%T", &gmdt );
+ } else if( !( attr & DBI_DATETIME_TIME )) {
+ localtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%d", &gmdt );
+ } else {
+ localtime_r( &_tmp_dt, &gmdt );
+ strftime( dt_string, sizeof( dt_string ), "%04Y-%m-%dT%T%z", &gmdt );
+ }
+
+ jsonObjectSetKey( object, columnName, jsonNewObject( dt_string ));
+ break;
+
+ case DBI_TYPE_BINARY :
+ osrfLogError( OSRF_LOG_MARK,
+ "Can't do binary at column %s : index %d", columnName, columnIndex );
+ }
+ }
+ ++columnIndex;
+ } // end while loop traversing result
+
+ return object;
+}
+
+// Interpret a string as true or false
+int str_is_true( const char* str ) {
+ if( NULL == str || strcasecmp( str, "true" ) )
+ return 0;
+ else
+ return 1;
+}
+
+// Interpret a jsonObject as true or false
+static int obj_is_true( const jsonObject* obj ) {
+ if( !obj )
+ return 0;
+ else switch( obj->type )
+ {
+ case JSON_BOOL :
+ if( obj->value.b )
+ return 1;
+ else
+ return 0;
+ case JSON_STRING :
+ if( strcasecmp( obj->value.s, "true" ) )
+ return 0;
+ else
+ return 1;
+ case JSON_NUMBER : // Support 1/0 for perl's sake
+ if( jsonObjectGetNumber( obj ) == 1.0 )
+ return 1;
+ else
+ return 0;
+ default :
+ return 0;
+ }
+}
+
+// Translate a numeric code into a text string identifying a type of
+// jsonObject. To be used for building error messages.
+static const char* json_type( int code ) {
+ switch ( code )
+ {
+ case 0 :
+ return "JSON_HASH";
+ case 1 :
+ return "JSON_ARRAY";
+ case 2 :
+ return "JSON_STRING";
+ case 3 :
+ return "JSON_NUMBER";
+ case 4 :
+ return "JSON_NULL";
+ case 5 :
+ return "JSON_BOOL";
+ default :
+ return "(unrecognized)";
+ }
+}
+
+// Extract the "primitive" attribute from an IDL field definition.
+// If we haven't initialized the app, then we must be running in
+// some kind of testbed. In that case, default to "string".
+static const char* get_primitive( osrfHash* field ) {
+ const char* s = osrfHashGet( field, "primitive" );
+ if( !s ) {
+ if( child_initialized )
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR No \"datatype\" attribute for field \"%s\"",
+ modulename,
+ osrfHashGet( field, "name" )
+ );
+
+ s = "string";
+ }
+ return s;
+}
+
+// Extract the "datatype" attribute from an IDL field definition.
+// If we haven't initialized the app, then we must be running in
+// some kind of testbed. In that case, default to to NUMERIC,
+// since we look at the datatype only for numbers.
+static const char* get_datatype( osrfHash* field ) {
+ const char* s = osrfHashGet( field, "datatype" );
+ if( !s ) {
+ if( child_initialized )
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s ERROR No \"datatype\" attribute for field \"%s\"",
+ modulename,
+ osrfHashGet( field, "name" )
+ );
+ else
+ s = "NUMERIC";
+ }
+ return s;
+}
+
+/**
+ @brief Determine whether a string is potentially a valid SQL identifier.
+ @param s The identifier to be tested.
+ @return 1 if the input string is potentially a valid SQL identifier, or 0 if not.
+
+ Purpose: to prevent certain kinds of SQL injection. To that end we don't necessarily
+ need to follow all the rules exactly, such as requiring that the first character not
+ be a digit.
+
+ We allow leading and trailing white space. In between, we do not allow punctuation
+ (except for underscores and dollar signs), control characters, or embedded white space.
+
+ More pedantically we should allow quoted identifiers containing arbitrary characters, but
+ for the foreseeable future such quoted identifiers are not likely to be an issue.
+*/
+int is_identifier( const char* s) {
+ if( !s )
+ return 0;
+
+ // Skip leading white space
+ while( isspace( (unsigned char) *s ) )
+ ++s;
+
+ if( !s )
+ return 0; // Nothing but white space? Not okay.
+
+ // Check each character until we reach white space or
+ // end-of-string. Letters, digits, underscores, and
+ // dollar signs are okay. With the exception of periods
+ // (as in schema.identifier), control characters and other
+ // punctuation characters are not okay. Anything else
+ // is okay -- it could for example be part of a multibyte
+ // UTF8 character such as a letter with diacritical marks,
+ // and those are allowed.
+ do {
+ if( isalnum( (unsigned char) *s )
+ || '.' == *s
+ || '_' == *s
+ || '$' == *s )
+ ; // Fine; keep going
+ else if( ispunct( (unsigned char) *s )
+ || iscntrl( (unsigned char) *s ) )
+ return 0;
+ ++s;
+ } while( *s && ! isspace( (unsigned char) *s ) );
+
+ // If we found any white space in the above loop,
+ // the rest had better be all white space.
+
+ while( isspace( (unsigned char) *s ) )
+ ++s;
+
+ if( *s )
+ return 0; // White space was embedded within non-white space
+
+ return 1;
+}
+
+/**
+ @brief Determine whether to accept a character string as a comparison operator.
+ @param op The candidate comparison operator.
+ @return 1 if the string is acceptable as a comparison operator, or 0 if not.
+
+ We don't validate the operator for real. We just make sure that it doesn't contain
+ any semicolons or white space (with special exceptions for a few specific operators).
+ The idea is to block certain kinds of SQL injection. If it has no semicolons or white
+ space but it's still not a valid operator, then the database will complain.
+
+ Another approach would be to compare the string against a short list of approved operators.
+ We don't do that because we want to allow custom operators like ">100*", which at this
+ writing would be difficult or impossible to express otherwise in a JSON query.
+*/
+int is_good_operator( const char* op ) {
+ if( !op ) return 0; // Sanity check
+
+ const char* s = op;
+ while( *s ) {
+ if( isspace( (unsigned char) *s ) ) {
+ // Special exceptions for SIMILAR TO, IS DISTINCT FROM,
+ // and IS NOT DISTINCT FROM.
+ if( !strcasecmp( op, "similar to" ) )
+ return 1;
+ else if( !strcasecmp( op, "is distinct from" ) )
+ return 1;
+ else if( !strcasecmp( op, "is not distinct from" ) )
+ return 1;
+ else
+ return 0;
+ }
+ else if( ';' == *s )
+ return 0;
+ ++s;
+ }
+ return 1;
+}
+
+/**
+ @name Query Frame Management
+
+ The following machinery supports a stack of query frames for use by SELECT().
+
+ A query frame caches information about one level of a SELECT query. When we enter
+ a subquery, we push another query frame onto the stack, and pop it off when we leave.
+
+ The query frame stores information about the core class, and about any joined classes
+ in the FROM clause.
+
+ The main purpose is to map table aliases to classes and tables, so that a query can
+ join to the same table more than once. A secondary goal is to reduce the number of
+ lookups in the IDL by caching the results.
+*/
+/*@{*/
+
+#define STATIC_CLASS_INFO_COUNT 3
+
+static ClassInfo static_class_info[ STATIC_CLASS_INFO_COUNT ];
+
+/**
+ @brief Allocate a ClassInfo as raw memory.
+ @return Pointer to the newly allocated ClassInfo.
+
+ Except for the in_use flag, which is used only by the allocation and deallocation
+ logic, we don't initialize the ClassInfo here.
+*/
+static ClassInfo* allocate_class_info( void ) {
+ // In order to reduce the number of mallocs and frees, we return a static
+ // instance of ClassInfo, if we can find one that we're not already using.
+ // We rely on the fact that the compiler will implicitly initialize the
+ // static instances so that in_use == 0.
+
+ int i;
+ for( i = 0; i < STATIC_CLASS_INFO_COUNT; ++i ) {
+ if( ! static_class_info[ i ].in_use ) {
+ static_class_info[ i ].in_use = 1;
+ return static_class_info + i;
+ }
+ }
+
+ // The static ones are all in use. Malloc one.
+
+ return safe_malloc( sizeof( ClassInfo ) );
+}
+
+/**
+ @brief Free any malloc'd memory owned by a ClassInfo, returning it to a pristine state.
+ @param info Pointer to the ClassInfo to be cleared.
+*/
+static void clear_class_info( ClassInfo* info ) {
+ // Sanity check
+ if( ! info )
+ return;
+
+ // Free any malloc'd strings
+
+ if( info->alias != info->alias_store )
+ free( info->alias );
+
+ if( info->class_name != info->class_name_store )
+ free( info->class_name );
+
+ free( info->source_def );
+
+ info->alias = info->class_name = info->source_def = NULL;
+ info->next = NULL;
+}
+
+/**
+ @brief Free a ClassInfo and everything it owns.
+ @param info Pointer to the ClassInfo to be freed.
+*/
+static void free_class_info( ClassInfo* info ) {
+ // Sanity check
+ if( ! info )
+ return;
+
+ clear_class_info( info );
+
+ // If it's one of the static instances, just mark it as not in use
+
+ int i;
+ for( i = 0; i < STATIC_CLASS_INFO_COUNT; ++i ) {
+ if( info == static_class_info + i ) {
+ static_class_info[ i ].in_use = 0;
+ return;
+ }
+ }
+
+ // Otherwise it must have been malloc'd, so free it
+
+ free( info );
+}
+
+/**
+ @brief Populate an already-allocated ClassInfo.
+ @param info Pointer to the ClassInfo to be populated.
+ @param alias Alias for the class. If it is NULL, or an empty string, use the class
+ name for an alias.
+ @param class Name of the class.
+ @return Zero if successful, or 1 if not.
+
+ Populate the ClassInfo with copies of the alias and class name, and with pointers to
+ the relevant portions of the IDL for the specified class.
+*/
+static int build_class_info( ClassInfo* info, const char* alias, const char* class ) {
+ // Sanity checks
+ if( ! info ){
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: No ClassInfo available to populate", modulename );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ }
+
+ if( ! class ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: No class name provided for lookup", modulename );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ }
+
+ // Alias defaults to class name if not supplied
+ if( ! alias || ! alias[ 0 ] )
+ alias = class;
+
+ // Look up class info in the IDL
+ osrfHash* class_def = osrfHashGet( oilsIDL(), class );
+ if( ! class_def ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: Class %s not defined in IDL", modulename, class );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ } else if( str_is_true( osrfHashGet( class_def, "virtual" ) ) ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: Class %s is defined as virtual", modulename, class );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ }
+
+ osrfHash* links = osrfHashGet( class_def, "links" );
+ if( ! links ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: No links defined in IDL for class %s", modulename, class );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ }
+
+ osrfHash* fields = osrfHashGet( class_def, "fields" );
+ if( ! fields ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: No fields defined in IDL for class %s", modulename, class );
+ info->alias = info->class_name = info->source_def = NULL;
+ info->class_def = info->fields = info->links = NULL;
+ return 1;
+ }
+
+ char* source_def = oilsGetRelation( class_def );
+ if( ! source_def )
+ return 1;
+
+ // We got everything we need, so populate the ClassInfo
+ if( strlen( alias ) > ALIAS_STORE_SIZE )
+ info->alias = strdup( alias );
+ else {
+ strcpy( info->alias_store, alias );
+ info->alias = info->alias_store;
+ }
+
+ if( strlen( class ) > CLASS_NAME_STORE_SIZE )
+ info->class_name = strdup( class );
+ else {
+ strcpy( info->class_name_store, class );
+ info->class_name = info->class_name_store;
+ }
+
+ info->source_def = source_def;
+
+ info->class_def = class_def;
+ info->links = links;
+ info->fields = fields;
+
+ return 0;
+}
+
+#define STATIC_FRAME_COUNT 3
+
+static QueryFrame static_frame[ STATIC_FRAME_COUNT ];
+
+/**
+ @brief Allocate a QueryFrame as raw memory.
+ @return Pointer to the newly allocated QueryFrame.
+
+ Except for the in_use flag, which is used only by the allocation and deallocation
+ logic, we don't initialize the QueryFrame here.
+*/
+static QueryFrame* allocate_frame( void ) {
+ // In order to reduce the number of mallocs and frees, we return a static
+ // instance of QueryFrame, if we can find one that we're not already using.
+ // We rely on the fact that the compiler will implicitly initialize the
+ // static instances so that in_use == 0.
+
+ int i;
+ for( i = 0; i < STATIC_FRAME_COUNT; ++i ) {
+ if( ! static_frame[ i ].in_use ) {
+ static_frame[ i ].in_use = 1;
+ return static_frame + i;
+ }
+ }
+
+ // The static ones are all in use. Malloc one.
+
+ return safe_malloc( sizeof( QueryFrame ) );
+}
+
+/**
+ @brief Free a QueryFrame, and all the memory it owns.
+ @param frame Pointer to the QueryFrame to be freed.
+*/
+static void free_query_frame( QueryFrame* frame ) {
+ // Sanity check
+ if( ! frame )
+ return;
+
+ clear_class_info( &frame->core );
+
+ // Free the join list
+ ClassInfo* temp;
+ ClassInfo* info = frame->join_list;
+ while( info ) {
+ temp = info->next;
+ free_class_info( info );
+ info = temp;
+ }
+
+ frame->join_list = NULL;
+ frame->next = NULL;
+
+ // If the frame is a static instance, just mark it as unused
+ int i;
+ for( i = 0; i < STATIC_FRAME_COUNT; ++i ) {
+ if( frame == static_frame + i ) {
+ static_frame[ i ].in_use = 0;
+ return;
+ }
+ }
+
+ // Otherwise it must have been malloc'd, so free it
+
+ free( frame );
+}
+
+/**
+ @brief Search a given QueryFrame for a specified alias.
+ @param frame Pointer to the QueryFrame to be searched.
+ @param target The alias for which to search.
+ @return Pointer to the ClassInfo for the specified alias, if found; otherwise NULL.
+*/
+static ClassInfo* search_alias_in_frame( QueryFrame* frame, const char* target ) {
+ if( ! frame || ! target ) {
+ return NULL;
+ }
+
+ ClassInfo* found_class = NULL;
+
+ if( !strcmp( target, frame->core.alias ) )
+ return &(frame->core);
+ else {
+ ClassInfo* curr_class = frame->join_list;
+ while( curr_class ) {
+ if( strcmp( target, curr_class->alias ) )
+ curr_class = curr_class->next;
+ else {
+ found_class = curr_class;
+ break;
+ }
+ }
+ }
+
+ return found_class;
+}
+
+/**
+ @brief Push a new (blank) QueryFrame onto the stack.
+*/
+static void push_query_frame( void ) {
+ QueryFrame* frame = allocate_frame();
+ frame->join_list = NULL;
+ frame->next = curr_query;
+
+ // Initialize the ClassInfo for the core class
+ ClassInfo* core = &frame->core;
+ core->alias = core->class_name = core->source_def = NULL;
+ core->class_def = core->fields = core->links = NULL;
+
+ curr_query = frame;
+}
+
+/**
+ @brief Pop a QueryFrame off the stack and destroy it.
+*/
+static void pop_query_frame( void ) {
+ // Sanity check
+ if( ! curr_query )
+ return;
+
+ QueryFrame* popped = curr_query;
+ curr_query = popped->next;
+
+ free_query_frame( popped );
+}
+
+/**
+ @brief Populate the ClassInfo for the core class.
+ @param alias Alias for the core class. If it is NULL or an empty string, we use the
+ class name as an alias.
+ @param class_name Name of the core class.
+ @return Zero if successful, or 1 if not.
+
+ Populate the ClassInfo of the core class with copies of the alias and class name, and
+ with pointers to the relevant portions of the IDL for the core class.
+*/
+static int add_query_core( const char* alias, const char* class_name ) {
+
+ // Sanity checks
+ if( ! curr_query ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: No QueryFrame available for class %s", modulename, class_name );
+ return 1;
+ } else if( curr_query->core.alias ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: Core class %s already populated as %s",
+ modulename, curr_query->core.class_name, curr_query->core.alias );
+ return 1;
+ }
+
+ build_class_info( &curr_query->core, alias, class_name );
+ if( curr_query->core.alias )
+ return 0;
+ else {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: Unable to look up core class %s", modulename, class_name );
+ return 1;
+ }
+}
+
+/**
+ @brief Search the current QueryFrame for a specified alias.
+ @param target The alias for which to search.
+ @return A pointer to the corresponding ClassInfo, if found; otherwise NULL.
+*/
+static inline ClassInfo* search_alias( const char* target ) {
+ return search_alias_in_frame( curr_query, target );
+}
+
+/**
+ @brief Search all levels of query for a specified alias, starting with the current query.
+ @param target The alias for which to search.
+ @return A pointer to the corresponding ClassInfo, if found; otherwise NULL.
+*/
+static ClassInfo* search_all_alias( const char* target ) {
+ ClassInfo* found_class = NULL;
+ QueryFrame* curr_frame = curr_query;
+
+ while( curr_frame ) {
+ if(( found_class = search_alias_in_frame( curr_frame, target ) ))
+ break;
+ else
+ curr_frame = curr_frame->next;
+ }
+
+ return found_class;
+}
+
+/**
+ @brief Add a class to the list of classes joined to the current query.
+ @param alias Alias of the class to be added. If it is NULL or an empty string, we use
+ the class name as an alias.
+ @param classname The name of the class to be added.
+ @return A pointer to the ClassInfo for the added class, if successful; otherwise NULL.
+*/
+static ClassInfo* add_joined_class( const char* alias, const char* classname ) {
+
+ if( ! classname || ! *classname ) { // sanity check
+ osrfLogError( OSRF_LOG_MARK, "Can't join a class with no class name" );
+ return NULL;
+ }
+
+ if( ! alias )
+ alias = classname;
+
+ const ClassInfo* conflict = search_alias( alias );
+ if( conflict ) {
+ osrfLogError( OSRF_LOG_MARK,
+ "%s ERROR: Table alias \"%s\" conflicts with class \"%s\"",
+ modulename, alias, conflict->class_name );
+ return NULL;
+ }
+
+ ClassInfo* info = allocate_class_info();
+
+ if( build_class_info( info, alias, classname ) ) {
+ free_class_info( info );
+ return NULL;
+ }
+
+ // Add the new ClassInfo to the join list of the current QueryFrame
+ info->next = curr_query->join_list;
+ curr_query->join_list = info;
+
+ return info;
+}
+
+/**
+ @brief Destroy all nodes on the query stack.
+*/
+static void clear_query_stack( void ) {
+ while( curr_query )
+ pop_query_frame();
+}
+
+/**
+ @brief Implement the set_audit_info method.
+ @param ctx Pointer to the method context.
+ @return Zero if successful, or -1 if not.
+
+ Issue a SAVEPOINT to the database server.
+
+ Method parameters:
+ - authkey
+ - user id (int)
+ - workstation id (int)
+
+ If user id is not provided the authkey will be used.
+ For PCRUD the authkey is always used, even if a user is provided.
+*/
+int setAuditInfo( osrfMethodContext* ctx ) {
+ if(osrfMethodVerifyContext( ctx )) {
+ osrfLogError( OSRF_LOG_MARK, "Invalid method context" );
+ return -1;
+ }
+
+ // Get the user id from the parameters
+ const char* user_id = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 1) );
+
+ if( enforce_pcrud || !user_id ) {
+ timeout_needs_resetting = 1;
+ const jsonObject* user = verifyUserPCRUD( ctx );
+ if( !user )
+ return -1;
+ osrfAppRespondComplete( ctx, NULL );
+ return 0;
+ }
+
+ // Not PCRUD and have a user_id?
+ int result = writeAuditInfo( ctx, user_id, jsonObjectGetString( jsonObjectGetIndex(ctx->params, 2) ) );
+ osrfAppRespondComplete( ctx, NULL );
+ return result;
+}
+
+/**
+ @brief Save a audit info
+ @param ctx Pointer to the method context.
+ @param user_id User ID to write as a string
+ @param ws_id Workstation ID to write as a string
+*/
+int writeAuditInfo( osrfMethodContext* ctx, const char* user_id, const char* ws_id) {
+ if( ctx && ctx->session ) {
+ osrfAppSession* session = ctx->session;
+
+ osrfHash* cache = session->userData;
+
+ // If the session doesn't already have a hash, create one. Make sure
+ // that the application session frees the hash when it terminates.
+ if( NULL == cache ) {
+ session->userData = cache = osrfNewHash();
+ osrfHashSetCallback( cache, &sessionDataFree );
+ ctx->session->userDataFree = &userDataFree;
+ }
+
+ dbi_result result = dbi_conn_queryf( writehandle, "SELECT auditor.set_audit_info( %s, %s );", user_id, ws_id ? ws_id : "NULL" );
+ if( !result ) {
+ osrfLogWarning( OSRF_LOG_MARK, "BAD RESULT" );
+ const char* msg;
+ int errnum = dbi_conn_error( writehandle, &msg );
+ osrfLogError(
+ OSRF_LOG_MARK,
+ "%s: Error setting auditor information: %d %s",
+ modulename,
+ errnum,
+ msg ? msg : "(No description available)"
+ );
+ osrfAppSessionStatus( ctx->session, OSRF_STATUS_INTERNALSERVERERROR,
+ "osrfMethodException", ctx->request, "Error setting auditor info" );
+ if( !oilsIsDBConnected( writehandle ))
+ osrfAppSessionPanic( ctx->session );
+ return -1;
+ } else {
+ dbi_result_free( result );
+ }
+ }
+ return 0;
+}
+
+/**
+ @brief Remove all but safe character from savepoint name
+ @param sp User-supplied savepoint name
+ @return sanitized savepoint name, or NULL
+
+ The caller is expected to free the returned string. Note that
+ this function exists only because we can't use PQescapeLiteral
+ without either forking libdbi or abandoning it.
+*/
+static char* _sanitize_savepoint_name( const char* sp ) {
+
+ const char* safe_chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012345789_";
+
+ // PostgreSQL uses NAMEDATALEN-1 as a max length for identifiers,
+ // and the default value of NAMEDATALEN is 64; that should be long enough
+ // for our purposes, and it's unlikely that anyone is going to recompile
+ // PostgreSQL to have a smaller value, so cap the identifier name
+ // accordingly to avoid the remote chance that someone manages to pass in a
+ // 12GB savepoint name
+ const int MAX_LITERAL_NAMELEN = 63;
+ int len = 0;
+ len = strlen( sp );
+ if (len > MAX_LITERAL_NAMELEN) {
+ len = MAX_LITERAL_NAMELEN;
+ }
+
+ char* safeSpName = safe_malloc( len + 1 );
+ int i = 0;
+ int j;
+ char* found;
+ for (j = 0; j < len; j++) {
+ found = strchr(safe_chars, sp[j]);
+ if (found) {
+ safeSpName[ i++ ] = found[0];
+ }
+ }
+ safeSpName[ i ] = '\0';
+ return safeSpName;
+}
+
+/*@}*/
--- /dev/null
+#include <ctype.h>
+#include <limits.h>
+#include "openils/oils_utils.h"
+#include "openils/oils_idl.h"
+
+osrfHash* oilsInitIDL(const char* idl_filename) {
+
+ char* freeable_filename = NULL;
+ const char* filename;
+
+ if(idl_filename)
+ filename = idl_filename;
+ else {
+ freeable_filename = osrf_settings_host_value("/IDL");
+ filename = freeable_filename;
+ }
+
+ if (!filename) {
+ osrfLogError(OSRF_LOG_MARK, "No settings config for '/IDL'");
+ return NULL;
+ }
+
+ osrfLogInfo(OSRF_LOG_MARK, "Parsing IDL %s", filename);
+
+ if (!oilsIDLInit( filename )) {
+ osrfLogError(OSRF_LOG_MARK, "Problem loading IDL file [%s]!", filename);
+ if(freeable_filename)
+ free(freeable_filename);
+ return NULL;
+ }
+
+ if(freeable_filename)
+ free(freeable_filename);
+ return oilsIDL();
+}
+
+/**
+ @brief Return a const string with the value of a specified column in a row object.
+ @param object Pointer to the row object.
+ @param field Name of the column.
+ @return Pointer to a const string representing the value of the specified column,
+ or NULL in case of error.
+
+ The row object must be a JSON_ARRAY with a classname. The column value must be a
+ JSON_STRING or a JSON_NUMBER. Any other object type results in a return of NULL.
+
+ The return value points into the internal contents of the row object, which
+ retains ownership.
+*/
+const char* oilsFMGetStringConst( const jsonObject* object, const char* field ) {
+ return jsonObjectGetString(oilsFMGetObject( object, field ));
+}
+
+/**
+ @brief Return a string with the value of a specified column in a row object.
+ @param object Pointer to the row object.
+ @param field Name of the column.
+ @return Pointer to a newly allocated string representing the value of the specified column,
+ or NULL in case of error.
+
+ The row object must be a JSON_ARRAY with a classname. The column value must be a
+ JSON_STRING or a JSON_NUMBER. Any other object type results in a return of NULL.
+
+ The calling code is responsible for freeing the returned string by calling free().
+ */
+char* oilsFMGetString( const jsonObject* object, const char* field ) {
+ return jsonObjectToSimpleString(oilsFMGetObject( object, field ));
+}
+
+/**
+ @brief Return a pointer to the value of a specified column in a row object.
+ @param object Pointer to the row object.
+ @param field Name of the column.
+ @return Pointer to the jsonObject representing the value of the specified column, or NULL
+ in case of error.
+
+ The row object must be a JSON_ARRAY with a classname.
+
+ The return value may point to a JSON_NULL, JSON_STRING, JSON_NUMBER, or JSON_ARRAY. It
+ points into the internal contents of the row object, which retains ownership.
+*/
+const jsonObject* oilsFMGetObject( const jsonObject* object, const char* field ) {
+ if(!(object && field)) return NULL;
+ if( object->type != JSON_ARRAY || !object->classname ) return NULL;
+ int pos = fm_ntop(object->classname, field);
+ if( pos > -1 )
+ return jsonObjectGetIndex( object, pos );
+ return NULL;
+}
+
+
+int oilsFMSetString( jsonObject* object, const char* field, const char* string ) {
+ if(!(object && field && string)) return -1;
+ osrfLogInternal(OSRF_LOG_MARK, "oilsFMSetString(): Collecing position for field %s", field);
+ int pos = fm_ntop(object->classname, field);
+ if( pos > -1 ) {
+ osrfLogInternal(OSRF_LOG_MARK, "oilsFMSetString(): Setting string "
+ "%s at field %s [position %d]", string, field, pos );
+ jsonObjectSetIndex( object, pos, jsonNewObject(string) );
+ return 0;
+ }
+ return -1;
+}
+
+
+int oilsUtilsIsDBTrue( const char* val ) {
+ if( val && strcasecmp(val, "f") && strcmp(val, "0") ) return 1;
+ return 0;
+}
+
+
+long oilsFMGetObjectId( const jsonObject* obj ) {
+ long id = -1;
+ if(!obj) return id;
+ char* ids = oilsFMGetString( obj, "id" );
+ if(ids) {
+ id = atol(ids);
+ free(ids);
+ }
+ return id;
+}
+
+int oilsUtilsTrackUserActivity(long usr, const char* ewho, const char* ewhat, const char* ehow) {
+ if (!usr && !(ewho || ewhat || ehow)) return 0;
+ int rowcount = 0;
+
+ jsonObject* params = jsonParseFmt(
+ "{\"from\":[\"actor.insert_usr_activity\", %ld, \"%s\", \"%s\", \"%s\"]}",
+ usr,
+ (NULL == ewho) ? "" : ewho,
+ (NULL == ewhat) ? "" : ewhat,
+ (NULL == ehow) ? "" : ehow
+ );
+
+ osrfAppSession* session = osrfAppSessionClientInit("open-ils.cstore");
+ osrfAppSessionConnect(session);
+ int reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
+ osrfMessage* omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+ if(omsg) {
+ osrfMessageFree(omsg);
+ reqid = osrfAppSessionSendRequest(session, params, "open-ils.cstore.json_query", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+ if(omsg) {
+ const jsonObject* rows = osrfMessageGetResult(omsg);
+ if (rows) rowcount = rows->size;
+ osrfMessageFree(omsg); // frees rows
+ if (rowcount) {
+ reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.commit", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+ osrfMessageFree(omsg);
+ } else {
+ reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+ osrfMessageFree(omsg);
+ }
+ }
+ }
+
+ osrfAppSessionFree(session); // calls disconnect internally
+ jsonObjectFree(params);
+ return rowcount;
+}
+
+
+
+oilsEvent* oilsUtilsCheckPerms( int userid, int orgid, char* permissions[], int size ) {
+ if (!permissions) return NULL;
+ int i;
+ oilsEvent* evt = NULL;
+
+ // Find the root org unit, i.e. the one with no parent.
+ // Assumption: there is only one org unit with no parent.
+ if (orgid == -1) {
+ jsonObject* where_clause = jsonParse( "{\"parent_ou\":null}" );
+ jsonObject* org = oilsUtilsQuickReq(
+ "open-ils.cstore",
+ "open-ils.cstore.direct.actor.org_unit.search",
+ where_clause
+ );
+ jsonObjectFree( where_clause );
+
+ orgid = (int)jsonObjectGetNumber( oilsFMGetObject( org, "id" ) );
+
+ jsonObjectFree(org);
+ }
+
+ for( i = 0; i < size && permissions[i]; i++ ) {
+
+ char* perm = permissions[i];
+ jsonObject* params = jsonParseFmt("[%d, \"%s\", %d]", userid, perm, orgid);
+ jsonObject* o = oilsUtilsQuickReq( "open-ils.storage",
+ "open-ils.storage.permission.user_has_perm", params );
+
+ char* r = jsonObjectToSimpleString(o);
+
+ if(r && !strcmp(r, "0"))
+ evt = oilsNewEvent3( OSRF_LOG_MARK, OILS_EVENT_PERM_FAILURE, perm, orgid );
+
+ jsonObjectFree(params);
+ jsonObjectFree(o);
+ free(r);
+
+ if(evt)
+ break;
+ }
+
+ return evt;
+}
+
+/**
+ @brief Perform a remote procedure call.
+ @param service The name of the service to invoke.
+ @param method The name of the method to call.
+ @param params The parameters to be passed to the method, if any.
+ @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+ doesn't return anything.
+
+ If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+ as a separate parameter. If it points to any other kind of jsonObject, pass it as a
+ single parameter. If it is NULL, pass no parameters.
+
+ The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsQuickReq( const char* service, const char* method,
+ const jsonObject* params ) {
+ if(!(service && method)) return NULL;
+
+ osrfLogDebug(OSRF_LOG_MARK, "oilsUtilsQuickReq(): %s - %s", service, method );
+
+ // Open an application session with the service, and send the request
+ osrfAppSession* session = osrfAppSessionClientInit( service );
+ int reqid = osrfAppSessionSendRequest( session, params, method, 1 );
+
+ // Get the response
+ osrfMessage* omsg = osrfAppSessionRequestRecv( session, reqid, 60 );
+ jsonObject* result = jsonObjectClone( osrfMessageGetResult(omsg) );
+
+ // Clean up
+ osrfMessageFree(omsg);
+ osrfAppSessionFree(session);
+ return result;
+}
+
+/**
+ @brief Call a method of the open-ils.storage service.
+ @param method Name of the method.
+ @param params Parameters to be passed to the method, if any.
+ @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+ doesn't return anything.
+
+ If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+ as a separate parameter. If it points to any other kind of jsonObject, pass it as a
+ single parameter. If it is NULL, pass no parameters.
+
+ The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsStorageReq( const char* method, const jsonObject* params ) {
+ return oilsUtilsQuickReq( "open-ils.storage", method, params );
+}
+
+/**
+ @brief Call a method of the open-ils.cstore service.
+ @param method Name of the method.
+ @param params Parameters to be passed to the method, if any.
+ @return A copy of whatever the method returns as a result, or a JSON_NULL if the method
+ doesn't return anything.
+
+ If the @a params parameter points to a JSON_ARRAY, pass each element of the array
+ as a separate parameter. If it points to any other kind of jsonObject, pass it as a
+ single parameter. If it is NULL, pass no parameters.
+
+ The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsCStoreReq( const char* method, const jsonObject* params ) {
+ return oilsUtilsQuickReq("open-ils.cstore", method, params);
+}
+
+
+
+/**
+ @brief Given a username, fetch the corresponding row from the actor.usr table, if any.
+ @param name The username for which to search.
+ @return A Fieldmapper object for the relevant row in the actor.usr table, if it exists;
+ or a JSON_NULL if it doesn't.
+
+ The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsFetchUserByUsername( const char* name ) {
+ if(!name) return NULL;
+ jsonObject* params = jsonParseFmt("{\"usrname\":\"%s\"}", name);
+ jsonObject* user = oilsUtilsQuickReq(
+ "open-ils.cstore", "open-ils.cstore.direct.actor.user.search", params );
+
+ jsonObjectFree(params);
+ long id = oilsFMGetObjectId(user);
+ osrfLogDebug(OSRF_LOG_MARK, "Fetched user %s:%ld", name, id);
+ return user;
+}
+
+/**
+ @brief Given a barcode, fetch the corresponding row from the actor.usr table, if any.
+ @param name The barcode for which to search.
+ @return A Fieldmapper object for the relevant row in the actor.usr table, if it exists;
+ or a JSON_NULL if it doesn't.
+
+ Look up the barcode in actor.card. Follow a foreign key from there to get a row in
+ actor.usr.
+
+ The calling code is responsible for freeing the returned object by calling jsonObjectFree().
+*/
+jsonObject* oilsUtilsFetchUserByBarcode(const char* barcode) {
+ if(!barcode) return NULL;
+
+ osrfLogInfo(OSRF_LOG_MARK, "Fetching user by barcode %s", barcode);
+
+ jsonObject* params = jsonParseFmt("{\"barcode\":\"%s\"}", barcode);
+ jsonObject* card = oilsUtilsQuickReq(
+ "open-ils.cstore", "open-ils.cstore.direct.actor.card.search", params );
+ jsonObjectFree(params);
+
+ if(!card)
+ return NULL; // No such card
+
+ // Get the user's id as a double
+ char* usr = oilsFMGetString(card, "usr");
+ jsonObjectFree(card);
+ if(!usr)
+ return NULL; // No user id (shouldn't happen)
+ double iusr = strtod(usr, NULL);
+ free(usr);
+
+ // Look up the user in actor.usr
+ params = jsonParseFmt("[%f]", iusr);
+ jsonObject* user = oilsUtilsQuickReq(
+ "open-ils.cstore", "open-ils.cstore.direct.actor.user.retrieve", params);
+
+ jsonObjectFree(params);
+ return user;
+}
+
+char* oilsUtilsFetchOrgSetting( int orgid, const char* setting ) {
+ if(!setting) return NULL;
+
+ jsonObject* params = jsonParseFmt("[%d, \"%s\"]", orgid, setting );
+
+ jsonObject* set = oilsUtilsQuickReq(
+ "open-ils.actor",
+ "open-ils.actor.ou_setting.ancestor_default", params);
+
+ char* value = jsonObjectToSimpleString( jsonObjectGetKeyConst( set, "value" ));
+ jsonObjectFree(params);
+ jsonObjectFree(set);
+ osrfLogDebug(OSRF_LOG_MARK, "Fetched org [%d] setting: %s => %s", orgid, setting, value);
+ return value;
+}
+
+
+
+char* oilsUtilsLogin( const char* uname, const char* passwd, const char* type, int orgId ) {
+ if(!(uname && passwd)) return NULL;
+
+ osrfLogDebug(OSRF_LOG_MARK, "Logging in with username %s", uname );
+ char* token = NULL;
+
+ jsonObject* params = jsonParseFmt("[\"%s\"]", uname);
+
+ jsonObject* o = oilsUtilsQuickReq(
+ "open-ils.auth", "open-ils.auth.authenticate.init", params );
+
+ const char* seed = jsonObjectGetString(o);
+ char* passhash = md5sum(passwd);
+ char buf[256];
+ snprintf(buf, sizeof(buf), "%s%s", seed, passhash);
+ char* fullhash = md5sum(buf);
+
+ jsonObjectFree(o);
+ jsonObjectFree(params);
+ free(passhash);
+
+ params = jsonParseFmt( "[\"%s\", \"%s\", \"%s\", \"%d\"]", uname, fullhash, type, orgId );
+ o = oilsUtilsQuickReq( "open-ils.auth",
+ "open-ils.auth.authenticate.complete", params );
+
+ if(o) {
+ const char* tok = jsonObjectGetString(
+ jsonObjectGetKeyConst( jsonObjectGetKey( o,"payload" ), "authtoken" ));
+ if( tok )
+ token = strdup( tok );
+ }
+
+ free(fullhash);
+ jsonObjectFree(params);
+ jsonObjectFree(o);
+
+ return token;
+}
+
+
+jsonObject* oilsUtilsFetchWorkstation( long id ) {
+ jsonObject* p = jsonParseFmt("[%ld]", id);
+ jsonObject* r = oilsUtilsQuickReq(
+ "open-ils.storage",
+ "open-ils.storage.direct.actor.workstation.retrieve", p );
+ jsonObjectFree(p);
+ return r;
+}
+
+jsonObject* oilsUtilsFetchWorkstationByName( const char* name ) {
+ jsonObject* p = jsonParseFmt("{\"name\":\"%s\"}", name);
+ jsonObject* r = oilsUtilsCStoreReq(
+ "open-ils.cstore.direct.actor.workstation.search", p);
+ jsonObjectFree(p);
+ return r;
+}
+
+/**
+ @brief Convert a string to a number representing a time interval in seconds.
+ @param interval Pointer to string, e.g. "420" or "2 weeks".
+ @return If successful, the number of seconds that the string represents; otherwise -1.
+
+ If the string is all digits (apart from any leading or trailing white space), convert
+ it directly. Otherwise pass it to PostgreSQL for translation.
+
+ The result is the same as if we were to pass every string to PostgreSQL, except that,
+ depending on the value of LONG_MAX, we return values for some strings that represent
+ intervals too long for PostgreSQL to represent (i.e. more than 2147483647 seconds).
+
+ WARNING: a valid interval of -1 second will be indistinguishable from an error. If
+ such an interval is a plausible possibility, don't use this function.
+*/
+long oilsUtilsIntervalToSeconds( const char* s ) {
+
+ if( !s ) {
+ osrfLogWarning( OSRF_LOG_MARK, "String to be converted is NULL" );
+ return -1;
+ }
+
+ // Skip leading white space
+ while( isspace( (unsigned char) *s ))
+ ++s;
+
+ if( '\0' == *s ) {
+ osrfLogWarning( OSRF_LOG_MARK, "String to be converted is empty or all white space" );
+ return -1;
+ }
+
+ // See if the string is a raw number, i.e. all digits
+ // (apart from any leading or trailing white space)
+
+ const char* p = s; // For traversing and examining the remaining string
+ if( isdigit( (unsigned char) *p )) {
+ // Looks like a number so far...skip over the digits
+ do {
+ ++p;
+ } while( isdigit( (unsigned char) *p ));
+ // Skip over any following white space
+ while( isspace( (unsigned char) *p ))
+ ++p;
+ if( '\0' == *p ) {
+ // This string is a raw number. Convert it directly.
+ long n = strtol( s, NULL, 10 );
+ if( LONG_MAX == n ) {
+ // numeric overflow
+ osrfLogWarning( OSRF_LOG_MARK,
+ "String \"%s\"represents a number too big for a long", s );
+ return -1;
+ } else
+ return n;
+ }
+ }
+
+ // If we get to this point, the string is not all digits. Pass it to PostgreSQL.
+
+ // Build the query
+ jsonObject* query_obj = jsonParseFmt(
+ "{\"from\":[\"config.interval_to_seconds\",\"%s\"]}", s );
+
+ // Execute the query
+ jsonObject* result = oilsUtilsCStoreReq(
+ "open-ils.cstore.json_query", query_obj );
+ jsonObjectFree( query_obj );
+
+ // Get the results
+ const jsonObject* seconds_obj = jsonObjectGetKeyConst( result, "config.interval_to_seconds" );
+ long seconds = -1;
+ if( seconds_obj && JSON_NUMBER == seconds_obj->type )
+ seconds = (long) jsonObjectGetNumber( seconds_obj );
+ else
+ osrfLogError( OSRF_LOG_MARK,
+ "Error calling json_query to convert \"%s\" to seconds", s );
+
+ jsonObjectFree( result );
+ return seconds;
+}
--- /dev/null
+#include "opensrf/osrf_app_session.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_settings.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/log.h"
+#include "openils/oils_utils.h"
+#include "openils/oils_constants.h"
+#include "openils/oils_event.h"
+
+#define OILS_AUTH_CACHE_PRFX "oils_auth_"
+#define OILS_AUTH_COUNT_SFFX "_count"
+
+#define MODULENAME "open-ils.auth"
+
+#define OILS_AUTH_OPAC "opac"
+#define OILS_AUTH_STAFF "staff"
+#define OILS_AUTH_TEMP "temp"
+#define OILS_AUTH_PERSIST "persist"
+
+// Default time for extending a persistent session: ten minutes
+#define DEFAULT_RESET_INTERVAL 10 * 60
+
+int osrfAppInitialize();
+int osrfAppChildInit();
+
+static long _oilsAuthOPACTimeout = 0;
+static long _oilsAuthStaffTimeout = 0;
+static long _oilsAuthOverrideTimeout = 0;
+static long _oilsAuthPersistTimeout = 0;
+static long _oilsAuthSeedTimeout = 0;
+static long _oilsAuthBlockTimeout = 0;
+static long _oilsAuthBlockCount = 0;
+
+
+/**
+ @brief Initialize the application by registering functions for method calls.
+ @return Zero in all cases.
+*/
+int osrfAppInitialize() {
+
+ osrfLogInfo(OSRF_LOG_MARK, "Initializing Auth Server...");
+
+ /* load and parse the IDL */
+ if (!oilsInitIDL(NULL)) return 1; /* return non-zero to indicate error */
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.authenticate.init",
+ "oilsAuthInit",
+ "Start the authentication process and returns the intermediate authentication seed"
+ " PARAMS( username )", 1, 0 );
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.authenticate.complete",
+ "oilsAuthComplete",
+ "Completes the authentication process. Returns an object like so: "
+ "{authtoken : <token>, authtime:<time>}, where authtoken is the login "
+ "token and authtime is the number of seconds the session will be active"
+ "PARAMS(username, md5sum( seed + md5sum( password ) ), type, org_id ) "
+ "type can be one of 'opac','staff', or 'temp' and it defaults to 'staff' "
+ "org_id is the location at which the login should be considered "
+ "active for login timeout purposes", 1, 0 );
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.authenticate.verify",
+ "oilsAuthComplete",
+ "Verifies the user provided a valid username and password."
+ "Params and are the same as open-ils.auth.authenticate.complete."
+ "Returns SUCCESS event on success, failure event on failure", 1, 0);
+
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.session.retrieve",
+ "oilsAuthSessionRetrieve",
+ "Pass in the auth token and this retrieves the user object. The auth "
+ "timeout is reset when this call is made "
+ "Returns the user object (password blanked) for the given login session "
+ "PARAMS( authToken )", 1, 0 );
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.session.delete",
+ "oilsAuthSessionDelete",
+ "Destroys the given login session "
+ "PARAMS( authToken )", 1, 0 );
+
+ osrfAppRegisterMethod(
+ MODULENAME,
+ "open-ils.auth.session.reset_timeout",
+ "oilsAuthResetTimeout",
+ "Resets the login timeout for the given session "
+ "Returns an ILS Event with payload = session_timeout of session "
+ "if found, otherwise returns the NO_SESSION event"
+ "PARAMS( authToken )", 1, 0 );
+
+ if(!_oilsAuthSeedTimeout) { /* Load the default timeouts */
+
+ jsonObject* value_obj;
+
+ value_obj = osrf_settings_host_value_object(
+ "/apps/open-ils.auth/app_settings/auth_limits/seed" );
+ _oilsAuthSeedTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+ jsonObjectFree(value_obj);
+ if( -1 == _oilsAuthSeedTimeout ) {
+ osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Auth Seeds - Using 30 seconds" );
+ _oilsAuthSeedTimeout = 30;
+ }
+
+ value_obj = osrf_settings_host_value_object(
+ "/apps/open-ils.auth/app_settings/auth_limits/block_time" );
+ _oilsAuthBlockTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+ jsonObjectFree(value_obj);
+ if( -1 == _oilsAuthBlockTimeout ) {
+ osrfLogWarning( OSRF_LOG_MARK, "Invalid timeout for Blocking Timeout - Using 3x Seed" );
+ _oilsAuthBlockTimeout = _oilsAuthSeedTimeout * 3;
+ }
+
+ value_obj = osrf_settings_host_value_object(
+ "/apps/open-ils.auth/app_settings/auth_limits/block_count" );
+ _oilsAuthBlockCount = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+ jsonObjectFree(value_obj);
+ if( -1 == _oilsAuthBlockCount ) {
+ osrfLogWarning( OSRF_LOG_MARK, "Invalid count for Blocking - Using 10" );
+ _oilsAuthBlockCount = 10;
+ }
+
+ osrfLogInfo(OSRF_LOG_MARK, "Set auth limits: "
+ "seed => %ld : block_timeout => %ld : block_count => %ld",
+ _oilsAuthSeedTimeout, _oilsAuthBlockTimeout, _oilsAuthBlockCount );
+ }
+
+ return 0;
+}
+
+/**
+ @brief Dummy placeholder for initializing a server drone.
+
+ There is nothing to do, so do nothing.
+*/
+int osrfAppChildInit() {
+ return 0;
+}
+
+/**
+ @brief Implement the "init" method.
+ @param ctx The method context.
+ @return Zero if successful, or -1 if not.
+
+ Method parameters:
+ - username
+
+ Return to client: Intermediate authentication seed.
+
+ Combine the username with a timestamp and process ID, and take an md5 hash of the result.
+ Store the hash in memcache, with a key based on the username. Then return the hash to
+ the client.
+
+ However: if the username includes one or more embedded blank spaces, return a dummy
+ hash without storing anything in memcache. The dummy will never match a stored hash, so
+ any attempt to authenticate with it will fail.
+*/
+int oilsAuthInit( osrfMethodContext* ctx ) {
+ OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+ char* username = jsonObjectToSimpleString( jsonObjectGetIndex(ctx->params, 0) );
+ if( username ) {
+
+ jsonObject* resp;
+
+ if( strchr( username, ' ' ) ) {
+
+ // Embedded spaces are not allowed in a username. Use "x" as a dummy
+ // seed. It will never be a valid seed because 'x' is not a hex digit.
+ resp = jsonNewObject( "x" );
+
+ } else {
+
+ // Build a key and a seed; store them in memcache.
+ char* key = va_list_to_string( "%s%s", OILS_AUTH_CACHE_PRFX, username );
+ char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, username, OILS_AUTH_COUNT_SFFX );
+ char* seed = md5sum( "%d.%ld.%s", (int) time(NULL), (long) getpid(), username );
+ jsonObject* countobject = osrfCacheGetObject( countkey );
+ if(!countobject) {
+ countobject = jsonNewNumberObject( (double) 0 );
+ }
+ osrfCachePutString( key, seed, _oilsAuthSeedTimeout );
+ osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+
+ osrfLogDebug( OSRF_LOG_MARK, "oilsAuthInit(): has seed %s and key %s", seed, key );
+
+ // Build a returnable object containing the seed.
+ resp = jsonNewObject( seed );
+
+ free( seed );
+ free( key );
+ free( countkey );
+ jsonObjectFree( countobject );
+ }
+
+ // Return the seed to the client.
+ osrfAppRespondComplete( ctx, resp );
+
+ jsonObjectFree(resp);
+ free(username);
+ return 0;
+ }
+
+ return -1; // Error: no username parameter
+}
+
+/**
+ Verifies that the user has permission to login with the
+ given type. If the permission fails, an oilsEvent is returned
+ to the caller.
+ @return -1 if the permission check failed, 0 if the permission
+ is granted
+*/
+static int oilsAuthCheckLoginPerm(
+ osrfMethodContext* ctx, const jsonObject* userObj ) {
+
+ if(!userObj) return -1;
+ oilsEvent* perm = NULL;
+
+ char* permissions[] = { "LOGIN" };
+ perm = oilsUtilsCheckPerms( oilsFMGetObjectId( userObj ), -1, permissions, 1 );
+
+ if(perm) {
+ osrfAppRespondComplete( ctx, oilsEventToJSON(perm) );
+ oilsEventFree(perm);
+ return -1;
+ }
+
+ return 0;
+}
+
+/**
+ Returns 1 if the password provided matches the user's real password
+ Returns 0 otherwise
+ Returns -1 on error
+*/
+/**
+ @brief Verify the password received from the client.
+ @param ctx The method context.
+ @param userObj An object from the database, representing the user.
+ @param password An obfuscated password received from the client.
+ @return 1 if the password is valid; 0 if it isn't; or -1 upon error.
+
+ (None of the so-called "passwords" used here are in plaintext. All have been passed
+ through at least one layer of hashing to obfuscate them.)
+
+ Take the password from the user object. Append it to the username seed from memcache,
+ as stored previously by a call to the init method. Take an md5 hash of the result.
+ Then compare this hash to the password received from the client.
+
+ In order for the two to match, other than by dumb luck, the client had to construct
+ the password it passed in the same way. That means it neded to know not only the
+ original password (either hashed or plaintext), but also the seed. The latter requirement
+ means that the client process needs either to be the same process that called the init
+ method or to receive the seed from the process that did so.
+*/
+static int oilsAuthVerifyPassword( const osrfMethodContext* ctx,
+ const jsonObject* userObj, const char* uname, const char* password ) {
+
+ // Get the username seed, as stored previously in memcache by the init method
+ char* seed = osrfCacheGetString( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+ if(!seed) {
+ return osrfAppRequestRespondException( ctx->session,
+ ctx->request, "No authentication seed found. "
+ "open-ils.auth.authenticate.init must be called first "
+ " (check that memcached is running and can be connected to) "
+ );
+ }
+
+ // We won't be needing the seed again, remove it
+ osrfCacheRemove( "%s%s", OILS_AUTH_CACHE_PRFX, uname );
+
+ // Get the hashed password from the user object
+ char* realPassword = oilsFMGetString( userObj, "passwd" );
+
+ osrfLogInternal(OSRF_LOG_MARK, "oilsAuth retrieved real password: [%s]", realPassword);
+ osrfLogDebug(OSRF_LOG_MARK, "oilsAuth retrieved seed from cache: %s", seed );
+
+ // Concatenate them and take an MD5 hash of the result
+ char* maskedPw = md5sum( "%s%s", seed, realPassword );
+
+ free(realPassword);
+ free(seed);
+
+ if( !maskedPw ) {
+ // This happens only if md5sum() runs out of memory
+ free( maskedPw );
+ return -1; // md5sum() ran out of memory
+ }
+
+ osrfLogDebug(OSRF_LOG_MARK, "oilsAuth generated masked password %s. "
+ "Testing against provided password %s", maskedPw, password );
+
+ int ret = 0;
+ if( !strcmp( maskedPw, password ) )
+ ret = 1;
+
+ free(maskedPw);
+
+ char* countkey = va_list_to_string( "%s%s%s", OILS_AUTH_CACHE_PRFX, uname, OILS_AUTH_COUNT_SFFX );
+ jsonObject* countobject = osrfCacheGetObject( countkey );
+ if(countobject) {
+ long failcount = (long) jsonObjectGetNumber( countobject );
+ if(failcount >= _oilsAuthBlockCount) {
+ ret = 0;
+ osrfLogInfo(OSRF_LOG_MARK, "oilsAuth found too many recent failures for '%s' : %i, forcing failure state.", uname, failcount);
+ }
+ if(ret == 0) {
+ failcount += 1;
+ }
+ jsonObjectSetNumber( countobject, failcount );
+ osrfCachePutObject( countkey, countobject, _oilsAuthBlockTimeout );
+ jsonObjectFree(countobject);
+ }
+ free(countkey);
+
+ return ret;
+}
+
+/**
+ @brief Determine the login timeout.
+ @param userObj Pointer to an object describing the user.
+ @param orgloc Org unit to use for settings lookups (negative or zero means unspecified)
+ @return The length of the timeout, in seconds.
+
+ The default timeout value comes from the configuration file.
+
+ The default may be overridden by a corresponding org unit setting. The @a orgloc
+ parameter says what org unit to use for the lookup. If @a orgloc <= 0, or if the
+ lookup for @a orgloc yields no result, we look up the setting for the user's home org unit
+ instead (except that if it's the same as @a orgloc we don't bother repeating the lookup).
+
+ Whether defined in the config file or in an org unit setting, a timeout value may be
+ expressed as a raw number (i.e. all digits, possibly with leading and/or trailing white
+ space) or as an interval string to be translated into seconds by PostgreSQL.
+*/
+static long oilsAuthGetTimeout( const jsonObject* userObj) {
+
+ if(!_oilsAuthTimeout) { /* Load the default timeouts */
+
+ jsonObject* value_obj;
+
+ value_obj = osrf_settings_host_value_object(
+ "/apps/sharestuff.auth/app_settings/default_timeout" );
+ _oilsAuthTimeout = oilsUtilsIntervalToSeconds( jsonObjectGetString( value_obj ));
+ jsonObjectFree(value_obj);
+ if( -1 == _oilsAuthTimeout ) {
+ osrfLogWarning( OSRF_LOG_MARK, "Invalid default timeout for logins" );
+ _oilsAuthTimeout = 0;
+ }
+
+ osrfLogInfo(OSRF_LOG_MARK, "Set default auth timeout: %ld", _oilsAuthTimeout );
+ }
+
+ int orgloc = (int) jsonObjectGetNumber( oilsFMGetObject( userObj, "home_ou" ));
+
+ // Get the org unit setting, if there is one.
+ char* timeout = oilsUtilsFetchOrgSetting( orgloc, OILS_ORG_SETTING_TIMEOUT );
+
+ if(!timeout)
+ return _oilsAuthTimeout; // No override from org unit setting
+
+ // Translate the org unit setting to a number
+ long t;
+ if( !*timeout ) {
+ osrfLogWarning( OSRF_LOG_MARK, "Timeout org unit setting is an empty string; using default" );
+ t = _oilsAuthTimeout;
+ } else {
+ // Treat timeout string as an interval, and convert it to seconds
+ t = oilsUtilsIntervalToSeconds( timeout );
+ if( -1 == t ) {
+ // Unable to convert; possibly an invalid interval string
+ osrfLogError( OSRF_LOG_MARK,
+ "Unable to convert timeout interval \"%s\"; using default",
+ timeout );
+ t = default_timeout;
+ }
+ }
+
+ free(timeout);
+ return t;
+}
+
+/*
+ Adds the authentication token to the user cache.
+ Returns the event that should be returned to the user.
+ Event must be freed
+*/
+static oilsEvent* oilsAuthHandleLoginOK( jsonObject* userObj, const char* uname ) {
+
+ oilsEvent* response;
+
+ long timeout;
+ char* wsorg = jsonObjectToSimpleString(oilsFMGetObject(userObj, "home_ou"));
+ osrfLogDebug( OSRF_LOG_MARK,
+ "Auth session trying workstation id %d for auth timeout", atoi(wsorg));
+ timeout = oilsAuthGetTimeout( userObj );
+ free(wsorg);
+
+ osrfLogDebug(OSRF_LOG_MARK, "Auth session timeout for %s: %ld", uname, timeout );
+
+ char* string = va_list_to_string(
+ "%d.%ld.%s", (long) getpid(), time(NULL), uname );
+ char* authToken = md5sum(string);
+ char* authKey = va_list_to_string(
+ "%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+
+ osrfLogActivity(OSRF_LOG_MARK,
+ "successful login: username=%s, authtoken=%s", uname, authToken );
+
+ oilsFMSetString( userObj, "passwd", "" );
+ jsonObject* cacheObj = jsonParseFmt( "{\"authtime\": %ld}", timeout );
+ jsonObjectSetKey( cacheObj, "userobj", jsonObjectClone(userObj));
+
+ osrfCachePutObject( authKey, cacheObj, (time_t) timeout );
+ jsonObjectFree(cacheObj);
+ osrfLogInternal(OSRF_LOG_MARK, "oilsAuthHandleLoginOK(): Placed user object into cache");
+ jsonObject* payload = jsonParseFmt(
+ "{ \"authtoken\": \"%s\", \"authtime\": %ld }", authToken, timeout );
+
+ response = oilsNewEvent2( OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload );
+ free(string); free(authToken); free(authKey);
+ jsonObjectFree(payload);
+
+ return response;
+}
+
+
+/**
+ @brief Implement the "complete" method.
+ @param ctx The method context.
+ @return -1 upon error; zero if successful, and if a STATUS message has been sent to the
+ client to indicate completion; a positive integer if successful but no such STATUS
+ message has been sent.
+
+ Method parameters:
+ - a hash with some combination of the following elements:
+ - "username"
+ - "password" (hashed with the cached seed; not plaintext)
+
+ Both username and password are required.
+
+ Return to client: Intermediate authentication seed.
+
+ Validate the password using the username.
+
+ Upon deciding whether to allow the logon, return a corresponding event to the client.
+*/
+int oilsAuthComplete( osrfMethodContext* ctx ) {
+ OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+ const jsonObject* args = jsonObjectGetIndex(ctx->params, 0);
+
+ const char* uname = jsonObjectGetString(jsonObjectGetKeyConst(args, "username"));
+ const char* password = jsonObjectGetString(jsonObjectGetKeyConst(args, "password"));
+
+ /* Use __FILE__, harmless_line_number for creating
+ * OILS_EVENT_AUTH_FAILED events (instead of OSRF_LOG_MARK) to avoid
+ * giving away information about why an authentication attempt failed.
+ */
+ int harmless_line_number = __LINE__;
+
+ if( !(uname && password) ) {
+ return osrfAppRequestRespondException( ctx->session, ctx->request,
+ "username and password required for method: %s", ctx->method->name );
+ }
+
+ oilsEvent* response = NULL;
+ jsonObject* userObj = NULL;
+
+ // Fetch a row from the actor.usr table by username.
+ userObj = oilsUtilsFetchUserByUsername( uname );
+ if( userObj && JSON_NULL == userObj->type ) {
+ jsonObjectFree( userObj );
+ userObj = NULL; // username not found
+ }
+
+ int barred = 0, deleted = 0;
+ char *barred_str, *deleted_str;
+
+ if(userObj) {
+ barred_str = oilsFMGetString( userObj, "barred" );
+ barred = oilsUtilsIsDBTrue( barred_str );
+ free( barred_str );
+
+ deleted_str = oilsFMGetString( userObj, "deleted" );
+ deleted = oilsUtilsIsDBTrue( deleted_str );
+ free( deleted_str );
+ }
+
+ if(!userObj || barred || deleted) {
+ response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+ osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s", uname );
+ osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+ oilsEventFree(response);
+ return 0; // No such user
+ }
+
+ // Such a user exists and isn't barred or deleted.
+ // Now see if he or she has the right credentials.
+ int passOK = -1;
+ passOK = oilsAuthVerifyPassword( ctx, userObj, uname, password );
+
+ if( passOK < 0 ) {
+ jsonObjectFree(userObj);
+ return passOK;
+ }
+
+ // See if the account is active
+ char* active = oilsFMGetString(userObj, "active");
+ if( !oilsUtilsIsDBTrue(active) ) {
+ if( passOK )
+ response = oilsNewEvent( OSRF_LOG_MARK, "PATRON_INACTIVE" );
+ else
+ response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+
+ osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+ oilsEventFree(response);
+ jsonObjectFree(userObj);
+ free(active);
+ return 0;
+ }
+ free(active);
+
+ // See if the user is even allowed to log in
+ if( oilsAuthCheckLoginPerm( ctx, userObj, type ) == -1 ) {
+ jsonObjectFree(userObj);
+ return 0;
+ }
+
+ if( passOK ) { // login successful
+
+ if (0 == strcmp(ctx->method->name, "open-ils.auth.authenticate.verify")) {
+ response = oilsNewEvent( OSRF_LOG_MARK, OILS_EVENT_SUCCESS );
+ } else {
+ response = oilsAuthHandleLoginOK( userObj, uname );
+ }
+
+ } else {
+ response = oilsNewEvent( __FILE__, harmless_line_number, OILS_EVENT_AUTH_FAILED );
+ osrfLogInfo(OSRF_LOG_MARK, "failed login: username=%s", uname );
+ }
+
+ jsonObjectFree(userObj);
+ osrfAppRespondComplete( ctx, oilsEventToJSON(response) );
+ oilsEventFree(response);
+
+ return 0;
+}
+
+
+
+int oilsAuthSessionDelete( osrfMethodContext* ctx ) {
+ OSRF_METHOD_VERIFY_CONTEXT(ctx);
+
+ const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0) );
+ jsonObject* resp = NULL;
+
+ if( authToken ) {
+ osrfLogDebug(OSRF_LOG_MARK, "Removing auth session: %s", authToken );
+ char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken ); /**/
+ osrfCacheRemove(key);
+ resp = jsonNewObject(authToken); /**/
+ free(key);
+ }
+
+ osrfAppRespondComplete( ctx, resp );
+ jsonObjectFree(resp);
+ return 0;
+}
+
+/**
+ * Fetches the user object from the database and updates the user object in
+ * the cache object, which then has to be re-inserted into the cache.
+ * User object is retrieved inside a transaction to avoid replication issues.
+ */
+static int _oilsAuthReloadUser(jsonObject* cacheObj) {
+ int reqid, userId;
+ osrfAppSession* session;
+ osrfMessage* omsg;
+ jsonObject *param, *userObj, *newUserObj;
+
+ userObj = jsonObjectGetKey( cacheObj, "userobj" );
+ userId = oilsFMGetObjectId( userObj );
+
+ session = osrfAppSessionClientInit( "open-ils.cstore" );
+ osrfAppSessionConnect(session);
+
+ reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.begin", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+
+ if(omsg) {
+
+ osrfMessageFree(omsg);
+ param = jsonNewNumberObject(userId);
+ reqid = osrfAppSessionSendRequest(session, param, "open-ils.cstore.direct.actor.user.retrieve", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+ jsonObjectFree(param);
+
+ if(omsg) {
+ newUserObj = jsonObjectClone( osrfMessageGetResult(omsg) );
+ osrfMessageFree(omsg);
+ reqid = osrfAppSessionSendRequest(session, NULL, "open-ils.cstore.transaction.rollback", 1);
+ omsg = osrfAppSessionRequestRecv(session, reqid, 60);
+ osrfMessageFree(omsg);
+ }
+ }
+
+ osrfAppSessionFree(session); // calls disconnect internally
+
+ if(newUserObj)
+ return 1;
+
+ osrfLogError(OSRF_LOG_MARK, "Error retrieving user %d from database", userId);
+ return 0;
+}
+
+/**
+ Resets the auth login timeout
+ @return The event object, OILS_EVENT_SUCCESS, or OILS_EVENT_NO_SESSION
+*/
+static oilsEvent* _oilsAuthResetTimeout( const char* authToken, int reloadUser ) {
+ if(!authToken) return NULL;
+
+ oilsEvent* evt = NULL;
+ time_t timeout;
+
+ osrfLogDebug(OSRF_LOG_MARK, "Resetting auth timeout for session %s", authToken);
+ char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+ jsonObject* cacheObj = osrfCacheGetObject( key );
+
+ osrfLogInfo(OSRF_LOG_MARK, "No user in the cache exists with key %s", key);
+ evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+
+ if(reloadUser) {
+ _oilsAuthReloadUser(cacheObj);
+ }
+
+ // Determine a new timeout value
+ jsonObject* endtime_obj = jsonObjectGetKey( cacheObj, "endtime" );
+ if( endtime_obj ) {
+ // Extend the current endtime by a fixed amount
+ time_t endtime = (time_t) jsonObjectGetNumber( endtime_obj );
+ int reset_interval = DEFAULT_RESET_INTERVAL;
+ const jsonObject* reset_interval_obj = jsonObjectGetKeyConst(
+ cacheObj, "reset_interval" );
+ if( reset_interval_obj ) {
+ reset_interval = (int) jsonObjectGetNumber( reset_interval_obj );
+ if( reset_interval <= 0 )
+ reset_interval = DEFAULT_RESET_INTERVAL;
+ }
+
+ time_t now = time( NULL );
+ time_t new_endtime = now + reset_interval;
+ if( new_endtime > endtime ) {
+ // Keep the session alive a little longer
+ jsonObjectSetNumber( endtime_obj, (double) new_endtime );
+ timeout = reset_interval;
+ osrfCachePutObject( key, cacheObj, timeout );
+ } else {
+ // The session isn't close to expiring, so don't reset anything.
+ // Just report the time remaining.
+ timeout = endtime - now;
+ }
+ } else {
+ // Reapply the existing timeout from the current time
+ timeout = (time_t) jsonObjectGetNumber( jsonObjectGetKeyConst( cacheObj, "authtime"));
+ osrfCachePutObject( key, cacheObj, timeout );
+ }
+
+ jsonObject* payload = jsonNewNumberObject( (double) timeout );
+ evt = oilsNewEvent2(OSRF_LOG_MARK, OILS_EVENT_SUCCESS, payload);
+ jsonObjectFree(payload);
+ jsonObjectFree(cacheObj);
+
+ free(key);
+ return evt;
+}
+
+int oilsAuthResetTimeout( osrfMethodContext* ctx ) {
+ OSRF_METHOD_VERIFY_CONTEXT(ctx);
+ const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+ double reloadUser = jsonObjectGetNumber( jsonObjectGetIndex(ctx->params, 1));
+ oilsEvent* evt = _oilsAuthResetTimeout(authToken, (int) reloadUser);
+ osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+ oilsEventFree(evt);
+ return 0;
+}
+
+
+int oilsAuthSessionRetrieve( osrfMethodContext* ctx ) {
+ OSRF_METHOD_VERIFY_CONTEXT(ctx);
+ bool returnFull = false;
+
+ const char* authToken = jsonObjectGetString( jsonObjectGetIndex(ctx->params, 0));
+
+ if(ctx->params->size > 1) {
+ // caller wants full cached object, with authtime, etc.
+ const char* rt = jsonObjectGetString(jsonObjectGetIndex(ctx->params, 1));
+ if(rt && strcmp(rt, "0") != 0)
+ returnFull = true;
+ }
+
+ jsonObject* cacheObj = NULL;
+ oilsEvent* evt = NULL;
+
+ if( authToken ){
+
+ // Reset the timeout to keep the session alive
+ evt = _oilsAuthResetTimeout(authToken, 0);
+
+ if( evt && strcmp(evt->event, OILS_EVENT_SUCCESS) ) {
+ osrfAppRespondComplete( ctx, oilsEventToJSON( evt )); // can't reset timeout
+
+ } else {
+
+ // Retrieve the cached session object
+ osrfLogDebug(OSRF_LOG_MARK, "Retrieving auth session: %s", authToken);
+ char* key = va_list_to_string("%s%s", OILS_AUTH_CACHE_PRFX, authToken );
+ cacheObj = osrfCacheGetObject( key );
+ if(cacheObj) {
+ // Return a copy of the cached user object
+ if(returnFull)
+ osrfAppRespondComplete( ctx, cacheObj);
+ else
+ osrfAppRespondComplete( ctx, jsonObjectGetKeyConst( cacheObj, "userobj"));
+ jsonObjectFree(cacheObj);
+ } else {
+ // Auth token is invalid or expired
+ oilsEvent* evt2 = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+ osrfAppRespondComplete( ctx, oilsEventToJSON(evt2) ); /* should be event.. */
+ oilsEventFree(evt2);
+ }
+ free(key);
+ }
+
+ } else {
+
+ // No session
+ evt = oilsNewEvent(OSRF_LOG_MARK, OILS_EVENT_NO_SESSION);
+ osrfAppRespondComplete( ctx, oilsEventToJSON(evt) );
+ }
+
+ if(evt)
+ oilsEventFree(evt);
+
+ return 0;
+}
--- /dev/null
+/*
+Copyright (C) 2009 Georgia Public Library Service
+Scott McKellar <scott@esilibrary.com>
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ Description : Translates a JSON query into SQL and writes the
+ results to standard output. Synopsis:
+
+ test_json_query [-i IDL_file] [-f file_name] [-v] query
+
+ -i supplies the name of the IDL file. If no IDL file is specified,
+ json_test_query uses the value of the environmental variable
+ OILS_IDL_FILENAME, if it is defined, or defaults to
+ "/openils/conf/fm_IDL.xml".
+
+ -f supplies the name of a text file containing the JSON query to
+ be translated. A file name constisting of a single hyphen
+ denotes standard input. If this option is present, all
+ non-option arguments are ignored.
+
+ -v verbose; outputs the name of the IDL file and the text of the
+ JSON query.
+
+ If there is no -f option supplied, json_query translates the
+ first non-option parameter. This parameter is subject to the
+ usual mangling by the shell. In most cases it will be sufficient
+ to enclose it in single quotes, but of course any single quotes
+ embedded within the query will need to be escaped.
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/osrf_json.h"
+#include "opensrf/osrf_application.h"
+#include "opensrf/osrf_app_session.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_sql.h"
+
+#define DISABLE_I18N 2
+#define SELECT_DISTINCT 1
+
+static int obj_is_true( const jsonObject* obj );
+static int test_json_query( const char* json_query );
+static char* load_query( const char* filename );
+
+int main( int argc, char* argv[] ) {
+
+ // Parse command line
+
+ const char* idl_file_name = NULL;
+ const char* query_file_name = NULL;
+ int verbose = 0; // boolean
+
+ int opt;
+ opterr = 0;
+ const char optstring[] = ":f:i:v";
+
+ while( ( opt = getopt( argc, argv, optstring ) ) != -1 ) {
+ switch( opt )
+ {
+ case 'f' : // get file name of query
+ if( query_file_name ) {
+ fprintf( stderr, "Multiple input files not allowed\n" );
+ return EXIT_FAILURE;
+ }
+ else
+ query_file_name = optarg;
+ break;
+ case 'i' : // get name of IDL file
+ if( idl_file_name ) {
+ fprintf( stderr, "Multiple IDL file names not allowed\n" );
+ return EXIT_FAILURE;
+ }
+ else
+ idl_file_name = optarg;
+ break;
+ case 'v' : // Verbose
+ verbose = 1;
+ break;
+ case '?' : // Invalid option
+ fprintf( stderr, "Invalid option '-%c' on command line\n",
+ (char) optopt );
+ return EXIT_FAILURE;
+ default : // Huh?
+ fprintf( stderr, "Internal error: unexpected value '%c'"
+ "for optopt", (char) optopt );
+ return EXIT_FAILURE;
+
+ }
+ }
+
+ // If the command line doesn't specify an IDL file, get it
+ // from an environmental variable, or apply a default
+ if( NULL == idl_file_name ) {
+ idl_file_name = getenv( "OILS_IDL_FILENAME" );
+ if( NULL == idl_file_name )
+ idl_file_name = "/openils/conf/fm_IDL.xml";
+ }
+
+ if( verbose )
+ printf( "IDL file: %s\n", idl_file_name );
+
+ char* loaded_json = NULL;
+ const char* json_query = NULL;
+
+ // Get the JSON query into a string
+ if( query_file_name ) { // Got a file? Load it
+ if( optind < argc )
+ fprintf( stderr, "Extra parameter(s) ignored\n" );
+ loaded_json = load_query( query_file_name );
+ if( !loaded_json )
+ return EXIT_FAILURE;
+ json_query = loaded_json;
+ } else { // No file? Use command line parameter
+ if ( optind == argc ) {
+ fprintf( stderr, "No JSON query specified\n" );
+ return EXIT_FAILURE;
+ } else
+ json_query = argv[ optind ];
+ }
+
+ if( verbose )
+ printf( "JSON query: %s\n", json_query );
+
+ osrfLogSetLevel( OSRF_LOG_WARNING ); // Suppress informational messages
+ (void) oilsIDLInit( idl_file_name ); // Load IDL into memory
+
+ // Load a database driver, connect to it, and install the connection in
+ // the cstore module. We don't actually connect to a database, but we
+ // need the driver to process quoted strings correctly.
+ if( dbi_initialize( NULL ) < 0 ) {
+ printf( "Unable to load database driver\n" );
+ return EXIT_FAILURE;
+ };
+
+ dbi_conn conn = dbi_conn_new( "pgsql" ); // change string if ever necessary
+ if( !conn ) {
+ printf( "Unable to establish dbi connection\n" );
+ dbi_shutdown();
+ return EXIT_FAILURE;
+ }
+
+ oilsSetDBConnection( conn );
+
+ // The foregoing is an inelegant kludge. The true, proper, and uniquely
+ // correct thing to do is to load the system settings and then call
+ // osrfAppInitialize() and osrfAppChildInit(). Maybe we'll actually
+ // do that some day, but this will do for now.
+
+ // Translate the JSON into SQL
+ int rc = test_json_query( json_query );
+
+ dbi_conn_close( conn );
+ dbi_shutdown();
+ if( loaded_json )
+ free( loaded_json );
+
+ return rc ? EXIT_FAILURE : EXIT_SUCCESS;
+}
+
+static int test_json_query( const char* json_query ) {
+
+ jsonObject* hash = jsonParse( json_query );
+ if( !hash ) {
+ fprintf( stderr, "Invalid JSON\n" );
+ return -1;
+ }
+
+ int flags = 0;
+
+ if ( obj_is_true( jsonObjectGetKeyConst( hash, "distinct" )))
+ flags |= SELECT_DISTINCT;
+
+ if ( obj_is_true( jsonObjectGetKeyConst( hash, "no_i18n" )))
+ flags |= DISABLE_I18N;
+
+ char* sql_query = buildQuery( NULL, hash, flags );
+
+ if ( !sql_query ) {
+ fprintf( stderr, "Invalid query\n" );
+ return -1;
+ }
+ else
+ printf( "%s\n", sql_query );
+
+ free( sql_query );
+ jsonObjectFree( hash );
+ return 0;
+}
+
+// Interpret a jsonObject as true or false
+static int obj_is_true( const jsonObject* obj ) {
+ if( !obj )
+ return 0;
+ else switch( obj->type )
+ {
+ case JSON_BOOL :
+ if( obj->value.b )
+ return 1;
+ else
+ return 0;
+ case JSON_STRING :
+ if( strcasecmp( obj->value.s, "true" ) )
+ return 0;
+ else
+ return 1;
+ case JSON_NUMBER : // Support 1/0 for perl's sake
+ if( jsonObjectGetNumber( obj ) == 1.0 )
+ return 1;
+ else
+ return 0;
+ default :
+ return 0;
+ }
+}
+
+static char* load_query( const char* filename ) {
+ FILE* fp;
+
+ // Sanity check
+ if( ! filename || ! *filename ) {
+ fprintf( stderr, "Name of query file is empty or missing\n" );
+ return NULL;
+ }
+
+ // Open query file, or use standard input
+ if( ! strcmp( filename, "-" ) )
+ fp = stdin;
+ else {
+ fp = fopen( filename, "r" );
+ if( !fp ) {
+ fprintf( stderr, "Unable to open query file \"%s\"\n", filename );
+ return NULL;
+ }
+ }
+
+ // Load file into a growing_buffer
+ size_t num_read;
+ char buf[ BUFSIZ + 1 ];
+ growing_buffer* gb = buffer_init( sizeof( buf ) );
+
+ while( ( num_read = fread( buf, 1, sizeof( buf ) - 1, fp ) ) ) {
+ buf[ num_read ] = '\0';
+ buffer_add( gb, buf );
+ }
+
+ if( fp != stdin )
+ fclose( fp );
+
+ return buffer_release( gb );
+}
--- /dev/null
+/**
+ @file test_qstore.c
+ @brief Test driver for routines to build queries from tables in the query schema.
+
+ This command-line utility exercises most of the code used in the qstore server, but
+ without the complications of sending and receiving OSRF messages.
+
+ Synopsis:
+
+ test_qstore [options] query_id
+
+ Query_id is the id of a row in the query.stored_query table, defining a stored query.
+
+ The program reads the specified row in query.stored_query, along with associated rows
+ in other tables, and displays the corresponding query as an SQL command. Optionally it
+ may execute the query, display the column names of the query result, and/or display the
+ bind variables.
+
+ In order to connect to the database, test_qstore uses various connection parameters
+ that may be specified on the command line. Any connection parameter not specified
+ reverts to a plausible default.
+
+ The database password may be read from a specified file or entered from the keyboard.
+
+ Options:
+
+ -b Boolean; Display the name of any bind variables, and their default values.
+
+ -D Specifies the name of the database driver; defaults to "pgsql".
+
+ -c Boolean; display column names of the query results, as assigned by PostgreSQL.
+
+ -d Specifies the database name; defaults to "evergreen".
+
+ -h Specifies the hostname of the database; defaults to "localhost".
+
+ -i Specifies the name of the IDL file; defaults to "/openils/conf/fm_IDL.xml".
+
+ -p Specifies the port number of the database; defaults to 5432.
+
+ -u Specifies the database user name; defaults to "evergreen".
+
+ -v Boolean; Run in verbose mode, spewing various detailed messages. This option is not
+ likely to be useful unless you are troubleshooting the code that loads the stored
+ query.
+
+ -w Specifies the name of a file containing the database password (no default).
+
+ -x Boolean: Execute the query and display the results.
+
+ Copyright (C) 2010 Equinox Software Inc.
+ Scott McKellar <scott@esilibrary.com>
+*/
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <errno.h>
+#include <ctype.h>
+#include <unistd.h>
+#include <termios.h>
+#include <dbi/dbi.h>
+#include "opensrf/utils.h"
+#include "opensrf/string_array.h"
+#include "opensrf/osrf_json.h"
+#include "openils/oils_idl.h"
+#include "openils/oils_buildq.h"
+
+typedef struct {
+ int new_argc;
+ char ** new_argv;
+
+ int bind;
+ char* driver;
+ int driver_found;
+ char* database;
+ int database_found;
+ char* host;
+ int host_found;
+ char* idl;
+ int idl_found;
+ unsigned long port;
+ int port_found;
+ char* user;
+ int user_found;
+ char* password_file;
+ int password_file_found;
+ int verbose;
+ int columns;
+ int execute;
+} Opts;
+
+static void show_bind_variables( osrfHash* vars );
+static void show_msgs( const osrfStringArray* sa );
+static dbi_conn connect_db( Opts* opts );
+static int load_pw( growing_buffer* buf, FILE* in );
+static int prompt_password( growing_buffer* buf );
+static void initialize_opts( Opts * pOpts );
+static int get_Opts( int argc, char * argv[], Opts * pOpts );
+
+int main( int argc, char* argv[] ) {
+
+ // Parse the command line
+ printf( "\n" );
+ Opts opts;
+ if( get_Opts( argc, argv, &opts )) {
+ fprintf( stderr, "Unable to parse command line\n" );
+ return EXIT_FAILURE;
+ }
+
+ // Connect to the database
+ dbi_conn dbhandle = connect_db( &opts );
+ if( NULL == dbhandle )
+ return EXIT_FAILURE;
+
+ if( opts.verbose )
+ oilsStoredQSetVerbose();
+
+ osrfLogSetLevel( OSRF_LOG_WARNING );
+
+ // Load the IDL
+ if ( !oilsIDLInit( opts.idl )) {
+ fprintf( stderr, "Unable to load IDL at %s\n", opts.idl );
+ return EXIT_FAILURE;
+ }
+
+ // Load the stored query
+ BuildSQLState* state = buildSQLStateNew( dbhandle );
+ state->defaults_usable = 1;
+ state->values_required = 0;
+ StoredQ* sq = getStoredQuery( state, atoi( opts.new_argv[ 1 ] ));
+
+ if( !sq ) {
+ show_msgs( state->error_msgs );
+ printf( "Unable to build query\n" );
+ } else {
+ // If so requested, show the bind variables
+ if( opts.bind )
+ show_bind_variables( state->bindvar_list );
+
+ // Build the SQL query
+ if( buildSQL( state, sq )) {
+ show_msgs( state->error_msgs );
+ fprintf( stderr, "Unable to build SQL statement\n" );
+ }
+ else {
+ printf( "%s\n", OSRF_BUFFER_C_STR( state->sql ));
+
+ // If so requested, get the column names and display them
+ if( opts.columns ) {
+ jsonObject* cols = oilsGetColNames( state, sq );
+ if( cols ) {
+ printf( "Column names:\n" );
+ char* cols_str = jsonObjectToJSON( cols );
+ char* cols_out = jsonFormatString( cols_str );
+ printf( "%s\n\n", cols_out );
+ free( cols_out );
+ free( cols_str );
+ jsonObjectFree( cols );
+ } else
+ fprintf( stderr, "Unable to get column names\n\n" );
+ }
+
+ // If so requested, execute the query and display the results
+ if( opts.execute ) {
+ jsonObject* row = oilsFirstRow( state );
+ if( state->error ) {
+ show_msgs( state->error_msgs );
+ fprintf( stderr, "Unable to execute query\n" );
+ } else {
+ printf( "[" );
+ int first = 1; // boolean
+ while( row ) {
+
+ if( first ) {
+ printf( "\n\t" );
+ first = 0;
+ } else
+ printf( ",\n\t" );
+
+ char* json = jsonObjectToJSON( row );
+ printf( "%s", json );
+ free( json );
+ row = oilsNextRow( state );
+ }
+ if( state->error ) {
+ show_msgs( state->error_msgs );
+ fprintf( stderr, "Unable to fetch row\n" );
+ }
+ printf( "\n]\n" );
+ }
+ }
+ }
+ }
+
+ storedQFree( sq );
+ buildSQLStateFree( state );
+
+ buildSQLCleanup();
+ if ( dbhandle )
+ dbi_conn_close( dbhandle );
+
+ return EXIT_SUCCESS;
+}
+
+/**
+ @brief Display the bind variables.
+ @param vars Pointer to a hash keyed on bind variable name.
+
+ The data for each hash entry is a BindVar, a C struct whose members define the
+ attributes of the bind variable.
+*/
+static void show_bind_variables( osrfHash* vars ) {
+ printf( "Bind variables:\n\n" );
+ BindVar* bind = NULL;
+ osrfHashIterator* iter = osrfNewHashIterator( vars );
+
+ // Traverse the hash of bind variables
+ while(( bind = osrfHashIteratorNext( iter ))) {
+ const char* type = NULL;
+ switch( bind->type ) {
+ case BIND_STR :
+ type = "string";
+ break;
+ case BIND_NUM :
+ type = "number";
+ break;
+ case BIND_STR_LIST :
+ type = "string list";
+ break;
+ case BIND_NUM_LIST :
+ type = "number list";
+ break;
+ default :
+ type = "(unrecognized)";
+ break;
+ }
+
+ // The default and actual values are in the form of jsonObjects.
+ // Transform them back into raw JSON.
+ char* default_value = NULL;
+ if( bind->default_value )
+ default_value = jsonObjectToJSONRaw( bind->default_value );
+
+ char* actual_value = NULL;
+ if( bind->actual_value )
+ actual_value = jsonObjectToJSONRaw( bind->actual_value );
+
+ // Display the attributes of the current bind variable.
+ printf( "Name: %s\n", bind->name );
+ printf( "Label: %s\n", bind->label );
+ printf( "Type: %s\n", type );
+ printf( "Desc: %s\n", bind->description ? bind->description : "(none)" );
+ printf( "Default: %s\n", default_value ? default_value : "(none)" );
+ printf( "Actual: %s\n", actual_value ? actual_value : "(none)" );
+ printf( "\n" );
+
+ if( default_value )
+ free( default_value );
+
+ if( actual_value )
+ free( actual_value );
+ } // end while
+
+ osrfHashIteratorFree( iter );
+}
+
+/**
+ @brief Write a series of strings to standard output.
+ @param sa Array of strings.
+
+ Display messages emitted by the query-building machinery.
+*/
+static void show_msgs( const osrfStringArray* sa ) {
+ if( sa ) {
+ int i;
+ for( i = 0; i < sa->size; ++i ) {
+ const char* s = osrfStringArrayGetString( sa, i );
+ if( s )
+ printf( "%s\n", s );
+ }
+ }
+}
+
+/**
+ @brief Connect to the database.
+ @return If successful, a database handle; otherwise NULL;
+*/
+static dbi_conn connect_db( Opts* opts ) {
+ // Get a database handle
+ dbi_initialize( NULL );
+ dbi_conn dbhandle = dbi_conn_new( opts->driver );
+ if( !dbhandle ) {
+ fprintf( stderr, "Error loading database driver [%s]", opts->driver );
+ return NULL;
+ }
+
+ char* pw = NULL;
+ growing_buffer* buf = buffer_init( 32 );
+
+ // Get the database password, either from a designated file
+ // or from the terminal.
+ if( opts->password_file_found ) {
+ FILE* pwfile = fopen( opts->password_file, "r" );
+ if( !pwfile ) {
+ fprintf( stderr, "Unable to open password file %s\n", opts->password_file );
+ buffer_free( buf );
+ return NULL;
+ } else {
+ if( load_pw( buf, pwfile )) {
+ fprintf( stderr, "Unable to load password file %s\n", opts->password_file );
+ buffer_free( buf );
+ return NULL;
+ } else
+ pw = buffer_release( buf );
+ }
+ } else {
+ if( prompt_password( buf )) {
+ fprintf( stderr, "Unable to get password\n" );
+ buffer_free( buf );
+ return NULL;
+ } else
+ pw = buffer_release( buf );
+ }
+
+ // Set database connection options
+ dbi_conn_set_option( dbhandle, "host", opts->host );
+ dbi_conn_set_option_numeric( dbhandle, "port", opts->port );
+ dbi_conn_set_option( dbhandle, "username", opts->user );
+ dbi_conn_set_option( dbhandle, "password", pw );
+ dbi_conn_set_option( dbhandle, "dbname", opts->database );
+
+ // Connect to the database
+ const char* err;
+ if( dbi_conn_connect( dbhandle) < 0 ) {
+ sleep( 1 );
+ if ( dbi_conn_connect( dbhandle ) < 0 ) {
+ dbi_conn_error( dbhandle, &err );
+ fprintf( stderr, "Error connecting to database: %s", err );
+ dbi_conn_close( dbhandle );
+ free( pw );
+ return NULL;
+ }
+ }
+
+ free( pw );
+ return dbhandle;
+}
+
+/**
+ @brief Load one line from an input stream into a growing_buffer.
+ @param buf Pointer to the receiving buffer.
+ @param in Pointer to the input stream.
+ @return 0 in all cases. If there's ever a way to fail, return 1 for failure.
+
+ Intended for use in loading a password.
+*/
+static int load_pw( growing_buffer* buf, FILE* in ) {
+ buffer_reset( buf );
+ while( 1 ) {
+ int c = getc( in );
+ if( '\n' == c || EOF == c )
+ break;
+ else if( '\b' == c )
+ buffer_chomp( buf );
+ else
+ OSRF_BUFFER_ADD_CHAR( buf, c );
+ }
+ return 0;
+}
+
+/**
+ @brief Read a password from the terminal, with echo turned off.
+ @param buf Pointer to the receiving buffer.
+ @return 0 if successful, or 1 if not.
+
+ Read from /dev/tty if possible, or from stdin if not.
+*/
+static int prompt_password( growing_buffer* buf ) {
+ struct termios oldterm;
+
+ printf( "Password: " );
+ fflush( stdout );
+
+ FILE* term = fopen( "//dev//tty", "rw" );
+ if( NULL == term )
+ term = stdin;
+
+ // Capture the current state of the terminal
+ if( tcgetattr( fileno( term ), &oldterm ))
+ return 1;
+
+ // Turn off echo
+ struct termios newterm = oldterm;
+ newterm.c_lflag &= ~ECHO;
+ if( tcsetattr( fileno( term ), TCSAFLUSH, &newterm ))
+ return 1;
+
+ // Read the password
+ int rc = load_pw( buf, term );
+
+ // Turn echo back on
+ (void) tcsetattr( fileno( term ), TCSAFLUSH, &oldterm ); // restore echo
+
+ if( term != stdin )
+ fclose( term );
+
+ return rc;
+}
+
+/**
+ @brief Initialize an Opts structure.
+ @param pOpts Pointer to the Opts to be initialized.
+*/
+static void initialize_opts( Opts * pOpts ) {
+ pOpts->new_argc = 0;
+ pOpts->new_argv = NULL;
+
+ pOpts->bind = 0;
+ pOpts->driver_found = 0;
+ pOpts->driver = NULL;
+ pOpts->database_found = 0;
+ pOpts->database = NULL;
+ pOpts->host_found = 0;
+ pOpts->host = NULL;
+ pOpts->idl_found = 0;
+ pOpts->idl = NULL;
+ pOpts->port_found = 0;
+ pOpts->port = 0;
+ pOpts->user_found = 0;
+ pOpts->user = NULL;
+ pOpts->password_file_found = 0;
+ pOpts->password_file = NULL;
+ pOpts->verbose = 0;
+ pOpts->columns = 0;
+ pOpts->execute = 0;
+}
+
+/**
+ @brief Parse the command line.
+ @param argc argc from the command line.
+ @param argv argv from the command line.
+ @param pOpts Pointer to the Opts to be populated.
+ @return Zero if successful, or 1 if not.
+*/
+static int get_Opts( int argc, char * argv[], Opts * pOpts ) {
+ int rc = 0; /* return code */
+ unsigned long port_value = 0;
+ char * tail = NULL;
+ int opt;
+
+ /* Define valid option characters */
+
+ const char optstring[] = ":bD:cd:h:i:p:u:vw:x";
+
+ /* Initialize members of struct */
+
+ initialize_opts( pOpts );
+
+ /* Suppress error messages from getopt() */
+
+ opterr = 0;
+
+ /* Examine command line options */
+
+ while( ( opt = getopt( argc, argv, optstring )) != -1 )
+ {
+ switch( opt )
+ {
+ case 'b' : /* Display bind variables */
+ pOpts->bind = 1;
+ break;
+ case 'c' : /* Display column names */
+ pOpts->columns = 1;
+ break;
+ case 'D' : /* Get database driver */
+ if( pOpts->driver_found )
+ {
+ fprintf( stderr, "Only one occurrence of -D option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->driver_found = 1;
+
+ pOpts->driver = optarg;
+ break;
+ case 'd' : /* Get database name */
+ if( pOpts->database_found )
+ {
+ fprintf( stderr, "Only one occurrence of -d option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->database_found = 1;
+
+ pOpts->database = optarg;
+ break;
+ case 'h' : /* Get hostname of database */
+ if( pOpts->host_found )
+ {
+ fprintf( stderr, "Only one occurrence of -h option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->host_found = 1;
+
+ pOpts->host = optarg;
+ break;
+ case 'i' : /* Get name of IDL file */
+ if( pOpts->idl_found )
+ {
+ fprintf( stderr, "Only one occurrence of -i option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->idl_found = 1;
+
+ pOpts->idl = optarg;
+ break;
+ case 'p' : /* Get port number of database */
+ if( pOpts->port_found )
+ {
+ fprintf( stderr, "Only one occurrence of -p option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->port_found = 1;
+
+ /* Skip white space; check for negative */
+
+ while( isspace( (unsigned char) *optarg ))
+ ++optarg;
+
+ if( '-' == *optarg )
+ {
+ fprintf( stderr, "Negative argument not allowed for "
+ "-p option: \"%s\"\n", optarg );
+ rc = 1;
+ break;
+ }
+
+ /* Convert to numeric value */
+
+ errno = 0;
+ port_value = strtoul( optarg, &tail, 10 );
+ if( *tail != '\0' )
+ {
+ fprintf( stderr, "Invalid or non-numeric argument "
+ "to -p option: \"%s\"\n", optarg );
+ rc = 1;
+ break;
+ }
+ else if( errno != 0 )
+ {
+ fprintf( stderr, "Too large argument "
+ "to -p option: \"%s\"\n", optarg );
+ rc = 1;
+ break;
+ }
+
+ pOpts->port = port_value;
+ break;
+ case 'u' : /* Get username of database account */
+ if( pOpts->user_found )
+ {
+ fprintf( stderr, "Only one occurrence of -u option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->user_found = 1;
+
+ pOpts->user = optarg;
+ break;
+ case 'v' : /* Set verbose mode */
+ pOpts->verbose = 1;
+ break;
+ case 'w' : /* Get name of password_file */
+ if( pOpts->password_file_found )
+ {
+ fprintf( stderr, "Only one occurrence of -w option allowed\n" );
+ rc = 1;
+ break;
+ }
+ pOpts->password_file_found = 1;
+
+ pOpts->password_file = optarg;
+ break;
+ case 'x' : /* Set execute */
+ pOpts->execute = 1;
+ break;
+ case ':' : /* Missing argument */
+ fprintf( stderr, "Required argument missing on -%c option\n",
+ (char) optopt );
+ rc = 1;
+ break;
+ case '?' : /* Invalid option */
+ fprintf( stderr, "Invalid option '-%c' on command line\n",
+ (char) optopt );
+ rc = 1;
+ break;
+ default : /* Programmer error */
+ fprintf( stderr, "Internal error: unexpected value '-%c'"
+ "for optopt", (char) optopt );
+ rc = 1;
+ break;
+ } /* end switch */
+ } /* end while */
+
+ /* See if required options were supplied; apply defaults */
+
+ if( ! pOpts->driver_found )
+ pOpts->driver = "pgsql";
+
+ if( ! pOpts->database_found )
+ pOpts->database = "evergreen";
+
+ if( ! pOpts->host_found )
+ pOpts->host = "localhost";
+
+ if( ! pOpts->idl_found )
+ pOpts->idl = "/openils/conf/fm_IDL.xml";
+
+ if( ! pOpts->port_found )
+ pOpts->port = 5432;
+
+ if( ! pOpts->user_found )
+ pOpts->user = "evergreen";
+
+ if( optind > argc )
+ {
+ /* This should never happen! */
+
+ fprintf( stderr, "Program error: found more arguments than expected\n" );
+ rc = 1;
+ }
+ else
+ {
+ /* Calculate new_argcv and new_argc to reflect */
+ /* the number of arguments consumed */
+
+ pOpts->new_argc = argc - optind + 1;
+ pOpts->new_argv = argv + optind - 1;
+
+ if( pOpts->new_argc < 2UL )
+ {
+ fprintf( stderr, "Not enough arguments beyond options; must be at least 1\n" );
+ rc = 1;
+ }
+ }
+
+ return rc;
+}