File Coverage

lib/Template/Plugin/TwoStage.pm
Criterion Covered Total %
statement 213 230 92.6
branch 42 58 72.4
condition 43 62 69.3
subroutine 37 38 97.3
pod 7 11 63.6
total 342 399 85.7


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.07'; # TRIAL
12              
13 4     4   24 use warnings;
  4         6  
  4         139  
14 4     4   19 use strict;
  4         5  
  4         109  
15              
16 4     4   20 use base qw( Template::Plugin Class::Data::Inheritable );
  4         7  
  4         2380  
17 4     4   6462 use Template 2.01 ();
  4         120  
  4         93  
18 4     4   23 use Template::Plugin ();
  4         6  
  4         69  
19 4     4   2528 use Template::Parser ();
  4         132786  
  4         151  
20 4     4   90 use Template::Exception ();
  4         7  
  4         119  
21 4     4   21 use Template::Provider ();
  4         6  
  4         163  
22              
23 4     4   21 use File::Path qw( rmtree mkpath );
  4         4  
  4         501  
24 4     4   25 use File::Spec ();
  4         6  
  4         96  
25 4     4   3142 use Digest::SHA1 qw( sha1_hex );
  4         3354  
  4         278  
26 4     4   2423 use Encode ();
  4         37736  
  4         182  
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   33 use constant DEBUG => $ENV{TWOSTAGE_DEBUG} || 0;
  4         8  
  4         370  
31 4     4   25 use constant UNSAFE => '^A-Za-z0-9_';
  4         7  
  4         227  
32 4     4   21 use constant CACHE_DIR_NAME => 'TT_P_TwoStage';
  4         7  
  4         556  
33              
34             BEGIN {
35 4     4   6 eval {
36 4         1984 require URI::Escape::XS;
37 4         10463 URI::Escape::XS->import( qw( uri_escape ) );
38             };
39              
40 4 50       20 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         3675 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 81 my $self = shift;
86 57         130 my $context = $self->{CONTEXT};
87 57         150 my $stash = $context->stash();
88              
89             # hook method for adding standard keys - return the keys => value -hash by reference!
90 57         294 {};
91             }
92              
93              
94              
95             # TT2 PLUGIN HOOK METHODS
96              
97             sub load {
98 5     5 1 104002 my ($class, $context) = @_;
99            
100 5         43 my $config = $class->compile_options( $context );
101              
102 5         22 my $caching_dir = $config->{ caching_dir };
103 5         10 eval { mkpath( $caching_dir, 0, 0700 ) };
  5         1769  
104 5 50       27 $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         49 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         13 do { my @dirs = File::Spec->splitdir( $directories ); pop @dirs; @dirs }
  5         45  
  5         13  
  5         125  
121             ),
122             $file
123             );
124              
125             my $p = Template::Provider->new(
126 5         45 { %{$context->{ CONFIG }},
127             INCLUDE_PATH => $inc_path,
128             CACHE_SIZE => $config->{ tt_cache_size },
129 5         13 COMPILE_EXT => '.ttc',
130             COMPILE_DIR => _concat_path( $inc_path, 'tt_compiled' )
131             }
132             );
133 5         2534 push @{$context->{ LOAD_TEMPLATES }}, $p;
  5         19  
