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