Mercurial > pub > Impl
comparison lib/IMPL/Web/View/TTContext.pm @ 407:c6e90e02dd17 ref20150831
renamed Lib->lib
author | cin |
---|---|
date | Fri, 04 Sep 2015 19:40:23 +0300 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
406:f23fcb19d3c1 | 407:c6e90e02dd17 |
---|---|
1 package IMPL::Web::View::TTContext; | |
2 use strict; | |
3 use Template::Base; | |
4 use Carp qw(carp); | |
5 use File::Spec(); | |
6 use IMPL::Resources::Format qw(FormatMessage); | |
7 use IMPL::Resources::Strings(); | |
8 | |
9 use IMPL::Exception(); | |
10 use IMPL::lang qw(is typeof hashApply hashMerge); | |
11 use IMPL::declare { | |
12 require => { | |
13 Document => '-Template::Document', | |
14 TypeKeyedCollection => 'IMPL::TypeKeyedCollection', | |
15 ArgException => '-IMPL::InvalidArgumentException', | |
16 Resources => 'IMPL::Resources', | |
17 Loader => 'IMPL::Code::Loader', | |
18 MetadataBase => '-IMPL::Web::View::Metadata::BaseMeta', | |
19 Metadata => 'IMPL::Web::View::Metadata::ObjectMeta', | |
20 StringMap => 'IMPL::Resources::StringLocaleMap' | |
21 }, | |
22 base => [ | |
23 'Template::Context' => '@_' | |
24 ] | |
25 }; | |
26 | |
27 BEGIN { | |
28 no strict 'refs'; | |
29 # modules is a global (for the whole document) templates cache | |
30 # tt_cache is a local (for the current context only) templtes cache | |
31 foreach my $prop (qw( | |
32 root | |
33 base | |
34 tt_ext | |
35 tt_cache | |
36 parent | |
37 prefix | |
38 cache | |
39 includes | |
40 modules | |
41 aliases | |
42 id | |
43 metadata | |
44 model | |
45 templateInfo | |
46 )) { | |
47 my $t = $prop; | |
48 | |
49 *{__PACKAGE__ . '::' . $prop} = sub { | |
50 my $this = shift; | |
51 return @_ ? $this->stash->set($t, @_) : $this->stash->get($t); | |
52 } | |
53 } | |
54 } | |
55 | |
56 sub clone { | |
57 my $this = shift; | |
58 my $params = shift; | |
59 | |
60 $this->localise(); | |
61 | |
62 my $args = { %{$this} }; | |
63 | |
64 $this->delocalise(); | |
65 | |
66 my $class = ref($this); | |
67 | |
68 delete $args->{CONFIG}; | |
69 | |
70 my $clone = $class->new($args); | |
71 | |
72 $clone->stash->update($params) if $params; | |
73 | |
74 return $clone; | |
75 } | |
76 | |
77 sub get_next_id { | |
78 my ($this) = @_; | |
79 | |
80 my $id = $this->stash->get('document.nextId') || 0; | |
81 $this->stash->set('document.nextId', $id + 1); | |
82 return "w-$id"; | |
83 } | |
84 | |
85 sub find_template { | |
86 my ($this,$name, $nothrow) = @_; | |
87 | |
88 my $cache = $this->tt_cache; | |
89 | |
90 $this->tt_cache($cache = {}) unless $cache; | |
91 | |
92 if(my $tpl = $cache->{$name}) { | |
93 return $tpl; | |
94 } | |
95 | |
96 my @inc = ($this->base, @{$this->includes || []}); | |
97 #my @inc = @{$this->includes || []}; | |
98 | |
99 my $ext = $this->tt_ext || ""; | |
100 | |
101 #warn "find: $name"; | |
102 | |
103 my $file; | |
104 | |
105 foreach my $dir (@inc) { | |
106 $file = $dir ? "$dir/$name" : $name; | |
107 | |
108 my @parts = split(/\/+/,$file); | |
109 | |
110 my $templateName = pop @parts; | |
111 | |
112 my $base = join('/',@parts); | |
113 | |
114 $file = $ext ? "$file.$ext" : $file; | |
115 | |
116 #warn " file: $file"; | |
117 | |
118 if (exists($this->modules->{$file})) { | |
119 my $info = $this->modules->{$file}; | |
120 return $cache->{$name} = $info | |
121 if $info; | |
122 } else { | |
123 if( my $tt = eval { $this->template($file) } ) { | |
124 #warn " found: $file"; | |
125 my $class; | |
126 if ($class = $tt->class) { | |
127 $class = $this->aliases->{$class} || $class; | |
128 Loader->safe->Require($class); | |
129 } | |
130 my $info = { | |
131 base => $base, | |
132 name => $templateName, | |
133 template => $tt, | |
134 initialized => 0, | |
135 class => $class, | |
136 file => $file | |
137 }; | |
138 $this->modules->{$file} = $info; | |
139 return $cache->{$name} = $info; | |
140 } else { | |
141 my $err = $@; | |
142 | |
143 #warn " not found: $err"; | |
144 | |
145 for(my $t = $err; is($t,'Template::Exception'); $t = $t->info ) { | |
146 die $err unless $t->type eq Template::Constants::ERROR_FILE; | |
147 } | |
148 $this->modules->{$file} = undef; | |
149 } | |
150 } | |
151 } | |
152 | |
153 $this->throw(Template::Constants::ERROR_FILE, "$name: not found") | |
154 unless $nothrow; | |
155 return; | |
156 } | |
157 | |
158 sub display_for { | |
159 my $this = shift; | |
160 my $path = shift; | |
161 my ($template, $args); | |
162 | |
163 if (ref $_[0] eq 'HASH') { | |
164 $args = shift; | |
165 } else { | |
166 $template = shift; | |
167 $args = shift; | |
168 } | |
169 | |
170 my $prefix = $this->prefix; | |
171 | |
172 my $info; | |
173 my $meta = $this->resolve_model($path,$args) | |
174 or return "[not found '$path']"; | |
175 | |
176 $info->{prefix} = join('.', grep($_, $prefix, $path)); | |
177 $info->{model} = $meta->model; | |
178 $info->{metadata} = $meta; | |
179 | |
180 $template ||= $info->{template}; | |
181 $template = $template ? $this->find_template($template) : $this->find_template_for($info->{metadata}); | |
182 | |
183 return $this->render( | |
184 $template, | |
185 hashApply( | |
186 $info, | |
187 $args | |
188 ) | |
189 ); | |
190 } | |
191 | |
192 sub display_model { | |
193 my $this = shift; | |
194 my $model = shift; | |
195 my ($template, $args); | |
196 | |
197 if (ref $_[0] eq 'HASH') { | |
198 $args = shift; | |
199 } else { | |
200 $template = shift; | |
201 $args = shift; | |
202 } | |
203 | |
204 #copy | |
205 $args = { %{$args || {}} }; | |
206 | |
207 $args->{prefix} = join('.',grep($_,$this->prefix,$args->{path})) | |
208 unless defined $args->{prefix}; | |
209 | |
210 if (is($model,MetadataBase)) { | |
211 $args->{model} = $model->model; | |
212 $args->{metadata} = $model; | |
213 } else { | |
214 $args->{model} = $model; | |
215 $args->{metadata} = Metadata->GetMetadataForModel($model); | |
216 } | |
217 | |
218 $template = $template ? $this->find_template($template) : $this->find_template_for($args->{metadata}); | |
219 | |
220 return $this->render( | |
221 $template, | |
222 $args | |
223 ); | |
224 } | |
225 | |
226 # обеспечивает необходимый уровень изоляции между контекстами | |
227 # $code - код, который нужно выполнить в новом контексте | |
228 # $env - хеш с переменными, которые будут переданы в новый контекст | |
229 # в процессе будет создан клон корневого контекста, со всеми его свойствами | |
230 # затем новый контекст будет локализован и в него будут добавлены новые переменные из $env | |
231 # созданный контекст будет передан параметром в $code | |
232 sub invoke_environment { | |
233 my ($this,$code,$env) = @_; | |
234 | |
235 $env ||= {}; | |
236 | |
237 my $ctx = ($this->root || $this)->clone(); | |
238 | |
239 my @includes = @{$this->includes || []}; | |
240 | |
241 if ($this->base) { | |
242 unshift @includes, $this->base; | |
243 } | |
244 | |
245 my $out = eval { | |
246 $ctx->localise( | |
247 hashApply( | |
248 { | |
249 includes => \@includes, | |
250 aliases => $this->aliases || {}, | |
251 root => $this->root || $ctx, | |
252 modules => $this->modules || {}, | |
253 cache => TypeKeyedCollection->new(), | |
254 display_for => sub { | |
255 $ctx->display_for(@_); | |
256 }, | |
257 render => sub { | |
258 $ctx->render(@_); | |
259 }, | |
260 display_model => sub { | |
261 $ctx->display_model(@_); | |
262 }, | |
263 tt_cache => {}, | |
264 labels => sub { | |
265 $ctx->load_labels(@_); | |
266 } | |
267 }, | |
268 $env | |
269 ) | |
270 ); | |
271 | |
272 &$code($ctx); | |
273 }; | |
274 | |
275 my $e = $@; | |
276 $ctx->delocalise(); | |
277 | |
278 die $e if $e; | |
279 | |
280 return $out; | |
281 } | |
282 | |
283 # использует указанный шаблон для создания фрагмента документа | |
284 # шаблон может быть как именем, так и хешем, содержащим информацию | |
285 # о шаблоне. | |
286 # отдельно следует отметить, что данный метод создает новый контекст | |
287 # для выполнения шаблона в котором задает переменные base, parent, id | |
288 # а также создает переменные для строковых констант из labels | |
289 # хеш с переменными $args будет передан самому шаблону в момент выполнения | |
290 # если у шаблона указан класс элемента управления, то при выполнении шаблона | |
291 # будет создан экземпляр этого класса и процесс выполнения шаблона будет | |
292 # делегирован методу Render этого экземпляра. | |
293 sub render { | |
294 my ($this,$template,$args) = @_; | |
295 | |
296 $args ||= {}; | |
297 | |
298 my $info = ref $template ? $template : $this->find_template($template); | |
299 | |
300 if (ref($info) ne 'HASH') { | |
301 carp "got an invalid template object: $info (" . ref($info) . ")"; | |
302 $info = { | |
303 template => $info, | |
304 base => $this->base, | |
305 initialized => 1 | |
306 }; | |
307 } | |
308 | |
309 return $this->invoke_environment( | |
310 sub { | |
311 my $ctx = shift; | |
312 | |
313 unless($info->{initialized}) { | |
314 if(my $init = $info->{template}->blocks->{INIT}) { | |
315 $info->{initialized} = 1; | |
316 eval { | |
317 $ctx->visit($info->{template}->blocks); | |
318 $ctx->include($init); | |
319 }; | |
320 $ctx->leave(); | |
321 } | |
322 } | |
323 | |
324 if (my $class = $info->{class}) { | |
325 $class->new($ctx,$info->{template},$args)->Render({}); | |
326 } else { | |
327 return $ctx->include($info->{template},$args); | |
328 } | |
329 }, | |
330 { | |
331 base => $info->{base}, | |
332 parent => $this, | |
333 id => $this->get_next_id, | |
334 templateInfo => $info | |
335 } | |
336 ) | |
337 } | |
338 | |
339 sub resolve_model { | |
340 my ($this,$prefix) = @_; | |
341 | |
342 die ArgException->new(prefix => "the prefix must be specified") | |
343 unless defined $prefix; | |
344 | |
345 my $meta = $this->metadata; | |
346 unless($meta) { | |
347 $meta = Metadata->GetMetadataForModel($this->model); | |
348 $this->metadata($meta); | |
349 } | |
350 | |
351 foreach my $part (grep length($_), split(/\.|\[(\d+)\]/, $prefix)) { | |
352 last unless $meta; | |
353 if ($part =~ /^\d+$/) { | |
354 $meta = $meta->GetItem($part); | |
355 } else { | |
356 $meta = $meta->GetProperty($part); | |
357 } | |
358 } | |
359 | |
360 return $meta; | |
361 } | |
362 | |
363 sub find_template_for { | |
364 my ($this,$meta, $nothrow) = @_; | |
365 | |
366 die ArgException->new(meta => 'An invalid metadata is supplied') | |
367 unless is($meta,MetadataBase); | |
368 | |
369 return $this->find_template($meta->template) | |
370 if ($meta->template); | |
371 | |
372 my $type = $meta->modelType; | |
373 | |
374 return $this->find_template('templates/plain') unless $type; | |
375 | |
376 if (my $template = $this->cache->Get($type)) { | |
377 return $template; | |
378 } else { | |
379 | |
380 no strict 'refs'; | |
381 | |
382 my @isa = $type; | |
383 | |
384 while (@isa) { | |
385 my $sclass = shift @isa; | |
386 | |
387 (my $name = $sclass) =~ s/:+/_/g; | |
388 my ($shortName) = ($sclass =~ m/(\w+)$/); | |
389 | |
390 $template = $this->find_template("templates/$name",1) || $this->find_template("templates/$shortName",1); | |
391 | |
392 if ($template) { | |
393 $this->cache->Set($sclass,$template); | |
394 return $template; | |
395 } | |
396 | |
397 #todo $meta->GetISA to implement custom hierachy | |
398 push @isa, @{"${sclass}::ISA"}; | |
399 } | |
400 | |
401 } | |
402 $this->throw(Template::Constants::ERROR_FILE, "can't find a template for the model $type") | |
403 unless $nothrow; | |
404 | |
405 return; | |
406 } | |
407 | |
408 sub get_real_file { | |
409 my ($this,$fname) = @_; | |
410 | |
411 return unless length $fname; | |
412 | |
413 my @path = split(/\/+/,$fname); | |
414 | |
415 foreach my $provider (@{$this->load_templates || []}) { | |
416 foreach my $dir (@{$provider->paths || []}) { | |
417 my $realName = File::Spec->catfile($dir,@path); | |
418 return $realName if -f $realName; | |
419 } | |
420 } | |
421 } | |
422 | |
423 sub load_labels { | |
424 my ($this,$data) = @_; | |
425 | |
426 die ArgException->new("A hash reference is required") | |
427 unless ref($data) eq 'HASH'; | |
428 | |
429 my $stringMap = StringMap->new($data); | |
430 | |
431 $this->stash->update({ | |
432 map { | |
433 my $id = $_; | |
434 $id, | |
435 sub { | |
436 $stringMap->GetString($id,@_); | |
437 }; | |
438 } keys %$data | |
439 }); | |
440 | |
441 my $ti = $this->templateInfo || {}; | |
442 | |
443 if (my $fullName = $this->get_real_file($ti->{file})) { | |
444 my ($vol,$dir,$fname) = File::Spec->splitpath($fullName); | |
445 | |
446 my $name = $this->templateInfo->{name}; | |
447 | |
448 my $localePath = File::Spec->catpath($vol, File::Spec->catdir($dir,'locale'),''); | |
449 | |
450 $stringMap->name($name); | |
451 $stringMap->paths($localePath); | |
452 } | |
453 return; | |
454 } | |
455 | |
456 1; | |
457 | |
458 __END__ | |
459 | |
460 =pod | |
461 | |
462 =head1 NAME | |
463 | |
464 C<IMPL::Web::View::TTContext> - доработанная версия контекста | |
465 | |
466 =head1 DESCRIPTION | |
467 | |
468 Расширяет функции C<Template::Context> | |
469 | |
470 =begin plantuml | |
471 | |
472 @startuml | |
473 | |
474 object RootContext { | |
475 document | |
476 globals | |
477 } | |
478 | |
479 object DocumentContext { | |
480 base | |
481 extends | |
482 } | |
483 | |
484 object ControlContext { | |
485 base | |
486 extends | |
487 } | |
488 | |
489 RootContext o-- DocumentContext | |
490 RootContext o-- ControlContext | |
491 | |
492 Document -- DocumentContext | |
493 Control - ControlContext | |
494 | |
495 Loader . RootContext: <<creates>> | |
496 Loader . Document: <<creates>> | |
497 Loader -up- Registry | |
498 | |
499 @enduml | |
500 | |
501 =end plantuml | |
502 | |
503 =head1 MEMBERS | |
504 | |
505 =head2 C<[get,set]base> | |
506 | |
507 Префикс пути для поиска шаблонов | |
508 | |
509 =head2 C<template($name)> | |
510 | |
511 Сначала пытается загрузить шаблон используя префикс C<base>, затем без префикса. | |
512 | |
513 =head2 C<clone()> | |
514 | |
515 Создает копию контекста, при этом C<stash> локализуется, таким образом | |
516 клонированный контекст имеет собственное пространство имен, вложенное в | |
517 пространство родительского контекста. | |
518 | |
519 =cut |