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