File Coverage

blib/lib/RPC/XML/Client.pm
Criterion Covered Total %
statement 77 238 32.3
branch 10 104 9.6
condition 4 46 8.7
subroutine 18 33 54.5
pod 5 5 100.0
total 114 426 26.7


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # This file copyright (c) 2001-2014 Randy J. Ray, all rights reserved
4             #
5             # Copying and distribution are permitted under the terms of the Artistic
6             # License 2.0 (http://www.opensource.org/licenses/artistic-license-2.0.php) or
7             # the GNU LGPL (http://www.opensource.org/licenses/lgpl-2.1.php).
8             #
9             ###############################################################################
10             #
11             # Description: This class implements an RPC::XML client, using LWP to
12             # manage the underlying communication protocols. It relies
13             # on the RPC::XML transaction core for data management.
14             #
15             # Functions: new
16             # send_request
17             # simple_request
18             # uri
19             # useragent
20             # request
21             #
22             # Libraries: LWP::UserAgent
23             # HTTP::Request
24             # URI
25             # RPC::XML
26             # RPC::XML::ParserFactory
27             # Compress::Raw::Zlib is used if available
28             #
29             # Global Consts: $VERSION
30             #
31             ###############################################################################
32              
33             package RPC::XML::Client;
34              
35 4     4   2885 use 5.008008;
  4         14  
36 4     4   16 use strict;
  4         5  
  4         71  
37 4     4   14 use warnings;
  4         4  
  4         107  
38 4     4   12 use vars qw($VERSION $COMPRESSION_AVAILABLE);
  4         4  
  4         215  
39 4         24 use subs qw(new simple_request send_request uri useragent request
40 4     4   15 fault_handler error_handler combined_handler timeout);
  4         7  
41              
42 4     4   294 use Scalar::Util 'blessed';
  4         6  
  4         167  
43 4     4   687 use File::Temp;
  4         12832  
  4         287  
44 4     4   20 use IO::Handle;
  4         4  
  4         196  
45 4     4   15 use Module::Load;
  4         6  
  4         28  
46              
47 4     4   2136 use LWP::UserAgent;
  4         57088  
  4         139  
48 4     4   26 use HTTP::Request;
  4         6  
  4         107  
49 4     4   17 use URI;
  4         5  
  4         89  
50              
51 4     4   17 use RPC::XML;
  4         7  
  4         180  
52 4     4   727 use RPC::XML::ParserFactory;
  4         5  
  4         29  
