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