view Lib/IMPL/Web/TT/Document.pm @ 201:0c018a247c8a

Reworked REST resource classes to be more transparent and intuitive
author sergey
date Tue, 24 Apr 2012 19:52:07 +0400
parents 4d0e1962161c
children
line wrap: on
line source

package IMPL::Web::TT::Document;
use strict;
use warnings;

use parent qw(IMPL::DOM::Document IMPL::Object::Disposable);
use Template::Context;
use Template::Provider;
use IMPL::Class::Property;
use File::Spec;
use Scalar::Util qw(blessed);
use IMPL::Web::TT::Collection;
use IMPL::Web::TT::Control;
use Carp;
use Encode();
use Data::Dumper;

BEGIN {
    private property _provider => prop_all;
    private property _context => prop_all;
    public property cache => prop_all;
    public property template => prop_get | owner_set;
    public property presenter => prop_all, { validate => \&_validatePresenter };
    public property preprocess => prop_all | prop_list,
    public property title => prop_all;
    private property _controlClassMap => prop_all;
}

our %CTOR = (
    'IMPL::DOM::Document' => sub { nodeName => 'document' }
);

sub CTOR {
    my ($this,%args) = @_;
    
    $this->_controlClassMap({});
    $this->registerControlClass( Control => 'IMPL::Web::TT::Control' );
    $this->appendChild( $this->Create(body => 'IMPL::Web::TT::Collection') );
    $this->appendChild( $this->Create(head => 'IMPL::Web::TT::Collection') );
    $this->cache($args{cache}) if $args{cache};
    $this->preprocess($args{preprocess}) if $args{preprocess};
}

sub CreateControl {
    my ($this,$name,$class,$args) = @_;
    
    $args = {} unless ref $args eq 'HASH';
    
    if (my $info = $this->_controlClassMap->{$class}) {
        my %nodeArgs = (%{$info->{args}},%$args);
        $nodeArgs{controlClass} = $class;
        
        return $this->Create($name,$info->{type},\%nodeArgs);
    } else {
        die new IMPL::Exception('A control is\'t registered', $class, $name);
    }
}

sub provider {
    my ($this,%args) = @_;
    
    if (my $provider = $this->_provider) {
        return $provider;
    } else {
        return $this->_provider(new Template::Provider(
            \%args
        ));
    }
}

sub context {
    my ($this) = @_;
    
    if (my $ctx = $this->_context) {
        return $ctx;
    } else {
        return $this->_context (
            new Template::Context(
                VARIABLES => {
                    document => $this,
                    this => $this,
                    render => sub {
                        $this->_process(@_);
                    },
                    encode => sub {
                        Encode::encode('utf8',shift);
                    },
                    dump => sub {
                        Dumper(shift);
                    },
                    as_list => sub {
                        [ map ref($_) eq 'ARRAY' ? @$_ : $_, @_ ]
                    }
                },
                RECURSION => 1,
                LOAD_TEMPLATES => [$this->provider]
            )
        )
    }
}

sub resolveVar {
    my ($this,$var) = @_;
    
    return $this->context->stash->get($var);
}

sub registerControlClass {
    my ($this, $controlClass, $type, $args) = @_;
    
    $type ||= 'IMPL::Web::TT::Control';
    
    die new IMPL::InvalidArgumentException("A controlClass must be a single word",$controlClass) unless $controlClass =~ /^\w+$/;
    
    eval "require $type; 1;" or die new IMPL::Exception("Failed to load a module",$type,"$@") unless eval { $type->can('new') };
    
    die new IMPL::InvalidArgumentException("A type must be subclass of IMPL::DOM::Node",$type) unless $type->isa('IMPL::DOM::Node');
    
    # resolve template name to a real template
    $args->{template} = $this->context->template($args->{template}) if $args->{template};
    
    $this->_controlClassMap->{$controlClass} = {
        controlClass => $controlClass,
        type => $type,
        args => ref $args eq 'HASH' ? $args : {}
    };
}

sub require {
    my ($this,$template) = @_;
    
    my $doc = $this->context->template($template);
    
    die new IMPL::InvalidOperationException("A specified template isn't a document",$template) unless eval{ $doc -> isa('Template::Document') };
    
    my $controlClass = $doc->class;
    my $type = $doc->nativeType;
    my $controlTemplate;
    my $out = "";
    
    die new IMPL::InvalidOperationException("A specified template isn't a control",$template) unless $controlClass;
    
    if (not $this->isControlClass($controlClass)) {
        if ($doc->template) {
            $controlTemplate = $doc->blocks()->{$doc->template} || $this->context->template($doc->template);
            $out = $this->context->include($doc);
        } else {
            $controlTemplate = $doc;
        }
        $this->registerControlClass($controlClass,$type,{ template => $controlTemplate } );
    }
    
    return $out;
}

sub isControlClass {
    my ($this,$name) = @_;
    return $this->_controlClassMap->{$name} ? 1 : 0;
}

sub _getControls {
    my ($this) = @_;
    
    my ($node) = $this->selectNodes('controls');
    return $node;
}

sub _validatePresenter {
    my ($this,$value) = @_;
    
    die new IMPL::InvalidArgumentException("A view object is required") unless blessed($value) and $value->isa('Template::View');
}

