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