File Coverage

blib/lib/Text/Template/Simple/Cache.pm
Criterion Covered Total %
statement 240 274 87.5
branch 76 132 57.5
condition 12 27 44.4
subroutine 28 30 93.3
pod 9 9 100.0
total 365 472 77.3


line stmt bran cond sub pod time code
1             ## no critic (ProhibitUnusedPrivateSubroutines)
2             package Text::Template::Simple::Cache;
3 60     60   209 use strict;
  60         59  
  60         1611  
4 60     60   185 use warnings;
  60         62  
  60         1351  
5              
6 60     60   182 use Carp qw( croak );
  60         59  
  60         3000  
7 60     60   20419 use Text::Template::Simple::Constants qw(:all);
  60         85  
  60         25955  
8 60     60   22049 use Text::Template::Simple::Util qw( DEBUG LOG fatal );
  60         88  
  60         142720  
9              
10             our $VERSION = '0.90';
11              
12             my $CACHE = {}; # in-memory template cache
13              
14             sub new {
15 90     90 1 138 my $class = shift;
16 90   33     247 my $parent = shift || fatal('tts.cache.new.parent');
17 90         167 my $self = [undef];
18 90         144 bless $self, $class;
19 90         253 $self->[CACHE_PARENT] = $parent;
20 90         189 return $self;
21             }
22              
23             sub id {
24 716     716 1 567 my $self = shift;
25 716         534 my $val = shift;
26 716 100       991 $self->[CACHE_PARENT][CID] = $val if $val;
27 716         1020 return $self->[CACHE_PARENT][CID];
28             }
29              
30             sub type {
31 10     10 1 19 my $self = shift;
32 10         19 my $parent = $self->[CACHE_PARENT];
33 10 100       62 return $parent->[CACHE] ? $parent->[CACHE_DIR] ? 'DISK'
    100          
34             : 'MEMORY'
35             : 'OFF';
36             }
37              
38             sub reset { ## no critic (ProhibitBuiltinHomonyms)
39 4     4 1 4 my $self = shift;
40 4         6 my $parent = $self->[CACHE_PARENT];
41 4         5 %{$CACHE} = ();
  4         12  
42              
43 4 100 33     19 if ( $parent->[CACHE] && $parent->[CACHE_DIR] ) {
44              
45 2         3 my $cdir = $parent->[CACHE_DIR];
46 2         10 require Symbol;
47 2         5 my $CDIRH = Symbol::gensym();
48 2 50       58 opendir $CDIRH, $cdir or fatal( 'tts.cache.opendir' => $cdir, $! );
49 2         7 require File::Spec;
50 2         4 my $ext = quotemeta CACHE_EXT;
51 2         2 my $file;
52              
53 2         15 while ( defined( $file = readdir $CDIRH ) ) {
54 6 100       55 if ( $file =~ m{ ( .* $ext) \z}xmsi ) {
55 2         22 $file = File::Spec->catfile( $parent->[CACHE_DIR], $1 );
56 2 50       8 LOG( UNLINK => $file ) if DEBUG;
57 2         143 unlink $file;
58             }
59             }
60              
61 2         15 closedir $CDIRH;
62             }
63 4         10 return 1;
64             }
65              
66             sub dumper {
67 20     20 1 21 my $self = shift;
68 20   50     51 my $type = shift || 'structure';
69 20   100     44 my $param = shift || {};
70 20 50       46 fatal('tts.cache.dumper.hash') if ref $param ne 'HASH';
71 20         29 my %valid = map { ($_, $_) } qw( ids structure );
  40         93  
72 20 50       42 fatal('tts.cache.dumper.type', $type) if not $valid{ $type };
73 20         28 my $method = '_dump_' . $type;
74 20         47 return $self->$method( $param ); # TODO: modify the methods to accept HASH
75             }
76              
77             sub _dump_ids {
78 8     8   10 my $self = shift;
79 8         11 my $parent = $self->[CACHE_PARENT];
80 8         5 my $p = shift;
81 8   100     24 my $VAR = $p->{varname} || q{$} . q{CACHE_IDS};
82 8         10 my @rv;
83              
84 8 100       14 if ( $parent->[CACHE_DIR] ) {
85              
86 4         18 require File::Find;
87 4         10 require File::Spec;
88 4         6 my $ext = quotemeta CACHE_EXT;
89 4         45 my $re = qr{ (.+?) $ext \z }xms;
90 4         4 my($id, @list);
91              
92             File::Find::find(
93             {
94             no_chdir => 1,
95             wanted => sub {
96 8 100   8   181 if ( $_ =~ $re ) {
97 4         24 ($id = $1) =~ s{.*[\\/]}{}xms;
98 4         55 push @list, $id;
99             }
100             },
101             },
102 4         220 $parent->[CACHE_DIR]
103             );
104              
105 4         25 @rv = sort @list;
106              
107             }
108             else {
109 4         4 @rv = sort keys %{ $CACHE };
  4         10  
110             }
111              
112 8         34 require Data::Dumper;
113 8         41 my $d = Data::Dumper->new( [ \@rv ], [ $VAR ]);
114 8         154 return $d->Dump;
115             }
116              
117             sub _dump_structure {
118 12     12   11 my $self = shift;
119 12         11 my $parent = $self->[CACHE_PARENT];
120 12         8 my $p = shift;
121 12   100     41 my $VAR = $p->{varname} || q{$} . q{CACHE};
122 12 100       20 my $deparse = $p->{no_deparse} ? 0 : 1;
123 12         49 require Data::Dumper;
124 12         13 my $d;
125              
126 12 100       15 if ( $parent->[CACHE_DIR] ) {
127 6         14 $d = Data::Dumper->new( [ $self->_dump_disk_cache ], [ $VAR ] );
128             }
129             else {
130 6         31 $d = Data::Dumper->new( [ $CACHE ], [ $VAR ]);
131 6 100       142 if ( $deparse ) {
132 4 50       16 fatal('tts.cache.dumper' => $Data::Dumper::VERSION)
133             if !$d->can('Deparse');
134 4         15 $d->Deparse(1);
135             }
136             }
137              
138 12         139 my $str = eval { $d->Dump; };
  12         26  
139              
140 12 50       10712 if ( my $error = $@ ) {
141 0 0 0     0 if ( $deparse && $error =~ RE_DUMP_ERROR ) {
142 0         0 my $name = ref($self) . '::dump_cache';
143 0         0 warn "$name: An error occurred when dumping with deparse "
144             ."(are you under mod_perl?). Re-Dumping without deparse...\n";
145 0         0 warn "$error\n";
146 0         0 my $nd = Data::Dumper->new( [ $CACHE ], [ $VAR ]);
147 0         0 $nd->Deparse(0);
148 0         0 $str = $nd->Dump;
149             }
150             else {
151 0         0 croak $error;
152             }
153             }
154              
155 12         106 return $str;
156             }
157              
158             sub _dump_disk_cache {
159 6     6   13 require File::Find;
160 6         10 require File::Spec;
161 6         7 my $self = shift;
162 6         6 my $parent = $self->[CACHE_PARENT];
163 6         6 my $pattern = quotemeta DISK_CACHE_MARKER;
164 6         4 my $ext = quotemeta CACHE_EXT;
165 6         46 my $re = qr{(.+?) $ext \z}xms;
166 6         6 my(%disk_cache);
167              
168             my $process = sub {
169 12     12   17 my $file = $_;
170 12         52 my @match = $file =~ $re;
171 12 100       295 return if ! @match;
172 6         23 (my $id = $match[0]) =~ s{.*[\\/]}{}xms;
173 6         17 my $content = $parent->io->slurp( File::Spec->canonpath($file) );
174 6         8 my $ok = 0; # reset
175 6         6 my $_temp = EMPTY_STRING; # reset
176              
177 6         32 foreach my $line ( split m{\n}xms, $content ) {
178 60 50       93 if ( $line =~ m{$pattern}xmso ) {
179 0         0 $ok = 1;
180 0         0 next;
181             }
182 60 50       81 next if not $ok;
183 0         0 $_temp .= $line;
184             }
185              
186 6         179 $disk_cache{ $id } = {
187             MTIME => (stat $file)[STAT_MTIME],
188             CODE => $_temp,
189             };
190 6         23 };
191              
192 6         350 File::Find::find(
193             {
194             no_chdir => 1,
195             wanted => $process,
196             },
197             $parent->[CACHE_DIR]
198             );
199 6         68 return \%disk_cache;
200             }
201              
202             sub size {
203 8     8 1 11 my $self = shift;
204 8         10 my $parent = $self->[CACHE_PARENT];
205              
206 8 50       20 return 0 if not $parent->[CACHE]; # calculate only if cache is enabled
207              
208 8 100       18 if ( my $cdir = $parent->[CACHE_DIR] ) { # disk cache
209 4         30 require File::Find;
210 4         5 my $total = 0;
211 4         5 my $ext = quotemeta CACHE_EXT;
212              
213             my $wanted = sub {
214 6 100   6   271 return if $_ !~ m{ $ext \z }xms; # only calculate "our" files
215 2         49 $total += (stat $_)[STAT_SIZE];
216 4         20 };
217              
218 4         286 File::Find::find( { wanted => $wanted, no_chdir => 1 }, $cdir );
219 4         30 return $total;
220              
221             }
222             else { # in-memory cache
223              
224 4         9 local $SIG{__DIE__};
225 4 50       5 if ( eval { require Devel::Size; 1; } ) {
  4         15  
  4         8  
226 4         19 my $dsv = Devel::Size->VERSION;
227 4 50       12 LOG( DEBUG => "Devel::Size v$dsv is loaded." ) if DEBUG;
228 4 50       13 fatal('tts.cache.develsize.buggy', $dsv) if $dsv < DEVEL_SIZE_VERSION;
229 4         3 my $size = eval { Devel::Size::total_size( $CACHE ) };
  4         169  
230 4 50       9 fatal('tts.cache.develsize.total', $@) if $@;
231 4         21 return $size;
232             }
233             else {
234 0         0 warn "Failed to load Devel::Size: $@\n";
235 0         0 return 0;
236             }
237              
238             }
239             }
240              
241             sub has {
242 24     24 1 43 my($self, @args ) = @_;
243 24 50       65 fatal('tts.cache.pformat') if @args % 2;
244 24         45 my %opt = @args;
245 24         25 my $parent = $self->[CACHE_PARENT];
246              
247 24 50       42 if ( not $parent->[CACHE] ) {
248 0 0       0 LOG( DEBUG => 'Cache is disabled!') if DEBUG;
249 0         0 return;
250             }
251              
252              
253 24         46 my $id = $parent->connector('Cache::ID')->new;
254             my $cid = $opt{id} ? $id->generate($opt{id} , 'custom')
255             : $opt{data} ? $id->generate($opt{data} )
256 24 50       95 : fatal('tts.cache.incache');
    100          
257              
258 24 100       43 if ( my $cdir = $parent->[CACHE_DIR] ) {
259 12         48 require File::Spec;
260 12 50       292 return -e File::Spec->catfile( $cdir, $cid . CACHE_EXT ) ? 1 : 0;
261             }
262             else {
263 12 50       46 return exists $CACHE->{ $cid } ? 1 : 0;
264             }
265             }
266              
267             sub _is_meta_version_old {
268 2     2   4 my $self = shift;
269 2         3 my $v = shift;
270 2 50       13 return 1 if ! $v; # no version? archaic then
271 2         25 my $pv = PARENT->VERSION;
272 2         7 foreach my $i ( $v, $pv ) {
273 4         9 $i =~ tr/_//d; # underscore versions cause warnings
274 4         14 $i += 0; # force number
275             }
276 2 50       8 return 1 if $v < $pv;
277 2         7 return;
278             }
279              
280             sub hit {
281             # TODO: return $CODE, $META;
282 18     18 1 21 my $self = shift;
283 18         20 my $cache_id = shift;
284 18   50     69 my $chkmt = shift || 0;
285              
286 18 100       46 my $method = $self->[CACHE_PARENT][CACHE_DIR] ? '_hit_disk' : '_hit_memory';
287 18         50 return $self->$method( $cache_id, $chkmt );
288             }
289              
290             sub _hit_memory {
291 10     10   16 my($self, $cache_id, $chkmt) = @_;
292 10 50       17 if ( $chkmt ) {
293 0   0     0 my $mtime = $CACHE->{$cache_id}{MTIME} || 0;
294 0 0       0 if ( $mtime != $chkmt ) {
295 0 0       0 LOG( MTIME_DIFF => "\tOLD: $mtime\n\t\tNEW: $chkmt" ) if DEBUG;
296 0         0 return; # i.e.: Update cache
297             }
298             }
299 10 50       21 LOG( MEM_CACHE => EMPTY_STRING ) if DEBUG;
300 10         31 return $CACHE->{$cache_id}->{CODE};
301             }
302              
303             sub _hit_disk {
304 8     8   13 my($self, $cache_id, $chkmt) = @_;
305 8         17 my $parent = $self->[CACHE_PARENT];
306 8         10 my $cdir = $parent->[CACHE_DIR];
307 8         40 require File::Spec;
308 8         106 my $cache = File::Spec->catfile( $cdir, $cache_id . CACHE_EXT );
309 8   66     186 my $ok = -e $cache && ! -d _ && -f _;
310 8 100       31 return if not $ok;
311              
312 2         12 my $disk_cache = $parent->io->slurp($cache);
313 2         3 my %meta;
314 2 50       13 if ( $disk_cache =~ m{ \A \#META: (.+?) \n }xms ) {
315 2         8 %meta = $self->_get_meta( $1 );
316 2 50       7 fatal('tts.cache.hit.meta', $@) if $@;
317             }
318 2 50       8 if ( $self->_is_meta_version_old( $meta{VERSION} ) ) {
319 0   0     0 my $id = $parent->[FILENAME] || $cache_id;
320 0         0 warn "(This message will only appear once) $id was compiled with"
321             .' an old version of ' . PARENT . ". Resetting cache.\n";
322 0         0 return;
323             }
324 2 50       6 if ( my $mtime = $meta{CHKMT} ) {
325 0 0       0 if ( $mtime != $chkmt ) {
326 0 0       0 LOG( MTIME_DIFF => "\tOLD: $mtime\n\t\tNEW: $chkmt") if DEBUG;
327 0         0 return; # i.e.: Update cache
328             }
329             }
330              
331 2         13 my($CODE, $error) = $parent->_wrap_compile($disk_cache);
332 2 50       8 $parent->[NEEDS_OBJECT] = $meta{NEEDS_OBJECT} if $meta{NEEDS_OBJECT};
333 2 50       8 $parent->[FAKER_SELF] = $meta{FAKER_SELF} if $meta{FAKER_SELF};
334              
335 2 50       7 fatal('tts.cache.hit.cache', $error) if $error;
336 2 50       7 LOG( FILE_CACHE => EMPTY_STRING ) if DEBUG;
337             #$parent->[COUNTER]++;
338 2         10 return $CODE;
339             }
340              
341             sub populate {
342 498     498 1 693 my($self, $cache_id, $parsed, $chkmt) = @_;
343 498         463 my $parent = $self->[CACHE_PARENT];
344 498 100       844 my $target = ! $parent->[CACHE] ? '_populate_no_cache'
    100          
345             : $parent->[CACHE_DIR] ? '_populate_disk'
346             : '_populate_memory'
347             ;
348              
349 498         1099 my($CODE, $error) = $self->$target( $parsed, $cache_id, $chkmt );
350 498 50       822 $self->_populate_error( $parsed, $cache_id, $error ) if $error;
351 498         581 ++$parent->[COUNTER];
352 498         865 return $CODE;
353             }
354              
355             sub _populate_error {
356 0     0   0 my($self, $parsed, $cache_id, $error) = @_;
357 0         0 my $parent = $self->[CACHE_PARENT];
358 0 0       0 croak $parent->[VERBOSE_ERRORS]
    0          
359             ? $parent->_mini_compiler(
360             $parent->_internal('compile_error'),
361             {
362             CID => $cache_id ? $cache_id : 'N/A',
363             ERROR => $error,
364             PARSED => $parsed,
365             TIDIED => $parent->_tidy( $parsed ),
366             }
367             )
368             : $error
369             ;
370             }
371              
372             sub _populate_no_cache {
373             # cache is disabled
374 484     484   496 my($self, $parsed, $cache_id, $chkmt) = @_;
375 484         1206 my($CODE, $error) = $self->[CACHE_PARENT]->_wrap_compile($parsed);
376 484 50       953 LOG( NC_POPUL => $cache_id ) if DEBUG >= DEBUG_LEVEL_INSANE;
377 484         856 return $CODE, $error;
378             }
379              
380             sub _populate_memory {
381 8     8   12 my($self, $parsed, $cache_id, $chkmt) = @_;
382 8         17 my $parent = $self->[CACHE_PARENT];
383 8         18 my $c = $CACHE->{ $cache_id } = {}; # init
384 8         37 my($CODE, $error) = $parent->_wrap_compile($parsed);
385 8         16 $c->{CODE} = $CODE;
386 8 50       18 $c->{MTIME} = $chkmt if $chkmt;
387 8         14 $c->{NEEDS_OBJECT} = $parent->[NEEDS_OBJECT];
388 8         14 $c->{FAKER_SELF} = $parent->[FAKER_SELF];
389 8 50       21 LOG( MEM_POPUL => $cache_id ) if DEBUG >= DEBUG_LEVEL_INSANE;
390 8         18 return $CODE, $error;
391             }
392              
393             sub _populate_disk {
394 6     6   12 my($self, $parsed, $cache_id, $chkmt) = @_;
395              
396 6         32 require File::Spec;
397 6         16 require Fcntl;
398 6         1643 require IO::File;
399              
400 6         3372 my $parent = $self->[CACHE_PARENT];
401 6         103 my %meta = (
402             CHKMT => $chkmt,
403             NEEDS_OBJECT => $parent->[NEEDS_OBJECT],
404             FAKER_SELF => $parent->[FAKER_SELF],
405             VERSION => PARENT->VERSION,
406             );
407              
408 6         71 my $cache = File::Spec->catfile( $parent->[CACHE_DIR], $cache_id . CACHE_EXT);
409 6         29 my $fh = IO::File->new;
410 6 50       198 $fh->open($cache, '>') or fatal('tts.cache.populate.write', $cache, $!);
411 6         578 flock $fh, Fcntl::LOCK_EX();
412 6         23 $parent->io->layer($fh);
413 6         49 my $warn = $parent->_mini_compiler(
414             $parent->_internal('disk_cache_comment'),
415             {
416             NAME => PARENT->class_id,
417             DATE => scalar localtime time,
418             }
419             );
420 6         23 my $ok = print { $fh } '#META:' . $self->_set_meta(\%meta) . "\n",
  6         22  
421             $warn,
422             $parsed;
423 6         241 flock $fh, Fcntl::LOCK_UN();
424 6 50       53 close $fh or croak "Unable to close filehandle: $!";
425 6 50       122 chmod(CACHE_FMODE, $cache) || fatal('tts.cache.populate.chmod');
426              
427 6         29 my($CODE, $error) = $parent->_wrap_compile($parsed);
428 6 50       18 LOG( DISK_POPUL => $cache_id ) if DEBUG >= DEBUG_LEVEL_INSANE;
429 6         35 return $CODE, $error;
430             }
431              
432             sub _get_meta {
433 2     2   4 my $self = shift;
434 2         5 my $raw = shift;
435 2         8 my %meta = map { split m{:}xms, $_ } split m{[|]}xms, $raw;
  8         19  
436 2         14 return %meta;
437             }
438              
439             sub _set_meta {
440 6     6   9 my $self = shift;
441 6         7 my $meta = shift;
442 6         7 my $rv = join q{|}, map { $_ . q{:} . $meta->{ $_ } } keys %{ $meta };
  24         54  
  6         15  
443 6         71 return $rv;
444             }
445              
446             sub DESTROY {
447 0     0     my $self = shift;
448 0 0         LOG( DESTROY => ref $self ) if DEBUG;
449 0           $self->[CACHE_PARENT] = undef;
450 0           @{$self} = ();
  0            
451 0           return;
452             }
453              
454             1;
455              
456             __END__
457              
458             =head1 NAME
459              
460             Text::Template::Simple::Cache - Cache manager
461              
462             =head1 SYNOPSIS
463              
464             TODO
465              
466             =head1 DESCRIPTION
467              
468             This document describes version C<0.90> of C<Text::Template::Simple::Cache>
469             released on C<5 July 2016>.
470              
471             Cache manager for C<Text::Template::Simple>.
472              
473             =head1 METHODS
474              
475             =head2 new PARENT_OBJECT
476              
477             Constructor. Accepts a C<Text::Template::Simple> object as the parameter.
478              
479             =head2 type
480              
481             Returns the type of the cache.
482              
483             =head2 reset
484              
485             Resets the in-memory cache and deletes all cache files,
486             if you are using a disk cache.
487              
488             =head2 dumper TYPE
489              
490             $template->cache->dumper( $type, \%opt );
491              
492             C<TYPE> can either be C<structure> or C<ids>.
493             C<dumper> accepts some arguments as a hash reference:
494              
495             $template->cache->dumper( $type, \%opt );
496              
497             =over 4
498              
499             =item C<varname>
500              
501             Controls the name of the dumped structure.
502              
503             =item no_deparse
504              
505             If you set this to a true value, C<deparsing> will be disabled
506              
507             =back
508              
509             =head3 structure
510              
511             Returns a string version of the dumped in-memory or disk-cache.
512             Cache is dumped via L<Data::Dumper>. C<Deparse> option is enabled
513             for in-memory cache.
514              
515             Early versions of C<Data::Dumper> don' t have a C<Deparse>
516             method, so you may need to upgrade your C<Data::Dumper> or
517             disable C<deparsing> if you want to use this method.
518              
519             =head3 ids
520              
521             Returns a list including the names (ids) of the templates in
522             the cache.
523              
524             =head2 id
525              
526             Gets/sets the cache id.
527              
528             =head2 size
529              
530             Returns the total cache (disk or memory) size in bytes. If
531             memory cache is used, then you must have L<Devel::Size> installed
532             on your system to get the size of the data structure inside memory.
533              
534             =head2 has data => TEMPLATE_DATA
535              
536             =head2 has id => TEMPLATE_ID
537              
538             This method can be called with C<data> or C<id> named parameter. If you
539             use the two together, C<id> will be used:
540              
541             if ( $template->cache->has( id => 'e369853df766fa44e1ed0ff613f563bd' ) ) {
542             print "ok!";
543             }
544              
545             or
546              
547             if ( $template->cache->has( data => q~Foo is <%=$bar%>~ ) ) {
548             print "ok!";
549             }
550              
551             =head2 hit
552              
553             TODO
554              
555             =head2 populate
556              
557             TODO
558              
559             =head1 AUTHOR
560              
561             Burak Gursoy <burak@cpan.org>.
562              
563             =head1 COPYRIGHT
564              
565             Copyright 2004 - 2016 Burak Gursoy. All rights reserved.
566              
567             =head1 LICENSE
568              
569             This library is free software; you can redistribute it and/or modify
570             it under the same terms as Perl itself, either Perl version 5.24.0 or,
571             at your option, any later version of Perl 5 you may have available.
572             =cut