File Coverage

blib/lib/XMLRPC/PurePerl.pm
Criterion Covered Total %
statement 134 289 46.3
branch 77 178 43.2
condition 14 18 77.7
subroutine 26 39 66.6
pod 7 17 41.1
total 258 541 47.6


line stmt bran cond sub pod time code
1             package XMLRPC::PurePerl;
2            
3 2     2   6948 use strict;
  2         5  
  2         60  
4 2     2   3100 use Data::Dumper;
  2         32751  
  2         221  
5 2     2   24 use Exporter;
  2         14  
  2         104  
6 2     2   3344 use LWP::UserAgent;
  2         109405  
  2         69  
7 2     2   20 use HTTP::Request;
  2         4  
  2         168  
8            
9             our $VERSION = "0.04";
10            
11             =head1 XMLRPC::PurePerl
12            
13             =head2 SYNOPSIS:
14             my $client = new XMLRPC::PurePerl("http://127.0.0.1:8080/");
15             my $result = $client->call("myMethod", { 'complex' => [ 'structure', 'goes' ] }, 'here' );
16            
17             my $xml = XMLRPC::PurePerl->encode_xmlrpc_call( $structure );
18             my $str = XMLRPC::PurePerl->decode_xmlrpc( $xml );
19            
20             # In case you don't have XML::Simple loaded... (a simple XML serializer / de-serializer)
21            
22             my $var_xml = XMLRPC::PurePerl->encode_variable( $structure );
23             my $var = XMLRPC::PurePerl->decode_variable( $var_xml );
24            
25             =head2 DESCRIPTION:
26            
27             This module implements the XML-RPC standard as defined at www.xmlrpc.com and serves as a (de)serialization engine as well as a client for such services.
28            
29             This module is in fairly close relation to an implementation that I wrote in javascript. The main problem I ran into web services and browsers was the dependence on the built in javascript XML parser. This module shows off how rolling your own can give you a bit of a boost in performance as well as avoiding dependencies for a compiled XML parser (for you guys who work in the DOD arena like me). If I had more time, I'd have rolled my own basic LWP modules just to avoid the extra dependencies. Anyway, this client provides the basic functionality that modules like RPC::XML or Frontier::RPC2 provide, the only difference is being the reason for the name, this is a pure perl implementation.
30            
31             =head2 DATATYPES:
32            
33             You can override the basic data types that perl will interpret by instantiating type objects. You simply pass the value as the sole argument, and it will transform into the appropriate XML upon serialization. Three data types will remain as type objects during de-serialization: datetime, base64 and boolean. More simply, date objects returned from the server will come back as a blessed reference of "XMLRPC::PurePerl::Type::datetime". All of the type modules contain simple "value" methods to pull the value from the blessed hash reference.
34            
35             There are also some simple static methods on XMLRPC::PurePerl to generate these structures.
36            
37             Lastly, the datetime constructur was given some flexibility. Instead of adding a full date parser, I wrote a few a regex's to parse out most of the sane date formats and put together the XMLRPC date format. Below are some examples of the acceptable formats..
38            
39             # Examples:
40            
41             my $boolean = XMLRPC::PurePerl->boolean(1);
42             my $string = XMLRPC::PurePerl->string(12345);
43             my $b64 = XMLRPC::PurePerl->base64("AB91231=");
44             my $double = XMLRPC::PurePerl->double(123.456);
45             my $date = XMLRPC::PurePerl->datetime("6 June 2006");
46            
47             my $value = $b64->value(); # example of using the value method for these data types
48            
49             # Acceptable date formats. (times are optional)
50            
51             # 20050701
52             # 2004/04/22 (dashes, spaces or hyphens)
53             # SEP 19, 2003
54             # 04-22-2004 (dashes, hyphens or spaces)
55             # 30 July 05
56             # July 30 2005
57            
58             # 20001109171200
59             # {ts '2003-06-23 12:21:43'}
60             # 302100ZSEP1998
61             # 2001-01-01T05:22:23.000Z
62            
63             Any of the first six formats can also have a time on the end. Here's the acceptable formats for time.
64            
65             # 00:00
66             # 00:00:00
67             # 00:00 AM (space optional)
68             # 00:00:00 AM
69            
70             =item Fault
71            
72             Faults are represented as an object as well, with a signature of XMLRPC::PurePerl::Type::Fault. The parser allows the fault param structure open to any data type, so if your server decides to send a complex structure back with the fault, it will deserialize it appropriately.
73            
74             =cut
75            
76             # this will set up our simple data type wrappers
77             BEGIN {
78 2     2   8 foreach my $pkg ( qw(i4 string boolean base64 double) ) {
79 10     1   11200 eval ( "package XMLRPC::PurePerl::Type::$pkg;\nsub new { return bless( { 'type' => '$pkg', 'val' => \$_[1] } ); }\nsub value { return (shift)->{'val'}; } " );
  1     1   27  
  1     3   16  
  3     0   79  
  0     3   0  
  3     1   82  
  1     1   9  
  1     1   12  
  1     1   8  
  1     0   21  
  0         0  
80             }
81             }
82            
83             our @ISA = qw(Exporter);
84             # be polite! allow these to be imported, but don't enforce import
85             our @EXPORT_OK = qw(encode_call_xmlrpc encode_response_xmlrpc decode_xmlrpc encode_variable decode_variable);
86            
87             # entity hash so I don't have to import HTML::Entities
88             our %entities = (
89             '<' => '<',
90             '>' => '>',
91             '&' => '&',
92             '"' => '"',
93             );
94             our %reverse_entities = reverse(%entities); # reverse it for the decode
95            
96             # These are the primary regex's used for parsing an XML document (probably need optimized a bit more)
97             my $scalarRgx = qr/^(?:string|i4|int|double)>([^<]+)/im;
98             my $memberRgx = qr/^name>([^<]+)/im;
99             my $valRgx = qr/^value>([^<]+)<\/value$/im;
100             my $boolRgx = qr/^boolean>([^<]+)/im;
101             my $b64Rgx = qr/^base64>([^<]+)/im;
102             my $dateRgx = qr/^[^>]+>([0-9]{4}[0-9]{2}[0-9]{2}T[0-9]{2}\:[0-9]{2}\:[0-9]{2})<[^<]+$/;
103             my $startString = qr/^(string|i4|int|double)/i;
104             my $startDate = qr/^(?:datetime|datetime.iso8601)/i;
105            
106             sub _entity_encode { # private method for encoding entities
107 1     1   1 my $val = shift;
108 1         2 $val =~ s/([&<>\"])/$entities{$1}/ge;
  0         0  
109 1         5 $val;
110             }
111             sub _entity_decode { # private entites for decoding entities
112 0     0   0 my $val = shift;
113 0         0 $val =~ s/(<|>|&|")/$reverse_entities{$1}/ge;
  0         0  
114 0         0 $val;
115             }
116            
117             =head2 Constructor
118            
119             my $client = new XMLRPC::PurePerl("http://validator.xmlrpc.com");
120            
121             Simply pass the fully qualified URL as your argument to the constructor, and off you go.
122            
123             =cut
124            
125             sub new {
126 0     0 1 0 my ( $class, $url ) = @_ ;
127            
128 0         0 my $this = {
129             'lwp' => new LWP::UserAgent(),
130             'request' => HTTP::Request->new(
131             'POST', $url, new HTTP::Headers( 'Content-Type' => 'text/xml' )
132             )
133             };
134 0         0 return bless($this);
135             }
136            
137             =head2 call
138            
139             my $result = $client->call("method", "argumunts");
140            
141             First argument to the call method is the method you wish to call, the rest will constitute the values that populate "". Each one will serialize into a "" entry.
142            
143             =cut
144            
145             sub call {
146 0     0 1 0 my $self = shift;
147 0 0       0 die("Instantiate this class to call this method...") if ( ref($self) !~ /^XMLRPC::PurePerl/ );
148            
149 0         0 my $xml = &encode_call_xmlrpc(@_);
150 0         0 $self->{'request'}->content($xml);
151 0         0 my $res = $self->{'lwp'}->request( $self->{'request'} );
152            
153 0 0       0 die $res->status_line() unless ( $res->is_success() ); # for HTTP failure
154            
155 0         0 return &decode_xmlrpc( $res->content() ); # don't die on fault
156             }
157            
158             =head2 encode_call_xmlrpc
159            
160             my $xml = XMLRPC::PurePerl->encode_call_xmlrpc("methodName", "arguments");
161            
162             This, will generate an XMLRPC request xml document based on the arguments passed to it.
163            
164             =cut
165            
166             sub encode_call_xmlrpc {
167 0 0   0 1 0 shift if ( $_[0] eq 'XMLRPC::PurePerl' );
168 0         0 my $method = shift;
169 0         0 my $xml = "\n\n$method\n\n";
170            
171 0         0 foreach my $struct ( @_ ) {
172 0         0 $xml .= "\n";
173 0         0 &encode_variable($struct, \$xml);
174 0         0 $xml .= "\n";
175             }
176 0         0 $xml .= "\n\n";
177 0         0 return $xml;
178             }
179            
180             =head2 encode_response_xmlrpc
181            
182             my $xml = XMLRPC::PurePerl->encode_response_xmlrpc("arguments");
183            
184             This, will generate an XMLRPC response xml document based on the arguments passed to it.
185            
186             =cut
187            
188             sub encode_response_xmlrpc {
189 0 0   0 1 0 shift if ( $_[0] eq 'XMLRPC::PurePerl' );
190 0         0 my $method = shift;
191 0         0 my $xml = "\n\n\n";
192            
193 0         0 foreach my $struct ( @_ ) {
194 0         0 $xml .= "\n";
195 0         0 &encode_variable($struct, \$xml);
196 0         0 $xml .= "\n";
197             }
198 0         0 $xml .= "\n\n";
199 0         0 return $xml;
200            
201             }
202            
203             =head2 encode_variable
204            
205             my $xml = XMLRPC::PurePerl->encode_variable("arguments");
206            
207             I'm a huge fan of XML::Simple, but having to remember all the options, and taking account for "force_array" to set values as array references instead of simple scalars (where you only have one value coming back is annoying. I have consistently ran into problems when my "simple" usage grew into more complex usage over time. This simple function solves this for, well, me at least.
208            
209             =cut
210            
211             sub encode_variable {
212 8 50   8 1 244 shift if ( $_[0] eq 'XMLRPC::PurePerl' );
213 8         14 my ( $obj, $xml ) = @_;
214 8         10 my $ref = ref($obj);
215            
216 8 100       34 if ( ! $ref ) {
    100          
    100          
    50          
    0          
217 1 50       7 if ( $obj =~ /^\-?[0-9]+\.[0-9]*$/ ) {
    50          
218 0         0 ${$xml} .= "$obj\n";
  0         0  
219             } elsif ( $obj =~ /^-?[0-9]+$/ ) {
220 0         0 ${$xml} .= "$obj\n";
  0         0  
221             } else {
222 1         1 ${$xml} .= "" . &_entity_encode($obj) . "\n";
  1         5  
223             }
224             } elsif ( $ref eq 'ARRAY' ) {
225 1         1 ${$xml} .= "\n";
  1         3  
226 1         1 foreach my $val ( @{$obj} ) {
  1         3  
227 0         0 &encode_variable($val, $xml);
228             }
229 1         1 ${$xml} .= "\n";
  1         4  
230             } elsif ( $ref eq 'HASH' ) {
231 1         2 ${$xml} .= "\n";
  1         2  
232 1         1 foreach my $key ( keys(%{$obj}) ) {
  1         3  
233 0         0 ${$xml} .= "" . &_entity_encode($key) . "";
  0         0  
234 0         0 &encode_variable( $obj->{$key}, $xml );
235 0         0 ${$xml} .= "\n";
  0         0  
236             }
237 1         2 ${$xml} .= "\n";
  1         3  
238             } elsif ( $ref =~ /^XMLRPC::PurePerl::Type::(.+)$/ ) {
239 5 100       11 if ( $1 eq 'datetime' ) {
240 1         1 ${$xml} .= "" . $obj->value() . "\n";
  1         3  
241             } else {
242 4         5 ${$xml} .= "<$1>" . $obj->value() . "";
  4         99  
243             }
244             } elsif ( $ref eq "CODE" ) {
245 0         0 die("Cannot serialize a subroutine!");
246             }
247             }
248            
249             =head2 decode_variable
250            
251             my $structure = XMLRPC::PurePerl->decode_variable("arguments");
252            
253             The deserializer of the previously mentioned function.
254            
255             =cut
256            
257             sub decode_variable {
258 8 50   8 1 1387 shift if ( $_[0] eq 'XMLRPC::PurePerl' );
259 8         12 my $xml = shift;
260 8         9 my @tokens;
261 8 50       15 if ( ref($xml) eq 'ARRAY' ) {
262 0         0 @tokens = @{$xml};
  0         0  
263             } else {
264 8         83 $xml =~ s/([<>])\s*/$1/g;
265 8         13 $xml =~ s/>\n/>/g;
266 8         28 @tokens = split("><", $xml);
267             }
268 8         12 my $position = 1;
269 8         7 my @outbound;
270            
271 8         19 until ( $position == scalar(@tokens) ) {
272 17 100       187 if ( $tokens[$position] =~ $startString ) {
    100          
    100          
    100          
    100          
    100          
273 3         15 my $ob = ($tokens[$position] =~ $scalarRgx)[0];
274 3         4 push(@outbound, $ob);
275             } elsif ( lc($tokens[$position]) eq 'struct' ) {
276 1         3 my $ob = {};
277 1         5 &parse_struct($ob, \@tokens, \$position);
278 1         1 push(@outbound, $ob);
279             } elsif ( lc($tokens[$position]) eq 'array' ) {
280 1         2 my $ob = [];
281 1         5 &parse_array($ob, \@tokens, \$position);
282 1         3 push(@outbound, $ob);
283             } elsif ( $tokens[$position] =~ $startDate ) {
284 1         8 my $ob = ($tokens[$position] =~ $dateRgx)[0];
285 1         4 push(@outbound, XMLRPC::PurePerl::Type::datetime->new($ob));
286             } elsif ( lc($tokens[$position]) =~ $boolRgx ) {
287 1         30 push(@outbound, XMLRPC::PurePerl::Type::boolean->new($1));
288             } elsif ( lc($tokens[$position]) =~ $b64Rgx ) {
289 1         26 push(@outbound, XMLRPC::PurePerl::Type::base64->new($1));
290             } else {
291             }
292 17         38 $position++;
293             }
294 8 50       17 if ( scalar(@outbound) == 1 ) {
295 8         27 return $outbound[0];
296             } else {
297 0 0       0 if ( wantarray ) {
298 0         0 return @outbound;
299             } else {
300 0         0 return \@outbound;
301             }
302             }
303             }
304            
305             =head2 decode_xmlrpc
306            
307             my $structure = XMLRPC::PurePerl->decode_xmlrpc();
308             if ( ref($structure) =~ /fault/i ) {
309             &do_something_to_handle_the_fault( $structure->value() );
310             }
311            
312             The data structure returned will be in scalar context, or in list context, depending on your lvalue's sigil.
313             If you're decoding a methodCall, you'll get a structure keyed by the methodName and the arguments passed to it as an array reference..
314            
315             # If you dumped out the de-serialization of a methodCall XML document
316             $VAR1 = {
317             'method' => 'myMethod'
318             'args' => [ 'a', 'b', 'c' ]
319             }
320            
321             =cut
322            
323             sub decode_xmlrpc {
324 0 0   0 1 0 shift if ( $_[0] eq 'XMLRPC::PurePerl' );
325 0         0 my $xml = shift;
326 0         0 $xml =~ s/([<>])\s*/$1/g;
327 0         0 $xml =~ s/>\n/>/g;
328 0         0 my @tokens = split("><", $xml);
329            
330 0 0       0 if ( $xml =~ // ) {
331 0         0 shift(@tokens) until ( $tokens[0] eq 'value' ); # whittle!
332 0         0 pop(@tokens) until ( $tokens[$#tokens] eq '/value' );
333 0         0 return XMLRPC::PurePerl::Fault->new( &decode_variable( \@tokens ) );
334             }
335            
336 0         0 my $methodName;
337             my $position;
338 0 0       0 if ( $tokens[1] eq 'methodCall' ) {
339 0         0 $position = 6;
340 0         0 $tokens[2] =~ />([^>]+)
341 0         0 $methodName = $1;
342             } else {
343 0         0 $position = 5;
344             }
345 0         0 my @outbound;
346            
347 0         0 until ( $position == scalar(@tokens) ) {
348 0 0       0 if ( $tokens[$position] =~ $startString ) {
    0          
    0          
    0          
    0          
    0          
349 0         0 my $ob = ($tokens[$position] =~ $scalarRgx)[0];
350 0         0 push(@outbound, $ob);
351             } elsif ( lc($tokens[$position]) eq 'struct' ) {
352 0         0 my $ob = {};
353 0         0 &parse_struct($ob, \@tokens, \$position);
354 0         0 push(@outbound, $ob);
355             } elsif ( lc($tokens[$position]) eq 'array' ) {
356 0         0 my $ob = [];
357 0         0 &parse_array($ob, \@tokens, \$position);
358 0         0 push(@outbound, $ob);
359             } elsif ( $tokens[$position] =~ $startDate ) {
360 0         0 my $ob = ($tokens[$position] =~ $dateRgx)[0];
361 0         0 push(@outbound, XMLRPC::PurePerl::Type::datetime->new($ob));
362             } elsif ( lc($tokens[$position]) =~ $boolRgx ) {
363 0         0 push(@outbound, XMLRPC::PurePerl::Type::boolean->new($1));
364             } elsif ( lc($tokens[$position]) =~ $b64Rgx ) {
365 0         0 push(@outbound, XMLRPC::PurePerl::Type::base64->new($1));
366             } else {
367             }
368 0         0 $position++;
369             }
370 0 0       0 if ( scalar(@outbound) == 1 ) { # Only 1 "param" in responses
371 0         0 return $outbound[0];
372             } else {
373 0 0       0 if ( wantarray ) {
    0          
374 0         0 return @outbound;
375             } elsif ( $methodName ) {
376             return {
377 0         0 'method' => $methodName,
378             'args' => \@outbound
379             }
380             } else { # for decoding methodCall xml files...
381 0         0 return \@outbound;
382             }
383             }
384             }
385            
386             # internal function for parsing arrays
387             sub parse_array {
388 1     1 0 4 my ( $structure, $tokens, $position ) = @_;
389 1         3 my $currentElement = 0;
390            
391 1         1 ${$position} += 2;
  1         3  
392            
393 1         3 for ( undef; ${$position}..scalar(@{$tokens}); ${$position}++ ) {
  1         4  
  1         6  
  0         0  
394 1 50       2 if ( $tokens->[${$position}] eq 'value' ) {
  1 50       4  
  1 50       18  
395 0         0 ${$position}++;
  0         0  
396 0 0       0 if ( $tokens->[${$position}] =~ $startString ) {
  0 0       0  
  0 0       0  
    0          
    0          
    0          
397 0         0 $structure->[$currentElement++] = &_entity_decode(($tokens->[${$position}] =~ $scalarRgx)[0]);
  0         0  
398 0         0 } elsif ( lc($tokens->[${$position}]) eq 'struct' ) {
399 0         0 my $outbound = {};
400 0         0 &parse_struct($outbound, $tokens, $position);
401 0         0 $structure->[$currentElement++] = $outbound;
402 0         0 } elsif ( lc($tokens->[${$position}]) eq 'array' ) {
403 0         0 my $outbound = [];
404 0         0 &parse_array($outbound, $tokens, $position);
405 0         0 $structure->[$currentElement++] = $outbound;
406 0         0 } elsif ( $tokens->[${$position}] =~ $startDate ) {
407 0         0 my $dt = ($tokens->[${$position}] =~ $dateRgx)[0];
  0         0  
408 0         0 $structure->[$currentElement++] = XMLRPC::PurePerl->datetime($dt);
409            
410 0         0 } elsif ( $tokens->[${$position}] =~ $boolRgx ) {
411 0         0 $structure->[$currentElement++] = XMLRPC::PurePerl->boolean( $1 );
412             } elsif ( $tokens->[${$position}] =~ $b64Rgx ) {
413 0         0 $structure->[$currentElement++] = XMLRPC::PurePerl->base64( $1 );
414             } else {
415             }
416 1         4 } elsif ( $tokens->[${$position}] =~ $valRgx ) { # is it a value
417 0         0 $structure->[ $currentElement++ ] = &_entity_encode($1);
418             } elsif ( $tokens->[${$position}] eq '/data' ) {
419 1         3 return;
420             } else {
421             }
422             }
423             }
424            
425             # internal function for parsing strcutures
426             sub parse_struct {
427 1     1 0 3 my ( $structure, $tokens, $position, $currentKey ) = @_;
428            
429 1         2 for ( undef; ${$position}..scalar(@{$tokens}); ${$position}++ ) {
  2         3  
  2         8  
  1         3  
430 2 50       3 if ( lc($tokens->[${$position}]) eq 'member' ) {
  2 100       6  
  2         7  
431 0         0 ${$position}++;
  0         0  
432 0         0 $currentKey = ($tokens->[${$position}] =~ $memberRgx)[0];
  0         0  
433 0         0 ${$position}++;
  0         0  
434            
435 0 0       0 if ( $tokens->[${$position}] =~ $valRgx ) { # is it a value
  0         0  
436 0         0 $structure->{$currentKey} = ($tokens->[${$position}] =~ $valRgx)[0];
  0         0  
437            
438             } else { # increment by one and retest
439 0         0 ${$position}++;
  0         0  
440            
441 0 0       0 if ( $tokens->[${$position}] =~ $startString ) {
  0 0       0  
  0 0       0  
    0          
    0          
    0          
442 0         0 $structure->{$currentKey} = &_entity_decode(($tokens->[${$position}] =~ $scalarRgx)[0]);
  0         0  
443            
444 0         0 } elsif ( $tokens->[${$position}] eq 'struct' ) {
445 0         0 my $outbound = {};
446 0         0 &parse_struct($outbound, $tokens, $position);
447 0         0 $structure->{$currentKey} = $outbound;
448            
449 0         0 } elsif ( $tokens->[${$position}] eq 'array' ) {
450 0         0 my $outbound = [];
451 0         0 &parse_array($outbound, $tokens, $position);
452 0         0 $structure->{$currentKey} = $outbound;
453            
454 0         0 } elsif ( $tokens->[${$position}] =~ $startDate ) {
455            
456 0         0 my $dt = ($tokens->[${$position}] =~ $dateRgx)[0];
  0         0  
457 0         0 $structure->{$currentKey} = XMLRPC::PurePerl->datetime($dt);
458            
459 0         0 } elsif ( $tokens->[${$position}] =~ $boolRgx ) {
460 0         0 $structure->{$currentKey} = XMLRPC::PurePerl->boolean( $1 );
461             } elsif ( $tokens->[${$position}] =~ $b64Rgx ) {
462 0         0 $structure->{$currentKey} = XMLRPC::PurePerl->base64( $1 );
463             } else {
464             }
465             }
466             } elsif ( lc($tokens->[${$position}]) eq '/struct' ) {
467 1         2 return;
468             }
469             }
470             }
471            
472             # sometimes I forget i4 == int
473             sub int {
474 0 0   0 0 0 shift if ( $_[0] =~ /^XMLRPC::/ );
475 0         0 return XMLRPC::PurePerl::Type::i4->new( $_[0] );
476             }
477             sub date {
478 18 50   18 0 1108 shift if ( $_[0] =~ /^XMLRPC::/ );
479 18         63 return XMLRPC::PurePerl::Type::datetime->new( shift );
480             }
481             sub datetime {
482 0 0   0 0 0 shift if ( $_[0] =~ /^XMLRPC::/ );
483 0         0 return XMLRPC::PurePerl::Type::datetime->new( shift );
484             }
485            
486             # generate a helper static subroutine for each data type
487             foreach my $pkg ( qw(i4 string boolean base64 double) ) {
488 2 50   2 0 15 eval ( "sub $pkg { shift if ( \$_[0] =~ /^XMLRPC::/ ); return new XMLRPC::PurePerl::Type::$pkg( shift, '$pkg' ); }" );
  2 50   2 0 62  
  2 50   1 0 12  
  2 50   1 0 73  
  1 0   0 0 26  
  1         27  
  1         5  
  1         26  
  0         0  
  0         0  
489             }
490            
491             package XMLRPC::PurePerl::Type::datetime;
492            
493             our %month_struct = (
494             "JAN" => "01", "FEB" => "02", "MAR" => "03", "APR" => "04", "MAY" => "05", "JUN" => "06", "JUL" => "07", "AUG" => "08", "SEP" => "09", "OCT" => "10", "NOV" => "11", "DEC" => "12", "01" => "JAN", "02" => "FEB", "03" => "MAR", "04" => "APR", "05" => "MAY", "06" => "JUN", "07" => "JUL", "08" => "AUG", "09" => "SEP", "10" => "OCT", "11" => "NOV", "12" => "DEC", "JANUARY" => "01", "FEBRUARY" => "02", "MARCH" => "03", "APRIL" => "04", "MAY" => "05", "JUNE" => "06", "JULY" => "07", "AUGUST" => "08", "SEPTEMBER" => "09", "OCTOBER" => "10", "NOVEMBER" => "11", "DECEMBER" => "12"
495             );
496            
497             # 20050701 , 20050701 00:00:00 , 20050701 00:00:00PM , 2004/04/22 , 2004/22/02 00:00
498             my $ymd = qr/^([0-9]{4})[\/\-\s]?([0-9]{2})[\/\-\s]?([0-9]{1,2})[T\s]?([0-9]{2})?(\:[0-9]{2}\:?(?:[0-9]{2})?)?[\s]?([AP]M)?$/i;
499             # SEP 19, 2003 09:45:00
500             my $Mdy = qr/^([A-Za-z]{3})\s(0?[1-9]|1[0-9]|2[0-9]|3[0-1]),?\s?([0-9]{4})\s*([0-9]{2})?(\:[0-9]{2}\:?(?:[0-9]{2})?)?\s?([AP]M)?$/i;
501             # 04-22-2004 , 04-22-2004 00:00AM, 04-22-2004 , 04-22-2004 00:00:00AM
502             my $mdy = qr/^(0?[1-9]|1[0-2])[\/\-\\s](0?[1-9]|1[0-9]|2[0-9]|3[0-1]|[1-9])[\/\-\\s]([0-9]{4})[\sT]?([0-9]{2})?(\:[0-9]{2}\:?(?:[0-9]{2})?)?\s?([AP]M)?$/i;
503             # 30 July 05
504             my $dmy = qr/^(0?[1-9]|1[0-9]|2[0-9]|3[0-1])\s*([A-Za-z]{1,9})\s?([0-9]{2,4})\s*([0-9]{2})?(\:[0-9]{2}\:?(?:[0-9]{2})?)?[\sT]?([AP]M)?$/i;
505             # July 30 2005 16:17 or July 30, 2005 16:17
506             my $MONTHdy = qr/^([A-Za-z]{1,9})\s?(0?[1-9]|1[0-9]|2[0-9]|3[0-1])[\s,]([0-9]{2,4})[\sT]?([0-9]{2})?(\:[0-9]{2}\:?(?:[0-9]{2})?)?\s?([AP]M)?$/i;
507             # 20001109171200
508             my $allnum = qr/^([0-9]{4})(0?[0-9]|1[0-2])(0?[1-9]|1[0-9]|2[0-9]|3[0-1])([0-9]{2})([0-9]{2})([0-9]{2})$/;
509             # {ts '2003-06-23 12:21:43'}
510             my $mssql = qr/\{ts '([0-9]{4})\-(0?[0-9]|1[0-2])\-(0?[1-9]|1[0-9]|2[0-9]|3[0-1])\s([0-9]{2})\:([0-9]{2})\:([0-9]{2})'\}/i;
511             # 302100ZSEP1998
512             my $dtg = qr/^(0?[1-9]|1[0-9]|2[0-9]|3[0-1])([0-9]{2})([0-9]{2})[A-Z]([A-Za-z]{3})([0-9]{2,4})$/i;
513             # 2001-01-01T05:22:23.000Z
514             my $prs = qr/^[0-9]{4}\-?[0-9]{2}\-?[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}/;
515            
516             # TODO: make single digit hours valid in the regex, auto pad the 0.. (thought about using printf, but it wouldn't handle AM/PM)
517            
518             sub new {
519 19     19   35 my ( $class, $date ) = @_;
520 19         42 my $this = { 'type' => 'datetime' };
521            
522             # Quick commentary on why there is a huge if/elsif block here for parsing dates..
523             # Date::Manip, Date::Parse, Date::Calc are all modules that I COULD have used for parsing "common"
524             # date formats.. I wanted to avoid adding the dependency, and I really just needed to get to the XMLRPC
525             # format more than anything...
526             # 19980717T14:08:55 is an example of the format we're after...
527            
528 19 100       398 if ( my ( $year, $month, $day, $hour, $minsec, $ampm ) = $date =~ $ymd ) { # 20050701 , 20050701 00:00:00 , 20050701 00:00:00PM , 2004/04/22 , 2004/22/02 00:00
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    50          
529 8   100     30 $hour ||= '00';
530 8   100     26 $minsec ||= ':00:00';
531 8   100     30 $ampm ||= '';
532 8 50       73 $this->{'val'} = ( length($year) == 2 ? '20' . $year : $year ) . $month . sprintf("%02d", $day) . 'T' . ( $hour ? ( $ampm eq 'PM' ? 12 + $hour : $hour ) . ( length($minsec) == 3 ? $minsec . ':00' : $minsec ) : '00:00:00' );
    50          
    100          
    50          
533            
534             } elsif ( $date =~ $prs ) { # 2001-01-01T05:22:23.000Z
535 1         4 $this->{'val'} = $date;
536 1         6 $this->{'val'} =~ s/\-//g;
537 1         6 $this->{'val'} =~ s/\..*$//;
538            
539             } elsif ( my ($Mdy_month, $Mdy_day, $Mdy_year, $Mdy_hour, $Mdy_minsec, $Mdy_ampm) = $date =~ $Mdy ) { # SEP 19, 2003 09:45:00
540 1   50     4 $Mdy_hour ||= '';
541 1   50     4 $Mdy_minsec ||= '';
542 1   50     6 $Mdy_ampm ||= '';
543 1 50       14 $this->{'val'} = $Mdy_year . $month_struct{uc($Mdy_month)} . sprintf("%02d", $Mdy_day) . 'T' . ( $Mdy_hour ? ( $Mdy_ampm eq 'PM' ? 12 + $Mdy_hour : $Mdy_hour ) . ( length($Mdy_minsec) == 3 ? $Mdy_minsec . ':00' : $Mdy_minsec ) : '00:00:00' );
    50          
    50          
544            
545             } elsif ( my ($mdy_month, $mdy_day, $mdy_year, $mdy_hour, $mdy_minsec, $mdy_ampm) = $date =~ $mdy ) { # 04-22-2004 , 04-22-2004 00:00AM, 04-22-2004 , 04-22-2004 00:00:00AM
546 2   100     11 $mdy_hour ||= '';
547 2   100     7 $mdy_minsec ||= '';
548 2   50     9 $mdy_ampm ||= '';
549 2 50       18 $this->{'val'} = $mdy_year . $mdy_day . sprintf("%02d", $mdy_month) . 'T' . ( $mdy_hour ? ( $mdy_ampm eq 'PM' ? 12 + $mdy_hour : $mdy_hour ) . ( length($mdy_minsec) == 3 ? $mdy_minsec . ':00' : $mdy_minsec ) : '00:00:00' );
    50          
    100          
550            
551             } elsif ( $date =~ $dtg ) { # 2001-01-01T05:22:23.000Z
552 1 50       16 $this->{'val'} = ( length($5) == 2 ? '20' . $5 : $5 ) . $month_struct{uc($4)} . $1 . 'T' . "$2:$3:00";
553            
554             } elsif ( $date =~ $dmy ) { # 30 July 05
555 3 50       35 $this->{'val'} = ( length($3) == 2 ? '20' . $3 : $3 ) . $month_struct{uc($2)} . sprintf("%02d", $1) . 'T' . ( $4 ? ( $6 eq 'PM' ? 12 + $4 : $4 ) . ( length($5) == 3 ? $5 . ':00' : $5 ) : '00:00:00' );
    50          
    50          
    100          
556            
557             } elsif ( $date =~ $MONTHdy ) { # July 30 2005 16:17 or July 30, 2005 16:17
558 1 50       18 $this->{'val'} = ( length($3) == 2 ? '20' . $3 : $3 ) . $month_struct{uc($1)} . sprintf("%02d", $2) . 'T' . ( $4 ? ( $6 eq 'PM' ? 12 + $4 : $4 ) . ( length($5) == 3 ? $5 . ':00' : $5 ) : '00:00:00' );
    50          
    50          
    50          
559            
560             } elsif ( $date =~ $allnum ) { # 20001109171200
561 1         7 $this->{'val'} = $1 . $2 . $3 . 'T' . $4 . ':' . $5 . ':' . $6;
562            
563             } elsif ( $date =~ $mssql ) { # {ts '2003-06-23 12:21:43'}
564 1         8 $this->{'val'} = $1 . $2 . $3 . 'T' . $4 . ':' . $5 . ':' . $6;
565            
566             } else {
567 0         0 warn "Date Format $date unknown...";
568 0         0 $this->{'val'} = undef;
569             }
570 19         157 return bless( $this );
571             }
572            
573             sub value {
574 17     17   51 return (shift)->{'val'};
575             }
576            
577             package XMLRPC::PurePerl::Fault;
578            
579             sub new {
580 0     0     my ( $class, $this ) = @_;
581 0           return bless( $this );
582             }
583            
584 0     0     sub value { return shift; }
585            
586             =head1 WHY DO THIS!?!
587            
588             Yeah, there's a bunch of these modules out there for this kind of stuff. I in no way mean to step on anyones toes, but I am quite proud of the benchmarks that this module is capable of producing. It does have it's limits, but for such a lightweight little engine, I think it does fairly well for itself. Let's keep in mind that this engine is a "fast and loose" engine, with very little in terms of defense from malformed XML, which RPC::XML and Frontier have more built in defense through the use of a true XML Parser.
589            
590             500 elements
591             ENCODING SPEED TEST
592             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
593             frontier: 11 wallclock secs (10.47 usr + 0.09 sys = 10.56 CPU) @ 26.70/s (n=282)
594             pureperl: 10 wallclock secs (10.69 usr + 0.03 sys = 10.72 CPU) @ 86.75/s (n=930)
595             rpcxml: 11 wallclock secs (10.55 usr + 0.05 sys = 10.59 CPU) @ 66.93/s (n=709)
596             DECODING SPEED TEST
597             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
598             frontier: 11 wallclock secs (10.64 usr + 0.02 sys = 10.66 CPU) @ 10.51/s (n=112)
599             pureperl: 11 wallclock secs (10.50 usr + 0.08 sys = 10.58 CPU) @ 14.65/s (n=155)
600             rpcxml: 11 wallclock secs (10.58 usr + 0.03 sys = 10.61 CPU) @ 6.69/s (n=71)
601            
602             1000 elements
603             ENCODING SPEED TEST
604             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
605             frontier: 10 wallclock secs (10.44 usr + 0.11 sys = 10.55 CPU) @ 11.95/s (n=126)
606             pureperl: 10 wallclock secs (10.55 usr + 0.00 sys = 10.55 CPU) @ 43.61/s (n=460)
607             rpcxml: 10 wallclock secs (10.50 usr + 0.09 sys = 10.59 CPU) @ 29.92/s (n=317)
608             DECODING SPEED TEST
609             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
610             frontier: 10 wallclock secs (10.08 usr + 0.00 sys = 10.08 CPU) @ 5.26/s (n=53)
611             pureperl: 11 wallclock secs (10.27 usr + 0.08 sys = 10.34 CPU) @ 7.35/s (n=76)
612             rpcxml: 9 wallclock secs (10.19 usr + 0.00 sys = 10.19 CPU) @ 3.34/s (n=34)
613            
614             5000 elements (beyond this, PurePerl isn't the best module to use)
615             ENCODING SPEED TEST
616             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
617             frontier: 11 wallclock secs (10.81 usr + 0.05 sys = 10.86 CPU) @ 1.10/s (n=12)
618             pureperl: 10 wallclock secs ( 9.98 usr + 0.08 sys = 10.06 CPU) @ 8.55/s (n=86)
619             rpcxml: 10 wallclock secs (10.16 usr + 0.19 sys = 10.34 CPU) @ 2.22/s (n=23)
620             DECODING SPEED TEST
621             Benchmark: running frontier, pureperl, rpcxml for at least 10 CPU seconds...
622             frontier: 10 wallclock secs (10.48 usr + 0.00 sys = 10.48 CPU) @ 1.05/s (n=11)
623             pureperl: 11 wallclock secs ( 9.31 usr + 0.94 sys = 10.25 CPU) @ 0.88/s (n=9)
624             rpcxml: 11 wallclock secs (10.45 usr + 0.03 sys = 10.48 CPU) @ 0.67/s (n=7)
625            
626             =head1 See also:
627            
628             RPC::XML (the best XMLRPC module out there for exacting precision of the specification)
629             Frontier::RPC2 (the reference implementation)
630             SOAP::Lite, XMLRPC::Lite (my quest will soon become conquering Document Literal (why is this so hard to do in Perl still?)
631            
632             =head1 Acknowledgements:
633            
634             Dave Winer, thanks for such a great protocol
635             Paul Lindner and Randy Ray (thanks for the kudos in your book "Programming Web Services in Perl"!), my former co-workers at Red Hat.
636             Joshua Blackburn, who pushed me to write the original javascript implementation of this module.
637             Claus Brunzema, for a very polite bug report dealing with negative integers!
638             Frank Rothhaupt, for a very polite bug report dealing with fault's!
639            
640             =head1 COPYRIGHT:
641            
642             The XMLRPC::PurePerl module is Copyright (c) 2006 Ryan Alan Dietrich. The XMLRPC::PurePerl module is free software; you can redistribute it and/or modify it under the same terms as Perl itself with the exception that it cannot be placed on a CD-ROM or similar media for commercial distribution without the prior approval of the author.
643            
644             =head1 AUTHOR:
645            
646             XMLRPC::PurePerl by Ryan Alan Dietrich
647            
648             =cut
649            
650             1;