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