Mercurial > pub > Impl
comparison Lib/IMPL/Web/Handler/TTView.pm @ 285:546957c50a36
*IMPL::Web::Handler::TTView Reworked template selection mechanism
*IMPL::Web::Application: refactoring
-Removed obsolete IMPL::Text modules
| author | cin | 
|---|---|
| date | Mon, 18 Feb 2013 02:55:59 +0400 | 
| parents | a8dbddf491dd | 
| children | 5d14baa35790 | 
   comparison
  equal
  deleted
  inserted
  replaced
| 284:f2a6bc5f3184 | 285:546957c50a36 | 
|---|---|
| 1 package IMPL::Web::Handler::TTView; | 1 package IMPL::Web::Handler::TTView; | 
| 2 use strict; | 2 use strict; | 
| 3 | 3 | 
| 4 use List::Util qw(first); | 4 use List::Util qw(first); | 
| 5 use IMPL::lang; | |
| 5 use IMPL::Const qw(:prop); | 6 use IMPL::Const qw(:prop); | 
| 6 use IMPL::declare { | 7 use IMPL::declare { | 
| 7 require => { | 8 require => { | 
| 8 Factory => 'IMPL::Web::View::ObjectFactory', | 9 Factory => 'IMPL::Web::View::ObjectFactory', | 
| 9 HttpResponse => 'IMPL::Web::HttpResponse', | 10 HttpResponse => 'IMPL::Web::HttpResponse', | 
| 18 | 19 | 
| 19 props => [ | 20 props => [ | 
| 20 contentType => PROP_RO, | 21 contentType => PROP_RO, | 
| 21 contentCharset => PROP_RO, | 22 contentCharset => PROP_RO, | 
| 22 loader => PROP_RO, | 23 loader => PROP_RO, | 
| 23 selectors => PROP_RO | PROP_LIST, | 24 selectors => PROP_RO, | 
| 24 defaultDocument => PROP_RW, | 25 defaultDocument => PROP_RW, | 
| 25 indexResource => PROP_RW, | 26 _selectorsCache => PROP_RW | 
| 26 _selectorsCache => PROP_RW, | |
| 27 _classTemplates => PROP_RW | |
| 28 ] | 27 ] | 
| 29 }; | 28 }; | 
| 30 | 29 | 
| 31 sub CTOR { | 30 sub CTOR { | 
| 32 my ($this) = @_; | 31 my ($this) = @_; | 
| 33 | 32 | 
| 34 $this->indexResource('index') unless $this->indexResource; | 33 $this->_selectorsCache([ map $this->ParseRule($_), @{$this->selectors || []} ]); | 
| 35 } | 34 } | 
| 36 | 35 | 
| 37 sub Invoke { | 36 sub Invoke { | 
| 38 my ( $this, $action, $next ) = @_; | 37 my ( $this, $action, $next ) = @_; | 
| 39 | 38 | 
| 51 my $vars = { | 50 my $vars = { | 
| 52 view => $view, | 51 view => $view, | 
| 53 model => $model, | 52 model => $model, | 
| 54 action => $action, | 53 action => $action, | 
| 55 app => $action->application, | 54 app => $action->application, | 
| 55 env => _cached($action->context->{environment}), | |
| 56 ImportClass => sub { | 56 ImportClass => sub { | 
| 57 my $class = shift; | 57 my $class = shift; | 
| 58 | 58 | 
| 59 $class = Loader->safe->Require($class); | 59 $class = Loader->safe->Require($class); | 
| 60 | 60 | 
| 81 return HttpResponse->new( | 81 return HttpResponse->new( | 
| 82 %responseParams | 82 %responseParams | 
| 83 ); | 83 ); | 
| 84 } | 84 } | 
| 85 | 85 | 
| 86 sub _cached { | |
| 87 my $arg = shift; | |
| 88 | |
| 89 return $arg unless ref $arg eq 'CODE'; | |
| 90 | |
| 91 return sub { | |
| 92 ref $arg eq 'CODE' ? $arg = &$arg() : $arg; | |
| 93 } | |
| 94 } | |
| 95 | |
| 86 sub SelectView { | 96 sub SelectView { | 
| 87 my ( $this, $action, $class ) = @_; | 97 my ($this,$action) = @_; | 
| 88 | 98 | 
| 89 my @path = split /\//, $action->query->path_info(), -1; | 99 my @path; | 
| 90 | 100 | 
| 91 shift @path; # remove always empty leading segment | 101 for(my $r = $action->context->{resource}; $r ; $r = $r->parent ) { | 
| 92 | 102 unshift @path, { | 
| 93 $this->BuildCache unless $this->_selectorsCache; | 103 name => $r->id, | 
| 94 my $cache = $this->_selectorsCache; | 104 class => typeof($r->model) | 
| 95 | 105 }; | 
| 96 @path = reverse @path; | 106 } | 
| 97 | 107 | 
| 98 foreach | 108 @path = map { name => $_}, split /\/+/, $action->query->path_info() | 
| 99 my $subclass ( $class ? ( _GetHierarchy($class), '-default' ) : '-default' ) | 109 unless (@path); | 
| 100 { | 110 | 
| 101 my @results; | 111 return $this->MatchPath(\@path,$this->_selectorsCache) || $this->defaultDocument; | 
| 102 push @results, | 112 } | 
| 103 { result => $this->_classTemplates->{$subclass}, level => 0 } | 113 | 
| 104 if $this->_classTemplates->{$subclass}; | 114 sub ParseRule { | 
| 105 | 115 my ($this, $rule) = @_; | 
| 106 if ( $cache->{$subclass} ) { | 116 | 
| 107 my $alternatives = | 117 my ($selector,$data) = split /\s+=>\s+/, $rule; | 
| 108 [ { selector => $cache->{$subclass}, immediate => 1 } ]; | 118 | 
| 109 $alternatives = | 119 my @parts; | 
| 110 $this->MatchAlternatives( $_, $alternatives, \@results ) | 120 my $first = 1; | 
| 111 foreach @path; | 121 my $weight = 0; | 
| 112 } | 122 foreach my $part ( split /\//, $selector ) { | 
| 113 | 123 # если первым символом является / | 
| 114 if (@results) { | 124 # значит путь в селекторе абсолютный и не нужно | 
| 115 @results = sort { $b->{level} <=> $a->{level} } @results; | 125 # добавлять "любой" элемент в начало | 
| 116 return ( shift @results )->{result}; | 126 | 
| 117 } | 127 if($part) { | 
| 118 } | 128 $weight ++; | 
| 119 | 129 push @parts,{ any => 1 } if $first; | 
| 120 return $this->defaultDocument; | 130 } else { | 
| 121 } | 131 push @parts,{ any => 1 } unless $first; | 
| 122 | 132 next; | 
| 123 sub _GetHierarchy { | 133 } | 
| 124 my ($class) = @_; | 134 | 
| 125 return unless $class; | 135 my ($name,$class) = split /@/, $part; | 
| 126 | 136 | 
| 127 no strict 'refs'; | 137 if ( my ( $varName, $rx ) = ( $name =~ m/^\{(?:(\w+)\:)?(.*)\}$/ ) ) { | 
| 128 | 138 #this is a regexp | 
| 129 return $class, map { _GetHierarchy($_) } @{"${class}::ISA"}; | 139 | 
| 130 } | 140 push @parts, { | 
| 131 | 141 rx => $rx, | 
| 132 sub BuildCache { | 142 var => $varName, | 
| 133 my ($this) = @_; | 143 class => $class, | 
| 134 | 144 }; | 
| 135 my @selectors; | 145 } else { | 
| 136 | 146 push @parts, { | 
| 137 my $cache = $this->_selectorsCache( {} ); | 147 name => length($name) ? $name : undef, | 
| 138 $this->_classTemplates( {} ); | 148 class => $class, | 
| 139 | 149 }; | 
| 140 foreach my $selector ( $this->selectors ) { | 150 } | 
| 141 if ( not ref $selector ) { | 151 } continue { | 
| 142 | 152 $first = 0; | 
| 143 my ( $path, $data ) = split( /\s*=>\s*/, $selector ); | 153 } | 
| 144 | 154 | 
| 145 my @path = split( /\s+/, $path ); | 155 return { selector => \@parts, data => $data, weight => $weight }; | 
| 146 | 156 } | 
| 147 my $class; | 157 | 
| 148 | 158 sub MatchPath { | 
| 149 # if this selector has a class part | 159 my ($this,$path,$rules) = @_; | 
| 150 if ( $path[$#path] =~ m/^\@(.*)/ ) { | 160 | 
| 151 $class = $1; | 161 $path ||= []; | 
| 152 pop @path; | 162 $rules ||= []; | 
| 163 | |
| 164 my @next; | |
| 165 | |
| 166 foreach my $segment (@$path) { | |
| 167 foreach my $rule (@$rules) { | |
| 168 my @selector = @{$rule->{selector}}; | |
| 169 | |
| 170 my $part = shift @selector; | |
| 171 | |
| 172 if ($part->{any}) { | |
| 173 #keep the rule for the next try | |
| 174 push @next, $rule; | |
| 175 | |
| 176 $part = shift @selector while $part->{any}; | |
| 153 } | 177 } | 
| 154 else { | 178 | 
| 155 $class = '-default'; | 179 # if this rule doesn't have a selector | 
| 180 next unless $part; | |
| 181 | |
| 182 my $newRule = { | |
| 183 selector => \@selector, | |
| 184 data => $rule->{data}, | |
| 185 weight => $rule->{weight}, | |
| 186 vars => { %{$rule->{vars} || {}} } | |
| 187 }; | |
| 188 | |
| 189 my $success = 1; | |
| 190 if (my $class = $part->{class}) { | |
| 191 $success = isclass($segment->{class},$class); | |
| 156 } | 192 } | 
| 157 | 193 | 
| 158 #if this selector has a path | 194 if($success && (my $name = $part->{name})) { | 
| 159 if (@path) { | 195 $success = $segment->{name} eq $name; | 
| 160 @path = reverse @path; | 196 } elsif ($success && (my $rx = $part->{rx})) { | 
| 161 my $last = pop @path; | 197 if( my @captures = ($segment->{name} =~ m/($rx)/) ) { | 
| 162 my $t = ( $cache->{$class} ||= {} ); | 198 $newRule->{vars}->{$part->{var}} = \@captures | 
| 163 my $level = 1; | 199 if $part->{var}; | 
| 164 foreach my $prim (@path) { | 200 } else { | 
| 165 $t = ( $t->{$prim}->{next} ||= {} ); | 201 $success = 0; | 
| 166 $level++; | |
| 167 } | |
| 168 $t->{$last}->{level} = $level; | |
| 169 $t->{$last}->{data} = $data; | |
| 170 | |
| 171 } | |
| 172 else { | |
| 173 | |
| 174 # we dont have a selector, only class | |
| 175 | |
| 176 $this->_classTemplates->{$class} = $data; | |
| 177 } | |
| 178 | |
| 179 } | |
| 180 } | |
| 181 } | |
| 182 | |
| 183 sub MatchAlternatives { | |
| 184 my ( $this, $segment, $alternatives, $results ) = @_; | |
| 185 | |
| 186 my @next; | |
| 187 | |
| 188 foreach my $alt (@$alternatives) { | |
| 189 while ( my ( $selector, $match ) = each %{ $alt->{selector} } ) { | |
| 190 | |
| 191 my $context = { | |
| 192 vars => \%{ $alt->{vars} || {} }, | |
| 193 selector => $match->{next} | |
| 194 }; | |
| 195 | |
| 196 if ( $selector =~ s/^>// ) { | |
| 197 $context->{immediate} = 1; | |
| 198 } | |
| 199 | |
| 200 if ( my ( $name, $rx ) = | |
| 201 ( $selector =~ m/^\{(?:(\w+)\:)?(.*)\}$/ ) ) | |
| 202 { | |
| 203 | |
| 204 #this is a regexp | |
| 205 | |
| 206 if ( my @captures = ( $segment =~ m/($rx)/ ) ) { | |
| 207 $context->{success} = 1; | |
| 208 | |
| 209 if ($name) { | |
| 210 $context->{vars}->{$name} = \@captures; | |
| 211 } | |
| 212 } | 202 } | 
| 213 } | 203 } | 
| 214 else { | 204 | 
| 215 | 205 push @next, $newRule if $success; | 
| 216 #this is a segment name | 206 | 
| 217 if ( $segment eq $selector ) { | 207 } | 
| 218 $context->{success} = 1; | 208 $rules = [@next]; | 
| 219 } | 209 undef @next; | 
| 210 } | |
| 211 | |
| 212 my $result = ( | |
| 213 sort { | |
| 214 $b->{weight} <=> $a->{weight} | |
| 215 } | |
| 216 grep { | |
| 217 scalar(@{$_->{selector}}) == 0 | |
| 218 } | |
| 219 @$rules | |
| 220 )[0]; | |
| 221 | |
| 222 if($result) { | |
| 223 my $data = $result->{data}; | |
| 224 $data =~ s/{(\w+)(?:\:(\d+))?}/ | |
| 225 my ($name,$index) = ($1,$2 || 0); | |
| 226 | |
| 227 if ($result->{vars}{$name}) { | |
| 228 $result->{vars}{$name}[$index]; | |
| 229 } else { | |
| 230 ""; | |
| 220 } | 231 } | 
| 221 | 232 /gex; | 
| 222 # test if there were a match | 233 | 
| 223 if ( delete $context->{success} ) { | 234 return $data; | 
| 224 if ( my $data = $match->{data} ) { | 235 } else { | 
| 225 | 236 return; | 
| 226 # interpolate data | 237 } | 
| 227 $data =~ s/{(\w+)(?:\:(\d+))?}/ | |
| 228 my ($name,$index) = ($1,$2 || 0); | |
| 229 | |
| 230 if ($context->{vars}{$name}) { | |
| 231 $context->{vars}{$name}[$index]; | |
| 232 } else { | |
| 233 ""; | |
| 234 } | |
| 235 /gex; | |
| 236 | |
| 237 push @$results, | |
| 238 { level => $match->{level}, result => $data }; | |
| 239 } | |
| 240 push @next, $context if $context->{selector}; | |
| 241 } | |
| 242 else { | |
| 243 | |
| 244 #repeat current alternative if it's not required to be immediate | |
| 245 push @next, | |
| 246 { | |
| 247 selector => { $selector, $match }, | |
| 248 vars => $alt->{vars} | |
| 249 } | |
| 250 unless $alt->{immediate}; | |
| 251 } | |
| 252 } | |
| 253 } | |
| 254 | |
| 255 return \@next; | |
| 256 } | 238 } | 
| 257 | 239 | 
| 258 1; | 240 1; | 
| 259 | 241 | 
| 260 __END__ | 242 __END__ | 
| 299 | 281 | 
| 300 Подбирает шаблон для представления результата, полученного при выполнении следующего обработчика. При | 282 Подбирает шаблон для представления результата, полученного при выполнении следующего обработчика. При | 
| 301 выборе используется принцип похожий на селекторы C<CSS>, основывающийся на именах ресурсов и их типах | 283 выборе используется принцип похожий на селекторы C<CSS>, основывающийся на именах ресурсов и их типах | 
| 302 данных. | 284 данных. | 
| 303 | 285 | 
| 286 Данный обработчик понимает определенные свойства контекста: | |
| 287 | |
| 288 =over | |
| 289 | |
| 290 =item * C<resourceLocation> | |
| 291 | |
| 292 В данном свойстве может быть передана информация о текущем расположении ресурса, | |
| 293 для которого строится представление. Эта информация будет доступна в шаблоне | |
| 294 через свойство документа C<location>. | |
| 295 | |
| 296 =item * C<environment> | |
| 297 | |
| 298 В данном совойстве контекста передается дополнительная информация об окружении | |
| 299 ресурса, например, которую задали родительские ресурсы. Использование данного | |
| 300 свойства позволяет не загромождать ресурс реализацией функциональности по | |
| 301 поддержке окружения. Это свойство может быть ссылкой на функцию, что позволяет | |
| 302 формировать контекст только по необходимости, при этом указанная функция будет | |
| 303 выполнена только один раз, при первом обращении. | |
| 304 | |
| 305 =back | |
| 306 | |
| 304 =head1 SELECTORS | 307 =head1 SELECTORS | 
| 305 | 308 | 
| 306 =begin text | 309 =begin text | 
| 307 | 310 | 
| 308 [url-template] [class] => template | 311 syntax::= selector => template | 
| 312 | |
| 313 selector::= ([>]segment-template[@class-name]) | |
| 314 | |
| 315 segment-template::= {'{'name:regular-expr'}'|segment-name} | |
| 316 | |
| 317 name::= \w+ | |
| 318 | |
| 319 segment-name::= \S+ | |
| 320 | |
| 321 class-name::= name[(::name)] | |
| 322 | |
| 323 url-template@class => template | |
| 309 | 324 | 
| 310 shoes => product/list | 325 shoes => product/list | 
| 311 {action:*.} @My::Data::Product => product/{action} | 326 /shop//{action:*.}@My::Data::Product => product/{action} | 
| 312 | 327 | 
| 313 stuff >list => product/list | 328 stuff >list => product/list | 
| 314 details => product/details | 329 details => product/details | 
| 315 | 330 | 
| 316 =end text | 331 =end text | 
