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