view Lib/IMPL/Web/View/TTContext.pm @ 362:715b9959b207

WebView: initial work on metadata providers
author cin
date Thu, 28 Nov 2013 20:00:20 +0400
parents 833e663796c4
children cbf4febf0930
line wrap: on
line source

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'
	},
	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
	# view is a special variable, which will be cloned and passed to the nested context
	foreach my $prop (qw(
	   root
	   base
	   tt_ext
	   tt_cache
	   parent
	   prefix
	   cache
	   includes
	   modules
	   aliases
	   id
	   view
	)) {
		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 $ext = $this->tt_ext || "";
	
	my $file;
	
	foreach my $dir (@inc) {
		$file = $dir ? "$dir/$name" : $name;
		
		my $base = join('/',splice([split(/\/+/,$file)],0,-1));
		
		$file =  $ext ? "$file.$ext" : $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) } ) {
            	my $class;
            	if ($class = $tt->class) {
            		$class = $this->aliases->{$class} || $class;
            		Loader->safe->Require($class);
            	}
                my $info = {
		            base => $base,
		            labels => $this->load_labels($file),
		            template => $tt,
		            initialized => 0,
		            class => $class
		        };
		        $this->modules->{$file} = $info;
		        return $cache->{$name} = $info;
            } else {
            	my $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 $model = shift;
	my ($template, $args);
	
	if (ref $_[0] eq 'HASH') {
		$args = shift;
	} else {
		$template = shift;
		$args = shift;
	}
	
	my $prefix = $this->prefix;
	
	my $info;
	
    if (not(($args and delete $args->{_no_resolve}) or ref $model)) {
		$info = $this->resolve_model($model,$args);
	} else {
		$info = {
			model => $model,
			prefix => ""
		};
	}
	
	$template ||= $info->{template};
	$template = $template ? $this->find_template($template) : $this->find_template_for($info->{model});
	
	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;
    }
    
    $args ||= {};
    
    my $prefix = delete $args->{prefix} || $this->prefix;
    
    if (my $rel = delete $args->{rel}) {
    	$prefix = $prefix ? "${prefix}.${rel}" : $rel;
    }
    
    $template = $template ? $this->find_template($template) : $this->find_template_for($model);
    
    return $this->render(
        $template,
        hashApply(
            {
                prefix => $prefix,
                model => $model,
            },
            $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 $view = $this->view;
	$view = ref $view eq 'HASH' ? { %{$view} } : {};
	
	hashApply($view, delete $env->{view});
	
	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 => {},
		            view => $view
				},
                $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);
       	    }
	   },
	   hashMerge(
	       $info->{labels} || {},
		   {
		   	base => $info->{base},
		   	parent => $this,
		   	id => $this->get_next_id
		   }
	   )
	)
}

sub resolve_model {
	my ($this,$prefix,$args) = @_;
	
	die ArgException->new(prefix => "the prefix must be specified")
	   unless defined $prefix;
	

	
	if (my $res = $this->stash->get(['resolve', [$this,$prefix,$args]] ) ) {
		return $res;
	}
	
	my @comp = map { $_, 0 } grep length($_), split(/\.|\[(\d+)\]/, $prefix);
	
	return {
		model => $this->stash->get(['model',0,@comp]),
		prefix => $this->prefix ? $this->prefix . ".$prefix" : $prefix
	};
}

sub find_template_for {
	my ($this,$model, $nothrow) = @_;
	
	my $type = typeof($model);
	
	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;
            } 
                
            push @isa, @{"${sclass}::ISA"};
        }
		
	}
	$this->throw(Template::Constants::ERROR_FILE, "can't find a template for the model " . typeof($model))
		unless $nothrow;

	return;
}

sub get_real_file {
	my ($this,$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,$fname) = @_;
    
    $fname = $this->get_real_file($fname);
    
    my %vars;
    
    my $flabels = "$fname.labels";
        
    if (-f $flabels) {
        
        my %labels;
        $labels{default} = IMPL::Resources::Strings::ParseStringsMap($flabels);
        
        while(my($label,$text) = each %{$labels{default}}) {
            $vars{$label} = sub {
                my ($params) = @_;
                my $locale = Resources->currentLocale;
            
                unless ($labels{$locale}) {
                $labels{$locale} = -f "$fname.$locale" ? 
                    IMPL::Resources::Strings::ParseStringsMap("$fname.$locale") :
                    {};
                }
                    
                return FormatMessage(($labels{$locale}{$label} || $text),$params);
            }
        }
    }
    
    return \%vars;
}

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