26f23725ea72f72fc3b2b31fc3b7e9ad31e58948
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / SuperCat.pm
1 # We'll be working with XML, so...
2 use XML::LibXML;
3 use XML::LibXSLT;
4 use Unicode::Normalize;
5
6 # ... and this has some handy common methods
7 use OpenILS::Application::AppUtils;
8
9 my $parser = new XML::LibXML;
10 my $U = 'OpenILS::Application::AppUtils';
11
12
13 package OpenILS::Application::SuperCat;
14
15 use strict;
16 use warnings;
17 use OpenILS::Utils::Normalize qw( naco_normalize );
18
19 # All OpenSRF applications must be based on OpenSRF::Application or
20 # a subclass thereof.  Makes sense, eh?
21 use OpenILS::Application;
22 use base qw/OpenILS::Application/;
23
24 # This is the client class, used for connecting to open-ils.storage
25 use OpenSRF::AppSession;
26
27 # This is an extension of Error.pm that supplies some error types to throw
28 use OpenSRF::EX qw(:try);
29
30 # This is a helper class for querying the OpenSRF Settings application ...
31 use OpenSRF::Utils::SettingsClient;
32
33 # ... and here we have the built in logging helper ...
34 use OpenSRF::Utils::Logger qw($logger);
35
36 # ... and this is our OpenILS object (en|de)coder and psuedo-ORM package.
37 use OpenILS::Utils::Fieldmapper;
38
39 use OpenILS::Utils::CStoreEditor q/:funcs/;
40 use OpenILS::Utils::TagURI;
41
42
43 our (
44   $_parser,
45   $_xslt,
46   %record_xslt,
47   %metarecord_xslt,
48   %holdings_data_cache,
49   %authority_browse_axis_cache,
50 );
51
52 sub child_init {
53     # we need an XML parser
54     $_parser = new XML::LibXML;
55
56     # and an xslt parser
57     $_xslt = new XML::LibXSLT;
58
59     # parse the MODS xslt ...
60     my $mods33_xslt = $_parser->parse_file(
61         OpenSRF::Utils::SettingsClient
62             ->new
63             ->config_value( dirs => 'xsl' ).
64         "/MARC21slim2MODS33.xsl"
65     );
66     # and stash a transformer
67     $record_xslt{mods33}{xslt} = $_xslt->parse_stylesheet( $mods33_xslt );
68     $record_xslt{mods33}{namespace_uri} = 'http://www.loc.gov/mods/v3';
69     $record_xslt{mods33}{docs} = 'http://www.loc.gov/mods/';
70     $record_xslt{mods33}{schema_location} = 'http://www.loc.gov/standards/mods/v3/mods-3-3.xsd';
71
72     # parse the MODS xslt ...
73     my $mods32_xslt = $_parser->parse_file(
74         OpenSRF::Utils::SettingsClient
75             ->new
76             ->config_value( dirs => 'xsl' ).
77         "/MARC21slim2MODS32.xsl"
78     );
79     # and stash a transformer
80     $record_xslt{mods32}{xslt} = $_xslt->parse_stylesheet( $mods32_xslt );
81     $record_xslt{mods32}{namespace_uri} = 'http://www.loc.gov/mods/v3';
82     $record_xslt{mods32}{docs} = 'http://www.loc.gov/mods/';
83     $record_xslt{mods32}{schema_location} = 'http://www.loc.gov/standards/mods/v3/mods-3-2.xsd';
84
85     # parse the MODS xslt ...
86     my $mods3_xslt = $_parser->parse_file(
87         OpenSRF::Utils::SettingsClient
88             ->new
89             ->config_value( dirs => 'xsl' ).
90         "/MARC21slim2MODS3.xsl"
91     );
92     # and stash a transformer
93     $record_xslt{mods3}{xslt} = $_xslt->parse_stylesheet( $mods3_xslt );
94     $record_xslt{mods3}{namespace_uri} = 'http://www.loc.gov/mods/v3';
95     $record_xslt{mods3}{docs} = 'http://www.loc.gov/mods/';
96     $record_xslt{mods3}{schema_location} = 'http://www.loc.gov/standards/mods/v3/mods-3-1.xsd';
97
98     # parse the MODS xslt ...
99     my $mods_xslt = $_parser->parse_file(
100         OpenSRF::Utils::SettingsClient
101             ->new
102             ->config_value( dirs => 'xsl' ).
103         "/MARC21slim2MODS.xsl"
104     );
105     # and stash a transformer
106     $record_xslt{mods}{xslt} = $_xslt->parse_stylesheet( $mods_xslt );
107     $record_xslt{mods}{namespace_uri} = 'http://www.loc.gov/mods/';
108     $record_xslt{mods}{docs} = 'http://www.loc.gov/mods/';
109     $record_xslt{mods}{schema_location} = 'http://www.loc.gov/standards/mods/mods.xsd';
110
111     # parse the ATOM entry xslt ...
112     my $atom_xslt = $_parser->parse_file(
113         OpenSRF::Utils::SettingsClient
114             ->new
115             ->config_value( dirs => 'xsl' ).
116         "/MARC21slim2ATOM.xsl"
117     );
118     # and stash a transformer
119     $record_xslt{atom}{xslt} = $_xslt->parse_stylesheet( $atom_xslt );
120     $record_xslt{atom}{namespace_uri} = 'http://www.w3.org/2005/Atom';
121     $record_xslt{atom}{docs} = 'http://www.ietf.org/rfc/rfc4287.txt';
122
123     # parse the RDFDC xslt ...
124     my $rdf_dc_xslt = $_parser->parse_file(
125         OpenSRF::Utils::SettingsClient
126             ->new
127             ->config_value( dirs => 'xsl' ).
128         "/MARC21slim2RDFDC.xsl"
129     );
130     # and stash a transformer
131     $record_xslt{rdf_dc}{xslt} = $_xslt->parse_stylesheet( $rdf_dc_xslt );
132     $record_xslt{rdf_dc}{namespace_uri} = 'http://purl.org/dc/elements/1.1/';
133     $record_xslt{rdf_dc}{schema_location} = 'http://purl.org/dc/elements/1.1/';
134
135     # parse the SRWDC xslt ...
136     my $srw_dc_xslt = $_parser->parse_file(
137         OpenSRF::Utils::SettingsClient
138             ->new
139             ->config_value( dirs => 'xsl' ).
140         "/MARC21slim2SRWDC.xsl"
141     );
142     # and stash a transformer
143     $record_xslt{srw_dc}{xslt} = $_xslt->parse_stylesheet( $srw_dc_xslt );
144     $record_xslt{srw_dc}{namespace_uri} = 'info:srw/schema/1/dc-schema';
145     $record_xslt{srw_dc}{schema_location} = 'http://www.loc.gov/z3950/agency/zing/srw/dc-schema.xsd';
146
147     # parse the OAIDC xslt ...
148     my $oai_dc_xslt = $_parser->parse_file(
149         OpenSRF::Utils::SettingsClient
150             ->new
151             ->config_value( dirs => 'xsl' ).
152         "/MARC21slim2OAIDC.xsl"
153     );
154     # and stash a transformer
155     $record_xslt{oai_dc}{xslt} = $_xslt->parse_stylesheet( $oai_dc_xslt );
156     $record_xslt{oai_dc}{namespace_uri} = 'http://www.openarchives.org/OAI/2.0/oai_dc/';
157     $record_xslt{oai_dc}{schema_location} = 'http://www.openarchives.org/OAI/2.0/oai_dc.xsd';
158
159     # parse the RSS xslt ...
160     my $rss_xslt = $_parser->parse_file(
161         OpenSRF::Utils::SettingsClient
162             ->new
163             ->config_value( dirs => 'xsl' ).
164         "/MARC21slim2RSS2.xsl"
165     );
166     # and stash a transformer
167     $record_xslt{rss2}{xslt} = $_xslt->parse_stylesheet( $rss_xslt );
168
169     # parse the FGDC xslt ...
170     my $fgdc_xslt = $_parser->parse_file(
171         OpenSRF::Utils::SettingsClient
172             ->new
173             ->config_value( dirs => 'xsl' ).
174         "/MARC21slim2FGDC.xsl"
175     );
176     # and stash a transformer
177     $record_xslt{fgdc}{xslt} = $_xslt->parse_stylesheet( $fgdc_xslt );
178     $record_xslt{fgdc}{docs} = 'http://www.fgdc.gov/metadata/csdgm/index_html';
179     $record_xslt{fgdc}{schema_location} = 'http://www.fgdc.gov/metadata/fgdc-std-001-1998.xsd';
180
181     register_record_transforms();
182
183     register_new_authorities_methods();
184
185     return 1;
186 }
187
188 sub register_record_transforms {
189     for my $type ( keys %record_xslt ) {
190         __PACKAGE__->register_method(
191             method    => 'retrieve_record_transform',
192             api_name  => "open-ils.supercat.record.$type.retrieve",
193             api_level => 1,
194             argc      => 1,
195             signature =>
196                 { desc     => "Returns the \U$type\E representation ".
197                               "of the requested bibliographic record",
198                   params   =>
199                     [
200                         { name => 'bibId',
201                           desc => 'An OpenILS biblio::record_entry id',
202                           type => 'number' },
203                     ],
204                 'return' =>
205                     { desc => "The bib record in \U$type\E",
206                       type => 'string' }
207                 }
208         );
209
210         __PACKAGE__->register_method(
211             method    => 'retrieve_isbn_transform',
212             api_name  => "open-ils.supercat.isbn.$type.retrieve",
213             api_level => 1,
214             argc      => 1,
215             signature =>
216                 { desc     => "Returns the \U$type\E representation ".
217                               "of the requested bibliographic record",
218                   params   =>
219                     [
220                         { name => 'isbn',
221                           desc => 'An ISBN',
222                           type => 'string' },
223                     ],
224                 'return' =>
225                     { desc => "The bib record in \U$type\E",
226                       type => 'string' }
227                 }
228         );
229     }
230 }
231
232 sub register_new_authorities_methods {
233     my %register_args = (
234         method    => "generic_new_authorities_method",
235         api_level => 1,
236         argc      => 1,
237         signature => {
238             desc => q/Generated method/,
239             params => [
240                 {name => "what",
241                     desc => "An axis, an authority tag number, or a bibliographic tag number, depending on invocation",
242                     type => "string"},
243                 {name => "term",
244                     desc => "A search term",
245                     type => "string"},
246                 {name => "page",
247                     desc => "zero-based page number of results",
248                     type => "number"},
249                 {name => "page size",
250                     desc => "number of results per page",
251                     type => "number"}
252             ],
253             return => {
254                 desc => "A list of authority record IDs", type => "array"
255             }
256         }
257     );
258
259     foreach my $how (qw/axis atag btag/) {
260         foreach my $action (qw/browse_center browse_top
261             search_rank search_heading/) {
262
263             $register_args{api_name} =
264                 "open-ils.supercat.authority.$action.by_$how";
265             __PACKAGE__->register_method(%register_args);
266
267             $register_args{api_name} =
268                 "open-ils.supercat.authority.$action.by_$how.refs";
269             __PACKAGE__->register_method(%register_args);
270
271         }
272     }
273 }
274
275 sub generic_new_authorities_method {
276     my $self = shift;
277     my $client = shift;
278
279     # We want to be extra careful with these arguments, since the next
280     # thing we're doing with them is passing them to a DB procedure.
281     my $what = ''.shift;
282     my $term = ''.shift;
283     my $page = int(shift || 0);
284     my $page_size = shift;
285     my $thesauruses = shift;
286
287     # undef ok, but other non numbers not ok
288     $page_size = int($page_size) if defined $page_size;
289
290     # Figure out how we were called and what DB procedure we'll call in turn.
291     $self->api_name =~ /\.by_(\w+)($|\.)/;
292     my $metaaxis = $1;
293     my $refs = $2;
294
295     $self->api_name =~ /authority\.(\w+)\./;
296     my $action = $1;
297
298     my $method = "${metaaxis}_$action";
299     $method .= "_refs" if $refs;
300
301     # Match authority.full_rec normalization
302     # XXX don't know whether we need second arg 'subfield'?
303     $term = naco_normalize($term);
304
305     my $storage = create OpenSRF::AppSession("open-ils.storage");
306     my $list = $storage->request(
307         "open-ils.storage.authority.in_db.browse_or_search",
308         $method, $what, $term, $page, $page_size, $thesauruses
309     )->gather(1);
310
311     $storage->kill_me;
312
313     return $list;
314 }
315
316
317 sub tree_walker {
318     my $tree = shift;
319     my $field = shift;
320     my $filter = shift;
321
322     return unless ($tree && ref($tree->$field));
323
324     my @things = $filter->($tree);
325     for my $v ( @{$tree->$field} ){
326         push @things, $filter->($v);
327         push @things, tree_walker($v, $field, $filter);
328     }
329     return @things
330 }
331
332 # find a label_sortkey for a call number with a label which is equal
333 # (or close to) a given label value
334 sub _label_sortkey_from_label {
335     my ($label, $_storage, $ou_ids, $cp_filter) = @_;
336
337     my $closest_cn = $_storage->request(
338             "open-ils.cstore.direct.asset.call_number.search.atomic",
339             { label      => { ">=" => { transform => "oils_text_as_bytea", value => ["oils_text_as_bytea", $label] } },
340               (scalar(@$ou_ids) ? (owning_lib => $ou_ids) : ()),
341               deleted    => 'f',
342               @$cp_filter
343             },
344             { flesh     => 1,
345               flesh_fields  => { acn => [qw/label_class/] },
346               limit     => 1,
347               order_by  => { acn => "oils_text_as_bytea(label), id" }
348             }
349         )->gather(1);
350     if (@$closest_cn) {
351         if ($closest_cn->[0]->label eq $label) {
352             # we found an exact match stop here
353             return $closest_cn->[0]->label_sortkey;
354         } else {
355             # we got as close as we could by label alone, let's try to
356             # normalize and get closer
357             $closest_cn = $_storage->request(
358                 "open-ils.cstore.direct.asset.call_number.search.atomic",
359                 { label_sortkey
360                              => { ">=" => [$closest_cn->[0]->label_class->normalizer, $label] },
361                   (scalar(@$ou_ids) ? (owning_lib => $ou_ids) : ()),
362                   deleted    => 'f',
363                   @$cp_filter
364                 },
365                 { limit     => 1,
366                   order_by  => { acn => "label_sortkey, id" }
367                 }
368             )->gather(1);
369             if (@$closest_cn) {
370                 return $closest_cn->[0]->label_sortkey;
371             }
372         }
373     }
374
375     return '~~~'; #fallback to high ascii value, we are at the end of the range
376 }
377
378 sub cn_browse {
379     my $self = shift;
380     my $client = shift;
381
382     my $label = shift;
383     my $ou = shift;
384     my $page_size = shift || 9;
385     my $page = shift || 0;
386     my $statuses = shift || [];
387     my $copy_locations = shift || [];
388
389     my ($before_limit,$after_limit) = (0,0);
390     my ($before_offset,$after_offset) = (0,0);
391
392     if (!$page) {
393         $before_limit = $after_limit = int($page_size / 2);
394         $after_limit += 1 if ($page_size % 2);
395     } else {
396         $before_offset = $after_offset = int($page_size / 2);
397         $before_offset += 1 if ($page_size % 2);
398         $before_limit = $after_limit = $page_size;
399     }
400
401     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
402
403     my @ou_ids;
404     # if OU is not specified, or if '-' is specified, do not enter the block
405     unless (!$ou || $ou eq '-') {
406         # check if the shortname of the top org_unit is specified
407             my $top_org = $_storage->request(
408             "open-ils.cstore.direct.actor.org_unit.search",
409              { parent_ou => undef }
410             )->gather(1);
411
412         if ($ou eq $top_org->shortname) {
413                 $logger->debug("Searching for CNs at top org $ou");
414             } else {
415             my $o_search = { shortname => $ou };
416
417             my $orgs = $_storage->request(
418                 "open-ils.cstore.direct.actor.org_unit.search",
419                 $o_search,
420                 { flesh     => 100,
421                 flesh_fields  => { aou    => [qw/children/] }
422                 }
423             )->gather(1);
424
425             @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
426
427             $logger->debug("Searching for CNs at orgs [".join(',',@ou_ids)."], based on $ou");
428         }
429     }
430
431     my @list = ();
432
433     my @cp_filter = ();
434     if (@$statuses || @$copy_locations) {
435         @cp_filter = (
436             '-exists' => {
437                 limit => 1,
438                 from  => 'acp',
439                 where => {
440                     call_number => { '=' => { '+acn' => 'id' } },
441                     deleted     => 'f',
442                     ((@$statuses)       ? ( status   => $statuses)       : ()),
443                     ((@$copy_locations) ? ( location => $copy_locations) : ())
444                 }
445             }
446         );
447     }
448
449     my $label_sortkey = _label_sortkey_from_label($label, $_storage, \@ou_ids, \@cp_filter);
450
451     if ($page <= 0) {
452         my $before = $_storage->request(
453             "open-ils.cstore.direct.asset.call_number.search.atomic",
454             { label_sortkey => { "<" => { transform => "oils_text_as_bytea", value => ["oils_text_as_bytea", $label_sortkey] } },
455               (scalar(@ou_ids) ? (owning_lib => \@ou_ids) : ()),
456               deleted => 'f',
457               @cp_filter
458             },
459             { flesh     => 1,
460               flesh_fields  => { acn => [qw/record owning_lib prefix suffix/] },
461               order_by  => { acn => "oils_text_as_bytea(label_sortkey) desc, oils_text_as_bytea(label) desc, id desc, owning_lib desc" },
462               limit     => $before_limit,
463               offset    => abs($page) * $page_size - $before_offset,
464             }
465         )->gather(1);
466         push @list, reverse(@$before);
467     }
468
469     if ($page >= 0) {
470         my $after = $_storage->request(
471             "open-ils.cstore.direct.asset.call_number.search.atomic",
472             { label_sortkey => { ">=" => { transform => "oils_text_as_bytea", value => ["oils_text_as_bytea", $label_sortkey] } },
473               (scalar(@ou_ids) ? (owning_lib => \@ou_ids) : ()),
474               deleted => 'f',
475               @cp_filter
476             },
477             { flesh     => 1,
478               flesh_fields  => { acn => [qw/record owning_lib prefix suffix/] },
479               order_by  => { acn => "oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib" },
480               limit     => $after_limit,
481               offset    => abs($page) * $page_size - $after_offset,
482             }
483         )->gather(1);
484         push @list, @$after;
485     }
486
487     return \@list;
488 }
489 __PACKAGE__->register_method(
490     method    => 'cn_browse',
491     api_name  => 'open-ils.supercat.call_number.browse',
492     api_level => 1,
493     argc      => 1,
494     signature =>
495         { desc     => <<"          DESC",
496 Returns the XML representation of the requested bibliographic record's holdings
497           DESC
498           params   =>
499             [
500                 { name => 'label',
501                   desc => 'The target call number label',
502                   type => 'string' },
503                 { name => 'org_unit',
504                   desc => 'The org unit shortname (or "-" or undef for global) to browse',
505                   type => 'string' },
506                 { name => 'page_size',
507                   desc => 'Count of call numbers to retrieve, default is 9',
508                   type => 'number' },
509                 { name => 'page',
510                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
511                   type => 'number' },
512                 { name => 'statuses',
513                   desc => 'Array of statuses to filter copies by, optional and can be undef.',
514                   type => 'array' },
515                 { name => 'locations',
516                   desc => 'Array of copy locations to filter copies by, optional and can be undef.',
517                   type => 'array' },
518             ],
519           'return' =>
520             { desc => 'Call numbers with owning_lib and record fleshed',
521               type => 'array' }
522         }
523 );
524
525 sub cn_startwith {
526     my $self = shift;
527     my $client = shift;
528
529     my $label = shift;
530     my $ou = shift;
531     my $limit = shift || 10;
532     my $page = shift || 0;
533     my $statuses = shift || [];
534     my $copy_locations = shift || [];
535
536
537     my $offset = abs($page) * $limit;
538     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
539
540     my $o_search = { shortname => $ou };
541     if (!$ou || $ou eq '-') {
542         $o_search = { parent_ou => undef };
543     }
544
545     my $orgs = $_storage->request(
546         "open-ils.cstore.direct.actor.org_unit.search",
547         $o_search,
548         { flesh     => 100,
549           flesh_fields  => { aou    => [qw/children/] }
550         }
551     )->gather(1);
552
553     my @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
554
555     $logger->debug("Searching for CNs at orgs [".join(',',@ou_ids)."], based on $ou");
556
557     my @list = ();
558
559     my @cp_filter = ();
560     if (@$statuses || @$copy_locations) {
561         @cp_filter = (
562             '-exists' => {
563                 limit => 1,
564                 from  => 'acp',
565                 where => {
566                     call_number => { '=' => { '+acn' => 'id' } },
567                     deleted     => 'f',
568                     ((@$statuses)       ? ( status   => $statuses)       : ()),
569                     ((@$copy_locations) ? ( location => $copy_locations) : ())
570                 }
571             }
572         );
573     }
574
575     my $label_sortkey = _label_sortkey_from_label($label, $_storage, \@ou_ids, \@cp_filter);
576
577     if ($page < 0) {
578         my $before = $_storage->request(
579             "open-ils.cstore.direct.asset.call_number.search.atomic",
580             { label_sortkey => { "<" => { transform => "oils_text_as_bytea", value => ["oils_text_as_bytea", $label_sortkey] } },
581               owning_lib    => \@ou_ids,
582               deleted => 'f',
583               @cp_filter
584             },
585             { flesh     => 1,
586               flesh_fields  => { acn => [qw/record owning_lib prefix suffix/] },
587               order_by  => { acn => "oils_text_as_bytea(label_sortkey) desc, oils_text_as_bytea(label) desc, id desc, owning_lib desc" },
588               limit     => $limit,
589               offset    => $offset,
590             }
591         )->gather(1);
592         push @list, reverse(@$before);
593     }
594
595     if ($page >= 0) {
596         my $after = $_storage->request(
597             "open-ils.cstore.direct.asset.call_number.search.atomic",
598             { label_sortkey => { ">=" => { transform => "oils_text_as_bytea", value => ["oils_text_as_bytea", $label_sortkey] } },
599               owning_lib    => \@ou_ids,
600               deleted => 'f',
601               @cp_filter
602             },
603             { flesh     => 1,
604               flesh_fields  => { acn => [qw/record owning_lib prefix suffix/] },
605               order_by  => { acn => "oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib" },
606               limit     => $limit,
607               offset    => $offset,
608             }
609         )->gather(1);
610         push @list, @$after;
611     }
612
613     return \@list;
614 }
615 __PACKAGE__->register_method(
616     method    => 'cn_startwith',
617     api_name  => 'open-ils.supercat.call_number.startwith',
618     api_level => 1,
619     argc      => 1,
620     signature =>
621         { desc     => <<"          DESC",
622 Returns the XML representation of the requested bibliographic record's holdings
623           DESC
624           params   =>
625             [
626                 { name => 'label',
627                   desc => 'The target call number label',
628                   type => 'string' },
629                 { name => 'org_unit',
630                   desc => 'The org unit shortname (or "-" or undef for global) to browse',
631                   type => 'string' },
632                 { name => 'page_size',
633                   desc => 'Count of call numbers to retrieve, default is 9',
634                   type => 'number' },
635                 { name => 'page',
636                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
637                   type => 'number' },
638                 { name => 'statuses',
639                   desc => 'Array of statuses to filter copies by, optional and can be undef.',
640                   type => 'array' },
641                 { name => 'locations',
642                   desc => 'Array of copy locations to filter copies by, optional and can be undef.',
643                   type => 'array' },
644             ],
645           'return' =>
646             { desc => 'Call numbers with owning_lib and record fleshed',
647               type => 'array' }
648         }
649 );
650
651
652 sub new_books_by_item {
653     my $self = shift;
654     my $client = shift;
655
656     my $ou = shift;
657     my $page_size = shift || 10;
658     my $page = shift || 1;
659     my $statuses = shift || [];
660     my $copy_locations = shift || [];
661
662     my $offset = $page_size * ($page - 1);
663
664     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
665
666     my @ou_ids;
667     if ($ou && $ou ne '-') {
668         my $orgs = $_storage->request(
669             "open-ils.cstore.direct.actor.org_unit.search",
670             { shortname => $ou },
671             { flesh     => 100,
672               flesh_fields  => { aou    => [qw/children/] }
673             }
674         )->gather(1);
675         @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
676     }
677
678     $logger->debug("Searching for records with new copies at orgs [".join(',',@ou_ids)."], based on $ou");
679     my $cns = $_storage->request(
680         "open-ils.cstore.json_query.atomic",
681         { select    => { acn => ['record'],
682                          acp => [{ aggregate => 1 => transform => max => column => create_date => alias => 'create_date'}]
683                        },
684           from      => { 'acn' => { 'acp' => { field => call_number => fkey => 'id' } } },
685           where     =>
686             { '+acp' =>
687                 { deleted => 'f',
688                   ((@ou_ids)          ? ( circ_lib => \@ou_ids)        : ()),
689                   ((@$statuses)       ? ( status   => $statuses)       : ()),
690                   ((@$copy_locations) ? ( location => $copy_locations) : ())
691                 }, 
692               '+acn' => { record => { '>' => 0 } },
693             }, 
694           order_by  => { acp => { create_date => { transform => 'max', direction => 'desc' } } },
695           limit     => $page_size,
696           offset    => $offset
697         }
698     )->gather(1);
699
700     return [ map { $_->{record} } @$cns ];
701 }
702 __PACKAGE__->register_method(
703     method    => 'new_books_by_item',
704     api_name  => 'open-ils.supercat.new_book_list',
705     api_level => 1,
706     argc      => 1,
707     signature =>
708         { desc     => <<"          DESC",
709 Returns the XML representation of the requested bibliographic record's holdings
710           DESC
711           params   =>
712             [
713                 { name => 'org_unit',
714                   desc => 'The org unit shortname (or "-" or undef for global) to list',
715                   type => 'string' },
716                 { name => 'page_size',
717                   desc => 'Count of records to retrieve, default is 10',
718                   type => 'number' },
719                 { name => 'page',
720                   desc => 'The page of records to retrieve, calculated based on page_size.  Starts at 1.',
721                   type => 'number' },
722                 { name => 'statuses',
723                   desc => 'Array of statuses to filter copies by, optional and can be undef.',
724                   type => 'array' },
725                 { name => 'locations',
726                   desc => 'Array of copy locations to filter copies by, optional and can be undef.',
727                   type => 'array' },
728             ],
729           'return' =>
730             { desc => 'Record IDs',
731               type => 'array' }
732         }
733 );
734
735 sub u2 {
736     my $self = shift;
737     my $client = shift;
738
739     my $u2 = shift;
740     my $format = shift || 'xml';
741
742     $u2 = OpenILS::Utils::TagURI->new($u2);
743     return '' unless $u2;
744
745     # Use pathinfo on acp as a lookup type specifier.
746     if ($u2->classname eq 'acp' and $u2->pathinfo =~ /\bbarcode\b/) {
747         my( $copy, $evt ) = $U->fetch_copy_by_barcode( $u2->id );
748         $u2->id( $copy->id );
749     }
750
751     return OpenSRF::AppSession->create('open-ils.cstore')->request(
752         "open-ils.cstore.json_query.atomic",
753         { from =>
754             [   'unapi.' . $u2->classname,
755                 $u2->id,
756                 $format,
757                 $u2->classname,
758                 '{' . ( $u2->includes ? join( ',', keys %{ $u2->includes } ) : '' ) . '}',
759                 $u2->location || undef,
760                 $u2->depth || undef
761             ]
762         }
763     )->gather(1)->[0]{'unapi.'. $u2->classname};
764 }
765 __PACKAGE__->register_method(
766     method    => 'u2',
767     api_name  => 'open-ils.supercat.u2',
768     api_level => 1,
769     argc      => 2,
770     signature =>
771         { desc     => <<"          DESC",
772 Returns the XML representation of the requested object
773           DESC
774           params   =>
775             [
776                 { name => 'u2',
777                   desc => 'The U2 Tag URI (OpenILS::Utils::TagURI)',
778                   type => 'object' },
779                 { name => 'format',
780                   desc => 'For bre and bre feeds, the xml transform format',
781                   type => 'string' }
782             ],
783           'return' =>
784             { desc => 'XML (or transformed) object data',
785               type => 'string' }
786         }
787 );
788
789
790 sub general_browse {
791     my $self = shift;
792     my $client = shift;
793     return tag_sf_browse($self, $client, $self->{tag}, $self->{subfield}, @_);
794 }
795 __PACKAGE__->register_method(
796     method    => 'general_browse',
797     api_name  => 'open-ils.supercat.title.browse',
798     tag       => 'tnf', subfield => 'a',
799     api_level => 1,
800     argc      => 1,
801     signature =>
802         { desc     => "Returns a list of the requested org-scoped record IDs held",
803           params   =>
804             [ { name => 'value', desc => 'The target title', type => 'string' },
805               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
806               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
807               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
808               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
809               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
810           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
811         }
812 );
813 __PACKAGE__->register_method(
814     method    => 'general_browse',
815     api_name  => 'open-ils.supercat.author.browse',
816     tag       => [qw/100 110 111/], subfield => 'a',
817     api_level => 1,
818     argc      => 1,
819     signature =>
820         { desc     => "Returns a list of the requested org-scoped record IDs held",
821           params   =>
822             [ { name => 'value', desc => 'The target author', type => 'string' },
823               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
824               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
825               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
826               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
827               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
828           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
829         }
830 );
831 __PACKAGE__->register_method(
832     method    => 'general_browse',
833     api_name  => 'open-ils.supercat.subject.browse',
834     tag       => [qw/600 610 611 630 648 650 651 653 655 656 662 690 691 696 697 698 699/], subfield => 'a',
835     api_level => 1,
836     argc      => 1,
837     signature =>
838         { desc     => "Returns a list of the requested org-scoped record IDs held",
839           params   =>
840             [ { name => 'value', desc => 'The target subject', type => 'string' },
841               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
842               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
843               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
844               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
845               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
846           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
847         }
848 );
849 __PACKAGE__->register_method(
850     method    => 'general_browse',
851     api_name  => 'open-ils.supercat.topic.browse',
852     tag       => [qw/650 690/], subfield => 'a',
853     api_level => 1,
854     argc      => 1,
855     signature =>
856         { desc     => "Returns a list of the requested org-scoped record IDs held",
857           params   =>
858             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
859               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
860               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
861               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
862               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
863               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
864           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
865         }
866 );
867 __PACKAGE__->register_method(
868     method    => 'general_browse',
869     api_name  => 'open-ils.supercat.series.browse',
870     tag       => [qw/440 490 800 810 811 830/], subfield => 'a',
871     api_level => 1,
872     argc      => 1,
873     signature =>
874         { desc     => "Returns a list of the requested org-scoped record IDs held",
875           params   =>
876             [ { name => 'value', desc => 'The target series', type => 'string' },
877               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
878               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
879               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
880               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
881               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
882           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
883         }
884 );
885
886
887 sub tag_sf_browse {
888     my $self = shift;
889     my $client = shift;
890
891     my $tag = shift;
892     my $subfield = shift;
893     my $value = shift;
894     my $ou = shift;
895     my $page_size = shift || 9;
896     my $page = shift || 0;
897     my $statuses = shift || [];
898     my $copy_locations = shift || [];
899
900     my ($before_limit,$after_limit) = (0,0);
901     my ($before_offset,$after_offset) = (0,0);
902
903     if (!$page) {
904         $before_limit = $after_limit = int($page_size / 2);
905         $after_limit += 1 if ($page_size % 2);
906     } else {
907         $before_offset = $after_offset = int($page_size / 2);
908         $before_offset += 1 if ($page_size % 2);
909         $before_limit = $after_limit = $page_size;
910     }
911
912     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
913
914     my @ou_ids;
915     if ($ou && $ou ne '-') {
916         my $orgs = $_storage->request(
917             "open-ils.cstore.direct.actor.org_unit.search",
918             { shortname => $ou },
919             { flesh     => 100,
920               flesh_fields  => { aou    => [qw/children/] }
921             }
922         )->gather(1);
923         @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
924     }
925
926     $logger->debug("Searching for records at orgs [".join(',',@ou_ids)."], based on $ou");
927
928     my @list = ();
929
930     if ($page <= 0) {
931         my $before = $_storage->request(
932             "open-ils.cstore.json_query.atomic",
933             { select    => { mfr => [qw/record value/] },
934               from      => 'mfr',
935               where     =>
936                 { '+mfr'    =>
937                     { tag   => $tag,
938                       subfield => $subfield,
939                       value => { '<' => lc($value) }
940                     },
941                   '-or' => [
942                     { '-exists' =>
943                         { select=> { acp => [ 'id' ] },
944                           from  => { acn => { acp => { field => 'call_number', fkey => 'id' } } },
945                           where =>
946                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } } },
947                               '+acp' =>
948                                 { deleted => 'f',
949                                   ((@ou_ids)          ? ( circ_lib => \@ou_ids)        : ()),
950                                   ((@$statuses)       ? ( status   => $statuses)       : ()),
951                                   ((@$copy_locations) ? ( location => $copy_locations) : ())
952                                 }
953                             },
954                           limit => 1
955                         }
956                     },
957                     { '-exists' =>
958                         { select=> { auri => [ 'id' ] },
959                           from  => { acn => { auricnm => { field => 'call_number', fkey => 'id', join => { auri => { field => 'id', fkey => 'uri' } } } } },
960                           where =>
961                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } }, (@ou_ids) ? ( owning_lib => \@ou_ids) : () },
962                               '+auri' => { active => 't' }
963                             },
964                           limit => 1
965                         }
966                     }
967                   ]
968                 }, 
969               order_by  => { mfr => { value => 'desc' } },
970               limit     => $before_limit,
971               offset    => abs($page) * $page_size - $before_offset,
972             }
973         )->gather(1);
974         push @list, map { $_->{record} } reverse(@$before);
975     }
976
977     if ($page >= 0) {
978         my $after = $_storage->request(
979             "open-ils.cstore.json_query.atomic",
980             { select    => { mfr => [qw/record value/] },
981               from      => 'mfr',
982               where     =>
983                 { '+mfr'    =>
984                     { tag   => $tag,
985                       subfield => $subfield,
986                       value => { '>=' => lc($value) }
987                     },
988                   '-or' => [
989                     { '-exists' =>
990                         { select=> { acp => [ 'id' ] },
991                           from  => { acn => { acp => { field => 'call_number', fkey => 'id' } } },
992                           where =>
993                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } } },
994                               '+acp' =>
995                                 { deleted => 'f',
996                                   ((@ou_ids)          ? ( circ_lib => \@ou_ids)        : ()),
997                                   ((@$statuses)       ? ( status   => $statuses)       : ()),
998                                   ((@$copy_locations) ? ( location => $copy_locations) : ())
999                                 }
1000                             },
1001                           limit => 1
1002                         }
1003                     },
1004                     { '-exists' =>
1005                         { select=> { auri => [ 'id' ] },
1006                           from  => { acn => { auricnm => { field => 'call_number', fkey => 'id', join => { auri => { field => 'id', fkey => 'uri' } } } } },
1007                           where =>
1008                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } }, (@ou_ids) ? ( owning_lib => \@ou_ids) : () },
1009                               '+auri' => { active => 't' }
1010                             },
1011                           limit => 1
1012                         },
1013                     }
1014                   ]
1015                 }, 
1016               order_by  => { mfr => { value => 'asc' } },
1017               limit     => $after_limit,
1018               offset    => abs($page) * $page_size - $after_offset,
1019             }
1020         )->gather(1);
1021         push @list, map { $_->{record} } @$after;
1022     }
1023
1024     return \@list;
1025 }
1026 __PACKAGE__->register_method(
1027     method    => 'tag_sf_browse',
1028     api_name  => 'open-ils.supercat.tag.browse',
1029     api_level => 1,
1030     argc      => 1,
1031     signature =>
1032         { desc     => <<"          DESC",
1033 Returns a list of the requested org-scoped record IDs held
1034           DESC
1035           params   =>
1036             [
1037                 { name => 'tag',
1038                   desc => 'The target MARC tag',
1039                   type => 'string' },
1040                 { name => 'subfield',
1041                   desc => 'The target MARC subfield',
1042                   type => 'string' },
1043                 { name => 'value',
1044                   desc => 'The target string',
1045                   type => 'string' },
1046                 { name => 'org_unit',
1047                   desc => 'The org unit shortname (or "-" or undef for global) to browse',
1048                   type => 'string' },
1049                 { name => 'page_size',
1050                   desc => 'Count of call numbers to retrieve, default is 9',
1051                   type => 'number' },
1052                 { name => 'page',
1053                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
1054                   type => 'number' },
1055                 { name => 'statuses',
1056                   desc => 'Array of statuses to filter copies by, optional and can be undef.',
1057                   type => 'array' },
1058                 { name => 'locations',
1059                   desc => 'Array of copy locations to filter copies by, optional and can be undef.',
1060                   type => 'array' },
1061             ],
1062           'return' =>
1063             { desc => 'Record IDs that have copies at the relevant org units',
1064               type => 'array' }
1065         }
1066 );
1067
1068 sub grab_authority_browse_axes {
1069     my ($self, $client, $full) = @_;
1070
1071     unless(scalar(keys(%authority_browse_axis_cache))) {
1072         my $axes = new_editor->search_authority_browse_axis([
1073             { code => { '<>' => undef } },
1074             { flesh => 2, flesh_fields => { aba => ['fields'], acsaf => ['bib_fields','sub_entries'] } }
1075         ]);
1076         $authority_browse_axis_cache{$_->code} = $_ for (@$axes);
1077     }
1078
1079     if ($full) {
1080         return [
1081             map { $authority_browse_axis_cache{$_} } sort keys %authority_browse_axis_cache
1082         ];
1083     } else {
1084         return [keys %authority_browse_axis_cache];
1085     }
1086 }
1087 __PACKAGE__->register_method(
1088     method    => 'grab_authority_browse_axes',
1089     api_name  => 'open-ils.supercat.authority.browse_axis_list',
1090     api_level => 1,
1091     argc      => 1,
1092     signature =>
1093         { desc     => "Returns a list of valid authority browse/startswith axes",
1094           params   => [
1095               { name => 'full', desc => 'Optional. If true, return array containing the full object for each axis, sorted by code. Otherwise just return an array of the codes.', type => 'number' }
1096           ],
1097           'return' => { desc => 'Axis codes or whole axes, see "full" param', type => 'array' }
1098         }
1099 );
1100
1101 sub axis_authority_browse {
1102     my $self = shift;
1103     my $client = shift;
1104     my $axis = shift;
1105
1106     $axis =~ s/^authority\.//;
1107     $axis =~ s/(\.refs)$//;
1108     my $refs = $1;
1109
1110     return undef unless ( grep { /$axis/ } @{ grab_authority_browse_axes() } );
1111
1112     my @tags;
1113     for my $f (@{$authority_browse_axis_cache{$axis}->fields}) {
1114         push @tags, $f->tag;
1115         if ($refs) {
1116             push @tags, $_->tag for @{$f->sub_entries};
1117         }
1118     }
1119
1120     return authority_tag_sf_browse($self, $client, \@tags, 'a', @_); # XXX TODO figure out something more correct for the subfield param
1121 }
1122 __PACKAGE__->register_method(
1123     method    => 'axis_authority_browse',
1124     api_name  => 'open-ils.supercat.authority.browse.by_axis',
1125     api_level => 1,
1126     argc      => 2,
1127     signature =>
1128         { desc     => "Returns a list of the requested authority record IDs held",
1129           params   =>
1130             [ { name => 'axis', desc => 'The target axis', type => 'string' },
1131               { name => 'value', desc => 'The target value', type => 'string' },
1132               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1133               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1134           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1135         }
1136 );
1137
1138 =pod
1139
1140 sub general_authority_browse {
1141     my $self = shift;
1142     my $client = shift;
1143     return authority_tag_sf_browse($self, $client, $self->{tag}, $self->{subfield}, @_);
1144 }
1145 __PACKAGE__->register_method(
1146     method    => 'general_authority_browse',
1147     api_name  => 'open-ils.supercat.authority.title.browse',
1148     tag       => ['130'], subfield => 'a',
1149     api_level => 1,
1150     argc      => 1,
1151     signature =>
1152         { desc     => "Returns a list of the requested authority record IDs held",
1153           params   =>
1154             [ { name => 'value', desc => 'The target title', type => 'string' },
1155               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1156               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1157           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1158         }
1159 );
1160 __PACKAGE__->register_method(
1161     method    => 'general_authority_browse',
1162     api_name  => 'open-ils.supercat.authority.author.browse',
1163     tag       => [qw/100 110 111/], subfield => 'a',
1164     api_level => 1,
1165     argc      => 1,
1166     signature =>
1167         { desc     => "Returns a list of the requested authority record IDs held",
1168           params   =>
1169             [ { name => 'value', desc => 'The target author', type => 'string' },
1170               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1171               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1172           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1173         }
1174 );
1175 __PACKAGE__->register_method(
1176     method    => 'general_authority_browse',
1177     api_name  => 'open-ils.supercat.authority.subject.browse',
1178     tag       => [qw/148 150 151 155/], subfield => 'a',
1179     api_level => 1,
1180     argc      => 1,
1181     signature =>
1182         { desc     => "Returns a list of the requested authority record IDs held",
1183           params   =>
1184             [ { name => 'value', desc => 'The target subject', type => 'string' },
1185               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1186               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1187           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1188         }
1189 );
1190 __PACKAGE__->register_method(
1191     method    => 'general_authority_browse',
1192     api_name  => 'open-ils.supercat.authority.topic.browse',
1193     tag       => ['150'], subfield => 'a',
1194     api_level => 1,
1195     argc      => 1,
1196     signature =>
1197         { desc     => "Returns a list of the requested authority record IDs held",
1198           params   =>
1199             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
1200               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1201               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1202           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1203         }
1204 );
1205 __PACKAGE__->register_method(
1206     method    => 'general_authority_browse',
1207     api_name  => 'open-ils.supercat.authority.title.refs.browse',
1208     tag       => ['130'], subfield => 'a',
1209     api_level => 1,
1210     argc      => 1,
1211     signature =>
1212         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1213           params   =>
1214             [ { name => 'value', desc => 'The target title', type => 'string' },
1215               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1216               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1217           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1218         }
1219 );
1220 __PACKAGE__->register_method(
1221     method    => 'general_authority_browse',
1222     api_name  => 'open-ils.supercat.authority.author.refs.browse',
1223     tag       => [qw/100 110 111/], subfield => 'a',
1224     api_level => 1,
1225     argc      => 1,
1226     signature =>
1227         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1228           params   =>
1229             [ { name => 'value', desc => 'The target author', type => 'string' },
1230               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1231               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1232           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1233         }
1234 );
1235 __PACKAGE__->register_method(
1236     method    => 'general_authority_browse',
1237     api_name  => 'open-ils.supercat.authority.subject.refs.browse',
1238     tag       => [qw/148 150 151 155/], subfield => 'a',
1239     api_level => 1,
1240     argc      => 1,
1241     signature =>
1242         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1243           params   =>
1244             [ { name => 'value', desc => 'The target subject', type => 'string' },
1245               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1246               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1247           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1248         }
1249 );
1250 __PACKAGE__->register_method(
1251     method    => 'general_authority_browse',
1252     api_name  => 'open-ils.supercat.authority.topic.refs.browse',
1253     tag       => ['150'], subfield => 'a',
1254     api_level => 1,
1255     argc      => 1,
1256     signature =>
1257         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1258           params   =>
1259             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
1260               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1261               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1262           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1263         }
1264 );
1265
1266 =cut
1267
1268 sub authority_tag_sf_browse {
1269     my $self = shift;
1270     my $client = shift;
1271
1272     my $tag = shift;
1273     my $subfield = shift;
1274     my $value = shift;
1275     my $page_size = shift || 9;
1276     my $page = shift || 0;
1277
1278     # Match authority.full_rec normalization
1279     $value = naco_normalize($value, $subfield);
1280
1281     my ($before_limit,$after_limit) = (0,0);
1282     my ($before_offset,$after_offset) = (0,0);
1283
1284     if (!$page) {
1285         $before_limit = $after_limit = int($page_size / 2);
1286         $after_limit += 1 if ($page_size % 2);
1287     } else {
1288         $before_offset = $after_offset = int($page_size / 2);
1289         $before_offset += 1 if ($page_size % 2);
1290         $before_limit = $after_limit = $page_size;
1291     }
1292
1293     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
1294
1295     # .refs variant includes 4xx and 5xx variants for see / see also
1296     my @ref_tags = ();
1297     foreach my $tagname (@$tag) {
1298         push(@ref_tags, $tagname);
1299         if ($self->api_name =~ /\.refs\./) {
1300             push(@ref_tags, '4' . substr($tagname, 1, 2));
1301             push(@ref_tags, '5' . substr($tagname, 1, 2));
1302         }
1303     }
1304     my @list = ();
1305
1306     if ($page <= 0) {
1307         my $before = $_storage->request(
1308             "open-ils.cstore.json_query.atomic",
1309             { select    => { afr => [qw/record value/] },
1310               from      => 'afr',
1311               where     => { tag => \@ref_tags, subfield => $subfield, value => { '<' => $value } },
1312               order_by  => { afr => { value => 'desc' } },
1313               limit     => $before_limit,
1314               offset    => abs($page) * $page_size - $before_offset,
1315             }
1316         )->gather(1);
1317         push @list, map { $_->{record} } reverse(@$before);
1318     }
1319
1320     if ($page >= 0) {
1321         my $after = $_storage->request(
1322             "open-ils.cstore.json_query.atomic",
1323             { select    => { afr => [qw/record value/] },
1324               from      => 'afr',
1325               where     => { tag => \@ref_tags, subfield => $subfield, value => { '>=' => $value } },
1326               order_by  => { afr => { value => 'asc' } },
1327               limit     => $after_limit,
1328               offset    => abs($page) * $page_size - $after_offset,
1329             }
1330         )->gather(1);
1331         push @list, map { $_->{record} } @$after;
1332     }
1333
1334     # If we're not pulling in see/see also references, just return the raw list
1335     if ($self->api_name !~ /\.refs\./) {
1336         return \@list;
1337     } 
1338
1339     # Remove dupe record IDs that turn up due to 4xx and 5xx matches
1340     my @retlist = ();
1341     my %seen;
1342     foreach my $record (@list) {
1343         next if exists $seen{$record};
1344         push @retlist, int($record);
1345         $seen{$record} = 1;
1346     }
1347
1348     return \@retlist;
1349 }
1350 __PACKAGE__->register_method(
1351     method    => 'authority_tag_sf_browse',
1352     api_name  => 'open-ils.supercat.authority.tag.browse',
1353     api_level => 1,
1354     argc      => 1,
1355     signature =>
1356         { desc     => <<"          DESC",
1357 Returns a list of the requested authority record IDs held
1358           DESC
1359           params   =>
1360             [
1361                 { name => 'tag',
1362                   desc => 'The target Authority MARC tag',
1363                   type => 'string' },
1364                 { name => 'subfield',
1365                   desc => 'The target Authority MARC subfield',
1366                   type => 'string' },
1367                 { name => 'value',
1368                   desc => 'The target string',
1369                   type => 'string' },
1370                 { name => 'page_size',
1371                   desc => 'Count of call numbers to retrieve, default is 9',
1372                   type => 'number' },
1373                 { name => 'page',
1374                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
1375                   type => 'number' },
1376             ],
1377           'return' =>
1378             { desc => 'Authority Record IDs that are near the target string',
1379               type => 'array' }
1380         }
1381 );
1382
1383 sub general_startwith {
1384     my $self = shift;
1385     my $client = shift;
1386     return tag_sf_startwith($self, $client, $self->{tag}, $self->{subfield}, @_);
1387 }
1388 __PACKAGE__->register_method(
1389     method    => 'general_startwith',
1390     api_name  => 'open-ils.supercat.title.startwith',
1391     tag       => 'tnf', subfield => 'a',
1392     api_level => 1,
1393     argc      => 1,
1394     signature =>
1395         { desc     => "Returns a list of the requested org-scoped record IDs held",
1396           params   =>
1397             [ { name => 'value', desc => 'The target title', type => 'string' },
1398               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
1399               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1400               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
1401               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
1402               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
1403           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
1404         }
1405 );
1406 __PACKAGE__->register_method(
1407     method    => 'general_startwith',
1408     api_name  => 'open-ils.supercat.author.startwith',
1409     tag       => [qw/100 110 111/], subfield => 'a',
1410     api_level => 1,
1411     argc      => 1,
1412     signature =>
1413         { desc     => "Returns a list of the requested org-scoped record IDs held",
1414           params   =>
1415             [ { name => 'value', desc => 'The target author', type => 'string' },
1416               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
1417               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1418               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
1419               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
1420               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
1421           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
1422         }
1423 );
1424 __PACKAGE__->register_method(
1425     method    => 'general_startwith',
1426     api_name  => 'open-ils.supercat.subject.startwith',
1427     tag       => [qw/600 610 611 630 648 650 651 653 655 656 662 690 691 696 697 698 699/], subfield => 'a',
1428     api_level => 1,
1429     argc      => 1,
1430     signature =>
1431         { desc     => "Returns a list of the requested org-scoped record IDs held",
1432           params   =>
1433             [ { name => 'value', desc => 'The target subject', type => 'string' },
1434               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
1435               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1436               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
1437               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
1438               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
1439           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
1440         }
1441 );
1442 __PACKAGE__->register_method(
1443     method    => 'general_startwith',
1444     api_name  => 'open-ils.supercat.topic.startwith',
1445     tag       => [qw/650 690/], subfield => 'a',
1446     api_level => 1,
1447     argc      => 1,
1448     signature =>
1449         { desc     => "Returns a list of the requested org-scoped record IDs held",
1450           params   =>
1451             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
1452               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
1453               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1454               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
1455               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
1456               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
1457           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
1458         }
1459 );
1460 __PACKAGE__->register_method(
1461     method    => 'general_startwith',
1462     api_name  => 'open-ils.supercat.series.startwith',
1463     tag       => [qw/440 490 800 810 811 830/], subfield => 'a',
1464     api_level => 1,
1465     argc      => 1,
1466     signature =>
1467         { desc     => "Returns a list of the requested org-scoped record IDs held",
1468           params   =>
1469             [ { name => 'value', desc => 'The target series', type => 'string' },
1470               { name => 'org_unit', desc => 'The org unit shortname (or "-" or undef for global) to browse', type => 'string' },
1471               { name => 'page_size', desc => 'Count of records to retrieve, default is 9', type => 'number' },
1472               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' },
1473               { name => 'statuses', desc => 'Array of statuses to filter copies by, optional and can be undef.', type => 'array' },
1474               { name => 'locations', desc => 'Array of copy locations to filter copies by, optional and can be undef.', type => 'array' }, ],
1475           'return' => { desc => 'Record IDs that have copies at the relevant org units', type => 'array' }
1476         }
1477 );
1478
1479
1480 sub tag_sf_startwith {
1481     my $self = shift;
1482     my $client = shift;
1483
1484     my $tag = shift;
1485     my $subfield = shift;
1486     my $value = shift;
1487     my $ou = shift;
1488     my $limit = shift || 10;
1489     my $page = shift || 0;
1490     my $statuses = shift || [];
1491     my $copy_locations = shift || [];
1492
1493     my $offset = $limit * abs($page);
1494     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
1495
1496     my @ou_ids;
1497     if ($ou && $ou ne '-') {
1498         my $orgs = $_storage->request(
1499             "open-ils.cstore.direct.actor.org_unit.search",
1500             { shortname => $ou },
1501             { flesh     => 100,
1502               flesh_fields  => { aou    => [qw/children/] }
1503             }
1504         )->gather(1);
1505         @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
1506     }
1507
1508     $logger->debug("Searching for records at orgs [".join(',',@ou_ids)."], based on $ou");
1509
1510     my @list = ();
1511
1512     if ($page < 0) {
1513         my $before = $_storage->request(
1514             "open-ils.cstore.json_query.atomic",
1515             { select    => { mfr => [qw/record value/] },
1516               from      => 'mfr',
1517               where     =>
1518                 { '+mfr'    =>
1519                     { tag   => $tag,
1520                       subfield => $subfield,
1521                       value => { '<' => lc($value) }
1522                     },
1523                   '-or' => [
1524                     { '-exists' =>
1525                         { select=> { acp => [ 'id' ] },
1526                           from  => { acn => { acp => { field => 'call_number', fkey => 'id' } } },
1527                           where =>
1528                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } } },
1529                               '+acp' =>
1530                                 { deleted => 'f',
1531                                   ((@ou_ids)          ? ( circ_lib => \@ou_ids)        : ()),
1532                                   ((@$statuses)       ? ( status   => $statuses)       : ()),
1533                                   ((@$copy_locations) ? ( location => $copy_locations) : ())
1534                                 }
1535                             },
1536                           limit => 1
1537                         }
1538                     },
1539                     { '-exists' =>
1540                         { select=> { auri => [ 'id' ] },
1541                           from  => { acn => { auricnm => { field => 'call_number', fkey => 'id', join => { auri => { field => 'id', fkey => 'uri' } } } } },
1542                           where =>
1543                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } }, (@ou_ids) ? ( owning_lib => \@ou_ids) : () },
1544                               '+auri' => { active => 't' }
1545                             },
1546                           limit => 1
1547                         }
1548                     }
1549                   ]
1550                 }, 
1551               order_by  => { mfr => { value => 'desc' } },
1552               limit     => $limit,
1553               offset    => $offset
1554             }
1555         )->gather(1);
1556         push @list, map { $_->{record} } reverse(@$before);
1557     }
1558
1559     if ($page >= 0) {
1560         my $after = $_storage->request(
1561             "open-ils.cstore.json_query.atomic",
1562             { select    => { mfr => [qw/record value/] },
1563               from      => 'mfr',
1564               where     =>
1565                 { '+mfr'    =>
1566                     { tag   => $tag,
1567                       subfield => $subfield,
1568                       value => { '>=' => lc($value) }
1569                     },
1570                   '-or' => [
1571                     { '-exists' =>
1572                         { select=> { acp => [ 'id' ] },
1573                           from  => { acn => { acp => { field => 'call_number', fkey => 'id' } } },
1574                           where =>
1575                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } } },
1576                               '+acp' =>
1577                                 { deleted => 'f',
1578                                   ((@ou_ids)          ? ( circ_lib => \@ou_ids)        : ()),
1579                                   ((@$statuses)       ? ( status   => $statuses)       : ()),
1580                                   ((@$copy_locations) ? ( location => $copy_locations) : ())
1581                                 }
1582                             },
1583                           limit => 1
1584                         }
1585                     },
1586                     { '-exists' =>
1587                         { select=> { auri => [ 'id' ] },
1588                           from  => { acn => { auricnm => { field => 'call_number', fkey => 'id', join => { auri => { field => 'id', fkey => 'uri' } } } } },
1589                           where =>
1590                             { '+acn' => { record => { '=' => { '+mfr' => 'record' } }, (@ou_ids) ? ( owning_lib => \@ou_ids) : () },
1591                               '+auri' => { active => 't' }
1592                             },
1593                           limit => 1
1594                         },
1595                     }
1596                   ]
1597                 }, 
1598               order_by  => { mfr => { value => 'asc' } },
1599               limit     => $limit,
1600               offset    => $offset
1601             }
1602         )->gather(1);
1603         push @list, map { $_->{record} } @$after;
1604     }
1605
1606     return \@list;
1607 }
1608 __PACKAGE__->register_method(
1609     method    => 'tag_sf_startwith',
1610     api_name  => 'open-ils.supercat.tag.startwith',
1611     api_level => 1,
1612     argc      => 1,
1613     signature =>
1614         { desc     => <<"          DESC",
1615 Returns a list of the requested org-scoped record IDs held
1616           DESC
1617           params   =>
1618             [
1619                 { name => 'tag',
1620                   desc => 'The target MARC tag',
1621                   type => 'string' },
1622                 { name => 'subfield',
1623                   desc => 'The target MARC subfield',
1624                   type => 'string' },
1625                 { name => 'value',
1626                   desc => 'The target string',
1627                   type => 'string' },
1628                 { name => 'org_unit',
1629                   desc => 'The org unit shortname (or "-" or undef for global) to browse',
1630                   type => 'string' },
1631                 { name => 'page_size',
1632                   desc => 'Count of call numbers to retrieve, default is 9',
1633                   type => 'number' },
1634                 { name => 'page',
1635                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
1636                   type => 'number' },
1637                 { name => 'statuses',
1638                   desc => 'Array of statuses to filter copies by, optional and can be undef.',
1639                   type => 'array' },
1640                 { name => 'locations',
1641                   desc => 'Array of copy locations to filter copies by, optional and can be undef.',
1642                   type => 'array' },
1643             ],
1644           'return' =>
1645             { desc => 'Record IDs that have copies at the relevant org units',
1646               type => 'array' }
1647         }
1648 );
1649
1650 sub axis_authority_startwith {
1651     my $self = shift;
1652     my $client = shift;
1653     my $axis = shift;
1654
1655     $axis =~ s/^authority\.//;
1656     $axis =~ s/(\.refs)$//;
1657     my $refs = $1;
1658
1659     return undef unless ( grep { /$axis/ } @{ grab_authority_browse_axes() } );
1660
1661     my @tags;
1662     for my $f (@{$authority_browse_axis_cache{$axis}->fields}) {
1663         push @tags, $f->tag;
1664         if ($refs) {
1665             push @tags, $_->tag for @{$f->sub_entries};
1666         }
1667     }
1668
1669     return authority_tag_sf_startwith($self, $client, \@tags, 'a', @_); # XXX TODO figure out something more correct for the subfield param
1670 }
1671 __PACKAGE__->register_method(
1672     method    => 'axis_authority_startwith',
1673     api_name  => 'open-ils.supercat.authority.startwith.by_axis',
1674     api_level => 1,
1675     argc      => 2,
1676     signature =>
1677         { desc     => "Returns a list of the requested authority record IDs held",
1678           params   =>
1679             [ { name => 'axis', desc => 'The target axis', type => 'string' },
1680               { name => 'value', desc => 'The target value', type => 'string' },
1681               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1682               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1683           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1684         }
1685 );
1686
1687 =pod
1688
1689 sub general_authority_startwith {
1690     my $self = shift;
1691     my $client = shift;
1692     return authority_tag_sf_startwith($self, $client, $self->{tag}, $self->{subfield}, @_);
1693 }
1694 __PACKAGE__->register_method(
1695     method    => 'general_authority_startwith',
1696     api_name  => 'open-ils.supercat.authority.title.startwith',
1697     tag       => ['130'], subfield => 'a',
1698     api_level => 1,
1699     argc      => 1,
1700     signature =>
1701         { desc     => "Returns a list of the requested authority record IDs held",
1702           params   =>
1703             [ { name => 'value', desc => 'The target title', type => 'string' },
1704               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1705               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1706           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1707         }
1708 );
1709 __PACKAGE__->register_method(
1710     method    => 'general_authority_startwith',
1711     api_name  => 'open-ils.supercat.authority.author.startwith',
1712     tag       => [qw/100 110 111/], subfield => 'a',
1713     api_level => 1,
1714     argc      => 1,
1715     signature =>
1716         { desc     => "Returns a list of the requested authority record IDs held",
1717           params   =>
1718             [ { name => 'value', desc => 'The target author', type => 'string' },
1719               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1720               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1721           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1722         }
1723 );
1724 __PACKAGE__->register_method(
1725     method    => 'general_authority_startwith',
1726     api_name  => 'open-ils.supercat.authority.subject.startwith',
1727     tag       => [qw/148 150 151 155/], subfield => 'a',
1728     api_level => 1,
1729     argc      => 1,
1730     signature =>
1731         { desc     => "Returns a list of the requested authority record IDs held",
1732           params   =>
1733             [ { name => 'value', desc => 'The target subject', type => 'string' },
1734               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1735               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1736           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1737         }
1738 );
1739 __PACKAGE__->register_method(
1740     method    => 'general_authority_startwith',
1741     api_name  => 'open-ils.supercat.authority.topic.startwith',
1742     tag       => ['150'], subfield => 'a',
1743     api_level => 1,
1744     argc      => 1,
1745     signature =>
1746         { desc     => "Returns a list of the requested authority record IDs held",
1747           params   =>
1748             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
1749               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1750               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1751           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1752         }
1753 );
1754 __PACKAGE__->register_method(
1755     method    => 'general_authority_startwith',
1756     api_name  => 'open-ils.supercat.authority.title.refs.startwith',
1757     tag       => ['130'], subfield => 'a',
1758     api_level => 1,
1759     argc      => 1,
1760     signature =>
1761         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1762           params   =>
1763             [ { name => 'value', desc => 'The target title', type => 'string' },
1764               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1765               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1766           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1767         }
1768 );
1769 __PACKAGE__->register_method(
1770     method    => 'general_authority_startwith',
1771     api_name  => 'open-ils.supercat.authority.author.refs.startwith',
1772     tag       => [qw/100 110 111/], subfield => 'a',
1773     api_level => 1,
1774     argc      => 1,
1775     signature =>
1776         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1777           params   =>
1778             [ { name => 'value', desc => 'The target author', type => 'string' },
1779               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1780               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1781           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1782         }
1783 );
1784 __PACKAGE__->register_method(
1785     method    => 'general_authority_startwith',
1786     api_name  => 'open-ils.supercat.authority.subject.refs.startwith',
1787     tag       => [qw/148 150 151 155/], subfield => 'a',
1788     api_level => 1,
1789     argc      => 1,
1790     signature =>
1791         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1792           params   =>
1793             [ { name => 'value', desc => 'The target subject', type => 'string' },
1794               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1795               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1796           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1797         }
1798 );
1799 __PACKAGE__->register_method(
1800     method    => 'general_authority_startwith',
1801     api_name  => 'open-ils.supercat.authority.topic.refs.startwith',
1802     tag       => ['150'], subfield => 'a',
1803     api_level => 1,
1804     argc      => 1,
1805     signature =>
1806         { desc     => "Returns a list of the requested authority record IDs held, including see (4xx) and see also (5xx) references",
1807           params   =>
1808             [ { name => 'value', desc => 'The target topical subject', type => 'string' },
1809               { name => 'page_size', desc => 'Count of records to retrieve, default is 10', type => 'number' },
1810               { name => 'page', desc => 'The page of records retrieved, calculated based on page_size.  Can be positive, negative or 0.', type => 'number' }, ],
1811           'return' => { desc => 'Authority Record IDs that are near the target string', type => 'array' }
1812         }
1813 );
1814
1815 =cut
1816
1817 sub authority_tag_sf_startwith {
1818     my $self = shift;
1819     my $client = shift;
1820
1821     my $tag = shift;
1822     my $subfield = shift;
1823
1824     my $value = shift;
1825     my $limit = shift || 10;
1826     my $page = shift || 0;
1827
1828     # Match authority.full_rec normalization
1829     $value = naco_normalize($value, $subfield);
1830
1831     my $ref_limit = $limit;
1832     my $offset = $limit * abs($page);
1833     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
1834
1835     my @ref_tags = ();
1836     # .refs variant includes 4xx and 5xx variants for see / see also
1837     foreach my $tagname (@$tag) {
1838         push(@ref_tags, $tagname);
1839         if ($self->api_name =~ /\.refs\./) {
1840             push(@ref_tags, '4' . substr($tagname, 1, 2));
1841             push(@ref_tags, '5' . substr($tagname, 1, 2));
1842         }
1843     }
1844
1845     my @list = ();
1846
1847     if ($page < 0) {
1848         # Don't skip the first actual page of results in descending order
1849         $offset = $offset - $limit;
1850
1851         my $before = $_storage->request(
1852             "open-ils.cstore.json_query.atomic",
1853             { select    => { afr => [qw/record value/] },
1854               from      => 'afr',
1855               where     => { tag => \@ref_tags, subfield => $subfield, value => { '<' => $value } },
1856               order_by  => { afr => { value => 'desc' } },
1857               limit     => $ref_limit,
1858               offset    => $offset,
1859             }
1860         )->gather(1);
1861         push @list, map { $_->{record} } reverse(@$before);
1862     }
1863
1864     if ($page >= 0) {
1865         my $after = $_storage->request(
1866             "open-ils.cstore.json_query.atomic",
1867             { select    => { afr => [qw/record value/] },
1868               from      => 'afr',
1869               where     => { tag => \@ref_tags, subfield => $subfield, value => { '>=' => $value } },
1870               order_by  => { afr => { value => 'asc' } },
1871               limit     => $ref_limit,
1872               offset    => $offset,
1873             }
1874         )->gather(1);
1875         push @list, map { $_->{record} } @$after;
1876     }
1877
1878     # If we're not pulling in see/see also references, just return the raw list
1879     if ($self->api_name !~ /\.refs\./) {
1880         return \@list;
1881     }
1882
1883     # Remove dupe record IDs that turn up due to 4xx and 5xx matches
1884     my @retlist = ();
1885     my %seen;
1886     foreach my $record (@list) {
1887         next if exists $seen{$record};
1888         push @retlist, int($record);
1889         $seen{$record} = 1;
1890     }
1891
1892     return \@retlist;
1893 }
1894 __PACKAGE__->register_method(
1895     method    => 'authority_tag_sf_startwith',
1896     api_name  => 'open-ils.supercat.authority.tag.startwith',
1897     api_level => 1,
1898     argc      => 1,
1899     signature =>
1900         { desc     => <<"          DESC",
1901 Returns a list of the requested authority record IDs held
1902           DESC
1903           params   =>
1904             [
1905                 { name => 'tag',
1906                   desc => 'The target Authority MARC tag',
1907                   type => 'string' },
1908                 { name => 'subfield',
1909                   desc => 'The target Authority MARC subfield',
1910                   type => 'string' },
1911                 { name => 'value',
1912                   desc => 'The target string',
1913                   type => 'string' },
1914                 { name => 'page_size',
1915                   desc => 'Count of call numbers to retrieve, default is 10',
1916                   type => 'number' },
1917                 { name => 'page',
1918                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
1919                   type => 'number' },
1920             ],
1921           'return' =>
1922             { desc => 'Authority Record IDs that are near the target string',
1923               type => 'array' }
1924         }
1925 );
1926
1927
1928 sub holding_data_formats {
1929     return [{
1930         marcxml => {
1931             namespace_uri     => 'http://www.loc.gov/MARC21/slim',
1932             docs          => 'http://www.loc.gov/marcxml/',
1933             schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
1934         }
1935     }];
1936 }
1937 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.acn.formats', api_level => 1 );
1938 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.acp.formats', api_level => 1 );
1939 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.auri.formats', api_level => 1 );
1940
1941
1942 __PACKAGE__->register_method(
1943     method    => 'retrieve_uri',
1944     api_name  => 'open-ils.supercat.auri.marcxml.retrieve',
1945     api_level => 1,
1946     argc      => 1,
1947     signature =>
1948         { desc     => <<"          DESC",
1949 Returns a fleshed call number object
1950           DESC
1951           params   =>
1952             [
1953                 { name => 'uri_id',
1954                   desc => 'An OpenILS asset::uri id',
1955                   type => 'number' },
1956             ],
1957           'return' =>
1958             { desc => 'fleshed uri',
1959               type => 'object' }
1960         }
1961 );
1962 sub retrieve_uri {
1963     my $self = shift;
1964     my $client = shift;
1965     my $cpid = shift;
1966     my $args = shift || {};
1967
1968     return OpenILS::Application::SuperCat::unAPI
1969         ->new(OpenSRF::AppSession
1970             ->create( 'open-ils.cstore' )
1971             ->request(
1972                 "open-ils.cstore.direct.asset.uri.retrieve",
1973                 $cpid,
1974                 { flesh     => 10,
1975                   flesh_fields  => {
1976                             auri    => [qw/call_number_maps/],
1977                             auricnm => [qw/call_number/],
1978                             acn     => [qw/owning_lib record prefix suffix/],
1979                     }
1980                 })
1981             ->gather(1))
1982         ->as_xml($args);
1983 }
1984
1985 __PACKAGE__->register_method(
1986     method    => 'retrieve_copy',
1987     api_name  => 'open-ils.supercat.acp.marcxml.retrieve',
1988     api_level => 1,
1989     argc      => 1,
1990     signature =>
1991         { desc     => <<"          DESC",
1992 Returns a fleshed call number object
1993           DESC
1994           params   =>
1995             [
1996                 { name => 'cn_id',
1997                   desc => 'An OpenILS asset::copy id',
1998                   type => 'number' },
1999             ],
2000           'return' =>
2001             { desc => 'fleshed copy',
2002               type => 'object' }
2003         }
2004 );
2005 sub retrieve_copy {
2006     my $self = shift;
2007     my $client = shift;
2008     my $cpid = shift;
2009     my $args = shift || {};
2010
2011     return OpenILS::Application::SuperCat::unAPI
2012         ->new(OpenSRF::AppSession
2013             ->create( 'open-ils.cstore' )
2014             ->request(
2015                 "open-ils.cstore.direct.asset.copy.retrieve",
2016                 $cpid,
2017                 { flesh     => 2,
2018                   flesh_fields  => {
2019                             acn => [qw/owning_lib record prefix suffix/],
2020                             acp => [qw/call_number location status circ_lib stat_cat_entries notes parts/],
2021                     }
2022                 })
2023             ->gather(1))
2024         ->as_xml($args);
2025 }
2026
2027 __PACKAGE__->register_method(
2028     method    => 'retrieve_callnumber',
2029     api_name  => 'open-ils.supercat.acn.marcxml.retrieve',
2030     api_level => 1,
2031     argc      => 1,
2032     stream    => 1,
2033     signature =>
2034         { desc     => <<"          DESC",
2035 Returns a fleshed call number object
2036           DESC
2037           params   =>
2038             [
2039                 { name => 'cn_id',
2040                   desc => 'An OpenILS asset::call_number id',
2041                   type => 'number' },
2042             ],
2043           'return' =>
2044             { desc => 'call number with copies',
2045               type => 'object' }
2046         }
2047 );
2048 sub retrieve_callnumber {
2049     my $self = shift;
2050     my $client = shift;
2051     my $cnid = shift;
2052     my $args = shift || {};
2053
2054     return OpenILS::Application::SuperCat::unAPI
2055         ->new(OpenSRF::AppSession
2056             ->create( 'open-ils.cstore' )
2057             ->request(
2058                 "open-ils.cstore.direct.asset.call_number.retrieve",
2059                 $cnid,
2060                 { flesh     => 5,
2061                   flesh_fields  => {
2062                             acn => [qw/owning_lib record copies uri_maps prefix suffix/],
2063                             auricnm => [qw/uri/],
2064                             acp => [qw/location status circ_lib stat_cat_entries notes parts/],
2065                     }
2066                 })
2067             ->gather(1))
2068         ->as_xml($args);
2069
2070 }
2071
2072 __PACKAGE__->register_method(
2073     method    => 'basic_record_holdings',
2074     api_name  => 'open-ils.supercat.record.basic_holdings.retrieve',
2075     api_level => 1,
2076     argc      => 1,
2077     stream    => 1,
2078     signature =>
2079         { desc     => <<"          DESC",
2080 Returns a basic hash representation of the requested bibliographic record's holdings
2081           DESC
2082           params   =>
2083             [
2084                 { name => 'bibId',
2085                   desc => 'An OpenILS biblio::record_entry id',
2086                   type => 'number' },
2087             ],
2088           'return' =>
2089             { desc => 'Hash of bib record holdings hierarchy (call numbers and copies)',
2090               type => 'string' }
2091         }
2092 );
2093 sub basic_record_holdings {
2094     my $self = shift;
2095     my $client = shift;
2096     my $bib = shift;
2097     my $ou = shift;
2098
2099     #  holdings hold an array of call numbers, which hold an array of copies
2100     #  holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ]
2101     my %holdings;
2102
2103     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2104
2105     my $tree = $_storage->request(
2106         "open-ils.cstore.direct.biblio.record_entry.retrieve",
2107         $bib,
2108         { flesh     => 5,
2109           flesh_fields  => {
2110                     bre => [qw/call_numbers/],
2111                     acn => [qw/copies owning_lib prefix suffix/],
2112                     acp => [qw/location status circ_lib parts/],
2113                 }
2114         }
2115     )->gather(1);
2116
2117     my $o_search = { shortname => uc($ou) };
2118     if (!$ou || $ou eq '-') {
2119         $o_search = { parent_ou => undef };
2120     }
2121
2122     my $orgs = $_storage->request(
2123         "open-ils.cstore.direct.actor.org_unit.search",
2124         $o_search,
2125         { flesh     => 100,
2126           flesh_fields  => { aou    => [qw/children/] }
2127         }
2128     )->gather(1);
2129
2130     my @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
2131
2132     $logger->debug("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2133
2134     for my $cn (@{$tree->call_numbers}) {
2135         next unless ( $cn->deleted eq 'f' || !$cn->deleted );
2136
2137         my $found = 0;
2138         for my $c (@{$cn->copies}) {
2139             next unless grep {$c->circ_lib->id == $_} @ou_ids;
2140             next unless _cp_is_visible($cn, $c);
2141             $found = 1;
2142             last;
2143         }
2144         next unless $found;
2145
2146         $holdings{$cn->label}{'owning_lib'} = $cn->owning_lib->shortname;
2147
2148         for my $cp (@{$cn->copies}) {
2149
2150             next unless grep { $cp->circ_lib->id == $_ } @ou_ids;
2151             next unless _cp_is_visible($cn, $cp);
2152
2153             push @{$holdings{$cn->label}{'copies'}}, {
2154                 barcode => $cp->barcode,
2155                 status => $cp->status->name,
2156                 location => $cp->location->name,
2157                 circlib => $cp->circ_lib->shortname
2158             };
2159
2160         }
2161     }
2162
2163     return \%holdings;
2164 }
2165
2166 sub _cp_is_visible {
2167     my $cn = shift;
2168     my $cp = shift;
2169
2170     my $visible = 0;
2171     if ( ($cp->deleted eq 'f' || !$cp->deleted) &&
2172          $cp->location->opac_visible eq 't' && 
2173          $cp->status->opac_visible eq 't' &&
2174          $cp->opac_visible eq 't' &&
2175          $cp->circ_lib->opac_visible eq 't' &&
2176          $cn->owning_lib->opac_visible eq 't'
2177     ) {
2178         $visible = 1;
2179     }
2180
2181     return $visible;
2182 }
2183
2184 #__PACKAGE__->register_method(
2185 #   method    => 'new_record_holdings',
2186 #   api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
2187 #   api_level => 1,
2188 #   argc      => 1,
2189 #   stream    => 1,
2190 #   signature =>
2191 #        { desc     => <<"          DESC",
2192 #Returns the XML representation of the requested bibliographic record's holdings
2193 #         DESC
2194 #         params   =>
2195 #           [
2196 #               { name => 'bibId',
2197 #                 desc => 'An OpenILS biblio::record_entry id',
2198 #                 type => 'number' },
2199 #           ],
2200 #         'return' =>
2201 #           { desc => 'Stream of bib record holdings hierarchy in XML',
2202 #             type => 'string' }
2203 #       }
2204 #);
2205 #
2206
2207 sub new_record_holdings {
2208     my $self = shift;
2209     my $client = shift;
2210     my $bib = shift;
2211     my $ou = shift;
2212     my $depth = shift;
2213     my $flesh = shift;
2214     my $paging = shift;
2215
2216     $paging = [-1,0] if (!$paging or !ref($paging) or @$paging == 0);
2217     my $limit = $$paging[0];
2218     my $offset = $$paging[1] || 0;
2219
2220     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2221     my $_search = OpenSRF::AppSession->create( 'open-ils.search' );
2222
2223     my $o_search = { shortname => uc($ou) };
2224     if (!$ou || $ou eq '-') {
2225         $o_search = { parent_ou => undef };
2226     }
2227
2228     my $one_org = $_storage->request(
2229         "open-ils.cstore.direct.actor.org_unit.search",
2230         $o_search
2231     )->gather(1);
2232
2233     my $count_req = $_search->request('open-ils.search.biblio.record.copy_count' => $one_org->id => $bib);
2234     my $staff_count_req = $_search->request('open-ils.search.biblio.record.copy_count.staff' => $one_org->id => $bib);
2235
2236     my $orgs = $_storage->request(
2237         'open-ils.cstore.json_query.atomic',
2238         { from => [ 'actor.org_unit_descendants', defined($depth) ? ( $one_org->id, $depth ) :  ( $one_org->id ) ] }
2239     )->gather(1);
2240
2241
2242     my @ou_ids = map { $_->{id} } @$orgs;
2243
2244     $logger->info("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2245
2246     my %subselect = ( '-or' => [
2247         { owning_lib => \@ou_ids },
2248         { '-exists'  =>
2249             { from  => 'acp',
2250               where => {
2251                 call_number => { '=' => {'+acn'=>'id'} },
2252                 deleted => 'f',
2253                 circ_lib => \@ou_ids
2254               },
2255               limit => 1
2256             }
2257         }
2258     ]);
2259
2260     # we are dealing with -full or -uris, so we need to flesh things out
2261     if ($flesh) {
2262
2263         # either way we're going to need uris
2264         # get all the uris up the tree (see also ba47ecc6196)
2265
2266         my $uri_orgs = $_storage->request(
2267             'open-ils.cstore.json_query.atomic',
2268             { from => [ 'actor.org_unit_ancestors', $one_org->id ] }
2269         )->gather(1);
2270
2271         my @uri_ou_ids = map { $_->{id} } @$uri_orgs;
2272
2273         # we have a -uris, just get the uris
2274         if ($flesh == 2) {
2275             %subselect = (
2276                 owning_lib => \@uri_ou_ids,
2277                 '-exists'  => {
2278                     limit => 1,
2279                     from  => { auricnm => 'auri' },
2280                     where => {
2281                         call_number => { '=' => {'+acn'=>'id'} },
2282                         '+auri' => { active => 't' }
2283                     }
2284                 }
2285             );
2286         # we have a -full, get all the things
2287         } elsif ($flesh == 1) {
2288             %subselect = ( '-or' => [
2289                 { owning_lib => \@ou_ids },
2290                 { '-exists'  =>
2291                     { from  => 'acp',
2292                       where => {
2293                         call_number => { '=' => {'+acn'=>'id'} },
2294                         deleted => 'f',
2295                         circ_lib => \@ou_ids
2296                       },
2297                       limit => 1
2298                     }
2299                 },
2300                 { '-and' => [
2301                     { owning_lib => \@uri_ou_ids },
2302                     { '-exists'  => {
2303                         from  => { auricnm => 'auri' },
2304                         where => {
2305                             call_number => { '=' => {'+acn'=>'id'} },
2306                             '+auri' => { active => 't' }
2307                         },
2308                         limit => 1
2309                     }}
2310                 ]}
2311             ]);
2312         }
2313     }
2314
2315     my $cns = $_storage->request(
2316         "open-ils.cstore.direct.asset.call_number.search.atomic",
2317         { record  => $bib,
2318           deleted => 'f',
2319           %subselect
2320         },
2321         { flesh     => 5,
2322           flesh_fields  => {
2323                     acn => [qw/copies owning_lib uri_maps prefix suffix/],
2324                     auricnm => [qw/uri/],
2325                     acp => [qw/circ_lib location status stat_cat_entries notes parts/],
2326                     asce    => [qw/stat_cat/],
2327                 },
2328           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2329           ( $offset     ? ( offset => $offset ) : () ),
2330           order_by  => { acn => { label_sortkey => {} } }
2331         }
2332     )->gather(1);
2333
2334     my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2335     $year += 1900;
2336     $month += 1;
2337
2338     $client->respond("<holdings xmlns='http://open-ils.org/spec/holdings/v1'><counts>\n");
2339
2340     my $copy_counts = $count_req->gather(1);
2341     my $staff_copy_counts = $staff_count_req->gather(1);
2342
2343     for my $c (@$copy_counts) {
2344         $$c{transcendant} ||= 0;
2345         my $out = "<count type='public'";
2346         $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2347         $client->respond("$out/>\n")
2348     }
2349
2350     for my $c (@$staff_copy_counts) {
2351         $$c{transcendant} ||= 0;
2352         my $out = "<count type='staff'";
2353         $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2354         $client->respond("$out/>\n")
2355     }
2356
2357     $client->respond("</counts><volumes>\n");
2358     
2359     for my $cn (@$cns) {
2360         next unless (@{$cn->copies} > 0 or (ref($cn->uri_maps) and @{$cn->uri_maps}));
2361
2362         # We don't want O:A:S:unAPI::acn to return the record, we've got that already
2363         # In the context of BibTemplate, copies aren't necessary because we pull those
2364         # in a separate call
2365         $client->respond(
2366             OpenILS::Application::SuperCat::unAPI::acn
2367                 ->new( $cn )
2368                 ->as_xml( {no_record => 1, no_copies => ($flesh ? 0 : 1)} )
2369         );
2370     }
2371
2372     $client->respond("</volumes><subscriptions>\n");
2373
2374     $logger->info("Searching for serial holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2375
2376     %subselect = ( '-or' => [
2377         { owning_lib => \@ou_ids },
2378         { '-exists'  =>
2379             { from  => 'sdist',
2380               where => { holding_lib => \@ou_ids },
2381               limit => 1
2382             }
2383         }
2384     ]);
2385
2386     my $ssubs = $_storage->request(
2387         "open-ils.cstore.direct.serial.subscription.search.atomic",
2388         { record_entry  => $bib,
2389           %subselect
2390         },
2391         { flesh     => 7,
2392           flesh_fields  => {
2393                     ssub    => [qw/distributions issuances scaps owning_lib/],
2394                     sdist   => [qw/basic_summary supplement_summary index_summary streams holding_lib/],
2395                     sstr    => [qw/items/],
2396                     sitem   => [qw/notes unit/],
2397                     sunit   => [qw/notes location status circ_lib stat_cat_entries call_number/],
2398                     acn => [qw/owning_lib prefix suffix/],
2399                 },
2400           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2401           ( $offset     ? ( offset => $offset ) : () ),
2402           order_by  => {
2403             ssub => {
2404                 start_date => {},
2405                 owning_lib => {},
2406                 id => {}
2407             },
2408             sdist => {
2409                 label => {},
2410                 owning_lib => {},
2411             },
2412             sunit => {
2413                 date_expected => {},
2414             }
2415           }
2416         }
2417     )->gather(1);
2418
2419
2420     for my $ssub (@$ssubs) {
2421         next unless (@{$ssub->distributions} or @{$ssub->issuances} or @{$ssub->scaps});
2422
2423         # We don't want O:A:S:unAPI::ssub to return the record, we've got that already
2424         # In the context of BibTemplate, copies aren't necessary because we pull those
2425         # in a separate call
2426         $client->respond(
2427             OpenILS::Application::SuperCat::unAPI::ssub
2428                 ->new( $ssub )
2429                 ->as_xml( {no_record => 1, no_items => ($flesh ? 0 : 1)} )
2430         );
2431     }
2432
2433
2434     return "</subscriptions></holdings>\n";
2435 }
2436 __PACKAGE__->register_method(
2437     method    => 'new_record_holdings',
2438     api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
2439     api_level => 1,
2440     argc      => 1,
2441     stream    => 1,
2442     signature =>
2443         { desc     => <<"          DESC",
2444 Returns the XML representation of the requested bibliographic record's holdings
2445           DESC
2446           params   =>
2447             [
2448                 { name => 'bibId',
2449                   desc => 'An OpenILS biblio::record_entry ID',
2450                   type => 'number' },
2451                 { name => 'orgUnit',
2452                   desc => 'An OpenILS actor::org_unit short name that limits the scope of returned holdings',
2453                   type => 'text' },
2454                 { name => 'depth',
2455                   desc => 'An OpenILS actor::org_unit_type depththat limits the scope of returned holdings',
2456                   type => 'number' },
2457                 { name => 'hideCopies',
2458                   desc => 'Flag that prevents the inclusion of copies in the returned holdings',
2459                   type => 'boolean' },
2460                 { name => 'paging',
2461                   desc => 'Arry of limit and offset for holdings paging',
2462                   type => 'array' },
2463             ],
2464           'return' =>
2465             { desc => 'Stream of bib record holdings hierarchy in XML',
2466               type => 'string' }
2467         }
2468 );
2469
2470 sub isbn_holdings {
2471     my $self = shift;
2472     my $client = shift;
2473     my $isbn = shift;
2474
2475     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2476
2477     my $recs = $_storage->request(
2478             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2479             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2480     )->gather(1);
2481
2482     return undef unless (@$recs);
2483
2484     return ($self->method_lookup( 'open-ils.supercat.record.holdings_xml.retrieve')->run( $recs->[0]->record ))[0];
2485 }
2486 __PACKAGE__->register_method(
2487     method    => 'isbn_holdings',
2488     api_name  => 'open-ils.supercat.isbn.holdings_xml.retrieve',
2489     api_level => 1,
2490     argc      => 1,
2491     signature =>
2492         { desc     => <<"          DESC",
2493 Returns the XML representation of the requested bibliographic record's holdings
2494           DESC
2495           params   =>
2496             [
2497                 { name => 'isbn',
2498                   desc => 'An isbn',
2499                   type => 'string' },
2500             ],
2501           'return' =>
2502             { desc => 'The bib record holdings hierarchy in XML',
2503               type => 'string' }
2504         }
2505 );
2506
2507 sub escape {
2508     my $self = shift;
2509     my $text = shift;
2510     return '' unless $text;
2511     $text =~ s/&/&amp;/gsom;
2512     $text =~ s/</&lt;/gsom;
2513     $text =~ s/>/&gt;/gsom;
2514     $text =~ s/"/&quot;/gsom;
2515     $text =~ s/'/&apos;/gsom;
2516     return $text;
2517 }
2518
2519 sub recent_changes {
2520     my $self = shift;
2521     my $client = shift;
2522     my $when = shift || '1-01-01';
2523     my $limit = shift;
2524
2525     my $type = 'biblio';
2526     my $hint = 'bre';
2527
2528     if ($self->api_name =~ /authority/o) {
2529         $type = 'authority';
2530         $hint = 'are';
2531     }
2532
2533     my $axis = 'create_date';
2534     $axis = 'edit_date' if ($self->api_name =~ /edit/o);
2535
2536     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2537
2538     return $_storage->request(
2539         "open-ils.cstore.direct.$type.record_entry.id_list.atomic",
2540         { $axis => { ">" => $when }, id => { '>' => 0 }, deleted => 'f', active => 't' },
2541         { order_by => { $hint => "$axis desc" }, limit => $limit }
2542     )->gather(1);
2543 }
2544
2545 for my $t ( qw/biblio authority/ ) {
2546     for my $a ( qw/import edit/ ) {
2547
2548         __PACKAGE__->register_method(
2549             method    => 'recent_changes',
2550             api_name  => "open-ils.supercat.$t.record.$a.recent",
2551             api_level => 1,
2552             argc      => 0,
2553             signature =>
2554                 { desc     => "Returns a list of recently ${a}ed $t records",
2555                   params   =>
2556                     [
2557                         { name => 'when',
2558                           desc => "Date to start looking for ${a}ed records",
2559                           default => '1-01-01',
2560                           type => 'string' },
2561
2562                         { name => 'limit',
2563                           desc => "Maximum count to retrieve",
2564                           type => 'number' },
2565                     ],
2566                   'return' =>
2567                     { desc => "An id list of $t records",
2568                       type => 'array' }
2569                 },
2570         );
2571     }
2572 }
2573
2574
2575 sub retrieve_authority_marcxml {
2576     my $self = shift;
2577     my $client = shift;
2578     my $rid = shift;
2579
2580     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2581
2582     my $record = $_storage->request( 'open-ils.cstore.direct.authority.record_entry.retrieve' => $rid )->gather(1);
2583     return $U->entityize( $record->marc ) if ($record);
2584     return undef;
2585 }
2586
2587 __PACKAGE__->register_method(
2588     method    => 'retrieve_authority_marcxml',
2589     api_name  => 'open-ils.supercat.authority.marcxml.retrieve',
2590     api_level => 1,
2591     argc      => 1,
2592     signature =>
2593         { desc     => <<"          DESC",
2594 Returns the MARCXML representation of the requested authority record
2595           DESC
2596           params   =>
2597             [
2598                 { name => 'authorityId',
2599                   desc => 'An OpenILS authority::record_entry id',
2600                   type => 'number' },
2601             ],
2602           'return' =>
2603             { desc => 'The authority record in MARCXML',
2604               type => 'string' }
2605         }
2606 );
2607
2608 sub retrieve_record_marcxml {
2609     my $self = shift;
2610     my $client = shift;
2611     my $rid = shift;
2612
2613     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2614
2615     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $rid )->gather(1);
2616     return $U->entityize( $record->marc ) if ($record);
2617     return undef;
2618 }
2619
2620 __PACKAGE__->register_method(
2621     method    => 'retrieve_record_marcxml',
2622     api_name  => 'open-ils.supercat.record.marcxml.retrieve',
2623     api_level => 1,
2624     argc      => 1,
2625     signature =>
2626         { desc     => <<"          DESC",
2627 Returns the MARCXML representation of the requested bibliographic record
2628           DESC
2629           params   =>
2630             [
2631                 { name => 'bibId',
2632                   desc => 'An OpenILS biblio::record_entry id',
2633                   type => 'number' },
2634             ],
2635           'return' =>
2636             { desc => 'The bib record in MARCXML',
2637               type => 'string' }
2638         }
2639 );
2640
2641 sub retrieve_isbn_marcxml {
2642     my $self = shift;
2643     my $client = shift;
2644     my $isbn = shift;
2645
2646     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2647
2648     my $recs = $_storage->request(
2649             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2650             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2651     )->gather(1);
2652
2653     return undef unless (@$recs);
2654
2655     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2656     return $U->entityize( $record->marc ) if ($record);
2657     return undef;
2658 }
2659
2660 __PACKAGE__->register_method(
2661     method    => 'retrieve_isbn_marcxml',
2662     api_name  => 'open-ils.supercat.isbn.marcxml.retrieve',
2663     api_level => 1,
2664     argc      => 1,
2665     signature =>
2666         { desc     => <<"          DESC",
2667 Returns the MARCXML representation of the requested ISBN
2668           DESC
2669           params   =>
2670             [
2671                 { name => 'ISBN',
2672                   desc => 'An ... um ... ISBN',
2673                   type => 'string' },
2674             ],
2675           'return' =>
2676             { desc => 'The bib record in MARCXML',
2677               type => 'string' }
2678         }
2679 );
2680
2681 sub retrieve_record_transform {
2682     my $self = shift;
2683     my $client = shift;
2684     my $rid = shift;
2685
2686     (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
2687
2688     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2689     #$_storage->connect;
2690
2691     my $record = $_storage->request(
2692         'open-ils.cstore.direct.biblio.record_entry.retrieve',
2693         $rid
2694     )->gather(1);
2695
2696     return undef unless ($record);
2697
2698     return $U->entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $record->marc ) )->toString);
2699 }
2700
2701 sub retrieve_isbn_transform {
2702     my $self = shift;
2703     my $client = shift;
2704     my $isbn = shift;
2705
2706     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2707
2708     my $recs = $_storage->request(
2709             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2710             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2711     )->gather(1);
2712
2713     return undef unless (@$recs);
2714
2715     (my $transform = $self->api_name) =~ s/^.+isbn\.([^\.]+)\.retrieve$/$1/o;
2716
2717     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2718
2719     return undef unless ($record);
2720
2721     return $U->entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $record->marc ) )->toString);
2722 }
2723
2724 sub retrieve_record_objects {
2725     my $self = shift;
2726     my $client = shift;
2727     my $ids = shift;
2728
2729     my $type = 'biblio';
2730
2731     if ($self->api_name =~ /authority/) {
2732         $type = 'authority';
2733     }
2734
2735     $ids = [$ids] unless (ref $ids);
2736     $ids = [grep {$_} @$ids];
2737
2738     return [] unless (@$ids);
2739
2740     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2741     return $_storage->request("open-ils.cstore.direct.$type.record_entry.search.atomic" => { id => [grep {$_} @$ids] })->gather(1);
2742 }
2743 __PACKAGE__->register_method(
2744     method    => 'retrieve_record_objects',
2745     api_name  => 'open-ils.supercat.record.object.retrieve',
2746     api_level => 1,
2747     argc      => 1,
2748     signature =>
2749         { desc     => <<"          DESC",
2750 Returns the Fieldmapper object representation of the requested bibliographic records
2751           DESC
2752           params   =>
2753             [
2754                 { name => 'bibIds',
2755                   desc => 'OpenILS biblio::record_entry ids',
2756                   type => 'array' },
2757             ],
2758           'return' =>
2759             { desc => 'The bib records',
2760               type => 'array' }
2761         }
2762 );
2763
2764 __PACKAGE__->register_method(
2765     method    => 'retrieve_record_objects',
2766     api_name  => 'open-ils.supercat.authority.object.retrieve',
2767     api_level => 1,
2768     argc      => 1,
2769     signature =>
2770         { desc     => <<"          DESC",
2771 Returns the Fieldmapper object representation of the requested authority records
2772           DESC
2773           params   =>
2774             [
2775                 { name => 'authIds',
2776                   desc => 'OpenILS authority::record_entry ids',
2777                   type => 'array' },
2778             ],
2779           'return' =>
2780             { desc => 'The authority records',
2781               type => 'array' }
2782         }
2783 );
2784
2785 sub retrieve_isbn_object {
2786     my $self = shift;
2787     my $client = shift;
2788     my $isbn = shift;
2789
2790     return undef unless ($isbn);
2791
2792     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2793     my $recs = $_storage->request(
2794             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2795             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2796     )->gather(1);
2797
2798     return undef unless (@$recs);
2799
2800     return $_storage->request(
2801         'open-ils.cstore.direct.biblio.record_entry.search.atomic',
2802         { id => $recs->[0]->record }
2803     )->gather(1);
2804 }
2805 __PACKAGE__->register_method(
2806     method    => 'retrieve_isbn_object',
2807     api_name  => 'open-ils.supercat.isbn.object.retrieve',
2808     api_level => 1,
2809     argc      => 1,
2810     signature =>
2811         { desc     => <<"          DESC",
2812 Returns the Fieldmapper object representation of the requested bibliographic record
2813           DESC
2814           params   =>
2815             [
2816                 { name => 'isbn',
2817                   desc => 'an ISBN',
2818                   type => 'string' },
2819             ],
2820           'return' =>
2821             { desc => 'The bib record',
2822               type => 'object' }
2823         }
2824 );
2825
2826
2827
2828 sub retrieve_metarecord_mods {
2829     my $self = shift;
2830     my $client = shift;
2831     my $rid = shift;
2832
2833     my $_storage = OpenSRF::AppSession->connect( 'open-ils.cstore' );
2834
2835     # Get the metarecord in question
2836     my $mr =
2837     $_storage->request(
2838         'open-ils.cstore.direct.metabib.metarecord.retrieve' => $rid
2839     )->gather(1);
2840
2841     # Now get the map of all bib records for the metarecord
2842     my $recs =
2843     $_storage->request(
2844         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
2845         {metarecord => $rid}
2846     )->gather(1);
2847
2848     $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
2849
2850     # and retrieve the lead (master) record as MODS
2851     my ($master) =
2852         $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2853             ->run($mr->master_record);
2854     my $master_mods = $_parser->parse_string($master)->documentElement;
2855     $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods" );
2856     $master_mods->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2857
2858     # ... and a MODS clone to populate, with guts removed.
2859     my $mods = $_parser->parse_string($master)->documentElement;
2860     $mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # modsCollection element
2861     $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2862     ($mods) = $mods->findnodes('//mods:mods');
2863     #$mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # mods element
2864     $mods->removeChildNodes;
2865     $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2866
2867     # Add the metarecord ID as a (locally defined) info URI
2868     my $recordInfo = $mods
2869         ->ownerDocument
2870         ->createElement("recordInfo");
2871
2872     my $recordIdentifier = $mods
2873         ->ownerDocument
2874         ->createElement("recordIdentifier");
2875
2876     my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2877     $year += 1900;
2878     $month += 1;
2879
2880     my $id = $mr->id;
2881     $recordIdentifier->appendTextNode(
2882         sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
2883     );
2884
2885     $recordInfo->appendChild($recordIdentifier);
2886     $mods->appendChild($recordInfo);
2887
2888     # Grab the title, author and ISBN for the master record and populate the metarecord
2889     my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
2890     
2891     if ($title) {
2892         $title->setNamespace( "http://www.loc.gov/mods/", "mods" );
2893         $title->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2894         $title = $mods->ownerDocument->importNode($title);
2895         $mods->appendChild($title);
2896     }
2897
2898     my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
2899     if ($author) {
2900         $author->setNamespace( "http://www.loc.gov/mods/", "mods" );
2901         $author->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2902         $author = $mods->ownerDocument->importNode($author);
2903         $mods->appendChild($author);
2904     }
2905
2906     my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
2907     if ($isbn) {
2908         $isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2909         $isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2910         $isbn = $mods->ownerDocument->importNode($isbn);
2911         $mods->appendChild($isbn);
2912     }
2913
2914     # ... and loop over the constituent records
2915     for my $map ( @$recs ) {
2916
2917         # get the MODS
2918         my ($rec) =
2919             $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2920                 ->run($map->source);
2921
2922         my $part_mods = $_parser->parse_string($rec);
2923         $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods" );
2924         $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2925         ($part_mods) = $part_mods->findnodes('//mods:mods');
2926
2927         for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
2928             $node->setNamespace( "http://www.loc.gov/mods/", "mods" );
2929             $node->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2930             $node = $mods->ownerDocument->importNode($node);
2931             $mods->appendChild( $node );
2932         }
2933
2934         my $relatedItem = $mods
2935             ->ownerDocument
2936             ->createElement("relatedItem");
2937
2938         $relatedItem->setAttribute( type => 'constituent' );
2939
2940         my $identifier = $mods
2941             ->ownerDocument
2942             ->createElement("identifier");
2943
2944         $identifier->setAttribute( type => 'uri' );
2945
2946         my $subRecordInfo = $mods
2947             ->ownerDocument
2948             ->createElement("recordInfo");
2949
2950         my $subRecordIdentifier = $mods
2951             ->ownerDocument
2952             ->createElement("recordIdentifier");
2953
2954         my $subid = $map->source;
2955         $subRecordIdentifier->appendTextNode(
2956             sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
2957                 $month,
2958                 $day
2959             )
2960         );
2961         $subRecordInfo->appendChild($subRecordIdentifier);
2962
2963         $relatedItem->appendChild( $subRecordInfo );
2964
2965         my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
2966         $tor->setNamespace( "http://www.loc.gov/mods/", "mods" );
2967         $tor->setNamespace( "http://www.loc.gov/mods/", undef, 1 ) if ($tor);
2968         $tor = $mods->ownerDocument->importNode($tor) if ($tor);
2969         $relatedItem->appendChild($tor) if ($tor);
2970
2971         if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
2972             $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2973             $part_isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2974             $part_isbn = $mods->ownerDocument->importNode($part_isbn);
2975             $relatedItem->appendChild( $part_isbn );
2976
2977             if (!$isbn) {
2978                 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
2979             }
2980         }
2981
2982         $mods->appendChild( $relatedItem );
2983
2984     }
2985
2986     $_storage->disconnect;
2987
2988     return $U->entityize($mods->toString);
2989
2990 }
2991 __PACKAGE__->register_method(
2992     method    => 'retrieve_metarecord_mods',
2993     api_name  => 'open-ils.supercat.metarecord.mods.retrieve',
2994     api_level => 1,
2995     argc      => 1,
2996     signature =>
2997         { desc     => <<"          DESC",
2998 Returns the MODS representation of the requested metarecord
2999           DESC
3000           params   =>
3001             [
3002                 { name => 'metarecordId',
3003                   desc => 'An OpenILS metabib::metarecord id',
3004                   type => 'number' },
3005             ],
3006           'return' =>
3007             { desc => 'The metarecord in MODS',
3008               type => 'string' }
3009         }
3010 );
3011
3012 sub list_metarecord_formats {
3013     my @list = (
3014         { mods =>
3015             { namespace_uri   => 'http://www.loc.gov/mods/',
3016               docs        => 'http://www.loc.gov/mods/',
3017               schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
3018             }
3019         }
3020     );
3021
3022     for my $type ( keys %metarecord_xslt ) {
3023         push @list,
3024             { $type => 
3025                 { namespace_uri   => $metarecord_xslt{$type}{namespace_uri},
3026                   docs        => $metarecord_xslt{$type}{docs},
3027                   schema_location => $metarecord_xslt{$type}{schema_location},
3028                 }
3029             };
3030     }
3031
3032     return \@list;
3033 }
3034 __PACKAGE__->register_method(
3035     method    => 'list_metarecord_formats',
3036     api_name  => 'open-ils.supercat.metarecord.formats',
3037     api_level => 1,
3038     argc      => 0,
3039     signature =>
3040         { desc     => <<"          DESC",
3041 Returns the list of valid metarecord formats that supercat understands.
3042           DESC
3043           'return' =>
3044             { desc => 'The format list',
3045               type => 'array' }
3046         }
3047 );
3048
3049
3050 sub list_authority_formats {
3051     my @list = (
3052         { marcxml =>
3053             { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
3054               docs        => 'http://www.loc.gov/marcxml/',
3055               schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
3056             },
3057           marc21 => { docs => 'http://www.loc.gov/marc/' }
3058         }
3059     );
3060
3061 #   for my $type ( keys %record_xslt ) {
3062 #       push @list,
3063 #           { $type => 
3064 #               { namespace_uri   => $record_xslt{$type}{namespace_uri},
3065 #                 docs        => $record_xslt{$type}{docs},
3066 #                 schema_location => $record_xslt{$type}{schema_location},
3067 #               }
3068 #           };
3069 #   }
3070 #
3071     return \@list;
3072 }
3073 __PACKAGE__->register_method(
3074     method    => 'list_authority_formats',
3075     api_name  => 'open-ils.supercat.authority.formats',
3076     api_level => 1,
3077     argc      => 0,
3078     signature =>
3079         { desc     => <<"          DESC",
3080 Returns the list of valid authority formats that supercat understands.
3081           DESC
3082           'return' =>
3083             { desc => 'The format list',
3084               type => 'array' }
3085         }
3086 );
3087
3088 sub list_record_formats {
3089     my @list = (
3090         { marcxml =>
3091             { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
3092               docs        => 'http://www.loc.gov/marcxml/',
3093               schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
3094             },
3095         },
3096         { marc21 => { docs => 'http://www.loc.gov/marc/' } }
3097     );
3098
3099     for my $type ( keys %record_xslt ) {
3100         push @list,
3101             { $type => 
3102                 { namespace_uri   => $record_xslt{$type}{namespace_uri},
3103                   docs        => $record_xslt{$type}{docs},
3104                   schema_location => $record_xslt{$type}{schema_location},
3105                 }
3106             };
3107     }
3108
3109     return \@list;
3110 }
3111 __PACKAGE__->register_method(
3112     method    => 'list_record_formats',
3113     api_name  => 'open-ils.supercat.record.formats',
3114     api_level => 1,
3115     argc      => 0,
3116     signature =>
3117         { desc     => <<"          DESC",
3118 Returns the list of valid record formats that supercat understands.
3119           DESC
3120           'return' =>
3121             { desc => 'The format list',
3122               type => 'array' }
3123         }
3124 );
3125 __PACKAGE__->register_method(
3126     method    => 'list_record_formats',
3127     api_name  => 'open-ils.supercat.isbn.formats',
3128     api_level => 1,
3129     argc      => 0,
3130     signature =>
3131         { desc     => <<"          DESC",
3132 Returns the list of valid record formats that supercat understands.
3133           DESC
3134           'return' =>
3135             { desc => 'The format list',
3136               type => 'array' }
3137         }
3138 );
3139
3140
3141 sub oISBN {
3142     my $self = shift;
3143     my $client = shift;
3144     my $isbn = shift;
3145
3146     $isbn =~ s/-//gso;
3147
3148     throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
3149         unless (length($isbn) >= 10);
3150
3151     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
3152
3153     # Create a storage session, since we'll be making muliple requests.
3154     $_storage->connect;
3155
3156     # Find the record that has that ISBN.
3157     my $bibrec = $_storage->request(
3158         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
3159         { tag => '020', subfield => 'a', value => { like => lc($isbn).'%'} }
3160     )->gather(1);
3161
3162     # Go away if we don't have one.
3163     return {} unless (@$bibrec);
3164
3165     # Find the metarecord for that bib record.
3166     my $mr = $_storage->request(
3167         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
3168         {source => $bibrec->[0]->record}
3169     )->gather(1);
3170
3171     # Find the other records for that metarecord.
3172     my $records = $_storage->request(
3173         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
3174         {metarecord => $mr->[0]->metarecord}
3175     )->gather(1);
3176
3177     # Just to be safe.  There's currently no unique constraint on sources...
3178     my %unique_recs = map { ($_->source, 1) } @$records;
3179     my @rec_list = sort keys %unique_recs;
3180
3181     # And now fetch the ISBNs for thos records.
3182     my $recs = [];
3183     push @$recs,
3184         $_storage->request(
3185             'open-ils.cstore.direct.metabib.full_rec.search',
3186             { tag => '020', subfield => 'a', record => $_ }
3187         )->gather(1) for (@rec_list);
3188
3189     # We're done with the storage server session.
3190     $_storage->disconnect;
3191
3192     # Return the oISBN data structure.  This will be XMLized at a higher layer.
3193     return
3194         { metarecord => $mr->[0]->metarecord,
3195           record_list => { map { $_ ? ($_->record, $_->value) : () } @$recs } };
3196
3197 }
3198 __PACKAGE__->register_method(
3199     method    => 'oISBN',
3200     api_name  => 'open-ils.supercat.oisbn',
3201     api_level => 1,
3202     argc      => 1,
3203     signature =>
3204         { desc     => <<"          DESC",
3205 Returns the ISBN list for the metarecord of the requested isbn
3206           DESC
3207           params   =>
3208             [
3209                 { name => 'isbn',
3210                   desc => 'An ISBN.  Duh.',
3211                   type => 'string' },
3212             ],
3213           'return' =>
3214             { desc => 'record to isbn map',
3215               type => 'object' }
3216         }
3217 );
3218
3219 sub return_bib_search_aliases {
3220     my %aliases;
3221
3222     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
3223
3224     my $cmsa = $_storage->request(
3225         'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3226         { alias => { '!=' => undef } }
3227     )->gather(1);
3228
3229     foreach (@$cmsa) {
3230         if ($_->alias =~ /\./) {
3231             my ($qualifier, $name) = $_->alias =~ m/^(.+?)\.(.+)$/;
3232             $aliases{$qualifier}{$name}{'index'} = $_->alias;
3233             # We will add a 'title' property in a subsequent schema
3234             $aliases{$qualifier}{$name}{'title'} = $name;
3235         } else {
3236             # au/kw/se/su/ti go into the default 'eg' qualifier
3237             $aliases{'eg'}{$_->alias}{'index'} = $_->alias;
3238             $aliases{'eg'}{$_->alias}{'title'} = $_->alias;
3239         }
3240     }
3241
3242     return \%aliases;
3243 }
3244
3245 __PACKAGE__->register_method(
3246     method    => 'return_bib_search_aliases',
3247     api_name  => 'open-ils.supercat.biblio.search_aliases',
3248     api_level => 1,
3249     argc      => 0,
3250     signature =>
3251         { desc     => <<"          DESC",
3252 Returns the set of qualified search aliases in the system
3253           DESC
3254           params   => [ ],
3255           'return' =>
3256             { desc => 'Hash of qualified search aliases',
3257               type => 'object' }
3258         }
3259 );
3260
3261
3262 package OpenILS::Application::SuperCat::unAPI;
3263 use base qw/OpenILS::Application::SuperCat/;
3264
3265 sub as_xml {
3266     die "dummy superclass, use a real class";
3267 }
3268
3269 sub new {
3270     my $class = shift;
3271     my $obj = shift;
3272     return unless ($obj);
3273
3274     $class = ref($class) || $class;
3275
3276     if ($class eq __PACKAGE__) {
3277         return unless (ref($obj));
3278         $class .= '::' . $obj->json_hint;
3279     }
3280
3281     return bless { obj => $obj } => $class;
3282 }
3283
3284 sub obj {
3285     my $self = shift;
3286     return $self->{obj};
3287 }
3288
3289 package OpenILS::Application::SuperCat::unAPI::auri;
3290 use base qw/OpenILS::Application::SuperCat::unAPI/;
3291
3292 sub as_xml {
3293     my $self = shift;
3294     my $args = shift;
3295
3296     my $xml = '      <uri xmlns="http://open-ils.org/spec/holdings/v1" ';
3297     $xml .= 'id="tag:open-ils.org:asset-uri/' . $self->obj->id . '" ';
3298     $xml .= 'use_restriction="' . $self->escape( $self->obj->use_restriction ) . '" ';
3299     $xml .= 'label="' . $self->escape( $self->obj->label ) . '" ';
3300     $xml .= 'href="' . $self->escape( $self->obj->href ) . '">';
3301
3302     if (!$args->{no_volumes}) {
3303         if (ref($self->obj->call_number_maps) && @{ $self->obj->call_number_maps }) {
3304             $xml .= "      <volumes>\n" . join(
3305                 '',
3306                 map {
3307                     OpenILS::Application::SuperCat::unAPI
3308                         ->new( $_->call_number )
3309                         ->as_xml({ %$args, no_uris=>1, no_copies=>1 })
3310                 } @{ $self->obj->call_number_maps }
3311             ) . "      </volumes>\n";
3312
3313         } else {
3314             $xml .= "      <volumes/>\n";
3315         }
3316     }
3317
3318     $xml .= "      </uri>\n";
3319
3320     return $xml;
3321 }
3322
3323 package OpenILS::Application::SuperCat::unAPI::acn;
3324 use base qw/OpenILS::Application::SuperCat::unAPI/;
3325
3326 sub as_xml {
3327     my $self = shift;
3328     my $args = shift;
3329
3330     my $xml = '    <volume xmlns="http://open-ils.org/spec/holdings/v1" ';
3331
3332     $xml .= 'id="tag:open-ils.org:asset-call_number/' . $self->obj->id . '" ';
3333     $xml .= 'lib="' . $self->escape( $self->obj->owning_lib->shortname ) . '" ';
3334     $xml .= 'opac_visible="' . $self->obj->owning_lib->opac_visible . '" ';
3335     $xml .= 'deleted="' . $self->obj->deleted . '" ';
3336     $xml .= 'label="' . $self->escape( $self->obj->label ) . '">';
3337     $xml .= "\n";
3338
3339     if (!$args->{no_copies}) {
3340         if (ref($self->obj->copies) && @{ $self->obj->copies }) {
3341             $xml .= "      <copies>\n" . join(
3342                 '',
3343                 map {
3344                     OpenILS::Application::SuperCat::unAPI
3345                         ->new( $_ )
3346                         ->as_xml({ %$args, no_volume=>1 })
3347                 } @{ $self->obj->copies }
3348             ) . "      </copies>\n";
3349
3350         } else {
3351             $xml .= "      <copies/>\n";
3352         }
3353     }
3354
3355     if (!$args->{no_uris}) {
3356         if (ref($self->obj->uri_maps) && @{ $self->obj->uri_maps }) {
3357             $xml .= "      <uris>\n" . join(
3358                 '',
3359                 map {
3360                     OpenILS::Application::SuperCat::unAPI
3361                         ->new( $_->uri )
3362                         ->as_xml({ %$args, no_volumes=>1 })
3363                 } @{ $self->obj->uri_maps }
3364             ) . "      </uris>\n";
3365
3366         } else {
3367             $xml .= "      <uris/>\n";
3368         }
3369     }
3370
3371
3372     $xml .= '      <prefix ';
3373     $xml .= 'ident="' . $self->obj->prefix->id . '" ';
3374     $xml .= 'id="tag:open-ils.org:asset-call_number_prefix/' . $self->obj->prefix->id . '" ';
3375     $xml .= 'label_sortkey="'.$self->escape( $self->obj->prefix->label_sortkey ) .'">';
3376     $xml .= $self->escape( $self->obj->prefix->label ) .'</prefix>';
3377     $xml .= "\n";
3378
3379     $xml .= '      <suffix ';
3380     $xml .= 'ident="' . $self->obj->suffix->id . '" ';
3381     $xml .= 'id="tag:open-ils.org:asset-call_number_suffix/' . $self->obj->suffix->id . '" ';
3382     $xml .= 'label_sortkey="'.$self->escape( $self->obj->suffix->label_sortkey ) .'">';
3383     $xml .= $self->escape( $self->obj->suffix->label ) .'</suffix>';
3384     $xml .= "\n";
3385
3386     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3387     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
3388     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
3389     $xml .= 'name="'.$self->escape( $self->obj->owning_lib->name ) .'"/>';
3390     $xml .= "\n";
3391
3392     unless ($args->{no_record}) {
3393         my $rec_tag = "tag:open-ils.org:biblio-record_entry/".$self->obj->record->id.'/'.$self->escape( $self->obj->owning_lib->shortname ) ;
3394
3395         my $r_doc = $parser->parse_string($self->obj->record->marc);
3396         $r_doc->documentElement->setAttribute( id => $rec_tag );
3397         $xml .= $U->entityize($r_doc->documentElement->toString);
3398     }
3399
3400     $xml .= "    </volume>\n";
3401
3402     return $xml;
3403 }
3404
3405 package OpenILS::Application::SuperCat::unAPI::ssub;
3406 use base qw/OpenILS::Application::SuperCat::unAPI/;
3407
3408 sub as_xml {
3409     my $self = shift;
3410     my $args = shift;
3411
3412     my $xml = '    <subscription xmlns="http://open-ils.org/spec/holdings/v1" ';
3413
3414     $xml .= 'id="tag:open-ils.org:serial-subscription/' . $self->obj->id . '" ';
3415     $xml .= 'start="' . $self->escape( $self->obj->start_date ) . '" ';
3416     $xml .= 'end="' . $self->escape( $self->obj->end_date ) . '" ';
3417     $xml .= 'expected_date_offset="' . $self->escape( $self->obj->expected_date_offset ) . '">';
3418     $xml .= "\n";
3419
3420     if (!$args->{no_distributions}) {
3421         if (ref($self->obj->distributions) && @{ $self->obj->distributions }) {
3422             $xml .= "      <distributions>\n" . join(
3423                 '',
3424                 map {