blib/lib/WebService/ISBNDB/Agent/REST.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 25 | 27 | 92.5 |
branch | n/a | ||
condition | n/a | ||
subroutine | 9 | 9 | 100.0 |
pod | n/a | ||
total | 34 | 36 | 94.4 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | ############################################################################### | ||||||
2 | # | ||||||
3 | # This file copyright (c) 2006-2008 by Randy J. Ray, all rights reserved | ||||||
4 | # | ||||||
5 | # See "LICENSE" in the documentation for licensing and redistribution terms. | ||||||
6 | # | ||||||
7 | ############################################################################### | ||||||
8 | # | ||||||
9 | # $Id: REST.pm 49 2008-04-06 10:45:43Z $ | ||||||
10 | # | ||||||
11 | # Description: This is the protocol-implementation class for making | ||||||
12 | # requests via the REST interface. At present, this is the | ||||||
13 | # the only supported interface. | ||||||
14 | # | ||||||
15 | # Functions: parse_authors | ||||||
16 | # parse_books | ||||||
17 | # parse_categories | ||||||
18 | # parse_publishers | ||||||
19 | # parse_subjects | ||||||
20 | # request | ||||||
21 | # request_method | ||||||
22 | # request_uri | ||||||
23 | # | ||||||
24 | # Libraries: Class::Std | ||||||
25 | # Error | ||||||
26 | # XML::LibXML | ||||||
27 | # WebService::ISBNDB::Agent | ||||||
28 | # WebService::ISBNDB::Iterator | ||||||
29 | # | ||||||
30 | # Global Consts: $VERSION | ||||||
31 | # $BASEURL | ||||||
32 | # | ||||||
33 | ############################################################################### | ||||||
34 | |||||||
35 | package WebService::ISBNDB::Agent::REST; | ||||||
36 | |||||||
37 | 9 | 9 | 25407 | use 5.006; | |||
9 | 35 | ||||||
9 | 356 | ||||||
38 | 9 | 9 | 49 | use strict; | |||
9 | 15 | ||||||
9 | 267 | ||||||
39 | 9 | 9 | 50 | use warnings; | |||
9 | 20 | ||||||
9 | 386 | ||||||
40 | 9 | 9 | 43 | no warnings 'redefine'; | |||
9 | 17 | ||||||
9 | 361 | ||||||
41 | 9 | 9 | 51 | use vars qw($VERSION $CAN_PARSE_DATES); | |||
9 | 18 | ||||||
9 | 433 | ||||||
42 | 9 | 9 | 66 | use base 'WebService::ISBNDB::Agent'; | |||
9 | 20 | ||||||
9 | 977 | ||||||
43 | |||||||
44 | 9 | 9 | 50 | use Class::Std; | |||
9 | 17 | ||||||
9 | 78 | ||||||
45 | 9 | 9 | 931 | use Error; | |||
9 | 17 | ||||||
9 | 83 | ||||||
46 | 9 | 9 | 4473 | use XML::LibXML; | |||
0 | |||||||
0 | |||||||
47 | |||||||
48 | use WebService::ISBNDB::Iterator; | ||||||
49 | |||||||
50 | $VERSION = "0.31"; | ||||||
51 | |||||||
52 | BEGIN | ||||||
53 | { | ||||||
54 | eval "use Date::Parse"; | ||||||
55 | $CAN_PARSE_DATES = ($@) ? 0 : 1; | ||||||
56 | } | ||||||
57 | |||||||
58 | my %baseurl : ATTR(:name |
||||||
59 | my %authors : ATTR(:name |
||||||
60 | my %books : ATTR(:name |
||||||
61 | my %categories : ATTR(:name |
||||||
62 | my %publishers : ATTR(:name |
||||||
63 | my %subjects : ATTR(:name |
||||||
64 | |||||||
65 | my %API_MAP = ( | ||||||
66 | API => {}, | ||||||
67 | Authors => \%authors, | ||||||
68 | Books => \%books, | ||||||
69 | Categories => \%categories, | ||||||
70 | Publishers => \%publishers, | ||||||
71 | Subjects => \%subjects, | ||||||
72 | ); | ||||||
73 | |||||||
74 | my %parse_table = ( | ||||||
75 | Authors => \&parse_authors, | ||||||
76 | Books => \&parse_books, | ||||||
77 | Categories => \&parse_categories, | ||||||
78 | Publishers => \&parse_publishers, | ||||||
79 | Subjects => \&parse_subjects, | ||||||
80 | ); | ||||||
81 | |||||||
82 | ############################################################################### | ||||||
83 | # | ||||||
84 | # Sub Name: new | ||||||
85 | # | ||||||
86 | # Description: Pass off to the super-class constructor, which handles | ||||||
87 | # the special cases for arguments. | ||||||
88 | # | ||||||
89 | ############################################################################### | ||||||
90 | sub new | ||||||
91 | { | ||||||
92 | shift->SUPER::new(@_); | ||||||
93 | } | ||||||
94 | |||||||
95 | ############################################################################### | ||||||
96 | # | ||||||
97 | # Sub Name: protocol | ||||||
98 | # | ||||||
99 | # Description: Return the name of the protocol we implement; if an | ||||||
100 | # argument is passed in, test that the argument matches | ||||||
101 | # our protocol. | ||||||
102 | # | ||||||
103 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
104 | # $self in ref Object | ||||||
105 | # $test in scalar If passed, test against our | ||||||
106 | # protocol | ||||||
107 | # | ||||||
108 | # Returns: Success: string or 1 | ||||||
109 | # Failure: 0 if we're testing and the protocol is no match | ||||||
110 | # | ||||||
111 | ############################################################################### | ||||||
112 | sub protocol | ||||||
113 | { | ||||||
114 | my ($self, $test) = @_; | ||||||
115 | |||||||
116 | return $test ? $test =~ /^rest$/i : 'REST'; | ||||||
117 | } | ||||||
118 | |||||||
119 | ############################################################################### | ||||||
120 | # | ||||||
121 | # Sub Name: request_method | ||||||
122 | # | ||||||
123 | # Description: Return the HTTP method used for requests | ||||||
124 | # | ||||||
125 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
126 | # $self in ref Object | ||||||
127 | # $obj in ref Object from the API hierarchy | ||||||
128 | # $args in hashref Arguments to the request | ||||||
129 | # | ||||||
130 | # Returns: 'GET' | ||||||
131 | # | ||||||
132 | ############################################################################### | ||||||
133 | sub request_method : RESTRICTED | ||||||
134 | { | ||||||
135 | 'GET'; | ||||||
136 | } | ||||||
137 | |||||||
138 | ############################################################################### | ||||||
139 | # | ||||||
140 | # Sub Name: request_uri | ||||||
141 | # | ||||||
142 | # Description: Return a URI object representing the target URL for the | ||||||
143 | # request. | ||||||
144 | # | ||||||
145 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
146 | # $self in ref Object | ||||||
147 | # $obj in ref Object from the API hierarchy | ||||||
148 | # $args in hashref Arguments to the request | ||||||
149 | # | ||||||
150 | # Returns: Success: URI instance | ||||||
151 | # Failure: throws Error::Simple | ||||||
152 | # | ||||||
153 | ############################################################################### | ||||||
154 | sub request_uri : RESTRICTED | ||||||
155 | { | ||||||
156 | my ($self, $obj, $args) = @_; | ||||||
157 | |||||||
158 | my $id = ident $self; | ||||||
159 | |||||||
160 | # $obj should already have been resolved, so the methods on it should work | ||||||
161 | my $key = $obj->get_api_key; | ||||||
162 | my $apiloc = $API_MAP{$obj->get_type}->{$id}; | ||||||
163 | my $argscopy = { %$args }; | ||||||
164 | |||||||
165 | # If $apiloc is null, we can't go on | ||||||
166 | throw Error::Simple("No API URL for the type '" . $obj->get_type . "'") | ||||||
167 | unless $apiloc; | ||||||
168 | |||||||
169 | # Only add the "access_key" argument if it isn't already present. They may | ||||||
170 | # have overridden it. It will have come from the enclosing object under | ||||||
171 | # the label "api_key". | ||||||
172 | $argscopy->{access_key} = $argscopy->{api_key} || $key; | ||||||
173 | delete $argscopy->{api_key}; # Just in case, so to not confuse their API | ||||||
174 | # Build the request parameters list | ||||||
175 | my @args = (); | ||||||
176 | for $key (sort keys %$argscopy) | ||||||
177 | { | ||||||
178 | if (ref $argscopy->{$key}) | ||||||
179 | { | ||||||
180 | # Some params, like "results", can appear multiple times. This is | ||||||
181 | # implemented as the value being an array reference. | ||||||
182 | for (@{$argscopy->{$key}}) | ||||||
183 | { | ||||||
184 | push(@args, "$key=$_"); | ||||||
185 | } | ||||||
186 | } | ||||||
187 | else | ||||||
188 | { | ||||||
189 | # Normal, one-shot argument | ||||||
190 | push(@args, "$key=$argscopy->{$key}"); | ||||||
191 | } | ||||||
192 | } | ||||||
193 | |||||||
194 | URI->new("$baseurl{$id}$apiloc?" . join('&', @args)); | ||||||
195 | } | ||||||
196 | |||||||
197 | ############################################################################### | ||||||
198 | # | ||||||
199 | # Sub Name: request | ||||||
200 | # | ||||||
201 | # Description: | ||||||
202 | # | ||||||
203 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
204 | # $self in ref Object | ||||||
205 | # $obj in scalar Object or type name or class | ||||||
206 | # $args in hashref Hash reference of arguments to | ||||||
207 | # the raw request | ||||||
208 | # | ||||||
209 | # Returns: Success: based on $single, a API-derived object or list | ||||||
210 | # Failure: throws Error::Simple | ||||||
211 | # | ||||||
212 | ############################################################################### | ||||||
213 | sub request : RESTRICTED | ||||||
214 | { | ||||||
215 | my ($self, $obj, $args) = @_; | ||||||
216 | $obj = $self->resolve_obj($obj); | ||||||
217 | |||||||
218 | my $content = $self->raw_request($obj, $args); | ||||||
219 | |||||||
220 | # First off, parse $content as XML | ||||||
221 | my $parser = XML::LibXML->new(); | ||||||
222 | my $dom = eval { $parser->parse_string($$content); }; | ||||||
223 | throw Error::Simple("XML parse error: $@") if $@; | ||||||
224 | |||||||
225 | my $top_elt = $dom->documentElement(); | ||||||
226 | throw Error::Simple("Service error: " . $self->_lr_trim($dom->textContent)) | ||||||
227 | if (($dom) = $top_elt->getElementsByTagName('ErrorMessage')); | ||||||
228 | my ($value, $stats) = $parse_table{$obj->get_type}->($self, $top_elt); | ||||||
229 | |||||||
230 | # Add two pieces to $stats that the iterator will need | ||||||
231 | $stats->{contents} = $value; | ||||||
232 | $stats->{request_args} = $args; | ||||||
233 | |||||||
234 | WebService::ISBNDB::Iterator->new($stats); | ||||||
235 | } | ||||||
236 | |||||||
237 | ############################################################################### | ||||||
238 | # | ||||||
239 | # Sub Name: parse_authors | ||||||
240 | # | ||||||
241 | # Description: | ||||||
242 | # | ||||||
243 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
244 | # $self in ref Object | ||||||
245 | # $root_elt in ref XML::LibXML::Node object | ||||||
246 | # | ||||||
247 | # Returns: Success: listref | ||||||
248 | # Failure: throws Error::Simple | ||||||
249 | # | ||||||
250 | ############################################################################### | ||||||
251 | sub parse_authors : RESTRICTED | ||||||
252 | { | ||||||
253 | my ($self, $root_elt) = @_; | ||||||
254 | |||||||
255 | my ($total_results, $page_size, $page_number, $shown_results, $list_elt, | ||||||
256 | @authorblocks, $authors, $one_author, $authorref, $tmp); | ||||||
257 | # The class should already be loaded before we got to this point: | ||||||
258 | my $class = WebService::ISBNDB::API->class_for_type('Authors'); | ||||||
259 | |||||||
260 | # For now, we aren't interested in the root element (the only useful piece | ||||||
261 | # of information in it is the server-time of the request). So skip down a | ||||||
262 | # level-- there should be exactly one AuthorList element. | ||||||
263 | ($list_elt) = $root_elt->getElementsByTagName('AuthorList'); | ||||||
264 | throw Error::Simple("No |
||||||
265 | unless (ref $list_elt); | ||||||
266 | |||||||
267 | # These attributes live on the AuthorList element | ||||||
268 | $total_results = $list_elt->getAttribute('total_results'); | ||||||
269 | $page_size = $list_elt->getAttribute('page_size'); | ||||||
270 | $page_number = $list_elt->getAttribute('page_number'); | ||||||
271 | $shown_results = $list_elt->getAttribute('shown_results'); | ||||||
272 | |||||||
273 | # Start with no categories in the list, and get the |
||||||
274 | $authors = []; | ||||||
275 | @authorblocks = $list_elt->getElementsByTagName('AuthorData'); | ||||||
276 | throw Error::Simple("Number of |
||||||
277 | "'shown_results' value") | ||||||
278 | unless ($shown_results == @authorblocks); | ||||||
279 | for $one_author (@authorblocks) | ||||||
280 | { | ||||||
281 | # Clean slate | ||||||
282 | $authorref = {}; | ||||||
283 | |||||||
284 | # ID is an attribute of AuthorData | ||||||
285 | $authorref->{id} = $one_author->getAttribute('person_id'); | ||||||
286 | # Name is just text | ||||||
287 | if (($tmp) = $one_author->getElementsByTagName('Name')) | ||||||
288 | { | ||||||
289 | $authorref->{name} = $self->_lr_trim($tmp->textContent); | ||||||
290 | } | ||||||
291 | # The element holds some data in attributes |
||||||
292 | if (($tmp) = $one_author->getElementsByTagName('Details')) | ||||||
293 | { | ||||||
294 | $authorref->{first_name} = | ||||||
295 | $self->_lr_trim($tmp->getAttribute('first_name')); | ||||||
296 | $authorref->{last_name} = | ||||||
297 | $self->_lr_trim($tmp->getAttribute('last_name')); | ||||||
298 | $authorref->{dates} = $tmp->getAttribute('dates'); | ||||||
299 | $authorref->{has_books} = $tmp->getAttribute('has_books'); | ||||||
300 | } | ||||||
301 | # Look for a list of categories and save the IDs | ||||||
302 | if (($tmp) = $one_author->getElementsByTagName('Categories')) | ||||||
303 | { | ||||||
304 | my $categories = []; | ||||||
305 | foreach ($tmp->getElementsByTagName('Category')) | ||||||
306 | { | ||||||
307 | push(@$categories, $_->getAttribute('category_id')); | ||||||
308 | } | ||||||
309 | |||||||
310 | $authorref->{categories} = $categories; | ||||||
311 | } | ||||||
312 | # Look for a list of subjects. We save those in a special format, here. | ||||||
313 | if (($tmp) = $one_author->getElementsByTagName('Subjects')) | ||||||
314 | { | ||||||
315 | my $subjects = []; | ||||||
316 | foreach ($tmp->getElementsByTagName('Subject')) | ||||||
317 | { | ||||||
318 | push(@$subjects, join(':', | ||||||
319 | $_->getAttribute('subject_id'), | ||||||
320 | $_->getAttribute('book_count'))); | ||||||
321 | } | ||||||
322 | |||||||
323 | $authorref->{subjects} = $subjects; | ||||||
324 | } | ||||||
325 | |||||||
326 | push(@$authors, $class->new($authorref)); | ||||||
327 | } | ||||||
328 | |||||||
329 | return ($authors, { total_results => $total_results, | ||||||
330 | page_size => $page_size, | ||||||
331 | page_number => $page_number, | ||||||
332 | shown_results => $shown_results }); | ||||||
333 | } | ||||||
334 | |||||||
335 | ############################################################################### | ||||||
336 | # | ||||||
337 | # Sub Name: parse_books | ||||||
338 | # | ||||||
339 | # Description: Parse the XML resulting from a call to the books API. | ||||||
340 | # | ||||||
341 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
342 | # $self in ref Object | ||||||
343 | # $root_elt in ref XML::LibXML::Node object | ||||||
344 | # | ||||||
345 | # Returns: Success: listref | ||||||
346 | # Failure: throws Error::Simple | ||||||
347 | # | ||||||
348 | ############################################################################### | ||||||
349 | sub parse_books : RESTRICTED | ||||||
350 | { | ||||||
351 | my ($self, $root_elt) = @_; | ||||||
352 | |||||||
353 | my ($total_results, $page_size, $page_number, $shown_results, $list_elt, | ||||||
354 | @bookblocks, $books, $one_book, $bookref, $tmp); | ||||||
355 | # The class should already be loaded before we got to this point: | ||||||
356 | my $class = WebService::ISBNDB::API->class_for_type('Books'); | ||||||
357 | |||||||
358 | # For now, we aren't interested in the root element (the only useful piece | ||||||
359 | # of information in it is the server-time of the request). So skip down a | ||||||
360 | # level-- there should be exactly one BookList element. | ||||||
361 | ($list_elt) = $root_elt->getElementsByTagName('BookList'); | ||||||
362 | throw Error::Simple("No |
||||||
363 | unless (ref $list_elt); | ||||||
364 | |||||||
365 | # These attributes live on the BookList element | ||||||
366 | $total_results = $list_elt->getAttribute('total_results'); | ||||||
367 | $page_size = $list_elt->getAttribute('page_size'); | ||||||
368 | $page_number = $list_elt->getAttribute('page_number'); | ||||||
369 | $shown_results = $list_elt->getAttribute('shown_results'); | ||||||
370 | |||||||
371 | # Start with no books in the list, and get the |
||||||
372 | $books = []; | ||||||
373 | @bookblocks = $list_elt->getElementsByTagName('BookData'); | ||||||
374 | throw Error::Simple("Number of |
||||||
375 | "'shown_results' value") | ||||||
376 | unless ($shown_results == @bookblocks); | ||||||
377 | for $one_book (@bookblocks) | ||||||
378 | { | ||||||
379 | # Clean slate | ||||||
380 | $bookref = {}; | ||||||
381 | |||||||
382 | # ID and ISBN are attributes of BookData | ||||||
383 | $bookref->{id} = $one_book->getAttribute('book_id'); | ||||||
384 | $bookref->{isbn} = $one_book->getAttribute('isbn'); | ||||||
385 | # Title is just text | ||||||
386 | if (($tmp) = $one_book->getElementsByTagName('Title')) | ||||||
387 | { | ||||||
388 | $bookref->{title} = $self->_lr_trim($tmp->textContent); | ||||||
389 | } | ||||||
390 | # TitleLong is just text | ||||||
391 | if (($tmp) = $one_book->getElementsByTagName('TitleLong')) | ||||||
392 | { | ||||||
393 | $bookref->{longtitle} = $self->_lr_trim($tmp->textContent); | ||||||
394 | } | ||||||
395 | # AuthorsText is just text | ||||||
396 | if (($tmp) = $one_book->getElementsByTagName('AuthorsText')) | ||||||
397 | { | ||||||
398 | $bookref->{authors_text} = $self->_lr_trim($tmp->textContent); | ||||||
399 | } | ||||||
400 | # PublisherText also identifies the publisher record by ID | ||||||
401 | if (($tmp) = $one_book->getElementsByTagName('PublisherText')) | ||||||
402 | { | ||||||
403 | $bookref->{publisher} = $tmp->getAttribute('publisher_id'); | ||||||
404 | $bookref->{publisher_text} = $self->_lr_trim($tmp->textContent); | ||||||
405 | } | ||||||
406 | # Look for a list of subjects | ||||||
407 | if (($tmp) = $one_book->getElementsByTagName('Subjects')) | ||||||
408 | { | ||||||
409 | my $subjects = []; | ||||||
410 | foreach ($tmp->getElementsByTagName('Subject')) | ||||||
411 | { | ||||||
412 | push(@$subjects, $_->getAttribute('subject_id')); | ||||||
413 | } | ||||||
414 | |||||||
415 | $bookref->{subjects} = $subjects; | ||||||
416 | } | ||||||
417 | # Look for the list of author records, for their IDs | ||||||
418 | if (($tmp) = $one_book->getElementsByTagName('Authors')) | ||||||
419 | { | ||||||
420 | my $authors = []; | ||||||
421 | foreach ($tmp->getElementsByTagName('Person')) | ||||||
422 | { | ||||||
423 | push(@$authors, $_->getAttribute('person_id')); | ||||||
424 | } | ||||||
425 | |||||||
426 | $bookref->{authors} = $authors; | ||||||
427 | } | ||||||
428 | # Get the Details tag to extract data from the attributes | ||||||
429 | if (($tmp) = $one_book->getElementsByTagName('Details')) | ||||||
430 | { | ||||||
431 | $bookref->{dewey_decimal} = $tmp->getAttribute('dewey_decimal'); | ||||||
432 | $bookref->{dewey_decimal_normalized} = | ||||||
433 | $tmp->getAttribute('dewey_decimal_normalized'); | ||||||
434 | $bookref->{lcc_number} = $tmp->getAttribute('lcc_number'); | ||||||
435 | $bookref->{language} = $tmp->getAttribute('language'); | ||||||
436 | $bookref->{physical_description_text} = | ||||||
437 | $tmp->getAttribute('physical_description_text'); | ||||||
438 | $bookref->{edition_info} = $tmp->getAttribute('edition_info'); | ||||||
439 | $bookref->{change_time} = $tmp->getAttribute('change_time'); | ||||||
440 | $bookref->{price_time} = $tmp->getAttribute('price_time'); | ||||||
441 | if ($CAN_PARSE_DATES) | ||||||
442 | { | ||||||
443 | $bookref->{change_time_sec} = str2time($bookref->{change_time}); | ||||||
444 | $bookref->{price_time_sec} = str2time($bookref->{price_time}); | ||||||
445 | } | ||||||
446 | } | ||||||
447 | # Look for summary text | ||||||
448 | if (($tmp) = $one_book->getElementsByTagName('Summary')) | ||||||
449 | { | ||||||
450 | $bookref->{summary} = $self->_lr_trim($tmp->textContent); | ||||||
451 | } | ||||||
452 | # Look for notes text | ||||||
453 | if (($tmp) = $one_book->getElementsByTagName('Notes')) | ||||||
454 | { | ||||||
455 | $bookref->{notes} = $self->_lr_trim($tmp->textContent); | ||||||
456 | } | ||||||
457 | # Look for URLs text | ||||||
458 | if (($tmp) = $one_book->getElementsByTagName('UrlsText')) | ||||||
459 | { | ||||||
460 | $bookref->{urlstext} = $self->_lr_trim($tmp->textContent); | ||||||
461 | } | ||||||
462 | # Look for awards text | ||||||
463 | if (($tmp) = $one_book->getElementsByTagName('AwardsText')) | ||||||
464 | { | ||||||
465 | $bookref->{awardstext} = $self->_lr_trim($tmp->textContent); | ||||||
466 | } | ||||||
467 | # MARC info block | ||||||
468 | if (($tmp) = $one_book->getElementsByTagName('MARCRecords')) | ||||||
469 | { | ||||||
470 | my $marcs = []; | ||||||
471 | foreach ($tmp->getElementsByTagName('MARC')) | ||||||
472 | { | ||||||
473 | push(@$marcs, | ||||||
474 | { library_name => $_->getAttribute('library_name'), | ||||||
475 | last_update => $_->getAttribute('last_update'), | ||||||
476 | marc_url => $_->getAttribute('marc_url') }); | ||||||
477 | if ($CAN_PARSE_DATES and $marcs->[$#$marcs]->{last_update}) | ||||||
478 | { | ||||||
479 | $marcs->[$#$marcs]->{last_update_sec} = | ||||||
480 | str2time($marcs->[$#$marcs]->{last_update}); | ||||||
481 | } | ||||||
482 | } | ||||||
483 | $bookref->{marc} = $marcs; | ||||||
484 | } | ||||||
485 | # Price info block | ||||||
486 | if (($tmp) = $one_book->getElementsByTagName('Prices')) | ||||||
487 | { | ||||||
488 | my $prices = []; | ||||||
489 | foreach ($tmp->getElementsByTagName('Price')) | ||||||
490 | { | ||||||
491 | push(@$prices, | ||||||
492 | { store_isbn => $_->getAttribute('store_isbn'), | ||||||
493 | store_title => $_->getAttribute('store_title'), | ||||||
494 | store_url => $_->getAttribute('store_url'), | ||||||
495 | store_id => $_->getAttribute('store_id'), | ||||||
496 | currency_code => $_->getAttribute('currency_code'), | ||||||
497 | is_in_stock => $_->getAttribute('is_in_stock'), | ||||||
498 | is_historic => $_->getAttribute('is_historic'), | ||||||
499 | is_new => $_->getAttribute('is_new'), | ||||||
500 | currency_rate => $_->getAttribute('currency_rate'), | ||||||
501 | price => $_->getAttribute('price'), | ||||||
502 | check_time => $_->getAttribute('check_time') }); | ||||||
503 | if ($CAN_PARSE_DATES and $prices->[$#$prices]->{check_time}) | ||||||
504 | { | ||||||
505 | $prices->[$#$prices]->{check_time_sec} = | ||||||
506 | str2time($prices->[$#$prices]->{check_time}); | ||||||
507 | } | ||||||
508 | } | ||||||
509 | $bookref->{prices} = $prices; | ||||||
510 | } | ||||||
511 | |||||||
512 | push(@$books, $class->new($bookref)); | ||||||
513 | } | ||||||
514 | |||||||
515 | return ($books, { total_results => $total_results, page_size => $page_size, | ||||||
516 | page_number => $page_number, | ||||||
517 | shown_results => $shown_results }); | ||||||
518 | } | ||||||
519 | |||||||
520 | ############################################################################### | ||||||
521 | # | ||||||
522 | # Sub Name: parse_categories | ||||||
523 | # | ||||||
524 | # Description: | ||||||
525 | # | ||||||
526 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
527 | # $self in ref Object | ||||||
528 | # $root_elt in ref XML::LibXML::Node object | ||||||
529 | # | ||||||
530 | # Returns: Success: listref | ||||||
531 | # Failure: throws Error::Simple | ||||||
532 | # | ||||||
533 | ############################################################################### | ||||||
534 | sub parse_categories : RESTRICTED | ||||||
535 | { | ||||||
536 | my ($self, $root_elt) = @_; | ||||||
537 | |||||||
538 | my ($total_results, $page_size, $page_number, $shown_results, $list_elt, | ||||||
539 | @catblocks, $cats, $one_cat, $catref, $tmp); | ||||||
540 | # The class should already be loaded before we got to this point: | ||||||
541 | my $class = WebService::ISBNDB::API->class_for_type('Categories'); | ||||||
542 | |||||||
543 | # For now, we aren't interested in the root element (the only useful piece | ||||||
544 | # of information in it is the server-time of the request). So skip down a | ||||||
545 | # level-- there should be exactly one CategoryList element. | ||||||
546 | ($list_elt) = $root_elt->getElementsByTagName('CategoryList'); | ||||||
547 | throw Error::Simple("No |
||||||
548 | unless (ref $list_elt); | ||||||
549 | |||||||
550 | # These attributes live on the CategoryList element | ||||||
551 | $total_results = $list_elt->getAttribute('total_results'); | ||||||
552 | $page_size = $list_elt->getAttribute('page_size'); | ||||||
553 | $page_number = $list_elt->getAttribute('page_number'); | ||||||
554 | $shown_results = $list_elt->getAttribute('shown_results'); | ||||||
555 | |||||||
556 | # Start with no categories in the list, and get the |
||||||
557 | $cats = []; | ||||||
558 | @catblocks = $list_elt->getElementsByTagName('CategoryData'); | ||||||
559 | throw Error::Simple("Number of |
||||||
560 | "'shown_results' value") | ||||||
561 | unless ($shown_results == @catblocks); | ||||||
562 | for $one_cat (@catblocks) | ||||||
563 | { | ||||||
564 | # Clean slate | ||||||
565 | $catref = {}; | ||||||
566 | |||||||
567 | # ID, book count, marc field, marc indicator 1 and marc indicator 2 | ||||||
568 | # are all attributes of SubjectData | ||||||
569 | $catref->{id} = $one_cat->getAttribute('category_id'); | ||||||
570 | $catref->{parent} = $one_cat->getAttribute('parent_id'); | ||||||
571 | # Name is just text | ||||||
572 | if (($tmp) = $one_cat->getElementsByTagName('Name')) | ||||||
573 | { | ||||||
574 | $catref->{name} = $self->_lr_trim($tmp->textContent); | ||||||
575 | } | ||||||
576 | # The element holds some data in attributes |
||||||
577 | if (($tmp) = $one_cat->getElementsByTagName('Details')) | ||||||
578 | { | ||||||
579 | $catref->{summary} = | ||||||
580 | $self->_lr_trim($tmp->getAttribute('summary')); | ||||||
581 | $catref->{depth} = $tmp->getAttribute('depth'); | ||||||
582 | $catref->{element_count} = $tmp->getAttribute('element_count'); | ||||||
583 | } | ||||||
584 | # Look for a list of sub-categories and save the IDs | ||||||
585 | if (($tmp) = $one_cat->getElementsByTagName('SubCategories')) | ||||||
586 | { | ||||||
587 | my $sub_categories = []; | ||||||
588 | foreach ($tmp->getElementsByTagName('SubCategory')) | ||||||
589 | { | ||||||
590 | push(@$sub_categories, $_->getAttribute('id')); | ||||||
591 | } | ||||||
592 | |||||||
593 | $catref->{sub_categories} = $sub_categories; | ||||||
594 | } | ||||||
595 | |||||||
596 | push(@$cats, $class->new($catref)); | ||||||
597 | } | ||||||
598 | |||||||
599 | return ($cats, { total_results => $total_results, page_size => $page_size, | ||||||
600 | page_number => $page_number, | ||||||
601 | shown_results => $shown_results }); | ||||||
602 | } | ||||||
603 | |||||||
604 | ############################################################################### | ||||||
605 | # | ||||||
606 | # Sub Name: parse_publishers | ||||||
607 | # | ||||||
608 | # Description: | ||||||
609 | # | ||||||
610 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
611 | # $self in ref Object | ||||||
612 | # $root_elt in ref XML::LibXML::Node object | ||||||
613 | # | ||||||
614 | # Returns: Success: listref | ||||||
615 | # Failure: throws Error::Simple | ||||||
616 | # | ||||||
617 | ############################################################################### | ||||||
618 | sub parse_publishers : RESTRICTED | ||||||
619 | { | ||||||
620 | my ($self, $root_elt) = @_; | ||||||
621 | |||||||
622 | my ($total_results, $page_size, $page_number, $shown_results, $list_elt, | ||||||
623 | @pubblocks, $pubs, $one_pub, $pubref, $tmp); | ||||||
624 | # The class should already be loaded before we got to this point: | ||||||
625 | my $class = WebService::ISBNDB::API->class_for_type('Publishers'); | ||||||
626 | |||||||
627 | # For now, we aren't interested in the root element (the only useful piece | ||||||
628 | # of information in it is the server-time of the request). So skip down a | ||||||
629 | # level-- there should be exactly one PublisherList element. | ||||||
630 | ($list_elt) = $root_elt->getElementsByTagName('PublisherList'); | ||||||
631 | throw Error::Simple("No |
||||||
632 | unless (ref $list_elt); | ||||||
633 | |||||||
634 | # These attributes live on the PublisherList element | ||||||
635 | $total_results = $list_elt->getAttribute('total_results'); | ||||||
636 | $page_size = $list_elt->getAttribute('page_size'); | ||||||
637 | $page_number = $list_elt->getAttribute('page_number'); | ||||||
638 | $shown_results = $list_elt->getAttribute('shown_results'); | ||||||
639 | |||||||
640 | # Start with no publishers in the list, and get the |
||||||
641 | $pubs = []; | ||||||
642 | @pubblocks = $list_elt->getElementsByTagName('PublisherData'); | ||||||
643 | throw Error::Simple("Number of |
||||||
644 | "'shown_results' value") | ||||||
645 | unless ($shown_results == @pubblocks); | ||||||
646 | for $one_pub (@pubblocks) | ||||||
647 | { | ||||||
648 | # Clean slate | ||||||
649 | $pubref = {}; | ||||||
650 | |||||||
651 | # ID is an attribute of PublisherData | ||||||
652 | $pubref->{id} = $one_pub->getAttribute('publisher_id'); | ||||||
653 | # Name is just text | ||||||
654 | if (($tmp) = $one_pub->getElementsByTagName('Name')) | ||||||
655 | { | ||||||
656 | $pubref->{name} = $self->_lr_trim($tmp->textContent); | ||||||
657 | } | ||||||
658 | # Details gives the location in an attribute | ||||||
659 | if (($tmp) = $one_pub->getElementsByTagName('Details')) | ||||||
660 | { | ||||||
661 | $pubref->{location} = $tmp->getAttribute('location'); | ||||||
662 | } | ||||||
663 | # Look for a list of categories and save the IDs | ||||||
664 | if (($tmp) = $one_pub->getElementsByTagName('Categories')) | ||||||
665 | { | ||||||
666 | my $categories = []; | ||||||
667 | foreach ($tmp->getElementsByTagName('Category')) | ||||||
668 | { | ||||||
669 | push(@$categories, $_->getAttribute('category_id')); | ||||||
670 | } | ||||||
671 | |||||||
672 | $pubref->{categories} = $categories; | ||||||
673 | } | ||||||
674 | |||||||
675 | push(@$pubs, $class->new($pubref)); | ||||||
676 | } | ||||||
677 | |||||||
678 | return ($pubs, { total_results => $total_results, page_size => $page_size, | ||||||
679 | page_number => $page_number, | ||||||
680 | shown_results => $shown_results }); | ||||||
681 | } | ||||||
682 | |||||||
683 | ############################################################################### | ||||||
684 | # | ||||||
685 | # Sub Name: parse_subjects | ||||||
686 | # | ||||||
687 | # Description: | ||||||
688 | # | ||||||
689 | # Arguments: NAME IN/OUT TYPE DESCRIPTION | ||||||
690 | # $self in ref Object | ||||||
691 | # $root_elt in ref XML::LibXML::Node object | ||||||
692 | # | ||||||
693 | # Returns: Success: listref | ||||||
694 | # Failure: throws Error::Simple | ||||||
695 | # | ||||||
696 | ############################################################################### | ||||||
697 | sub parse_subjects : RESTRICTED | ||||||
698 | { | ||||||
699 | my ($self, $root_elt) = @_; | ||||||
700 | |||||||
701 | my ($total_results, $page_size, $page_number, $shown_results, $list_elt, | ||||||
702 | @subjectblocks, $subjects, $one_subject, $subjectref, $tmp); | ||||||
703 | # The class should already be loaded before we got to this point: | ||||||
704 | my $class = WebService::ISBNDB::API->class_for_type('Subjects'); | ||||||
705 | |||||||
706 | # For now, we aren't interested in the root element (the only useful piece | ||||||
707 | # of information in it is the server-time of the request). So skip down a | ||||||
708 | # level-- there should be exactly one SubjectList element. | ||||||
709 | ($list_elt) = $root_elt->getElementsByTagName('SubjectList'); | ||||||
710 | throw Error::Simple("No |
||||||
711 | unless (ref $list_elt); | ||||||
712 | |||||||
713 | # These attributes live on the SubjectList element | ||||||
714 | $total_results = $list_elt->getAttribute('total_results'); | ||||||
715 | $page_size = $list_elt->getAttribute('page_size'); | ||||||
716 | $page_number = $list_elt->getAttribute('page_number'); | ||||||
717 | $shown_results = $list_elt->getAttribute('shown_results'); | ||||||
718 | |||||||
719 | # Start with no subjects in the list, and get the |
||||||
720 | $subjects = []; | ||||||
721 | @subjectblocks = $list_elt->getElementsByTagName('SubjectData'); | ||||||
722 | throw Error::Simple("Number of |
||||||
723 | "'shown_results' value") | ||||||
724 | unless ($shown_results == @subjectblocks); | ||||||
725 | for $one_subject (@subjectblocks) | ||||||
726 | { | ||||||
727 | # Clean slate | ||||||
728 | $subjectref = {}; | ||||||
729 | |||||||
730 | # ID, book count, marc field, marc indicator 1 and marc indicator 2 | ||||||
731 | # are all attributes of SubjectData | ||||||
732 | $subjectref->{id} = $one_subject->getAttribute('subject_id'); | ||||||
733 | $subjectref->{book_count} = $one_subject->getAttribute('book_count'); | ||||||
734 | $subjectref->{marc_field} = $one_subject->getAttribute('marc_field'); | ||||||
735 | $subjectref->{marc_indicator_1} = | ||||||
736 | $one_subject->getAttribute('marc_indicator_1'); | ||||||
737 | $subjectref->{marc_indicator_2} = | ||||||
738 | $one_subject->getAttribute('marc_indicator_2'); | ||||||
739 | # Name is just text | ||||||
740 | if (($tmp) = $one_subject->getElementsByTagName('Name')) | ||||||
741 | { | ||||||
742 | $subjectref->{name} = $self->_lr_trim($tmp->textContent); | ||||||
743 | } | ||||||
744 | # Look for a list of categories and save the IDs | ||||||
745 | if (($tmp) = $one_subject->getElementsByTagName('Categories')) | ||||||
746 | { | ||||||
747 | my $categories = []; | ||||||
748 | foreach ($tmp->getElementsByTagName('Category')) | ||||||
749 | { | ||||||
750 | push(@$categories, $_->getAttribute('category_id')); | ||||||
751 | } | ||||||
752 | |||||||
753 | $subjectref->{categories} = $categories; | ||||||
754 | } | ||||||
755 | |||||||
756 | push(@$subjects, $class->new($subjectref)); | ||||||
757 | } | ||||||
758 | |||||||
759 | return ($subjects, { total_results => $total_results, | ||||||
760 | page_size => $page_size, | ||||||
761 | page_number => $page_number, | ||||||
762 | shown_results => $shown_results }); | ||||||
763 | } | ||||||
764 | |||||||
765 | 1; | ||||||
766 | |||||||
767 | =pod | ||||||
768 | |||||||
769 | =head1 NAME | ||||||
770 | |||||||
771 | WebService::ISBNDB::Agent::REST - Agent sub-class for the REST protocol | ||||||
772 | |||||||
773 | =head1 SYNOPSIS | ||||||
774 | |||||||
775 | This module should not be directly used by user applications. | ||||||
776 | |||||||
777 | =head1 DESCRIPTION | ||||||
778 | |||||||
779 | This module implements the REST-based communication protocol for getting data | ||||||
780 | from the B |
||||||
781 | service supports. | ||||||
782 | |||||||
783 | =head1 METHODS | ||||||
784 | |||||||
785 | This class provides the following methods, most of which are restricted to | ||||||
786 | this class and any sub-classes of it that may be written: | ||||||
787 | |||||||
788 | =over 4 | ||||||
789 | |||||||
790 | =item parse_authors($ROOT) (R) | ||||||
791 | |||||||
792 | =item parse_books($ROOT) (R) | ||||||
793 | |||||||
794 | =item parse_categories($ROOT) (R) | ||||||
795 | |||||||
796 | =item parse_publishers($ROOT) (R) | ||||||
797 | |||||||
798 | =item parse_subjects($ROOT) (R) | ||||||
799 | |||||||
800 | Each of these parses the XML response for the corresponding API call. The | ||||||
801 | C<$ROOT> parameter is a B |
||||||
802 | the XML returned by the service. | ||||||
803 | |||||||
804 | Each of these returns a list-reference of objects, even when there is only | ||||||
805 | one result value. All of these methods are restricted to this class and | ||||||
806 | its decendants. | ||||||
807 | |||||||
808 | =item request($OBJ, $ARGS) (R) | ||||||
809 | |||||||
810 | Use the B |
||||||
811 | C<$OBJ> indicates what type of data request is being made, and C<$ARGS> is a | ||||||
812 | hash-reference of arguments to be passed in the request. The return value is | ||||||
813 | an object of the B |
||||||
814 | |||||||
815 | This method is restricted to this class, and is the required overload of the | ||||||
816 | request() method from the parent class (L |
||||||
817 | |||||||
818 | =item request_method($OBJ, $ARGS) | ||||||
819 | |||||||
820 | Returns the HTTP method (GET, POST, etc.) to use when making the request. The | ||||||
821 | C<$OBJ> and C<$ARGS> parameters may be used to determine the method (in the | ||||||
822 | case of this protocol, they are ignored since B |
||||||
823 | HTTP method). | ||||||
824 | |||||||
825 | =item request_uri($OBJ, $ARGS) | ||||||
826 | |||||||
827 | Returns the complete HTTP URI to use in making the request. C<$OBJ> is used | ||||||
828 | to derive the type of data being fetched, and thus the base URI to use. The | ||||||
829 | key/value pairs in the hash-reference provided by C<$ARGS> are used in the | ||||||
830 | REST protocol to set the query parameters that govern the request. | ||||||
831 | |||||||
832 | =item protocol([$TESTVAL]) | ||||||
833 | |||||||
834 | With no arguments, returns the name of this protocol as a simple string. If | ||||||
835 | an argument is passed, it is tested against the protocol name to see if it | ||||||
836 | is a match, returning a true or false value as appropriate. | ||||||
837 | |||||||
838 | =back | ||||||
839 | |||||||
840 | The class also implements a constructor method, which is needed to co-operate | ||||||
841 | with the parent class under B |
||||||
842 | have to call the constructor directly: | ||||||
843 | |||||||
844 | =over 4 | ||||||
845 | |||||||
846 | =item new([$ARGS]) | ||||||
847 | |||||||
848 | Calls into the parent constructor with any arguments passed in. | ||||||
849 | |||||||
850 | =back | ||||||
851 | |||||||
852 | =head1 CAVEATS | ||||||
853 | |||||||
854 | The data returned by this class is only as accurate as the data retrieved from | ||||||
855 | B |
||||||
856 | |||||||
857 | The list of results from calling search() is currently limited to 10 items. | ||||||
858 | This limit will be removed in an upcoming release, when iterators are | ||||||
859 | implemented. | ||||||
860 | |||||||
861 | =head1 SEE ALSO | ||||||
862 | |||||||
863 | L |
||||||
864 | L |
||||||
865 | |||||||
866 | =head1 AUTHOR | ||||||
867 | |||||||
868 | Randy J. Ray E |
||||||
869 | |||||||
870 | =head1 LICENSE | ||||||
871 | |||||||
872 | This module and the code within are released under the terms of the Artistic | ||||||
873 | License 2.0 (http://www.opensource.org/licenses/artistic-license-2.0.php). This | ||||||
874 | code may be redistributed under either the Artistic License or the GNU | ||||||
875 | Lesser General Public License (LGPL) version 2.1 | ||||||
876 | (http://www.opensource.org/licenses/lgpl-license.php). | ||||||
877 | |||||||
878 | =cut |