modules/qi/query_instructions.c

/* [<][>]
[^][v][top][bottom][index][help] */

FUNCTIONS

This source file includes following functions.
  1. log_inst_print
  2. create_name_query
  3. add_filter
  4. create_query
  5. write_results
  6. write_objects
  7. insert_radix_serials
  8. map_qc2rx
  9. QI_execute
  10. instruction_free
  11. QI_free
  12. QI_new

/***************************************
  $Revision: 1.22 $


  Sql module (sq).  This is a mysql implementation of an sql module.

  Status: NOT REVUED, NOT TESTED

  Note: this code has been heavily coupled to MySQL, and may need to be changed
  (to improve performance) if a new RDBMS is used.

  ******************/ /******************
  Filename            : query_instructions.c
  Author              : ottrey@ripe.net
  OSs Tested          : Solaris
  Problems            : Moderately linked to MySQL.  Not sure which inverse
                        attributes each option has.  Would like to modify this
                        after re-designing the objects module.
  Comments            : Not sure about the different keytypes.
  ******************/ /******************
  Copyright (c) 1999                              RIPE NCC
 
  All Rights Reserved
  
  Permission to use, copy, modify, and distribute this software and its
  documentation for any purpose and without fee is hereby granted,
  provided that the above copyright notice appear in all copies and that
  both that copyright notice and this permission notice appear in
  supporting documentation, and that the name of the author not be
  used in advertising or publicity pertaining to distribution of the
  software without specific, written prior permission.
  
  THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
  ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS; IN NO EVENT SHALL
  AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
  DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
  AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
  OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  ***************************************/
#include <stdio.h>
#include <string.h>
#include "which_keytypes.h"
#include "query_instructions.h"
#include "mysql_driver.h"
#include "rxroutines.h"
#include "stubs.h"
#include "constants.h"

/*+ String sizes +*/
#define STR_S   63
#define STR_M   255
#define STR_L   1023
#define STR_XL  4095
#define STR_XXL 16383


#include "QI_queries.def"

/* log_inst_print() */
/*++++++++++++++++++++++++++++++++++++++
  Log the instruction.

  char *str instruction to be logged.
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static void log_inst_print(char *str) {
/* [<][>][^][v][top][bottom][index][help] */
  FILE *logf;

  if (CO_get_instr_logging() == 1) {
    if (strcmp(CO_get_instr_logfile(), "stdout") == 0) {
      printf("%s", str);
    }
    else {
      logf = fopen(CO_get_instr_logfile(), "a");
      fprintf(logf, "%s", str);
      fclose(logf);
    }
  }

} /* log_inst_print() */

/* create_name_query() */
/*++++++++++++++++++++++++++++++++++++++
  Create an sql query for the names table. 

  char *query_str

  const char *sql_query

  const char *keys
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static void create_name_query(char *query_str, const char *sql_query, const char *keys) {
/* [<][>][^][v][top][bottom][index][help] */
  int i;
  char *word;
  char from_clause_atom[STR_XL];
  char from_clause[STR_XXL];
  char where_clause_atom[STR_XL];
  char where_clause[STR_XXL];
  char *keys_tmp;

  strcpy(from_clause, "");
  strcpy(where_clause, "");

  keys_tmp = (char *)calloc(1, strlen(keys)+1);
  strcpy(keys_tmp, keys);

  word = (char *)strtok(keys_tmp, " ");

  sprintf(from_clause_atom, "names N%.2d", 1);
  sprintf(where_clause_atom, "N%.2d.name='%s'", 1, word);

  strcat(from_clause, from_clause_atom);
  strcat(where_clause, where_clause_atom);

  for (i=2; (word=(char *)strtok(NULL, " ")) != NULL; i++) {
    sprintf(from_clause_atom, ", names N%.2d", i);
    sprintf(where_clause_atom, " AND N%.2d.name='%s' AND N01.pe_ro_id = N%.2d.pe_ro_id", i, word, i);

    strcat(from_clause, from_clause_atom);
    strcat(where_clause, where_clause_atom);

    strcpy(from_clause_atom, "");
    strcpy(where_clause_atom, "");
  }

  sprintf(query_str, sql_query, from_clause, where_clause);

  /* XXX Free here */
  /*
  free(keys_tmp);
  */

} /* create_name_query() */

