File Coverage

lib/Template/Plugin/TwoStage.pm
Criterion Covered Total %
statement 213 230 92.6
branch 42 58 72.4
condition 44 62 70.9
subroutine 37 38 97.3
pod 7 11 63.6
total 343 399 85.9


line stmt bran cond sub pod time code
1             #
2             # This file is part of Template-Plugin-TwoStage
3             #
4             # This software is copyright (c) 2014 by Alexander Kühne.
5             #
6             # This is free software; you can redistribute it and/or modify it under
7             # the same terms as the Perl 5 programming language system itself.
8             #
9             package Template::Plugin::TwoStage;
10             # ABSTRACT: two stage processing of template blocks with first stage caching
11             $Template::Plugin::TwoStage::VERSION = '0.08';
12              
13 4     4   21 use warnings;
  4         5  
  4         101  
14 4     4   18 use strict;
  4         5  
  4         172  
15              
16 4     4   18 use base qw( Template::Plugin Class::Data::Inheritable );
  4         7  
  4         3372  
17 4     4   7988 use Template 2.01 ();
  4         136  
  4         75  
18 4     4   23 use Template::Plugin ();
  4         9  
  4         61  
19 4     4   4675 use Template::Parser ();
  4         162333  
  4         141  
20 4     4   94 use Template::Exception ();
  4         7  
  4         59  
21 4     4   40 use Template::Provider ();
  4         7  
  4         81  
22              
23 4     4   22 use File::Path qw( rmtree mkpath );
  4         7  
  4         369  
24 4     4   20 use File::Spec ();
  4         6  
  4         66  
25 4     4   3358 use Digest::SHA1 qw( sha1_hex );
  4         3397  
  4         226  
26 4     4   2683 use Encode ();
  4         35143  
  4         136  
27              
28             # declare constants one by one - as opposed to a multiple constants declaration -
29             # in order to be compatible with constant.pm version 1.02 shipped with perl 5.6
30 4   50 4   29 use constant DEBUG => $ENV{TWOSTAGE_DEBUG} || 0;
  4         7  
  4         271  
31 4     4   18 use constant UNSAFE => '^A-Za-z0-9_';
  4         8  
  4         163  
32 4     4   20 use constant CACHE_DIR_NAME => 'TT_P_TwoStage';
  4         7  
  4         520  
