JOAP::Proxy - Base class for client-side proxies of JOAP objects


JOAP documentation Contained in the JOAP distribution.

Index


Code Index:

NAME

Top

JOAP::Proxy - Base class for client-side proxies of JOAP objects

SYNOPSIS

Top

  use Net::Jabber qw(Client);
  use JOAP::Proxy;

  # set up a Net::Jabber connection (your responsibility)

  sub jabber_con {

    my $user = shift;
    my $server = shift;
    my $password = shift;
    my $port = shift || 5222;
    my $resource = shift || 'JOAPProxy';

    if (!$user || !$server || !$password) {
        return undef;
    }

    my $con = new Net::Jabber::Client;

    my $status = $con->Connect(hostname => $server,
                               port => $port);

    if (!(defined($status))) {
        return undef;
    }

    my @result = $con->AuthSend(username => $user,
                                password => $password,
                                resource => $resource);

    if ($result[0] ne "ok") {
        $con->Disconnect;
        return undef;
    }

    $con->RosterGet();
    $con->PresenceSend(priority => 0);

    return $con;
  }

  my $con = jabber_con('me', 'example.com', 'very secret') ||
    die("Can't connect.");

  # Make that available to all proxy objects

  JOAP::Proxy->Connection($con);

ABSTRACT

Top

This is an abstract base class for local, client-side proxies to remote JOAP objects -- object servers, classes, and instances. It provides some default behavior for subclasses, and contains a class variable for Jabber connectivity, but otherwise it should not be used directly.

DESCRIPTION

Top

All of the classes that proxy for remote JOAP objects are subclasses of JOAP::Proxy. This package defines a lot of common behavior, but almost none of it should be used directly. Consequently, the appropriate methods and such are documented in the subclasses; see below for links to these classes.

Class Methods

There's really only one method in this package that you should care about, and that's the one to set up the Jabber connection used by all the proxies.

Connection
Connection($con)

This is the Jabber connection used to send and receive messages about proxy information. You should use this method as a mutator in your programs as early as possible, and definitely before using any of the other proxy classes.

The argument $con is a Net::Jabber::Protocol object. It can be a Net::Jabber::Client object or a Net::Jabber::Component object, and possibly even a Net::Jabber::Server object if you're adventurous.

The synopsis above has a good example of setting up the connection. You should avoid setting any callbacks on the connection, or at least setting any that interfere with JOAP's namespace or the 'jabber:iq:rpc' namespace.

This is a translucent, inheritable class data accessor. What that means is that you can, in theory, set the Connection class attribute for subclasses of this package, and it will only affect that subclass. For example:

  package FooProxy;
  use JOAP::Proxy::Package::Class;
  use base qw(JOAP::Proxy::Package::Class);

  FooProxy->Address('Foo@bar.example.com'); # say what class this is proxying for

  package main;

  # set up the connection for all proxies

  my $con1 = jabber_con('me', 'example.com', 'very secret') ||
    die("Can't connect.");

  JOAP::Proxy->Connection($con1);

  # set up a different connection just for foo proxies

  my $con2 = jabber_con('you', 'example.net', 'also secret') ||
    die("Can't connect.");

  JOAP::Proxy->Connection($con2);

This is pretty untested, and I wouldn't rely on it too much if I were you. But the possibility is there to use different connections for different proxy packages.

EXPORT

Top

None by default.

SEE ALSO

Top

You can create a proxy for a remote JOAP object server using JOAP::Proxy::Package::Server (preferred) or JOAP::Proxy::Server.

You can create a proxy for a remote JOAP class using JOAP::Proxy::Package::Class (preferred) or JOAP::Proxy::Class.

You can create a proxy for a remote JOAP instance using JOAP::Proxy::Package::Class (preferred) or JOAP::Proxy::Instance.

JOAP has more general JOAP information, as well as bug-reporting and contact information.

Net::Jabber has more information on setting up, and tearing down, Jabber connections.

AUTHOR

Top

Evan Prodromou <evan@prodromou.san-francisco.ca.us>

COPYRIGHT AND LICENSE

Top


JOAP documentation Contained in the JOAP distribution.

# JOAP::Proxy -- Base Class for Things JOAP Clients Use
#
# Copyright (c) 2003, Evan Prodromou <evan@prodromou.san-francisco.ca.us>
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA

# tag: JOAP client object base class

package JOAP::Proxy;
use base qw/Exporter Class::Data::Inheritable/;

use 5.008;
use strict;
use warnings;
use Net::Jabber qw/Client/;
use JOAP;
use Error qw(:try);
use Symbol;
use JOAP::Proxy::Error;

