3 # Copyright (C) 2015 BC Libraries Cooperative
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 # ======================================================================
20 # OpenSRF requests are handled by the main OpenILS::Application::EbookAPI module,
21 # which determines which "handler" submodule to use based on the params of the
22 # OpenSRF request. Each vendor API (OneClickdigital, OverDrive, etc.) has its
23 # own separate handler class, since they all work a little differently.
25 # An instance of the handler class represents an EbookAPI session -- that is, we
26 # instantiate a new handler object when we start a new session with the external API.
27 # Thus everything we need to talk to the API, like client keys or auth tokens, is
28 # an attribute of the handler object.
30 # API endpoints are defined in the handler class. The handler constructs HTTP
31 # requests, then passes them to the the request() method of the parent class
32 # (OpenILS::Application::EbookAPI), which sets some default headers and manages
33 # the actual mechanics of sending the request and receiving the response. It's
34 # up to the handler class to do something with the response.
36 # At a minimum, each handler must have the following methods, since the parent
37 # class presumes they exist; it may be a no-op if the API doesn't support that
38 # bit of functionality:
40 # - initialize: assign values for basic attributes (e.g. library_id,
41 # basic_token) based on library settings
42 # - do_client_auth: authenticate client with external API (e.g. get client
44 # - do_patron_auth: get a patron-specific bearer token, or just the patron ID
45 # - get_title_info: get basic title details (title, author, optional cover image)
46 # - do_holdings_lookup: how many total/available "copies" are there for this
47 # title? (n/a for OneClickdigital)
48 # - do_availability_lookup: does this title have available "copies"? y/n
53 # - suspend_hold (n/a for OneClickdigital)
55 # - get_patron_checkouts: returns an array of hashrefs representing checkouts;
56 # each checkout hashref has the following keys:
64 # ======================================================================
66 package OpenILS::Application::EbookAPI::Test;
71 use OpenILS::Application;
72 use OpenILS::Application::EbookAPI;
73 use base qw/OpenILS::Application::EbookAPI/;
74 use OpenSRF::AppSession;
75 use OpenSRF::EX qw(:try);
76 use OpenSRF::Utils::SettingsClient;
77 use OpenSRF::Utils::Logger qw($logger);
78 use OpenSRF::Utils::Cache;
79 use OpenILS::Application::AppUtils;
81 use DateTime::Format::ISO8601;
83 my $U = 'OpenILS::Application::AppUtils';
85 # create new handler object
87 my( $class, $args ) = @_;
89 # A new handler object represents a new API session, so we instantiate it
90 # by passing it a hashref containing the following basic attributes
91 # available to us when we start the session:
92 # - vendor: a string indicating the vendor whose API we're talking to
93 # - ou: org unit ID for current session
94 # - session_id: unique ID for the session represented by this object
96 $class = ref $class || $class;
97 return bless $args, $class;
100 # set API-specific handler attributes based on library settings
104 # At a minimum, you are likely to need some kind of basic API key or token
105 # to allow the client (Evergreen) to use the API.
106 # Other attributes will vary depending on the API. Consult your API
107 # documentation for details.
112 # authorize client session against API
116 # Some APIs require client authorization, and may return an auth token
117 # which must be included in subsequent requests. This is where you do
118 # that. If you get an auth token, you'll want to add it as an attribute to
119 # the handler object so that it's available to use in subsequent requests.
120 # If your API doesn't require this step, you don't need to return anything
126 # authenticate patron against API
130 # We authenticate the patron using the barcode of their active card.
131 # We may capture this on OPAC login (along with password, if required),
132 # in which case it should already be an attribute of the handler object;
133 # otherwise, it should be passed to this method as a parameter.
136 if (!$self->{patron_barcode}) {
137 $self->{patron_barcode} = $barcode;
138 } elsif ($barcode ne $self->{patron_barcode}) {
139 $logger->error("EbookAPI: patron barcode in auth request does not match patron barcode for this session");
143 if (!$self->{patron_barcode}) {
144 $logger->error("EbookAPI: Cannot authenticate patron with unknown barcode");
146 $barcode = $self->{patron_barcode};
150 # We really don't want to be handling the patron's unencrypted password.
151 # But if we need to, it should be added to our handler object on login
152 # via the open-ils.ebook_api.patron.cache_password OpenSRF API call
153 # before we attempt to authenticate the patron against the external API.
155 if ($self->{patron_password}) {
156 $password = $self->{patron_password};
159 # return external patron ID or patron auth token
161 # For testing, only barcode 99999359616 is valid.
162 return 'USER001' if ($barcode eq '99999359616');
164 # All other values return undef.
168 # get basic info (title, author, eventually a thumbnail URL) for a title
172 # External ID for title. Depending on the API, this could be an ISBN
173 # or an identifier unique to that vendor.
174 my $title_id = shift;
176 # Prepare data structure to be used as return value.
182 # If title lookup fails or title is not found, our return value
183 # is somewhat different.
184 my $title_not_found = {
185 error => 'Title not found.'
188 # For testing purposes, we have only three valid titles (001, 002, 003).
189 # All other title IDs return an error message.
190 if ($title_id eq '001') {
191 $title_info->{title} = 'The Fellowship of the Ring';
192 $title_info->{author} = 'J.R.R. Tolkien';
193 } elsif ($title_id eq '002') {
194 $title_info->{title} = 'The Two Towers';
195 $title_info->{author} = 'J.R.R. Tolkien';
196 } elsif ($title_id eq '003') {
197 $title_info->{title} = 'The Return of the King';
198 $title_info->{author} = 'J.R.R. Tolkien';
200 return $title_not_found;
205 # get detailed holdings information (copy counts and formats), OR basic
206 # availability if detailed info is not provided by the API
207 sub do_holdings_lookup {
210 # External ID for title. Depending on the API, this could be an ISBN
211 # or an identifier unique to that vendor.
212 my $title_id = shift;
214 # Prepare data structure to be used as return value.
215 # NOTE: If the external API does not provide detailed holdings info,
216 # return simple availability information: { available => 1 }
219 copies_available => 0,
223 # 001 and 002 are unavailable.
224 if ($title_id eq '001' || $title_id eq '002') {
225 $holdings->{copies_owned} = 1;
226 $holdings->{copies_available} = 0;
227 push @{$holdings->{formats}}, 'ebook';
231 if ($title_id eq '003') {
232 $holdings->{copies_owned} = 1;
233 $holdings->{copies_available} = 1;
234 push @{$holdings->{formats}}, 'ebook';
237 # All other title IDs are unknown.
242 # look up whether a title is currently available for checkout; returns a boolean value
243 sub do_availability_lookup {
246 # External ID for title. Depending on the API, this could be an ISBN
247 # or an identifier unique to that vendor.
248 my $title_id = shift;
250 # At this point, you would lookup title availability via an API request.
251 # In our case, since this is a test module, we just return availability info
252 # based on hard-coded values:
254 # 001 and 002 are unavailable.
255 return 0 if ($title_id eq '001');
256 return 0 if ($title_id eq '002');
259 return 1 if ($title_id eq '003');
261 # All other title IDs are unknown.
265 # check out a title to a patron
269 # External ID of title to be checked out.
270 my $title_id = shift;
272 # Patron ID or patron auth token, as returned by do_patron_auth().
273 my $user_token = shift;
275 # If checkout succeeds, the response is a hashref with the following fields:
277 # - xact_id (optional)
279 # If checkout fails, the response is a hashref with the following fields:
280 # - error_msg: a string containing an error message or description of why
281 # the checkout failed (e.g. "Checkout limit of (4) reached").
283 # If no valid response is received from the API, return undef.
285 # For testing purposes, user ID USER001 is our only valid user,
286 # and title 003 is the only available title.
287 if ($title_id && $user_token) {
288 if ($user_token eq 'USER001' && $title_id eq '003') {
289 return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
291 return { msg => 'Checkout failed.' };
302 # External ID of title to be renewed.
303 my $title_id = shift;
305 # Patron ID or patron auth token, as returned by do_patron_auth().
306 my $user_token = shift;
308 # If renewal succeeds, the response is a hashref with the following fields:
310 # - xact_id (optional)
312 # If renewal fails, the response is a hashref with the following fields:
313 # - error_msg: a string containing an error message or description of why
314 # the renewal failed (e.g. "Renewal limit reached").
316 # If no valid response is received from the API, return undef.
318 # For testing purposes, user ID USER001 is our only valid user,
319 # and title 001 is the only renewable title.
320 if ($title_id && $user_token) {
321 if ($user_token eq 'USER001' && $title_id eq '001') {
322 return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
324 return { error_msg => 'Renewal failed.' };
334 # External ID of title to be checked in.
335 my $title_id = shift;
337 # Patron ID or patron auth token, as returned by do_patron_auth().
338 my $user_token = shift;
340 # If checkin succeeds, return an empty hashref (actually it doesn't
341 # need to be empty, it just must NOT contain "error_msg" as a key).
343 # If checkin fails, return a hashref with the following fields:
344 # - error_msg: a string containing an error message or description of why
345 # the checkin failed (e.g. "Checkin failed").
347 # If no valid response is received from the API, return undef.
349 # For testing purposes, user ID USER001 is our only valid user,
350 # and title 003 is the only title that can be checked in.
351 if ($title_id && $user_token) {
352 if ($user_token eq 'USER001' && $title_id eq '003') {
355 return { error_msg => 'Checkin failed' };
365 # External ID of title to be held.
366 my $title_id = shift;
368 # Patron ID or patron auth token, as returned by do_patron_auth().
369 my $user_token = shift;
371 # Email address of patron (optional, not used here).
374 # If hold is successfully placed, return a hashref with the following
376 # - queue_position: this user's position in hold queue for this title
377 # - queue_size: total number of holds on this title
378 # - expire_date: when the hold expires
380 # If hold fails, return a hashref with the following fields:
381 # - error_msg: a string containing an error message or description of why
382 # the hold failed (e.g. "Hold limit (4) reached").
384 # If no valid response is received from the API, return undef.
386 # For testing purposes, we always and only allow placing a hold on title
387 # 002 by user ID USER001.
388 if ($title_id && $user_token) {
389 if ($user_token eq 'USER001' && $title_id eq '002') {
393 expire_date => DateTime->today()->add( days => 70 )->iso8601()
396 return { error_msg => 'Unable to place hold' };
406 # External ID of title.
407 my $title_id = shift;
409 # Patron ID or patron auth token, as returned by do_patron_auth().
410 my $user_token = shift;
412 # If hold is successfully canceled, return an empty hashref (actually it
413 # doesn't need to be empty, it just must NOT contain "error_msg" as a key).
415 # If hold is NOT canceled, return a hashref with the following fields:
416 # - error_msg: a string containing an error message or description of why
417 # the hold was not canceled (e.g. "Hold could not be canceled").
419 # If no valid response is received from the API, return undef.
421 # For testing purposes, we always and only allow canceling a hold on title
422 # 002 by user ID USER001.
423 if ($title_id && $user_token) {
424 if ($user_token eq 'USER001' && $title_id eq '002') {
427 return { error_msg => 'Unable to cancel hold' };
437 sub get_patron_checkouts {
440 # Patron ID or patron auth token.
441 my $user_token = shift;
443 # Return an array of hashrefs representing checkouts;
444 # each hashref should have the following keys:
445 # - xact_id: unique ID for this transaction (if used by API)
446 # - title_id: unique ID for this title
449 # - title: title of item, formatted for display
450 # - author: author of item, formatted for display
453 # USER001 is our only valid user, so we only return checkouts for them.
454 if ($user_token eq 'USER001') {
458 due_date => DateTime->today()->add( days => 7 )->iso8601(),
459 download_url => 'http://example.com/ebookapi/t/001/download',
460 title => 'The Fellowship of the Ring',
461 author => 'J. R. R. Tolkien'
464 $self->{checkouts} = $checkouts;
465 return $self->{checkouts};
468 sub get_patron_holds {
471 # Patron ID or patron auth token.
472 my $user_token = shift;
474 # Return an array of hashrefs representing holds;
475 # each hashref should have the following keys:
476 # - title_id: unique ID for this title
477 # - queue_position: this user's position in hold queue for this title
478 # - queue_size: total number of holds on this title
479 # - is_ready: whether hold is currently available for checkout
480 # - is_frozen: whether hold is suspended
481 # - thaw_date: when hold suspension expires (if suspended)
482 # - create_date: when the hold was placed
483 # - expire_date: when the hold expires
484 # - title: title of item, formatted for display
485 # - author: author of item, formatted for display
488 # USER001 is our only valid user, so we only return checkouts for them.
489 if ($user_token eq 'USER001') {
496 create_date => DateTime->today()->subtract( days => 10 )->iso8601(),
497 expire_date => DateTime->today()->add( days => 60 )->iso8601(),
498 title => 'The Two Towers',
499 author => 'J. R. R. Tolkien'
502 $self->{holds} = $holds;
503 return $self->{holds};