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