static void add_filter(char *query_str, const Query_command *qc) {
/* [<][>][^][v][top][bottom][index][help] */
  int i;
  int qlen;
  char filter_atom[STR_M];

  if (MA_bitcount(qc->object_type_bitmap) > 0) { 
    strcat(query_str, " AND (");
    for (i=0; i < C_END; i++) {
      if (MA_isset(qc->object_type_bitmap, i)) {
        strcpy(filter_atom, "");
        sprintf(filter_atom, "i.object_type = %d OR ", DF_get_class_dbase_code(i));
        strcat(query_str, filter_atom);
      }
    }
    qlen = strlen(query_str);
    query_str[qlen-3] = ')';
    query_str[qlen-2] = '\0';
    query_str[qlen-1] = '\0';
  }
  
} /* add_filter() */

/* create_query() */
/*++++++++++++++++++++++++++++++++++++++
  Create an sql query from the query_command and the matching keytype and the
  selected inverse attributes.
  Note this clears the first inv_attribute it sees, so is called sequentially
  until there are no inv_attributes left.

  WK_Type keytype The matching keytype.

  const Query_command *qc The query command.

  mask_t *inv_attrs_bitmap The selected inverse attributes.
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static char *create_query(Query_t q, const Query_command *qc) {
/* [<][>][^][v][top][bottom][index][help] */
  char query_str_buf[STR_XXL];
  char *query_str=NULL;
  Q_Type_t querytype;
  int conduct_test = 0;

  int ibits = MA_bitcount(qc->inv_attrs_bitmap);
  if (ibits > 0 ) {
    querytype = Q_INVERSE;
  }
  else {
    querytype = Q_LOOKUP;
  }

  strcpy(query_str_buf, "");

  if (   (q.query != NULL) 
      && (q.querytype == querytype) ) {

    if (querytype == Q_LOOKUP) {
      if (MA_bitcount(qc->object_type_bitmap) == 0) {
        conduct_test=1;
      }
      else if (MA_isset(qc->object_type_bitmap, q.class)) {
        conduct_test=1;
      }
    }
    else {
      conduct_test=1;
    }
  }
  else {
    conduct_test=0;
  }

  if (conduct_test == 1) {
    if (q.keytype == WK_NAME) { 
      /* Name queries require special treatment. */
      create_name_query(query_str_buf, q.query, qc->keys);
    }
    else {
      sprintf(query_str_buf, q.query, qc->keys);
    }

    if (q.querytype == -1) {
      /* It is class type ANY so add the object filtering */
      add_filter(query_str_buf, qc);
    }

    if (strcmp(query_str_buf, "") == 0) {
      query_str = NULL;
    }
    else {
      query_str = (char *)calloc(1, strlen(query_str_buf)+1);
      strcpy(query_str, query_str_buf);
    }
  }

  return query_str;
} /* create_query() */

/* write_results() */
/*++++++++++++++++++++++++++++++++++++++
  Write the results to the client socket.

  SQ_result_set_t *result The result set returned from the sql query.
  
  int sock The socket that the client is connected to.

  XXX NB. this is very dependendant on what rows are returned in the result!!!
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static int write_results(SQ_result_set_t *result, int sock) {
/* [<][>][^][v][top][bottom][index][help] */
  SQ_row_t *row;
  char *str;
  char log_str[STR_L];
  int retrieved_objects=0;

  /* Get all the results - one at a time */
  if (result != NULL) {
    while ( (row = SQ_row_next(result)) != NULL) {
      str = SQ_get_column_string(result, row, 0);
      if (str != NULL) {
        strcpy(log_str, "");
        sprintf(log_str, "Retrieved serial id = %d\n", atoi(str));
        log_inst_print(log_str);
      }
      free(str);

      str = SQ_get_column_string(result, row, 2);
      if (str != NULL) {
        SK_puts(sock, str);
        SK_puts(sock, "\n");
        retrieved_objects++;
      }
      free(str);
    }
  }
  
  return retrieved_objects;
} /* write_results() */


/* write_objects() */
/*++++++++++++++++++++++++++++++++++++++
  This is linked into MySQL by the fact that MySQL doesn't have sub selects
  (yet).  The queries are done in two stages.  Make some temporary tables and
  insert into them.  Then use them in the next select.

  SQ_connection_t *sql_connection The connection to the database.

  char *id_table The id of the temporary table (This is a result of the hacky
                  way we've tried to get MySQL to do sub-selects.)
  
  unsigned int recursive A recursive query.

  unsigned int sock The client socket.

  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  ++++++++++++++++++++++++++++++++++++++*/
