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