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