static void write_objects(SQ_connection_t *sql_connection, char *id_table, unsigned int recursive, unsigned int sock) {
/* [<][>][^][v][top][bottom][index][help] */
  /* XXX This should really return a linked list of the objects */

  SQ_result_set_t *result;
  int retrieved_objects=0;

  char sql_command[STR_XL];
  char log_str[STR_L];
    
  strcpy(sql_command, "");
  /* XXX These may and should change a lot. */
  sprintf(sql_command, Q_OBJECTS, id_table, id_table);
  result = SQ_execute_query(sql_connection, sql_command);

  retrieved_objects += write_results(result, sock);

  SQ_free_result(result);

  /* Now for recursive queries */
  if (recursive == 1) {
    strcpy(sql_command, "");
    sprintf(sql_command, Q_REC, id_table, "admin_c", id_table, id_table);
    SQ_execute_query(sql_connection, sql_command);

    strcpy(sql_command, "");
    sprintf(sql_command, Q_REC, id_table, "tech_c", id_table, id_table);
    SQ_execute_query(sql_connection, sql_command);

    strcpy(sql_command, "");
    sprintf(sql_command, Q_REC, id_table, "zone_c", id_table, id_table);
    SQ_execute_query(sql_connection, sql_command);

    /* XXX These may and should change a lot. */
    strcpy(sql_command, "");
    sprintf(sql_command, Q_REC_OBJECTS, id_table, id_table, id_table);
    result = SQ_execute_query(sql_connection, sql_command);

    retrieved_objects += write_results(result, sock);

    SQ_free_result(result);

    /* Now drop the IDS recursive table */
    strcpy(sql_command, "");
    sprintf(sql_command, "DROP TABLE %s_R", id_table);
    SQ_execute_query(sql_connection, sql_command);
  }

  /* If nothing is retreived default to return the 0th object - I.e "not found" */
  if (retrieved_objects == 0) {
    result = SQ_execute_query(sql_connection, Q_NO_OBJECTS);
    write_results(result, sock);

    SQ_free_result(result);
  }

  /* Now drop the IDS table */
  strcpy(sql_command, "");
  sprintf(sql_command, "DROP TABLE %s", id_table);
  SQ_execute_query(sql_connection, sql_command);

  strcpy(log_str, "");
  sprintf(log_str, "%d object(s) retrieved\n\n", retrieved_objects);
  log_inst_print(log_str);

  if (CO_get_accounting() == 1) {
    /* XXX - later man.
    account(retrieved_objects);
    */
    printf("account(retrieved_objects);\n");
  }

} /* write_objects() */

/* insert_radix_serials() */
/*++++++++++++++++++++++++++++++++++++++
  Insert the radix serial numbers into a temporary table in the database.

  mask_t bitmap The bitmap of attribute to be converted.
   
  SQ_connection_t *sql_connection The connection to the database.

  char *id_table The id of the temporary table (This is a result of the hacky
                  way we've tried to get MySQL to do sub-selects.)
  
  GList *datlist The list of data from the radix tree.

  XXX Hmmmmm this isn't really a good place to free things... infact it's quite nasty.  :-(
  
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
             <LI><A HREF="http://www.gtk.org/rdp/glib/glib-doubly-linked-lists.html">Glist</A>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static void insert_radix_serials(SQ_connection_t *sql_connection, char *id_table, GList *datlist) {
/* [<][>][^][v][top][bottom][index][help] */
  GList    *qitem;
  char sql_command[STR_XL];
  int serial;
  int i;

/*
  TODO -- marek --- TODO
  for( qitem = g_list_last(datlist); qitem != NULL; qitem = g_list_previous(qitem)) {
 */
  for( qitem = g_list_first(datlist); qitem != NULL; qitem = g_list_next(qitem)) {
    rx_datcpy_t *datcpy = qitem->data;

    serial = datcpy->leafcpy.data_key;

    strcpy(sql_command, "");
    sprintf(sql_command, "INSERT INTO %s values (%d)", id_table, serial);
    SQ_execute_query(sql_connection, sql_command);

    /* XXX AAAARGH why here?!?! */
    wr_free(datcpy->leafcpy.data_ptr);
  }

  /* XXX Possible memory leak here. -fix above?? */
  g_list_foreach(datlist, rx_free_list_element, NULL);
  g_list_free(datlist);

} /* insert_radix_serials() */

