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