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 | 
