view Lib/IMPL/DOM/Schema/Validator/Compare.pm @ 384:4edd36025051

DOM schema refactoring
author cin
date Mon, 10 Feb 2014 17:41:34 +0400
parents 89179bb8c388
children 5aff94ba842f
line wrap: on
line source

package IMPL::DOM::Schema::Validator::Compare;
use strict;

use parent qw(IMPL::DOM::Schema::Validator);

use IMPL::Resources::Format qw(FormatMessage);
use IMPL::Class::Property;

BEGIN {
    public property targetProperty => prop_all;
    public property op => prop_all;
    public property nodePath => prop_all;
    public property optional => prop_all;
    private property _pathTranslated => prop_all;
    private property _targetNode => prop_all;
    private property _sourceSchema => prop_all;
    public property message => prop_all;
}

our %CTOR = (
    'IMPL::DOM::Schema::Validator' => sub {
        my %args = @_;
        $args{nodeName} ||= 'Compare';
        delete @args{qw(targetProperty op nodePath optional message)};
        %args;
    }
);

our %Ops = (
    '='  => \&_equals,
    'eq' => \&_equalsString,
    '!=' => \&_notEquals,
    'ne' => \&_notEqualsString,
    '=~' => \&_matchRx,
    '!~' => \&_notMatchRx,
    '<'  => \&_less,
    '>'  => \&_greater,
    'lt' => \&_lessString,
    'gt' => \&_greaterString
);

my $rxOps = map qr/$_/, join( '|', keys %Ops );

sub CTOR {
    my ($this,%args) = @_;
    
    $this->targetProperty($args{targetProperty} || 'nodeValue');
    $this->op( $Ops{ $args{op} || '=' } ) or die new IMPL::InvalidArgumentException("Invalid parameter value",'op',$args{op},$this->path);
    $this->nodePath($args{nodePath}) or die new IMPL::InvalidArgumentException("The argument is required", 'nodePath', $this->path);
    $this->message($args{message} || 'The value of %node.path% %source.op% %value% (%source.nodePath%)' );
    $this->optional($args{optional}) if $args{optional};
}

sub TranslatePath {
    my ($this,$path) = @_;
    
    $path ||= '';
    
    my @selectQuery;
    
    my $i = 0;
    
    foreach my $chunk (split /\//,$path) {
        $chunk = 'document:*' if $i == 0 and not length $chunk;
        next if not length $chunk;
        
        my $query;        
        my ($axis,$filter) = ( $chunk =~ /^(?:(\w+):)?(.*)$/);
        
        if ($filter =~ /^\w+|\*$/ ) {
            $query = $filter eq '*' ? undef : $filter;
        } elsif ( $filter =~ /^(\w+|\*)\s*((?:\[\s*\w+\s*(?:=|!=|=~|!~|eq|ne|lt|gt)\s*["'](?:[^\\'"]|\\[\\"'])*["']\])+)$/) {
            my ($nodeName,$filterArgs) = ($1,$2);

            
            my @parsedFilters = map {
                my ($prop,$op,$value) = ($_ =~ /\s*(\w+)\s*(=|!=|=~|!~|eq|ne|lt|gt)\s*(?:["']((?:[^\\'"]|\\[\\"'])*)["'])/);

                $value =~ s/\\[\\'"]/$1/g;
                {
                    prop => $prop,
                    op => $Ops{$op},
                    value => $value
                }
            } grep ( $_, split ( /[\]\[]+/,$filterArgs ) );
            
            $query = sub {
                my ($node) = shift;
                
                $node->nodeName eq $nodeName or return 0 if $nodeName ne '*';
                $_->{op}->(
                        _resovleProperty($node,$_->{prop}),
                        FormatMessage($_->{value},{
                            Schema => $this->parentNode,
                            Node => $this->_targetNode,
                            schema => $this->parentNode,
                            node => $this->_targetNode,
                            source => $this->_sourceSchema
                        },\&_resovleProperty)
                    ) or return 0 foreach @parsedFilters;
                return 1;
            };
        } else {
            die new IMPL::Exception("Invalid query syntax",$path,$chunk);
        }
        
        push @selectQuery, $axis ? { $axis => $query } : $query;
        
        $i++;
    }
    
    return \@selectQuery;
}

sub Validate {
    my ($this,$node,$ctx) = @_;
    
    my @result;
    
    my $Source = $ctx && $ctx->{Source} || $this->parentNode;
    
    $this->_sourceSchema($Source);
    
    $this->_targetNode($node);
    
    my $query = $this->_pathTranslated() || $this->_pathTranslated($this->TranslatePath($this->nodePath));
    
    my ($foreignNode) = $node->selectNodes(@$query);
    
    
    
    if ($foreignNode) {
        my $value = $this->nodeValue;
        
        if ($value) {
            $value = FormatMessage($value, { Schema => $this->parentNode, Node => $this->_targetNode, ForeignNode => $foreignNode },\&_resovleProperty);
        } else {
            $value = $foreignNode->nodeValue;
        }
        
        push @result, new IMPL::DOM::Schema::ValidationError(
            node => $node,
            foreignNode => $foreignNode,
            value => $value,
            source => $Source,
            schema => $this->parentNode,
            message => $this->message
        ) unless $this->op->(_resovleProperty($node,$this->targetProperty),$value);
    } elsif (not $this->optional) {
        push @result,  new IMPL::DOM::Schema::ValidationError(
            node => $node,
            value => '',
            source => $Source,
            schema => $this->parentNode,
            message => $this->message
        );
    }
    
    $this->_targetNode(undef);
    $this->_sourceSchema(undef);
    
    return @result;
}

sub _resovleProperty {
    my ($node,$prop) = @_;
    
    return $node->can($prop) ? $node->$prop() : $node->nodeProperty($prop);
}

sub _matchRx {
    $_[0] =~ $_[1];
}

sub _notMatchRx {
    $_[0] !~ $_[1];
}

sub _equals {
    $_[0] == $_[1];
}

sub _notEquals {
    $_[0] != $_[0];
}

sub _equalsString {
    $_[0] eq $_[1];
}

sub _notEqualsString {
    $_[0] ne $_[1];
}

sub _less {
    $_[0] < $_[1];
}

sub _greater {
    $_[0] > $_[1];
}

sub _lessString {
    $_[0] lt $_[1];
}

sub _greaterString {
    $_[0] gt $_[1];
}

sub _lessEq {
    $_[0] <= $_[1];
}

sub _greaterEq {
    $_[0] >= $_[1];
}

1;

__END__

=pod

=head1 NAME

C<IMPL::DOM::Schema::Validator::Compare> - ограничение на содержимое текущего узла,
сравнивая его со значением другого узла.

=head1 SYNOPSIS

Пример типа описания поля с проверочным полем

=begin code xml

<schema>
    <SimpleType type="retype_field">
        <Property name="linkedNode" message="Для узла %Node.nodeName% необходимо задать свойство %Source.name%"/>
        <Compare op="eq" nodePath="sibling:*[nodeName eq '%Node.linkedNode%']"/>
    </SimpleType>
</schema>

=begin code xml

=head1 DESCRIPTION

Позволяет сравнивать значение текущего узла со значением другого узла. 

=cut