view Lib/IMPL/DOM/Transform/ObjectToDOM.pm @ 311:d3b5a67ad2e8

sync
author cin
date Mon, 22 Apr 2013 03:42:53 +0400
parents c9c2ec29793f
children 50ff1595bd62
line wrap: on
line source

package IMPL::DOM::Transform::ObjectToDOM;
use strict;

use IMPL::Const qw(:prop :access);
use IMPL::declare {
    require => {
        PropertyInfo => 'IMPL::Class::PropertyInfo',
        Builder => 'IMPL::DOM::Navigator::Builder',
        Exception => 'IMPL::Exception',
        ArgumentException => '-IMPL::InvalidArgumentException',
        OperationException => '-IMPL::InvalidOperationException'
    },
    base => [
        'IMPL::Transform' => sub {
            -plain => 'TransformPlain',
            HASH => 'TransformHash',
            -default => 'TransformDefault'
        }
    ],
    props => [
        documentSchema => PROP_RO,
        _schema => PROP_RW,
        _navi => PROP_RW
    ]
};

use constant {
    SchemaNode => 'IMPL::DOM::Schema::Node',
    ComplexNode => 'IMPL::DOM::Schema::ComplexNode'
};

sub CTOR {
    my ($this,$docName,$docSchema,$transforms) = @_;
    
    my $docNodeSchema = $docSchema->selectSingleNode(sub { $_->isa(SchemaNode) and $_->name eq $docName } )
        or die OperationException->new("Can't find a node schema for the document '$docName'");
       
    my $docClass = ($docNodeSchema->can('nativeType') ? $docNodeSchema->nativeType : undef) || 'IMPL::DOM::Document';
    
    $this->documentSchema($docNodeSchema);
    
    $this->_navi(
        Builder->new(
            $docClass,
            $docSchema,
            ignoreUndefined => 1
        )
    );
    $this->_schema($docSchema);
    
    $this->_navi->NavigateCreate($docName);
    $this->currentNode->nodeProperty(schemaDocument => $docSchema);
}

sub TransformPlain {
    my ($this,$data) = @_;
    
    $this->_navi->Current->nodeValue( $data );
    return $this->_navi->Current;
}

sub currentNode {
    shift->_navi->Current;
}

sub inflateNodeValue {
    shift->_navi->inflateValue(shift);
}

sub TransformHash {
    my ($this,$data) = @_;
    
    die ArgumentException->new(data => 'A HASH reference is required')
        unless ref $data eq 'HASH';
        
    return $this->StoreObject($this->currentNode,$data)
        if !$this->currentNode->schema->isa(ComplexNode);

    KEYLOOP: foreach my $key (keys %$data) {
        my $value = $data->{$key};
        
        if (ref $value eq 'ARRAY') {
            #TODO: collapse empty values only if needed
            foreach my $subval (grep $_, @$value) {
                
                $this->_navi->saveState();
                
                my $node = $this->_navi->NavigateCreate($key);
                
                unless(defined $node) {
                    #$this->_navi->Back();
                    $this->_navi->restoreState();
                    next KEYLOOP;
                }
                
                $this->_navi->applyState();
                
                $this->Transform($subval);
                
                $this->_navi->Back();
            }
        } else {
            $this->_navi->saveState();
            my $node = $this->_navi->NavigateCreate($key);

            unless(defined $node) {
                #$this->_navi->Back();
                $this->_navi->restoreState();
                next KEYLOOP;
            }
            
            $this->_navi->applyState();
            
            $this->Transform($value);
            
            $this->_navi->Back();            
        }
    }
    return $this->_navi->Current;
}

# this method handles situatuions when a complex object must be stored in a
# simple node.
sub StoreObject {
    my ($this,$node,$data) = @_;
    
    $node->nodeValue($data);
    
    return $node;
}

