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