db78f30a73d34c2b0ca0eb1033f1c0b4b3e8d382
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / Publisher.pm
1 package OpenILS::Application::Storage::Publisher;
2 use base qw/OpenILS::Application::Storage/;
3 our $VERSION = 1;
4
5 use Digest::MD5 qw/md5_hex/;
6 use OpenSRF::EX qw/:try/;
7 use OpenSRF::Utils;
8 use OpenSRF::Utils::Logger qw/:level/;
9 use OpenILS::Utils::Fieldmapper;
10
11 my $log = 'OpenSRF::Utils::Logger';
12
13
14 sub register_method {
15     my $class = shift;
16     my %args = @_;
17     my %dup_args = %args;
18
19     $class = ref($class) || $class;
20
21     $args{package} ||= $class;
22     __PACKAGE__->SUPER::register_method( %args );
23
24     if (exists($dup_args{cachable}) and $dup_args{cachable}) {
25         (my $name = $dup_args{api_name}) =~ s/^open-ils\.storage/open-ils.storage.cachable/o;
26         if ($name ne $dup_args{api_name}) {
27             $dup_args{real_api_name} = $dup_args{api_name};
28             $dup_args{method} = 'cachable_wrapper';
29             $dup_args{api_name} = $name;
30             $dup_args{package} = __PACKAGE__;
31             __PACKAGE__->SUPER::register_method( %dup_args );
32         }
33     }
34
35     if ($dup_args{real_api_name} =~ /^open-ils\.storage\.direct\..+\.search.+/o ||
36         $dup_args{api_name} =~ /^open-ils\.storage\.direct\..+\.search.+/o) {
37         $dup_args{api_name} = $dup_args{real_api_name} if ($dup_args{real_api_name});
38
39         (my $name = $dup_args{api_name}) =~ s/\.direct\./.id_list./o;
40
41         $dup_args{notes} = $dup_args{real_api_name};
42         $dup_args{real_api_name} = $dup_args{api_name};
43         $dup_args{method} = 'search_ids';
44         $dup_args{api_name} = $name;
45         $dup_args{package} = __PACKAGE__;
46
47         __PACKAGE__->SUPER::register_method( %dup_args );
48     }
49 }
50
51 sub cachable_wrapper {
52     my $self = shift;
53     my $client = shift;
54     my @args = @_;
55
56     my %cache_args = (
57         limit       => 100,
58         offset      => 0,
59         timeout     => 7200,
60         cache_page_size => 1000,
61     );
62
63     my @real_args;
64     my $key_string = $self->api_name;
65     for (my $ind = 0; $ind < scalar(@args); $ind++) {
66         if (    $args[$ind] eq 'limit' ||
67             $args[$ind] eq 'offset' ||
68             $args[$ind] eq 'cache_page_size' ||
69             $args[$ind] eq 'timeout' ) {
70
71             my $key_ind = $ind;
72             $ind++;
73             my $value_ind = $ind;
74             $cache_args{$args[$key_ind]} = $args[$value_ind];
75             $log->debug("Cache limiter value for $args[$key_ind] is $args[$value_ind]", INTERNAL);
76             next;
77         }
78         $key_string .= $args[$ind];
79         $log->debug("Partial cache key value is $args[$ind]", INTERNAL);
80         push @real_args, $args[$ind];
81     }
82
83     my $cache_page = int($cache_args{offset} / $cache_args{cache_page_size});
84     my $cache_key;
85     {   use bytes;
86         $cache_key = md5_hex($key_string.$cache_page);
87     }
88
89     $log->debug("Key string for cache lookup is $key_string -> $cache_key", DEBUG);
90     $log->debug("Cache page is $cache_page", DEBUG);
91
92     my $cached_res = OpenSRF::Utils::Cache->new->get_cache( $cache_key );
93     if (defined $cached_res) {
94         $log->debug("Found ".scalar(@$cached_res)." records in the cache", INFO);
95         $log->debug("Values from cache: ".join(', ', @$cached_res), INTERNAL);
96         my $start = int($cache_args{offset} - ($cache_page * $cache_args{cache_page_size}));
97         my $end = int($start + $cache_args{limit} - 1);
98         $log->debug("Responding with values from ".$start.' to '.$end,DEBUG);
99             $client->respond( $_ ) for ( grep { defined } @$cached_res[ $start .. $end ]);
100         return undef;
101     }
102
103     my $method = $self->method_lookup($self->{real_api_name});
104     my @res = $method->run(@real_args);
105
106
107         $client->respond( $_ ) for ( grep { defined } @res[$cache_args{offset} .. int($cache_args{offset} + $cache_args{limit} - 1)] );
108
109     $log->debug("Saving values from ".int($cache_page * $cache_args{cache_page_size})." to ".
110         int(($cache_page + 1) * $cache_args{cache_page_size}). "to the cache", INTERNAL);
111     try {
112         OpenSRF::Utils::Cache->new->put_cache(
113             $cache_key =>
114             [@res[int($cache_page * $cache_args{cache_page_size}) .. int(($cache_page + 1) * $cache_args{cache_page_size}) ]] =>
115             OpenSRF::Utils->interval_to_seconds( $cache_args{timeout} )
116         );
117     } catch Error with {
118         my $e = shift;
119         $log->error("Cache seems to be down, $e");
120     };
121
122     return undef;
123 }
124
125 sub random_object {
126     my $self = shift;
127     my $client = shift;
128
129     my $cdbi = $self->{cdbi};
130     my $table = $cdbi->table;
131     my $sql = <<"    SQL";
132         SELECT  id
133           FROM  $table
134           WHERE id IN (( SELECT (RANDOM() * (SELECT MAX(id) FROM $table))::INT LIMIT 1 ));
135     SQL
136
137     my $trys = 100;
138     while ($trys--) {
139
140         my $id = $cdbi->db_Main->selectcol_arrayref($sql);
141         next unless (@$id);
142
143         return ($cdbi->fast_fieldmapper(@$id))[0];
144     }
145     return undef;
146 }
147
148 sub retrieve_node {
149     my $self = shift;
150     my $client = shift;
151     my @ids = @_;
152
153     my $cdbi = $self->{cdbi};
154
155     for my $id ( @ids ) {
156         next unless ($id);
157
158         my ($rec) = $cdbi->fast_fieldmapper($id);
159         if ($self->api_name !~ /batch/o) {
160             return $rec if ($rec);
161         }
162         $client->respond($rec);
163     }
164     return undef;
165 }
166
167 sub search_ids {
168     my $self = shift;
169     my $client = shift;
170     my @args = @_;
171
172     my @res = $self->method_lookup($self->{real_api_name})->run(@args);
173
174     if (ref($res[0]) eq 'ARRAY') {
175         return [ map { $_->id } @{ $res[0] } ];
176     }
177
178     $client->respond($_) for ( map { $_->id } @res );
179     return undef;
180 }
181
182 sub search_where {
183     my $self = shift;
184     my $client = shift;
185     my @args = @_;
186
187     if (ref($args[0]) eq 'HASH') {
188         if ($args[1]) {
189             $args[1]{limit_dialect} = $self->{cdbi}->db_Main;
190         } else {
191             $args[1] = {limit_dialect => $self->{cdbi}->db_Main };
192         }
193     } else {
194         $args[0] = { @args };
195         $args[1] = {limit_dialect => $self->{cdbi} };
196     }
197
198     my $cdbi = $self->{cdbi};
199
200     for my $obj ($cdbi->search_where(@args)) {
201         next unless ref($obj);
202         $client->respond( $obj->to_fieldmapper );
203     }
204     return undef;
205 }
206
207 sub search {
208     my $self = shift;
209     my $client = shift;
210     my @args = @_;
211
212     my $cdbi = $self->{cdbi};
213
214     (my $search_type = $self->api_name) =~ s/.*\.(search[^.]*).*/$1/o;
215
216     for my $obj ($cdbi->$search_type(@args)) {
217         next unless ref($obj);
218         $client->respond( $obj->to_fieldmapper );
219     }
220     return undef;
221 }
222
223 sub search_one_field {
224     my $self = shift;
225     my $client = shift;
226     my @args = @_;
227
228     (my $field = $self->api_name) =~ s/.*\.([^\.]+)$/$1/o;
229
230     return search( $self, $client, $field, @args );
231 }
232
233 sub old_search_one_field {
234     my $self = shift;
235     my $client = shift;
236     my @terms = @_;
237
238     (my $search_type = $self->api_name) =~ s/.*\.(search[^.]*).*/$1/o;
239     (my $col = $self->api_name) =~ s/.*\.$search_type\.([^.]+).*/$1/;
240     my $cdbi = $self->{cdbi};
241
242     my $like = 0;
243     $like = 1 if ($search_type =~ /like$/o);
244     $like = 2 if ($search_type =~ /fts$/o);
245     $like = 3 if ($search_type =~ /regex$/o);
246
247     for my $term (@terms) {
248         $log->debug("Searching $cdbi for $col using type $search_type, value '$term'",DEBUG);
249         if (@terms == 1) {
250             return [ $cdbi->fast_fieldmapper($term,$col,$like) ];
251         }
252         $client->respond( [ $cdbi->fast_fieldmapper($term,$col,$like) ] );
253     }
254     return undef;
255 }
256
257
258 sub create_node {
259     my $self = shift;
260     my $client = shift;
261     my $node = shift;
262
263     local $OpenILS::Application::Storage::WRITE = 1;
264
265     my $cdbi = $self->{cdbi};
266
267     my $success;
268     try {
269         my $rec = $cdbi->create($node);
270         $success = $rec->id if ($rec);
271     } catch Error with {
272         $success = 0;
273     };
274
275     return $success;
276 }
277
278 sub update_node {
279     my $self = shift;
280     my $client = shift;
281     my $node = shift;
282
283     local $OpenILS::Application::Storage::WRITE = 1;
284
285     my $cdbi = $self->{cdbi};
286
287     return $cdbi->update($node);
288 }
289
290 sub mass_delete {
291     my $self = shift;
292     my $client = shift;
293     my $search = shift;
294
295     local $OpenILS::Application::Storage::WRITE = 1;
296
297     my $where = 'WHERE ';
298
299     my $cdbi = $self->{cdbi};
300     my $table = $cdbi->table;
301
302     my @keys = sort keys %$search;
303     
304     my @binds;
305     my @wheres;
306     for my $col ( @keys ) {
307         if (ref($$search{$col}) and ref($$search{$col}) =~ /ARRAY/o) {
308             push @wheres, "$col IN (" . join(',', map { '?' } @{ $$search{$col} }) . ')';
309             push @binds, map { "$_" } @{ $$search{$col} };
310         } else {
311             push @wheres, "$col = ?";
312             push @binds, $$search{$col};
313         }
314     }
315     $where .= join ' AND ', @wheres;
316
317     my $delete = "DELETE FROM $table $where";
318
319     $log->debug("Performing MASS deletion : $delete",DEBUG);
320
321     my $dbh = $cdbi->db_Main;
322     my $success = 1;
323     try {
324         my $sth = $dbh->prepare($delete);
325         $sth->execute( @binds );
326         $sth->finish;
327         $log->debug("MASS Delete succeeded",DEBUG);
328     } catch Error with {
329         $log->debug("MASS Delete FAILED : ".shift(),DEBUG);
330         $success = 0;
331     };
332     return $success;
333 }
334
335 sub merge_node {
336     my $self = shift;
337     my $client = shift;
338     my $keys = shift;
339     my $vals = shift;
340
341     local $OpenILS::Application::Storage::WRITE = 1;
342
343     my $cdbi = $self->{cdbi};
344
345     my $success = 1;
346     try {
347         $success = $cdbi->merge($keys,$vals)->id;
348     } catch Error with {
349         $success = 0;
350     };
351     return $success;
352 }
353
354 sub delete_node {
355     my $self = shift;
356     my $client = shift;
357     my $node = shift;
358
359     local $OpenILS::Application::Storage::WRITE = 1;
360
361     my $cdbi = $self->{cdbi};
362
363     my $success = 1;
364     try {
365         $success = $cdbi->delete($node);
366     } catch Error with {
367         $success = 0;
368     };
369     return $success;
370 }
371
372 sub batch_call {
373     my $self = shift;
374     my $client = shift;
375     my @nodes = @_;
376
377     my $unwrap = $self->{unwrap};
378
379     my $cdbi = $self->{cdbi};
380     my $api_name = $self->api_name;
381     (my $single_call_api_name = $api_name) =~ s/batch\.//o;
382
383     $log->debug("Default $api_name looking up $single_call_api_name...",INTERNAL);
384     my $method = $self->method_lookup($single_call_api_name);
385
386     my @success;
387     while ( my $node = shift(@nodes) ) {
388         my ($res) = $method->run( ($unwrap ? (@$node) : ($node)) ); 
389         push(@success, 1) if ($res >= 0);
390     }
391
392     my $insert_total = 0;
393     $insert_total += $_ for (@success);
394
395     return $insert_total;
396 }
397
398
399 # --------------------- End of generic methods -----------------------
400
401
402 for my $pkg ( qw/actor action asset biblio config metabib authority money permission container/ ) {
403     "OpenILS::Application::Storage::Publisher::$pkg"->use;
404     if ($@) {
405         $log->debug("ARG! Couldn't load $pkg class Publisher: $@", ERROR);
406         throw OpenSRF::EX::ERROR ("ARG! Couldn't load $pkg class Publisher: $@");
407     }
408 }
409
410 for my $fmclass ( (Fieldmapper->classes) ) {
411
412     $log->debug("Generating methods for Fieldmapper class $fmclass", DEBUG);
413
414     next if ($fmclass->is_virtual);
415
416     (my $cdbi = $fmclass) =~ s/^Fieldmapper:://o;
417     (my $class = $cdbi) =~ s/::.*//o;
418     (my $api_class = $cdbi) =~ s/::/./go;
419     my $registration_class = __PACKAGE__ . "::$class";
420     my $api_prefix = 'open-ils.storage.direct.'.$api_class;
421
422     # Create the search methods
423     unless ( __PACKAGE__->is_registered( $api_prefix.'.search' ) ) {
424         __PACKAGE__->register_method(
425             api_name    => $api_prefix.'.search',
426             method      => 'search',
427             api_level   => 1,
428             argc        => 2,
429             stream      => 1,
430             cdbi        => $cdbi,
431             cachable    => 1,
432         );
433     }
434
435     unless ( __PACKAGE__->is_registered( $api_prefix.'.search_where' ) ) {
436         __PACKAGE__->register_method(
437             api_name    => $api_prefix.'.search_where',
438             method      => 'search_where',
439             api_level   => 1,
440             stream      => 1,
441             argc        => 1,
442             cdbi        => $cdbi,
443             cachable    => 1,
444         );
445     }
446
447 =head1 comment
448
449     unless ( __PACKAGE__->is_registered( $api_prefix.'.search_like' ) ) {
450         __PACKAGE__->register_method(
451             api_name    => $api_prefix.'.search_like',
452             method      => 'search',
453             api_level   => 1,
454             stream      => 1,
455             cdbi        => $cdbi,
456             cachable    => 1,
457             argc        => 2,
458         );
459     }
460
461     if (\&Class::DBI::search_fts and $cdbi->columns('FTS')) {
462         unless ( __PACKAGE__->is_registered( $api_prefix.'.search_fts' ) ) {
463             __PACKAGE__->register_method(
464                 api_name    => $api_prefix.'.search_fts',
465                 method      => 'search',
466                 api_level   => 1,
467                 stream      => 1,
468                 cdbi        => $cdbi,
469                 cachable    => 1,
470                 argc        => 2,
471             );
472         }
473     }
474
475     if (\&Class::DBI::search_regex) {
476         unless ( __PACKAGE__->is_registered( $api_prefix.'.search_regex' ) ) {
477             __PACKAGE__->register_method(
478                 api_name    => $api_prefix.'.search_regex',
479                 method      => 'search',
480                 api_level   => 1,
481                 stream      => 1,
482                 cdbi        => $cdbi,
483                 cachable    => 1,
484                 argc        => 2,
485             );
486         }
487     }
488
489     if (\&Class::DBI::search_ilike) {
490         unless ( __PACKAGE__->is_registered( $api_prefix.'.search_ilike' ) ) {
491             __PACKAGE__->register_method(
492                 api_name    => $api_prefix.'.search_ilike',
493                 method      => 'search',
494                 api_level   => 1,
495                 stream      => 1,
496                 cdbi        => $cdbi,
497                 cachable    => 1,
498                 argc        => 2,
499             );
500         }
501     }
502
503 =cut
504
505     # Create the random method
506     unless ( __PACKAGE__->is_registered( $api_prefix.'.random' ) ) {
507         __PACKAGE__->register_method(
508             api_name    => $api_prefix.'.random',
509             method      => 'random_object',
510             api_level   => 1,
511             cdbi        => $cdbi,
512             argc        => 0,
513         );
514     }
515
516     # Create the retrieve method
517     unless ( __PACKAGE__->is_registered( $api_prefix.'.retrieve' ) ) {
518         __PACKAGE__->register_method(
519             api_name    => $api_prefix.'.retrieve',
520             method      => 'retrieve_node',
521             api_level   => 1,
522             cdbi        => $cdbi,
523             cachable    => 1,
524             argc        => 1,
525         );
526     }
527
528     # Create the batch retrieve method
529     unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.retrieve' ) ) {
530         __PACKAGE__->register_method(
531             api_name    => $api_prefix.'.batch.retrieve',
532             method      => 'retrieve_node',
533             api_level   => 1,
534             stream      => 1,
535             cdbi        => $cdbi,
536             cachable    => 1,
537             argc        => 1,
538         );
539     }
540
541     for my $field ($fmclass->real_fields) {
542         unless ( __PACKAGE__->is_registered( $api_prefix.'.search.'.$field ) ) {
543             __PACKAGE__->register_method(
544                 api_name    => $api_prefix.'.search.'.$field,
545                 method      => 'search_one_field',
546                 api_level   => 1,
547                 cdbi        => $cdbi,
548                 cachable    => 1,
549                 stream      => 1,
550                 argc        => 1,
551             );
552         }
553
554 =head1 comment
555
556         unless ( __PACKAGE__->is_registered( $api_prefix.'.search_like.'.$field ) ) {
557             __PACKAGE__->register_method(
558                 api_name    => $api_prefix.'.search_like.'.$field,
559                 method      => 'search_one_field',
560                 api_level   => 1,
561                 cdbi        => $cdbi,
562                 cachable    => 1,
563                 stream      => 1,
564                 argc        => 1,
565             );
566         }
567         if (\&Class::DBI::search_fts and grep { $field eq $_ } $cdbi->columns('FTS')) {
568             unless ( __PACKAGE__->is_registered( $api_prefix.'.search_fts.'.$field ) ) {
569                 __PACKAGE__->register_method(
570                     api_name    => $api_prefix.'.search_fts.'.$field,
571                     method      => 'search_one_field',
572                     api_level   => 1,
573                     cdbi        => $cdbi,
574                     cachable    => 1,
575                     stream      => 1,
576                     argc        => 1,
577                 );
578             }
579         }
580         if (\&Class::DBI::search_regex) {
581             unless ( __PACKAGE__->is_registered( $api_prefix.'.search_regex.'.$field ) ) {
582                 __PACKAGE__->register_method(
583                     api_name    => $api_prefix.'.search_regex.'.$field,
584                     method      => 'search_one_field',
585                     api_level   => 1,
586                     cdbi        => $cdbi,
587                     cachable    => 1,
588                     stream      => 1,
589                     argc        => 1,
590                 );
591             }
592         }
593         if (\&Class::DBI::search_ilike) {
594             unless ( __PACKAGE__->is_registered( $api_prefix.'.search_ilike.'.$field ) ) {
595                 __PACKAGE__->register_method(
596                     api_name    => $api_prefix.'.search_ilike.'.$field,
597                     method      => 'search_one_field',
598                     api_level   => 1,
599                     cdbi        => $cdbi,
600                     cachable    => 1,
601                     stream      => 1,
602                     argc        => 1,
603                 );
604             }
605         }
606
607 =cut
608
609     }
610
611
612     unless ($fmclass->is_readonly) {
613         # Create the create method
614         unless ( __PACKAGE__->is_registered( $api_prefix.'.create' ) ) {
615             __PACKAGE__->register_method(
616                 api_name    => $api_prefix.'.create',
617                 method      => 'create_node',
618                 api_level   => 1,
619                 cdbi        => $cdbi,
620                 argc        => 1,
621             );
622         }
623
624         # Create the batch create method
625         unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.create' ) ) {
626             __PACKAGE__->register_method(
627                 api_name    => $api_prefix.'.batch.create',
628                 method      => 'batch_call',
629                 api_level   => 1,
630                 cdbi        => $cdbi,
631                 argc        => 1,
632             );
633         }
634
635         # Create the update method
636         unless ( __PACKAGE__->is_registered( $api_prefix.'.update' ) ) {
637             __PACKAGE__->register_method(
638                 api_name    => $api_prefix.'.update',
639                 method      => 'update_node',
640                 api_level   => 1,
641                 cdbi        => $cdbi,
642                 argc        => 1,
643             );
644         }
645
646         # Create the batch update method
647         unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.update' ) ) {
648             __PACKAGE__->register_method(
649                 api_name    => $api_prefix.'.batch.update',
650                 method      => 'batch_call',
651                 api_level   => 1,
652                 cdbi        => $cdbi,
653                 argc        => 1,
654             );
655         }
656
657         # Create the delete method
658         unless ( __PACKAGE__->is_registered( $api_prefix.'.delete' ) ) {
659             __PACKAGE__->register_method(
660                 api_name    => $api_prefix.'.delete',
661                 method      => 'delete_node',
662                 api_level   => 1,
663                 cdbi        => $cdbi,
664                 argc        => 1,
665             );
666         }
667
668         # Create the batch delete method
669         unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.delete' ) ) {
670             __PACKAGE__->register_method(
671                 api_name    => $api_prefix.'.batch.delete',
672                 method      => 'batch_call',
673                 api_level   => 1,
674                 cdbi        => $cdbi,
675                 argc        => 1,
676             );
677         }
678
679         # Create the merge method
680         unless ( __PACKAGE__->is_registered( $api_prefix.'.merge' ) ) {
681             __PACKAGE__->register_method(
682                 api_name    => $api_prefix.'.merge',
683                 method      => 'merge_node',
684                 api_level   => 1,
685                 cdbi        => $cdbi,
686                 argc        => 1,
687             );
688         }
689
690         # Create the batch merge method
691         unless ( __PACKAGE__->is_registered( $api_prefix.'.batch.merge' ) ) {
692             __PACKAGE__->register_method(
693                 api_name    => $api_prefix.'.batch.merge',
694                 method      => 'batch_call',
695                 unwrap      => 1,
696                 api_level   => 1,
697                 cdbi        => $cdbi,
698                 argc        => 1,
699             );
700         }
701
702         # Create the search-based mass delete method
703         unless ( __PACKAGE__->is_registered( $api_prefix.'.mass_delete' ) ) {
704             __PACKAGE__->register_method(
705                 api_name    => $api_prefix.'.mass_delete',
706                 method      => 'mass_delete',
707                 api_level   => 1,
708                 cdbi        => $cdbi,
709                 argc        => 1,
710             );
711         }
712     }
713 }
714
715 1;