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