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