]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/SuperCat.pm
Minor bug fixes (alignment of fm classes, etc); Do not check deletedness, just remove...
[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', 'are' },
1664               where     => {
1665                 '+afr' => { tag => \@ref_tags, subfield => $subfield, value => { '<' => $value } },
1666                 '+are' => { deleted => 'f' }
1667               },
1668               order_by  => { afr => { value => 'desc' } },
1669               limit     => $ref_limit,
1670               offset    => $offset,
1671             }
1672         )->gather(1);
1673         push @list, map { $_->{record} } reverse(@$before);
1674     }
1675
1676     if ($page >= 0) {
1677         my $after = $_storage->request(
1678             "open-ils.cstore.json_query.atomic",
1679             { select    => { afr => [qw/record value/] },
1680               from      => { 'afr', 'are' },
1681               where     => {
1682                 '+afr' => { tag => \@ref_tags, subfield => $subfield, value => { '>=' => $value } },
1683                 '+are' => { deleted => 'f' }
1684               },
1685               order_by  => { afr => { value => 'asc' } },
1686               limit     => $ref_limit,
1687               offset    => $offset,
1688             }
1689         )->gather(1);
1690         push @list, map { $_->{record} } @$after;
1691     }
1692
1693     # If we're not pulling in see/see also references, just return the raw list
1694     if ($self->api_name !~ /\.refs\./) {
1695         return \@list;
1696     }
1697
1698     # Remove dupe record IDs that turn up due to 4xx and 5xx matches
1699     my @retlist = ();
1700     my %seen;
1701     foreach my $record (@list) {
1702         next if exists $seen{$record};
1703         push @retlist, int($record);
1704         $seen{$record} = 1;
1705     }
1706
1707     return \@retlist;
1708 }
1709 __PACKAGE__->register_method(
1710         method    => 'authority_tag_sf_startwith',
1711         api_name  => 'open-ils.supercat.authority.tag.startwith',
1712         api_level => 1,
1713         argc      => 1,
1714         signature =>
1715                 { desc     => <<"                 DESC",
1716 Returns a list of the requested authority record IDs held
1717                   DESC
1718                   params   =>
1719                         [
1720                                 { name => 'tag',
1721                                   desc => 'The target Authority MARC tag',
1722                                   type => 'string' },
1723                                 { name => 'subfield',
1724                                   desc => 'The target Authority MARC subfield',
1725                                   type => 'string' },
1726                                 { name => 'value',
1727                                   desc => 'The target string',
1728                                   type => 'string' },
1729                                 { name => 'page_size',
1730                                   desc => 'Count of call numbers to retrieve, default is 10',
1731                                   type => 'number' },
1732                                 { name => 'page',
1733                                   desc => 'The page of call numbers to retrieve, calculated based on page_size.  Can be positive, negative or 0.',
1734                                   type => 'number' },
1735                         ],
1736                   'return' =>
1737                         { desc => 'Authority Record IDs that are near the target string',
1738                           type => 'array' }
1739                 }
1740 );
1741
1742
1743 sub holding_data_formats {
1744     return [{
1745         marcxml => {
1746             namespace_uri         => 'http://www.loc.gov/MARC21/slim',
1747                         docs              => 'http://www.loc.gov/marcxml/',
1748                         schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
1749                 }
1750         }];
1751 }
1752 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.acn.formats', api_level => 1 );
1753 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.acp.formats', api_level => 1 );
1754 __PACKAGE__->register_method( method => 'holding_data_formats', api_name => 'open-ils.supercat.auri.formats', api_level => 1 );
1755
1756
1757 __PACKAGE__->register_method(
1758         method    => 'retrieve_uri',
1759         api_name  => 'open-ils.supercat.auri.marcxml.retrieve',
1760         api_level => 1,
1761         argc      => 1,
1762         signature =>
1763                 { desc     => <<"                 DESC",
1764 Returns a fleshed call number object
1765                   DESC
1766                   params   =>
1767                         [
1768                                 { name => 'uri_id',
1769                                   desc => 'An OpenILS asset::uri id',
1770                                   type => 'number' },
1771                         ],
1772                   'return' =>
1773                         { desc => 'fleshed uri',
1774                           type => 'object' }
1775                 }
1776 );
1777 sub retrieve_uri {
1778         my $self = shift;
1779         my $client = shift;
1780         my $cpid = shift;
1781         my $args = shift || {};
1782
1783     return OpenILS::Application::SuperCat::unAPI
1784         ->new(OpenSRF::AppSession
1785             ->create( 'open-ils.cstore' )
1786             ->request(
1787                 "open-ils.cstore.direct.asset.uri.retrieve",
1788                     $cpid,
1789                     { flesh             => 10,
1790                           flesh_fields  => {
1791                                                 auri    => [qw/call_number_maps/],
1792                                                 auricnm => [qw/call_number/],
1793                                                 acn         => [qw/owning_lib record prefix suffix/],
1794                                 }
1795                     })
1796             ->gather(1))
1797         ->as_xml($args);
1798 }
1799
1800 __PACKAGE__->register_method(
1801         method    => 'retrieve_copy',
1802         api_name  => 'open-ils.supercat.acp.marcxml.retrieve',
1803         api_level => 1,
1804         argc      => 1,
1805         signature =>
1806                 { desc     => <<"                 DESC",
1807 Returns a fleshed call number object
1808                   DESC
1809                   params   =>
1810                         [
1811                                 { name => 'cn_id',
1812                                   desc => 'An OpenILS asset::copy id',
1813                                   type => 'number' },
1814                         ],
1815                   'return' =>
1816                         { desc => 'fleshed copy',
1817                           type => 'object' }
1818                 }
1819 );
1820 sub retrieve_copy {
1821         my $self = shift;
1822         my $client = shift;
1823         my $cpid = shift;
1824         my $args = shift || {};
1825
1826     return OpenILS::Application::SuperCat::unAPI
1827         ->new(OpenSRF::AppSession
1828             ->create( 'open-ils.cstore' )
1829             ->request(
1830                 "open-ils.cstore.direct.asset.copy.retrieve",
1831                     $cpid,
1832                     { flesh             => 2,
1833                           flesh_fields  => {
1834                                                 acn     => [qw/owning_lib record prefix suffix/],
1835                                                 acp     => [qw/call_number location status circ_lib stat_cat_entries notes parts/],
1836                                 }
1837                     })
1838             ->gather(1))
1839         ->as_xml($args);
1840 }
1841
1842 __PACKAGE__->register_method(
1843         method    => 'retrieve_callnumber',
1844         api_name  => 'open-ils.supercat.acn.marcxml.retrieve',
1845         api_level => 1,
1846         argc      => 1,
1847         stream    => 1,
1848         signature =>
1849                 { desc     => <<"                 DESC",
1850 Returns a fleshed call number object
1851                   DESC
1852                   params   =>
1853                         [
1854                                 { name => 'cn_id',
1855                                   desc => 'An OpenILS asset::call_number id',
1856                                   type => 'number' },
1857                         ],
1858                   'return' =>
1859                         { desc => 'call number with copies',
1860                           type => 'object' }
1861                 }
1862 );
1863 sub retrieve_callnumber {
1864         my $self = shift;
1865         my $client = shift;
1866         my $cnid = shift;
1867         my $args = shift || {};
1868
1869     return OpenILS::Application::SuperCat::unAPI
1870         ->new(OpenSRF::AppSession
1871             ->create( 'open-ils.cstore' )
1872             ->request(
1873                 "open-ils.cstore.direct.asset.call_number.retrieve",
1874                     $cnid,
1875                     { flesh             => 5,
1876                           flesh_fields  => {
1877                                                 acn     => [qw/owning_lib record copies uri_maps prefix suffix/],
1878                                                 auricnm => [qw/uri/],
1879                                                 acp     => [qw/location status circ_lib stat_cat_entries notes parts/],
1880                                 }
1881                     })
1882             ->gather(1))
1883         ->as_xml($args);
1884
1885 }
1886
1887 __PACKAGE__->register_method(
1888         method    => 'basic_record_holdings',
1889         api_name  => 'open-ils.supercat.record.basic_holdings.retrieve',
1890         api_level => 1,
1891         argc      => 1,
1892         stream    => 1,
1893         signature =>
1894                 { desc     => <<"                 DESC",
1895 Returns a basic hash representation of the requested bibliographic record's holdings
1896                   DESC
1897                   params   =>
1898                         [
1899                                 { name => 'bibId',
1900                                   desc => 'An OpenILS biblio::record_entry id',
1901                                   type => 'number' },
1902                         ],
1903                   'return' =>
1904                         { desc => 'Hash of bib record holdings hierarchy (call numbers and copies)',
1905                           type => 'string' }
1906                 }
1907 );
1908 sub basic_record_holdings {
1909         my $self = shift;
1910         my $client = shift;
1911         my $bib = shift;
1912         my $ou = shift;
1913
1914         #  holdings hold an array of call numbers, which hold an array of copies
1915         #  holdings => [ label: { library, [ copies: { barcode, location, status, circ_lib } ] } ]
1916         my %holdings;
1917
1918         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
1919
1920         my $tree = $_storage->request(
1921                 "open-ils.cstore.direct.biblio.record_entry.retrieve",
1922                 $bib,
1923                 { flesh         => 5,
1924                   flesh_fields  => {
1925                                         bre     => [qw/call_numbers/],
1926                                         acn     => [qw/copies owning_lib prefix suffix/],
1927                                         acp     => [qw/location status circ_lib parts/],
1928                                 }
1929                 }
1930         )->gather(1);
1931
1932         my $o_search = { shortname => uc($ou) };
1933         if (!$ou || $ou eq '-') {
1934                 $o_search = { parent_ou => undef };
1935         }
1936
1937         my $orgs = $_storage->request(
1938                 "open-ils.cstore.direct.actor.org_unit.search",
1939                 $o_search,
1940                 { flesh         => 100,
1941                   flesh_fields  => { aou        => [qw/children/] }
1942                 }
1943         )->gather(1);
1944
1945         my @ou_ids = tree_walker($orgs, 'children', sub {shift->id}) if $orgs;
1946
1947         $logger->debug("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
1948
1949         for my $cn (@{$tree->call_numbers}) {
1950         next unless ( $cn->deleted eq 'f' || $cn->deleted == 0 );
1951
1952                 my $found = 0;
1953                 for my $c (@{$cn->copies}) {
1954                         next unless grep {$c->circ_lib->id == $_} @ou_ids;
1955                         next unless ( $c->deleted eq 'f' || $c->deleted == 0 );
1956                         $found = 1;
1957                         last;
1958                 }
1959                 next unless $found;
1960
1961                 $holdings{$cn->label}{'owning_lib'} = $cn->owning_lib->shortname;
1962
1963                 for my $cp (@{$cn->copies}) {
1964
1965                         next unless grep { $cp->circ_lib->id == $_ } @ou_ids;
1966                         next unless ( $cp->deleted eq 'f' || $cp->deleted == 0 );
1967
1968                         push @{$holdings{$cn->label}{'copies'}}, {
1969                 barcode => $cp->barcode,
1970                 status => $cp->status->name,
1971                 location => $cp->location->name,
1972                 circlib => $cp->circ_lib->shortname
1973             };
1974
1975                 }
1976         }
1977
1978         return \%holdings;
1979 }
1980
1981 #__PACKAGE__->register_method(
1982 #       method    => 'new_record_holdings',
1983 #       api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
1984 #       api_level => 1,
1985 #       argc      => 1,
1986 #       stream    => 1,
1987 #       signature =>
1988 #               { desc     => <<"                 DESC",
1989 #Returns the XML representation of the requested bibliographic record's holdings
1990 #                 DESC
1991 #                 params   =>
1992 #                       [
1993 #                               { name => 'bibId',
1994 #                                 desc => 'An OpenILS biblio::record_entry id',
1995 #                                 type => 'number' },
1996 #                       ],
1997 #                 'return' =>
1998 #                       { desc => 'Stream of bib record holdings hierarchy in XML',
1999 #                         type => 'string' }
2000 #               }
2001 #);
2002 #
2003
2004 sub new_record_holdings {
2005         my $self = shift;
2006         my $client = shift;
2007         my $bib = shift;
2008         my $ou = shift;
2009         my $depth = shift;
2010         my $flesh = shift;
2011         my $paging = shift;
2012
2013     $paging = [-1,0] if (!$paging or !ref($paging) or @$paging == 0);
2014     my $limit = $$paging[0];
2015     my $offset = $$paging[1] || 0;
2016
2017         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2018         my $_search = OpenSRF::AppSession->create( 'open-ils.search' );
2019
2020         my $o_search = { shortname => uc($ou) };
2021         if (!$ou || $ou eq '-') {
2022                 $o_search = { parent_ou => undef };
2023         }
2024
2025     my $one_org = $_storage->request(
2026         "open-ils.cstore.direct.actor.org_unit.search",
2027         $o_search
2028     )->gather(1);
2029
2030     my $count_req = $_search->request('open-ils.search.biblio.record.copy_count' => $one_org->id => $bib);
2031     my $staff_count_req = $_search->request('open-ils.search.biblio.record.copy_count.staff' => $one_org->id => $bib);
2032
2033     my $orgs = $_storage->request(
2034         'open-ils.cstore.json_query.atomic',
2035         { from => [ 'actor.org_unit_descendants', defined($depth) ? ( $one_org->id, $depth ) :  ( $one_org->id ) ] }
2036     )->gather(1);
2037
2038
2039         my @ou_ids = map { $_->{id} } @$orgs;
2040
2041         $logger->info("Searching for holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2042
2043     my %subselect = ( '-or' => [
2044         { owning_lib => \@ou_ids },
2045         { '-exists'  =>
2046             { from  => 'acp',
2047               where => {
2048                 call_number => { '=' => {'+acn'=>'id'} },
2049                 deleted => 'f',
2050                 circ_lib => \@ou_ids
2051               }
2052             }
2053         }
2054     ]);
2055
2056     if ($flesh and $flesh eq 'uris') {
2057         %subselect = (
2058             owning_lib => \@ou_ids,
2059             '-exists'  => {
2060                 from  => { auricnm => 'auri' },
2061                 where => {
2062                     call_number => { '=' => {'+acn'=>'id'} },
2063                     '+auri' => { active => 't' }
2064                 }
2065             }
2066         );
2067     }
2068
2069
2070         my $cns = $_storage->request(
2071                 "open-ils.cstore.direct.asset.call_number.search.atomic",
2072                 { record  => $bib,
2073           deleted => 'f',
2074           %subselect
2075         },
2076                 { flesh         => 5,
2077                   flesh_fields  => {
2078                                         acn     => [qw/copies owning_lib uri_maps prefix suffix/],
2079                                         auricnm => [qw/uri/],
2080                                         acp     => [qw/circ_lib location status stat_cat_entries notes parts/],
2081                                         asce    => [qw/stat_cat/],
2082                                 },
2083           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2084           ( $offset     ? ( offset => $offset ) : () ),
2085           order_by  => { acn => { label_sortkey => {} } }
2086                 }
2087         )->gather(1);
2088
2089         my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2090         $year += 1900;
2091         $month += 1;
2092
2093         $client->respond("<holdings xmlns='http://open-ils.org/spec/holdings/v1'><counts>\n");
2094
2095         my $copy_counts = $count_req->gather(1);
2096         my $staff_copy_counts = $staff_count_req->gather(1);
2097
2098         for my $c (@$copy_counts) {
2099                 $$c{transcendant} ||= 0;
2100                 my $out = "<count type='public'";
2101                 $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2102                 $client->respond("$out/>\n")
2103         }
2104
2105         for my $c (@$staff_copy_counts) {
2106                 $$c{transcendant} ||= 0;
2107                 my $out = "<count type='staff'";
2108                 $out .= " $_='$$c{$_}'" for (qw/count available unshadow transcendant org_unit depth/);
2109                 $client->respond("$out/>\n")
2110         }
2111
2112     $client->respond("</counts><volumes>\n");
2113     
2114         for my $cn (@$cns) {
2115                 next unless (@{$cn->copies} > 0 or (ref($cn->uri_maps) and @{$cn->uri_maps}));
2116
2117                 # We don't want O:A:S:unAPI::acn to return the record, we've got that already
2118                 # In the context of BibTemplate, copies aren't necessary because we pull those
2119                 # in a separate call
2120         $client->respond(
2121             OpenILS::Application::SuperCat::unAPI::acn
2122                 ->new( $cn )
2123                 ->as_xml( {no_record => 1, no_copies => ($flesh ? 0 : 1)} )
2124         );
2125         }
2126
2127         $client->respond("</volumes><subscriptions>\n");
2128
2129         $logger->info("Searching for serial holdings at orgs [".join(',',@ou_ids)."], based on $ou");
2130
2131     %subselect = ( '-or' => [
2132         { owning_lib => \@ou_ids },
2133         { '-exists'  =>
2134             { from  => 'sdist',
2135               where => { holding_lib => \@ou_ids }
2136             }
2137         }
2138     ]);
2139
2140         my $ssubs = $_storage->request(
2141                 "open-ils.cstore.direct.serial.subscription.search.atomic",
2142                 { record_entry  => $bib,
2143           %subselect
2144         },
2145                 { flesh         => 7,
2146                   flesh_fields  => {
2147                                         ssub    => [qw/distributions issuances scaps owning_lib/],
2148                                         sdist   => [qw/basic_summary supplement_summary index_summary streams holding_lib/],
2149                                         sstr    => [qw/items/],
2150                                         sitem   => [qw/notes unit/],
2151                                         sunit   => [qw/notes location status circ_lib stat_cat_entries call_number/],
2152                                         acn     => [qw/owning_lib prefix suffix/],
2153                                 },
2154           ( $limit > -1 ? ( limit  => $limit  ) : () ),
2155           ( $offset     ? ( offset => $offset ) : () ),
2156           order_by  => {
2157                         ssub => {
2158                                 start_date => {},
2159                                 owning_lib => {},
2160                                 id => {}
2161                         },
2162                         sdist => {
2163                                 label => {},
2164                                 owning_lib => {},
2165                         },
2166                         sunit => {
2167                                 date_expected => {},
2168                         }
2169                   }
2170                 }
2171         )->gather(1);
2172
2173
2174         for my $ssub (@$ssubs) {
2175                 next unless (@{$ssub->distributions} or @{$ssub->issuances} or @{$ssub->scaps});
2176
2177                 # We don't want O:A:S:unAPI::ssub to return the record, we've got that already
2178                 # In the context of BibTemplate, copies aren't necessary because we pull those
2179                 # in a separate call
2180         $client->respond(
2181             OpenILS::Application::SuperCat::unAPI::ssub
2182                 ->new( $ssub )
2183                 ->as_xml( {no_record => 1, no_items => ($flesh ? 0 : 1)} )
2184         );
2185         }
2186
2187
2188         return "</subscriptions></holdings>\n";
2189 }
2190 __PACKAGE__->register_method(
2191         method    => 'new_record_holdings',
2192         api_name  => 'open-ils.supercat.record.holdings_xml.retrieve',
2193         api_level => 1,
2194         argc      => 1,
2195         stream    => 1,
2196         signature =>
2197                 { desc     => <<"                 DESC",
2198 Returns the XML representation of the requested bibliographic record's holdings
2199                   DESC
2200                   params   =>
2201                         [
2202                                 { name => 'bibId',
2203                                   desc => 'An OpenILS biblio::record_entry ID',
2204                                   type => 'number' },
2205                                 { name => 'orgUnit',
2206                                   desc => 'An OpenILS actor::org_unit short name that limits the scope of returned holdings',
2207                                   type => 'text' },
2208                                 { name => 'depth',
2209                                   desc => 'An OpenILS actor::org_unit_type depththat limits the scope of returned holdings',
2210                                   type => 'number' },
2211                                 { name => 'hideCopies',
2212                                   desc => 'Flag that prevents the inclusion of copies in the returned holdings',
2213                                   type => 'boolean' },
2214                                 { name => 'paging',
2215                                   desc => 'Arry of limit and offset for holdings paging',
2216                                   type => 'array' },
2217                         ],
2218                   'return' =>
2219                         { desc => 'Stream of bib record holdings hierarchy in XML',
2220                           type => 'string' }
2221                 }
2222 );
2223
2224 sub isbn_holdings {
2225         my $self = shift;
2226         my $client = shift;
2227         my $isbn = shift;
2228
2229         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2230
2231         my $recs = $_storage->request(
2232                         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2233                         { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2234         )->gather(1);
2235
2236         return undef unless (@$recs);
2237
2238         return ($self->method_lookup( 'open-ils.supercat.record.holdings_xml.retrieve')->run( $recs->[0]->record ))[0];
2239 }
2240 __PACKAGE__->register_method(
2241         method    => 'isbn_holdings',
2242         api_name  => 'open-ils.supercat.isbn.holdings_xml.retrieve',
2243         api_level => 1,
2244         argc      => 1,
2245         signature =>
2246                 { desc     => <<"                 DESC",
2247 Returns the XML representation of the requested bibliographic record's holdings
2248                   DESC
2249                   params   =>
2250                         [
2251                                 { name => 'isbn',
2252                                   desc => 'An isbn',
2253                                   type => 'string' },
2254                         ],
2255                   'return' =>
2256                         { desc => 'The bib record holdings hierarchy in XML',
2257                           type => 'string' }
2258                 }
2259 );
2260
2261 sub escape {
2262         my $self = shift;
2263         my $text = shift;
2264     return '' unless $text;
2265         $text =~ s/&/&amp;/gsom;
2266         $text =~ s/</&lt;/gsom;
2267         $text =~ s/>/&gt;/gsom;
2268         $text =~ s/"/&quot;/gsom;
2269         $text =~ s/'/&apos;/gsom;
2270         return $text;
2271 }
2272
2273 sub recent_changes {
2274         my $self = shift;
2275         my $client = shift;
2276         my $when = shift || '1-01-01';
2277         my $limit = shift;
2278
2279         my $type = 'biblio';
2280         my $hint = 'bre';
2281
2282         if ($self->api_name =~ /authority/o) {
2283                 $type = 'authority';
2284                 $hint = 'are';
2285         }
2286
2287         my $axis = 'create_date';
2288         $axis = 'edit_date' if ($self->api_name =~ /edit/o);
2289
2290         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2291
2292         return $_storage->request(
2293                 "open-ils.cstore.direct.$type.record_entry.id_list.atomic",
2294                 { $axis => { ">" => $when }, id => { '>' => 0 }, deleted => 'f', active => 't' },
2295                 { order_by => { $hint => "$axis desc" }, limit => $limit }
2296         )->gather(1);
2297 }
2298
2299 for my $t ( qw/biblio authority/ ) {
2300         for my $a ( qw/import edit/ ) {
2301
2302                 __PACKAGE__->register_method(
2303                         method    => 'recent_changes',
2304                         api_name  => "open-ils.supercat.$t.record.$a.recent",
2305                         api_level => 1,
2306                         argc      => 0,
2307                         signature =>
2308                                 { desc     => "Returns a list of recently ${a}ed $t records",
2309                                   params   =>
2310                                         [
2311                                                 { name => 'when',
2312                                                   desc => "Date to start looking for ${a}ed records",
2313                                                   default => '1-01-01',
2314                                                   type => 'string' },
2315
2316                                                 { name => 'limit',
2317                                                   desc => "Maximum count to retrieve",
2318                                                   type => 'number' },
2319                                         ],
2320                                   'return' =>
2321                                         { desc => "An id list of $t records",
2322                                           type => 'array' }
2323                                 },
2324                 );
2325         }
2326 }
2327
2328
2329 sub retrieve_authority_marcxml {
2330         my $self = shift;
2331         my $client = shift;
2332         my $rid = shift;
2333
2334         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2335
2336         my $record = $_storage->request( 'open-ils.cstore.direct.authority.record_entry.retrieve' => $rid )->gather(1);
2337         return $U->entityize( $record->marc ) if ($record);
2338         return undef;
2339 }
2340
2341 __PACKAGE__->register_method(
2342         method    => 'retrieve_authority_marcxml',
2343         api_name  => 'open-ils.supercat.authority.marcxml.retrieve',
2344         api_level => 1,
2345         argc      => 1,
2346         signature =>
2347                 { desc     => <<"                 DESC",
2348 Returns the MARCXML representation of the requested authority record
2349                   DESC
2350                   params   =>
2351                         [
2352                                 { name => 'authorityId',
2353                                   desc => 'An OpenILS authority::record_entry id',
2354                                   type => 'number' },
2355                         ],
2356                   'return' =>
2357                         { desc => 'The authority record in MARCXML',
2358                           type => 'string' }
2359                 }
2360 );
2361
2362 sub retrieve_record_marcxml {
2363         my $self = shift;
2364         my $client = shift;
2365         my $rid = shift;
2366
2367         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2368
2369         my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $rid )->gather(1);
2370         return $U->entityize( $record->marc ) if ($record);
2371         return undef;
2372 }
2373
2374 __PACKAGE__->register_method(
2375         method    => 'retrieve_record_marcxml',
2376         api_name  => 'open-ils.supercat.record.marcxml.retrieve',
2377         api_level => 1,
2378         argc      => 1,
2379         signature =>
2380                 { desc     => <<"                 DESC",
2381 Returns the MARCXML representation of the requested bibliographic record
2382                   DESC
2383                   params   =>
2384                         [
2385                                 { name => 'bibId',
2386                                   desc => 'An OpenILS biblio::record_entry id',
2387                                   type => 'number' },
2388                         ],
2389                   'return' =>
2390                         { desc => 'The bib record in MARCXML',
2391                           type => 'string' }
2392                 }
2393 );
2394
2395 sub retrieve_isbn_marcxml {
2396         my $self = shift;
2397         my $client = shift;
2398         my $isbn = shift;
2399
2400         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2401
2402         my $recs = $_storage->request(
2403                         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2404                         { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2405         )->gather(1);
2406
2407         return undef unless (@$recs);
2408
2409         my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2410         return $U->entityize( $record->marc ) if ($record);
2411         return undef;
2412 }
2413
2414 __PACKAGE__->register_method(
2415         method    => 'retrieve_isbn_marcxml',
2416         api_name  => 'open-ils.supercat.isbn.marcxml.retrieve',
2417         api_level => 1,
2418         argc      => 1,
2419         signature =>
2420                 { desc     => <<"                 DESC",
2421 Returns the MARCXML representation of the requested ISBN
2422                   DESC
2423                   params   =>
2424                         [
2425                                 { name => 'ISBN',
2426                                   desc => 'An ... um ... ISBN',
2427                                   type => 'string' },
2428                         ],
2429                   'return' =>
2430                         { desc => 'The bib record in MARCXML',
2431                           type => 'string' }
2432                 }
2433 );
2434
2435 sub retrieve_record_transform {
2436         my $self = shift;
2437         my $client = shift;
2438         my $rid = shift;
2439
2440         (my $transform = $self->api_name) =~ s/^.+record\.([^\.]+)\.retrieve$/$1/o;
2441
2442         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2443         #$_storage->connect;
2444
2445         my $record = $_storage->request(
2446                 'open-ils.cstore.direct.biblio.record_entry.retrieve',
2447                 $rid
2448         )->gather(1);
2449
2450         return undef unless ($record);
2451
2452         return $U->entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $record->marc ) )->toString);
2453 }
2454
2455 sub retrieve_isbn_transform {
2456         my $self = shift;
2457         my $client = shift;
2458         my $isbn = shift;
2459
2460         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2461
2462         my $recs = $_storage->request(
2463                         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2464                         { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2465         )->gather(1);
2466
2467         return undef unless (@$recs);
2468
2469         (my $transform = $self->api_name) =~ s/^.+isbn\.([^\.]+)\.retrieve$/$1/o;
2470
2471         my $record = $_storage->request( 'open-ils.cstore.direct.biblio.record_entry.retrieve' => $recs->[0]->record )->gather(1);
2472
2473         return undef unless ($record);
2474
2475         return $U->entityize($record_xslt{$transform}{xslt}->transform( $_parser->parse_string( $record->marc ) )->toString);
2476 }
2477
2478 sub retrieve_record_objects {
2479         my $self = shift;
2480         my $client = shift;
2481         my $ids = shift;
2482
2483         my $type = 'biblio';
2484
2485         if ($self->api_name =~ /authority/) {
2486                 $type = 'authority';
2487         }
2488
2489         $ids = [$ids] unless (ref $ids);
2490         $ids = [grep {$_} @$ids];
2491
2492         return [] unless (@$ids);
2493
2494         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2495         return $_storage->request("open-ils.cstore.direct.$type.record_entry.search.atomic" => { id => [grep {$_} @$ids] })->gather(1);
2496 }
2497 __PACKAGE__->register_method(
2498         method    => 'retrieve_record_objects',
2499         api_name  => 'open-ils.supercat.record.object.retrieve',
2500         api_level => 1,
2501         argc      => 1,
2502         signature =>
2503                 { desc     => <<"                 DESC",
2504 Returns the Fieldmapper object representation of the requested bibliographic records
2505                   DESC
2506                   params   =>
2507                         [
2508                                 { name => 'bibIds',
2509                                   desc => 'OpenILS biblio::record_entry ids',
2510                                   type => 'array' },
2511                         ],
2512                   'return' =>
2513                         { desc => 'The bib records',
2514                           type => 'array' }
2515                 }
2516 );
2517
2518 __PACKAGE__->register_method(
2519         method    => 'retrieve_record_objects',
2520         api_name  => 'open-ils.supercat.authority.object.retrieve',
2521         api_level => 1,
2522         argc      => 1,
2523         signature =>
2524                 { desc     => <<"                 DESC",
2525 Returns the Fieldmapper object representation of the requested authority records
2526                   DESC
2527                   params   =>
2528                         [
2529                                 { name => 'authIds',
2530                                   desc => 'OpenILS authority::record_entry ids',
2531                                   type => 'array' },
2532                         ],
2533                   'return' =>
2534                         { desc => 'The authority records',
2535                           type => 'array' }
2536                 }
2537 );
2538
2539 sub retrieve_isbn_object {
2540         my $self = shift;
2541         my $client = shift;
2542         my $isbn = shift;
2543
2544         return undef unless ($isbn);
2545
2546         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2547         my $recs = $_storage->request(
2548                         'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2549                         { tag => { like => '02%'}, value => {like => "$isbn\%"}}
2550         )->gather(1);
2551
2552         return undef unless (@$recs);
2553
2554         return $_storage->request(
2555                 'open-ils.cstore.direct.biblio.record_entry.search.atomic',
2556                 { id => $recs->[0]->record }
2557         )->gather(1);
2558 }
2559 __PACKAGE__->register_method(
2560         method    => 'retrieve_isbn_object',
2561         api_name  => 'open-ils.supercat.isbn.object.retrieve',
2562         api_level => 1,
2563         argc      => 1,
2564         signature =>
2565                 { desc     => <<"                 DESC",
2566 Returns the Fieldmapper object representation of the requested bibliographic record
2567                   DESC
2568                   params   =>
2569                         [
2570                                 { name => 'isbn',
2571                                   desc => 'an ISBN',
2572                                   type => 'string' },
2573                         ],
2574                   'return' =>
2575                         { desc => 'The bib record',
2576                           type => 'object' }
2577                 }
2578 );
2579
2580
2581
2582 sub retrieve_metarecord_mods {
2583         my $self = shift;
2584         my $client = shift;
2585         my $rid = shift;
2586
2587         my $_storage = OpenSRF::AppSession->connect( 'open-ils.cstore' );
2588
2589         # Get the metarecord in question
2590         my $mr =
2591         $_storage->request(
2592                 'open-ils.cstore.direct.metabib.metarecord.retrieve' => $rid
2593         )->gather(1);
2594
2595         # Now get the map of all bib records for the metarecord
2596         my $recs =
2597         $_storage->request(
2598                 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
2599                 {metarecord => $rid}
2600         )->gather(1);
2601
2602         $logger->debug("Adding ".scalar(@$recs)." bib record to the MODS of the metarecord");
2603
2604         # and retrieve the lead (master) record as MODS
2605         my ($master) =
2606                 $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2607                         ->run($mr->master_record);
2608         my $master_mods = $_parser->parse_string($master)->documentElement;
2609         $master_mods->setNamespace( "http://www.loc.gov/mods/", "mods" );
2610         $master_mods->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2611
2612         # ... and a MODS clone to populate, with guts removed.
2613         my $mods = $_parser->parse_string($master)->documentElement;
2614         $mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # modsCollection element
2615         $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2616         ($mods) = $mods->findnodes('//mods:mods');
2617         #$mods->setNamespace( "http://www.loc.gov/mods/", "mods" ); # mods element
2618         $mods->removeChildNodes;
2619         $mods->setNamespace('http://www.loc.gov/mods/', undef, 1);
2620
2621         # Add the metarecord ID as a (locally defined) info URI
2622         my $recordInfo = $mods
2623                 ->ownerDocument
2624                 ->createElement("recordInfo");
2625
2626         my $recordIdentifier = $mods
2627                 ->ownerDocument
2628                 ->createElement("recordIdentifier");
2629
2630         my ($year,$month,$day) = reverse( (localtime)[3,4,5] );
2631         $year += 1900;
2632         $month += 1;
2633
2634         my $id = $mr->id;
2635         $recordIdentifier->appendTextNode(
2636                 sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:metabib-metarecord/$id", $month, $day)
2637         );
2638
2639         $recordInfo->appendChild($recordIdentifier);
2640         $mods->appendChild($recordInfo);
2641
2642         # Grab the title, author and ISBN for the master record and populate the metarecord
2643         my ($title) = $master_mods->findnodes( './mods:titleInfo[not(@type)]' );
2644         
2645         if ($title) {
2646                 $title->setNamespace( "http://www.loc.gov/mods/", "mods" );
2647                 $title->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2648                 $title = $mods->ownerDocument->importNode($title);
2649                 $mods->appendChild($title);
2650         }
2651
2652         my ($author) = $master_mods->findnodes( './mods:name[mods:role/mods:text[text()="creator"]]' );
2653         if ($author) {
2654                 $author->setNamespace( "http://www.loc.gov/mods/", "mods" );
2655                 $author->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2656                 $author = $mods->ownerDocument->importNode($author);
2657                 $mods->appendChild($author);
2658         }
2659
2660         my ($isbn) = $master_mods->findnodes( './mods:identifier[@type="isbn"]' );
2661         if ($isbn) {
2662                 $isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2663                 $isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2664                 $isbn = $mods->ownerDocument->importNode($isbn);
2665                 $mods->appendChild($isbn);
2666         }
2667
2668         # ... and loop over the constituent records
2669         for my $map ( @$recs ) {
2670
2671                 # get the MODS
2672                 my ($rec) =
2673                         $self   ->method_lookup('open-ils.supercat.record.mods.retrieve')
2674                                 ->run($map->source);
2675
2676                 my $part_mods = $_parser->parse_string($rec);
2677                 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", "mods" );
2678                 $part_mods->documentElement->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2679                 ($part_mods) = $part_mods->findnodes('//mods:mods');
2680
2681                 for my $node ( ($part_mods->findnodes( './mods:subject' )) ) {
2682                         $node->setNamespace( "http://www.loc.gov/mods/", "mods" );
2683                         $node->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2684                         $node = $mods->ownerDocument->importNode($node);
2685                         $mods->appendChild( $node );
2686                 }
2687
2688                 my $relatedItem = $mods
2689                         ->ownerDocument
2690                         ->createElement("relatedItem");
2691
2692                 $relatedItem->setAttribute( type => 'constituent' );
2693
2694                 my $identifier = $mods
2695                         ->ownerDocument
2696                         ->createElement("identifier");
2697
2698                 $identifier->setAttribute( type => 'uri' );
2699
2700                 my $subRecordInfo = $mods
2701                         ->ownerDocument
2702                         ->createElement("recordInfo");
2703
2704                 my $subRecordIdentifier = $mods
2705                         ->ownerDocument
2706                         ->createElement("recordIdentifier");
2707
2708                 my $subid = $map->source;
2709                 $subRecordIdentifier->appendTextNode(
2710                         sprintf("tag:open-ils.org,$year-\%0.2d-\%0.2d:biblio-record_entry/$subid",
2711                                 $month,
2712                                 $day
2713                         )
2714                 );
2715                 $subRecordInfo->appendChild($subRecordIdentifier);
2716
2717                 $relatedItem->appendChild( $subRecordInfo );
2718
2719                 my ($tor) = $part_mods->findnodes( './mods:typeOfResource' );
2720                 $tor->setNamespace( "http://www.loc.gov/mods/", "mods" );
2721                 $tor->setNamespace( "http://www.loc.gov/mods/", undef, 1 ) if ($tor);
2722                 $tor = $mods->ownerDocument->importNode($tor) if ($tor);
2723                 $relatedItem->appendChild($tor) if ($tor);
2724
2725                 if ( my ($part_isbn) = $part_mods->findnodes( './mods:identifier[@type="isbn"]' ) ) {
2726                         $part_isbn->setNamespace( "http://www.loc.gov/mods/", "mods" );
2727                         $part_isbn->setNamespace( "http://www.loc.gov/mods/", undef, 1 );
2728                         $part_isbn = $mods->ownerDocument->importNode($part_isbn);
2729                         $relatedItem->appendChild( $part_isbn );
2730
2731                         if (!$isbn) {
2732                                 $isbn = $mods->appendChild( $part_isbn->cloneNode(1) );
2733                         }
2734                 }
2735
2736                 $mods->appendChild( $relatedItem );
2737
2738         }
2739
2740         $_storage->disconnect;
2741
2742         return $U->entityize($mods->toString);
2743
2744 }
2745 __PACKAGE__->register_method(
2746         method    => 'retrieve_metarecord_mods',
2747         api_name  => 'open-ils.supercat.metarecord.mods.retrieve',
2748         api_level => 1,
2749         argc      => 1,
2750         signature =>
2751                 { desc     => <<"                 DESC",
2752 Returns the MODS representation of the requested metarecord
2753                   DESC
2754                   params   =>
2755                         [
2756                                 { name => 'metarecordId',
2757                                   desc => 'An OpenILS metabib::metarecord id',
2758                                   type => 'number' },
2759                         ],
2760                   'return' =>
2761                         { desc => 'The metarecord in MODS',
2762                           type => 'string' }
2763                 }
2764 );
2765
2766 sub list_metarecord_formats {
2767         my @list = (
2768                 { mods =>
2769                         { namespace_uri   => 'http://www.loc.gov/mods/',
2770                           docs            => 'http://www.loc.gov/mods/',
2771                           schema_location => 'http://www.loc.gov/standards/mods/mods.xsd',
2772                         }
2773                 }
2774         );
2775
2776         for my $type ( keys %metarecord_xslt ) {
2777                 push @list,
2778                         { $type => 
2779                                 { namespace_uri   => $metarecord_xslt{$type}{namespace_uri},
2780                                   docs            => $metarecord_xslt{$type}{docs},
2781                                   schema_location => $metarecord_xslt{$type}{schema_location},
2782                                 }
2783                         };
2784         }
2785
2786         return \@list;
2787 }
2788 __PACKAGE__->register_method(
2789         method    => 'list_metarecord_formats',
2790         api_name  => 'open-ils.supercat.metarecord.formats',
2791         api_level => 1,
2792         argc      => 0,
2793         signature =>
2794                 { desc     => <<"                 DESC",
2795 Returns the list of valid metarecord formats that supercat understands.
2796                   DESC
2797                   'return' =>
2798                         { desc => 'The format list',
2799                           type => 'array' }
2800                 }
2801 );
2802
2803
2804 sub list_authority_formats {
2805         my @list = (
2806                 { marcxml =>
2807                         { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
2808                           docs            => 'http://www.loc.gov/marcxml/',
2809                           schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
2810                         }
2811                 }
2812         );
2813
2814 #       for my $type ( keys %record_xslt ) {
2815 #               push @list,
2816 #                       { $type => 
2817 #                               { namespace_uri   => $record_xslt{$type}{namespace_uri},
2818 #                                 docs            => $record_xslt{$type}{docs},
2819 #                                 schema_location => $record_xslt{$type}{schema_location},
2820 #                               }
2821 #                       };
2822 #       }
2823 #
2824         return \@list;
2825 }
2826 __PACKAGE__->register_method(
2827         method    => 'list_authority_formats',
2828         api_name  => 'open-ils.supercat.authority.formats',
2829         api_level => 1,
2830         argc      => 0,
2831         signature =>
2832                 { desc     => <<"                 DESC",
2833 Returns the list of valid authority formats that supercat understands.
2834                   DESC
2835                   'return' =>
2836                         { desc => 'The format list',
2837                           type => 'array' }
2838                 }
2839 );
2840
2841 sub list_record_formats {
2842         my @list = (
2843                 { marcxml =>
2844                         { namespace_uri   => 'http://www.loc.gov/MARC21/slim',
2845                           docs            => 'http://www.loc.gov/marcxml/',
2846                           schema_location => 'http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd',
2847                         }
2848                 }
2849         );
2850
2851         for my $type ( keys %record_xslt ) {
2852                 push @list,
2853                         { $type => 
2854                                 { namespace_uri   => $record_xslt{$type}{namespace_uri},
2855                                   docs            => $record_xslt{$type}{docs},
2856                                   schema_location => $record_xslt{$type}{schema_location},
2857                                 }
2858                         };
2859         }
2860
2861         return \@list;
2862 }
2863 __PACKAGE__->register_method(
2864         method    => 'list_record_formats',
2865         api_name  => 'open-ils.supercat.record.formats',
2866         api_level => 1,
2867         argc      => 0,
2868         signature =>
2869                 { desc     => <<"                 DESC",
2870 Returns the list of valid record formats that supercat understands.
2871                   DESC
2872                   'return' =>
2873                         { desc => 'The format list',
2874                           type => 'array' }
2875                 }
2876 );
2877 __PACKAGE__->register_method(
2878         method    => 'list_record_formats',
2879         api_name  => 'open-ils.supercat.isbn.formats',
2880         api_level => 1,
2881         argc      => 0,
2882         signature =>
2883                 { desc     => <<"                 DESC",
2884 Returns the list of valid record formats that supercat understands.
2885                   DESC
2886                   'return' =>
2887                         { desc => 'The format list',
2888                           type => 'array' }
2889                 }
2890 );
2891
2892
2893 sub oISBN {
2894         my $self = shift;
2895         my $client = shift;
2896         my $isbn = shift;
2897
2898         $isbn =~ s/-//gso;
2899
2900         throw OpenSRF::EX::InvalidArg ('I need an ISBN please')
2901                 unless (length($isbn) >= 10);
2902
2903         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2904
2905         # Create a storage session, since we'll be making muliple requests.
2906         $_storage->connect;
2907
2908         # Find the record that has that ISBN.
2909         my $bibrec = $_storage->request(
2910                 'open-ils.cstore.direct.metabib.full_rec.search.atomic',
2911                 { tag => '020', subfield => 'a', value => { like => lc($isbn).'%'} }
2912         )->gather(1);
2913
2914         # Go away if we don't have one.
2915         return {} unless (@$bibrec);
2916
2917         # Find the metarecord for that bib record.
2918         my $mr = $_storage->request(
2919                 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
2920                 {source => $bibrec->[0]->record}
2921         )->gather(1);
2922
2923         # Find the other records for that metarecord.
2924         my $records = $_storage->request(
2925                 'open-ils.cstore.direct.metabib.metarecord_source_map.search.atomic',
2926                 {metarecord => $mr->[0]->metarecord}
2927         )->gather(1);
2928
2929         # Just to be safe.  There's currently no unique constraint on sources...
2930         my %unique_recs = map { ($_->source, 1) } @$records;
2931         my @rec_list = sort keys %unique_recs;
2932
2933         # And now fetch the ISBNs for thos records.
2934         my $recs = [];
2935         push @$recs,
2936                 $_storage->request(
2937                         'open-ils.cstore.direct.metabib.full_rec.search',
2938                         { tag => '020', subfield => 'a', record => $_ }
2939                 )->gather(1) for (@rec_list);
2940
2941         # We're done with the storage server session.
2942         $_storage->disconnect;
2943
2944         # Return the oISBN data structure.  This will be XMLized at a higher layer.
2945         return
2946                 { metarecord => $mr->[0]->metarecord,
2947                   record_list => { map { $_ ? ($_->record, $_->value) : () } @$recs } };
2948
2949 }
2950 __PACKAGE__->register_method(
2951         method    => 'oISBN',
2952         api_name  => 'open-ils.supercat.oisbn',
2953         api_level => 1,
2954         argc      => 1,
2955         signature =>
2956                 { desc     => <<"                 DESC",
2957 Returns the ISBN list for the metarecord of the requested isbn
2958                   DESC
2959                   params   =>
2960                         [
2961                                 { name => 'isbn',
2962                                   desc => 'An ISBN.  Duh.',
2963                                   type => 'string' },
2964                         ],
2965                   'return' =>
2966                         { desc => 'record to isbn map',
2967                           type => 'object' }
2968                 }
2969 );
2970
2971 sub return_bib_search_aliases {
2972     my %aliases;
2973
2974         my $_storage = OpenSRF::AppSession->create( 'open-ils.cstore' );
2975
2976         my $cmsa = $_storage->request(
2977                 'open-ils.cstore.direct.config.metabib_search_alias.search.atomic',
2978                 { alias => { '!=' => undef } }
2979         )->gather(1);
2980
2981     foreach (@$cmsa) {
2982         if ($_->alias =~ /\./) {
2983             my ($qualifier, $name) = $_->alias =~ m/^(.+?)\.(.+)$/;
2984             $aliases{$qualifier}{$name}{'index'} = $_->alias;
2985             # We will add a 'title' property in a subsequent schema
2986             $aliases{$qualifier}{$name}{'title'} = $name;
2987         } else {
2988             # au/kw/se/su/ti go into the default 'eg' qualifier
2989             $aliases{'eg'}{$_->alias}{'index'} = $_->alias;
2990             $aliases{'eg'}{$_->alias}{'title'} = $_->alias;
2991         }
2992     }
2993
2994     return \%aliases;
2995 }
2996
2997 __PACKAGE__->register_method(
2998         method    => 'return_bib_search_aliases',
2999         api_name  => 'open-ils.supercat.biblio.search_aliases',
3000         api_level => 1,
3001         argc      => 0,
3002         signature =>
3003                 { desc     => <<"                 DESC",
3004 Returns the set of qualified search aliases in the system
3005                   DESC
3006                   params   => [ ],
3007                   'return' =>
3008                         { desc => 'Hash of qualified search aliases',
3009                           type => 'object' }
3010                 }
3011 );
3012
3013
3014 package OpenILS::Application::SuperCat::unAPI;
3015 use base qw/OpenILS::Application::SuperCat/;
3016
3017 sub as_xml {
3018     die "dummy superclass, use a real class";
3019 }
3020
3021 sub new {
3022     my $class = shift;
3023     my $obj = shift;
3024     return unless ($obj);
3025
3026     $class = ref($class) || $class;
3027
3028     if ($class eq __PACKAGE__) {
3029         return unless (ref($obj));
3030         $class .= '::' . $obj->json_hint;
3031     }
3032
3033     return bless { obj => $obj } => $class;
3034 }
3035
3036 sub obj {
3037     my $self = shift;
3038     return $self->{obj};
3039 }
3040
3041 package OpenILS::Application::SuperCat::unAPI::auri;
3042 use base qw/OpenILS::Application::SuperCat::unAPI/;
3043
3044 sub as_xml {
3045     my $self = shift;
3046     my $args = shift;
3047
3048     my $xml = '      <uri xmlns="http://open-ils.org/spec/holdings/v1" ';
3049     $xml .= 'id="tag:open-ils.org:asset-uri/' . $self->obj->id . '" ';
3050     $xml .= 'use_restriction="' . $self->escape( $self->obj->use_restriction ) . '" ';
3051     $xml .= 'label="' . $self->escape( $self->obj->label ) . '" ';
3052     $xml .= 'href="' . $self->escape( $self->obj->href ) . '">';
3053
3054     if (!$args->{no_volumes}) {
3055         if (ref($self->obj->call_number_maps) && @{ $self->obj->call_number_maps }) {
3056             $xml .= "      <volumes>\n" . join(
3057                 '',
3058                 map {
3059                     OpenILS::Application::SuperCat::unAPI
3060                         ->new( $_->call_number )
3061                         ->as_xml({ %$args, no_uris=>1, no_copies=>1 })
3062                 } @{ $self->obj->call_number_maps }
3063             ) . "      </volumes>\n";
3064
3065         } else {
3066             $xml .= "      <volumes/>\n";
3067         }
3068     }
3069
3070     $xml .= "      </uri>\n";
3071
3072     return $xml;
3073 }
3074
3075 package OpenILS::Application::SuperCat::unAPI::acn;
3076 use base qw/OpenILS::Application::SuperCat::unAPI/;
3077
3078 sub as_xml {
3079     my $self = shift;
3080     my $args = shift;
3081
3082     my $xml = '    <volume xmlns="http://open-ils.org/spec/holdings/v1" ';
3083
3084     $xml .= 'id="tag:open-ils.org:asset-call_number/' . $self->obj->id . '" ';
3085     $xml .= 'lib="' . $self->escape( $self->obj->owning_lib->shortname ) . '" ';
3086     $xml .= 'opac_visible="' . $self->obj->owning_lib->opac_visible . '" ';
3087     $xml .= 'deleted="' . $self->obj->deleted . '" ';
3088     $xml .= 'label="' . $self->escape( $self->obj->label ) . '">';
3089     $xml .= "\n";
3090
3091     if (!$args->{no_copies}) {
3092         if (ref($self->obj->copies) && @{ $self->obj->copies }) {
3093             $xml .= "      <copies>\n" . join(
3094                 '',
3095                 map {
3096                     OpenILS::Application::SuperCat::unAPI
3097                         ->new( $_ )
3098                         ->as_xml({ %$args, no_volume=>1 })
3099                 } @{ $self->obj->copies }
3100             ) . "      </copies>\n";
3101
3102         } else {
3103             $xml .= "      <copies/>\n";
3104         }
3105     }
3106
3107     if (!$args->{no_uris}) {
3108         if (ref($self->obj->uri_maps) && @{ $self->obj->uri_maps }) {
3109             $xml .= "      <uris>\n" . join(
3110                 '',
3111                 map {
3112                     OpenILS::Application::SuperCat::unAPI
3113                         ->new( $_->uri )
3114                         ->as_xml({ %$args, no_volumes=>1 })
3115                 } @{ $self->obj->uri_maps }
3116             ) . "      </uris>\n";
3117
3118         } else {
3119             $xml .= "      <uris/>\n";
3120         }
3121     }
3122
3123
3124     $xml .= '      <prefix ';
3125     $xml .= 'ident="' . $self->obj->prefix->id . '" ';
3126     $xml .= 'id="tag:open-ils.org:asset-call_number_prefix/' . $self->obj->prefix->id . '" ';
3127     $xml .= 'label_sortkey="'.$self->escape( $self->obj->prefix->label_sortkey ) .'">';
3128     $xml .= $self->escape( $self->obj->prefix->label ) .'</prefix>';
3129     $xml .= "\n";
3130
3131     $xml .= '      <suffix ';
3132     $xml .= 'ident="' . $self->obj->suffix->id . '" ';
3133     $xml .= 'id="tag:open-ils.org:asset-call_number_suffix/' . $self->obj->suffix->id . '" ';
3134     $xml .= 'label_sortkey="'.$self->escape( $self->obj->suffix->label_sortkey ) .'">';
3135     $xml .= $self->escape( $self->obj->suffix->label ) .'</suffix>';
3136     $xml .= "\n";
3137
3138     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3139     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
3140     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
3141     $xml .= 'name="'.$self->escape( $self->obj->owning_lib->name ) .'"/>';
3142     $xml .= "\n";
3143
3144     unless ($args->{no_record}) {
3145         my $rec_tag = "tag:open-ils.org:biblio-record_entry/".$self->obj->record->id.'/'.$self->escape( $self->obj->owning_lib->shortname ) ;
3146
3147         my $r_doc = $parser->parse_string($self->obj->record->marc);
3148         $r_doc->documentElement->setAttribute( id => $rec_tag );
3149         $xml .= $U->entityize($r_doc->documentElement->toString);
3150     }
3151
3152     $xml .= "    </volume>\n";
3153
3154     return $xml;
3155 }
3156
3157 package OpenILS::Application::SuperCat::unAPI::ssub;
3158 use base qw/OpenILS::Application::SuperCat::unAPI/;
3159
3160 sub as_xml {
3161     my $self = shift;
3162     my $args = shift;
3163
3164     my $xml = '    <subscription xmlns="http://open-ils.org/spec/holdings/v1" ';
3165
3166     $xml .= 'id="tag:open-ils.org:serial-subscription/' . $self->obj->id . '" ';
3167     $xml .= 'start="' . $self->escape( $self->obj->start_date ) . '" ';
3168     $xml .= 'end="' . $self->escape( $self->obj->end_date ) . '" ';
3169     $xml .= 'expected_date_offset="' . $self->escape( $self->obj->expected_date_offset ) . '">';
3170     $xml .= "\n";
3171
3172     if (!$args->{no_distributions}) {
3173         if (ref($self->obj->distributions) && @{ $self->obj->distributions }) {
3174             $xml .= "      <distributions>\n" . join(
3175                 '',
3176                 map {
3177                     OpenILS::Application::SuperCat::unAPI
3178                         ->new( $_ )
3179                         ->as_xml({ %$args, no_subscription=>1, no_issuance=>1 })
3180                 } @{ $self->obj->distributions }
3181             ) . "      </distributions>\n";
3182
3183         } else {
3184             $xml .= "      <distributions/>\n";
3185         }
3186     }
3187
3188     if (!$args->{no_captions_and_patterns}) {
3189         if (ref($self->obj->scaps) && @{ $self->obj->scaps }) {
3190             $xml .= "      <captions_and_patterns>\n" . join(
3191                 '',
3192                 map {
3193                     OpenILS::Application::SuperCat::unAPI
3194                         ->new( $_ )
3195                         ->as_xml({ %$args, no_subscription=>1 })
3196                 } @{ $self->obj->scaps }
3197             ) . "      </captions_and_patterns>\n";
3198
3199         } else {
3200             $xml .= "      <captions_and_patterns/>\n";
3201         }
3202     }
3203
3204     if (!$args->{no_issuances}) {
3205         if (ref($self->obj->issuances) && @{ $self->obj->issuances }) {
3206             $xml .= "      <issuances>\n" . join(
3207                 '',
3208                 map {
3209                     OpenILS::Application::SuperCat::unAPI
3210                         ->new( $_ )
3211                         ->as_xml({ %$args, no_subscription=>1, no_items=>1 })
3212                 } @{ $self->obj->issuances }
3213             ) . "      </issuances>\n";
3214
3215         } else {
3216             $xml .= "      <issuances/>\n";
3217         }
3218     }
3219
3220
3221     $xml .= '      <owning_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3222     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->owning_lib->id . '" ';
3223     $xml .= 'shortname="'.$self->escape( $self->obj->owning_lib->shortname ) .'" ';
3224     $xml .= 'name="'.$self->escape( $self->obj->owning_lib->name ) .'"/>';
3225     $xml .= "\n";
3226
3227     unless ($args->{no_record}) {
3228         my $rec_tag = "tag:open-ils.org:biblio-record_entry/".$self->obj->record->id.'/'.$self->escape( $self->obj->owning_lib->shortname ) ;
3229
3230         my $r_doc = $parser->parse_string($self->obj->record_entry->marc);
3231         $r_doc->documentElement->setAttribute( id => $rec_tag );
3232         $xml .= $U->entityize($r_doc->documentElement->toString);
3233     }
3234
3235     $xml .= "    </subscription>\n";
3236
3237     return $xml;
3238 }
3239
3240 package OpenILS::Application::SuperCat::unAPI::ssum_base;
3241 use base qw/OpenILS::Application::SuperCat::unAPI/;
3242
3243 sub as_xml {
3244     my $self = shift;
3245     my $args = shift;
3246
3247     (my $type = ref($self)) =~ s/^.+([^:]+)$/$1/;
3248
3249     my $xml = "    <serial_summary xmlns=\"http://open-ils.org/spec/holdings/v1\" type=\"$type\" ";
3250
3251     $xml .= "id=\"tag:open-ils.org:serial-summary-$type/" . $self->obj->id . '" ';
3252     $xml .= 'generated_coverage="' . $self->escape( $self->obj->generated_coverage ) . '" ';
3253     $xml .= 'show_generated="' . $self->escape( $self->obj->show_generated ) . '" ';
3254     $xml .= 'textual_holdings="' . $self->escape( $self->obj->textual_holdings ) . '">';
3255     $xml .= "\n";
3256
3257         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->distribution )->as_xml({ %$args, no_summaries=>1 }) if (!$args->{no_distribution});
3258
3259     $xml .= "    </serial_summary>\n";
3260
3261     return $xml;
3262 }
3263
3264
3265 package OpenILS::Application::SuperCat::unAPI::sssum;
3266 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3267
3268 package OpenILS::Application::SuperCat::unAPI::sbsum;
3269 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3270
3271 package OpenILS::Application::SuperCat::unAPI::sisum;
3272 use base qw/OpenILS::Application::SuperCat::unAPI::ssum_base/;
3273
3274 package OpenILS::Application::SuperCat::unAPI::sdist;
3275 use base qw/OpenILS::Application::SuperCat::unAPI/;
3276
3277 sub as_xml {
3278     my $self = shift;
3279     my $args = shift;
3280
3281     my $xml = '    <distribution xmlns="http://open-ils.org/spec/holdings/v1" ';
3282
3283     $xml .= 'id="tag:open-ils.org:serial-distribution/' . $self->obj->id . '" ';
3284     $xml .= 'label="' . $self->escape( $self->obj->label ) . '" ';
3285     $xml .= 'unit_label_prefix="' . $self->escape( $self->obj->unit_label_prefix ) . '" ';
3286     $xml .= 'unit_label_suffix="' . $self->escape( $self->obj->unit_label_suffix ) . '">';
3287     $xml .= "\n";
3288
3289     if (!$args->{no_distributions}) {
3290         if (ref($self->obj->streams) && @{ $self->obj->streams }) {
3291             $xml .= "      <streams>\n" . join(
3292                 '',
3293                 map {
3294                     OpenILS::Application::SuperCat::unAPI
3295                         ->new( $_ )
3296                         ->as_xml({ %$args, no_distribution=>1 })
3297                 } @{ $self->obj->streams }
3298             ) . "      </streams>\n";
3299
3300         } else {
3301             $xml .= "      <streams/>\n";
3302         }
3303     }
3304
3305     if (!$args->{no_summaries}) {
3306         $xml .= "      <summaries>\n";
3307         $xml .= join ('',
3308         map {
3309             defined $_ ?
3310                 OpenILS::Application::SuperCat::unAPI
3311                 ->new( $_ )
3312                 ->as_xml({ %$args, no_distribution=>1 }) : ""
3313         } ($self->obj->basic_summary, $self->obj->supplement_summary, $self->obj->index_summary)
3314         );
3315
3316         $xml .= "      </summaries>\n";
3317     }
3318
3319
3320     $xml .= '      <holding_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3321     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->holding_lib->id . '" ';
3322     $xml .= 'shortname="'.$self->escape( $self->obj->holding_lib->shortname ) .'" ';
3323     $xml .= 'name="'.$self->escape( $self->obj->holding_lib->name ) .'"/>';
3324     $xml .= "\n";
3325
3326         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_distributions=>1 }) if (!$args->{no_subscription});
3327
3328     if (!$args->{no_record} && $self->obj->record_entry) {
3329         my $rec_tag = "tag:open-ils.org:serial-record_entry/".$self->obj->record_entry->id ;
3330
3331         my $r_doc = $parser->parse_string($self->obj->record_entry->marc);
3332         $r_doc->documentElement->setAttribute( id => $rec_tag );
3333         $xml .= $U->entityize($r_doc->documentElement->toString);
3334     }
3335
3336     $xml .= "    </distribution>\n";
3337
3338     return $xml;
3339 }
3340
3341 package OpenILS::Application::SuperCat::unAPI::sstr;
3342 use base qw/OpenILS::Application::SuperCat::unAPI/;
3343
3344 sub as_xml {
3345     my $self = shift;
3346     my $args = shift;
3347
3348     my $xml = '    <stream xmlns="http://open-ils.org/spec/holdings/v1" ';
3349
3350     $xml .= 'id="tag:open-ils.org:serial-stream/' . $self->obj->id . '" ';
3351     $xml .= 'routing_label="' . $self->escape( $self->obj->routing_label ) . '">';
3352     $xml .= "\n";
3353
3354     if (!$args->{no_items}) {
3355         if (ref($self->obj->items) && @{ $self->obj->items }) {
3356             $xml .= "      <items>\n" . join(
3357                 '',
3358                 map {
3359                     OpenILS::Application::SuperCat::unAPI
3360                         ->new( $_ )
3361                         ->as_xml({ %$args, no_stream=>1 })
3362                 } @{ $self->obj->items }
3363             ) . "      </items>\n";
3364
3365         } else {
3366             $xml .= "      <items/>\n";
3367         }
3368     }
3369
3370         #XXX routing_list_user's?
3371
3372         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->distribution )->as_xml({ %$args, no_streams=>1 }) if (!$args->{no_distribution});
3373
3374     $xml .= "    </stream>\n";
3375
3376     return $xml;
3377 }
3378
3379 package OpenILS::Application::SuperCat::unAPI::sitem;
3380 use base qw/OpenILS::Application::SuperCat::unAPI/;
3381
3382 sub as_xml {
3383     my $self = shift;
3384     my $args = shift;
3385
3386     my $xml = '    <serial_item xmlns="http://open-ils.org/spec/holdings/v1" ';
3387
3388     $xml .= 'id="tag:open-ils.org:serial-item/' . $self->obj->id . '" ';
3389     $xml .= 'date_expected="' . $self->escape( $self->obj->date_expected ) . '"';
3390     $xml .= ' date_received="' . $self->escape( $self->obj->date_received ) .'"'if ($self->obj->date_received);
3391
3392         if ($args->{no_issuance}) {
3393                 my $siss = ref($self->obj->issuance) ? $self->obj->issuance->id : $self->obj->issuance;
3394             $xml .= ' issuance="tag:open-ils.org:serial-issuance/' . $siss . '"';
3395         }
3396
3397     $xml .= ">\n";
3398
3399         if (ref($self->obj->notes) && $self->obj->notes) {
3400                 $xml .= "        <notes>\n";
3401                 for my $note ( @{$self->obj->notes} ) {
3402                         next unless ( $note->pub eq 't' );
3403                         $xml .= sprintf('        <note date="%s" title="%s">%s</note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3404                         $xml .= "\n";
3405                 }
3406                 $xml .= "        </notes>\n";
3407     } else {
3408         $xml .= "      <notes/>\n";
3409         }
3410
3411         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->issuance )->as_xml({ %$args, no_items=>1 }) if (!$args->{no_issuance});
3412         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->stream )->as_xml({ %$args, no_items=>1 }) if (!$args->{no_stream});
3413         $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});
3414         $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});
3415
3416     $xml .= "    </serial_item>\n";
3417
3418     return $xml;
3419 }
3420
3421 package OpenILS::Application::SuperCat::unAPI::sunit;
3422 use base qw/OpenILS::Application::SuperCat::unAPI/;
3423
3424 sub as_xml {
3425     my $self = shift;
3426     my $args = shift;
3427
3428     my $xml = '      <serial_unit xmlns="http://open-ils.org/spec/holdings/v1" '.
3429         'id="tag:open-ils.org:serial-unit/' . $self->obj->id . '" ';
3430
3431     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3432         create_date edit_date copy_number circulate deposit ref holdable deleted
3433         deposit_amount price barcode circ_modifier circ_as_type opac_visible cost
3434         status_changed_time floating mint_condition detailed_contents sort_key summary_contents
3435     /);
3436
3437     $xml .= ">\n";
3438
3439     $xml .= '        <status ident="' . $self->obj->status->id . '" opac_visible="' . $self->obj->status->opac_visible . '">' . $self->escape( $self->obj->status->name  ) . "</status>\n";
3440     $xml .= '        <location ident="' . $self->obj->location->id . '">' . $self->escape( $self->obj->location->name  ) . "</location>\n";
3441     $xml .= '        <circlib ident="' . $self->obj->circ_lib->id . '">' . $self->escape( $self->obj->circ_lib->name  ) . "</circlib>\n";
3442
3443     $xml .= '        <circ_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3444     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->circ_lib->id . '" ';
3445     $xml .= 'shortname="'.$self->escape( $self->obj->circ_lib->shortname ) .'" ';
3446     $xml .= 'name="'.$self->escape( $self->obj->circ_lib->name ) .'"/>';
3447     $xml .= "\n";
3448
3449         $xml .= "        <copy_notes>\n";
3450         if (ref($self->obj->notes) && $self->obj->notes) {
3451                 for my $note ( @{$self->obj->notes} ) {
3452                         next unless ( $note->pub eq 't' );
3453                         $xml .= sprintf('        <copy_note date="%s" title="%s">%s</copy_note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3454                         $xml .= "\n";
3455                 }
3456         }
3457
3458         $xml .= "        </copy_notes>\n";
3459     $xml .= "        <statcats>\n";
3460
3461         if (ref($self->obj->stat_cat_entries) && $self->obj->stat_cat_entries) {
3462                 for my $sce ( @{$self->obj->stat_cat_entries} ) {
3463                         next unless ( $sce->stat_cat->opac_visible eq 't' );
3464                         $xml .= sprintf('          <statcat name="%s">%s</statcat>',$self->escape($sce->stat_cat->name) ,$self->escape($sce->value));
3465                         $xml .= "\n";
3466                 }
3467         }
3468         $xml .= "        </statcats>\n";
3469
3470     unless ($args->{no_volume}) {
3471         if (ref($self->obj->call_number)) {
3472             $xml .= OpenILS::Application::SuperCat::unAPI
3473                         ->new( $self->obj->call_number )
3474                         ->as_xml({ %$args, no_copies=>1 });
3475         } else {
3476             $xml .= "    <volume/>\n";
3477         }
3478     }
3479
3480     $xml .= "      </serial_unit>\n";
3481
3482     return $xml;
3483 }
3484
3485 package OpenILS::Application::SuperCat::unAPI::scap;
3486 use base qw/OpenILS::Application::SuperCat::unAPI/;
3487
3488 sub as_xml {
3489     my $self = shift;
3490     my $args = shift;
3491
3492     my $xml = '      <caption_and_pattern xmlns="http://open-ils.org/spec/holdings/v1" '.
3493         'id="tag:open-ils.org:serial-caption_and_pattern/' . $self->obj->id . '" ';
3494
3495     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3496         create_date type active pattern_code enum_1 enum_2 enum_3 enum_4
3497                 enum_5 enum_6 chron_1 chron_2 chron_3 chron_4 chron_5 start_date end_date
3498     /);
3499     $xml .= ">\n";
3500         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_captions_and_patterns=>1 }) if (!$args->{no_subscription});
3501     $xml .= "      </caption_and_pattern>\n";
3502
3503     return $xml;
3504 }
3505
3506 package OpenILS::Application::SuperCat::unAPI::siss;
3507 use base qw/OpenILS::Application::SuperCat::unAPI/;
3508
3509 sub as_xml {
3510     my $self = shift;
3511     my $args = shift;
3512
3513     my $xml = '      <issuance xmlns="http://open-ils.org/spec/holdings/v1" '.
3514         'id="tag:open-ils.org:serial-issuance/' . $self->obj->id . '" ';
3515
3516     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" '
3517                 for (qw/create_date edit_date label date_published holding_code holding_type holding_link_id/);
3518
3519     $xml .= ">\n";
3520
3521     if (!$args->{no_items}) {
3522         if (ref($self->obj->items) && @{ $self->obj->items }) {
3523             $xml .= "      <items>\n" . join(
3524                 '',
3525                 map {
3526                     OpenILS::Application::SuperCat::unAPI
3527                         ->new( $_ )
3528                         ->as_xml({ %$args, no_stream=>1 })
3529                 } @{ $self->obj->items }
3530             ) . "      </items>\n";
3531
3532         } else {
3533             $xml .= "      <items/>\n";
3534         }
3535     }
3536
3537         $xml .= OpenILS::Application::SuperCat::unAPI->new( $self->obj->subscription )->as_xml({ %$args, no_issuances=>1 }) if (!$args->{no_subscription});
3538     $xml .= "      </issuance>\n";
3539
3540     return $xml;
3541 }
3542
3543 package OpenILS::Application::SuperCat::unAPI::acp;
3544 use base qw/OpenILS::Application::SuperCat::unAPI/;
3545
3546 sub as_xml {
3547     my $self = shift;
3548     my $args = shift;
3549
3550     my $xml = '      <copy xmlns="http://open-ils.org/spec/holdings/v1" '.
3551         'id="tag:open-ils.org:asset-copy/' . $self->obj->id . '" ';
3552
3553     $xml .= $_ . '="' . $self->escape( $self->obj->$_  ) . '" ' for (qw/
3554         create_date edit_date copy_number circulate deposit ref holdable deleted
3555         deposit_amount price barcode circ_modifier circ_as_type opac_visible
3556     /);
3557
3558     $xml .= ">\n";
3559
3560     $xml .= '        <status ident="' . $self->obj->status->id . '" opac_visible="' . $self->obj->status->opac_visible . '">' . $self->escape( $self->obj->status->name  ) . "</status>\n";
3561     $xml .= '        <location ident="' . $self->obj->location->id . '" opac_visible="'.$self->obj->location->opac_visible.'">' . $self->escape( $self->obj->location->name  ) . "</location>\n";
3562     $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";
3563
3564     $xml .= '        <circ_lib xmlns="http://open-ils.org/spec/actors/v1" ';
3565     $xml .= 'id="tag:open-ils.org:actor-org_unit/' . $self->obj->circ_lib->id . '" ';
3566     $xml .= 'shortname="'.$self->escape( $self->obj->circ_lib->shortname ) .'" ';
3567     $xml .= 'name="'.$self->escape( $self->obj->circ_lib->name ) .'" opac_visible="'.$self->obj->circ_lib->opac_visible.'"/>';
3568     $xml .= "\n";
3569
3570         $xml .= "        <monograph_parts>\n";
3571         if (ref($self->obj->parts) && $self->obj->parts) {
3572                 for my $part ( @{$self->obj->parts} ) {
3573                         $xml .= sprintf('        <monograph_part record="%s" sortkey="%s">%s</monograph_part>',$part->record, $self->escape($part->label_sortkey), $self->escape($part->label));
3574                         $xml .= "\n";
3575                 }
3576         }
3577
3578         $xml .= "        </monograph_parts>\n";
3579         $xml .= "        <copy_notes>\n";
3580         if (ref($self->obj->notes) && $self->obj->notes) {
3581                 for my $note ( @{$self->obj->notes} ) {
3582                         next unless ( $note->pub eq 't' );
3583                         $xml .= sprintf('        <copy_note date="%s" title="%s">%s</copy_note>',$note->create_date, $self->escape($note->title), $self->escape($note->value));
3584                         $xml .= "\n";
3585                 }
3586         }
3587
3588         $xml .= "        </copy_notes>\n";
3589     $xml .= "        <statcats>\n";
3590
3591         if (ref($self->obj->stat_cat_entries) && $self->obj->stat_cat_entries) {
3592                 for my $sce ( @{$self->obj->stat_cat_entries} ) {
3593                         next unless ( $sce->stat_cat->opac_visible eq 't' );
3594                         $xml .= sprintf('          <statcat name="%s">%s</statcat>',$self->escape($sce->stat_cat->name) ,$self->escape($sce->value));
3595                         $xml .= "\n";
3596                 }
3597         }
3598         $xml .= "        </statcats>\n";
3599
3600     unless ($args->{no_volume}) {
3601         if (ref($self->obj->call_number)) {
3602             $xml .= OpenILS::Application::SuperCat::unAPI
3603                         ->new( $self->obj->call_number )
3604                         ->as_xml({ %$args, no_copies=>1 });
3605         } else {
3606             $xml .= "    <volume/>\n";
3607         }
3608     }
3609
3610     $xml .= "      </copy>\n";
3611
3612     return $xml;
3613 }
3614
3615
3616 1;
3617 # vim: et:ts=4:sw=4