#                              -*- Mode: Perl -*-
# $Basename: Server.pm $
# $Revision: 1.5 $
# ITIID           : $ITI$ $Header $__Header$
# Author          : Ulrich Pfeifer
# Created On      : Sat Sep 28 13:53:36 1996
# Last Modified By: Ulrich Pfeifer
# Last Modified On: Sun Nov 22 18:44:38 1998
# Language        : CPerl
# Update Count    : 280
# Status          : Unknown, Use with caution!
#
# Copyright (c) 1996-1997, Ulrich Pfeifer
# 

package WAIT::Server;
use vars qw($VERSION @ISA @EXPORT);
use WAIT::Config;
use IO::Socket;
use IO::Select;
use strict;
use sigtrap qw(handler IGNORE error-signals);
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(server);

my($ver) = '$ProjectVersion: 16.2 $ ' =~ /([\d.]+)/; $VERSION = sprintf '%5.3f', $ver/10;

sub server {
  my %opt  = @_;
  my $port = $opt{port} || $WAIT::Config->{port} || 1404;

  my $lsn  = new WAIT::Handle(Reuse     => 1,
                              Listen    => 5,
                              LocalPort => $port,
                              Proto     => 'tcp');
  die "Could not connect to port $port: $!\n" unless defined $lsn;

  my $SEL  = new IO::Select( $lsn );
  my %CON;
  my $fh;
  my @ready;

  print "listening on port $port\n";
  
  while(1) {
    alarm(0);
    @ready = $SEL->can_read;
    #printf STDERR "=== %s %s\n", unpack ('b*', $SEL->[0]), join ':', @ready;
    #sleep 1;
  REQUEST:
    alarm(25);
    foreach $fh (@ready) {
      if($fh == $lsn) {
        my $new = $lsn->accept; # Create a new socket
        $CON{$new} = new WAIT::Server::Connection $new, $VERSION;
        $SEL->add($new);
      } else {
        my ($cmd, $func, @args, @cmd);
        my $fno = fileno($fh); 

        $cmd = $fh->getline();
        if ($cmd =~ /^post/i) {
/`/;
          my $buf =
            $cmd .
              join('', @{${*$fh}{'net_cmd_lines'}}) .
                ${*$fh}{'net_cmd_partial'};
          ($cmd) = ($buf =~ /^Command: (.*)$/m);
          ($cmd, @cmd) = (split (/:/, $cmd), 'quit');
          ${*$fh}{'net_cmd_partial'} = '';
/`/;
          $CON{$fh}->{http} = 1;
}
      COMMAND:
        for $cmd ($cmd, @cmd) {
          ($func, @args) = split ' ', $cmd;
          unless (fileno($fh)) {
            printf STDERR "Shuttig down $fh(%d)\n", $fno;
            delete $CON{$fh};
            $SEL->remove($fno);
            next REQUEST;
          }
          $func = lc($func);
          $func = $CON{$fh}->dispatch($func, @args);
          if ($func eq 'quit') {
            printf STDERR  "closed\n";
            $SEL->remove($fh);
            $CON{$fh}->close;
            delete $CON{$fh};
            last COMMAND;
          }
        }
      }
    }
  }
}

package WAIT::Handle;
use Net::Cmd;
use IO::Socket;
use vars qw(@ISA);
use strict;

@ISA = qw(Net::Cmd IO::Socket::INET);

# Snarfed from Net::Cmd; we don't expect an answer.
sub dataend
{
 my $cmd = shift;

 return 1
    unless(exists ${*$cmd}{'net_cmd_lastch'});

 if(${*$cmd}{'net_cmd_lastch'} eq "\015")
  {
   syswrite($cmd,"\012",1);
   print STDERR "\n"
    if($cmd->debug);
  }
 elsif(${*$cmd}{'net_cmd_lastch'} ne "\012")
  {
   syswrite($cmd,"\015\012",2);
   print STDERR "\n"
    if($cmd->debug);
  }

 print STDERR "$cmd>>> .\n"
    if($cmd->debug);

 syswrite($cmd,".\015\012",3);

 delete ${*$cmd}{'net_cmd_lastch'};

}

