# Connection.pm
# Scott Bronson
# 8 Dec 2003
#
# This is a connection abstraction library for the xmlexer utility.
# Right now, only the RPC::XML is supported, but it should be very
# easy to add your own client library.  It should work with anything
# that exports a set of callable functions -- it doesn't even have
# to be XML-based!

# hierarchy (parens means not implemented yet):
#
#     base object           protocol object           client object
#     -----------------------------------------------------------
#     Connection     <-     XMLRPC             <-     RPCXMLClient
#                                                     (Frontier::RPC)
#                                                     (Soap::Lite)
#
#                           Soap               <-     SOAPLiteClient
#                                                     (SOAP::RPC)
#


package Connection;

use strict;

use vars qw($VERSION);
$VERSION = '0.8';


# this is the base class common to all connections

# protocol objects need to implement
# calc_command_set

# client objects need to implement
# new
# get_url
# call


# Returns the previously cached command set
sub get_command_set
{
	my $self = shift;
	return $self->{commands} || {};
}

# Call introspection methods to retrieve the command set from the server
# there's no longer any need to have these routines separate...
sub retrieve_command_set
{
	my $self = shift;
	return $self->calc_command_set(@_);
}

# Clear commands from the command set
sub clear_command_set
{
	my $self = shift;

	if(@_) {
		for(@_) {
			if(exists $self->{commands}->{$_}) {
				delete $self->{commands}->{$_};
				print "$_ cleared\n" if $self->verb > 0;
			} else {
				print "$_ doesn't exist!\n";
			}
		}
	} else {
		# clear all commands
		my $cnt = keys %{$self->{commands}};
		delete $self->{commands} if exists $self->{commands};
		print "$cnt commands cleared\n" if $self->verb > 0;
	}
}


# Sets the noise level for all subsequent calls.
sub set_verbosity
{
	my $self = shift;
	$self->{verbosity} = shift;
}

# Returns the verbosity (cuts down on typing)
sub verb
{
	return shift->{verbosity};
}


sub trimstring
{
	my $self = shift;
	my $val = shift;
	my $maxlen = shift || 40;

	$val =~ s/\n//;
	my $len = length($val);
	$val = substr($val, 0, $maxlen);
	if($len > length($val)) {
		$val =~ s/...$/\.\.\./;
	}

	return $val;
}


sub summarize_array
{
	my $self = shift;
	my $type = shift;
	my $val = shift;

	my $str = "$type with " . @$val . " item" . (@$val==1?'':'s');
	if(@$val) {
		$str .= ": (" . $self->trimstring("'".join("', '", @$val)."'") . ")\n";
	} else {
		$str .= '.';
	}

	return $str;
}



sub summarize
{
	my $self = shift;
	my $val = shift;

	if(ref $val eq 'ARRAY') {
		return $self->summarize_array('array', $val);
	} elsif(ref $val eq 'HASH') {
		return $self->summarize_array('hash', [keys %$val]);
	} elsif(ref $val eq '') {
		return "scalar: \"" . $self->trimstring($val) . "\"\n";
	}
	return "Unknown type: " . ref($val) . "\n";
}



package Connection::Soap;

# Protocol object handling SOAP

# one day...





package Connection::XMLRPC;

# Protocol object handling XML-RPC

# This module uses the standardized XML-RPC introspection calls to
# discover the list of callable functions.  Most XML-RPC implementation
# objects will want to inherit from this one.

use strict;
use vars qw(@ISA);
@ISA = qw(Connection);


# If a server doesn't support the introspection call, we can fake it
# using methodSignature and methodHelp.  This will never return the
# version though.

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

	my $intro = { name => $cmd };

	my $sig = $self->call('system.methodSignature', $cmd);
	if(ref($sig) eq 'ARRAY') {
		$intro->{'signature'} = $sig;
	} else {
		print "system.methodSignature($cmd) did not return an array!\n";
	}

	my $help = $self->call('system.methodHelp', $cmd);
	if(ref($help) eq '') {
		$intro->{'help'} = $help;
	} else {
		print "system.methodHelp($cmd) returned a " . ref($help) . "!\n";
	}

	return $intro;
}


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

	my $intro = $self->call('system.introspection', $cmd);
	unless(ref($intro) eq 'HASH') {
		print "system.introspection($cmd) did not return a hash!\n";
	}

	return $intro;
}


# returns the number of new commands loaded
sub calc_command_set
{
	my $self = shift;
	# rest of args are names of commands to load

	my $verb = $self->verb;
	my $cset = $self->{commands} || {};
	$self->{commands} = $cset;

	my $cmds;
	if(@_) {
		# commands to load are specified by caller
		$cmds = [@_];
	} else {
		# get a list of all commands using listMethods
		$cmds = $self->call('system.listMethods');
		unless(ref($cmds) eq 'ARRAY') {
			print "system.listMethods did not return an array!\n";
			return {};
		}
	}

	my $loadcnt = 0;
	my $use_real = 1;
	for my $cmd (@$cmds) {
		# don't load functions we may have loaded before.
		next if exists $cset->{$cmd};

		my $t;
		if($use_real) {
			print "Trying real introspection on $cmd\n" if $verb > 6;
			$t = eval { $self->real_introspection($cmd) };
			die $@ if $@ =~ /^Interrupt/;
		}
		if(!defined($t)) {
			print "Real introspection failed, trying fake on $cmd\n" if $verb > 6;
			$t = $self->fake_introspection($cmd);
			$use_real = 0;
		}
		if(defined($t)) {
			print "Loaded $cmd\n" if $verb > 0;
			$loadcnt += 1;

			if($t->{help}) {
				my $maxdesclen = 42;
				$t->{help} =~ /^(.*)$/;
				$t->{desc} = substr $1, 0, $maxdesclen;	# first line of help
				if(length($t->{desc}) == $maxdesclen) {
					$t->{desc} =~ s/...$/\.\.\./;	# add ellipsis if it's too long.
				}
			} else {
				$t->{desc} = '(no description available)';
			}

			$t->{args} = sub { $self->args_from_sig(@_) };
			$t->{doc} = $t->{help};
			$t->{proc} = sub { $self->call_proc($cmd, @_) };

			$cset->{$cmd} = $t;
		} else {
			print "no info on $cmd\n" if $verb > 0;
		}
	}

	return $loadcnt;
}


package Connection::RPCXMLClient;

# Client object using the RPC::XML::Client

use strict;
use vars qw(@ISA);
@ISA = qw(Connection::XMLRPC);


# returns the object if it was created, or an error string if not.

sub new
{
	require RPC::XML::Client;

	my $type = shift;
	my $url = shift;

	my $self = {};
	bless $self, $type;

	my $cli = RPC::XML::Client->new($url);
	unless(ref $cli) {
		print "$cli\n";
		return undef;
	}

	$self->{cli} = $cli;
	return $self;
}


sub get_url
{
	my $self = shift;
	return $self->{cli}->uri()->as_string();
}


sub call
{
	my $self = shift;
	my $verb = $self->verb;

	print "Calling: [" . join("], [", @_) . "]\n" if $verb > 2;
	my $resp = $self->{cli}->send_request(@_);
	if(ref($resp)) {
		if($resp->is_fault()) {
			die $resp->code() . ": " . $resp->string() . "\n";
		}
		my $val = $resp->value();
		print "Received: " . $self->summarize($val) if $verb > 3;
		if($verb > 5) {
			require Data::Dumper;
			print Data::Dumper->Dump([$val], ['Result']);
		}
		return $val;
	}

	die defined($resp) && length($resp) ? $resp : "Unknown RPC::XML error!\n"
}


1;