sub LoadFile {
    my ($this,$src,$encoding,$includes,$vars) = @_;
    
    die new IMPL::InvalidArgumentException("A template parameter is required") unless $src;
    
    $includes = [$includes] if $includes and not ref $includes;
    
    $encoding ||= 'utf8';
    
    $this->_context(undef);
    $this->_provider(undef);
    
    if (not ref $src) {
        my ($vol,$dir,$fileName) = File::Spec->splitpath($src);
        unshift @$includes, File::Spec->catpath($vol,$dir,'');
        $src = $fileName;
    }
    
    $this->provider(
        ENCODING => $encoding,
        INTERPOLATE => 1,
        PRE_CHOMP => 1,
        POST_CHOMP => 1,
        TRIM => 0,
        COMPILE_EXT => $this->cache ? '.ttc' : undef,
        COMPILE_DIR => $this->cache,
        INCLUDE_PATH => $includes 
    );
    
    if ($vars) {
        while ( my ($var,$val) = each %$vars ) {
            $this->AddVar($var,$val);
        }
    }
    
    $this->context->process($_) foreach $this->preprocess;
    
    my $template = $this->context->template($src);
    $this->title($template->title);
    if ( $template->template ) {
        $this->context->process($template);
        $this->template($template->template);
    } else {
        $this->template($template);
    }
    
}

sub AddVar {
    my ($this,$name,$value) = @_;
    
    $this->context->stash->set($name,$value);
}

sub Render {
    my ($this) = @_;
    
    return $this->context->process($this->template);
}

# Формирует представление для произвольных объектов 
sub _process {
    my ($this,@items) = @_;
    
    my @result;
    
    foreach my $item (@items) {
        if (blessed($item) and $item->isa('IMPL::Web::TT::Control')) {
            push @result, $item->Render();    
        } elsif(blessed($item)) {
            if ($this->presenter) {
                push @result, $this->presenter->print($item);
            } else {
                push @result, $this->toString;
            }
        } else {
            push @result, $item;
        }
    }
    
    return join '',@result;
}

our $AUTOLOAD;
sub AUTOLOAD {
    my $this = shift;
    my ($method) = ($AUTOLOAD =~ /(\w+)$/);
    
    if($method =~ /^create(\w+)/) {
        my ($name,$args) = @_;
        return $this->CreateControl($name,$1,$args);
    }
    
    my @result = $this->selectNodes($method);
    
    return $result[0] if @result;
    carp "Looks like you have a mistake, the document doesn't have a such property or child: $method";
    return;
}

sub Dispose {
    my ($this) = @_;
    
    $this->template(undef);
    $this->_context(undef);
    $this->_provider(undef);
    
    $this->supercall::Dispose();
}

1;
__END__
=pod

=head1 NAME

C<IMPL::Web::TT::Document> - Документ, позволяющий строить представление по шаблону

=head1 SYNOPSIS

=begin code 

// create new document
my $doc = new IMPL::Web::TT::Document;

// load template
$doc->loadFile('Templates/index.tt');

// render file
print $doc->Render();

=end code

=head1 DESCRIPTION

C<use parent qw(IMPL::DOM::Document)>

Документ, основанный на шаблоне Template::Toolkit. Позволяет загрузить шаблон,
и сформировать окончательный документ. Является наследником C<IMPL::DOM::Node>,
т.о. может быть использован для реализации DOM модели.

Внутри шаблона переменная C<document> ссылается на объект документа. По этой
причине образуется циклическая ссылка между объектами шаблона и документом, что
требует вызова метода C<Dispose> для освобождения документа.

=head1 METHODS

=over

=item C<CTOR()>

Создает новый экземпляр документа, свойство C<nodeName> устанавливается в 'C<document>'

=item C<$doc->LoadFile($fileName,$encoding)>

Загружает шаблон из файла C<$fileName>, используя кодировку C<$encoding>. Если
кодировка не указана, использует utf-8.

=item C<$doc->Render()>

Возвращает данные построенные на основе загруженного шаблона.

=item C<$doc->Dispose()>

Освобождает ресурсы и помечает объект как освобожденный.

=back

=head1 DOM

Документ представляет собой DOM документ, состоящий из узлов, которые представляют собой данные
для отображения. Для форматированого вывода используется C<template>.

В качестве элементов документа могут присутсвовать специальные объекты C<IMPL::Web::TT::Control>,
которые внутри содержат шаблон для форматирования собственного содержимого.



Документ предоставляет ряд фнукций для работы с элементами управления.

=head1 TEMPLATE

=begin code html

[% CALL document.registerClass( 'Table', 'My::TableClass', template => 'tables/pretty.tt' ) %]
[% CALL document.registerClass( 'Form' )%]

[% table = document.сreateTable('env') %]

[% FOEACH item in document.result %]
    [% table.rows.Add( item.get('name','value') ) %]
[% END %]

[% form = document.createForm('login') %]
[% form.template = 'LOGIN_FORM'%]

[% FOREACH item IN document.childNodes %]
    [%render(item)%]
[% END %]
    
[% BLOCK LOGIN_FORM %]
<form method="POST" action='/login.pl'>
    user: [% render(this.item('name')) %] password: [% render(this.item('password')) %] <input type="submit"/>
</form>
[% END %]

=end code html

=cut