/* qi_query - module for query command */
/* Bruce Tanner - Cerritos College */


/* 1993/09/14 - [email protected] fixed misextracted ATTR byte in display_id()
                                   that caused sequence numbers to spill over
                                   into ATTR                 */

#include stdio
#include string
#include ctype
#include ssdef
#include descrip
#include rms
#include psldef
#include "qi.h"

struct FAB idxfab, datfab;
struct RAB idxrab, datrab;
struct XABKEY idxxab, datxab;
char idx_record[IDX_RECORD_SIZE + 1];
char idx_key[IDX_KEY_SIZE + 1];
char dat_record[DAT_RECORD_SIZE + 1];
char dat_key[DAT_KEY_SIZE + 1];
int db_status;

typedef struct rstruct {
   int id, field;
} Result;

#define NotRet(x) ((x->type & TYPE_RETURN) == 0)

void writestring(int, char *);
void swrite(int, char *, ...);
char *new_string(char *);
Arg *make_arg(char *, int, char *, int);
void free_args(Arg *);
void qilog(int, char *, ...);
void *my_realloc(void *, int);
char *getlogical(char *);

extern Fields fields[];
extern int mode;


char *get_value(int sock, char *key, char *in_field, char *out_field)
{
   char *cp, temp[KEYWORD_SIZE + 1];
   int status;

   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$l_rop = 0;            /* set up exact match */
   idxrab.rab$b_ksz = KEYWORD_SIZE + FIELD_SIZE;
   if (cp = strchr(key, ' '))
       *cp = '\0';                  /* strip trailing spaces */
   strcpy(temp, key);
   for (cp = temp; *cp; cp++)
       *cp = _tolower(*cp);         /* force lowercase */
   sprintf(idx_key, "%-*s%s", KEYWORD_SIZE, temp, in_field);

   status = sys$get(&idxrab);
   if ((status & 1) == 0) {
       if (DEBUG)
           swrite(sock, "get_value doesn't find key %s\r\n", idx_key);
       return NULL;
   }
   if (DEBUG)
       swrite(sock, "get_value finds key %s\r\n", idx_record);

   sprintf(dat_key, "%s%s%0*d", idx_record + KEYWORD_SIZE + FIELD_SIZE,
           out_field, SEQ_SIZE, 0);
   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$l_rop = 0;
   idxrab.rab$b_ksz = strlen(dat_key);
   if (DEBUG)
       swrite(sock, "get_value lookup %s\r\n", dat_key);

   status = sys$get(&datrab);
   if ((status & 1) == 0) {
       if (DEBUG)
           swrite(sock, "get_value doesn't find field %s, status %d\r\n",
                  out_field, status);
       return NULL;
   }
   dat_record[datrab.rab$w_rsz] = '\0';  /* terminate string */
   if (DEBUG)
       if (strcmp(out_field, PASSWORD_FIELD))
           swrite(sock, "get_value finds field %s value %s\r\n",
                  out_field, dat_record);
       else
           swrite(sock, "get_value finds password but I'm not going to show it to you.\r\n");
   return (dat_record + ID_SIZE + FIELD_SIZE + SEQ_SIZE + ATTR_SIZE);
}


int find_exact(int sock, char *query, int field)
{
   char *cp;
   int status;

   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$l_rop = 0;            /* set up exact match */
   idxrab.rab$b_ksz = KEYWORD_SIZE + FIELD_SIZE;
   if (cp = strchr(query, ' '))
       *cp = '\0';                  /* strip trailing spaces */
   if (field == atoi(SOUNDEX_FIELD))         /* soundex field means convert */
       soundex(query, query, SOUNDEX_SIZE);  /* and store soundex in query */

   sprintf(idx_key, "%-*s%0*d", KEYWORD_SIZE, query,
           FIELD_SIZE, field);

   status = sys$get(&idxrab);
   if (DEBUG)
       swrite(sock, "exact match returns %d\r\n", status);
   return (status & 1);
}


/* find index that starts with the query string */
int find_approx(int sock, char *query, int field)
{
   int status;

   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$l_rop = RAB$M_KGE;     /* set up approximate generic match */
   idxrab.rab$b_ksz = strlen(query); /* actual key size */
   sprintf(idx_key, "%-*s", KEYWORD_SIZE, query);

   status = sys$get(&idxrab);
   if ((status & 1) == 0) {
       if (DEBUG)
           swrite(sock,"approx match returns %d\r\n", status);
       return status;
   }
   /* this sould always find something, is it the right record? */
   status = (strncmp(idx_record, query, strlen(query)) == 0);
   if (DEBUG)
       swrite(sock, "approx match finds %s and returns %d\r\n",
               idx_record, status);
   return status;
}


