teach interval_to_seconds about negative intervals
[OpenSRF.git] / src / perl / lib / OpenSRF / Utils.pm
1 package OpenSRF::Utils;
2
3 =head1 NAME 
4
5 OpenSRF::Utils
6
7 =head1 DESCRIPTION 
8
9 This is a container package for methods that are useful to derived modules.
10 It has no constructor, and is generally not useful by itself... but this
11 is where most of the generic methods live.
12  
13
14 =head1 METHODS 
15
16
17 =cut
18
19 use vars qw/@ISA $AUTOLOAD %EXPORT_TAGS @EXPORT_OK @EXPORT $VERSION/;
20 push @ISA, 'Exporter';
21
22 $VERSION = do { my @r=(q$Revision$=~/\d+/g); sprintf "%d."."%02d"x$#r,@r };
23
24 use Time::Local;
25 use Errno;
26 use POSIX;
27 use FileHandle;
28 #use Cache::FileCache;
29 #use Storable qw(dclone);
30 use Digest::MD5 qw(md5 md5_hex md5_base64);
31 use Exporter;
32 use DateTime;
33 use DateTime::Format::ISO8601;
34 use DateTime::TimeZone;
35
36 our $date_parser = DateTime::Format::ISO8601->new;
37
38 # This turns errors into warnings, so daemons don't die.
39 #$Storable::forgive_me = 1;
40
41 %EXPORT_TAGS = (
42         common          => [qw(interval_to_seconds seconds_to_interval sendmail tree_filter)],
43         daemon          => [qw(safe_fork set_psname daemonize)],
44         datetime        => [qw(clense_ISO8601 gmtime_ISO8601 interval_to_seconds seconds_to_interval)],
45 );
46
47 Exporter::export_ok_tags('common','daemon','datetime');  # add aa, cc and dd to @EXPORT_OK
48
49 sub AUTOLOAD {
50         my $self = shift;
51         my $type = ref($self) or return undef;
52
53         my $name = $AUTOLOAD;
54         $name =~ s/.*://;   # strip fully-qualified portion
55
56         if (defined($_[0])) {
57                 return $self->{$name} = shift;
58         }
59         return $self->{$name};
60 }
61
62
63 sub _sub_builder {
64         my $self = shift;
65         my $class = ref($self) || $self;
66         my $part = shift;
67         unless ($class->can($part)) {
68                 *{$class.'::'.$part} =
69                         sub {
70                                 my $self = shift;
71                                 my $new_val = shift;
72                                 if ($new_val) {
73                                         $$self{$part} = $new_val;
74                                 }
75                                 return $$self{$part};
76                 };
77         }
78 }
79
80 sub tree_filter {
81         my $tree = shift;
82         my $field = shift;
83         my $filter = shift;
84
85         my @things = $filter->($tree);
86         for my $v ( @{$tree->$field} ){
87                 push @things, $filter->($v);
88                 push @things, tree_filter($v, $field, $filter);
89         }
90         return @things
91 }
92
93 #sub standalone_ipc_cache {
94 #       my $self = shift;
95 #       my $class = ref($self) || $self;
96 #       my $uniquifier = shift || return undef;
97 #       my $expires = shift || 3600;
98
99 #       return new Cache::FileCache ( { namespace => $class.'::'.$uniquifier, default_expires_in => $expires } );
100 #}
101
102 sub sendmail {
103         my $self = shift;
104         my $message = shift || $self;
105
106         open SM, '|/usr/sbin/sendmail -U -t' or return 0;
107         print SM $message;
108         close SM or return 0;
109         return 1;
110 }
111
112 sub __strip_comments {
113         my $self = shift;
114         my $config_file = shift;
115         my ($line, @done);
116         while (<$config_file>) {
117                 s/^\s*(.*)\s*$/$1/o if (lc($$self{keep_space}) ne 'true');
118                 /^(.*)$/o;
119                 $line .= $1;
120                 # keep new lines if keep_space is true
121                 if ($line =~ /^$/o && (lc($$self{keep_space}) ne 'true')) {
122                         $line = '';
123                         next;
124                 }
125                 if (/^([^<]+)\s*<<\s*(\w+)\s*$/o) {
126                         $line = "$1 = ";
127                         my $breaker = $2;
128                         while (<$config_file>) {
129                                 chomp;
130                                 last if (/^$breaker/);
131                                 $line .= $_;
132                         }
133                 }
134
135                 if ($line =~ /^#/ && $line !~ /^#\s*include\s+/o) {
136                         $line = '';
137                         next;
138                 }
139                 if ($line =~ /\\$/o) {
140                         chomp $line;
141                         $line =~ s/^\s*(.*)\s*\\$/$1/o;
142                         next;
143                 }
144                 push @done, $line;
145                 $line = '';
146         }
147         return @done;
148 }
149
150
151 =head2 $thing->encrypt(@stuff)
152
153 Returns a one way hash (MD5) of the values appended together.
154
155 =cut
156
157 sub encrypt {
158         my $self = shift;
159         return md5_hex(join('',@_));
160 }
161
162 =head2 $utils_obj->es_time('field') OR noo_es_time($timestamp)
163
164 Returns the epoch-second style timestamp for the value stored in
165 $utils_obj->{field}.  Returns B<0> for an empty or invalid date stamp, and
166 assumes a PostgreSQL style datestamp to be supplied.
167
168 =cut
169
170 sub es_time {
171         my $self = shift;
172         my $part = shift;
173         my $es_part = $part.'_ES';
174         return $$self{$es_part} if (exists($$self{$es_part}) && defined($$self{$es_part}) && $$self{$es_part});
175         if (!$$self{$part} or $$self{$part} !~ /\d+/) {
176                 return 0;
177
178         }
179         my @tm = reverse($$self{$part} =~ /([\d\.]+)/og);
180         if ($tm[5] > 0) {
181                 $tm[5] -= 1;
182         }
183
184         return $$self{$es_part} = noo_es_time($$self{$part});
185 }
186
187 =head2 noo_es_time($timestamp) (non-OO es_time)
188
189 Returns the epoch-second style timestamp for the B<$timestamp> passed
190 in.  Returns B<0> for an empty or invalid date stamp, and
191 assumes a PostgreSQL style datestamp to be supplied.
192
193 =cut
194
195 sub noo_es_time {
196         my $timestamp = shift;
197
198         my @tm = reverse($timestamp =~ /([\d\.]+)/og);
199         if ($tm[5] > 0) {
200                 $tm[5] -= 1;
201         }
202         return timelocal(int($tm[1]), int($tm[2]), int($tm[3]), int($tm[4]) || 1, int($tm[5]), int($tm[6]) || 2002 );
203 }
204
205
206 =head2 $thing->interval_to_seconds('interval') OR interval_to_seconds('interval')
207
208 =head2 $thing->seconds_to_interval($seconds) OR seconds_to_interval($seconds)
209
210 Returns the number of seconds for any interval passed, or the interval for the seconds.
211 This is the generic version of B<interval> listed below.
212
213 The interval must match the regex I</\s*\+?\s*(\d+)\s*(\w{1})\w*\s*/g>, for example
214 B<2 weeks, 3 d and 1hour + 17 Months> or
215 B<1 year, 5 Months, 2 weeks, 3 days and 1 hour of seconds> meaning 46148400 seconds.
216
217         my $expire_time = time() + $thing->interval_to_seconds('17h 9m');
218
219 The time size indicator may be one of
220
221 =over 2
222
223 =item s[econd[s]]
224
225 for seconds
226
227 =item m[inute[s]]
228
229 for minutes
230
231 =item h[our[s]]
232
233 for hours
234
235 =item d[ay[s]]
236
237 for days
238
239 =item w[eek[s]]
240
241 for weeks
242
243 =item M[onth[s]]
244
245 for months (really (365 * 1d)/12 ... that may get smarter, though)
246
247 =item y[ear[s]]
248
249 for years (this is 365 * 1d)
250
251 =back
252
253 =cut
254 sub interval_to_seconds {
255         my $self = shift;
256         my $interval = shift || $self;
257
258         $interval =~ s/and/,/g;
259         $interval =~ s/,/ /g;
260
261         my $amount = 0;
262         while ($interval =~ /\s*([\+-]?)\s*(\d+)\s*(\w+)\s*/g) {
263                 my ($sign, $count, $type) = ($1, $2, $3);
264                 $count = "$sign$count" if ($sign);
265                 $amount += $count if ($type eq 's');
266                 $amount += 60 * $count if ($type =~ /^m(?!o)/oi);
267                 $amount += 60 * 60 * $count if ($type =~ /^h/);
268                 $amount += 60 * 60 * 24 * $count if ($type =~ /^d/oi);
269                 $amount += 60 * 60 * 24 * 7 * $count if ($2 =~ /^w/oi);
270                 $amount += ((60 * 60 * 24 * 365)/12) * $count if ($type =~ /^mo/io);
271                 $amount += 60 * 60 * 24 * 365 * $count if ($type =~ /^y/oi);
272         }
273         return $amount;
274 }
275
276 sub seconds_to_interval {
277         my $self = shift;
278         my $interval = shift || $self;
279
280         my $limit = shift || 's';
281         $limit =~ s/^(.)/$1/o;
282
283         my ($y,$ym,$M,$Mm,$w,$wm,$d,$dm,$h,$hm,$m,$mm,$s,$string);
284         my ($year, $month, $week, $day, $hour, $minute, $second) =
285                 ('year','Month','week','day', 'hour', 'minute', 'second');
286
287         if ($y = int($interval / (60 * 60 * 24 * 365))) {
288                 $string = "$y $year". ($y > 1 ? 's' : '');
289                 $ym = $interval % (60 * 60 * 24 * 365);
290         } else {
291                 $ym = $interval;
292         }
293         return $string if ($limit eq 'y');
294
295         if ($M = int($ym / ((60 * 60 * 24 * 365)/12))) {
296                 $string .= ($string ? ', ':'')."$M $month". ($M > 1 ? 's' : '');
297                 $Mm = $ym % ((60 * 60 * 24 * 365)/12);
298         } else {
299                 $Mm = $ym;
300         }
301         return $string if ($limit eq 'M');
302
303         if ($w = int($Mm / 604800)) {
304                 $string .= ($string ? ', ':'')."$w $week". ($w > 1 ? 's' : '');
305                 $wm = $Mm % 604800;
306         } else {
307                 $wm = $Mm;
308         }
309         return $string if ($limit eq 'w');
310
311         if ($d = int($wm / 86400)) {
312                 $string .= ($string ? ', ':'')."$d $day". ($d > 1 ? 's' : '');
313                 $dm = $wm % 86400;
314         } else {
315                 $dm = $wm;
316         }
317         return $string if ($limit eq 'd');
318
319         if ($h = int($dm / 3600)) {
320                 $string .= ($string ? ', ' : '')."$h $hour". ($h > 1 ? 's' : '');
321                 $hm = $dm % 3600;
322         } else {
323                 $hm = $dm;
324         }
325         return $string if ($limit eq 'h');
326
327         if ($m = int($hm / 60)) {
328                 $string .= ($string ? ', ':'')."$m $minute". ($m > 1 ? 's' : '');
329                 $mm = $hm % 60;
330         } else {
331                 $mm = $hm;
332         }
333         return $string if ($limit eq 'm');
334
335         if ($s = int($mm)) {
336                 $string .= ($string ? ', ':'')."$s $second". ($s > 1 ? 's' : '');
337         } else {
338                 $string = "0s" unless ($string);
339         }
340         return $string;
341 }
342
343 sub full {
344         my $self = shift;
345         $$self{empty} = 0;
346 }
347
348 =head2 $utils_obj->set_psname('string') OR set_psname('string')
349
350 Sets the name of this process in a B<ps> listing to B<string>.
351
352
353 =cut
354
355 sub set_psname {
356         my $self = shift;
357         my $PS_NAME = shift || $self;
358         $0 = $PS_NAME if ($PS_NAME);
359 }
360
361 sub gmtime_ISO8601 {
362         my $self = shift;
363         my @date = gmtime;
364
365         my $y = $date[5] + 1900;
366         my $M = $date[4] + 1;
367         my $d = $date[3];
368         my $h = $date[2];
369         my $m = $date[1];
370         my $s = $date[0];
371
372         return sprintf('%d-%0.2d-%0.2dT%0.2d:%0.2d:%0.2d+00:00', $y, $M, $d, $h, $m, $s);
373 }
374
375 sub clense_ISO8601 {
376         my $self = shift;
377         my $date = shift || $self;
378         if ($date =~ /^\s*(\d{4})-?(\d{2})-?(\d{2})/o) {
379                 my $new_date = "$1-$2-$3";
380
381                 if ($date =~/(\d{2}):(\d{2}):(\d{2})/o) {
382                         $new_date .= "T$1:$2:$3";
383
384                         my $z;
385                         if ($date =~ /([-+]{1})([0-9]{1,2})(?::?([0-9]{1,2}))*\s*$/o) {
386                                 $z = sprintf('%s%0.2d%0.2d',$1,$2,$3)
387                         } else {
388                                 $z =  DateTime::TimeZone::offset_as_string(
389                                         DateTime::TimeZone
390                                                 ->new( name => 'local' )
391                                                 ->offset_for_datetime(
392                                                         $date_parser->parse_datetime($new_date)
393                                                 )
394                                 );
395                         }
396
397                         if (length($z) > 3 && index($z, ':') == -1) {
398                                 substr($z,3,0) = ':';
399                                 substr($z,6,0) = ':' if (length($z) > 6);
400                         }
401                 
402                         $new_date .= $z;
403                 } else {
404                         $new_date .= "T00:00:00";
405                 }
406
407                 return $new_date;
408         }
409         return $date;
410 }
411
412 =head2 $utils_obj->daemonize('ps_name') OR daemonize('ps_name')
413
414 Turns the current process into a daemon.  B<ps_name> is optional, and is used
415 as the argument to I<< set_psname() >> if passed.
416
417
418 =cut
419
420 sub daemonize {
421         my $self = shift;
422         my $PS_NAME = shift || $self;
423         my $pid;
424         if ($pid = safe_fork($self)) {
425                 exit 0;
426         } elsif (defined($pid)) {
427                 set_psname($PS_NAME);
428                 chdir '/';
429                 setsid;
430                 return $$;
431         }
432 }
433
434 =head2 $utils_obj->safe_fork('ps_name') OR safe_fork('ps_name');
435
436 Forks the current process in a retry loop.  B<ps_name> is optional, and is used
437 as the argument to I<< set_psname() >> if passed.
438
439
440 =cut
441
442 sub safe_fork {
443         my $self = shift;
444         my $pid;
445
446 FORK:
447         {
448                 if (defined($pid = fork())) {
449                         srand(time ^ ($$ + ($$ << 15))) unless ($pid);
450                         return $pid;
451                 } elsif ($! == EAGAIN) {
452                         $self->error("Can't fork()!  $!, taking 5 and trying again.") if (ref $self);
453                         sleep 5;
454                         redo FORK;
455                 } else {
456                         $self->error("Can't fork()! $!") if ($! && ref($self));
457                         exit $!;
458                 }
459         }
460 }
461
462 #------------------------------------------------------------------------------------------------------------------------------------
463
464
465 1;