package WAIT::Server::Connection;
use strict;
use Sys::Hostname;
use Socket qw(AF_INET unpack_sockaddr_in);
use vars qw(%CMD %MSG %HELP);

my $HOST = hostname;
{
  no strict;
  local *stab = *WAIT::Server::Connection::;
  my ($key,$val);
  while (($key,$val) = each(%stab)) {
    next unless $key =~ /^cmd_(.*)/;
    local(*ENTRY) = $val;
    if (defined &ENTRY) {
      $CMD{$1} = \&ENTRY;
    }
  }
}


sub new {
  my $type = shift;
  my $fh   = shift;
  my $msg  = shift;
  my $self = {_fh => $fh};
  
  my $hersockaddr    = $fh->peername();
  my ($port, $iaddr) = unpack_sockaddr_in($hersockaddr);
  my $peer           = gethostbyaddr($iaddr, AF_INET);
  $self->{peer}      = $peer;
  $self->{database}  = 'DB';
  $self->{table}     = 'cpan';
  $self->{hits}      = 10;
  print "Connection from $peer\n";
  bless $self, $type;
  $self->msg(200, $msg);
  $self;
}

sub close {
  my $self = shift;

  $self->{_fh}->close;
}


sub dispatch {
  my $self = shift;
  my $cmd  = shift;

  print "$cmd @_\n";
  unless (exists $CMD{$cmd}) {
    $self->msg(500);
  } else {
    &{$CMD{$cmd}}($self, @_);
  }
  $cmd;
}

sub msg {
  my $self = shift;
  my $code = shift;
  my $msg  = $MSG{$code} || '';
  printf("%s %s %03d $msg\r\n", scalar(localtime(time)), $self->{peer}, $code, @_);
  $self->{_fh}->datasend(sprintf("%03d $msg\r\n", $code, @_));
}

sub end {
  my $self = shift;
  $self->{_fh}->dataend;
}


require WAIT::Query::Wais;
require WAIT::Database;
use Fcntl;

my %DB;                         # cache Databas handles
sub DATABASE {
  my $dn = shift;

  return $DB{$dn} if exists $DB{$dn};
  $DB{$dn} = WAIT::Database->open(name      => $dn,
                                  directory => $WAIT::Config->{'WAIT_home'},
                                  mode      => O_RDONLY);
  return $DB{$dn};
}

my %TB;                         # cache Table handles
sub TABLE {
  my ($dbname, $tname) = @_;

  return $TB{$dbname.$tname} if exists $TB{$dbname.$tname};
  my $db = DATABASE($dbname);

  $TB{$dbname.$tname} = $db->table(name => $tname);
  $TB{$dbname.$tname};
}


# helpers
sub result {
  my $self   = shift;
  my $hit    = shift;
  my $did;

  # http uses raw document id's
  if ($self->{http}) {
    return $hit;
  }
  unless ($self->{result}) {
    $self->msg(404);
    return;
  }
  unless ($did = $self->{result}->[$hit-1]) {
    $self->msg(405);
    return;
  }
  return $did;
}

sub table {
  my $self   = shift;
  
  TABLE($self->{database}, $self->{table});
}

sub output {
  my $self = shift;

  $self->{_fh}->datasend(@_);
}


# The commands

sub cmd_help {
  my $self = shift;

  $self->msg(100);
  for (sort keys %CMD) {
    $self->output(sprintf("%-15s %s\r\n", $_, $HELP{$_}||''));
  }
  $self->end;
}

sub cmd_quit {
  my $self = shift;
  $self->msg(205);
}