134              
135 5         23 $context->{ PREFIX_MAP }->{ twostage } = [ $p ];
136            
137 5         8 print STDERR "$class:\nwe use caching dir: $caching_dir\n" if DEBUG;
138              
139 5         20 $class;
140             }
141              
142              
143             sub new {
144 40     40 1 215754 my ($class, $context, @params) = @_;
145            
146 40         58 print STDERR "new $class\n" if DEBUG;
147 40         220 $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 66 my ($class, $context, $params) = @_;
157              
158 40         57 print STDERR "create \n" if DEBUG;
159              
160             # let parameters overwrite a selected set of the compiled options
161             bless {
162             CONTEXT => $context,
163             CONFIG => {
164 40         115 %{$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     69 do { delete @$params{ qw( caching_dir tt_cache_size ) }; %$params } :
  3         52  
  3         42  
169             ()
170             )
171             }
172             }, $class;
173             }
174              
175             sub compile_options {
176 5     5 0 25 my ($class, $context) = @_;
177              
178 5         9 my %config;
179 5         13 @config{ @options } = map { $class->$_ } @options;
  35         468  
180              
181 5         84 $config{ extend_keys } = \&Template::Plugin::TwoStage::extend_keys;
182              
183 5 100 100     106 if ( $class eq __PACKAGE__ && ( my $c = $context->{ CONFIG }->{ TwoStage } ) ) {
    100          
184              
185 1         4 my @ack_opts = grep { scalar grep /^$_$/, @options } keys %$c;
  1         86  
186             # slurp in all options from TT2 main configuration hash
187 1         5 @config{ @ack_opts } = @$c{ @ack_opts };
188 1         2 my $xk = $c->{ extend_keys };
189 1 50 33     5 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   31 no strict 'refs';
  4         6  
  4         9801  
197 2         16 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         13 &_concat_path( $config{ caching_dir }, [ CACHE_DIR_NAME, do { uri_escape( $class, UNSAFE ) } ] );
  5         36  
207              
208 5         13 print STDERR join( ', ', ( %config ) )."\n" if DEBUG;
209              
210 5         41 $context->{ CONFIG }->{ _TwoStage }->{ compiled_options }->{ $class } = \%config;
211             }
212              
213             sub compiled_options {
214 145     145 0 406 my $proto = shift;
215 145   66     488 my $class = ref $proto || $proto;
216 145 100       389 my $context = ref $proto ? $proto->{ CONTEXT } : shift;
217 145         188 my $name = shift;
218              
219 145         297 my $c = $context->{ CONFIG }->{ _TwoStage }->{ compiled_options }->{ $class };
220 145 100       887 defined $name ? $c->{ $name } : $c;
221             }
222              
223             sub dump_options {
224 4     4 0 1311 my $self = shift;
225            
226 4         8 my $options_dump = '';
227             map {
228 36 100       65 if ( $_ ne 'extend_keys' ) {
229 32 100       139 $options_dump.= "$_: ".( defined $self->{CONFIG}->{$_} ? $self->{CONFIG}->{$_} : '' )."\n"
230             }
231 4         5 } sort keys %{$self->{CONFIG}};
  4         52  
232              
233 4         20 $options_dump;
234             }
235              
236              
237             sub process {
238 57     57 1 4021274 my( $self, $params, $localize ) = @_;
239 57   100     290 $localize ||= 0;
240 57         120 my $context = $self->{CONTEXT};
241 57         175 my $stash = $context->stash();
242              
243 57 50       254 exists( $params->{template} ) || $self->error( "Pass template => \$name !" );
244 57         153 $self->{prec_template} = {}; # store for properties of current template processed
245 57         157 $self->{params} = $params; # parameters handed to process()
246 57   100     411 $self->{params}->{keys} = $self->_complement_keys( $params->{keys} || {} );
247              
248             # make the config options local to this call
249             local $self->{CONFIG} =
250             {
251 57         202 %{$self->{CONFIG}},
252 57         1715 do {
253 57         213 my %p = %$params;
254             # specify invalid options as parameters to process()/include()
255 57         139 delete @p{ qw( caching_dir tt_cache_size ) };
256 57         404 %p
257             }
258             };
259              
260 57 50       525 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         367 ."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         170 my @stat = stat( $self->_file_path );
282              
283 57         114 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     950 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         491 print STDERR "file ".$self->_file_path." successfully stat()ed\n" if DEBUG;
297            
298 24         30 my $output;
299 24         35 eval {
300             $output =
301             $context->process(
302             'twostage:' # prefix for provider selection
303             .uri_escape( ref($self), UNSAFE ).'/'
304 24 100       121 .( do { my $dirs = join( '/', @{$self->_dynamic_dir_segments} ); $dirs ? $dirs.'/' : '' } )
  24         799  
  24         63  
  24         108  
305             .$self->_signature,
306             {},
307             $localize
308             );
309             };
310            
311 24 50       28113 $self->error( "Retrieval though stat()'ed successfully (".$self->_file_path."): FAILED ($@)\n" ) if $@;
312 24         34 print STDERR "Using cached output:\n\n $output\n\n" if DEBUG;
313              
314 24         213 return $output;
315             }
316              
317             # process precompiled component
318 33         249 $context->process( $self->_precompile, {}, $localize );
319             }
320              
321             sub include {
322 2     2 1 66 (shift)->process( @_, 1 );
323             }
324              
325              
326             sub purge {
327 11     11 1 520 my $self = shift;
328 11         23 my $class = ref($self);
329            
330 11         19 my $CACHE_DIR_NAME = CACHE_DIR_NAME;
331 11         43 my $caching_dir = $self->compiled_options( 'caching_dir' );
332              
333 11 100 33     19 if (
      66        
      66        
334 11         47 do { my $class_ue = uri_escape($class, UNSAFE ); $caching_dir =~ /$class_ue/; } &&
  11         914  
335             $caching_dir =~ /${CACHE_DIR_NAME}/ &&
336             -e $caching_dir &&
337             -d $caching_dir # kind of paranoia
338             ) {
339 10         20 eval { rmtree( $caching_dir, 0, 1 ) };
  10         8778  
340 10 50       49 if ( $@ ) {
341 0         0 $class->error( "Couldn't remove directory tree: $caching_dir. Error message: $@" );
342             }
343             }
344              
345 11         87 '';
346             }
347              
348              
349             sub _complement_keys {
350 57     57   85 my $self = shift;
351 57         64 my $keys = shift;
352              
353 57         166 my $callers = $self->{CONTEXT}->stash->get( 'component.callers' );
354              
355             +{
356 57         174 %{ $self->{CONFIG}->{extend_keys}->( $self ) },
357 57         378 %{$keys},
358             '_file_scope' =>
359 4         20 ( ref($callers) ? join( '\\', @{$callers} ) : '' )
360 57 100       2137 .$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   44 my $self = shift;
372 33         62 my $context = $self->{CONTEXT};
373 33         94 my $stash = $context->stash();
374            
375             my $TAGS_tag =
376             $TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{precompile_tag_style} }->[0]
377             .' TAGS '.$self->{CONFIG}->{runtime_tag_style}.' '
378 33         287 .$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         33 my $template;
383 33         38 eval {
384 33         219 $template = $context->process( $self->{params}->{template}, { TwoStage_precompile_mode => 1 }, 1 );
385             };
386              
387 33 50       5731 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         38 print STDERR "storing ".$self->_signature."\n\n" if DEBUG;
393              
394 33         37 eval { mkpath( $self->_file_dir, 0, 0700 ) };
  33         112  
395 33 50       111 if ($@) {
396 0         0 $self->error( "Couldn't create ".$self->_file_dir.": $@" );
397             }
398              
399 33 50       213 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             .join( "\n\t", map { "$_ => ".( defined $self->{params}->{keys}->{$_} ? $self->{params}->{keys}->{$_} : 'undef' ) } keys %{$self->{params}->{keys}} )."\n "
408 33   100     348 .$TAG_STYLE_unquotemeta->{ $self->{CONFIG}->{runtime_tag_style} }->[1]."\n"
409             ||
410             ''
411             )
412             .$template;
413            
414              
415 33 100       190 if ( Encode::is_utf8( $template ) ) {
416              
417 1         8 print STDERR "_precompile: encode\n" if DEBUG;
418 1         8 $out = Encode::decode_utf8( "\x{ef}\x{bb}\x{bf}" ).$out; # utf8 bom is stripped off again on load by Template::Provider
419 1         74 binmode( $fh ); # turn off crlf io layer!?
420 1     1   50 binmode( $fh, ':encoding(utf8)' );
  1         9  
  1         3  
  1         12  
421              
422             } else {
423              
424 32         32 print STDERR "_precompile: octets\n" if DEBUG;
425 32         107 binmode( $fh );
426             }
427            
428 33         1660 print $fh $out;
429 33         1323 close $fh;
430              
431 33         373 return \($TAGS_tag.$template);
432             }
433              
434             sub _signature {
435 81     81   180 my $self = shift;
436             # produce signature
437            
438             $self->{prec_template}->{signature}
439             ||=
440             sha1_hex(
441             join(
442             ':',
443             (
444             $self->{params}->{template},
445 81   100     371 map { "$_=".( $self->{params}->{keys}->{$_} || '' ) } sort keys %{$self->{params}->{keys}}
  69   66     974  
  57         258  
446             )
447             )
448             ).'.tt';
449             }
450              
451             sub _dynamic_dir_segments {
452 108     108   137 my $self = shift;
453            
454             $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             map { uri_escape( $_, UNSAFE ),
465 4         86 uri_escape( 'value-'.$self->{params}->{keys}->{$_}, UNSAFE )
466             }
467             ( ref( $self->{CONFIG}->{dir_keys} )
468             ?
469 6         15 grep( { exists $self->{params}->{keys}->{$_} } @{$self->{CONFIG}->{dir_keys}} )
  2         9  
470             :
471 108 100 66     1575 keys %{$self->{params}->{keys}}
  0 50 100     0  
    100          
472             )
473             )
474             :
475             ()
476             )
477             ];
478             }
479              
480             sub _rel_file_path {
481 57     57   69 my $self = shift;
482              
483 57   33     302 $self->{prec_template}->{rel_file_path} ||= &_concat_path( $self->_rel_file_dir, $self->_signature );
484             }
485              
486             sub _file_path {
487 90     90   100 my $self = shift;
488              
489 90   66     117930 $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   88 my $self = shift;
494            
495 90   100     255 $self->{prec_template}->{rel_file_dir} ||= File::Spec->catdir( @{$self->_dynamic_dir_segments} );
  84         178  
496             }
497              
498             sub _file_dir {
499 33     33   42 my $self = shift;
500              
501 33   33     169 $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   656 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         1188 my ($base_volume, $base_directories, $base_file) = File::Spec->splitpath( $base_path, 1 );
512             File::Spec->catpath(
513             $base_volume,
514             File::Spec->catdir(
515             File::Spec->splitdir( $base_directories ),
516 165 100       8729 ( ref($append_dirs) ? @{$append_dirs} : File::Spec->splitdir( $append_dirs ) )
  13         725  
517             )
518             ,
519             $base_file
520             );
521             }
522              
523              
524             1; # End of Template::Plugin::TwoStage
525              
526             __END__