| 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 |