/* find soundex match */
int find_soundex(int sock, char *query, int *field)
{
   int status;

   /* ensure only name field is implicitely searched for soundex */
   if (*field != atoi(NAME_FIELD)) {
       if (DEBUG)
           swrite(sock, "soundex match rejects field %d\r\n", *field);
       return;
   }

   *field = atoi(SOUNDEX_FIELD);
   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$l_rop = 0;            /* set up exact match */
   idxrab.rab$b_ksz = KEYWORD_SIZE + FIELD_SIZE;
   sprintf(idx_key, "%-*s%s", KEYWORD_SIZE,
           soundex(query, query, SOUNDEX_SIZE),
           SOUNDEX_FIELD);

   status = sys$get(&idxrab);
   if (DEBUG)
       swrite(sock, "soundex match returns %d\r\n", status);
   return (status & 1);
}


int find(int sock, Arg *arg, Result **result, int size)
{
   int status, id, field;
   char *cp, query[DATA_SIZE + 1], wild_query[DATA_SIZE + 1];
   char found_keyword[KEYWORD_SIZE + 1], found_id[ID_SIZE + 1];
   char found_field[FIELD_SIZE + 1];
   Result *rptr, *pptr;
   $DESCRIPTOR(wild_dsc, wild_query);
   $DESCRIPTOR(found_dsc, found_keyword);

   /* find() only works on indexed fields */
   if ((fields[arg->field].attrib & ATTR_INDEXED) == 0)
       return size;

   if (DEBUG)
       swrite(sock, "Find %s in field %d\r\n", arg->value, arg->field);

   strcpy(query, arg->value);
   wild_query[0] = '\0';
   field = arg->field;

   for (cp = query; *cp; cp++)              /* convert all '?' to '%' */
       if (*cp == '?')                      /* STR$WILDCARD uses '%' */
           *cp = '%';
   if ((cp = strchr(query, '*')) || (cp = strchr(query, '%'))) {  /* wildcard? */
       strcpy(wild_query, query);           /* make a copy with the wildcard */
       *cp = '\0';                          /* truncate at the wildcard */
       if (!find_approx(sock, query, field)) /* try to find the first part */
           return size;                     /* no match */
   }
   else
       if (!find_exact(sock, query, field))     /* no wildcard, find the item */
           if (!EXACT && !find_approx(sock, query, field)) /* no exact match, try approx match */
               if (!find_soundex(sock, query, &field)) /* no approx, try soundex */
                   return size;             /* no match */


   idxrab.rab$b_rac = RAB$C_SEQ;
   do {
       strncpy(found_keyword, idx_record, KEYWORD_SIZE);
       found_keyword[KEYWORD_SIZE] = '\0';
       if (cp = strchr(found_keyword, ' ')) *cp = '\0';
       if ((idxrab.rab$l_rop == 0) &&   /* if exact match, */
           strcmp(found_keyword, query)) /* do exact compare */
           break;  /* no match */
       if ((idxrab.rab$l_rop == RAB$M_KGE) &&   /* if approx match, */
           strncmp(found_keyword, query, strlen(query))) /* approx compare */
           break;  /* no match */
       if (strlen(wild_query)) {   /* if wildcard match, */
           found_dsc.dsc$w_length = (short) strlen(found_keyword);
           wild_dsc.dsc$w_length = (short) strlen(wild_query);
           if ((str$match_wild(&found_dsc, &wild_dsc) & 1) == 0)  /* wild compare */
               continue;  /* no match, try again */
       }
       sprintf(found_field, "%0*d", FIELD_SIZE, field);
       if (strncmp(found_field, idx_record + KEYWORD_SIZE, FIELD_SIZE) != 0)
           continue;  /* this isn't the field we're looking for */
       strncpy(found_id, idx_record + KEYWORD_SIZE + FIELD_SIZE, ID_SIZE);
       found_id[ID_SIZE] = '\0';
       if (DEBUG)
           swrite(sock, "-100: Find >> %s\r\n", idx_record);
       *result = (Result *) my_realloc((Result *) *result, (size + 1) * sizeof(Result));
       id = atoi(found_id);
       (*result)[size].id = id;
       (*result)[size].field = atoi(found_field);
       size++;
   } while (((status = sys$get(&idxrab)) & 1) == SS$_NORMAL);

   return size;
}


