File Coverage

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 :default<"http://isbndb.com">);
59             my %authors : ATTR(:name :default<"/api/authors.xml">);
60             my %books : ATTR(:name :default<"/api/books.xml">);
61             my %categories : ATTR(:name :default<"/api/categories.xml">);
62             my %publishers : ATTR(:name :default<"/api/publishers.xml">);
63             my %subjects : ATTR(:name :default<"/api/subjects.xml">);
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 element found in response")
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 nodes
274             $authors = [];
275             @authorblocks = $list_elt->getElementsByTagName('AuthorData');
276             throw Error::Simple("Number of blocks does not match " .
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 element found in response")
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 nodes
372             $books = [];
373             @bookblocks = $list_elt->getElementsByTagName('BookData');
374             throw Error::Simple("Number of blocks does not match " .
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 element found in response")
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 nodes
557             $cats = [];
558             @catblocks = $list_elt->getElementsByTagName('CategoryData');
559             throw Error::Simple("Number of blocks does not match " .
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 element found in response")
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 nodes
641             $pubs = [];
642             @pubblocks = $list_elt->getElementsByTagName('PublisherData');
643             throw Error::Simple("Number of blocks does not match " .
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 element found in response")
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 nodes
720             $subjects = [];
721             @subjectblocks = $list_elt->getElementsByTagName('SubjectData');
722             throw Error::Simple("Number of blocks does not match " .
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 service. At present, this is the only protocol the
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 object, obtained from parsing
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 object to make a request on the remote service.
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 class.
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 is always the chosen
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 structure. You should generally not
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, L,
864             L
865              
866             =head1 AUTHOR
867              
868             Randy J. Ray Erjray@blackperl.comE
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