package POE::Component::Client::SOAP; use warnings; use strict; our @EXPORT = ( ); use Switch; use Carp qw(croak); use POE; use POE::Component::Generic; use base qw(Exporter); use vars qw($VERSION); $VERSION = '0.01'; use POE::Component::Client::SOAP::Nonblock; #my @reconnections = qw(60 120 240 480 960 1920 3840); my @reconnections = qw(1 2 3); #TODO: Take a look at Proc::BackOff our $soap_config; #lexical scope sub spawn { my $package = shift; croak "$package requires an even number of parameters" if @_ & 1; my %args = @_; my $self = bless ({}, $package); $args{Alias} = 'soap-client' unless $args{Alias}; croak "$package missing SOAP proxy" unless $args{proxy}; croak "$package missing SOAP service" unless $args{service}; $args{retry_reconnect} = 0 unless $args{retry_reconnect}; $self->{CONFIG} = \%args; $soap_config = $self->{CONFIG}; #lexical scope $self->{count} = scalar(@reconnections); $self->{attempts} = 0; POE::Session->create( object_states => [ $self => { _start => '_client_start', _stop => '_client_stop', shutdown => '_client_close', reconnect => '_server_connect', }, $self => [ qw( child_sig _server_connect _client_close _pocog_error _soap_response _soap_result _handle_error handle_result handle_error handle_connected) ], ], (ref $args{options} eq 'HASH' ? (options => $args{options}) : () ), ); return $self; } # only for debug sub child_sig { my ($kernel, $self) = @_[KERNEL, OBJECT]; $self->log($kernel, 'debug', "CHLD:".$_[ARG1]); } sub _client_start { my ($kernel, $self) = @_[KERNEL, OBJECT]; $self->log($kernel, 'debug', '_client_start()'); $kernel->alias_set($self->config('Alias')); $kernel->sig(CHLD => 'child_sig'); $kernel->yield('_server_connect'); } sub _client_stop { my ($kernel, $self) = @_[KERNEL, OBJECT]; $self->log($kernel, 'debug', '_client_stop()'); } sub _client_close { my ($kernel, $self) = @_[KERNEL, OBJECT]; $self->log($kernel, 'debug', '_client_close()'); $self->handle_shutdown($kernel); $kernel->alias_remove($self->config('Alias')); } sub _server_connect { my ($kernel, $self) = @_[KERNEL, OBJECT]; $self->log($kernel, 'debug', '_server_connect()'); # TODO: trusting PoCo::Generic not to leak PIDs. This needs further # revision. Don't know what PoCo:Generic does with dead childs and # how to handle them. unless(defined $self->{soap} and defined $self->{soap}->{litex}){ $self->{soap}->{litex} = undef; $self->{soap} = undef; my $soap = POE::Component::Client::SOAP::Nonblock->new($self->{CONFIG}); if (defined $soap) { $self->{soap} = $soap; } else { $self->_reconnect($kernel); } } $kernel->yield('handle_connected'); $self->{please_wait} = 0; } # log any poco generic errors sub _pocog_error { my( $kernel, $self, $session, $err ) = @_[ KERNEL, OBJECT, SESSION, ARG0 ]; my $errstr = undef; unless($err->{stderr}){ my $op = $err->{operation}?$err->{operation}:'undef'; my $en = $err->{errnum}?$err->{errnum}:'undef'; my $es = $err->{errstr}?$err->{errstr}:'undef'; $errstr = qq|OP:$op EN:$en ES:$es|; $self->log($kernel, 'error', "_pocog_error() wheel error: $errstr"); $self->{pocog_error} = 1; } } sub _reconnect { my ($self, $kernel) = @_; $self->log($kernel, 'debug', "Attempts: $self->{attempts}, Count: $self->{count}"); if ($self->{attempts} < $self->{count}) { my $delay = $reconnections[$self->{attempts}]; $self->log($kernel, 'warning', "Attempting reconnection: $self->{attempts}, waiting: $delay seconds"); $self->{attempts}++; $kernel->delay('reconnect', $delay); } else { if ($self->config('retry_reconnect')) { $self->log($kernel, 'warning', 'Cycling reconnection attempts, but not shutting down...'); $self->{attempts} = 0; $kernel->yield('reconnect'); } else { $self->log($kernel, 'warning', 'Shutting down, too many reconnection attempts'); $kernel->yield('shutdown'); } } } sub _soap_response { my ($kernel, $self, $resp, $pobj) = @_[ KERNEL, OBJECT, ARG0, ARG1]; $self->log($kernel, 'debug', '_soap_response'); my $error = undef; # error was probably because of a faulty SOAP call... if($self->{pocog_error}){ $error = 'Faulty PoCo::Generic Object'; } do{ if ($pobj->{package} ne '') { eval { $pobj->result({ session => $self->config('Alias'), event => '_soap_result', data => $resp->{factory}, }); }; $error = $@ if $@; } else { $error = 'soap response has no package'; } } unless $error; if ($error) { $self->log($kernel, 'warning', "_soap_response() exception in result(): $error"); $kernel->yield('_handle_error', $error); } } sub _soap_result { my ($kernel, $self, $resp, $pobj) = @_[ KERNEL, OBJECT, ARG0, ARG1]; $self->log($kernel, 'debug', '_soap_result'); # return the object as returned by SOAP if (defined $pobj) { # pobj is the data, resp->{data} is the SOAP call name $kernel->yield('handle_result', [$resp->{data},$pobj]); } else { my $error = 'SOAP result returned invalid object'; $self->log($kernel, 'warning', "_soap_result() exception in result(): $error"); $kernel->yield('_handle_error', $error); } } sub AUTOLOAD { my $self = shift; our $AUTOLOAD; return undef if $self->{soap}->{fail}; if ( $AUTOLOAD =~ m/::(\w+)$/ ) { unless($1 eq 'DESTROY' or $self->{please_wait}){ eval q|$self->{soap}->{litex}->|.$1.q|( {session => $self->config('Alias'), event => '_soap_response'}, @_)|; if ($@) { $self->log($poe_kernel, 'warning', "exception in main autoload: $@"); $poe_kernel->yield('_handle_error', $@); } } } } sub _handle_error { my ($kernel, $self, $error) = @_[KERNEL, OBJECT, ARG0]; $self->{please_wait} = 1; $self->log($kernel, 'error', 'Error in service. Reset, and attempting to reconnect...'); $kernel->post($self->config('Alias'), 'handle_error', ['-ERROR-', $error]); $self->{pocog_error} = undef; $self->_reconnect($kernel); } # --------------------------------------------------------------------- # Public accessors # --------------------------------------------------------------------- sub config { my ($self, $arg) = @_; return $self->{CONFIG}->{$arg}; } # --------------------------------------------------------------------- # Public methods, these should be overridden, as needed # --------------------------------------------------------------------- sub log { my ($self, $kernel, $level, $message) = @_; } sub handle_result { my ($kernel, $self, $frame) = @_[KERNEL, OBJECT, ARG0]; } sub handle_error { my ($kernel, $self, $frame) = @_[KERNEL, OBJECT, ARG0]; } sub handle_shutdown { my ($self, $kernel) = @_; } sub handle_connected { my ($kernel, $self) = @_[KERNEL, OBJECT]; } ###################################################################### # # # POE::Component::Client::SOAP::Nonblock # # # ###################################################################### package POE::Component::Client::SOAP::Nonblock; use warnings; use strict; our @EXPORT = ( ); use Switch; use Carp qw(croak); use POE; use POE::Component::Generic; use base qw(Exporter); use vars qw($VERSION); $VERSION = '0.01'; use SOAP::Lite; use Devel::Symdump; sub new { my $package = shift; my $params = shift; my $self = bless ({}, $package); my $soap = undef; eval{ $soap = SOAP::Lite->new( proxy => $soap_config->{proxy}, service => $soap_config->{service}, ); }; return undef if $@ or !defined($soap); croak "Bad SOAP Service (non valid URI):".$soap->{_service} unless ($soap->{_service} =~ /^\w+\:\/\/\w+.*$/); # all methods from all services are mapped my @services = keys %{$soap->{_schema}->{_services}}; my @soap_methods = ( ); foreach my $service (@services) { foreach my $soap_method (keys %{$soap->{_schema}->{_services}->{$service}}) { push @soap_methods, $soap_method; } } # defined methods my $symdump = Devel::Symdump->new(qw(POE::Component::Client::SOAP::LiteX)); my @defined_methods = $symdump->functions; # map all to the LiteX namespace foreach my $soap_method (@soap_methods) { unless($self->_find_symbol($soap_method, \@defined_methods)){ eval q| sub POE::Component::Client::SOAP::LiteX::|.$soap_method.q| { my $self = shift; my $call = undef; eval { $call = $self->{soap_lite}->|.$soap_method.q|(@_); }; if($@){ warn $@; return undef; } else { return $call; } }|; warn "Error mapping SOAP method to LiteX: $@" if $@; } } # now PoCo::Generic thinks they are real ;-) $soap = POE::Component::Generic->spawn( alias => 'soapgeneric', package => 'POE::Component::Client::SOAP::LiteX', object_options => [ proxy => $params->{proxy}, service => $params->{service}, ], factories => \@soap_methods, error => '_pocog_error', debug => $params->{debug}?$params->{debug}:0, verbose => $params->{verbose}?$params->{verbose}:0, ); $self->{litex} = $soap?$soap:undef; return $soap?$self:undef; } sub _find_symbol { my ($self, $symbol, $where) = @_; foreach my $s (@$where) { $s =~ m/::(\w+)$/; return 1 if $symbol eq $1; } return 0; } ###################################################################### # # # POE::Component::Client::SOAP::LiteX # # # ###################################################################### package POE::Component::Client::SOAP::LiteX; use warnings; use strict; our @EXPORT = ( ); use Carp qw(croak); use base qw(Exporter); use vars qw($VERSION); $VERSION = '0.01'; use SOAP::Lite; sub new { my $package = shift; croak "$package requires an even number of parameters" if @_ & 1; my @args = @_; my $self = bless ({}, $package); my $soap_lite = SOAP::Lite->new(@args); return undef unless $soap_lite; $self->{soap_lite} = $soap_lite; return $self; } # Trap method not defined in the WSDL sub AUTOLOAD { my $self = shift; our $AUTOLOAD; if ( $AUTOLOAD =~ m/::(\w+)$/ ) { unless($1 eq 'DESTROY'){ warn "Sorry, cannot do: $1 in LiteX, since it's not defined in the WSDL."; } } } 1; __END__ =head1 NAME POE::Component::Client::SOAP =head1 SYNOPSIS # (1) Derive you own client class package My::SOAP; use base qw(POE::Component::Client::SOAP); sub spawn{ my $package = shift; my $proxy = shift; my $service = shift; my $self = $class->SUPER::spawn( retry_reconnect => 1, proxy => $proxy, service => $service, ); # initialized flag $self->{soc_stat} = 0; return $self; } # override connected method for status, for example sub handle_connected { my ($kernel, $self, $frame) = @_[KERNEL,OBJECT,ARG0]; # marks stomp client as initialized $self->{soc_stat} = 1; } # this must be overriden; it's the result from the call sub handle_result { my ($kernel, $self, $result) = @_[KERNEL,OBJECT,ARG0]; # post to your session, or do whatever with SOAP result.... $kernel->post($your_session, $call_back_event, $result); # result contains the SOAP call name and data } # usually overriden sub log { my ($self, $kernel, $level, $message) = @_; } sub handle_error { my ($kernel, $self, $result) = @_[KERNEL,OBJECT,ARG0]; $self->{soc_stat} = 0; $kernel->post($your_session,$error_call_back_event, $result); # result contains the SOAP call name and error data } # (2) use it in the actual implementation use My::SOAP; my $soap = My::SOAP->spawn( proxy => 'http://your.soap.server', service = > 'http://your.soap.server/service.wsdl', ) $soap->soapCall(params); ... sub call_back_event_handler{ my ( $self, $kernel, $session, $soc_ret ) = @_[ OBJECT, KERNEL, SESSION, ARG0 ]; $call = $soc_ret->[0]; # which SOAP call is returning $data = $soc_ret->[1]; # the data of the SOAP return } =head1 DESCRIPTION This module is a non-blocking wrapper around SOAP::Lite for POE. As any non-blocking wrapper it needs to spawn a dedicated process to deal with the blocking SOAP calls. As with most non-blocking wrappers we used PoCo::Generic for abstracting all the details of the dedicated process and it's events. Nevertheless, because we use PoCo::Generic, it is mandatory to know all the SOAP methods beforehand, hence the need to require a service descriptor (WSDL) to work with this library. If you have a particular need to use SOAP WITHOUT WSDL, please drop a line a we'll see what we can do to help you. =head1 SEE ALSO L L L L L =head1 AUTHOR Alejandro Imass Alejandro Imass =head1 COPYRIGHT AND LICENSE Copyright (C) 2008 by Alejandro Imass / Corcaribe TecnologĂ­a C.A. for the P2EE Project This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.8 or, at your option, any later version of Perl 5 you may have available. =cut