33              
34             BEGIN {
35 4     4   8 eval {
36 4         3325 require URI::Escape::XS;
37 4         16307 URI::Escape::XS->import( qw( uri_escape ) );
38             };
39              
40 4 50       19 if ($@) {
41 0         0 print STDERR "URI::Escape::XS not available ($@)...\n" if DEBUG;
42 0         0 require URI::Escape;
43 0         0 URI::Escape->import( qw( uri_escape ) );
44             } else {
45 4         3869 print STDERR "URI::Escape::XS available ...\n" if DEBUG;
46             }
47             };
48              
49             my $TAG_STYLE_unquotemeta = {
50             map {
51             my @tags = @{$Template::Parser::TAG_STYLE->{$_}};
52             ( $_, [ map { $_ =~ s/\\([^A-Za-z_0-9]{1})/$1/g; $_ } @tags ] )
53             } keys %$Template::Parser::TAG_STYLE
54             };
55              
56             # declare options here
57             my @options = qw( caching_dir dev_mode namespace ttl dir_keys runtime_tag_style tt_cache_size );
58              
59              
60              
61             __PACKAGE__->mk_classdata( caching_dir => File::Spec->tmpdir );
62              
63              
64             __PACKAGE__->mk_classdata( dev_mode => 0 );
65              
66              
67             __PACKAGE__->mk_classdata( ttl => 0 );
68              
69              
70             __PACKAGE__->mk_classdata( dir_keys => undef );
71              
72              
73             __PACKAGE__->mk_classdata( namespace => undef );
74              
75              
76             __PACKAGE__->mk_classdata( runtime_tag_style => 'star' );
77              
78             __PACKAGE__->mk_classdata( precompile_tag_style => undef ); # is always the configured tag style of the Template object
79              
80              
81             __PACKAGE__->mk_classdata( tt_cache_size => undef );
82              
83              
84             sub extend_keys {
85 57     57 1 91 my $self = shift;
86 57         120 my $context = $self->{CONTEXT};
87 57         196 my $stash = $context->stash();
88              
89             # hook method for adding standard keys - return the keys => value -hash by reference!
90 57         344 {};
91             }
92              
93              
94              
95             # TT2 PLUGIN HOOK METHODS
96              
97             sub load {
98 5     5 1 172969 my ($class, $context) = @_;
99            
100 5         48 my $config = $class->compile_options( $context );
101              
102 5         17 my $caching_dir = $config->{ caching_dir };
103 5         9 eval { mkpath( $caching_dir, 0, 0700 ) };
  5         3385  
104 5 50       21 $class->error( "Couldn't create directory: $caching_dir. Error message: $@" ) if $@;
105            
106             # We choose to have a specific provider for the plugin, because we do not want
107             # to make any assumptions about which provider class is used by the user.
108            
109             # make include path
110              
111 5         44 my ($volume, $directories, $file) = File::Spec->splitpath( $caching_dir, 1 );
112             # Strip off the class name from the caching directory
113             # (which itself contains the class name as the last directory).
114             # The class name will be part of the template's relative path when calling process().
115            
116             my $inc_path =
117             File::Spec->catpath(
118             $volume,
119             File::Spec->catdir(
120 5         18 do { my @dirs = File::Spec->splitdir( $directories ); pop @dirs; @dirs }
  5         57  
  5         13  
  5         93  
121             ),
122             $file
123             );
124              
125             my $p = Template::Provider->new(
126 5         44 { %{$context->{ CONFIG }},
127             INCLUDE_PATH => $inc_path,
128             CACHE_SIZE => $config->{ tt_cache_size },
129 5         18 COMPILE_EXT => '.ttc',
130             COMPILE_DIR => _concat_path( $inc_path, 'tt_compiled' )
131             }
132             );
133 5         13257 push @{$context->{ LOAD_TEMPLATES }}, $p;
  5         29  
134              
135 5         20 $context->{ PREFIX_MAP }->{ twostage } = [ $p ];
136            
137 5         17 print STDERR "$class:\nwe use caching dir: $caching_dir\n" if DEBUG;
138              
139 5         33 $class;
140             }
141              
142              
143             sub new {
144 40     40 1 307596 my ($class, $context, @params) = @_;
145            
146 40         73 print STDERR "new $class\n" if DEBUG;
147 40         236 $class->create($context, @params);
148             }
149              
150             sub error {
151 0     0 1 0 my $proto = shift;
152 0 0       0 die( ref( $_[0] ) ? @_ : do { $proto->SUPER::error(@_); Template::Exception->new( 'TwoStage', $proto->SUPER::error ) } );
  0         0  
  0         0  
153             }
154              
155             sub create {
156 40     40 0 81 my ($class, $context, $params) = @_;
157              
158 40         94 print STDERR "create \n" if DEBUG;
159              
160             # let parameters overwrite a selected set of the compiled options
161 40         181 bless {
162             CONTEXT => $context,
163             CONFIG => {
164             %{$class->compiled_options( $context )},
165             precompile_tag_style => ( $class->precompile_tag_style || $context->{CONFIG}->{TAG_STYLE} || 'default' ),
166             ( defined $params ?
167             # specify invalid options for plugin construction
168 40 100 50     74 do { delete @$params{ qw( caching_dir tt_cache_size ) }; %$params } :
  3         53  
  3         50  
169             ()
170             )
171             }
172             }, $class;
173             }
174              
175             sub compile_options {
176 5     5 0 14 my ($class, $context) = @_;
177              
178 5         11 my %config;
179 5         23 @config{ @options } = map { $class->$_ } @options;
  35         1132  
180              
181 5         263 $config{ extend_keys } = \&Template::Plugin::TwoStage::extend_keys;
182              
183 5 100 100     191 if ( $class eq __PACKAGE__ && ( my $c = $context->{ CONFIG }->{ TwoStage } ) ) {
    100          
184              
185 1         11 my @ack_opts = grep { scalar grep /^$_$/, @options } keys %$c;
  1         97  
186             # slurp in all options from TT2 main configuration hash
187 1         10 @config{ @ack_opts } = @$c{ @ack_opts };
188 1         4 my $xk = $c->{ extend_keys };
189 1 50 33     12 if ( defined $xk && ref $xk eq 'CODE' ) {
190             # xk() as configuration option in TT2 main configuration hash
191 0         0 $config{ extend_keys } = $xk;
192             }
193              
194             } elsif ( $class ne __PACKAGE__ ) {
195              
196 4     4   35 no strict 'refs';
  4         5  
  4         9715  
197 2         10 my $meth_name = "${class}::extend_keys";
198 2 50       5 if ( defined &{$meth_name} ) {
  2         16  
199             # xk() as redefined callback method in derived class
200 0         0 $config{ extend_keys } = \&{$meth_name};
  0         0  
201             }
202              
203             }
204              
205             $config{ caching_dir } =
206 5         15 &_concat_path( $config{ caching_dir }, [ CACHE_DIR_NAME, do { uri_escape( $class, UNSAFE ) } ] );
  5         51  
207              
208 5         24 print STDERR join( ', ', ( %config ) )."\n" if DEBUG;
209              
210 5         62 $context->{ CONFIG }->{ _TwoStage }->{ compiled_options }->{ $class } = \%config;
211             }
212              
213             sub compiled_options {
214 145     145 0 424 my $proto = shift;
215 145   66     586 my $class = ref $proto || $proto;
216 145 100       390 my $context = ref $proto ? $proto->{ CONTEXT } : shift;
217 145         217 my $name = shift;
218              
219 145         439 my $c = $context->{ CONFIG }->{ _TwoStage }->{ compiled_options }->{ $class };
220 145 100       1054 defined $name ? $c->{ $name } : $c;
221             }
222              
223             sub dump_options {
224 4     4 0 1312 my $self = shift;
225            
226 4         8 my $options_dump = '';
227             map {
228 36 100       86 if ( $_ ne 'extend_keys' ) {
  4         65  
229 32 100       144 $options_dump.= "$_: ".( defined $self->{CONFIG}->{$_} ? $self->{CONFIG}->{$_} : '' )."\n"
230             }
231 4         7 } sort keys %{$self->{CONFIG}};
232              
233 4         22 $options_dump;
234             }
235              
236              
237             sub process {
238 57     57 1 4023546 my( $self, $params, $localize ) = @_;
239 57   100     291 $localize ||= 0;
240 57         130 my $context = $self->{CONTEXT};
241 57         198 my $stash = $context->stash();
242              
243 57 50       330 exists( $params->{template} ) || $self->error( "Pass template => \$name !" );
244 57         160 $self->{prec_template} = {}; # store for properties of current template processed
245 57         186 $self->{params} = $params; # parameters handed to process()
246 57   100     451 $self->{params}->{keys} = $self->_complement_keys( $params->{keys} || {} );
247              
248             # make the config options local to this call
249 57         231 local $self->{CONFIG} =
250             {
251             %{$self->{CONFIG}},
252 57         3338 do {
253 57         253 my %p = %$params;
254             # specify invalid options as parameters to process()/include()
255 57         190 delete @p{ qw( caching_dir tt_cache_size ) };
256 57         528 %p
257             }
258             };
259              
260 57 50       589 if ( $stash->get( 'TwoStage_precompile_mode') ) {
261              
262             # don't do runtime phase processing if the template is called in precompilation mode
263 0         0 print STDERR "$params->{template}: precompile_mode ack..." if DEBUG;
264 0         0 return $context->process( $params->{template}, {}, 1 );
265             }
266              
267              
268             print STDERR
269             "try using cached version of component ($params->{template}) ".$self->_signature."\n"
270             ."dev_mode: ".$self->{CONFIG}->{dev_mode}."\n"
271             ."INCLUDE_PATH: ".join( ' : ', @{$context->{CONFIG}->{INCLUDE_PATH}} )."\n"
272 57         445 ."keys: \n".( join "\n", map { "$_ -> $self->{params}->{keys}->{$_}" } keys %{$self->{params}->{keys}} )."\n\n"
273             if DEBUG;
274              
275             # stat() the cached precompiled version to play safely with negative
276             # caching of TT2 introduced in recent versions!
277             # Else requesting for a not yet existing precompiled version
278             # would lead to an immediate decline of a future request for the same precompiled template without
279             # further stat() checks by the provider - even if it has been created on disk in the meantime.
280              
281 57         635 my @stat = stat( $self->_file_path );
282              
283 57         287 print STDERR "template.modtime: ".$stash->get( 'template.modtime' )." - ttl: $self->{CONFIG}->{ttl} ".time()." <= ".( $stat[9] + $self->{CONFIG}->{ttl})."\n"
284             if DEBUG && scalar( @stat );
285              
286 57 100 66     832 if ( scalar( @stat )
      100        
      66        
      66        
287             &&
288             $stash->get( 'template.modtime' ) <= $stat[9] # cached version outdated?
289             &&
290             !$self->{CONFIG}->{dev_mode} # forces in cases of nested TwoStage processed templates a refresh also for modified inner templates
291             &&
292             ( !$self->{CONFIG}->{ttl} || time() <= ($stat[9] + $self->{CONFIG}->{ttl}) )
293              
294             ) {
295              
296 24         534 print STDERR "file ".$self->_file_path." successfully stat()ed\n" if DEBUG;
297            
298 24         39 my $output;
299 24         32 eval {
300             $output =
301             $context->process(
302             'twostage:' # prefix for provider selection
303             .uri_escape( ref($self), UNSAFE ).'/'
304 24 100       158 .( do { my $dirs = join( '/', @{$self->_dynamic_dir_segments} ); $dirs ? $dirs.'/' : '' } )
  24         869  
  24         66  
  24         97  
305             .$self->_signature,
306             {},
307             $localize
308             );
309             };
310            
311 24 50       34648 $self->error( "Retrieval though stat()'ed successfully (".$self->_file_path."): FAILED ($@)\n" ) if $@;
312 24         32 print STDERR "Using cached output:\n\n $output\n\n" if DEBUG;
313              
314 24         240 return $output;
315             }
316              
317             # process precompiled component
318 33         274 $context->process( $self->_precompile, {}, $localize );
319             }
320              
321             sub include {
322 2     2 1 82 (shift)->process( @_, 1 );
323             }
324              
325              
326             sub purge {
327 11     11 1 590 my $self = shift;
328 11         24 my $class = ref($self);
329            
330 11         27 my $CACHE_DIR_NAME = CACHE_DIR_NAME;
331 11         46 my $caching_dir = $self->compiled_options( 'caching_dir' );
332              
333 11 100 33     23 if (
      66        
      66        
334 11         48 do { my $class_ue = uri_escape($class, UNSAFE ); $caching_dir =~ /$class_ue/; } &&
  11         1183  
335             $caching_dir =~ /${CACHE_DIR_NAME}/ &&
336             -e $caching_dir &&
337             -d $caching_dir # kind of paranoia
338             ) {
339 10         17 eval { rmtree( $caching_dir, 0, 1 ) };
  10         11533  
340 10 50       47 if ( $@ ) {
341 0         0 $class->error( "Couldn't remove directory tree: $caching_dir. Error message: $@" );
342             }
343             }
344              
345 11         78 '';
346             }
347              
348              
349             sub _complement_keys {
350 57     57   98 my $self = shift;
351 57         122 my $keys = shift;
352              
353 57         201 my $callers = $self->{CONTEXT}->stash->get( 'component.callers' );
354              
355             +{
356 57         215 %{ $self->{CONFIG}->{extend_keys}->( $self ) },
  57         290  
357 4         21 %{$keys},
358             '_file_scope' =>
359 57 100       2588 ( ref($callers) ? join( '\\', @{$callers} ) : '' )
360             .$self->{CONTEXT}->stash->get( 'component.name' )
361             # For making BLOCK name in template file scoped we need a unique identifier:
362             # component.callers + component.name
363             # This approach introduces the drawback that a BLOCK defined in a template being
364             # included in different other templates as an "intra" is cached for each call stack
365             # path seperately! But it is a feasable workaround as we don't know how to figure
366             # out the name of the template the BLOCK was defined in.
367             };
368             }
369              
370             sub _precompile {
371 33     33   59 my $self = shift;
372 33         680 my $context = $self->{CONTEXT};
373 33         116 my $stash = $context->stash();
374            
375 33         324 my $TAGS_tag =
376             $TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{precompile_tag_style} }->[0]
377             .' TAGS '.$self->{CONFIG}->{runtime_tag_style}.' '
378             .$TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{precompile_tag_style} }->[1]."\n";
379              
380 33         38 print STDERR "We are using tag style: $self->{CONFIG}->{precompile_tag_style}\n" if DEBUG;
381              
382 33         68 my $template;
383 33         57 eval {
384 33         272 $template = $context->process( $self->{params}->{template}, { TwoStage_precompile_mode => 1 }, 1 );
385             };
386              
387 33 50       7080 if ( $@ ) {
388 0         0 print STDERR "\tFAILED ($@)\n" if DEBUG;
389 0 0       0 $self->error( ref($@) ? $@ : "Precompilation of module $self->{params}->{template}: $@ \n" );
390             }
391              
392 33         46 print STDERR "storing ".$self->_signature."\n\n" if DEBUG;
393              
394 33         44 eval { mkpath( $self->_file_dir, 0, 0700 ) };
  33         128  