53              
54             BEGIN
55             {
56             # Check for compression support
57             $COMPRESSION_AVAILABLE =
58 4 50   4   9 (eval { load Compress::Zlib; 1; }) ? 'deflate' : q{};
  4         17  
  4         49554  
59             }
60              
61             $VERSION = '1.42';
62             $VERSION = eval $VERSION; ## no critic (ProhibitStringyEval)
63              
64             ###############################################################################
65             #
66             # Sub Name: new
67             #
68             # Description: Create a LWP::UA instance and add some extra material
69             # specific to our purposes.
70             #
71             # Arguments: NAME IN/OUT TYPE DESCRIPTION
72             # $class in scalar Class to bless into
73             # $location in scalar URI path for requests to go to
74             # %attrs in hash Extra info
75             #
76             # Globals: $VERSION
77             #
78             # Returns: Success: object reference
79             # Failure: error string
80             #
81             ###############################################################################
82             sub new
83             {
84 2     2   12336 my ($class, $location, %attrs) = @_;
85              
86 2   33     12 $class = ref($class) || $class;
87 2 100       4 if (! $location)
88             {
89 1         3 return "${class}::new: Missing location argument";
90             }
91              
92 1         2 my ($self, $UA, $REQ);
93              
94             # Start by getting the LWP::UA object
95             $UA = LWP::UserAgent->new(
96 1 50       10 (exists $attrs{useragent}) ? @{$attrs{useragent}} : ()
  0         0  
97             );
98 1         1899 $UA->agent(sprintf '%s/%s %s', $class, $VERSION, $UA->agent);
99 1         70 $self->{__useragent} = $UA;
100 1         1 delete $attrs{useragent};
101              
102             # Next get the request object for later use
103 1         7 $REQ = HTTP::Request->new(POST => $location);
104 1         4973 $self->{__request} = $REQ;
105 1         11 $REQ->header(Content_Type => 'text/xml');
106 1         55 $REQ->protocol('HTTP/1.0');
107              
108             # Note compression support
109 1         8 $self->{__compress} = $COMPRESSION_AVAILABLE;
110             # It looks wasteful to keep using the hash key, but it makes it easier
111             # to change the string in just one place (BEGIN block, above) if I have to.
112             # Also (for now) I prefer to manipulate the private keys directly, before
113             # blessing $self, rather than using accessors. This is just for performance
114             # and I might change my mind later.
115 1 50       3 if ($self->{__compress})
116             {
117 1         4 $REQ->header(Accept_Encoding => $self->{__compress});
118             }
119 1   50     30 $self->{__compress_thresh} = $attrs{compress_thresh} || 4096;
120 1         10 $self->{__compress_re} = qr/$self->{__compress}/;
121             # They can change this value with a method
122 1         2 $self->{__compress_requests} = 0;
123 1         2 delete $attrs{compress_thresh};
124              
125             # Parameters to control the point at which messages are shunted to temp
126             # files due to size, and where to home the temp files. Start with a size
127             # threshhold of 1Meg and no specific dir (which will fall-through to the
128             # tmpdir() method of File::Spec).
129 1   50     4 $self->{__message_file_thresh} = $attrs{message_file_thresh} || 1_048_576;
130 1   50     5 $self->{__message_temp_dir} = $attrs{message_temp_dir} || q{};
131 1         3 delete @attrs{qw(message_file_thresh message_temp_dir)};
132              
133             # Note and preserve any error or fault handlers. Check the combo-handler
134             # first, as it is superceded by anything more specific.
135 1 50       4 if (ref $attrs{combined_handler})
136             {
137 0         0 $self->{__error_cb} = $attrs{combined_handler};
138 0         0 $self->{__fault_cb} = $attrs{combined_handler};
139 0         0 delete $attrs{combined_handler};
140             }
141 1 50       3 if (ref $attrs{fault_handler})
142             {
143 0         0 $self->{__fault_cb} = $attrs{fault_handler};
144 0         0 delete $attrs{fault_handler};
145             }
146 1 50       2 if (ref $attrs{error_handler})
147             {
148 0         0 $self->{__error_cb} = $attrs{error_handler};
149 0         0 delete $attrs{error_handler};
150             }
151              
152             # Get the RPC::XML::Parser instance from the ParserFactory
153             $self->{__parser} =
154 1 50       10 RPC::XML::ParserFactory->new($attrs{parser} ? @{$attrs{parser}} : ())
  0 50          
155             or return "${class}::new: Unable to get RPC::XML::Parser object";
156 0           delete $attrs{parser};
157              
158             # Now preserve any remaining attributes passed in
159 0           for (keys %attrs)
160             {
161 0           $self->{$_} = $attrs{$_};
162             }
163              
164 0           return bless $self, $class;
165             }
166              
167             ###############################################################################
168             #
169             # Sub Name: simple_request
170             #
171             # Description: Simplify the request process by both allowing for direct
172             # data on the incoming side, and for returning a native
173             # value rather than an object reference.
174             #
175             # Arguments: NAME IN/OUT TYPE DESCRIPTION
176             # $self in ref Class instance
177             # @args in list Various args -- see comments
178             #
179             # Globals: $RPC::XML::ERROR
180             #
181             # Returns: Success: value
182             # Failure: undef, error in $RPC::XML::ERROR
183             #
184             ###############################################################################
185             sub simple_request
186             {
187 0     0     my ($self, @args) = @_;
188              
189 0           my $return;
190              
191 0           $RPC::XML::ERROR = q{};
192              
193 0           $return = $self->send_request(@args);
194 0 0         if (! ref $return)
195             {
196 0           $RPC::XML::ERROR = ref($self) . "::simple_request: $return";
197 0           return;
198             }
199              
200 0           return $return->value;
201             }
202              
203             ###############################################################################
204             #
205             # Sub Name: send_request
206             #
207             # Description: Take a RPC::XML::request object, dispatch a request, and
208             # parse the response. The return value should be a
209             # RPC::XML::response object, or an error string.
210             #
211             # Arguments: NAME IN/OUT TYPE DESCRIPTION
212             # $self in ref Class instance
213             # $req in ref RPC::XML::request object or
214             # remote method name
215             # @args in list If $req is a method name, these
216             # are potential arguments for
217             # the remote call
218             #
219             # Returns: Success: RPC::XML::response object instance
220             # Failure: error string
221             #
222             ###############################################################################
223             sub send_request ## no critic (ProhibitExcessComplexity)
224             {
225 0     0     my ($self, $req, @args) = @_;
226              
227 0           my ($me, $message, $response, $reqclone, $content, $can_compress, $value,
228             $do_compress, $req_fh, $tmpdir, $com_engine);
229              
230 0           $me = ref($self) . '::send_request';
231              
232 0 0 0       if (! $req)
    0          
233             {
234 0           return "$me: No request object or remote method name given";
235             }
236             elsif (! (blessed $req and $req->isa('RPC::XML::request')))
237             {
238             # Assume that $req is the name of the routine to be called
239 0 0         if (! ($req = RPC::XML::request->new($req, @args)))
240             {
241 0           return "$me: Error creating RPC::XML::request object: " .
242             $RPC::XML::ERROR;
243             }
244             }
245              
246             # Start by setting up the request-clone for using in this instance
247 0           $reqclone = $self->request->clone;
248 0 0         if (! $reqclone->header('Host')) {
249 0           $reqclone->header(Host => URI->new($reqclone->uri)->host_port);
250             }
251 0           $can_compress = $self->compress; # Avoid making 4+ calls to the method
252 0 0 0       if ($self->compress_requests and $can_compress and
      0        
253             $req->length >= $self->compress_thresh)
254             {
255             # If this is a candidate for compression, set a flag and note it
256             # in the Content-encoding header.
257 0           $do_compress = 1;
258 0           $reqclone->content_encoding($can_compress);
259             }
260              
261             # Next step, determine our content's disposition. If it is above the
262             # threshhold for a requested file cut-off, send it to a temp file and use
263             # a closure on the request object to manage content.
264 0 0 0       if ($self->message_file_thresh and
265             $self->message_file_thresh <= $req->length)
266             {
267 0           require File::Spec;
268             # Start by creating a temp-file
269 0   0       $tmpdir = $self->message_temp_dir || File::Spec->tmpdir;
270             # File::Temp->new() croaks on error, rather than just returning undef
271 0           $req_fh = eval { File::Temp->new(UNLINK => 1, DIR => $tmpdir) };
  0            
272 0 0         if (! $req_fh)
273             {
274 0           return "$me: Error opening tmpfile: $@";
275             }
276 0           binmode $req_fh;
277             # Make it auto-flush
278 0           $req_fh->autoflush();
279              
280             # Now that we have it, spool the request to it. This is a little
281             # hairy, since we still have to allow for compression. And though the
282             # request could theoretically be HUGE, in order to compress we have to
283             # write it to a second temp-file first, so that we can compress it
284             # into the primary handle.
285 0 0 0       if ($do_compress && ($req->length >= $self->compress_thresh))
286             {
287 0           my $fh_compress = eval {
288 0           File::Temp->new(UNLINK => 1, DIR => $tmpdir);
289             };
290 0 0         if (! $fh_compress)
291             {
292 0           return "$me: Error opening compression tmpfile: $@";
293             }
294             # Make it auto-flush
295 0           $fh_compress->autoflush();
296              
297             # Write the request to the second FH
298 0           $req->serialize($fh_compress);
299 0           seek $fh_compress, 0, 0;
300              
301             # Spin up the compression engine
302 0           $com_engine = Compress::Zlib::deflateInit();
303 0 0         if (! $com_engine)
304             {
305 0           return "$me: Unable to initialize the Compress::Zlib engine";
306             }
307              
308             # Spool from the second FH through the compression engine, into
309             # the intended FH.
310 0           my $buf = q{};
311 0           my $out;
312 0           while (read $fh_compress, $buf, 4096)
313             {
314 0           $out = $com_engine->deflate(\$buf);
315 0 0         if (! defined $out)
316             {
317 0           return "$me: Compression failure in deflate()";
318             }
319 0           print {$req_fh} $out;
  0            
320             }
321             # Make sure we have all that's left
322 0           $out = $com_engine->flush;
323 0 0         if (! defined $out)
324             {
325 0           return "$me: Compression flush failure in deflate()";
326             }
327 0           print {$req_fh} $out;
  0            
328              
329             # Close the secondary FH. Rewinding the primary is done later.
330 0 0         if (! close $fh_compress)
331             {
332 0           return "$me: Error closing spool-file: $!";
333             }
334             }
335             else
336             {
337 0           $req->serialize($req_fh);
338             }
339 0           seek $req_fh, 0, 0;
340              
341 0           $reqclone->content_length(-s $req_fh);
342             $reqclone->content(sub {
343 0     0     my $b = q{};
344 0 0         if (! defined read $req_fh, $b, 4096)
345             {
346 0           return;
347             }
348              
349 0           return $b;
350 0           });
351             }
352             else
353             {
354             # Treat the content strictly in-memory
355 0           utf8::encode($content = $req->as_string);
356 0 0         if ($do_compress)
357             {
358 0           $content = Compress::Zlib::compress($content);
359             }
360 0           $reqclone->content($content);
361             # Because $content has been force-downgraded, length() should work
362 0           $reqclone->content_length(length $content);
363             }
364              
365             # Content used to be handled as an in-memory string. Now, to avoid eating
366             # up huge chunks due to potentially-massive messages (thanks Tellme), we
367             # parse incrementally with the XML::Parser::ExpatNB class. What's more,
368             # to use the callback-form of request(), we can't get just the headers
369             # first. We have to check things like compression and such on the fly.
370 0           my $compression;
371 0           my $parser = $self->parser->parse(); # Gets the ExpatNB object
372             my $cb = sub {
373 0     0     my ($data_in, $resp) = @_;
374              
375 0 0         if (! defined $compression)
376             {
377 0 0 0       $compression =
378             (($resp->content_encoding || q{}) =~
379             $self->compress_re) ? 1 : 0;
380 0 0 0       if ($compression and (! $can_compress))
381             {
382 0           die "$me: Compressed content encoding not supported\n";
383             }
384 0 0         if ($compression)
385             {
386 0 0         if (! ($com_engine = Compress::Zlib::inflateInit()))
387             {
388 0           die "$me: Unable to initialize de-compression engine\n";
389             }
390             }
391             }
392              
393 0 0         if ($compression)
394             {
395 0           my $error;
396 0 0         if (! (($data_in, $error) = $com_engine->inflate($data_in)))
397             {
398 0           die "$me: Error in inflate() expanding data: $error\n";
399             }
400             }
401              
402 0           $parser->parse_more($data_in);
403 0           1;
404 0           };
405              
406 0           $response = $self->useragent->request($reqclone, $cb);
407 0 0         if ($message = $response->headers->header('X-Died'))
408             {
409 0           $parser->release();
410              
411             # One of the die's was triggered
412 0 0         return ('CODE' eq ref $self->error_handler) ?
413             $self->error_handler->($message) : $message;
414             }
415 0 0         if (! $response->is_success)
416             {
417 0           $parser->release();
418              
419 0           $message = "$me: HTTP server error: " . $response->message;
420 0 0         return ('CODE' eq ref $self->error_handler) ?
421             $self->error_handler->($message) : $message;
422             }
423              
424             # Whee. No errors from the callback or the server. Finalize the parsing
425             # process.
426 0 0         if (! eval { $value = $parser->parse_done(); 1; })
  0            
  0            
427             {
428 0 0         if ($@)
429             {
430             # One of the die's was triggered
431 0 0         return ('CODE' eq ref $self->error_handler) ?
432             $self->error_handler->($@) : $@;
433             }
434             }
435              
436             # Check if there is a callback to be invoked in the case of
437             # errors or faults
438 0 0         if (! ref $value)
    0          
439             {
440 0           $message = "$me: parse-level error: $value";
441 0 0         return ('CODE' eq ref $self->error_handler) ?
442             $self->error_handler->($message) : $message;
443             }
444             elsif ($value->is_fault)
445             {
446 0 0         return ('CODE' eq ref $self->fault_handler) ?
447             $self->fault_handler->($value->value) : $value->value;
448             }
449              
450 0           return $value->value;
451             }
452              
453             ###############################################################################
454             #
455             # Sub Name: timeout
456             #
457             # Description: Get or set the timeout() setting on the underlying
458             # LWP::UserAgent object.
459             #
460             # Arguments: NAME IN/OUT TYPE DESCRIPTION
461             # $self in ref Object of this class
462             # $time in scalar New timeout value, if passed
463             #
464             # Returns: Return value from LWP::UserAgent->timeout()
465             #
466             ###############################################################################
467             sub timeout ## no critic (RequireArgUnpacking)
468             {
469 0     0     my $self = shift;
470              
471 0           return $self->useragent->timeout(@_);
472             }
473              
474             ###############################################################################
475             #
476             # Sub Name: uri
477             #
478             # Description: Get or set the URI portion of the request
479             #
480             # Arguments: NAME IN/OUT TYPE DESCRIPTION
481             # $self in ref Object of this class
482             # $uri in scalar New URI, if passed
483             #
484             # Returns: Current URI, undef if trying to set an invalid URI
485             #
486             ###############################################################################
487             sub uri ## no critic (RequireArgUnpacking)
488             {
489 0     0     my $self = shift;
490              
491 0           return $self->request->uri(@_);
492             }
493              
494             ###############################################################################
495             #
496             # Sub Name: credentials
497             #
498             # Description: Set basic-auth credentials on the underlying user-agent
499             # object
500             #
501             # Arguments: NAME IN/OUT TYPE DESCRIPTION
502             # $self in ref Object of this class
503             # $realm in scalar Realm to authenticate for
504             # $user in scalar User name to authenticate
505             # $pass in scalar Password for $user
506             #
507             # Returns: $self
508             #
509             ###############################################################################
510             sub credentials
511             {
512 0     0 1   my ($self, $realm, $user, $pass) = @_;
513              
514 0           my $uri = URI->new($self->uri);
515 0           $self->useragent->credentials($uri->host_port, $realm, $user, $pass);
516              
517 0           return $self;
518             }
519              
520             # Immutable accessor methods
521             BEGIN
522             {
523 4     4   25 no strict 'refs'; ## no critic (ProhibitNoStrict)
  4         5  
  4         251  
524              
525 4     4   10 for my $method (qw(useragent request compress_re compress parser))
526             {
527 20     0   1285 *{$method} = sub { shift->{"__$method"} }
  0         0  
528 20         49 }
529             }
530              
531             # Fetch/set the compression threshhold
532             sub compress_thresh
533             {
534 0     0 1   my $self = shift;
535 0   0       my $value = shift || 0;
536              
537 0           my $old = $self->{__compress_thresh};
538 0 0         if ($value)
539             {
540 0           $self->{__compress_thresh} = $value;
541             }
542              
543 0           return $old;
544             }
545              
546             # This doesn't actually *get* the original value, it only sets the value
547             sub compress_requests
548             {
549 0     0 1   my ($self, $value) = @_;
550              
551 0 0         if (! $value)
552             {
553 0           return $self->{__compress_requests};
554             }
555              
556 0 0         return $self->{__compress_requests} = $value ? 1 : 0;
557             }
558              
559             # These are get/set accessors for the fault-handler, error-handler and the
560             # combined fault/error handler.
561             sub fault_handler
562             {
563 0     0     my ($self, $newval) = @_;
564              
565 0           my $val = $self->{__fault_cb};
566 0 0 0       if ($newval and ref $newval)
567             {
568 0           $self->{__fault_cb} = $newval;
569             }
570             # Special: an explicit undef is used to clear the callback
571 0 0 0       if (@_ == 2 and (! defined $newval))
572             {
573 0           $self->{__fault_cb} = undef;
574             }
575              
576 0           return $val;
577             }
578              
579             sub error_handler
580             {
581 0     0     my ($self, $newval) = @_;
582              
583 0           my $val = $self->{__error_cb};
584 0 0 0       if ($newval and ref $newval)
585             {
586 0           $self->{__error_cb} = $newval;
587             }
588             # Special: an explicit undef is used to clear the callback
589 0 0 0       if (@_ == 2 and (! defined $newval))
590             {
591 0           $self->{__error_cb} = undef;
592             }
593              
594 0           return $val;
595             }
596              
597             sub combined_handler
598             {
599 0     0     my ($self, $newval) = @_;
600              
601 0           return ($self->fault_handler($newval), $self->error_handler($newval));
602             }
603              
604             # Control whether, and at what point, messages are considered too large to
605             # handle in-memory.
606             sub message_file_thresh
607             {
608 0     0 1   my ($self, $thresh) = @_;
609              
610 0 0         if (! $thresh)
611             {
612 0           return $self->{__message_file_thresh};
613             }
614              
615 0           return $self->{__message_file_thresh} = $thresh;
616             }
617              
618             sub message_temp_dir
619             {
620 0     0 1   my ($self, $dir) = @_;
621              
622 0 0         if (! $dir)
623             {
624 0           return $self->{__message_temp_dir};
625             }
626              
627 0           return $self->{__message_temp_dir} = $dir;
628             }
629              
630             1;
631              
632             __END__