diff 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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/IMPL/Web/View/TTContext.pm	Fri Sep 04 19:40:23 2015 +0300
@@ -0,0 +1,519 @@
+package IMPL::Web::View::TTContext;
+use strict;
+use Template::Base;
+use Carp qw(carp);
+use File::Spec();
+use IMPL::Resources::Format qw(FormatMessage);
+use IMPL::Resources::Strings();
+
+use IMPL::Exception();
+use IMPL::lang qw(is typeof hashApply hashMerge);
+use IMPL::declare {
+	require => {
+	   Document => '-Template::Document',
+	   TypeKeyedCollection => 'IMPL::TypeKeyedCollection',
+	   ArgException => '-IMPL::InvalidArgumentException',
+	   Resources => 'IMPL::Resources',
+	   Loader => 'IMPL::Code::Loader',
+	   MetadataBase => '-IMPL::Web::View::Metadata::BaseMeta',
+	   Metadata => 'IMPL::Web::View::Metadata::ObjectMeta',
+	   StringMap => 'IMPL::Resources::StringLocaleMap'
+	},
+	base => [
+		'Template::Context' => '@_'
+	]
+};
+
+BEGIN {
+	no strict 'refs';
+	# modules is a global (for the whole document) templates cache
+	# tt_cache is a local (for the current context only) templtes cache
+	foreach my $prop (qw(
+	   root
+	   base
+	   tt_ext
+	   tt_cache
+	   parent
+	   prefix
+	   cache
+	   includes
+	   modules
+	   aliases
+	   id
+	   metadata
+	   model
+	   templateInfo
+	)) {
+		my $t = $prop;
+		
+		*{__PACKAGE__ . '::' . $prop} = sub {
+			my $this = shift;
+			return @_ ? $this->stash->set($t, @_) : $this->stash->get($t);
+		}
+	}
+}
+
+sub clone {
+	my $this = shift;
+	my $params = shift;
+	
+	$this->localise();	
+
+	my $args = { %{$this} };
+
+	$this->delocalise();
+	
+	my $class = ref($this);
+	
+    delete $args->{CONFIG};
+    
+    my $clone = $class->new($args);
+    
+    $clone->stash->update($params) if $params;
+    
+    return $clone;
+}
+
+sub get_next_id {
+	my ($this) = @_;
+	
+	my $id = $this->stash->get('document.nextId') || 0;
+	$this->stash->set('document.nextId', $id + 1);
+	return "w-$id";
+}
+
+sub find_template {
+	my ($this,$name, $nothrow) = @_;
+	
+	my $cache = $this->tt_cache;
+	
+	$this->tt_cache($cache = {}) unless $cache;
+	
+	if(my $tpl = $cache->{$name}) {
+        return $tpl;
+	}
+	
+	my @inc = ($this->base, @{$this->includes || []});
+	#my @inc = @{$this->includes || []};
+	
+	my $ext = $this->tt_ext || "";
+	
+	#warn "find: $name";
+	
+	my $file;
+	
+	foreach my $dir (@inc) {
+		$file = $dir ? "$dir/$name" : $name;
+		
+		my @parts = split(/\/+/,$file);
+		
+		my $templateName = pop @parts;
+		
+		my $base = join('/',@parts);
+		
+		$file =  $ext ? "$file.$ext" : $file;
+		
+		#warn "  file: $file";
+		
+		if (exists($this->modules->{$file})) {
+			my $info = $this->modules->{$file};
+			return $cache->{$name} = $info
+                if $info;
+		} else {
+            if( my $tt = eval { $this->template($file) } ) {
+            	#warn "  found: $file";
+            	my $class;
+            	if ($class = $tt->class) {
+            		$class = $this->aliases->{$class} || $class;
+            		Loader->safe->Require($class);
+            	}
+                my $info = {
+		            base => $base,
+		            name => $templateName,
+		            template => $tt,
+		            initialized => 0,
+		            class => $class,
+		            file => $file
+		        };
+		        $this->modules->{$file} = $info;
+		        return $cache->{$name} = $info;
+            } else {
+            	my $err = $@;
+            	
+            	#warn "  not found: $err";
+            	
+            	for(my $t = $err; is($t,'Template::Exception'); $t = $t->info ) {
+            		die $err unless $t->type eq Template::Constants::ERROR_FILE;
+            	}
+            	$this->modules->{$file} = undef;
+            }
+		}
+	}
+	
+	$this->throw(Template::Constants::ERROR_FILE, "$name: not found")
+		unless $nothrow;
+	return;
+}
+
+sub display_for {
+	my $this = shift;
+	my $path = shift;
+	my ($template, $args);
+	
+	if (ref $_[0] eq 'HASH') {
+		$args = shift;
+	} else {
+		$template = shift;
+		$args = shift;
+	}
+	
+	my $prefix = $this->prefix;
+	
+	my $info;
+	my $meta = $this->resolve_model($path,$args)
+		or return "[not found '$path']";
+	
+	$info->{prefix} = join('.', grep($_, $prefix, $path));
+	$info->{model} = $meta->model;
+	$info->{metadata} = $meta;
+	
+	$template ||= $info->{template};
+	$template = $template ? $this->find_template($template) : $this->find_template_for($info->{metadata});
+	
+	return $this->render(
+        $template,
+        hashApply(
+            $info,
+            $args
+        )
+    );
+}
+
+sub display_model {
+	my $this = shift;
+    my $model = shift;
+    my ($template, $args);
+    
+    if (ref $_[0] eq 'HASH') {
+        $args = shift;
+    } else {
+        $template = shift;
+        $args = shift;
+    }
+    
+    #copy
+    $args = { %{$args || {}} };
+    
+    $args->{prefix} = join('.',grep($_,$this->prefix,$args->{path}))
+    	unless defined $args->{prefix};
+    	
+    if (is($model,MetadataBase)) {
+    	$args->{model} = $model->model;
+    	$args->{metadata} = $model;
+    } else {
+    	$args->{model} = $model;
+    	$args->{metadata} = Metadata->GetMetadataForModel($model);
+    }
+    
+    $template = $template ? $this->find_template($template) : $this->find_template_for($args->{metadata});
+    
+    return $this->render(
+        $template,
+        $args
+    );
+}
+
+# обеспечивает необходимый уровень изоляции между контекстами
+# $code - код, который нужно выполнить в новом контексте
+# $env - хеш с переменными, которые будут переданы в новый контекст
+# в процессе будет создан клон корневого контекста, со всеми его свойствами
+# затем новый контекст будет локализован и в него будут добавлены новые переменные из $env
+# созданный контекст будет передан параметром в $code
+sub invoke_environment {
+	my ($this,$code,$env) = @_;
+	
+	$env ||= {};
+	
+	my $ctx = ($this->root || $this)->clone();
+	
+	my @includes = @{$this->includes || []};
+	
+	if ($this->base) {
+		unshift @includes, $this->base;
+	}
+	
+	my $out = eval {
+		$ctx->localise(
+            hashApply(
+	            {
+	            	includes => \@includes,
+	            	aliases => $this->aliases || {},
+					root => $this->root || $ctx,
+					modules => $this->modules || {},
+					cache => TypeKeyedCollection->new(),
+		            display_for => sub {
+		                $ctx->display_for(@_);
+		            },
+		            render => sub {
+		            	$ctx->render(@_);
+		            },
+		            display_model => sub {
+		            	$ctx->display_model(@_);
+		            },
+		            tt_cache => {},
+		            labels => sub {
+		            	$ctx->load_labels(@_);
+		            }
+				},
+                $env
+            )
+        );
+		
+		&$code($ctx);
+	};
+	
+	my $e = $@;
+	$ctx->delocalise();
+	
+	die $e if $e;
+    
+    return $out;
+}
+
+# использует указанный шаблон для создания фрагмента документа
+# шаблон может быть как именем, так и хешем, содержащим информацию
+# о шаблоне.
+# отдельно следует отметить, что данный метод создает новый контекст
+# для выполнения шаблона в котором задает переменные base, parent, id
+# а также создает переменные для строковых констант из labels
+# хеш с переменными $args будет передан самому шаблону в момент выполнения
+# если у шаблона указан класс элемента управления, то при выполнении шаблона
+# будет создан экземпляр этого класса и процесс выполнения шаблона будет
+# делегирован методу Render этого экземпляра. 
+sub render {
+	my ($this,$template,$args) = @_;
+	
+	$args ||= {};
+	
+	my $info = ref $template ? $template : $this->find_template($template);
+	
+	if (ref($info) ne 'HASH') {
+		carp "got an invalid template object: $info (" . ref($info) . ")";
+    	$info = {
+    		template => $info,
+    		base => $this->base,
+    		initialized => 1
+    	};
+    }
+    
+    return $this->invoke_environment(
+        sub {
+       	    my $ctx = shift;
+       	    
+       	    unless($info->{initialized}) {
+       	        if(my $init = $info->{template}->blocks->{INIT}) {
+       	        	$info->{initialized} = 1;
+       	            eval {
+       	                $ctx->visit($info->{template}->blocks);
+       	                $ctx->include($init);
+       	            };
+       	            $ctx->leave();
+       	        }
+       	    }
+       	    
+       	    if (my $class = $info->{class}) {
+       	    	$class->new($ctx,$info->{template},$args)->Render({});
+       	    } else {
+            	return $ctx->include($info->{template},$args);
+       	    }
+		},
+		{
+			base => $info->{base},
+			parent => $this,
+			id => $this->get_next_id,
+			templateInfo => $info
+		}
+	)
+}
+
+sub resolve_model {
+	my ($this,$prefix) = @_;
+	
+	die ArgException->new(prefix => "the prefix must be specified")
+	   unless defined $prefix;
+	
+	my $meta = $this->metadata;
+	unless($meta) {
+		$meta = Metadata->GetMetadataForModel($this->model);
+		$this->metadata($meta);
+	}
+	
+	foreach my $part (grep length($_), split(/\.|\[(\d+)\]/, $prefix)) {
+		last unless $meta;
+		if ($part =~ /^\d+$/) {
+			$meta = $meta->GetItem($part);
+		} else {
+			$meta = $meta->GetProperty($part);
+		}
+	}
+	
+	return $meta;
+}
+
+sub find_template_for {
+	my ($this,$meta, $nothrow) = @_;
+	
+	die ArgException->new(meta => 'An invalid metadata is supplied')
+		unless is($meta,MetadataBase);
+	
+	return $this->find_template($meta->template)
+		if ($meta->template);
+	
+	my $type = $meta->modelType;
+	
+	return $this->find_template('templates/plain') unless $type;
+	
+	if (my $template = $this->cache->Get($type)) {
+		return $template;
+	} else {
+		
+		no strict 'refs';
+               
+        my @isa = $type;
+        
+        while (@isa) {
+            my $sclass = shift @isa;
+            
+            (my $name = $sclass) =~ s/:+/_/g;
+            my ($shortName) = ($sclass =~ m/(\w+)$/);
+
+            $template = $this->find_template("templates/$name",1) || $this->find_template("templates/$shortName",1);
+            
+            if ($template) {
+            	$this->cache->Set($sclass,$template);
+            	return $template;
+            } 
+            
+            #todo $meta->GetISA to implement custom hierachy 
+            push @isa, @{"${sclass}::ISA"};
+        }
+		
+	}
+	$this->throw(Template::Constants::ERROR_FILE, "can't find a template for the model $type")
+		unless $nothrow;
+
+	return;
+}
+
+sub get_real_file {
+	my ($this,$fname) = @_;
+	
+	return unless length $fname;
+	
+	my @path = split(/\/+/,$fname);
+	
+	foreach my $provider (@{$this->load_templates || []}) {
+		foreach my $dir (@{$provider->paths || []}) {
+			my $realName = File::Spec->catfile($dir,@path);
+			return $realName if -f $realName; 
+		}
+	}
+}
+
+sub load_labels {
+    my ($this,$data) = @_;
+    
+    die ArgException->new("A hash reference is required")
+    	unless ref($data) eq 'HASH';
+    
+    my $stringMap = StringMap->new($data);
+    
+    $this->stash->update({
+    	map {
+    		my $id = $_;
+    		$id,
+    		sub {
+    			$stringMap->GetString($id,@_);
+    		};
+    	} keys %$data
+    });
+    
+    my $ti = $this->templateInfo || {};
+    
+    if (my $fullName = $this->get_real_file($ti->{file})) {
+	    my ($vol,$dir,$fname) = File::Spec->splitpath($fullName);
+	    
+	    my $name = $this->templateInfo->{name};
+	    
+	    my $localePath = File::Spec->catpath($vol, File::Spec->catdir($dir,'locale'),'');
+	    
+	    $stringMap->name($name);
+	    $stringMap->paths($localePath);
+	}
+    return;
+}
+
+1;
+
+__END__
+
+=pod
+
+=head1 NAME
+
+C<IMPL::Web::View::TTContext> - доработанная версия контекста
+
+=head1 DESCRIPTION
+
+Расширяет функции C<Template::Context>
+
+=begin plantuml
+
+@startuml
+
+object RootContext {
+    document
+    globals
+}
+
+object DocumentContext {
+    base
+    extends
+}
+
+object ControlContext {
+    base
+    extends
+}
+
+RootContext o-- DocumentContext 
+RootContext o-- ControlContext 
+
+Document -- DocumentContext
+Control - ControlContext
+
+Loader . RootContext: <<creates>>
+Loader . Document: <<creates>>
+Loader -up- Registry
+
+@enduml
+
+=end plantuml
+
+=head1 MEMBERS
+
+=head2 C<[get,set]base>
+
+Префикс пути для поиска шаблонов
+
+=head2 C<template($name)>
+
+Сначала пытается загрузить шаблон используя префикс C<base>, затем без префикса.
+
+=head2 C<clone()>
+
+Создает копию контекста, при этом C<stash> локализуется, таким образом
+клонированный контекст имеет собственное пространство имен, вложенное в
+пространство родительского контекста.
+
+=cut
\ No newline at end of file