/* this routine handles the rare case where a name= query
  matches both name and nickname (inflating the match count)
  and another indexed query has no matches;
  thus a false matched == iq in resolve()
*/
int validate_match(Result *results, int end, int matched, Arg *list)
{
   Arg *ptr;
   int ind;

   /* look for an indexed query without a corresponding result */
   for (ptr = list; ptr && NotRet(ptr); ptr = ptr->next) {         /* all queries */
       if ((fields[ptr->field].attrib & ATTR_INDEXED) &&  /* this is indexed */
           (ptr->field != atoi(NAME_FIELD)) &&
           (ptr->field != atoi(NICKNAME_FIELD))) {   /* that isn't name= */
           for (ind = end - matched; ind < end; ind++)
               if (results[ind].field == ptr->field)
                   break;       /* a result matched this query */
           if (ind == end)      /* didn't find a match */
               return False;    /* for this query */
       }
   }
   return True;
}


/* see if a query field matches the id */
int lookup(int sock, int id, Arg *arg)
{
   int status;
   char *cp, value[DATA_SIZE + 1], data[DATA_SIZE + 1];
   $DESCRIPTOR(value_dsc, value);
   $DESCRIPTOR(data_dsc, data);

   if (DEBUG)
       swrite(sock, "lookup %d\r\n", id);

   /* first, read the data record */
   datrab.rab$b_rac = RAB$C_KEY;
   datrab.rab$b_ksz = ID_SIZE + FIELD_SIZE;  /* partial key size */
   datrab.rab$l_rop = RAB$M_KGE;             /* find any sequence number */
   sprintf(dat_key, "%0*d%0*d%*s", ID_SIZE, id,
           FIELD_SIZE, arg->field, SEQ_SIZE, "");
   while ((status = sys$get(&datrab)) & 1) {
       datrab.rab$b_rac = RAB$C_SEQ;
       if (strncmp(dat_key, dat_record, ID_SIZE + FIELD_SIZE))
           break;                            /* finished with this id/field */
       dat_record[datrab.rab$w_rsz] = '\0';  /* terminate string */
       strcpy(data, dat_record + ID_SIZE + FIELD_SIZE + SEQ_SIZE + ATTR_SIZE);
       data_dsc.dsc$w_length = (short) strlen(data);
       if (DEBUG)
           swrite(sock, "-100: Lookup >> %s\r\n", dat_record);
       /* force case for compare; there's no case-blind flag to match_wild */
       for (cp = data; *cp; cp++) *cp = _tolower(*cp);
       sprintf(value, "*%s*", arg->value);
       value_dsc.dsc$w_length = (short) strlen(value);
       if (str$match_wild(&data_dsc, &value_dsc) & 1)
           return True;  /* one match is good enough */
   }
   return False;
}


/* find the non-indexed query fields in the data file
  and see if id will match them
*/
int find_non_indexed(int sock, int id, Arg *list)
{
   Arg *ptr;

   for (ptr = list; ptr && NotRet(ptr); ptr = ptr->next) {
       if (fields[ptr->field].attrib & ATTR_INDEXED)
           continue;                   /* already did indexed fields */
       if (!lookup(sock, id, ptr))
           return False;               /* no match, you loose */
   }
   return True;                        /* all fields matched */
}


int compar(Result *a, Result *b)
{
   if (a->id < b->id) return (-1);
   if (a->id > b->id) return (1);
   if (a->field < b->field) return (-1);
   if (a->field > b->field) return (1);
   return (0);
}