/* map_qc2rx() */
/*++++++++++++++++++++++++++++++++++++++
  The mapping between a query_command and a radix query.

  Query_instruction *qi The Query Instruction to be created from the mapping
                        of the query command.

  const Query_command *qc The query command to be mapped.

  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static int map_qc2rx(Query_instruction *qi, const Query_command *qc) {
/* [<][>][^][v][top][bottom][index][help] */
  int result=1;

  qi->rx_keys = qc->keys;

  switch(qi->family) {
    case RX_FAM_IN:
      if (!MA_isset(qc->object_type_bitmap, C_IN)) {
        result=0;
      }
    break;

    case RX_FAM_RT:
      if (!MA_isset(qc->object_type_bitmap, C_RT)) {
        result=0;
      }
    break;
    
    default:
      fprintf(stderr, "ERROR: Bad family type in radix query\n");
  }

  if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
    qi->rx_srch_mode = RX_SRCH_EXLESS;
    qi->rx_par_a = 0;
  }
  else if ( (qc->L == 1) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
    qi->rx_srch_mode = RX_SRCH_LESS;
    qi->rx_par_a = RX_ALL_DEPTHS;
  }
  else if ( (qc->L == 0) && (qc->M == 1) && (qc->l == 0) && (qc->m == 0) && (qc->x == 0) ) {
    qi->rx_srch_mode = RX_SRCH_MORE;
    qi->rx_par_a = RX_ALL_DEPTHS;
  }
  else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 1) && (qc->m == 0) && (qc->x == 0) ) {
    qi->rx_srch_mode = RX_SRCH_LESS;
    qi->rx_par_a = 1;
  }
  else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 1) && (qc->x == 0) ) {
    qi->rx_srch_mode = RX_SRCH_MORE;
    qi->rx_par_a = 1;
  }
  else if ( (qc->L == 0) && (qc->M == 0) && (qc->l == 0) && (qc->m == 0) && (qc->x == 1) ) {
    qi->rx_srch_mode = RX_SRCH_EXACT;
    qi->rx_par_a = 0;
  }
  else {
    strcpy(log_str, "");
    sprintf(log_str, "ERROR in qc2rx mapping: bad combination of flags\n");
    log_inst_print(log_str);
    result = 0;
  }

  return result;

} /* map_qc2rx() */


/* QI_execute() */
/*++++++++++++++++++++++++++++++++++++++
  Execute the query instructions.  This is called by a g_list_foreach
  function, so each of the sources in the "database source" list can be passed
  into this function.

  void *database_voidptr Pointer to the database.
  
  void *qis_voidptr Pointer to the query_instructions.
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
             <LI><A
             HREF="http://www.gtk.org/rdp/glib/glib-singly-linked-lists.html#G-SLIST-FOREACH">g_list_foreach</A>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
void QI_execute(void *database_voidptr, void *qis_voidptr) {
/* [<][>][^][v][top][bottom][index][help] */
  char *database = (char *)database_voidptr;
  Query_instructions *qis = (Query_instructions *)qis_voidptr;
  Query_instruction **ins=NULL;
  char id_table[STR_S];
  char sql_command[STR_XL];
  GList *datlist=NULL;
  int i;

  char log_str[STR_L];

  SQ_connection_t *sql_connection=NULL;

  sql_connection = SQ_get_connection(CO_get_host(), CO_get_database_port(), database, CO_get_user(), CO_get_password() );

  if (sql_connection == NULL) {
    SK_puts(qis->sock, "% WARNING: Failed to make connection to ");
    SK_puts(qis->sock, database);
    SK_puts(qis->sock, " database mirror.\n\n");
  }
  else {
    strcpy(sql_command, "");

    /* XXX This is a really bad thing to do.  It should'nt _have_ to be called here.
       But unfortunately it does.   -- Sigh. */
    sprintf(id_table, "ID_%d", mysql_thread_id(sql_connection) );

    strcpy(sql_command, "");
    sprintf(sql_command, "CREATE TABLE %s ( id int ) TYPE=HEAP", id_table);
    SQ_execute_query(sql_connection, sql_command);

    if (qis->recursive == 1) {
      strcpy(sql_command, "");
      sprintf(sql_command, "CREATE TABLE %s_R ( id int ) TYPE=HEAP", id_table);
      SQ_execute_query(sql_connection, sql_command);
    }

    ins = qis->instruction;
    for (i=0; ins[i] != NULL; i++) {
      Query_instruction *qi = ins[i];

      switch ( qi->search_type ) {
        case R_SQL:
          if ( qi->query_str != NULL ) {
            strcpy(sql_command, "");
            sprintf(sql_command, "INSERT INTO %s %s", id_table, qi->query_str);
            SQ_execute_query(sql_connection, sql_command);
          }
        break;

#define RIPE_REG 17
        case R_RADIX:
          datlist = NULL;
          /* Apply the object filter */
          if ( RX_asc_search(qi->rx_srch_mode, qi->rx_par_a, 0, qi->rx_keys, RIPE_REG, qi->space, qi->family, &datlist, RX_ANS_ALL) == RX_OK ) {
            insert_radix_serials(sql_connection, id_table, datlist);
          }
          else {
            /* Skip query */
            strcpy(log_str, "");
            sprintf(log_str, " /* Skip in query */\n");
            log_inst_print(log_str);
          }
        break;

        default:
          strcpy(log_str, "");
          sprintf(log_str, "ERROR: bad search_type\n");
          log_inst_print(log_str);
      } /* switch */
    }
    write_objects(sql_connection, id_table, qis->recursive, qis->sock);
  }

  SQ_close_connection(sql_connection);
  
} /* QI_execute() */

