File Coverage

blib/lib/Frontier/RPC2.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             #
2             # Copyright (C) 1998, 1999 Ken MacLeod
3             # Frontier::RPC is free software; you can redistribute it
4             # and/or modify it under the same terms as Perl itself.
5             #
6             # $Id: RPC2.pm,v 1.18 2002/08/02 18:35:21 ivan420 Exp $
7             #
8              
9             # NOTE: see Storable for marshalling.
10              
11 1     1   5 use strict;
  1         2  
  1         38  
12              
13             package Frontier::RPC2;
14 1     1   1704 use XML::Parser;
  0            
  0            
15              
16             use vars qw{%scalars %char_entities};
17              
18             %char_entities = (
19             '&' => '&',
20             '<' => '&lt;',
21             '>' => '&gt;',
22             '"' => '&quot;',
23             );
24              
25             # FIXME I need a list of these
26             %scalars = (
27             'base64' => 1,
28             'boolean' => 1,
29             'dateTime.iso8601' => 1,
30             'double' => 1,
31             'int' => 1,
32             'i4' => 1,
33             'string' => 1,
34             );
35              
36             sub new {
37             my $class = shift;
38             my $self = ($#_ == 0) ? { %{ (shift) } } : { @_ };
39              
40             bless $self, $class;
41              
42             if (defined $self->{'encoding'}) {
43             $self->{'encoding_'} = " encoding=\"$self->{'encoding'}\"";
44             } else {
45             $self->{'encoding_'} = "";
46             }
47              
48             return $self;
49             }
50              
51             sub encode_call {
52             my $self = shift; my $proc = shift;
53              
54             my @text;
55             push @text, <<EOF;
56             <?xml version="1.0"$self->{'encoding_'}?>
57             <methodCall>
58             <methodName>$proc</methodName>
59             <params>
60             EOF
61              
62             push @text, $self->_params([@_]);
63              
64             push @text, <<EOF;
65             </params>
66             </methodCall>
67             EOF
68              
69             return join('', @text);
70             }
71              
72             sub encode_response {
73             my $self = shift;
74              
75             my @text;
76             push @text, <<EOF;
77             <?xml version="1.0"$self->{'encoding_'}?>
78             <methodResponse>
79             <params>
80             EOF
81              
82             push @text, $self->_params([@_]);
83              
84             push @text, <<EOF;
85             </params>
86             </methodResponse>
87             EOF
88              
89             return join('', @text);
90             }
91              
92             sub encode_fault {
93             my $self = shift; my $code = shift; my $message = shift;
94              
95             my @text;
96             push @text, <<EOF;
97             <?xml version="1.0"$self->{'encoding_'}?>
98             <methodResponse>
99             <fault>
100             EOF
101              
102             push @text, $self->_item({faultCode => $code, faultString => $message});
103              
104             push @text, <<EOF;
105             </fault>
106             </methodResponse>
107             EOF
108              
109             return join('', @text);
110             }
111              
112             sub serve {
113             my $self = shift; my $xml = shift; my $methods = shift;
114              
115             my $call;
116             # FIXME bug in Frontier's XML
117             $xml =~ s/(<\?XML\s+VERSION)/\L$1\E/;
118             eval { $call = $self->decode($xml) };
119              
120             if ($@) {
121             return $self->encode_fault(1, "error decoding RPC.\n" . $@);
122             }
123              
124             if ($call->{'type'} ne 'call') {
125             return $self->encode_fault(2,"expected RPC \`methodCall', got \`$call->{'type'}'\n");
126             }
127              
128             my $method = $call->{'method_name'};
129             if (!defined $methods->{$method}) {
130             return $self->encode_fault(3, "no such method \`$method'\n");
131             }
132              
133             my $result;
134             my $eval = eval { $result = &{ $methods->{$method} }(@{ $call->{'value'} }) };
135             if ($@) {
136             return $self->encode_fault(4, "error executing RPC \`$method'.\n" . $@);
137             }
138              
139             my $response_xml = $self->encode_response($result);
140             return $response_xml;
141             }
142              
143             sub _params {
144             my $self = shift; my $array = shift;
145              
146             my @text;
147              
148             my $item;
149             foreach $item (@$array) {
150             push (@text, "<param>",
151             $self->_item($item),
152             "</param>\n");
153             }
154              
155             return @text;
156             }
157              
158             sub _item {
159             my $self = shift; my $item = shift;
160              
161             my @text;
162              
163             my $ref = ref($item);
164             if (!$ref) {
165             push (@text, $self->_scalar ($item));
166             } elsif ($ref eq 'ARRAY') {
167             push (@text, $self->_array($item));
168             } elsif ($ref eq 'HASH') {
169             push (@text, $self->_hash($item));
170             } elsif ($ref eq 'Frontier::RPC2::Boolean') {
171             push @text, "<value><boolean>", $item->repr, "</boolean></value>\n";
172             } elsif ($ref eq 'Frontier::RPC2::String') {
173             push @text, "<value><string>", $item->repr, "</string></value>\n";
174             } elsif ($ref eq 'Frontier::RPC2::Integer') {
175             push @text, "<value><int>", $item->repr, "</int></value>\n";
176             } elsif ($ref eq 'Frontier::RPC2::Double') {
177             push @text, "<value><double>", $item->repr, "</double></value>\n";
178             } elsif ($ref eq 'Frontier::RPC2::DateTime::ISO8601') {
179             push @text, "<value><dateTime.iso8601>", $item->repr, "</dateTime.iso8601></value>\n";
180             } elsif ($ref eq 'Frontier::RPC2::Base64') {
181             push @text, "<value><base64>", $item->repr, "</base64></value>\n";
182             } elsif ($ref =~ /=HASH\(/) {
183             push @text, $self->_hash($item);
184             } elsif ($ref =~ /=ARRAY\(/) {
185             push @text, $self->_array($item);
186             } else {
187             die "can't convert \`$item' to XML\n";
188             }
189              
190             return @text;
191             }
192              
193             sub _hash {
194             my $self = shift; my $hash = shift;
195              
196             my @text = "<value><struct>\n";
197              
198             my ($key, $value);
199             while (($key, $value) = each %$hash) {
200             push (@text,
201             "<member><name>$key</name>",
202             $self->_item($value),
203             "</member>\n");
204             }
205              
206             push @text, "</struct></value>\n";
207              
208             return @text;
209             }
210              
211              
212             sub _array {
213             my $self = shift; my $array = shift;
214              
215             my @text = "<value><array><data>\n";
216              
217             my $item;
218             foreach $item (@$array) {
219             push @text, $self->_item($item);
220             }
221              
222             push @text, "</data></array></value>\n";
223              
224             return @text;
225             }
226              
227             sub _scalar {
228             my $self = shift; my $value = shift;
229              
230             # these are from `perldata(1)'
231             if ($value =~ /^[+-]?\d+$/) {
232             return ("<value><i4>$value</i4></value>");
233             } elsif ($value =~ /^(-?(?:\d+(?:\.\d*)?|\.\d+)|([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?)$/) {
234             return ("<value><double>$value</double></value>");
235             } else {
236             $value =~ s/([&<>\"])/$char_entities{$1}/ge;
237             return ("<value><string>$value</string></value>");
238             }
239             }
240              
241             sub decode {
242             my $self = shift; my $string = shift;
243              
244             $self->{'parser'} = XML::Parser->new( Style => ref($self),
245             'use_objects' => $self->{'use_objects'} );
246             return $self->{'parser'}->parsestring($string);
247             }
248              
249             # shortcuts
250             sub base64 {
251             my $self = shift;
252              
253             return Frontier::RPC2::Base64->new(@_);
254             }
255              
256             sub boolean {
257             my $self = shift;
258             my $elem = shift;
259             if($elem == 0 or $elem == 1) {
260             return Frontier::RPC2::Boolean->new($elem);
261             } else {
262             die "error in rendering RPC type \`$elem\' not a boolean\n";
263             }
264             }
265              
266             sub double {
267             my $self = shift;
268             my $elem = shift;
269             # this is from `perldata(1)'
270             if($elem =~ /^([+-]?)(?=\d|\.\d)\d*(\.\d*)?([Ee]([+-]?\d+))?$/) {
271             return Frontier::RPC2::Double->new($elem);
272             } else {
273             die "error in rendering RPC type \`$elem\' not a double\n";
274             }
275             }
276              
277             sub int {
278             my $self = shift;
279             my $elem = shift;
280             # this is from `perldata(1)'
281             if($elem =~ /^[+-]?\d+$/) {
282             return Frontier::RPC2::Integer->new($elem);
283             } else {
284             die "error in rendering RPC type \`$elem\' not an int\n";
285             }
286             }
287              
288             sub string {
289             my $self = shift;
290              
291             return Frontier::RPC2::String->new(@_);
292             }
293              
294             sub date_time {
295             my $self = shift;
296              
297             return Frontier::RPC2::DateTime::ISO8601->new(@_);
298             }
299              
300             ######################################################################
301             ###
302             ### XML::Parser callbacks
303             ###
304              
305             sub die {
306             my $expat = shift; my $message = shift;
307              
308             die $message
309             . "at line " . $expat->current_line
310             . " column " . $expat->current_column . "\n";
311             }
312              
313             sub init {
314             my $expat = shift;
315              
316             $expat->{'rpc_state'} = [];
317             $expat->{'rpc_container'} = [ [] ];
318             $expat->{'rpc_member_name'} = [];
319             $expat->{'rpc_type'} = undef;
320             $expat->{'rpc_args'} = undef;
321             }
322              
323             # FIXME this state machine wouldn't be necessary if we had a DTD.
324             sub start {
325             my $expat = shift; my $tag = shift;
326              
327             my $state = $expat->{'rpc_state'}[-1];
328              
329             if (!defined $state) {
330             if ($tag eq 'methodCall') {
331             $expat->{'rpc_type'} = 'call';
332             push @{ $expat->{'rpc_state'} }, 'want_method_name';
333             } elsif ($tag eq 'methodResponse') {
334             push @{ $expat->{'rpc_state'} }, 'method_response';
335             } else {
336             Frontier::RPC2::die($expat, "unknown RPC type \`$tag'\n");
337             }
338             } elsif ($state eq 'want_method_name') {
339             Frontier::RPC2::die($expat, "wanted \`methodName' tag, got \`$tag'\n")
340             if ($tag ne 'methodName');
341             push @{ $expat->{'rpc_state'} }, 'method_name';
342             $expat->{'rpc_text'} = "";
343             } elsif ($state eq 'method_response') {
344             if ($tag eq 'params') {
345             $expat->{'rpc_type'} = 'response';
346             push @{ $expat->{'rpc_state'} }, 'params';
347             } elsif ($tag eq 'fault') {
348             $expat->{'rpc_type'} = 'fault';
349             push @{ $expat->{'rpc_state'} }, 'want_value';
350             }
351             } elsif ($state eq 'want_params') {
352             Frontier::RPC2::die($expat, "wanted \`params' tag, got \`$tag'\n")
353             if ($tag ne 'params');
354             push @{ $expat->{'rpc_state'} }, 'params';
355             } elsif ($state eq 'params') {
356             Frontier::RPC2::die($expat, "wanted \`param' tag, got \`$tag'\n")
357             if ($tag ne 'param');
358             push @{ $expat->{'rpc_state'} }, 'want_param_name_or_value';
359             } elsif ($state eq 'want_param_name_or_value') {
360             if ($tag eq 'value') {
361             $expat->{'may_get_cdata'} = 1;
362             $expat->{'rpc_text'} = "";
363             push @{ $expat->{'rpc_state'} }, 'value';
364             } elsif ($tag eq 'name') {
365             push @{ $expat->{'rpc_state'} }, 'param_name';
366             } else {
367             Frontier::RPC2::die($expat, "wanted \`value' or \`name' tag, got \`$tag'\n");
368             }
369             } elsif ($state eq 'param_name') {
370             Frontier::RPC2::die($expat, "wanted parameter name data, got tag \`$tag'\n");
371             } elsif ($state eq 'want_value') {
372             Frontier::RPC2::die($expat, "wanted \`value' tag, got \`$tag'\n")
373             if ($tag ne 'value');
374             $expat->{'rpc_text'} = "";
375             $expat->{'may_get_cdata'} = 1;
376             push @{ $expat->{'rpc_state'} }, 'value';
377             } elsif ($state eq 'value') {
378             $expat->{'may_get_cdata'} = 0;
379             if ($tag eq 'array') {
380             push @{ $expat->{'rpc_container'} }, [];
381             push @{ $expat->{'rpc_state'} }, 'want_data';
382             } elsif ($tag eq 'struct') {
383             push @{ $expat->{'rpc_container'} }, {};
384             push @{ $expat->{'rpc_member_name'} }, undef;
385             push @{ $expat->{'rpc_state'} }, 'struct';
386             } elsif ($scalars{$tag}) {
387             $expat->{'rpc_text'} = "";
388             push @{ $expat->{'rpc_state'} }, 'cdata';
389             } else {
390             Frontier::RPC2::die($expat, "wanted a data type, got \`$tag'\n");
391             }
392             } elsif ($state eq 'want_data') {
393             Frontier::RPC2::die($expat, "wanted \`data', got \`$tag'\n")
394             if ($tag ne 'data');
395             push @{ $expat->{'rpc_state'} }, 'array';
396             } elsif ($state eq 'array') {
397             Frontier::RPC2::die($expat, "wanted \`value' tag, got \`$tag'\n")
398             if ($tag ne 'value');
399             $expat->{'rpc_text'} = "";
400             $expat->{'may_get_cdata'} = 1;
401             push @{ $expat->{'rpc_state'} }, 'value';
402             } elsif ($state eq 'struct') {
403             Frontier::RPC2::die($expat, "wanted \`member' tag, got \`$tag'\n")
404             if ($tag ne 'member');
405             push @{ $expat->{'rpc_state'} }, 'want_member_name';
406             } elsif ($state eq 'want_member_name') {
407             Frontier::RPC2::die($expat, "wanted \`name' tag, got \`$tag'\n")
408             if ($tag ne 'name');
409             push @{ $expat->{'rpc_state'} }, 'member_name';
410             $expat->{'rpc_text'} = "";
411             } elsif ($state eq 'member_name') {
412             Frontier::RPC2::die($expat, "wanted data, got tag \`$tag'\n");
413             } elsif ($state eq 'cdata') {
414             Frontier::RPC2::die($expat, "wanted data, got tag \`$tag'\n");
415             } else {
416             Frontier::RPC2::die($expat, "internal error, unknown state \`$state'\n");
417             }
418             }
419              
420             sub end {
421             my $expat = shift; my $tag = shift;
422              
423             my $state = pop @{ $expat->{'rpc_state'} };
424              
425             if ($state eq 'cdata') {
426             my $value = $expat->{'rpc_text'};
427             if ($tag eq 'base64') {
428             $value = Frontier::RPC2::Base64->new($value);
429             } elsif ($tag eq 'boolean') {
430             $value = Frontier::RPC2::Boolean->new($value);
431             } elsif ($tag eq 'dateTime.iso8601') {
432             $value = Frontier::RPC2::DateTime::ISO8601->new($value);
433             } elsif ($expat->{'use_objects'}) {
434             if ($tag eq 'i4' or $tag eq 'int') {
435             $value = Frontier::RPC2::Integer->new($value);
436             } elsif ($tag eq 'float') {
437             $value = Frontier::RPC2::Float->new($value);
438             } elsif ($tag eq 'string') {
439             $value = Frontier::RPC2::String->new($value);
440             }
441             }
442             $expat->{'rpc_value'} = $value;
443             } elsif ($state eq 'member_name') {
444             $expat->{'rpc_member_name'}[-1] = $expat->{'rpc_text'};
445             $expat->{'rpc_state'}[-1] = 'want_value';
446             } elsif ($state eq 'method_name') {
447             $expat->{'rpc_method_name'} = $expat->{'rpc_text'};
448             $expat->{'rpc_state'}[-1] = 'want_params';
449             } elsif ($state eq 'struct') {
450             $expat->{'rpc_value'} = pop @{ $expat->{'rpc_container'} };
451             pop @{ $expat->{'rpc_member_name'} };
452             } elsif ($state eq 'array') {
453             $expat->{'rpc_value'} = pop @{ $expat->{'rpc_container'} };
454             } elsif ($state eq 'value') {
455             # the rpc_text is a string if no type tags were given
456             if ($expat->{'may_get_cdata'}) {
457             $expat->{'may_get_cdata'} = 0;
458             if ($expat->{'use_objects'}) {
459             $expat->{'rpc_value'}
460             = Frontier::RPC2::String->new($expat->{'rpc_text'});
461             } else {
462             $expat->{'rpc_value'} = $expat->{'rpc_text'};
463             }
464             }
465             my $container = $expat->{'rpc_container'}[-1];
466             if (ref($container) eq 'ARRAY') {
467             push @$container, $expat->{'rpc_value'};
468             } elsif (ref($container) eq 'HASH') {
469             $container->{ $expat->{'rpc_member_name'}[-1] } = $expat->{'rpc_value'};
470             }
471             }
472             }
473              
474             sub char {
475             my $expat = shift; my $text = shift;
476              
477             $expat->{'rpc_text'} .= $text;
478             }
479              
480             sub proc {
481             }
482              
483             sub final {
484             my $expat = shift;
485              
486             $expat->{'rpc_value'} = pop @{ $expat->{'rpc_container'} };
487            
488             return {
489             value => $expat->{'rpc_value'},
490             type => $expat->{'rpc_type'},
491             method_name => $expat->{'rpc_method_name'},
492             };
493             }
494              
495             package Frontier::RPC2::DataType;
496              
497             sub new {
498             my $type = shift; my $value = shift;
499              
500             return bless \$value, $type;
501             }
502              
503             # `repr' returns the XML representation of this data, which may be
504             # different [in the future] from what is returned from `value'
505             sub repr {
506             my $self = shift;
507              
508             return $$self;
509             }
510              
511             # sets or returns the usable value of this data
512             sub value {
513             my $self = shift;
514             @_ ? ($$self = shift) : $$self;
515             }
516              
517             package Frontier::RPC2::Base64;
518              
519             use vars qw{@ISA};
520             @ISA = qw{Frontier::RPC2::DataType};
521              
522             package Frontier::RPC2::Boolean;
523              
524             use vars qw{@ISA};
525             @ISA = qw{Frontier::RPC2::DataType};
526              
527             package Frontier::RPC2::Integer;
528              
529             use vars qw{@ISA};
530             @ISA = qw{Frontier::RPC2::DataType};
531              
532             package Frontier::RPC2::String;
533              
534             use vars qw{@ISA};
535             @ISA = qw{Frontier::RPC2::DataType};
536              
537             sub repr {
538             my $self = shift;
539             my $value = $$self;
540             $value =~ s/([&<>\"])/$Frontier::RPC2::char_entities{$1}/ge;
541             $value;
542             }
543              
544             package Frontier::RPC2::Double;
545              
546             use vars qw{@ISA};
547             @ISA = qw{Frontier::RPC2::DataType};
548              
549             package Frontier::RPC2::DateTime::ISO8601;
550              
551             use vars qw{@ISA};
552             @ISA = qw{Frontier::RPC2::DataType};
553              
554             =head1 NAME
555              
556             Frontier::RPC2 - encode/decode RPC2 format XML
557              
558             =head1 SYNOPSIS
559              
560             use Frontier::RPC2;
561              
562             $coder = Frontier::RPC2->new;
563              
564             $xml_string = $coder->encode_call($method, @args);
565             $xml_string = $coder->encode_response($result);
566             $xml_string = $coder->encode_fault($code, $message);
567              
568             $call = $coder->decode($xml_string);
569              
570             $response_xml = $coder->serve($request_xml, $methods);
571              
572             $boolean_object = $coder->boolean($boolean);
573             $date_time_object = $coder->date_time($date_time);
574             $base64_object = $coder->base64($base64);
575             $int_object = $coder->int(42);
576             $float_object = $coder->float(3.14159);
577             $string_object = $coder->string("Foo");
578              
579             =head1 DESCRIPTION
580              
581             I<Frontier::RPC2> encodes and decodes XML RPC calls.
582              
583             =over 4
584              
585             =item $coder = Frontier::RPC2->new( I<OPTIONS> )
586              
587             Create a new encoder/decoder. The following option is supported:
588              
589             =over 4
590              
591             =item encoding
592              
593             The XML encoding to be specified in the XML declaration of encoded RPC
594             requests or responses. Decoded results may have a different encoding
595             specified; XML::Parser will convert decoded data to UTF-8. The
596             default encoding is none, which uses XML 1.0's default of UTF-8. For
597             example:
598              
599             $server = Frontier::RPC2->new( 'encoding' => 'ISO-8859-1' );
600              
601             =item use_objects
602              
603             If set to a non-zero value will convert incoming E<lt>i4E<gt>,
604             E<lt>floatE<gt>, and E<lt>stringE<gt> values to objects instead of
605             scalars. See int(), float(), and string() below for more details.
606              
607             =back
608              
609             =item $xml_string = $coder->encode_call($method, @args)
610              
611             `C<encode_call>' converts a method name and it's arguments into an
612             RPC2 `C<methodCall>' element, returning the XML fragment.
613              
614             =item $xml_string = $coder->encode_response($result)
615              
616             `C<encode_response>' converts the return value of a procedure into an
617             RPC2 `C<methodResponse>' element containing the result, returning the
618             XML fragment.
619              
620             =item $xml_string = $coder->encode_fault($code, $message)
621              
622             `C<encode_fault>' converts a fault code and message into an RPC2
623             `C<methodResponse>' element containing a `C<fault>' element, returning
624             the XML fragment.
625              
626             =item $call = $coder->decode($xml_string)
627              
628             `C<decode>' converts an XML string containing an RPC2 `C<methodCall>'
629             or `C<methodResponse>' element into a hash containing three members,
630             `C<type>', `C<value>', and `C<method_name>'. `C<type>' is one of
631             `C<call>', `C<response>', or `C<fault>'. `C<value>' is array
632             containing the parameters or result of the RPC. For a `C<call>' type,
633             `C<value>' contains call's parameters and `C<method_name>' contains
634             the method being called. For a `C<response>' type, the `C<value>'
635             array contains call's result. For a `C<fault>' type, the `C<value>'
636             array contains a hash with the two members `C<faultCode>' and
637             `C<faultMessage>'.
638              
639             =item $response_xml = $coder->serve($request_xml, $methods)
640              
641             `C<serve>' decodes `C<$request_xml>', looks up the called method name
642             in the `C<$methods>' hash and calls it, and then encodes and returns
643             the response as XML.
644              
645             =item $boolean_object = $coder->boolean($boolean);
646              
647             =item $date_time_object = $coder->date_time($date_time);
648              
649             =item $base64_object = $coder->base64($base64);
650              
651             These methods create and return XML-RPC-specific datatypes that can be
652             passed to the encoder. The decoder may also return these datatypes.
653             The corresponding package names (for use with `C<ref()>', for example)
654             are `C<Frontier::RPC2::Boolean>',
655             `C<Frontier::RPC2::DateTime::ISO8601>', and
656             `C<Frontier::RPC2::Base64>'.
657              
658             You can change and retrieve the value of boolean, date/time, and
659             base64 data using the `C<value>' method of those objects, i.e.:
660              
661             $boolean = $boolean_object->value;
662              
663             $boolean_object->value(1);
664              
665             Note: `C<base64()>' does I<not> encode or decode base64 data for you,
666             you must use MIME::Base64 or similar module for that.
667              
668             =item $int_object = $coder->int(42);
669              
670             =item $float_object = $coder->float(3.14159);
671              
672             =item $string_object = $coder->string("Foo");
673              
674             By default, you may pass ordinary Perl values (scalars) to be encoded.
675             RPC2 automatically converts them to XML-RPC types if they look like an
676             integer, float, or as a string. This assumption causes problems when
677             you want to pass a string that looks like "0096", RPC2 will convert
678             that to an E<lt>i4E<gt> because it looks like an integer. With these
679             methods, you could now create a string object like this:
680              
681             $part_num = $coder->string("0096");
682              
683             and be confident that it will be passed as an XML-RPC string. You can
684             change and retrieve values from objects using value() as described
685             above.
686              
687             =back
688              
689             =head1 SEE ALSO
690              
691             perl(1), Frontier::Daemon(3), Frontier::Client(3)
692              
693             <http://www.scripting.com/frontier5/xml/code/rpc.html>
694              
695             =head1 AUTHOR
696              
697             Ken MacLeod <ken@bitsko.slc.ut.us>
698              
699             =cut
700              
701             1;