395 33 50       157 if ($@) {
396 0         0 $self->error( "Couldn't create ".$self->_file_dir.": $@" );
397             }
398              
399 33 50       367 open( my $fh, "> ", $self->_file_path ) || $self->error( "Could not get a filehandle! Error: $!" );
400              
401             my $out =
402             $TAGS_tag
403             .( $self->{CONFIG}->{dev_mode}
404             &&
405             $TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{runtime_tag_style} }->[0]
406             ."# This precompiled template ( $self->{params}->{template} ) is stored together with the following keys:\n\t"
407 33   100     311 .join( "\n\t", map { "$_ => ".( defined $self->{params}->{keys}->{$_} ? $self->{params}->{keys}->{$_} : 'undef' ) } keys %{$self->{params}->{keys}} )."\n "
408             .$TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{runtime_tag_style} }->[1]."\n"
409             ||
410             ''
411             )
412             .$template;
413            
414              
415 33 100       164 if ( Encode::is_utf8( $template ) ) {
416              
417 1         3 print STDERR "_precompile: encode\n" if DEBUG;
418 1         11 $out = Encode::decode_utf8( "\x{ef}\x{bb}\x{bf}" ).$out; # utf8 bom is stripped off again on load by Template::Provider
419 1         134 binmode( $fh ); # turn off crlf io layer!?
420 1     1   53 binmode( $fh, ':encoding(utf8)' );
  1         10  
  1         2  
  1         18  
421              
422             } else {
423              
424 32         36 print STDERR "_precompile: octets\n" if DEBUG;
425 32         113 binmode( $fh );
426             }
427            
428 33         2217 print $fh $out;
429 33         2136 close $fh;
430              
431 33         509 return \($TAGS_tag.$template);
432             }
433              
434             sub _signature {
435 81     81   222 my $self = shift;
436             # produce signature
437            
438 69   100     1022 $self->{prec_template}->{signature}
439             ||=
440             sha1_hex(
441             join(
442             ':',
443             (
444             $self->{params}->{template},
445 81   66     440 map { "$_=".( $self->{params}->{keys}->{$_} || '' ) } sort keys %{$self->{params}->{keys}}
  57         381  
446             )
447             )
448             ).'.tt';
449             }
450              
451             sub _dynamic_dir_segments {
452 108     108   233 my $self = shift;
453            
454 4         93 $self->{prec_template}->{dynamic_dir_segments}
455             ||=
456             [
457             # include a possible namespace
458             ( $self->{CONFIG}->{namespace} ? $self->{CONFIG}->{namespace} : () ),
459             # include dir_keys - we offer this feature only in testing mode!
460             ( $self->{CONFIG}->{dev_mode} && $self->{CONFIG}->{dir_keys}
461             ?
462             (
463             $self->{params}->{template},
464 6         19 map { uri_escape( $_, UNSAFE ),
465             uri_escape( 'value-'.$self->{params}->{keys}->{$_}, UNSAFE )
466             }
467             ( ref( $self->{CONFIG}->{dir_keys} )
468             ?
469 2         7 grep( { exists $self->{params}->{keys}->{$_} } @{$self->{CONFIG}->{dir_keys}} )
  0         0  
470             :
471 108 100 100     2416 keys %{$self->{params}->{keys}}
    50 100        
    100          
472             )
473             )
474             :
475             ()
476             )
477             ];
478             }
479              
480             sub _rel_file_path {
481 57     57   81 my $self = shift;
482              
483 57   33     386 $self->{prec_template}->{rel_file_path} ||= &_concat_path( $self->_rel_file_dir, $self->_signature );
484             }
485              
486             sub _file_path {
487 90     90   116 my $self = shift;
488              
489 90   66     4469 $self->{prec_template}->{file_path} ||= &_concat_path( $self->compiled_options( 'caching_dir' ), $self->_rel_file_path );
490             }
491              
492             sub _rel_file_dir {
493 90     90   119 my $self = shift;
494            
495 90   100     312 $self->{prec_template}->{rel_file_dir} ||= File::Spec->catdir( @{$self->_dynamic_dir_segments} );
  84         234  
496             }
497              
498             sub _file_dir {
499 33     33   47 my $self = shift;
500              
501 33   33     210 $self->{prec_template}->{file_dir} ||= &_concat_path( $self->compiled_options( 'caching_dir' ), $self->_rel_file_dir );
502             }
503              
504             # helpers
505              
506             sub _concat_path {
507 165     165   773 my ( $base_path, $append_dirs ) = @_;
508             # $base_dir: base path (no filename) as string
509             # $append_dirs: directories to append as string or an array reference
510            
511 165         1803 my ($base_volume, $base_directories, $base_file) = File::Spec->splitpath( $base_path, 1 );
512 13         1124 File::Spec->catpath(
513             $base_volume,
514             File::Spec->catdir(
515             File::Spec->splitdir( $base_directories ),
516 165 100       12202 ( ref($append_dirs) ? @{$append_dirs} : File::Spec->splitdir( $append_dirs ) )
517             )
518             ,
519             $base_file
520             );
521             }
522              
523              
524             1; # End of Template::Plugin::TwoStage
525              
526             __END__