/* display the requested fields associated with id */
void display_id(int sock, int id, Arg *list, int match)
{
   Arg *ptr;
   int request[MAX_FIELD], use_defaults = True;
   int max, ind, status, num, seq, attrib;
   char data[DATA_SIZE + 1], field[20];

/* first we need to know whether there are any requested fields */

   for (ind = 0; ind < MAX_FIELD; ind++)
       request[ind] = False;
   for (ptr = list; ptr; ptr = ptr->next) {
       if (ptr->type & TYPE_RETURN)
           use_defaults = False;
       else if (strcmp(ptr->value, "all") == 0)
           for (ind = 0; ind < MAX_FIELD; ind++)
               request[ind] = True;    /* mark all as requested */
       else if (!use_defaults)
           request[ptr->field] = True;
   }

/* if use_defaults = true, the Default fields will be used
  otherwise, request[] contains the fields requested,
*/

   /* find the max field name size */
   for (ind = 0, max = 0; ind < MAX_FIELD; ind++)
       if (fields[ind].name && (strlen(fields[ind].name) > max))
           max = strlen(fields[ind].name);

   datrab.rab$b_rac = RAB$C_KEY;
   datrab.rab$b_ksz = ID_SIZE;               /* partial key size */
   datrab.rab$l_rop = RAB$M_KGE;             /* find all records */
   sprintf(dat_key, "%0*d%0*d%*s", ID_SIZE, id,
           FIELD_SIZE, 0, SEQ_SIZE, "");
   while ((status = sys$get(&datrab)) & 1) {
       datrab.rab$b_rac = RAB$C_SEQ;
       if (strncmp(dat_key, dat_record, ID_SIZE))
           break;                            /* finished with this id */
       dat_record[datrab.rab$w_rsz] = '\0';  /* terminate string */
       strncpy(field, dat_record + ID_SIZE, FIELD_SIZE);
       field[FIELD_SIZE] = '\0';
       num = atoi(field);
       strncpy(field, dat_record + ID_SIZE + FIELD_SIZE, SEQ_SIZE);
       field[SEQ_SIZE] = '\0';
       seq = atoi(field);
       strncpy(field, dat_record + ID_SIZE + FIELD_SIZE + SEQ_SIZE, ATTR_SIZE);
       field[ATTR_SIZE] = '\0';
       attrib = atoi(field);
       strcpy(data, dat_record + ID_SIZE + FIELD_SIZE + SEQ_SIZE + ATTR_SIZE);

       /* if a public field is requested OR no fields were requested and the
          field is a default AND the field isn't suppressed OR the field may
          not be suppressed (ForcePub) then print the record
       */
       if (((request[num] && (fields[num].attrib & ATTR_PUBLIC)) ||
            (use_defaults && (fields[num].attrib & ATTR_DEFAULT))) &&  /* Default implies Public */
           (((attrib & ATTR_SUPPRESS) == 0) || (fields[num].attrib & ATTR_FORCEPUB))) {
           swrite(sock, "-200:%d:%*s: %s\r\n", match, max,
               seq ? "" : fields[num].name, data);
       }
   }
}



/*
  given an array of indexed fields that match indexed part of query,
  return the count of actual matches with the list in results[]
*/
int resolve(int sock, Result *results, int size, Arg *list)
{
   Arg *ptr;
   int ind, iq, matches, count = 0;

   qsort((char *) results, size, sizeof(Result), compar); /* sort the hits */
   for (ptr = list, iq = 0; ptr && NotRet(ptr); ptr = ptr->next) {
       if (fields[ptr->field].attrib & ATTR_INDEXED)
           iq++;  /* Get count of indexed queries (iq) */
   }

   if (DEBUG)
       for (ind = 0; ind < size; ind++)
           swrite(sock, "-100: Resolve >> field %d  id %d\r\n",
                   results[ind].field, results[ind].id);

   /* find sequences of 'iq' matches of one id */
   for (ind = 1, matches = 1; ind < (size + 1); ind++) {
       if ((ind < size) && (results[ind-1].id == results[ind].id))
           matches++;
       else
           if ( (matches >= iq) &&          /* if everything matches */
                 validate_match(results, ind, matches, list) &&
                 find_non_indexed(sock, results[ind-1].id, list) ) {
               results[count++] = results[ind-1];  /* save the id */
               matches = 1;
           }
   }
   return count;
}


/*
  return number of indexed fields
  ensure that and all fields exist and have lookup attribute
*/
int validate_fields(int sock, Arg *list)
{
   int highest = 0, indexed = 0;
   Arg *ptr, *new;
   char *cp;

   for (ptr = list; ptr && NotRet(ptr); ptr = ptr->next) {
       if (ptr->name && (ptr->field == -1)) {
           writestring(sock, "507:Field does not exist.\r\n");
           return (0);  /* force failure */
       }
       if (ptr->field == -1) {          /* null field name is 'name' field */
           ptr->field = atoi(NAME_FIELD);
           ptr->name = new_string(fields[ptr->field].name);
       }
#if NAME_HACK
       /* mirror the code in qi_build.c */
       if (ptr->field == atoi(NAME_FIELD)) {
           cp = strchr(ptr->value, '\'');
           if (cp) strcpy(cp, cp+1);  /* squeeze out apostrophe */
           cp = strchr(ptr->value, '-');
           if (cp) {
               *cp = '\0';
               new = make_arg(ptr->name, ptr->field, cp + 1, ptr->type); /* copy second name */
               new->next = ptr->next;
               ptr->next = new;
           }
       }
#endif
       if (fields[ptr->field].attrib & ATTR_INDEXED)
           indexed++;
       if ((fields[ptr->field].attrib & ATTR_LOOKUP) == 0) {
           writestring(sock, "516:No authorization for request.\r\n");
           return (0);  /* force failure */
       }
   }
   return indexed;
}


