Document handlers.
[irspy-moved-to-github.git] / lib / ZOOM / Pod.pm
1 # $Id: Pod.pm,v 1.7 2006-05-11 15:51:36 mike Exp $
2
3 package ZOOM::Pod;
4
5 use strict;
6 use warnings;
7
8 use ZOOM;
9
10 BEGIN {
11     # Just register the name
12     ZOOM::Log::mask_str("pod");
13     ZOOM::Log::mask_str("pod_unhandled");
14 }
15
16 =head1 NAME
17
18 ZOOM::Pod - Perl extension for handling pods of concurrent ZOOM connections
19
20 =head1 SYNOPSIS
21
22  use ZOOM::Pod;
23
24  $pod = new ZOOM::Pod("bagel.indexdata.com/gils",
25                       "bagel.indexdata.com/marc");
26  $pod->callback(ZOOM::Event::RECV_SEARCH, \&completed_search);
27  $pod->callback(ZOOM::Event::RECV_RECORD, \&got_record);
28  $pod->search_pqf("the");
29  $err = $pod->wait();
30  die "$pod->wait() failed with error $err" if $err;
31
32  sub completed_search {
33      ($conn, undef, $rs) = @_;
34      print $conn->option("host"), ": found ", $rs->size(), " records\n";
35      $rs->records(0, 1, 0); # Queues a request for the record
36      return 0;
37  }
38
39  sub got_record {
40      ($conn, undef, $rs) = @_;
41      $rec = $rs->record(0);
42      print $conn->option("host"), ": got $rec = '", $rec->render(), "'\n";
43      return 0;
44  }
45
46 =head1 DESCRIPTION
47
48 C<ZOOM:Pod> provides an API that simplifies asynchronous programming
49 using ZOOM.  A pod is a collection of asynchronous connections that
50 are run simultaneously to achieve broadcast searching and retrieval.
51 When a pod is created, a set of connections (or target-strings to
52 connect to) are specified.  Thereafter, they are treated as a unit,
53 and methods for searching, option-setting, etc. that are invoked on
54 the pod are delegated to each of its members.
55
56 The key method on a pod is C<wait()>, which enters a loop accepting
57 and dispatching events occurring on any of the connections in the pod.
58 Unless interrupted,the loop runs until there are no more events left,
59 i.e. no searches are outstanding and no requested records have still
60 to be received.
61
62 Event dispatching is done by means of callback functions, which can be
63 registered for each event.  A registered callback is invoked whenever
64 a corresponding event occurs.  A special callback can be nominated to
65 handle errors.
66
67 =head1 METHODS
68
69 =head2 new()
70
71  $pod = new ZOOM::Pod($conn1, $conn2, $conn3);
72  $pod = new ZOOM::Pod("bagel.indexdata.com/gils",
73                       "bagel.indexdata.com/marc");
74
75
76 Creates a new pod containing one or more connections.  Each connection
77 may be specified either by an existing C<ZOOM::Connection> object,
78 which I<must> be asynchronous; or by a ZOOM target string, in which
79 case the pod module will make the connection object itself.
80
81 Returns the new pod.
82
83 =cut
84
85 sub new {
86     my $class = shift();
87     my(@conn) = @_;
88
89     die "$class with no connections" if @conn == 0;
90     my @state; # Hashrefs with application state associated with connections
91     foreach my $conn (@conn) {
92         if (!ref $conn) {
93             $conn = new ZOOM::Connection($conn, 0, async => 1);
94             # The $conn object is always made, even if no there's no
95             # server.  Such errors are caught later, by the _check()
96             # call in wait(). 
97         }
98         push @state, {};
99     }
100
101     return bless {
102         conn => \@conn,
103         state => \@state,
104         rs => [],
105         callback => {},
106     }, $class;
107 }
108
109 =head2 option()
110
111  $oldElemSet = $pod->option("elementSetName");
112  $pod->option(elementSetName => "b");
113
114 Sets a specified option in all the connections in a pod.  Returns the
115 old value that the option had in first of the connections in the pod:
116 be aware that this value was not necessarily shared by all the members
117 of the pod ... but that is true often enough to be useful.
118
119 =cut
120
121 sub option {
122     my $this = shift();
123     my($key, $value) = @_;
124
125     my $old = $this->{conn}->[0]->option($key);
126     foreach my $conn (@{ $this->{conn} }) {
127         $conn->option($key, $value);
128     }
129
130     return $old;
131 }
132
133 =head2 callback()
134
135  $pod->callback(ZOOM::Event::RECV_SEARCH, \&completed_search);
136  $pod->callback("exception", sub { print "never mind: $@\n"; return 0 } );
137
138 Registers a callback to be invoked by the pod when an event happens.
139 Callback functions are invoked by C<wait()> (q.v.).
140
141 When registering a callback, the first argument is an event-code - one
142 of those defined in the C<ZOOM::Event> enumeration - and the second is
143 a function reference, or equivalently an inline code-fragment.  It is
144 acceptable to nominate the same function as the callback for multiple
145 events, by multiple invocations of C<callback()>.
146
147 When an event occurs during the execution of C<wait()>, the relevant
148 callback function is passed four arguments: the connection that the
149 event happened on; a state hash-reference associated with the
150 connection; the result-set associated with the connection; and the
151 event-type (so that a single function that handles events of multiple
152 types can switch on the code where necessary).  The callback function
153 can handle the event as it wishes, finishing up by returning an
154 integer.  If this is zero, then C<wait()> continues as normal; if it
155 is anything else, then that value is immediately returned from
156 C<wait()>.
157
158 So a typical, simple, event-handler might look like this:
159
160  sub got_event {
161       ($conn, $state, $rs, $event) = @_;
162       print "event $event on connection ", $conn->option("host"), "\n";
163       print "Found ", $rs->size(), " records\n"
164           if $event == ZOOM::Event::RECV_SEARCH;
165       return 0;
166  }
167
168 In addition to the event-type callbacks discussed above, there is a
169 special callback, C<"exception">, which is invoked if an exception
170 occurs.  This will nearly always be a ZOOM error, but this can be
171 tested using C<ref($@) eq "ZOOM::Exception">.  This callback is
172 invoked with the same arguments as described above, except that
173 instead of the event-type, the fourth argument is a copy of the
174 exception, C<$@>.  Exception-handling callbacks may of course re-throw
175 the exception using C<die $@>.
176
177 So a simple error-handler might look like this:
178
179  sub got_error {
180       ($conn, $state, $rs, $exception) = @_;
181       if ($exception->isa("ZOOM::Exception")) {
182           print "Caught error $exception -- continuing";
183           return 0;
184       }
185       die $exception;
186  }
187
188 I<### state>
189
190 =cut
191
192 sub callback {
193     my $this = shift();
194     my($event, $sub) = @_;
195
196     my $old = $this->{callback}->{$event};
197     $this->{callback}->{$event} = $sub
198         if defined $sub;
199
200     return $old;
201 }
202
203 =head2 search_pqf()
204
205 I<###>
206
207 B<WARNING!>
208 An important simplifying assumption is that each connection can only
209 have one search active on it at a time - this allows the pod to
210 maintain a one-to-one mapping between connections and result-sets.  
211
212 =cut
213
214 sub search_pqf {
215     my $this = shift();
216     my($pqf) = @_;
217
218     foreach my $i (0..@{ $this->{conn} }-1) {
219         $this->{rs}->[$i] = $this->{conn}->[$i]->search_pqf($pqf);
220     }
221 }
222
223 =head2 wait()
224
225 I<###>
226
227 =cut
228
229 sub wait {
230     my $this = shift();
231     my $res = 0;
232
233     while ((my $i = ZOOM::event($this->{conn})) != 0) {
234         my $conn = $this->{conn}->[$i-1];
235         my $ev = $conn->last_event();
236         my $evstr = ZOOM::event_str($ev);
237         ZOOM::Log::log("pod", "connection ", $i-1, ": $evstr");
238
239         eval {
240             $conn->_check();
241         }; if ($@) {
242             my $sub = $this->{callback}->{exception};
243             die $@ if !defined $sub;
244             $res = &$sub($conn, $this->{state}->[$i-1],
245                          $this->{rs}->[$i-1], $@);
246             last if $res != 0;
247             next;
248         }
249
250         my $sub = $this->{callback}->{$ev};
251         if (defined $sub) {
252             $res = &$sub($conn, $this->{state}->[$i-1],
253                          $this->{rs}->[$i-1], $ev);
254             last if $res != 0;
255         } else {
256             ZOOM::Log::log("pod_unhandled", "unhandled event $ev ($evstr)");
257         }
258     }
259
260     return $res;
261 }
262
263
264 =head1 SEE ALSO
265
266 The underlying
267 C<ZOOM>
268 module (part of the
269 C<Net::Z3950::ZOOM>
270 distribution).
271
272 =head1 AUTHOR
273
274 Mike Taylor, E<lt>mike@indexdata.comE<gt>
275
276 =head1 COPYRIGHT AND LICENCE
277
278 Copyright (C) 2006 by Index Data.
279
280 This library is free software; you can redistribute it and/or modify
281 it under the same terms as Perl itself, either Perl version 5.8.4 or,
282 at your option, any later version of Perl 5 you may have available.
283
284 =cut
285
286
287 1;