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