int query(char *cmd, int sock)
{
   int status, ind, hits, size = 0;
   Arg *list, *listp, *nick;
   Result *results = NULL;

   list = parse_cmd(cmd, sock);       /* build query struct from cmd */

   if (validate_fields(sock, list) == 0)
       writestring(sock, "515:No indexed field in query.\r\n");
   else {
       for (listp = list; listp && NotRet(listp); listp = listp->next) {
           size = find(sock, listp, &results, size);    /* record ids that match query */
           /* if this is a name field, lookup nickname.  Problems with resolver? */
           if (listp->field == atoi(NAME_FIELD)) {
               nick = make_arg(listp->name, atoi(NICKNAME_FIELD), listp->value, listp->type),
               size = find(sock, nick, &results, size);
               free_args(nick);
           }
       }
   }
   if (size == 0)  /* no index matches */
       writestring(sock, "501:No matches to query.\r\n");
   else {
       hits = resolve(sock, results, size, list);
       if (hits == 0)
           writestring(sock, "501:No matches to query.\r\n");
       else if (hits > MAX_RECORDS) {
           writestring(sock, "502:Too many matches to query.\r\n");
           qilog(sock, "Too many matches (%d)", hits);
       }
       else {
           for (ind = 0; ind < hits; ind++)
               display_id(sock, results[ind].id, list, ind+1);
           qilog(sock, "Returned %d out of %d", hits, size);
           writestring(sock, "200:Ok.\r\n");
       }
   }

   free_args(list);
   free(results);
   return True;
}


void db_open()
{
   idxfab = cc$rms_fab;
   idxfab.fab$b_fac = FAB$M_GET;
   idxfab.fab$l_fna = INDEX_NAME;
   idxfab.fab$b_fns = strlen(INDEX_NAME);
   idxfab.fab$b_shr = FAB$M_SHRGET;
   idxfab.fab$v_lnm_mode = PSL$C_EXEC;
   idxfab.fab$l_xab = &idxxab;

   idxrab = cc$rms_rab;
   idxrab.rab$l_fab = &idxfab;
   idxrab.rab$l_kbf = idx_key;
   idxrab.rab$b_ksz = IDX_KEY_SIZE;
   idxrab.rab$b_rac = RAB$C_KEY;
   idxrab.rab$w_usz = IDX_RECORD_SIZE;
   idxrab.rab$l_ubf = idx_record;

   idxxab = cc$rms_xabkey;
   idxxab.xab$w_pos0 = 0;
   idxxab.xab$b_siz0 = IDX_KEY_SIZE;

   datfab = cc$rms_fab;
   datfab.fab$b_fac = FAB$M_GET;
   datfab.fab$l_fna = DATA_NAME;
   datfab.fab$b_fns = strlen(DATA_NAME);
   datfab.fab$b_shr = FAB$M_SHRGET;
   datfab.fab$v_lnm_mode = PSL$C_EXEC;
   datfab.fab$l_xab = &datxab;

   datrab = cc$rms_rab;
   datrab.rab$l_fab = &datfab;
   datrab.rab$l_kbf = dat_key;
   datrab.rab$b_ksz = DAT_KEY_SIZE;
   datrab.rab$b_rac = RAB$C_KEY;
   datrab.rab$w_usz = DAT_RECORD_SIZE;
   datrab.rab$l_ubf = dat_record;

   datxab = cc$rms_xabkey;
   datxab.xab$w_pos0 = 0;
   datxab.xab$b_siz0 = DAT_KEY_SIZE;


   if (((db_status = sys$open(&idxfab)) & 1) != SS$_NORMAL)
       return;

   if (((db_status = sys$connect(&idxrab)) & 1) != SS$_NORMAL)
       return;

   if (idxfab.fab$b_org != FAB$C_IDX) {
       db_status = 0;
       return;
   }

   if (((db_status = sys$open(&datfab)) & 1) != SS$_NORMAL)
       return;

   if (((db_status = sys$connect(&datrab)) & 1) != SS$_NORMAL)
       return;

   if (datfab.fab$b_org != FAB$C_IDX) {
       db_status = 0;
       return;
   }
}


void db_close()
{
   sys$close(&idxfab);
   sys$close(&datfab);
}