sub TransformDefault {
    my ($this,$data) = @_;
    
    return $this->StoreObject($this->currentNode,$data)
        if !$this->currentNode->schema->isa(ComplexNode);
    
    if ( ref $data and eval { $data->can('GetMeta') } ) {
        my %props = map {
            $_->name, 1
        } $data->GetMeta(PropertyInfo, sub { $_->access == ACCESS_PUBLIC }, 1 );
        
        my %values = map {
            $_,
            $data->$_();
        } keys %props;
        
        return $this->Transform(\%values);
    } else {
        die OperationException->new("Don't know how to transform $data");
    }
    
    return $this->_navi->Current;
}

sub buildErrors {
    my ($this) = @_;
    
    return $this->_navi->buildErrors;
}

1;

__END__

=pod

=head1 NAME

C<IMPL::DOM::Transform::ObjectToDOM> -преобразование объекта в DOM документ.

=head1 SYNOPSIS 

=begin code

use IMPL::require {
    Schema => 'IMPL::DOM::Schema',
    Config => 'IMPL::Config'
}

my $data = {
    id => '12313-232',
    name => 'Peter',
    age => 20
};

my $schema = Schema->LoadSchema(Config->AppBase('schemas','person.xml'));
my $transorm = IMPL::DOM::Transform::ObjectToDOM->new('edit', $schema);

my $form = $transform->Transform($data);

my @errors;
    
push @errors, $transform->buildErrors;
push @errors, $schema->Validate($doc);

=end code

=head1 DESCRIPTION

Наследует C<IMPL::Transform>. Определяет базовые преобразования для хешей и
объектов, поддерживающих метаданные.

Результатом выполнения преобразования является DOM документ. При построении
документа используется навигатор C<IMPL::DOM::Navigator::Builder> для
сопоставления схемы и свойств преобразуемого объекта. Элементы полученного
документа имеют ссылки на соответствующие им элементы схемы.

После того, как документ построен и преобразование будет очищено, не останется
объектов, которые бы ссылались на документ со схемой, поскольку элементы схемы
имеют слабые ссылки на саму схему и не могут предотвратить ее удаление.
Для предотвращения очитски документа схемы, ссылка на него сохраняется в
атрибуте документа C<schemaDocument>, что обеспечит жизнь схемы на протяжении
жизни документа.

Преобразование происходит рекурсивно, сначала используется метод
C<NavigateCreate> для создания элемента соответсвующего свойству объекта,
затем вызывается метод C<Transform> для преобразования значения свойства, при
этом C<currentNode> указывает на только что созданный элемент документа.

Для изменения поведения преобразования можно добавлять новые обработчики, как
в случае со стандартным преобразованием, а также можно унаследовать текущий
класс для переопределения его некоторых методов.

=head1 MEMBERS

=head2 C<CTOR($docName,$schema)>

Создает преобразование, при этом будет создан документ состоящий только из
корневого элемента с именем C<$docName> и будет найдена подходящий для него
элемент схемы C<$schema>. 

=over

=item * C<$docName>

Имя корневого узла документа, которое будет использовано для поиска
соответствующего элемента схемы C<$schema>

=item * C<$schema>

Схема, содержащая описание документа. Если в данной схеме нет описания корневого
элемента с именем C<$docName>, будет вызвано исключение.

=back

=head2 C<[get]documentSchema>

Элемент схемы C<ComplexNode> соответствующий документу. Определяется в
конструкторе исходя из имени документа. 

=head2 C<[get]currentNode>

Текущий элемент документа. После создания преобразования - это сам документ.
Данное свойство использется внутри преобразования для работы с текущим
элементом.

=head2 C<[virtual]StoreObject($node,$data)>

Метод, который вызывается преобразованием в случае если текущий узел документа
является простым, а значени которое ему соответсвует является объектом (ссылкой).

По-умолчанию будет выполнено присваивание C<< $node->nodeValue($data) >>, однако
это можно заменить, например, на преобразование в строку.

=head2 C<inflateNodeValue($data)>

Метод который используется для преобразования значений к правильным типам,
используя атрибут C<inflator> элемента схемы. Этот метод можно использовать для
C<TransformPlain>, однако по-умолчанию он не используется, поскольку
предполагается, что входной объект имеет уже преобразованные значения в своих
свойствах.

=cut