]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/EbookAPI/Test.pm
LP#1541559: ebook API service and test module
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / EbookAPI / Test.pm
1 #!/usr/bin/perl
2
3 # Copyright (C) 2015 BC Libraries Cooperative
4 #
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.
9
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.
14
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.
18
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.
24 #
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.
29 #
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.
35 #
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:
39 #
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
43 #     token if needed)
44 #   - do_patron_auth: get a patron-specific bearer token, or just the patron ID
45 #   - do_holdings_lookup: how many total/available "copies" are there for this
46 #     title? (n/a for OneClickdigital)
47 #   - do_availability_lookup: does this title have available "copies"? y/n
48 #   - checkout
49 #   - renew
50 #   - checkin
51 #   - place_hold
52 #   - suspend_hold (n/a for OneClickdigital)
53 #   - cancel_hold
54 #   - get_patron_checkouts: returns an array of hashrefs representing checkouts;
55 #     each checkout hashref has the following keys:
56 #       - xact_id
57 #       - title_id
58 #       - due_date
59 #       - download_url
60 #       - title
61 #       - author
62 #   - get_patron_holds
63 # ====================================================================== 
64
65 package OpenILS::Application::EbookAPI::Test;
66
67 use strict;
68 use warnings;
69
70 use OpenILS::Application;
71 use OpenILS::Application::EbookAPI;
72 use base qw/OpenILS::Application::EbookAPI/;
73 use OpenSRF::AppSession;
74 use OpenSRF::EX qw(:try);
75 use OpenSRF::Utils::SettingsClient;
76 use OpenSRF::Utils::Logger qw($logger);
77 use OpenSRF::Utils::Cache;
78 use OpenILS::Application::AppUtils;
79 use DateTime;
80 use DateTime::Format::ISO8601;
81
82 my $U = 'OpenILS::Application::AppUtils';
83
84 # create new handler object
85 sub new {
86     my( $class, $args ) = @_;
87
88     # A new handler object represents a new API session, so we instantiate it
89     # by passing it a hashref containing the following basic attributes
90     # available to us when we start the session:
91     #   - vendor: a string indicating the vendor whose API we're talking to
92     #   - ou: org unit ID for current session
93     #   - session_id: unique ID for the session represented by this object
94
95     $class = ref $class || $class;
96     return bless $args, $class;
97 }
98
99 # set API-specific handler attributes based on library settings
100 sub initialize {
101     my $self = shift;
102
103     # At a minimum, you are likely to need some kind of basic API key or token
104     # to allow the client (Evergreen) to use the API.
105     # Other attributes will vary depending on the API.  Consult your API
106     # documentation for details.
107
108     return $self;
109 }
110
111 # authorize client session against API
112 sub do_client_auth {
113     my $self = shift;
114
115     # Some APIs require client authorization, and may return an auth token
116     # which must be included in subsequent requests.  This is where you do
117     # that.  If you get an auth token, you'll want to add it as an attribute to
118     # the handler object so that it's available to use in subsequent requests.
119     # If your API doesn't require this step, you don't need to return anything
120     # here.
121
122     return;
123 }
124
125 # authenticate patron against API
126 sub do_patron_auth {
127     my $self = shift;
128
129     # We authenticate the patron using the barcode of their active card.
130     # We may capture this on OPAC login (along with password, if required),
131     # in which case it should already be an attribute of the handler object;
132     # otherwise, it should be passed to this method as a parameter.
133     my $barcode = shift;
134     if ($barcode) {
135         if (!$self->{patron_barcode}) {
136             $self->{patron_barcode} = $barcode;
137         } elsif ($barcode ne $self->{patron_barcode}) {
138             $logger->error("EbookAPI: patron barcode in auth request does not match patron barcode for this session");
139             return;
140         }
141     } else {
142         if (!$self->{patron_barcode}) {
143             $logger->error("EbookAPI: Cannot authenticate patron with unknown barcode");
144         } else {
145             $barcode = $self->{patron_barcode};
146         }
147     }
148
149     # We really don't want to be handling the patron's unencrypted password.
150     # But if we need to, it should be added to our handler object on login
151     # via the open-ils.ebook_api.patron.cache_password OpenSRF API call
152     # before we attempt to authenticate the patron against the external API.
153     my $password;
154     if ($self->{patron_password}) {
155         $password = $self->{patron_password};
156     }
157
158     # return external patron ID or patron auth token
159
160     # For testing, only barcode 99999359616 is valid.
161     return 'USER001' if ($barcode eq '99999359616');
162
163     # All other values return undef.
164     return undef;
165 }
166
167 # get detailed holdings information (copy counts and formats), OR basic
168 # availability if detailed info is not provided by the API
169 sub do_holdings_lookup {
170     my $self = shift;
171
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;
175
176     # Prepare data structure to be used as return value.
177     # NOTE: If the external API does not provide detailed holdings info,
178     # return simple availability information: { available => 1 }
179     my $holdings = {
180         copies_owned => 0,
181         copies_available => 0,
182         formats => []
183     };
184
185     # 001 and 002 are unavailable.
186     if ($title_id eq '001' || $title_id eq '002') {
187         $holdings->{copies_owned} = 1;
188         $holdings->{copies_available} = 0;
189         push @{$holdings->{formats}}, 'ebook';
190     }
191
192     # 003 is available.
193     if ($title_id eq '003') {
194         $holdings->{copies_owned} = 1;
195         $holdings->{copies_available} = 1;
196         push @{$holdings->{formats}}, 'ebook';
197     }
198
199     # All other title IDs are unknown.
200
201     return $holdings;
202 }
203
204 # look up whether a title is currently available for checkout; returns a boolean value
205 sub do_availability_lookup {
206     my $self = shift;
207
208     # External ID for title.  Depending on the API, this could be an ISBN
209     # or an identifier unique to that vendor.
210     my $title_id = shift;
211
212     # At this point, you would lookup title availability via an API request.
213     # In our case, since this is a test module, we just return availability info
214     # based on hard-coded values:
215
216     # 001 and 002 are unavailable.
217     return 0 if ($title_id eq '001');
218     return 0 if ($title_id eq '002');
219
220     # 003 is available.
221     return 1 if ($title_id eq '003');
222
223     # All other title IDs are unknown.
224     return undef;
225 }
226
227 # check out a title to a patron
228 sub checkout {
229     my $self = shift;
230
231     # External ID of title to be checked out.
232     my $title_id = shift;
233
234     # Patron ID or patron auth token, as returned by do_patron_auth().
235     my $user_token = shift;
236
237     # If checkout succeeds, the response is a hashref with the following fields:
238     # - due_date
239     # - xact_id (optional)
240     #
241     # If checkout fails, the response is a hashref with the following fields:
242     # - error_msg: a string containing an error message or description of why
243     #   the checkout failed (e.g. "Checkout limit of (4) reached").
244     #
245     # If no valid response is received from the API, return undef.
246
247     # For testing purposes, user ID USER001 is our only valid user, 
248     # and title 003 is the only available title.
249     if ($title_id && $user_token) {
250         if ($user_token eq 'USER001' && $title_id eq '003') {
251             return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
252         } else {
253             return { msg => 'Checkout failed.' };
254         }
255     } else {
256         return undef;
257     }
258
259 }
260
261 sub renew {
262     my $self = shift;
263
264     # External ID of title to be renewed.
265     my $title_id = shift;
266
267     # Patron ID or patron auth token, as returned by do_patron_auth().
268     my $user_token = shift;
269
270     # If renewal succeeds, the response is a hashref with the following fields:
271     # - due_date
272     # - xact_id (optional)
273     #
274     # If renewal fails, the response is a hashref with the following fields:
275     # - error_msg: a string containing an error message or description of why
276     #   the renewal failed (e.g. "Renewal limit reached").
277     #
278     # If no valid response is received from the API, return undef.
279
280     # For testing purposes, user ID USER001 is our only valid user, 
281     # and title 001 is the only renewable title.
282     if ($title_id && $user_token) {
283         if ($user_token eq 'USER001' && $title_id eq '001') {
284             return { due_date => DateTime->today()->add( days => 14 )->iso8601() };
285         } else {
286             return { error_msg => 'Renewal failed.' };
287         }
288     } else {
289         return undef;
290     }
291 }
292
293 sub checkin {
294     my $self = shift;
295
296     # External ID of title to be checked in.
297     my $title_id = shift;
298
299     # Patron ID or patron auth token, as returned by do_patron_auth().
300     my $user_token = shift;
301
302     # If checkin succeeds, return an empty hashref (actually it doesn't
303     # need to be empty, it just must NOT contain "error_msg" as a key).
304     #
305     # If checkin fails, return a hashref with the following fields:
306     # - error_msg: a string containing an error message or description of why
307     #   the checkin failed (e.g. "Checkin failed").
308     #
309     # If no valid response is received from the API, return undef.
310
311     # For testing purposes, user ID USER001 is our only valid user, 
312     # and title 003 is the only title that can be checked in.
313     if ($title_id && $user_token) {
314         if ($user_token eq 'USER001' && $title_id eq '003') {
315             return {};
316         } else {
317             return { error_msg => 'Checkin failed' };
318         }
319     } else {
320         return undef;
321     }
322 }
323
324 sub place_hold {
325     my $self = shift;
326
327     # External ID of title to be held.
328     my $title_id = shift;
329
330     # Patron ID or patron auth token, as returned by do_patron_auth().
331     my $user_token = shift;
332
333     # If hold is successfully placed, return a hashref with the following
334     # fields:
335     # - queue_position: this user's position in hold queue for this title
336     # - queue_size: total number of holds on this title
337     # - expire_date: when the hold expires
338     #
339     # If hold fails, return a hashref with the following fields:
340     # - error_msg: a string containing an error message or description of why
341     #   the hold failed (e.g. "Hold limit (4) reached").
342     #
343     # If no valid response is received from the API, return undef.
344
345     # For testing purposes, we always and only allow placing a hold on title
346     # 002 by user ID USER001.
347     if ($title_id && $user_token) {
348         if ($user_token eq 'USER001' && $title_id eq '002') {
349             return {
350                 queue_position => 1,
351                 queue_size => 1,
352                 expire_date => DateTime->today()->add( days => 70 )->iso8601()
353             };
354         } else {
355             return { error_msg => 'Unable to place hold' };
356         }
357     } else {
358         return undef;
359     }
360 }
361
362 sub cancel_hold {
363     my $self = shift;
364
365     # External ID of title.
366     my $title_id = shift;
367
368     # Patron ID or patron auth token, as returned by do_patron_auth().
369     my $user_token = shift;
370
371     # If hold is successfully canceled, return an empty hashref (actually it
372     # doesn't need to be empty, it just must NOT contain "error_msg" as a key).
373     #
374     # If hold is NOT canceled, return a hashref with the following fields:
375     # - error_msg: a string containing an error message or description of why
376     #   the hold was not canceled (e.g. "Hold could not be canceled"). 
377     #
378     # If no valid response is received from the API, return undef.
379
380     # For testing purposes, we always and only allow canceling a hold on title
381     # 002 by user ID USER001.
382     if ($title_id && $user_token) {
383         if ($user_token eq 'USER001' && $title_id eq '002') {
384             return {};
385         } else {
386             return { error_msg => 'Unable to cancel hold' };
387         }
388     } else {
389         return undef;
390     }
391 }
392
393 sub suspend_hold {
394 }
395
396 sub get_patron_checkouts {
397     my $self = shift;
398
399     # Patron ID or patron auth token.
400     my $user_token = shift;
401
402     # Return an array of hashrefs representing checkouts;
403     # each hashref should have the following keys:
404     #   - xact_id: unique ID for this transaction (if used by API)
405     #   - title_id: unique ID for this title
406     #   - due_date
407     #   - download_url
408     #   - title: title of item, formatted for display
409     #   - author: author of item, formatted for display
410
411     my $checkouts = [];
412     # USER001 is our only valid user, so we only return checkouts for them.
413     if ($user_token eq 'USER001') {
414         push @$checkouts, {
415             xact_id => '1',
416             title_id => '001',
417             due_date => DateTime->today()->add( days => 7 )->iso8601(),
418             download_url => 'http://example.com/ebookapi/t/001/download',
419             title => 'The Fellowship of the Ring',
420             author => 'J. R. R. Tolkien'
421         };
422     }
423     $self->{checkouts} = $checkouts;
424     return $self->{checkouts};
425 }
426
427 sub get_patron_holds {
428     my $self = shift;
429
430     # Patron ID or patron auth token.
431     my $user_token = shift;
432
433     # Return an array of hashrefs representing holds;
434     # each hashref should have the following keys:
435     #   - title_id: unique ID for this title
436     #   - queue_position: this user's position in hold queue for this title
437     #   - queue_size: total number of holds on this title
438     #   - is_ready: whether hold is currently available for checkout
439     #   - is_frozen: whether hold is suspended
440     #   - thaw_date: when hold suspension expires (if suspended)
441     #   - create_date: when the hold was placed
442     #   - expire_date: when the hold expires
443     #   - title: title of item, formatted for display
444     #   - author: author of item, formatted for display
445
446     my $holds = [];
447     # USER001 is our only valid user, so we only return checkouts for them.
448     if ($user_token eq 'USER001') {
449         push @$holds, {
450             title_id => '002',
451             queue_position => 1,
452             queue_size => 1,
453             is_ready => 0,
454             is_frozen => 0,
455             create_date => DateTime->today()->subtract( days => 10 )->iso8601(),
456             expire_date => DateTime->today()->add( days => 60 )->iso8601(),
457             title => 'The Two Towers',
458             author => 'J. R. R. Tolkien'
459         };
460     }
461     $self->{holds} = $holds;
462     return $self->{holds};
463 }
464