sub cmd_database {
  my $self   = shift;
  my $dbname = shift || $self->{database};


  if (DATABASE($dbname)) {
    delete $self->{'result'};
    $self->{database} = $dbname;
    $self->msg(201, $dbname);
  } else {
    $self->msg(401, $dbname);
  }
}

sub cmd_table {
  my $self   = shift;
  my $table  = shift || $self->{'table'};
  my $dbname = $self->{'database'};

  if (TABLE($dbname, $table)) {
    delete $self->{'result'};
    $self->{'table'} = $table;
    $self->msg(202, $table);
  } else {
    $self->msg(402, $table);
  }
}

sub cmd_hits {
  my $self   = shift;
  my $hits   = shift;

  if ($hits) {
    $self->{hits} = $hits;
    $self->msg(204, $hits);
  } else {
    $self->msg(501);
  }
}

sub cmd_info {
  my $self   = shift;
  my $hit    = shift;

  my $did    = $self->result($hit);
  return unless $did;
  
  my $tb     = $self->table;

  my %rec    = $tb->fetch($did);
  $self->msg(207, $did);
  for (keys %rec) {
    $self->{_fh}->datasend(sprintf("%-15s %s\n", $_, $rec{$_}));
  }
  $self->end;
}

sub cmd_get {
  my $self   = shift;
  my $hit    = shift;
  my $did    = $self->result($hit);

  return unless $did;
  my $tb     = $self->table;
  my %rec    = $tb->fetch($did);
  my $key    = $rec{docid};

  $key = $tb->dir . '/' . $key if $key =~ m(^data/);

  my $text   = $tb->fetch_extern($key);
  
  $self->msg(206, $did);
  $self->output($text);
  $self->output("\n") unless $text =~ /\n$/;
  $self->end;
}

sub cmd_search {
  my $self   = shift;
  my $query  = join ' ', @_;
  my $tb     = $self->table;

  my $wq = eval {WAIT::Query::Wais::query($tb, $query)};
  unless ($wq) {
    $self->msg(403);
    return;
  }
  my %hits = $wq->execute();
  my @did = sort {$hits{$b} <=> $hits{$a}}keys %hits;

  # sanity check. this is expensive and should be obsolete!
  # @did =  grep $tb->fetch($_), @did;

  $self->{'result'} = \@did;
  my $all_hits  = scalar @did;
  my $send_hits = $all_hits;

  if ($send_hits > $self->{hits}) {
    $send_hits = $self->{hits};
  }
  $self->msg(203, $all_hits, $send_hits);
  my $i;
  
  for ($i=1;$i<=$send_hits;$i++) {
    my $did = $did[$i-1];
    my %rec = $tb->fetch($did);
    $self->{_fh}->datasend(sprintf("%2d %5.3f %s\n",
                                   $self->{http}?$did:$i,
                                   $hits{$did},
                                   $rec{headline}));
  }
  $self->end();
}

# read status messages
my $line;
while (defined ($line = <DATA>)) {
  chomp($line);
  my ($cmd, $msg) = split ' ', $line, 2;
  last unless $cmd;
  $HELP{$cmd} = $msg;
}
while (defined ($line = <DATA>)) {
  chomp($line);
  next unless $line =~ /^\d/;
  my ($code, $msg) = split ' ', $line, 2;
  $MSG{$code} = $msg;
}


1;

__DATA__
help     -                    display this help message
database name                 set database name
table    name                 set table name
search   query                submitt query
get      number               fetch full text of hit with number
info     number               display info record of hit with number
format   text|html|term
hits     number               set maximum hits displayed to number
quit

100 help message follows
200 WAIT server %s ready
205 closing connection - goodbye!
201 database %s selected
401 could not open database %s
202 table %s selected
203 query returnes %d hits, %d hits follow
204 will return %d hits
207 record %d follows
206 text of record %d follows
402 could not open table %s
403 syntax error in query
404 use search first
405 no such hit
500 command not recognized
501 command syntax error
502 access restriction or permission denied
503 program fault - command not performed
1;
