407
|
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 |