/* instruction_free() */
/*++++++++++++++++++++++++++++++++++++++
  Free the instruction.

  Query_instruction *qi query_instruction to be freed.
   
  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
static void instruction_free(Query_instruction *qi) {
/* [<][>][^][v][top][bottom][index][help] */
  if (qi != NULL) {
    if (qi->query_str != NULL) {
      free(qi->query_str);
    }
    free(qi);
  }
} /* instruction_free() */

/* QI_free() */
/*++++++++++++++++++++++++++++++++++++++
  Free the query_instructions.

  Query_instructions *qis Query_instructions to be freed.
   
  XXX This isn't working too well at the moment.

  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
void QI_free(Query_instructions *qis) {
/* [<][>][^][v][top][bottom][index][help] */
  /* XXX huh!?H?
  int i;

  for (i=0; qis[i] != NULL; i++) {
    instruction_free(*qis[i]);
  }
  */
  if (qis != NULL) {
    free(qis);
  }

} /* QI_free() */

/* QI_new() */
/*++++++++++++++++++++++++++++++++++++++
  Create a new set of query_instructions.

  mask_t bitmap The bitmap of attribute to be converted.
   
  const Query_command *qc The query_command that the instructions are created
                          from.

  unsigned int sock The client socket.

  More:
  +html+ <PRE>
  Authors:
        ottrey
  +html+ </PRE><DL COMPACT>
  +html+ <DT>Online References:
  +html+ <DD><UL>
  +html+ </UL></DL>

  ++++++++++++++++++++++++++++++++++++++*/
Query_instructions *QI_new(const Query_command *qc, unsigned int sock) {
/* [<][>][^][v][top][bottom][index][help] */
  Query_instructions *qis=NULL;
  Query_instruction *qi=NULL;
  int i_no=0,j;
  int i;
  char *query_str;

  char log_str[STR_L];

  qis = (Query_instructions *)calloc(1, sizeof(Query_instructions));
  qis->sock = sock;
  qis->recursive = qc->qe.recursive;


  for (i=0; Query[i].query != NULL; i++) {

    /* If matched the keytype */
    if ( MA_isset(qc->keytypes_bitmap, Query[i].keytype) == 1) {
      qi = (Query_instruction *)calloc(1, sizeof(Query_instruction));

      /* SQL Query */
      if ( Query[i].refer == R_SQL) {
        qi->search_type = R_SQL;
        query_str = create_query(Query[i], qc);
        if (query_str != NULL) {
          qi->query_str = query_str;
          qis->instruction[i_no++] = qi;
        }
      }
      /* Radix Query */
      else if (Query[i].refer == R_RADIX) {
        qi->search_type = R_RADIX;
        qi->space = Query[i].space;
        qi->family = Query[i].family;

        if (map_qc2rx(qi, qc) == 1) {
          /* Add the query_instruction to the array */
          qis->instruction[i_no++] = qi;
        }
      }
      else {
        strcpy(log_str, "");
        sprintf(log_str, "ERROR: bad search_type\n");
        log_inst_print(log_str);
      }
    }
  }
  qis->instruction[i_no++] = NULL;

  return qis;

} /* QI_new() */

/* [<][>][^][v][top][bottom][index][help] */