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