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