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