41ac7fc0edef57d3a5418d4b8099aa29f2d7dfb9
[working/NCIPServer.git] / lib / NCIP / ILS / Evergreen.pm
1 # ---------------------------------------------------------------
2 # Copyright © 2014 Jason Stephenson <jason@sigio.com>
3 #
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 # GNU General Public License for more details.
13 # ---------------------------------------------------------------
14 package NCIP::ILS::Evergreen;
15
16 use Modern::Perl;
17 use Object::Tiny qw/name/;
18 use XML::XPath;
19 use OpenSRF::System;
20 use OpenSRF::Utils::SettingsClient;
21 use Digest::MD5 qw/md5_hex/;
22 use OpenILS::Utils::Fieldmapper;
23 use OpenILS::Utils::CStoreEditor qw/:funcs/;
24 use OpenILS::Application::AppUtils;
25 use OpenILS::Const qw/:const/;
26 use MARC::Record;
27 use MARC::Field;
28 use MARC::File::XML;
29
30 # Default values we define for things that might be missing in our
31 # runtime environment or configuration file that absolutely must have
32 # values.
33 #
34 # OILS_NCIP_CONFIG_DEFAULT is the default location to find our
35 # driver's configuration file.  This location can be overridden by
36 # setting the path in the OILS_NCIP_CONFIG environment variable.
37 #
38 # BIB_SOURCE_DEFAULT is the config.bib_source.id to use when creating
39 # "short" bibs.  It is used only if no entry is supplied in the
40 # configuration file.  The provided default is 2, the id of the
41 # "System Local" source that comes with a default Evergreen
42 # installation.
43 use constant {
44     OILS_NCIP_CONFIG_DEFAULT => '/openils/conf/oils_ncip.xml',
45     BIB_SOURCE_DEFAULT => 2
46 };
47
48 # A common Evergreen code shortcut to use AppUtils:
49 my $U = 'OpenILS::Application::AppUtils';
50
51 # The usual constructor:
52 sub new {
53     my $class = shift;
54     $class = ref $class or $class;
55
56     # Instantiate our Object::Tiny parent with the rest of the
57     # arguments.  It creates a blessed hashref.
58     my $self = $class->SUPER::new(@_);
59
60     # Look for our configuration file, load, and parse it:
61     $self->_configure();
62
63     # Bootstrap OpenSRF and prepare some OpenILS components.
64     $self->_bootstrap();
65
66     # Initialize the rest of our internal state.
67     $self->_init();
68
69     return $self;
70 }
71
72 # Subroutines required by the NCIPServer interface:
73 sub itemdata {}
74
75 sub userdata {}
76
77 sub checkin {}
78
79 sub checkout {}
80
81 sub renew {}
82
83 sub request {}
84
85 sub cancelrequest {}
86
87 sub acceptitem {}
88
89 # Implementation functions that might be useful to a subclass.
90
91 # Get a CStoreEditor:
92 sub editor {
93     my $self = shift;
94
95     # If we have an editor, check the validity of the auth session, then
96     # invalidate the editor if the session is not valid.
97     if ($self->{editor}) {
98         undef($self->{editor}) unless ($self->checkauth());
99     }
100
101     # If we don't have an editor, make a new one.
102     unless (defined($self->{editor})) {
103         $self->login() unless ($self->checkauth());
104         $self->{editor} = new_editor(authtoken=>$self->{session}->{authtoken});
105     }
106
107     return $self->{editor};
108 }
109
110 # Login via OpenSRF to Evergreen.
111 sub login {
112     my $self = shift;
113
114     # Get the authentication seed.
115     my $seed = $U->simple_req(
116         'open-ils.auth',
117         'open-ils.auth.authenticate.init',
118         $self->{config}->{username}
119     );
120
121     # Actually login.
122     if ($seed) {
123         my $response = $U->simple_req(
124             'open-ils.auth',
125             'open-ils.auth.authenticate.complete',
126             {
127                 username => $self->{config}->{username},
128                 password => md5_hex(
129                     $seed . md5_hex($self->{config}->{password})
130                 ),
131                 type => 'staff',
132                 workstation => $self->{config}->{workstation}
133             }
134         );
135         if ($response) {
136             $self->{session}->{authtoken} = $response->{payload}->{authtoken};
137             $self->{session}->{authtime} = $response->{payload}->{authtime};
138         }
139     }
140 }
141
142 # Return 1 if we have a 'valid' authtoken, 0 if not.
143 sub checkauth {
144     my $self = shift;
145
146     # We implement our own version of this function, rather than rely
147     # on CStoreEditor, because we may want to check this at times that
148     # we don't have a CStoreEditor.
149
150     # We use AppUtils to do the heavy lifting.
151     if (defined($self->{session})) {
152         if ($U->check_user_session($self->{session}->{authtoken})) {
153             return 1;
154         } else {
155             return 0;
156         }
157     }
158
159     # If we reach here, we don't have a session, so we are definitely
160     # not logged in.
161     return 0;
162 }
163
164 # private subroutines not meant to be used directly by subclasses.
165 # Most have to do with setup and/or state checking of implementation
166 # components.
167
168 # Find, load, and parse our configuration file:
169 sub _configure {
170     my $self = shift;
171
172     # Find the configuration file via variables:
173     my $file = OILS_NCIP_CONFIG_DEFAULT;
174     $file = $ENV{OILS_NCIP_CONFIG} if ($ENV{OILS_NCIP_CONFIG});
175
176     # Load our configuration with XML::XPath.
177     my $xpath = XML::XPath->new(filename => $file);
178     # Load configuration into $self:
179     $self->{config}->{bootstrap} =
180         _strip($xpath->findvalue("/ncip/bootstrap")->value());
181     $self->{config}->{username} =
182         _strip($xpath->findvalue("/ncip/credentials/username")->value());
183     $self->{config}->{password} =
184         _strip($xpath->findvalue("/ncip/credentials/password")->value());
185     $self->{config}->{work_ou} =
186         _strip($xpath->findvalue("/ncip/credentials/work_ou")->value());
187     $self->{config}->{workstation} =
188         _strip($xpath->findvalue("/ncip/credentials/workstation")->value());
189     # Look for a list of patron profiles to treat as blocked.  This is
190     # useful if you have a patron group or groups that are not
191     # permitted to do ILL.
192     $self->{config}->{barred_groups} = [];
193     my $nodes = $xpath->findnodes('/ncip/patrons/block_profile');
194     if ($nodes) {
195         foreach my $node ($nodes->get_nodelist()) {
196             my $data = {id => 0, name => ""};
197             my $attr = $xpath->findvalue('@pgt', $node);
198             if ($attr) {
199                 $data->{id} = $attr;
200             }
201             $data->{name} = _strip($node->string_value());
202             push(@{$self->{config}->{barred_groups}}, $data)
203                 if ($data->{id} || $data->{name});
204         }
205     }
206     # Check for the use_precats setting for acceptitem.  This should
207     # only be set if you are using 2.7.0-alpha or later of Evergreen.
208     $self->{config}->{use_precats} = 0;
209     undef($nodes);
210     $nodes = $xpath->find('/ncip/items/use_precats');
211     $self->{config}->{use_precats} = 1 if ($nodes);
212     # If we're not using precats, we will be making "short" bibs.  We
213     # need to look up and see if a special bib source has been
214     # configured for these.
215     undef($nodes);
216     $nodes = $xpath->findnodes('/ncip/items/bib_source');
217     if ($nodes) {
218         my $node = $nodes->get_node(1);
219         my $attr = $xpath->findvalue('@cbs', $node);
220         if ($attr) {
221             $self->{config}->{cbs}->{id} = $attr;
222         }
223         $self->{config}->{cbs}->{name} = _strip($node->string_value());
224     }
225     # Look for any required asset.copy.stat_cat_entry entries.
226     $self->{config}->{asces} = [];
227     undef($nodes);
228     $nodes = $xpath->findnodes('/ncip/items/stat_cat_entry');
229     if ($nodes) {
230         foreach my $node ($nodes->get_nodelist()) {
231             my $data = {asc => 0, id => 0, name => ''};
232             my $asc = $xpath->findvalue('@asc', $node);
233             $data->{asc} = $asc if ($asc);
234             my $asce = $xpath->findvalue('@asce', $node);
235             $data->{id} = $asce if ($asce);
236             $data->{name} = _strip($node->string_value());
237             push(@{$self->{config}->{asces}}, $data)
238                 if ($data->{id} || ($data->{name} && $data->{asc}));
239         }
240     }
241 }
242
243 # Bootstrap OpenSRF::System, load the IDL, and initialize the
244 # CStoreEditor module.
245 sub _bootstrap {
246     my $self = shift;
247
248     my $bootstrap_config = $self->{config}->{bootstrap};
249     OpenSRF::System->bootstrap_client(config_file => $bootstrap_config);
250
251     my $idl = OpenSRF::Utils::SettingsClient->new->config_value("IDL");
252     Fieldmapper->import(IDL => $idl);
253
254     OpenILS::Utils::CStoreEditor->init;
255 }
256
257 # Login and then initialize some object data based on the
258 # configuration.
259 sub _init {
260     my $self = shift;
261
262     # Login to Evergreen.
263     $self->login();
264
265     # Create an editor.
266     my $e = $self->editor();
267
268     # Load the barred groups as pgt objects into a blocked_profiles
269     # list.
270     $self->{blocked_profiles} = [];
271     foreach (@{$self->{config}->{barred_groups}}) {
272         if ($_->{id}) {
273             my $pgt = $e->retrieve_permission_grp_tree($_->{id});
274             push(@{$self->{blocked_profiles}}, $pgt) if ($pgt);
275         } else {
276             my $result = $e->search_permission_grp_tree(
277                 {name => $_->{name}}
278             );
279             if ($result && @$result) {
280                 map {push(@{$self->{blocked_profiles}}, $_)} @$result;
281             }
282         }
283     }
284
285     # Load the bib source if we're not using precats.
286     unless ($self->{config}->{use_precats}) {
287         # Retrieve the default
288         my $cbs = $e->retrieve_config_bib_source(BIB_SOURCE_DEFAULT);
289         my $data = $self->{config}->{cbs};
290         if ($data) {
291             if ($data->{id}) {
292                 my $result = $e->retrieve_config_bib_source($data->{id});
293                 $cbs = $result if ($result);
294             } else {
295                 my $result = $e->search_config_bib_source(
296                     {source => $data->{name}}
297                 );
298                 if ($result && @$result) {
299                     $cbs = $result->[0]; # Use the first one.
300                 }
301             }
302         }
303         $self->{bib_source} = $cbs;
304     }
305
306     # Load the required asset.stat_cat_entries:
307     $self->{asces} = [];
308     foreach (@{$self->{config}->{asces}}) {
309         if ($_->{id}) {
310             my $asce = $e->retrieve_asset_stat_cat_entry($_->{id});
311             push(@{$self->{asces}}, $asce) if ($asce);
312         } elsif ($_->{asc} && $_->{name}) {
313             # We may actually want to retrieve the ancestor tree
314             # beginning with $self->{config}->{work_ou} and limit the
315             # next search where the owner is one of those org units.
316             my $result = $e->search_asset_stat_cat_entry(
317                 {
318                     stat_cat => $_->{asc},
319                     value => $_->{name}
320                 }
321             );
322             if ($result && @$result) {
323                 map {push(@{$self->{asces}}, $_)} @$result;
324             }
325         }
326     }
327 }
328
329 # Standalone, "helper" functions.  These do not take an object or
330 # class reference.
331
332 # Strip leading and trailing whitespace (incl. newlines) from a string
333 # value.
334 sub _strip {
335     my $string = shift;
336     if ($string) {
337         $string =~ s/^\s+//;
338         $string =~ s/\s+$//;
339     }
340     return $string;
341 }
342
343 1;