our %EXPORT_TAGS = ( 'all' => [ qw// ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = qw//;

our $VERSION = $JOAP::VERSION;
our $AUTOLOAD;

JOAP::Proxy->mk_classdata('Connection');

sub get {

    my $proto = shift;
    my $pkg = ref($proto) || $proto;
    my $address = shift;
    my $self = bless({_address => $address}, $pkg);

    $self->_read();

    return $self;
}

sub refresh {

    my $self = shift;

    # XXX: anything else?

    return $self->_read;
}

sub save {

    my $self = shift;

    # XXX: anything else?

    return $self->_edit;
}

sub address {
    my $self = shift;
    return $self->{_address};
}

sub timestamp {
    my $self = shift;
    return $self->{_timestamp};
}

sub _set_timestamp {
    my $self = shift;
    return $self->{_timestamp} = shift;
}

sub description {
    my $self = shift;
    return $self->{_description};
}

sub _set_description {
    my $self = shift;
    return $self->{_description} = shift;
}

sub attributes {
    my $self = shift;
    return (@_) ? $self->{_attributes} = shift : $self->{_attributes};
}

sub methods {
    my $self = shift;
    return (@_) ? $self->{_methods} = shift : $self->{_methods};
}

sub _attribute_descriptor {
    my $self = shift;
    my $name = shift;
    return $self->attributes->{$name};
}

sub _method_descriptor {
    my $self = shift;
    my $name = shift;
    return $self->methods->{$name};
}

sub _described {
    my $self = shift;
    return $self->timestamp;
}

sub _read {

    my $self = shift;
    my $con = $self->Connection;

    if (!$con) {
	throw JOAP::Proxy::Error::Local("No JOAP proxy connection set.");
    }

    if (!$self->_described) {
        $self->_describe;
    }

    my $iq = new Net::Jabber::IQ;

    $iq->SetTo($self->address);
    $iq->SetType('get');
    $iq->NewQuery($JOAP::NS, 'read');

    # XXX: configure to allow reading just some attributes

    my $resp = $con->SendAndReceiveWithID($iq);

    if ($resp->GetType eq 'error') {
	my $code = $resp->GetErrorCode;
	my $text = $resp->GetError;
	throw JOAP::Proxy::Error::Remote($text, $code);
    }

    my $read = $resp->GetQuery;

    my @attrs = $read->GetAttribute;

    foreach my $attr (@attrs) {
        my $name = $attr->GetName;
        # XXX: check returned attributes for type
        my $value = JOAP->decode($attr->GetValue);
        $self->_set($name, $value);
    }

    # FIXME: what should we return?

    return $resp;
}

sub _set {

    my $self = shift;
    my $name = shift;
    my $value = shift;

    $self->{$name} = $value;
}

sub _edit {

    my $self = shift;
    my $con = $self->Connection;

    if (!$con) {
	throw JOAP::Proxy::Error::Local("No JOAP proxy connection set.");
    }

    if (!$self->_described) {
        $self->_describe;
    }

    my $iq = new Net::Jabber::IQ;

    $iq->SetTo($self->address);
    $iq->SetType('set');
    my $edit = $iq->NewQuery($JOAP::NS, 'edit');

    my $attrs = $self->_default_edit_attrs();

    while (my($name, $descriptor) = each %$attrs) {
        no strict 'refs';
        my $loc = $self->$name;
        use strict 'refs';
        my $tval = JOAP->encode($descriptor->{type}, $loc);
        my $val = $edit->AddAttribute(name => $name)->AddValue;
        JOAP->copy_value($tval, $val);
    }

    my $resp = $con->SendAndReceiveWithID($iq);

    if ($resp->GetType eq 'error') {
	throw JOAP::Proxy::Error::Remote($resp->GetError, $resp->GetErrorCode);
    }

    return $resp;
}

sub _default_edit_attrs {

    my $self = shift;

    my $attrs = $self->attributes;

    # find names of writable attributes

    my @writable = grep { $attrs->{$_}->{writable} } keys %$attrs;

    # make that into a hash

    my %write = map {($_, $attrs->{$_})} @writable;

    # return a reference to that hash

    return \%write;
}

sub _describe {

    my $self = shift;
    my $con = $self->Connection;

    if (!$con) {
	throw JOAP::Proxy::Error::Local("No JOAP proxy connection set.");
    }

    my $iq = new Net::Jabber::IQ;

    $iq->SetTo($self->address);
    $iq->SetType('get');
    $iq->NewQuery($JOAP::NS, 'describe');

    my $resp = $con->SendAndReceiveWithID($iq);

    if ($resp->GetType eq 'error') {
	throw JOAP::Proxy::Error::Remote($resp->GetError, $resp->GetErrorCode);
    }

    my $desc = $resp->GetQuery;

    # FIXME: handle multiple descriptions

    if ($desc->DefinedDesc) {
        $self->_set_description($desc->GetDesc);
    }

    my $attrs = {};

    my @attrdescs = $desc->GetAttributeDescription;

    foreach my $attrdesc (@attrdescs) {

        my $name = $attrdesc->GetName;
        my $type = $attrdesc->GetType;
        my $required = $attrdesc->GetRequired || 0;
        my $writable = $attrdesc->GetWritable || 0;
        my $allocation = $attrdesc->GetAllocation || 'instance';
        my $desc = $attrdesc->GetDesc || '';

	$attrs->{$attrdesc->GetName} = {name => $name,
                                        type => $type,
                                        required => $required,
                                        writable => $writable,
                                        allocation => $allocation,
                                        desc => $desc};
    }

    $self->attributes($attrs);

    my $meths = {};

    my @methdescs = $desc->GetMethodDescription;

    foreach my $methdesc (@methdescs) {
	$meths->{$methdesc->GetName} = {name => $methdesc->GetName,
                                        returnType => $methdesc->GetReturnType,
                                        allocation => $methdesc->GetAllocation,
                                        desc => $methdesc->GetDesc};

        my $params = [];
        my @params = $methdesc->GetParams->GetParams;

        foreach my $param (@params) {
            push @$params, {name => $param->GetName,
                            type => $param->GetType,
                            desc => $param->GetDesc};
        }

        $meths->{$methdesc->GetName}->{params} = $params;
    }

    $self->methods($meths);

    # save the timestamp

    $self->_set_timestamp($desc->GetTimestamp);

    return $resp;
}

# This allows us to say $self->can('autoloadedmethod'). AUTOLOAD (below)
# uses this method to create methods if necessary.

sub can {

    my($self) = shift;
    my($name) = shift;
    my($func) = $self->SUPER::can($name); # See if it's findable by standard lookup.

    if (!$func) { # if not, see if it's something we should make ourselves.
	if (my $methdesc = $self->_method_descriptor($name)) {
            $func = $self->_proxy_method($methdesc);
	} elsif (my $attrdesc = $self->_attribute_descriptor($name)) {
            $func = $self->_proxy_accessor($attrdesc);
        }
    }

    return $func;
}

sub _proxy_method {

    my $self = shift;
    my $methdesc = shift;

    my @param_types = map { $_->{type} } @{$methdesc->{params}};
    my $param_cnt = scalar(@param_types);

    my $name = $methdesc->{name};

    return sub {

        my $self = shift;

        my $con = $self->Connection || throw JOAP::Proxy::Error::Local("Can't call remote method if not connected.");

        my @args = @_;

        # XXX: allow named parameters if scalar(@args) == $param_cnt * 2

        if (scalar(@args) != $param_cnt) {
            throw JOAP::Proxy::Error::Local("Wrong number of parameters (need $param_cnt) for method '$name'.");
        }

        my $iq = new Net::Jabber::IQ;
        $iq->SetIQ(to => $self->address, type => 'set');

        my $mc = $iq->NewQuery('jabber:iq:rpc')->AddMethodCall;

        $mc->SetMethodName($name);

        my $params = $mc->AddParams;

        my $i;

        for ($i = 0; $i < $param_cnt; $i++) {
            my $pv = $params->AddParam->AddValue;
            my $tv = JOAP->encode($param_types[$i], $args[$i]);
            JOAP->copy_value($tv, $pv);
        }

        my $resp = $con->SendAndReceiveWithID($iq);

        if ($resp->GetType eq 'error') {
            throw JOAP::Proxy::Error::Remote($resp->GetError, $resp->GetErrorCode);
        }

        my $mr = $resp->GetQuery->GetMethodResponse;

        if ($mr->DefinedFault) {

            my $struct = $mr->GetFault->GetValue->GetStruct;
            my ($code, $text);

            foreach my $member ($struct->GetMembers()) {
                if ($member->GetName eq 'faultCode') {
                    $code = JOAP->decode($member->GetValue);
                } elsif ($member->GetName eq 'faultString'){
                    $text = JOAP->decode($member->GetValue);
                }
            }

            throw JOAP::Proxy::Error::Fault($text, $code);

        } else {
            # FIXME: check return type
            my @results = map { JOAP->decode($_->GetValue) } $mr->GetParams->GetParams;
            return @results;
        }
    };
}

sub _proxy_accessor {

    my $self = shift;
    my $descriptor = shift;

    my $name = $descriptor->{name};
    my $writable = $descriptor->{writable};
    my $type = $descriptor->{type};

    my $func = undef;

    if ($writable) {
        $func = sub {
            my $self = shift;
            return (@_) ? $self->{$name} = JOAP->coerce($type, shift) : $self->{$name};
        };
    } else {
        $func = sub {
            my $self = shift;
            if (@_) {
                throw JOAP::Proxy::Error::Local("Can't modify read-only attribute $name.");
            }
            return $self->{$name};
        };
    }

    return $func;
}

sub AUTOLOAD {

    my ($self) = $_[0];
    my ($sub) = $AUTOLOAD;

    my ($pkg,$name) = ($sub =~ /(.*)::([^:]+)$/);
    my ($func) = $self->can($name);

    if ($func) {
        &$func(@_);
    } else {
        throw JOAP::Proxy::Error::Local("No attribute or method '$name'");
    }
}

# skip autoload hoohaw for DESTROY

sub DESTROY { }

1; # of these days, Alice

__END__