]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
LP20474587 Fix copy stat cat fleshing in 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                             asce => [qw/stat_cat/],
2030                     }
2031                 })
2032             ->gather(1))
2033         ->as_xml($args);
2034 }
2035
2036 __PACKAGE__->register_method(
2037     method    => 'retrieve_callnumber',
2038     api_name  => 'open-ils.supercat.acn.marcxml.retrieve',
2039     api_level => 1,
2040     argc      => 1,
2041     stream    => 1,
2042     signature =>
2043         { desc     => <<"          DESC",
2044 Returns a fleshed call number object
2045           DESC
2046           params   =>
2047             [
2048                 { name => 'cn_id',
2049                   desc => 'An OpenILS asset::call_number id',
2050                   type => 'number' },
2051             ],
2052           'return' =>
2053             { desc => 'call number with copies',
2054               type => 'object' }
2055         }
2056 );
2057 sub retrieve_callnumber {
2058     my $self = shift;
2059     my $client = shift;
2060     my $cnid = shift;
2061     my $args = shift || {};
2062
2063     return OpenILS::Application::SuperCat::unAPI
2064         ->new(OpenSRF::AppSession
2065             ->create( 'open-ils.cstore' )
2066             ->request(
2067                 "open-ils.cstore.direct.asset.call_number.retrieve",
2068                 $cnid,
2069                 { flesh     => 5,
2070                   flesh_fields  => {
2071                             acn => [qw/owning_lib record copies uri_maps prefix suffix/],
2072                             auricnm => [qw/uri/],
2073                             acp => [qw/location status circ_lib stat_cat_entries notes parts/],
2074                             asce => [qw/stat_cat/],
2075                     }
2076                 })
2077             ->gather(1))
2078         ->as_xml($args);
2079
2080 }
2081
2082 __PACKAGE__->register_method(
2083     method    => 'basic_record_holdings',
2084     api_name  => 'open-ils.supercat.record.basic_holdings.retrieve',
2085     api_level => 1,
2086     argc      => 1,
2087     stream    => 1,
2088     signature =>
2089         { desc     => <<"          DESC",
2090 Returns a basic hash representation of the requested bibliographic record's holdings
2091           DESC
2092           params   =>
2093             [
2094                 { name => 'bibId',
2095                   desc => 'An OpenILS biblio::record_entry id',
2096                   type => 'number' },
2097             ],
2098           'return' =>
2099             { desc => 'Hash of bib record holdings hierarchy (call numbers and copies)',
2100               type => 'string' }
2101         }
2102 );
2103 sub basic_record_holdings {
2104     my $self = shift;
2105     my $client = shift;
2106     my $bib = shift;
2107     my $ou = shift;
2108
2109     #  holdings hold an array of call numbers, which hold an array of copies
2110     #  holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ]
2111     my %holdings;
2112
2113     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2114
2115     my $tree = $_storage->request(
2116         "open-ils.cstore.direct.biblio.record_entry.retrieve",
2117         $bib,
2118         { flesh     => 5,
2119           flesh_fields  => {
2120                     bre => [qw/call_numbers/],
2121                     acn => [qw/copies owning_lib prefix suffix/],
2122                     acp => [qw/location status circ_lib parts/],
2123                 }
2124         }
2125     )->gather(1);
2126
2127     my $o_search = { shortname => uc($ou) };
2128     if (!$ou || $ou eq '-') {
2129         $o_search = { parent_ou => undef };
2130     }
2131
2132     my $orgs = $_storage->request(
2133         "open-ils.cstore.direct.actor.org_unit.search",
2134         $o_search,
2135         { flesh     => 100,
2136           flesh_fields  => { aou    => [qw/children/] }
2137         }
2138     )->gather(1);
2139
2140     my @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
2141
2142     $logger->debug("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2143
2144     for my $cn (@{$tree->call_numbers}) {
2145         next unless ( $cn->deleted eq 'f' || !$cn->deleted );
2146
2147         my $found = 0;
2148         for my $c (@{$cn->copies}) {
2149             next unless grep {$c->circ_lib->id == $_} @ou_ids;
2150             next unless _cp_is_visible($cn, $c);
2151             $found = 1;
2152             last;
2153         }
2154         next unless $found;
2155
2156         $holdings{$cn->label}{'owning_lib'} = $cn->owning_lib->shortname;
2157
2158         for my $cp (@{$cn->copies}) {
2159
2160             next unless grep { $cp->circ_lib->id == $_ } @ou_ids;
2161             next unless _cp_is_visible($cn, $cp);
2162
2163             push @{$holdings{$cn->label}{'copies'}}, {
2164                 barcode => $cp->barcode,
2165                 status => $cp->status->name,
2166                 location => $cp->location->name,
2167                 circlib => $cp->circ_lib->shortname
2168             };
2169
2170         }
2171     }
2172
2173     return \%holdings;
2174 }
2175
2176 sub _cp_is_visible {
2177     my $cn = shift;
2178     my $cp = shift;
2179
2180     my $visible = 0;
2181     if ( ($cp->deleted eq 'f' || !$cp->deleted) &&
2182          $cp->location->opac_visible eq 't' && 
2183          $cp->status->opac_visible eq 't' &&
2184          $cp->opac_visible eq 't' &&
2185          $cp->circ_lib->opac_visible eq 't' &&
2186          $cn->owning_lib->opac_visible eq 't'
2187     ) {
2188         $visible = 1;
2189     }
2190
2191     return $visible;
2192 }
2193
2194 #__PACKAGE__->register_method(
2195 #   method    => 'new_record_holdings',
2196 #   api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
2197 #   api_level => 1,
2198 #   argc      => 1,
2199 #   stream    => 1,
2200 #   signature =>
2201 #        { desc     => <<"          DESC",
2202 #Returns the XML representation of the requested bibliographic record's holdings
2203 #         DESC
2204 #         params   =>
2205 #           [
2206 #               { name => 'bibId',
2207 #                 desc => 'An OpenILS biblio::record_entry id',
2208 #                 type => 'number' },
2209 #           ],
2210 #         'return' =>
2211 #           { desc => 'Stream of bib record holdings hierarchy in XML',
2212 #             type => 'string' }
2213 #       }
2214 #);
2215 #
2216
2217 sub new_record_holdings {
2218     my $self = shift;
2219     my $client = shift;
2220     my $bib = shift;
2221     my $ou = shift;
2222     my $depth = shift;
2223     my $flesh = shift;
2224     my $paging = shift;
2225
2226     $paging = [-1,0] if (!$paging or !ref($paging) or @$paging == 0);
2227     my $limit = $$paging[0];
2228     my $offset = $$paging[1] || 0;
2229
2230     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2231     my $_search = OpenSRF::AppSession->create( 'open-ils.search' );
2232
2233     my $o_search = { shortname => uc($ou) };
2234     if (!$ou || $ou eq '-') {
2235         $o_search = { parent_ou => undef };
2236     }
2237
2238     my $one_org = $_storage->request(
2239         "open-ils.cstore.direct.actor.org_unit.search",
2240         $o_search
2241     )->gather(1);
2242
2243     my $count_req = $_search->request('open-ils.search.biblio.record.copy_count' => $one_org->id => $bib);
2244     my $staff_count_req = $_search->request('open-ils.search.biblio.record.copy_count.staff' => $one_org->id => $bib);
2245
2246     my $orgs = $_storage->request(
2247         'open-ils.cstore.json_query.atomic',
2248         { from => [ 'actor.org_unit_descendants', defined($depth) ? ( $one_org->id, $depth ) :  ( $one_org->id ) ] }
2249     )->gather(1);
2250
2251
2252     my @ou_ids = map { $_->{id} } @$orgs;
2253
2254     $logger->info("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2255
2256     my %subselect = ( '-or' => [
2257         { owning_lib => \@ou_ids },
2258         { '-exists'  =>
2259             { from  => 'acp',
2260               where => {
2261                 call_number => { '=' => {'+acn'=>'id'} },
2262                 deleted => 'f',
2263                 circ_lib => \@ou_ids
2264               },
2265               limit => 1
2266             }
2267         }
2268     ]);
2269
2270     # we are dealing with -full or -uris, so we need to flesh things out
2271     if ($flesh) {
2272
2273         # either way we're going to need uris
2274         # get all the uris up the tree (see also ba47ecc6196)
2275
2276         my $uri_orgs = $_storage->request(
2277             'open-ils.cstore.json_query.atomic',
2278             { from => [ 'actor.org_unit_ancestors', $one_org->id ] }
2279         )->gather(1);
2280
2281         my @uri_ou_ids = map { $_->{id} } @$uri_orgs;
2282
2283         # we have a -uris, just get the uris
2284         if ($flesh == 2) {
2285             %subselect = (
2286                 owning_lib => \@uri_ou_ids,
2287                 '-exists'  => {
2288                     limit => 1,
2289                     from  => { auricnm => 'auri' },
2290                     where => {
2291                         call_number => { '=' => {'+acn'=>'id'} },
2292                         '+auri' => { active => 't' }
2293                     }
2294                 }
2295             );
2296         # we have a -full, get all the things
2297         } elsif ($flesh == 1) {
2298             %subselect = ( '-or' => [
2299                 { owning_lib => \@ou_ids },
2300                 { '-exists'  =>
2301                     { from  => 'acp',
2302                       where => {
2303                         call_number => { '=' => {'+acn'=>'id'} },
2304                         deleted => 'f',
2305                         circ_lib => \@ou_ids
2306                       },
2307                       limit => 1
2308                     }
2309                 },
2310                 { '-and' => [
2311                     { owning_lib => \@uri_ou_ids },
2312                     { '-exists'  => {
2313                         from  => { auricnm => 'auri' },
2314                         where => {
2315                             call_number => { '=' => {'+acn'=>'id'} },
2316                             '+auri' => { active => 't' }
2317                         },
2318                         limit => 1
2319                     }}
2320                 ]}
2321             ]);
2322         }
2323     }
2324
2325     my $cns = $_storage->request(
2326         "open-ils.cstore.direct.asset.call_number.search.atomic",
2327         { record  => $bib,
2328           deleted => 'f',
2329           %subselect
2330         },
2331         { flesh     => 5,
2332           flesh_fields  => {
2333                     acn => [qw/copies owning_lib uri_maps prefix suffix/],
2334                     auricnm => [qw/uri/],
2335                     acp => [qw/circ_lib location status stat_cat_entries notes parts/],
2336                     asce    => [qw/stat_cat/],
2337                 },
2338           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2339           ( $offset     ? ( offset => $offset ) : () ),
2340           order_by  => { acn => { label_sortkey => {} } }
2341         }
2342     )->gather(1);
2343
2344     my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2345     $year += 1900;
2346     $month += 1;
2347
2348     $client->respond("<holdings xmlns='http://open-ils.org/spec/holdings/v1'><counts>\n");
2349
2350     my $copy_counts = $count_req->gather(1);
2351     my $staff_copy_counts = $staff_count_req->gather(1);
2352
2353     for my $c (@$copy_counts) {
2354         $$c{transcendant} ||= 0;
2355         my $out = "<count type='public'";
2356         $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2357         $client->respond("$out/>\n")
2358     }
2359
2360     for my $c (@$staff_copy_counts) {
2361         $$c{transcendant} ||= 0;
2362         my $out = "<count type='staff'";
2363         $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2364         $client->respond("$out/>\n")
2365     }
2366
2367     $client->respond("</counts><volumes>\n");
2368     
2369     for my $cn (@$cns) {
2370         next unless (@{$cn->copies} > 0 or (ref($cn->uri_maps) and @{$cn->uri_maps}));
2371
2372         # We don't want O:A:S:unAPI::acn to return the record, we've got that already
2373         # In the context of BibTemplate, copies aren't necessary because we pull those
2374         # in a separate call
2375         $client->respond(
2376             OpenILS::Application::SuperCat::unAPI::acn
2377                 ->new( $cn )
2378                 ->as_xml( {no_record => 1, no_copies => ($flesh ? 0 : 1)} )
2379         );
2380     }
2381
2382     $client->respond("</volumes><subscriptions>\n");
2383
2384     $logger->info("Searching for serial holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2385
2386     %subselect = ( '-or' => [
2387         { owning_lib => \@ou_ids },
2388         { '-exists'  =>
2389             { from  => 'sdist',
2390               where => { holding_lib => \@ou_ids },
2391               limit => 1
2392             }
2393         }
2394     ]);
2395
2396     my $ssubs = $_storage->request(
2397         "open-ils.cstore.direct.serial.subscription.search.atomic",
2398         { record_entry  => $bib,
2399           %subselect
2400         },
2401         { flesh     => 7,
2402           flesh_fields  => {
2403                     ssub    => [qw/distributions issuances scaps owning_lib/],
2404                     sdist   => [qw/basic_summary supplement_summary index_summary streams holding_lib/],
2405                     sstr    => [qw/items/],
2406                     sitem   => [qw/notes unit/],
2407                     sunit   => [qw/notes location status circ_lib stat_cat_entries call_number/],
2408                     asce    => [qw/stat_cat/],
2409                     acn => [qw/owning_lib prefix suffix/],
2410                 },
2411           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2412           ( $offset     ? ( offset => $offset ) : () ),
2413           order_by  => {
2414             ssub => {
2415                 start_date => {},
2416                 owning_lib => {},
2417                 id => {}
2418             },
2419             sdist => {
2420                 label => {},
2421                 owning_lib => {},
2422             },
2423             sunit => {
2424                 date_expected => {},
2425             }
2426           }
2427         }
2428     )->gather(1);
2429
2430
2431     for my $ssub (@$ssubs) {
2432         next unless (@{$ssub->distributions} or @{$ssub->issuances} or @{$ssub->scaps});
2433
2434         # We don't want O:A:S:unAPI::ssub to return the record, we've got that already
2435         # In the context of BibTemplate, copies aren't necessary because we pull those
2436         # in a separate call
2437         $client->respond(
2438             OpenILS::Application::SuperCat::unAPI::ssub
2439                 ->new( $ssub )
2440                 ->as_xml( {no_record => 1, no_items => ($flesh ? 0 : 1)} )
2441         );
2442     }
2443
2444
2445     return "</subscriptions></holdings>\n";
2446 }
2447 __PACKAGE__->register_method(
2448     method    => 'new_record_holdings',
2449     api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
2450     api_level => 1,
2451     argc      => 1,
2452     stream    => 1,
2453     signature =>
2454         { desc     => <<"          DESC",
2455 Returns the XML representation of the requested bibliographic record's holdings
2456           DESC
2457           params   =>
2458             [
2459                 { name => 'bibId',
2460                   desc => 'An OpenILS biblio::record_entry ID',
2461                   type => 'number' },
2462                 { name => 'orgUnit',
2463                   desc => 'An OpenILS actor::org_unit short name that limits the scope of returned holdings',
2464                   type => 'text' },
2465                 { name => 'depth',
2466                   desc => 'An OpenILS actor::org_unit_type depththat limits the scope of returned holdings',
2467                   type => 'number' },
2468                 { name => 'hideCopies',
2469                   desc => 'Flag that prevents the inclusion of copies in the returned holdings',
2470                   type => 'boolean' },
2471                 { name => 'paging',
2472                   desc => 'Arry of limit and offset for holdings paging',
2473                   type => 'array' },
2474             ],
2475           'return' =>
2476             { desc => 'Stream of bib record holdings hierarchy in XML',
2477               type => 'string' }
2478         }
2479 );
2480
2481 sub isbn_holdings {
2482     my $self = shift;
2483     my $client = shift;
2484     my $isbn = shift;
2485
2486     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2487
2488     my $recs = $_storage->request(
2489             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2490             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2491     )->gather(1);
2492
2493     return undef unless (@$recs);
2494
2495     return ($self->method_lookup( 'open-ils.supercat.record.holdings_xml.retrieve')->run( $recs->[0]->record ))[0];
2496 }
2497 __PACKAGE__->register_method(
2498     method    => 'isbn_holdings',
2499     api_name  => 'open-ils.supercat.isbn.holdings_xml.retrieve',
2500     api_level => 1,
2501     argc      => 1,
2502     signature =>
2503         { desc     => <<"          DESC",
2504 Returns the XML representation of the requested bibliographic record's holdings
2505           DESC
2506           params   =>
2507             [
2508                 { name => 'isbn',
2509                   desc => 'An isbn',
2510                   type => 'string' },
2511             ],
2512           'return' =>
2513             { desc => 'The bib record holdings hierarchy in XML',
2514               type => 'string' }
2515         }
2516 );
2517
2518 sub escape {
2519     my $self = shift;
2520     my $text = shift;
2521     return '' unless $text;
2522     $text =~ s/&/&amp;/gsom;
2523     $text =~ s/</&lt;/gsom;
2524     $text =~ s/>/&gt;/gsom;
2525     $text =~ s/"/&quot;/gsom;
2526     $text =~ s/'/&apos;/gsom;
2527     return $text;
2528 }
2529
2530 sub recent_changes {
2531     my $self = shift;
2532     my $client = shift;
2533     my $when = shift || '1-01-01';
2534     my $limit = shift;
2535
2536     my $type = 'biblio';
2537     my $hint = 'bre';
2538
2539     if ($self->api_name =~ /authority/o) {
2540         $type = 'authority';
2541         $hint = 'are';
2542     }
2543
2544     my $axis = 'create_date';
2545     $axis = 'edit_date' if ($self->api_name =~ /edit/o);
2546
2547     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2548
2549     return $_storage->request(
2550         "open-ils.cstore.direct.$type.record_entry.id_list.atomic",
2551         { $axis => { ">" => $when }, id => { '>' => 0 }, deleted => 'f', active => 't' },
2552         { order_by => { $hint => "$axis desc" }, limit => $limit }
2553     )->gather(1);
2554 }
2555
2556 for my $t ( qw/biblio authority/ ) {
2557     for my $a ( qw/import edit/ ) {
2558
2559         __PACKAGE__->register_method(
2560             method    => 'recent_changes',
2561             api_name  => "open-ils.supercat.$t.record.$a.recent",
2562             api_level => 1,
2563             argc      => 0,
2564             signature =>
2565                 { desc     => "Returns a list of recently ${a}ed $t records",
2566                   params   =>
2567                     [
2568                         { name => 'when',
2569                           desc => "Date to start looking for ${a}ed records",
2570                           default => '1-01-01',
2571                           type => 'string' },
2572
2573                         { name => 'limit',
2574                           desc => "Maximum count to retrieve",
2575                           type => 'number' },
2576                     ],
2577                   'return' =>
2578                     { desc => "An id list of $t records",
2579                       type => 'array' }
2580                 },
2581         );
2582     }
2583 }
2584
2585
2586 sub retrieve_authority_marcxml {
2587     my $self = shift;
2588     my $client = shift;
2589     my $rid = shift;
2590
2591     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2592
2593     my $record = $_storage->request( 'open-ils.cstore.direct.authority.record_entry.retrieve' => $rid )->gather(1);
2594     return $U->entityize( $record->marc ) if ($record);
2595     return undef;
2596 }
2597
2598 __PACKAGE__->register_method(
2599     method    => 'retrieve_authority_marcxml',
2600     api_name  => 'open-ils.supercat.authority.marcxml.retrieve',
2601     api_level => 1,
2602     argc      => 1,
2603     signature =>
2604         { desc     => <<"          DESC",
2605 Returns the MARCXML representation of the requested authority record
2606           DESC
2607           params   =>
2608             [
2609                 { name => 'authorityId',
2610                   desc => 'An OpenILS authority::record_entry id',
2611                   type => 'number' },
2612             ],
2613           'return' =>
2614             { desc => 'The authority record in MARCXML',
2615               type => 'string' }
2616         }
2617 );
2618
2619 sub retrieve_record_marcxml {
2620     my $self = shift;
2621     my $client = shift;
2622     my $rid = shift;
2623
2624     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2625
2626     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $rid )->gather(1);
2627     return $U->entityize( $record->marc ) if ($record);
2628     return undef;
2629 }
2630
2631 __PACKAGE__->register_method(
2632     method    => 'retrieve_record_marcxml',
2633     api_name  => 'open-ils.supercat.record.marcxml.retrieve',
2634     api_level => 1,
2635     argc      => 1,
2636     signature =>
2637         { desc     => <<"          DESC",
2638 Returns the MARCXML representation of the requested bibliographic record
2639           DESC
2640           params   =>
2641             [
2642                 { name => 'bibId',
2643                   desc => 'An OpenILS biblio::record_entry id',
2644                   type => 'number' },
2645             ],
2646           'return' =>
2647             { desc => 'The bib record in MARCXML',
2648               type => 'string' }
2649         }
2650 );
2651
2652 sub retrieve_isbn_marcxml {
2653     my $self = shift;
2654     my $client = shift;
2655     my $isbn = shift;
2656
2657     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2658
2659     my $recs = $_storage->request(
2660             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2661             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2662     )->gather(1);
2663
2664     return undef unless (@$recs);
2665
2666     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2667     return $U->entityize( $record->marc ) if ($record);
2668     return undef;
2669 }
2670
2671 __PACKAGE__->register_method(
2672     method    => 'retrieve_isbn_marcxml',
2673     api_name  => 'open-ils.supercat.isbn.marcxml.retrieve',
2674     api_level => 1,
2675     argc      => 1,
2676     signature =>
2677         { desc     => <<"          DESC",
2678 Returns the MARCXML representation of the requested ISBN
2679           DESC
2680           params   =>
2681             [
2682                 { name => 'ISBN',
2683                   desc => 'An ... um ... ISBN',
2684                   type => 'string' },
2685             ],
2686           'return' =>
2687             { desc => 'The bib record in MARCXML',
2688               type => 'string' }
2689         }
2690 );
2691
2692 sub retrieve_record_transform {
2693     my $self = shift;
2694     my $client = shift;
2695     my $rid = shift;
2696
2697     (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
2698     my $xslt = $record_xslt{$transform}{xslt};
2699
2700     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2701     #$_storage->connect;
2702
2703     my $record = $_storage->request(
2704         'open-ils.cstore.direct.biblio.record_entry.retrieve',
2705         $rid
2706     )->gather(1);
2707
2708     return undef unless ($record);
2709
2710     return $U->entityize($xslt->output_as_chars($xslt->transform($_parser->parse_string($record->marc))));
2711 }
2712
2713 sub retrieve_isbn_transform {
2714     my $self = shift;
2715     my $client = shift;
2716     my $isbn = shift;
2717
2718     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2719
2720     my $recs = $_storage->request(
2721             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2722             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2723     )->gather(1);
2724
2725     return undef unless (@$recs);
2726
2727     (my $transform = $self->api_name) =~ s/^.+isbn\.([^\.]+)\.retrieve$/$1/o;
2728     my $xslt = $record_xslt{$transform}{xslt};
2729
2730     my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2731
2732     return undef unless ($record);
2733
2734     return $U->entityize($xslt->output_as_chars($xslt->transform($_parser->parse_string($record->marc))));
2735 }
2736
2737 sub retrieve_record_objects {
2738     my $self = shift;
2739     my $client = shift;
2740     my $ids = shift;
2741
2742     my $type = 'biblio';
2743
2744     if ($self->api_name =~ /authority/) {
2745         $type = 'authority';
2746     }
2747
2748     $ids = [$ids] unless (ref $ids);
2749     $ids = [grep {$_} @$ids];
2750
2751     return [] unless (@$ids);
2752
2753     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2754     return $_storage->request("open-ils.cstore.direct.$type.record_entry.search.atomic" => { id => [grep {$_} @$ids] })->gather(1);
2755 }
2756 __PACKAGE__->register_method(
2757     method    => 'retrieve_record_objects',
2758     api_name  => 'open-ils.supercat.record.object.retrieve',
2759     api_level => 1,
2760     argc      => 1,
2761     signature =>
2762         { desc     => <<"          DESC",
2763 Returns the Fieldmapper object representation of the requested bibliographic records
2764           DESC
2765           params   =>
2766             [
2767                 { name => 'bibIds',
2768                   desc => 'OpenILS biblio::record_entry ids',
2769                   type => 'array' },
2770             ],
2771           'return' =>
2772             { desc => 'The bib records',
2773               type => 'array' }
2774         }
2775 );
2776
2777 __PACKAGE__->register_method(
2778     method    => 'retrieve_record_objects',
2779     api_name  => 'open-ils.supercat.authority.object.retrieve',
2780     api_level => 1,
2781     argc      => 1,
2782     signature =>
2783         { desc     => <<"          DESC",
2784 Returns the Fieldmapper object representation of the requested authority records
2785           DESC
2786           params   =>
2787             [
2788                 { name => 'authIds',
2789                   desc => 'OpenILS authority::record_entry ids',
2790                   type => 'array' },
2791             ],
2792           'return' =>
2793             { desc => 'The authority records',
2794               type => 'array' }
2795         }
2796 );
2797
2798 sub retrieve_isbn_object {
2799     my $self = shift;
2800     my $client = shift;
2801     my $isbn = shift;
2802
2803     return undef unless ($isbn);
2804
2805     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2806     my $recs = $_storage->request(
2807             'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2808             { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2809     )->gather(1);
2810
2811     return undef unless (@$recs);
2812
2813     return $_storage->request(
2814         'open-ils.cstore.direct.biblio.record_entry.search.atomic',
2815         { id => $recs->[0]->record }
2816     )->gather(1);
2817 }
2818 __PACKAGE__->register_method(
2819     method    => 'retrieve_isbn_object',
2820     api_name  => 'open-ils.supercat.isbn.object.retrieve',
2821     api_level => 1,
2822     argc      => 1,
2823     signature =>
2824         { desc     => <<"          DESC",
2825 Returns the Fieldmapper object representation of the requested bibliographic record
2826           DESC
2827           params   =>
2828             [
2829                 { name => 'isbn',
2830                   desc => 'an ISBN',
2831                   type => 'string' },
2832             ],
2833           'return' =>
2834             { desc => 'The bib record',
2835               type => 'object' }
2836         }
2837 );
2838
2839
2840
2841 sub retrieve_metarecord_mods {
2842     my $self = shift;
2843     my $client = shift;
2844     my $rid = shift;
2845
2846     my $_storage = OpenSRF::AppSession->connect( 'open-ils.cstore' );
2847
2848     # Get the metarecord in question
2849     my $mr =
2850     $_storage->request(
2851         'open-ils.cstore.direct.metabib.metarecord.retrieve' => $rid
2852     )->gather(1);
2853
2854     # Now get the map of all bib records for the metarecord
2855     my $recs =
2856     $_storage->request(
2857         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
2858         {metarecord => $rid}
2859     )->gather(1);
2860
2861     $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
2862
2863     # and retrieve the lead (master) record as MODS
2864     my ($master) =
2865         $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2866             ->run($mr->master_record);
2867     my $master_mods = $_parser->parse_string($master)->documentElement;
2868     $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods" );
2869     $master_mods->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2870
2871     # ... and a MODS clone to populate, with guts removed.
2872     my $mods = $_parser->parse_string($master)->documentElement;
2873     $mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # modsCollection element
2874     $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2875     ($mods) = $mods->findnodes('//mods:mods');
2876     #$mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # mods element
2877     $mods->removeChildNodes;
2878     $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2879
2880     # Add the metarecord ID as a (locally defined) info URI
2881     my $recordInfo = $mods
2882         ->ownerDocument
2883         ->createElement("recordInfo");
2884
2885     my $recordIdentifier = $mods
2886         ->ownerDocument
2887         ->createElement("recordIdentifier");
2888
2889     my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2890     $year += 1900;
2891     $month += 1;
2892
2893     my $id = $mr->id;
2894     $recordIdentifier->appendTextNode(
2895         sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
2896     );
2897
2898     $recordInfo->appendChild($recordIdentifier);
2899     $mods->appendChild($recordInfo);
2900
2901     # Grab the title, author and ISBN for the master record and populate the metarecord
2902     my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
2903     
2904     if ($title) {
2905         $title->setNamespace( "http://www.loc.gov/mods/", "mods" );
2906         $title->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2907         $title = $mods->ownerDocument->importNode($title);
2908         $mods->appendChild($title);
2909     }
2910
2911     my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
2912     if ($author) {
2913         $author->setNamespace( "http://www.loc.gov/mods/", "mods" );
2914         $author->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2915         $author = $mods->ownerDocument->importNode($author);
2916         $mods->appendChild($author);
2917     }
2918
2919     my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
2920     if ($isbn) {
2921         $isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2922         $isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2923         $isbn = $mods->ownerDocument->importNode($isbn);
2924         $mods->appendChild($isbn);
2925     }
2926
2927     # ... and loop over the constituent records
2928     for my $map ( @$recs ) {
2929
2930         # get the MODS
2931         my ($rec) =
2932             $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2933                 ->run($map->source);
2934
2935         my $part_mods = $_parser->parse_string($rec);
2936         $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods" );
2937         $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2938         ($part_mods) = $part_mods->findnodes('//mods:mods');
2939
2940         for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
2941             $node->setNamespace( "http://www.loc.gov/mods/", "mods" );
2942             $node->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2943             $node = $mods->ownerDocument->importNode($node);
2944             $mods->appendChild( $node );
2945         }
2946
2947         my $relatedItem = $mods
2948             ->ownerDocument
2949             ->createElement("relatedItem");
2950
2951         $relatedItem->setAttribute( type => 'constituent' );
2952
2953         my $identifier = $mods
2954             ->ownerDocument
2955             ->createElement("identifier");
2956
2957         $identifier->setAttribute( type => 'uri' );
2958
2959         my $subRecordInfo = $mods
2960             ->ownerDocument
2961             ->createElement("recordInfo");
2962
2963         my $subRecordIdentifier = $mods
2964             ->ownerDocument
2965             ->createElement("recordIdentifier");
2966
2967         my $subid = $map->source;
2968         $subRecordIdentifier->appendTextNode(
2969             sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
2970                 $month,
2971                 $day
2972             )
2973         );
2974         $subRecordInfo->appendChild($subRecordIdentifier);
2975
2976         $relatedItem->appendChild( $subRecordInfo );
2977
2978         my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
2979         $tor->setNamespace( "http://www.loc.gov/mods/", "mods" );
2980         $tor->setNamespace( "http://www.loc.gov/mods/", undef, 1 ) if ($tor);
2981         $tor = $mods->ownerDocument->importNode($tor) if ($tor);
2982         $relatedItem->appendChild($tor) if ($tor);
2983
2984         if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
2985             $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2986             $part_isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2987             $part_isbn = $mods->ownerDocument->importNode($part_isbn);
2988             $relatedItem->appendChild( $part_isbn );
2989
2990             if (!$isbn) {
2991                 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
2992             }
2993         }
2994
2995         $mods->appendChild( $relatedItem );
2996
2997     }
2998
2999     $_storage->disconnect;
3000
3001     return $U->entityize($mods->toString);
3002
3003 }
3004 __PACKAGE__->register_method(
3005     method    => 'retrieve_metarecord_mods',
3006     api_name  => 'open-ils.supercat.metarecord.mods.retrieve',
3007     api_level => 1,
3008     argc      => 1,
3009     signature =>
3010         { desc     => <<"          DESC",
3011 Returns the MODS representation of the requested metarecord
3012           DESC
3013           params   =>
3014             [
3015                 { name => 'metarecordId',
3016                   desc => 'An OpenILS metabib::metarecord id',
3017                   type => 'number' },
3018             ],
3019           'return' =>
3020             { desc => 'The metarecord in MODS',
3021               type => 'string' }
3022         }
3023 );
3024
3025 sub list_metarecord_formats {
3026     my @list = (
3027         { mods =>
3028             { namespace_uri   => 'http://www.loc.gov/mods/',
3029               docs        => 'http://www.loc.gov/mods/',
3030               schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
3031             }
3032         }
3033     );
3034
3035     for my $type ( keys %metarecord_xslt ) {
3036         push @list,
3037             { $type => 
3038                 { namespace_uri   => $metarecord_xslt{$type}{namespace_uri},
3039                   docs        => $metarecord_xslt{$type}{docs},
3040                   schema_location => $metarecord_xslt{$type}{schema_location},
3041                 }
3042             };
3043     }
3044
3045     return \@list;
3046 }
3047 __PACKAGE__->register_method(
3048     method    => 'list_metarecord_formats',
3049     api_name  => 'open-ils.supercat.metarecord.formats',
3050     api_level => 1,
3051     argc      => 0,
3052     signature =>
3053         { desc     => <<"          DESC",
3054 Returns the list of valid metarecord formats that supercat understands.
3055           DESC
3056           'return' =>
3057             { desc => 'The format list',
3058               type => 'array' }
3059         }
3060 );
3061
3062
3063 sub list_authority_formats {
3064     my @list = (
3065         { marcxml =>
3066             { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
3067               docs        => 'http://www.loc.gov/marcxml/',
3068               schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
3069             },
3070           marc21 => { docs => 'http://www.loc.gov/marc/' }
3071         }
3072     );
3073
3074 #   for my $type ( keys %record_xslt ) {
3075 #       push @list,
3076 #           { $type => 
3077 #               { namespace_uri   => $record_xslt{$type}{namespace_uri},
3078 #                 docs        => $record_xslt{$type}{docs},
3079 #                 schema_location => $record_xslt{$type}{schema_location},
3080 #               }
3081 #           };
3082 #   }
3083 #
3084     return \@list;
3085 }
3086 __PACKAGE__->register_method(
3087     method    => 'list_authority_formats',
3088     api_name  => 'open-ils.supercat.authority.formats',
3089     api_level => 1,
3090     argc      => 0,
3091     signature =>
3092         { desc     => <<"          DESC",
3093 Returns the list of valid authority formats that supercat understands.
3094           DESC
3095           'return' =>
3096             { desc => 'The format list',
3097               type => 'array' }
3098         }
3099 );
3100
3101 sub list_record_formats {
3102     my @list = (
3103         { marcxml =>
3104             { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
3105               docs        => 'http://www.loc.gov/marcxml/',
3106               schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
3107             },
3108         },
3109         { marc21 => { docs => 'http://www.loc.gov/marc/' } }
3110     );
3111
3112     for my $type ( keys %record_xslt ) {
3113         push @list,
3114             { $type => 
3115                 { namespace_uri   => $record_xslt{$type}{namespace_uri},
3116                   docs        => $record_xslt{$type}{docs},
3117                   schema_location => $record_xslt{$type}{schema_location},
3118                 }
3119             };
3120     }
3121
3122     return \@list;
3123 }
3124 __PACKAGE__->register_method(
3125     method    => 'list_record_formats',
3126     api_name  => 'open-ils.supercat.record.formats',
3127     api_level => 1,
3128     argc      => 0,
3129     signature =>
3130         { desc     => <<"          DESC",
3131 Returns the list of valid record formats that supercat understands.
3132           DESC
3133           'return' =>
3134             { desc => 'The format list',
3135               type => 'array' }
3136         }
3137 );
3138 __PACKAGE__->register_method(
3139     method    => 'list_record_formats',
3140     api_name  => 'open-ils.supercat.isbn.formats',
3141     api_level => 1,
3142     argc      => 0,
3143     signature =>
3144         { desc     => <<"          DESC",
3145 Returns the list of valid record formats that supercat understands.
3146           DESC
3147           'return' =>
3148             { desc => 'The format list',
3149               type => 'array' }
3150         }
3151 );
3152
3153
3154 sub oISBN {
3155     my $self = shift;
3156     my $client = shift;
3157     my $isbn = shift;
3158
3159     $isbn =~ s/-//gso;
3160
3161     throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
3162         unless (length($isbn) >= 10);
3163
3164     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
3165
3166     # Create a storage session, since we'll be making muliple requests.
3167     $_storage->connect;
3168
3169     # Find the record that has that ISBN.
3170     my $bibrec = $_storage->request(
3171         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
3172         { tag => '020', subfield => 'a', value => { like => lc($isbn).'%'} }
3173     )->gather(1);
3174
3175     # Go away if we don't have one.
3176     return {} unless (@$bibrec);
3177
3178     # Find the metarecord for that bib record.
3179     my $mr = $_storage->request(
3180         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
3181         {source => $bibrec->[0]->record}
3182     )->gather(1);
3183
3184     # Find the other records for that metarecord.
3185     my $records = $_storage->request(
3186         'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
3187         {metarecord => $mr->[0]->metarecord}
3188     )->gather(1);
3189
3190     # Just to be safe.  There's currently no unique constraint on sources...
3191     my %unique_recs = map { ($_->source, 1) } @$records;
3192     my @rec_list = sort keys %unique_recs;
3193
3194     # And now fetch the ISBNs for thos records.
3195     my $recs = [];
3196     push @$recs,
3197         $_storage->request(
3198             'open-ils.cstore.direct.metabib.full_rec.search',
3199             { tag => '020', subfield => 'a', record => $_ }
3200         )->gather(1) for (@rec_list);
3201
3202     # We're done with the storage server session.
3203     $_storage->disconnect;
3204
3205     # Return the oISBN data structure.  This will be XMLized at a higher layer.
3206     return
3207         { metarecord => $mr->[0]->metarecord,
3208           record_list => { map { $_ ? ($_->record, $_->value) : () } @$recs } };
3209
3210 }
3211 __PACKAGE__->register_method(
3212     method    => 'oISBN',
3213     api_name  => 'open-ils.supercat.oisbn',
3214     api_level => 1,
3215     argc      => 1,
3216     signature =>
3217         { desc     => <<"          DESC",
3218 Returns the ISBN list for the metarecord of the requested isbn
3219           DESC
3220           params   =>
3221             [
3222                 { name => 'isbn',
3223                   desc => 'An ISBN.  Duh.',
3224                   type => 'string' },
3225             ],
3226           'return' =>
3227             { desc => 'record to isbn map',
3228               type => 'object' }
3229         }
3230 );
3231
3232 sub return_bib_search_aliases {
3233     my %aliases;
3234
3235     my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
3236
3237     my $cmsa = $_storage->request(
3238         'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
3239         { alias => { '!=' => undef } }
3240     )->gather(1);
3241
3242     foreach (@$cmsa) {
3243         if ($_->alias =~ /\./) {
3244             my ($qualifier, $name) = $_->alias =~ m/^(.+?)\.(.+)$/;
3245             $aliases{$qualifier}{$name}{'index'} = $_->alias;
3246             # We will add a 'title' property in a subsequent schema
3247             $aliases{$qualifier}{$name}{'title'} = $name;
3248         } else {
3249             # au/kw/se/su/ti go into the default 'eg' qualifier
3250             $aliases{'eg'}{$_->alias}{'index'} = $_->alias;
3251             $aliases{'eg'}{$_->alias}{'title'} = $_->alias;
3252         }
3253     }
3254
3255     return \%aliases;
3256 }
3257
3258 __PACKAGE__->register_method(
3259     method    => 'return_bib_search_aliases',
3260     api_name  => 'open-ils.supercat.biblio.search_aliases',
3261     api_level => 1,
3262     argc      => 0,
3263     signature =>
3264         { desc     => <<"          DESC",
3265 Returns the set of qualified search aliases in the system
3266           DESC
3267           params   => [ ],
3268           'return' =>
3269             { desc => 'Hash of qualified search aliases',
3270               type => 'object' }
3271         }
3272 );
3273
3274
3275 package OpenILS::Application::SuperCat::unAPI;
3276 use base qw/OpenILS::Application::SuperCat/;
3277
3278 sub as_xml {
3279     die "dummy superclass, use a real class";
3280 }
3281
3282 sub new {
3283     my $class = shift;
3284     my $obj = shift;
3285     return unless ($obj);
3286
3287     $class = ref($class) || $class;
3288
3289     if ($class eq __PACKAGE__) {
3290         return unless (ref($obj));
3291         $class .= '::' . $obj->json_hint;
3292     }
3293
3294     return bless { obj => $obj } => $class;
3295 }
3296
3297 sub obj {
3298     my $self = shift;
3299     return $self->{obj};
3300 }
3301
3302 package OpenILS::Application::SuperCat::unAPI::auri;
3303 use base qw/OpenILS::Application::SuperCat::unAPI/;
3304
3305 sub as_xml {
3306     my $self = shift;
3307     my $args = shift;
3308
3309     my $xml = '      <uri xmlns="http://open-ils.org/spec/holdings/v1" ';
3310     $xml .= 'id="tag:open-ils.org:asset-uri/' . $self->obj->id . '" ';
3311     $xml .= 'use_restriction="' . $self->escape( $self->obj->use_restriction ) . '" ';
3312     $xml .= 'label="' . $self->escape( $self->obj->label ) . '" ';
3313     $xml .= 'href="' . $self->escape( $self->obj->href ) . '">';
3314
3315     if (!$args->{no_volumes}) {
3316         if (ref($self->obj->call_number_maps) && @{ $self->obj->call_number_maps }) {
3317             $xml .= "      <volumes>\n" . join(
3318                 '',
3319                 map {
3320                     OpenILS::Application::SuperCat::unAPI
3321                         ->new( $_->call_number )
3322                         ->as_xml({ %$args, no_uris=>1, no_copies=>1 })
3323                 } @{ $self->obj->call_number_maps }
3324             ) . "      </volumes>\n";
3325
3326         } else {
3327             $xml .= "      <volumes/>\n";
3328         }
3329     }
3330
3331     $xml .= "      </uri>\n";
3332
3333     return $xml;
3334 }
3335
3336 package OpenILS::Application::SuperCat::unAPI::acn;
3337 use base qw/OpenILS::Application::SuperCat::unAPI/;
3338
3339 sub as_xml {
3340     my $self = shift;
3341     my $args = shift;
3342
3343     my $xml = '    <volume xmlns="http://open-ils.org/spec/holdings/v1" ';
3344
3345     $xml .= 'id="tag:open-ils.org:asset-call_number/' . $self->obj->id . '" ';
3346     $xml .= 'lib="' . $self->escape( $self->obj->owning_lib->shortname ) . '" ';
3347     $xml .= 'opac_visible="' . $self->obj->owning_lib->opac_visible . '" ';
3348     $xml .= 'deleted="' . $self->obj->deleted . '" ';
3349     $xml .= 'label="' . $self->escape( $self->obj->label ) . '">';
3350     $xml .= "\n";
3351
3352     if (!$args->{no_copies}) {
3353         if (ref($self->obj->copies) && @{ $self->obj->copies }) {
3354             $xml .= "      <copies>\n" . join(
3355                 '',
3356                 map {
3357                     OpenILS::Application::SuperCat::unAPI
3358                         ->new( $_ )
3359                         ->as_xml({ %$args, no_volume=>1 })
3360                 } @{ $self->obj->copies }
3361             ) . "      </copies>\n";
3362
3363         } else {
3364             $xml .= "      <copies/>\n";
3365         }
3366     }
3367
3368     if (!$args->{no_uris}) {
3369         if (ref($self->obj->uri_maps) && @{ $self->obj->uri_maps }) {
3370             $xml .= "      <uris>\n" . join(
3371                 '',
3372                 map {
3373                     OpenILS::Application::SuperCat::unAPI
3374                         ->new( $_->uri )
3375                         ->as_xml({ %$args, no_volumes=>1 })
3376                 } @{ $self->obj->uri_maps }
3377             ) . "      </uris>\n";
3378
3379         } else {
3380             $xml .= "      <uris/>\n";
3381         }
3382     }
3383
3384
3385     $xml .= '      <prefix ';
3386     $xml .= 'ident="' . $self->obj->prefix->id . '" ';
3387     $xml .= 'id="tag:open-ils.org:asset-call_number_prefix/' . $self->obj->prefix->id . '" ';
3388     $xml .= 'label_sortkey="'.$self->escape( $self->obj->prefix->label_sortkey ) .'">';
3389     $xml .= $self->escape( $self->obj->prefix->label ) .'</prefix>';
3390     $xml .= "\n";
3391
3392     $xml .= '      <suffix ';
3393     $xml .= 'ident="' . $self->obj->suffix->id . '" ';
3394     $xml .= 'id="tag:open-ils.org:asset-call_number_suffix/' . $self->obj->suffix->id . '" ';
3395     $xml .= 'label_sortkey="'.$self->escape( $self->obj->suffix->label_sortkey ) .'">';
3396     $xml .= $self->escape( $self->obj->suffix->label ) .'</suffix>';
3397     $xml .= "\n";
3398
3399     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3400     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
3401     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
3402     $xml .= 'name="'.$self->escape( $self->obj->owning_lib->name ) .'"/>';
3403     $xml .= "\n";
3404
3405     unless ($args->{no_record}) {
3406         my $rec_tag = "tag:open-ils.org:biblio-record_entry/".$self->obj->record->id.'/'.$self->escape( $self->obj->owning_lib->shortname ) ;
3407
3408         my $r_doc = $parser->parse_string($self->obj->record->marc);
3409         $r_doc->documentElement->setAttribute( id => $rec_tag );
3410         $xml .= $U->entityize($r_doc->documentElement->toString);
3411     }
3412
3413     $xml .= "    </volume>\n";
3414
3415     return $xml;
3416 }
3417
3418 package OpenILS::Application::SuperCat::unAPI::ssub;
3419 use base qw/OpenILS::Application::SuperCat::unAPI/;
3420
3421 sub as_xml {
3422     my $self = shift;
3423     my $args = shift;
3424
3425     my $xml = '    <subscription xmlns="http://open-ils.org/spec/holdings/v1" ';
3426
3427     $xml .= 'id="tag:open-ils.org:serial-subscription/' . $self->obj->id . '" ';
3428     $xml .= 'start="' . $self->escape( $self->obj->start_date ) . '" ';
3429     $xml .= 'end="' . $self->escape( $self->obj->end_date ) . '" ';
3430     $xml .= 'expected_date_offset="' . $self->escape( $self->obj->expected_date_offset ) . '">';
3431     $xml .= "\n";
3432
3433     if (!$args->{no_distributions}) {
3434         if (ref($self->obj->distributions) && @{ $self->obj->distributions }) {
3435             $xml .= "      <distributions>\n" . join(
3436                 '',
3437                 map {
3438                     OpenILS::Application::SuperCat::unAPI
3439                         ->new( $_ )
3440                         ->as_xml({ %$args, no_subscription=>1, no_issuance=>1 })
3441                 } @{ $self->obj->distributions }
3442             ) . "      </distributions>\n";
3443
3444         } else {
3445             $xml .= "      <distributions/>\n";
3446         }
3447     }
3448
3449     if (!$args->{no_captions_and_patterns}) {
3450         if (ref($self->obj->scaps) && @{ $self->obj->scaps }) {
3451             $xml .= "      <captions_and_patterns>\n" . join(
3452                 '',
3453                 map {
3454                     OpenILS::Application::SuperCat::unAPI
3455                         ->new( $_ )
3456                         ->as_xml({ %$args, no_subscription=>1 })
3457                 } @{ $self->obj->scaps }
3458             ) . "      </captions_and_patterns>\n";
3459
3460         } else {
3461             $xml .= "      <captions_and_patterns/>\n";
3462         }
3463     }
3464
3465     if (!$args->{no_issuances}) {
3466         if (ref($self->obj->issuances) && @{ $self->obj->issuances }) {
3467             $xml .= "      <issuances>\n" . join(
3468                 '',
3469                 map {
3470                     OpenILS::Application::SuperCat::unAPI
3471                         ->new( $_ )
3472                         ->as_xml({ %$args, no_subscription=>1, no_items=>1 })
3473                 } @{ $self->obj->issuances }
3474             ) . "      </issuances>\n";
3475
3476         } else {
3477             $xml .= "      <issuances/>\n";
3478         }
3479     }
3480
3481
3482     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3483     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
3484     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
3485     $xml .= 'name="'.$self->escape( $self->obj->owning_lib->name ) .'"/>';
3486     $xml .= "\n";
3487
3488     unless ($args->{no_record}) {
3489         my $rec_tag = "tag:open-ils.org:biblio-record_entry/".$self->obj->record->id.'/'.$self->escape( $self->obj->owning_lib->shortname ) ;
3490
3491         my $r_doc = $parser->parse_string($self->obj->record_entry->marc);
3492         $r_doc->documentElement->setAttribute( id => $rec_tag );
3493         $xml .= $U->entityize($r_doc->documentElement->toString);
3494     }
3495
3496     $xml .= "    </subscription>\n";
3497
3498     return $xml;
3499 }
3500
3501 package OpenILS::Application::SuperCat::unAPI::ssum_base;
3502 use base qw/OpenILS::Application::SuperCat::unAPI/;
3503
3504 sub as_xml {
3505     my $self = shift;
3506     my $args = shift;
3507
3508     (my $type = ref($self)) =~ s/^.+([^:]+)$/$1/;
3509
3510     my $xml = "    <serial_summary xmlns=\"http://open-ils.org/spec/holdings/v1\" type=\"$type\" ";
3511
3512     $xml .= "id=\"tag:open-ils.org:serial-summary-$type/" . $self->obj->id . '" ';
3513     $xml .= 'generated_coverage="' . $self->escape( $self->obj->generated_coverage ) . '" ';
3514     $xml .= 'show_generated="' . $self->escape( $self->obj->show_generated ) . '" ';
3515     $xml .= 'textual_holdings="' . $self->escape( $self->obj->textual_holdings ) . '">';
3516     $xml .= "\n";
3517
3518     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->distribution )->as_xml({ %$args, no_summaries=>1 }) if (!$args->{no_distribution});
3519
3520     $xml .= "    </serial_summary>\n";
3521
3522     return $xml;
3523 }
3524
3525
3526 package OpenILS::Application::SuperCat::unAPI::sssum;
3527 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3528
3529 package OpenILS::Application::SuperCat::unAPI::sbsum;
3530 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3531
3532 package OpenILS::Application::SuperCat::unAPI::sisum;
3533 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3534
3535 package OpenILS::Application::SuperCat::unAPI::sdist;
3536 use base qw/OpenILS::Application::SuperCat::unAPI/;
3537
3538 sub as_xml {
3539     my $self = shift;
3540     my $args = shift;
3541
3542     my $xml = '    <distribution xmlns="http://open-ils.org/spec/holdings/v1" ';
3543
3544     $xml .= 'id="tag:open-ils.org:serial-distribution/' . $self->obj->id . '" ';
3545     $xml .= 'label="' . $self->escape( $self->obj->label ) . '" ';
3546     $xml .= 'unit_label_prefix="' . $self->escape( $self->obj->unit_label_prefix ) . '" ';
3547     $xml .= 'unit_label_suffix="' . $self->escape( $self->obj->unit_label_suffix ) . '">';
3548     $xml .= "\n";
3549
3550     if (!$args->{no_distributions}) {
3551         if (ref($self->obj->streams) && @{ $self->obj->streams }) {
3552             $xml .= "      <streams>\n" . join(
3553                 '',
3554                 map {
3555                     OpenILS::Application::SuperCat::unAPI
3556                         ->new( $_ )
3557                         ->as_xml({ %$args, no_distribution=>1 })
3558                 } @{ $self->obj->streams }
3559             ) . "      </streams>\n";
3560
3561         } else {
3562             $xml .= "      <streams/>\n";
3563         }
3564     }
3565
3566     if (!$args->{no_summaries}) {
3567         $xml .= "      <summaries>\n";
3568         $xml .= join ('',
3569         map {
3570             defined $_ ?
3571                 OpenILS::Application::SuperCat::unAPI
3572                 ->new( $_ )
3573                 ->as_xml({ %$args, no_distribution=>1 }) : ""
3574         } ($self->obj->basic_summary, $self->obj->supplement_summary, $self->obj->index_summary)
3575         );
3576
3577         $xml .= "      </summaries>\n";
3578     }
3579
3580
3581     $xml .= '      <holding_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3582     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->holding_lib->id . '" ';
3583     $xml .= 'shortname="'.$self->escape( $self->obj->holding_lib->shortname ) .'" ';
3584     $xml .= 'name="'.$self->escape( $self->obj->holding_lib->name ) .'"/>';
3585     $xml .= "\n";
3586
3587     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_distributions=>1 }) if (!$args->{no_subscription});
3588
3589     if (!$args->{no_record} && $self->obj->record_entry) {
3590         my $rec_tag = "tag:open-ils.org:serial-record_entry/".$self->obj->record_entry->id ;
3591
3592         my $r_doc = $parser->parse_string($self->obj->record_entry->marc);
3593         $r_doc->documentElement->setAttribute( id => $rec_tag );
3594         $xml .= $U->entityize($r_doc->documentElement->toString);
3595     }
3596
3597     $xml .= "    </distribution>\n";
3598
3599     return $xml;
3600 }
3601
3602 package OpenILS::Application::SuperCat::unAPI::sstr;
3603 use base qw/OpenILS::Application::SuperCat::unAPI/;
3604
3605 sub as_xml {
3606     my $self = shift;
3607     my $args = shift;
3608
3609     my $xml = '    <stream xmlns="http://open-ils.org/spec/holdings/v1" ';
3610
3611     $xml .= 'id="tag:open-ils.org:serial-stream/' . $self->obj->id . '" ';
3612     $xml .= 'routing_label="' . $self->escape( $self->obj->routing_label ) . '">';
3613     $xml .= "\n";
3614
3615     if (!$args->{no_items}) {
3616         if (ref($self->obj->items) && @{ $self->obj->items }) {
3617             $xml .= "      <items>\n" . join(
3618                 '',
3619                 map {
3620                     OpenILS::Application::SuperCat::unAPI
3621                         ->new( $_ )
3622                         ->as_xml({ %$args, no_stream=>1 })
3623                 } @{ $self->obj->items }
3624             ) . "      </items>\n";
3625
3626         } else {
3627             $xml .= "      <items/>\n";
3628         }
3629     }
3630
3631     #XXX routing_list_user's?
3632
3633     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->distribution )->as_xml({ %$args, no_streams=>1 }) if (!$args->{no_distribution});
3634
3635     $xml .= "    </stream>\n";
3636
3637     return $xml;
3638 }
3639
3640 package OpenILS::Application::SuperCat::unAPI::sitem;
3641 use base qw/OpenILS::Application::SuperCat::unAPI/;
3642
3643 sub as_xml {
3644     my $self = shift;
3645     my $args = shift;
3646
3647     my $xml = '    <serial_item xmlns="http://open-ils.org/spec/holdings/v1" ';
3648
3649     $xml .= 'id="tag:open-ils.org:serial-item/' . $self->obj->id . '" ';
3650     $xml .= 'date_expected="' . $self->escape( $self->obj->date_expected ) . '"';
3651     $xml .= ' date_received="' . $self->escape( $self->obj->date_received ) .'"'if ($self->obj->date_received);
3652
3653     if ($args->{no_issuance}) {
3654         my $siss = ref($self->obj->issuance) ? $self->obj->issuance->id : $self->obj->issuance;
3655         $xml .= ' issuance="tag:open-ils.org:serial-issuance/' . $siss . '"';
3656     }
3657
3658     $xml .= ">\n";
3659
3660     if (ref($self->obj->notes) && $self->obj->notes) {
3661         $xml .= "        <notes>\n";
3662         for my $note ( @{$self->obj->notes} ) {
3663             next unless ( $note->pub eq 't' );
3664             $xml .= sprintf('        <note date="%s" title="%s">%s</note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3665             $xml .= "\n";
3666         }
3667         $xml .= "        </notes>\n";
3668     } else {
3669         $xml .= "      <notes/>\n";
3670     }
3671
3672     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->issuance )->as_xml({ %$args, no_items=>1 }) if (!$args->{no_issuance});
3673     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->stream )->as_xml({ %$args, no_items=>1 }) if (!$args->{no_stream});
3674     $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});
3675     $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});
3676
3677     $xml .= "    </serial_item>\n";
3678
3679     return $xml;
3680 }
3681
3682 package OpenILS::Application::SuperCat::unAPI::sunit;
3683 use base qw/OpenILS::Application::SuperCat::unAPI/;
3684
3685 sub as_xml {
3686     my $self = shift;
3687     my $args = shift;
3688
3689     my $xml = '      <serial_unit xmlns="http://open-ils.org/spec/holdings/v1" '.
3690         'id="tag:open-ils.org:serial-unit/' . $self->obj->id . '" ';
3691
3692     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3693         create_date edit_date copy_number circulate deposit ref holdable deleted
3694         deposit_amount price barcode circ_modifier circ_as_type opac_visible cost
3695     status_changed_time floating mint_condition detailed_contents sort_key summary_contents
3696     /);
3697
3698     $xml .= ">\n";
3699
3700     $xml .= '        <status ident="' . $self->obj->status->id . '" opac_visible="' . $self->obj->status->opac_visible . '">' . $self->escape( $self->obj->status->name  ) . "</status>\n";
3701     $xml .= '        <location ident="' . $self->obj->location->id . '">' . $self->escape( $self->obj->location->name  ) . "</location>\n";
3702     $xml .= '        <circlib ident="' . $self->obj->circ_lib->id . '">' . $self->escape( $self->obj->circ_lib->name  ) . "</circlib>\n";
3703
3704     $xml .= '        <circ_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3705     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->circ_lib->id . '" ';
3706     $xml .= 'shortname="'.$self->escape( $self->obj->circ_lib->shortname ) .'" ';
3707     $xml .= 'name="'.$self->escape( $self->obj->circ_lib->name ) .'"/>';
3708     $xml .= "\n";
3709
3710     $xml .= "        <copy_notes>\n";
3711     if (ref($self->obj->notes) && $self->obj->notes) {
3712         for my $note ( @{$self->obj->notes} ) {
3713             next unless ( $note->pub eq 't' );
3714             $xml .= sprintf('        <copy_note date="%s" title="%s">%s</copy_note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3715             $xml .= "\n";
3716         }
3717     }
3718
3719     $xml .= "        </copy_notes>\n";
3720     $xml .= "        <statcats>\n";
3721
3722     if (ref($self->obj->stat_cat_entries) && $self->obj->stat_cat_entries) {
3723         for my $sce ( @{$self->obj->stat_cat_entries} ) {
3724             next unless ( $sce->stat_cat->opac_visible eq 't' );
3725             $xml .= sprintf('          <statcat name="%s">%s</statcat>',$self->escape($sce->stat_cat->name) ,$self->escape($sce->value));
3726             $xml .= "\n";
3727         }
3728     }
3729     $xml .= "        </statcats>\n";
3730
3731     unless ($args->{no_volume}) {
3732         if (ref($self->obj->call_number)) {
3733             $xml .= OpenILS::Application::SuperCat::unAPI
3734                         ->new( $self->obj->call_number )
3735                         ->as_xml({ %$args, no_copies=>1 });
3736         } else {
3737             $xml .= "    <volume/>\n";
3738         }
3739     }
3740
3741     $xml .= "      </serial_unit>\n";
3742
3743     return $xml;
3744 }
3745
3746 package OpenILS::Application::SuperCat::unAPI::scap;
3747 use base qw/OpenILS::Application::SuperCat::unAPI/;
3748
3749 sub as_xml {
3750     my $self = shift;
3751     my $args = shift;
3752
3753     my $xml = '      <caption_and_pattern xmlns="http://open-ils.org/spec/holdings/v1" '.
3754         'id="tag:open-ils.org:serial-caption_and_pattern/' . $self->obj->id . '" ';
3755
3756     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3757         create_date type active pattern_code enum_1 enum_2 enum_3 enum_4
3758         enum_5 enum_6 chron_1 chron_2 chron_3 chron_4 chron_5 start_date end_date
3759     /);
3760     $xml .= ">\n";
3761     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_captions_and_patterns=>1 }) if (!$args->{no_subscription});
3762     $xml .= "      </caption_and_pattern>\n";
3763
3764     return $xml;
3765 }
3766
3767 package OpenILS::Application::SuperCat::unAPI::siss;
3768 use base qw/OpenILS::Application::SuperCat::unAPI/;
3769
3770 sub as_xml {
3771     my $self = shift;
3772     my $args = shift;
3773
3774     my $xml = '      <issuance xmlns="http://open-ils.org/spec/holdings/v1" '.
3775         'id="tag:open-ils.org:serial-issuance/' . $self->obj->id . '" ';
3776
3777     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" '
3778         for (qw/create_date edit_date label date_published holding_code holding_type holding_link_id/);
3779
3780     $xml .= ">\n";
3781
3782     if (!$args->{no_items}) {
3783         if (ref($self->obj->items) && @{ $self->obj->items }) {
3784             $xml .= "      <items>\n" . join(
3785                 '',
3786                 map {
3787                     OpenILS::Application::SuperCat::unAPI
3788                         ->new( $_ )
3789                         ->as_xml({ %$args, no_stream=>1 })
3790                 } @{ $self->obj->items }
3791             ) . "      </items>\n";
3792
3793         } else {
3794             $xml .= "      <items/>\n";
3795         }
3796     }
3797
3798     $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_issuances=>1 }) if (!$args->{no_subscription});
3799     $xml .= "      </issuance>\n";
3800
3801     return $xml;
3802 }
3803
3804 package OpenILS::Application::SuperCat::unAPI::acp;
3805 use base qw/OpenILS::Application::SuperCat::unAPI/;
3806
3807 sub as_xml {
3808     my $self = shift;
3809     my $args = shift;
3810
3811     my $xml = '      <copy xmlns="http://open-ils.org/spec/holdings/v1" '.
3812         'id="tag:open-ils.org:asset-copy/' . $self->obj->id . '" ';
3813
3814     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3815         create_date edit_date copy_number circulate deposit ref holdable deleted
3816         deposit_amount price barcode circ_modifier circ_as_type opac_visible
3817     /);
3818
3819     $xml .= ">\n";
3820
3821     $xml .= '        <status ident="' . $self->obj->status->id . '" opac_visible="' . $self->obj->status->opac_visible . '">' . $self->escape( $self->obj->status->name  ) . "</status>\n";
3822     $xml .= '        <location ident="' . $self->obj->location->id . '" opac_visible="'.$self->obj->location->opac_visible.'">' . $self->escape( $self->obj->location->name  ) . "</location>\n";
3823     $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";
3824
3825     $xml .= '        <circ_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3826     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->circ_lib->id . '" ';
3827     $xml .= 'shortname="'.$self->escape( $self->obj->circ_lib->shortname ) .'" ';
3828     $xml .= 'name="'.$self->escape( $self->obj->circ_lib->name ) .'" opac_visible="'.$self->obj->circ_lib->opac_visible.'"/>';
3829     $xml .= "\n";
3830
3831     $xml .= "        <monograph_parts>\n";
3832     if (ref($self->obj->parts) && $self->obj->parts) {
3833         for my $part ( @{$self->obj->parts} ) {
3834             next if $U->is_true($part->deleted);
3835             $xml .= sprintf('        <monograph_part record="%s" sortkey="%s">%s</monograph_part>',$part->record, $self->escape($part->label_sortkey), $self->escape($part->label));
3836             $xml .= "\n";
3837         }
3838     }
3839
3840     $xml .= "        </monograph_parts>\n";
3841     $xml .= "        <copy_notes>\n";
3842     if (ref($self->obj->notes) && $self->obj->notes) {
3843         for my $note ( @{$self->obj->notes} ) {
3844             next unless ( $note->pub eq 't' );
3845             $xml .= sprintf('        <copy_note date="%s" title="%s">%s</copy_note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3846             $xml .= "\n";
3847         }
3848     }
3849
3850     $xml .= "        </copy_notes>\n";
3851     $xml .= "        <statcats>\n";
3852
3853     if (ref($self->obj->stat_cat_entries) && $self->obj->stat_cat_entries) {
3854         for my $sce ( @{$self->obj->stat_cat_entries} ) {
3855             next unless ( $sce->stat_cat->opac_visible eq 't' );
3856             $xml .= sprintf('          <statcat name="%s">%s</statcat>',$self->escape($sce->stat_cat->name) ,$self->escape($sce->value));
3857             $xml .= "\n";
3858         }
3859     }
3860     $xml .= "        </statcats>\n";
3861
3862     unless ($args->{no_volume}) {
3863         if (ref($self->obj->call_number)) {
3864             $xml .= OpenILS::Application::SuperCat::unAPI
3865                         ->new( $self->obj->call_number )
3866                         ->as_xml({ %$args, no_copies=>1 });
3867         } else {
3868             $xml .= "    <volume/>\n";
3869         }
3870     }
3871
3872     $xml .= "      </copy>\n";
3873
3874     return $xml;
3875 }
3876
3877
3878 1;
3879 # vim: et:ts=4:sw=4