#!/usr/bin/perl -w
#
# Daemon.pm - XPC Daemon
#
# TODO: Make this a subclass of HTTP::Daemon and override
# &product_tokens() to return "XPC::Daemon" + version number
# so that the Server HTTP header won't say libwww*.
#
# Copyright (C) 2001 Gregor N. Purdy.
# All rights reserved.
#
# This is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.
#


use strict;

package XPC::Daemon;

use HTTP::Daemon;
use HTTP::Status;
use Data::Dumper;

use XPC;


#
# new()
#

sub new
{
  my $class = shift;

  my $self = bless { PROCEDURES => { } }, $class;

  $self->{DAEMON} = new HTTP::Daemon;
  $self->{DEBUG} = 0;

  return $self;
}


#
# debug()
#

sub debug
{
  my $self = shift;

  $self->{DEBUG} = ($_[0] ? 1 : 0) if @_;

  return $self->{DEBUG};
}


#
# url()
#

sub url
{
  my $self = shift;

  return $self->daemon->url . "XPC";
}


#
# run()
#

sub run
{
  my $self = shift;

  die "$0: No procedures defined!" unless keys %{$self->{PROCEDURES}};

  while (my $c = $self->daemon->accept) {
    while (my $r = $c->get_request) {
      if ($r->method eq 'POST' and $r->url->path eq "/XPC") {
        my @response = $self->process_request($r);
        my $xpc = XPC->new;
        map { $xpc->add_response($_); } @response;
        my $http_res = new HTTP::Response;
        $http_res->content($xpc->as_string);
        $http_res->code(200);
        $c->send_response($http_res);
      } else {
        $c->send_error(RC_FORBIDDEN)
      }
    }
    $c->close;
    undef($c);
  }
}



#
# add_procedure()
#

sub add_procedure
{
  my $self = shift;
  my ($procedure, $callback) = @_;

  $self->{PROCEDURES}{$procedure} = $callback;

  print "&XPC::Daemon::add_procedure(): Added procedure '$procedure'.\n" if $self->debug();
}


#
# callback()
#

sub callback
{
  my ($self, $procedure) = @_;

  return $self->{PROCEDURES}{$procedure};
}


#
# daemon()
#

sub daemon
{
  my $self = shift;
  return $self->{DAEMON};
}


#
# process_request()
#

sub process_request
{
  my $self = shift;
  my $r = shift;

  my @response;
  my $id_required;

  if ($self->debug()) {
    print "&XPC::Daemon::process_request(): Recieved request:\n";
    print $r->content;
    print "\n";

    print "&XPC::Daemon::process_request(): Parsing request...\n";
  }

  my $xml = $r->content;
  my $xpc;

  eval { $xpc = XPC->new($xml); };

  if ($@ or !defined $xpc) {
    print "&XPC::Daemon::process_request(): Unable to parse XPC request:\n";
    print $xml;
    print "\n";
    push @response, make_fault(7, "Unable to parse request!");
    return @response;
  }

  print "&XPC::Daemon::process_request(): Request parses as class ", ref $xpc, ".\n" if $self->debug;

  $xpc = $xpc->[0];

  if ($self->debug()) {
    print "&XPC::Daemon::process_request(): Class is ", ref $xpc, ".\n";
    print Dumper($xpc);
    print "\n";
  }


  my @requests = grep { ref $_ ne 'XPC::Characters' } @{$xpc->{Kids}};

  print "&XPC::Daemon::process_request(): Processing queries and calls...\n" if $self->debug();

  foreach my $req (@requests) {
    if (@requests > 1 and not $req->id) {
      @response = ( make_fault(3, "Every request of a multi-request must set 'id'!") );
      last;

      # TODO: We really should scan them first so we don't cause any side-effects.
    }

    if (ref $req eq 'XPC::call') {
      push @response, $self->process_call($req);
    } elsif (ref $req eq 'XPC::query') {
      push @response, make_fault(1, "<query>s are not supported!");
    } elsif (ref $req eq 'XPC::result') {
      push @response, make_fault(5, "<result>s are not requests!");
    } elsif (ref $req eq 'XPC::fault') {
      push @response, make_fault(6, "<fault>s are not requests!");
    } else {
      push @response, make_fault(4, sprintf("Unknown request type '%s'!", ref $req));
    }
  }

  return @response;
}


#
# process_call()
#

sub process_call
{
  my $self = shift;
  my $call = shift;

  my $procedure = $call->procedure;

  print "&XPC::Daemon::process_call(): Processing call to '$procedure'...\n" if $self->debug();

  my $callback  = $self->callback($procedure);

  if ($callback) {
    return make_result(scalar(&$callback()));
  } else {
    return make_fault(2, sprintf("<call> to unknown procedure '%s'!", $procedure));
  }
}


##############################################################################
##
## UTILITIES:
##
##############################################################################


#
# make_fault()
#

sub make_fault
{
  return new XPC::fault(@_);
}


#
# make_result()
#

sub make_result
{
  return XPC::result->new_scalar(@_);
}


1;


=head1 NAME

XPC::Daemon - XML Procedure Call daemon class


=head1 SYNOPSIS

  use XPC::Daemon;
  my $daemon = new XPC::Daemon;
  $daemon->add_procedure('localtime', sub { localtime });
  my $pid = fork;
  die "$0: Unable to fork!\n" unless  defined $pid;
  
  if ($pid) {
    print STDOUT $daemon->url, "\n";
    print STDERR "$0: Forked child $pid.\n";
    exit 0;
  } else {
    $daemon->run;
    exit 0;
  } 


=head1 DESCRIPTION

This class is a generic XPC-over-HTTP server daemon. Use the C<add_procedure>
method to give it specific functionality.


=head1 AUTHOR

Gregor N. Purdy E<lt>gregor@focusresearch.comE<gt>


=head1 COPYRIGHT

Copyright (C) 2001 Gregor N. Purdy.
All rights reserved.

This is free software; you can redistribute it and/or modify it under
the same terms as Perl itself.



syntax highlighted by Code2HTML, v. 0.9.1