Add skipped test for ininite recursion issue, re-arrange TODO to put dev release...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Dispatcher.pm
CommitLineData
68a748b9 1package Catalyst::Dispatcher;
1abd6db7 2
059c085b 3use Moose;
068c0898 4use Class::MOP;
059c085b 5
a2f2cde9 6use Catalyst::Exception;
f05af9ba 7use Catalyst::Utils;
fbcc39ad 8use Catalyst::Action;
b7aebc12 9use Catalyst::ActionContainer;
b96f127f 10use Catalyst::DispatchType::Default;
bcccee4e 11use Catalyst::DispatchType::Index;
87b85407 12use Text::SimpleTable;
1abd6db7 13use Tree::Simple;
14use Tree::Simple::Visitor::FindByPath;
e72f8f51 15use Scalar::Util ();
1abd6db7 16
5fb12dbb 17#do these belong as package vars or should we build these via a builder method?
6d030e6f 18# Preload these action types
61a9002d 19our @PRELOAD = qw/Index Path Regex/;
1abd6db7 20
2d1d8f91 21# Postload these action types
61a9002d 22our @POSTLOAD = qw/Default/;
2d1d8f91 23
5fb12dbb 24has _tree => (is => 'rw');
8c80e4f8 25has _dispatch_types => (is => 'rw', default => sub { [] }, required => 1, lazy => 1);
26has _registered_dispatch_types => (is => 'rw', default => sub { {} }, required => 1, lazy => 1);
27has _method_action_class => (is => 'rw', default => 'Catalyst::Action');
28has _action_container_class => (is => 'rw', default => 'Catalyst::ActionContainer');
29
5fb12dbb 30has preload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@PRELOAD] });
31has postload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@POSTLOAD] });
32has _action_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
33has _container_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
059c085b 34
35no Moose;
36
1abd6db7 37=head1 NAME
38
9c053379 39Catalyst::Dispatcher - The Catalyst Dispatcher
1abd6db7 40
41=head1 SYNOPSIS
42
43See L<Catalyst>.
44
45=head1 DESCRIPTION
46
4ab87e27 47This is the class that maps public urls to actions in your Catalyst
48application based on the attributes you set.
49
1abd6db7 50=head1 METHODS
51
ac5c933b 52=head2 new
4ab87e27 53
54Construct a new dispatcher.
55
e7bb8d33 56=cut
57
059c085b 58sub BUILD {
59 my ($self, $params) = @_;
9e81ba44 60
068c0898 61 my $container =
059c085b 62 Catalyst::ActionContainer->new( { part => '/', actions => {} } );
a13e21ab 63
059c085b 64 $self->_tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) );
e7bb8d33 65}
66
67=head2 $self->preload_dispatch_types
68
69An arrayref of pre-loaded dispatchtype classes
70
71Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
72To use a custom class outside the regular C<Catalyst> namespace, prefix
73it with a C<+>, like so:
74
75 +My::Dispatch::Type
76
77=head2 $self->postload_dispatch_types
78
79An arrayref of post-loaded dispatchtype classes
80
81Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
82To use a custom class outside the regular C<Catalyst> namespace, prefix
83it with a C<+>, like so:
84
85 +My::Dispatch::Type
86
b5ecfcf0 87=head2 $self->dispatch($c)
1abd6db7 88
4ab87e27 89Delegate the dispatch to the action that matched the url, or return a
90message about unknown resource
91
92
1abd6db7 93=cut
94
95sub dispatch {
fbcc39ad 96 my ( $self, $c ) = @_;
e63bdf38 97 if ( my $action = $c->action ) {
98 $c->forward( join( '/', '', $action->namespace, '_DISPATCH' ) );
fbcc39ad 99 }
100
101 else {
1abd6db7 102 my $path = $c->req->path;
103 my $error = $path
104 ? qq/Unknown resource "$path"/
105 : "No default action defined";
106 $c->log->error($error) if $c->debug;
107 $c->error($error);
108 }
109}
110
2f381252 111# $self->_command2action( $c, $command [, \@arguments ] )
112# Search for an action, from the command and returns C<($action, $args)> on
113# success. Returns C<(0)> on error.
1abd6db7 114
2f381252 115sub _command2action {
e72f8f51 116 my ( $self, $c, $command, @extra_params ) = @_;
99fe1710 117
1abd6db7 118 unless ($command) {
2f381252 119 $c->log->debug('Nothing to go to') if $c->debug;
1abd6db7 120 return 0;
121 }
99fe1710 122
e72f8f51 123 my @args;
068c0898 124
e72f8f51 125 if ( ref( $extra_params[-1] ) eq 'ARRAY' ) {
126 @args = @{ pop @extra_params }
127 } else {
2f381252 128 # this is a copy, it may take some abuse from
129 # ->_invoke_as_path if the path had trailing parts
e72f8f51 130 @args = @{ $c->request->arguments };
131 }
132
133 my $action;
134
2f381252 135 # go to a string path ("/foo/bar/gorch")
e31b525c 136 # or action object
137 if (Scalar::Util::blessed($command) && $command->isa('Catalyst::Action')) {
138 $action = $command;
139 }
140 else {
141 $action = $self->_invoke_as_path( $c, "$command", \@args );
142 }
99fe1710 143
2f381252 144 # go to a component ( "MyApp::*::Foo" or $c->component("...")
145 # - a path or an object)
e72f8f51 146 unless ($action) {
147 my $method = @extra_params ? $extra_params[0] : "process";
148 $action = $self->_invoke_as_component( $c, $command, $method );
149 }
99fe1710 150
2f381252 151 return $action, \@args;
152}
153
ae0e35ee 154=head2 $self->visit( $c, $command [, \@arguments ] )
2f381252 155
156Documented in L<Catalyst>
157
158=cut
159
ae0e35ee 160sub visit {
2f381252 161 my $self = shift;
ae0e35ee 162 $self->_do_visit('visit', @_);
163}
164
165sub _do_visit {
166 my $self = shift;
167 my $opname = shift;
2f381252 168 my ( $c, $command ) = @_;
169 my ( $action, $args ) = $self->_command2action(@_);
ae0e35ee 170 my $error = qq/Couldn't $opname("$command"): /;
2f381252 171
ae0e35ee 172 if (!$action) {
3ea37672 173 $error .= qq/Couldn't $opname to command "$command": /
174 .qq/Invalid action or component./;
ae0e35ee 175 }
176 elsif (!defined $action->namespace) {
177 $error .= qq/Action has no namespace: cannot $opname() to a plain /
178 .qq/method or component, must be a :Action or some sort./
179 }
180 elsif (!$action->class->can('_DISPATCH')) {
181 $error .= qq/Action cannot _DISPATCH. /
182 .qq/Did you try to $opname() a non-controller action?/;
183 }
184 else {
185 $error = q();
186 }
187
188 if($error) {
2f381252 189 $c->error($error);
190 $c->log->debug($error) if $c->debug;
191 return 0;
192 }
193
52f71256 194 $action = $self->expand_action($action);
195
2f381252 196 local $c->request->{arguments} = $args;
ae0e35ee 197 local $c->{namespace} = $action->{'namespace'};
198 local $c->{action} = $action;
199
2f381252 200 $self->dispatch($c);
ae0e35ee 201}
202
203=head2 $self->go( $c, $command [, \@arguments ] )
204
205Documented in L<Catalyst>
206
207=cut
2f381252 208
ae0e35ee 209sub go {
210 my $self = shift;
211 $self->_do_visit('go', @_);
2f381252 212 die $Catalyst::GO;
213}
214
215=head2 $self->forward( $c, $command [, \@arguments ] )
216
217Documented in L<Catalyst>
218
219=cut
220
221sub forward {
222 my $self = shift;
3ea37672 223 $self->_do_forward(forward => @_);
224}
225
226sub _do_forward {
227 my $self = shift;
228 my $opname = shift;
2f381252 229 my ( $c, $command ) = @_;
230 my ( $action, $args ) = $self->_command2action(@_);
99fe1710 231
3ea37672 232 if (!$action) {
233 my $error .= qq/Couldn't $opname to command "$command": /
234 .qq/Invalid action or component./;
e540158b 235 $c->error($error);
236 $c->log->debug($error) if $c->debug;
237 return 0;
238 }
bd7d2e94 239
059c085b 240 no warnings 'recursion';
241
6680c772 242 my $orig_args = $c->request->arguments();
a9078a5a 243 $c->request->arguments($args);
b8f669f3 244 $action->dispatch( $c );
6680c772 245 $c->request->arguments($orig_args);
3ea37672 246
1abd6db7 247 return $c->state;
248}
249
3ea37672 250=head2 $self->detach( $c, $command [, \@arguments ] )
251
252Documented in L<Catalyst>
253
254=cut
255
256sub detach {
257 my ( $self, $c, $command, @args ) = @_;
258 $self->_do_forward(detach => $c, $command, @args ) if $command;
259 die $Catalyst::DETACH;
260}
261
adb53907 262sub _action_rel2abs {
e540158b 263 my ( $self, $c, $path ) = @_;
264
265 unless ( $path =~ m#^/# ) {
266 my $namespace = $c->stack->[-1]->namespace;
267 $path = "$namespace/$path";
268 }
269
270 $path =~ s#^/##;
271 return $path;
adb53907 272}
273
274sub _invoke_as_path {
e540158b 275 my ( $self, $c, $rel_path, $args ) = @_;
276
e540158b 277 my $path = $self->_action_rel2abs( $c, $rel_path );
278
279 my ( $tail, @extra_args );
280 while ( ( $path, $tail ) = ( $path =~ m#^(?:(.*)/)?(\w+)?$# ) )
281 { # allow $path to be empty
282 if ( my $action = $c->get_action( $tail, $path ) ) {
283 push @$args, @extra_args;
284 return $action;
285 }
286 else {
287 return
288 unless $path
289 ; # if a match on the global namespace failed then the whole lookup failed
290 }
291
292 unshift @extra_args, $tail;
293 }
adb53907 294}
295
296sub _find_component_class {
e540158b 297 my ( $self, $c, $component ) = @_;
adb53907 298
e540158b 299 return ref($component)
300 || ref( $c->component($component) )
301 || $c->component($component);
adb53907 302}
303
304sub _invoke_as_component {
e540158b 305 my ( $self, $c, $component, $method ) = @_;
306
307 my $class = $self->_find_component_class( $c, $component ) || return 0;
e540158b 308
309 if ( my $code = $class->can($method) ) {
059c085b 310 return $self->_method_action_class->new(
e540158b 311 {
312 name => $method,
313 code => $code,
314 reverse => "$class->$method",
315 class => $class,
316 namespace => Catalyst::Utils::class2prefix(
317 $class, $c->config->{case_sensitive}
318 ),
319 }
320 );
321 }
322 else {
323 my $error =
324 qq/Couldn't forward to "$class". Does not implement "$method"/;
325 $c->error($error);
326 $c->log->debug($error)
327 if $c->debug;
328 return 0;
329 }
adb53907 330}
331
b5ecfcf0 332=head2 $self->prepare_action($c)
fbcc39ad 333
4ab87e27 334Find an dispatch type that matches $c->req->path, and set args from it.
335
fbcc39ad 336=cut
337
338sub prepare_action {
339 my ( $self, $c ) = @_;
e63bdf38 340 my $req = $c->req;
341 my $path = $req->path;
342 my @path = split /\//, $req->path;
343 $req->args( \my @args );
fbcc39ad 344
61a9002d 345 unshift( @path, '' ); # Root action
78d760bb 346
b96f127f 347 DESCEND: while (@path) {
fbcc39ad 348 $path = join '/', @path;
61a9002d 349 $path =~ s#^/##;
fbcc39ad 350
61a9002d 351 $path = '' if $path eq '/'; # Root action
78d760bb 352
22f3a8dd 353 # Check out dispatch types to see if any will handle the path at
354 # this level
355
059c085b 356 foreach my $type ( @{ $self->_dispatch_types } ) {
2633d7dc 357 last DESCEND if $type->match( $c, $path );
66e28e3f 358 }
b96f127f 359
22f3a8dd 360 # If not, move the last part path to args
4082e678 361 my $arg = pop(@path);
362 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
363 unshift @args, $arg;
fbcc39ad 364 }
365
e63bdf38 366 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$req->captures||[]};
66d7ad40 367
e63bdf38 368 $c->log->debug( 'Path is "' . $req->match . '"' )
076bfad3 369 if ( $c->debug && defined $req->match && length $req->match );
e3a13771 370
fbcc39ad 371 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
372 if ( $c->debug && @args );
373}
374
b5ecfcf0 375=head2 $self->get_action( $action, $namespace )
1abd6db7 376
4ab87e27 377returns a named action from a given namespace.
378
1abd6db7 379=cut
380
381sub get_action {
bcd1002b 382 my ( $self, $name, $namespace ) = @_;
79a3189a 383 return unless $name;
3d0d6d21 384
2f381252 385 $namespace = join( "/", grep { length } split '/', ( defined $namespace ? $namespace : "" ) );
99fe1710 386
e63bdf38 387 return $self->_action_hash->{"${namespace}/${name}"};
1abd6db7 388}
389
ac5c933b 390=head2 $self->get_action_by_path( $path );
068c0898 391
ac5c933b 392Returns the named action by its full path.
3d0d6d21 393
068c0898 394=cut
3d0d6d21 395
396sub get_action_by_path {
397 my ( $self, $path ) = @_;
ea0e58d9 398 $path =~ s/^\///;
28928de9 399 $path = "/$path" unless $path =~ /\//;
059c085b 400 $self->_action_hash->{$path};
3d0d6d21 401}
402
b5ecfcf0 403=head2 $self->get_actions( $c, $action, $namespace )
a9dc674c 404
405=cut
406
407sub get_actions {
408 my ( $self, $c, $action, $namespace ) = @_;
409 return [] unless $action;
3d0d6d21 410
28928de9 411 $namespace = join( "/", grep { length } split '/', $namespace || "" );
a9dc674c 412
413 my @match = $self->get_containers($namespace);
414
684d10ed 415 return map { $_->get_action($action) } @match;
a9dc674c 416}
417
b5ecfcf0 418=head2 $self->get_containers( $namespace )
cfd04b0c 419
4ab87e27 420Return all the action containers for a given namespace, inclusive
421
cfd04b0c 422=cut
423
424sub get_containers {
425 my ( $self, $namespace ) = @_;
a13e21ab 426 $namespace ||= '';
427 $namespace = '' if $namespace eq '/';
cfd04b0c 428
a13e21ab 429 my @containers;
cfd04b0c 430
7f23827b 431 if ( length $namespace ) {
432 do {
059c085b 433 push @containers, $self->_container_hash->{$namespace};
7f23827b 434 } while ( $namespace =~ s#/[^/]+$## );
435 }
90ce41ba 436
059c085b 437 return reverse grep { defined } @containers, $self->_container_hash->{''};
90ce41ba 438
e63bdf38 439 #return (split '/', $namespace); # isnt this more clear?
a13e21ab 440 my @parts = split '/', $namespace;
cfd04b0c 441}
442
ea0e58d9 443=head2 $self->uri_for_action($action, \@captures)
444
445Takes a Catalyst::Action object and action parameters and returns a URI
446part such that if $c->req->path were this URI part, this action would be
447dispatched to with $c->req->captures set to the supplied arrayref.
448
449If the action object is not available for external dispatch or the dispatcher
450cannot determine an appropriate URI, this method will return undef.
451
452=cut
453
454sub uri_for_action {
455 my ( $self, $action, $captures) = @_;
456 $captures ||= [];
059c085b 457 foreach my $dispatch_type ( @{ $self->_dispatch_types } ) {
ea0e58d9 458 my $uri = $dispatch_type->uri_for_action( $action, $captures );
81e75875 459 return( $uri eq '' ? '/' : $uri )
460 if defined($uri);
ea0e58d9 461 }
462 return undef;
463}
464
ae0e35ee 465=head2 expand_action
466
467expand an action into a full representation of the dispatch.
468mostly useful for chained, other actions will just return a
469single action.
470
471=cut
472
52f71256 473sub expand_action {
474 my ($self, $action) = @_;
475
476 foreach my $dispatch_type (@{ $self->_dispatch_types }) {
477 my $expanded = $dispatch_type->expand_action($action);
478 return $expanded if $expanded;
479 }
480
481 return $action;
482}
483
b5ecfcf0 484=head2 $self->register( $c, $action )
aad72cc9 485
4ab87e27 486Make sure all required dispatch types for this action are loaded, then
487pass the action to our dispatch types so they can register it if required.
488Also, set up the tree with the action containers.
489
aad72cc9 490=cut
491
79a3189a 492sub register {
493 my ( $self, $c, $action ) = @_;
494
059c085b 495 my $registered = $self->_registered_dispatch_types;
694d15f1 496
e63bdf38 497 #my $priv = 0; #seems to be unused
694d15f1 498 foreach my $key ( keys %{ $action->attributes } ) {
9a6ecf4f 499 next if $key eq 'Private';
694d15f1 500 my $class = "Catalyst::DispatchType::$key";
501 unless ( $registered->{$class} ) {
5fb67d52 502 #some error checking rethrowing here wouldn't hurt.
068c0898 503 eval { Class::MOP::load_class($class) };
059c085b 504 push( @{ $self->_dispatch_types }, $class->new ) unless $@;
694d15f1 505 $registered->{$class} = 1;
506 }
507 }
508
509 # Pass the action to our dispatch types so they can register it if reqd.
059c085b 510 foreach my $type ( @{ $self->_dispatch_types } ) {
9a6ecf4f 511 $type->register( $c, $action );
694d15f1 512 }
513
79a3189a 514 my $namespace = $action->namespace;
a13e21ab 515 my $name = $action->name;
c7116517 516
ad5e4650 517 my $container = $self->_find_or_create_action_container($namespace);
15e9b5dd 518
519 # Set the method value
a13e21ab 520 $container->add_action($action);
c7116517 521
059c085b 522 $self->_action_hash->{"$namespace/$name"} = $action;
523 $self->_container_hash->{$namespace} = $container;
15e9b5dd 524}
525
ad5e4650 526sub _find_or_create_action_container {
a13e21ab 527 my ( $self, $namespace ) = @_;
528
059c085b 529 my $tree ||= $self->_tree;
99fe1710 530
a13e21ab 531 return $tree->getNodeValue unless $namespace;
78d760bb 532
a13e21ab 533 my @namespace = split '/', $namespace;
534 return $self->_find_or_create_namespace_node( $tree, @namespace )
535 ->getNodeValue;
8505565b 536}
90ce41ba 537
8505565b 538sub _find_or_create_namespace_node {
a13e21ab 539 my ( $self, $parent, $part, @namespace ) = @_;
78d760bb 540
a13e21ab 541 return $parent unless $part;
8505565b 542
a13e21ab 543 my $child =
544 ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
8505565b 545
a13e21ab 546 unless ($child) {
547 my $container = Catalyst::ActionContainer->new($part);
548 $parent->addChild( $child = Tree::Simple->new($container) );
549 }
99fe1710 550
a13e21ab 551 $self->_find_or_create_namespace_node( $child, @namespace );
1abd6db7 552}
553
4ab87e27 554=head2 $self->setup_actions( $class, $context )
555
1abd6db7 556
557=cut
558
559sub setup_actions {
11bd4e3e 560 my ( $self, $c ) = @_;
99fe1710 561
12e28165 562
9e81ba44 563 my @classes =
ad5e4650 564 $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
059c085b 565 @{ $self->_registered_dispatch_types }{@classes} = (1) x @classes;
b96f127f 566
49070d25 567 foreach my $comp ( values %{ $c->components } ) {
568 $comp->register_actions($c) if $comp->can('register_actions');
1abd6db7 569 }
e494bd6b 570
ad5e4650 571 $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
6d030e6f 572
11bd4e3e 573 return unless $c->debug;
99fe1710 574
684d10ed 575 my $privates = Text::SimpleTable->new(
dbf03873 576 [ 20, 'Private' ],
34d28dfd 577 [ 36, 'Class' ],
dbf03873 578 [ 12, 'Method' ]
684d10ed 579 );
99fe1710 580
87b85407 581 my $has_private = 0;
1abd6db7 582 my $walker = sub {
583 my ( $walker, $parent, $prefix ) = @_;
584 $prefix .= $parent->getNodeValue || '';
585 $prefix .= '/' unless $prefix =~ /\/$/;
b7aebc12 586 my $node = $parent->getNodeValue->actions;
99fe1710 587
78d760bb 588 for my $action ( keys %{$node} ) {
b7aebc12 589 my $action_obj = $node->{$action};
b0bb11ec 590 next
591 if ( ( $action =~ /^_.*/ )
592 && ( !$c->config->{show_internal_actions} ) );
684d10ed 593 $privates->row( "$prefix$action", $action_obj->class, $action );
87b85407 594 $has_private = 1;
1abd6db7 595 }
99fe1710 596
1abd6db7 597 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
598 };
99fe1710 599
059c085b 600 $walker->( $walker, $self->_tree, '' );
1cf0345b 601 $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
602 if $has_private;
99fe1710 603
a9cbd748 604 # List all public actions
059c085b 605 $_->list($c) for @{ $self->_dispatch_types };
1abd6db7 606}
607
ad5e4650 608sub _load_dispatch_types {
9e81ba44 609 my ( $self, @types ) = @_;
610
611 my @loaded;
612
613 # Preload action types
614 for my $type (@types) {
615 my $class =
616 ( $type =~ /^\+(.*)$/ ) ? $1 : "Catalyst::DispatchType::${type}";
0fc2d522 617
068c0898 618 eval { Class::MOP::load_class($class) };
9e81ba44 619 Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
620 if $@;
059c085b 621 push @{ $self->_dispatch_types }, $class->new;
9e81ba44 622
623 push @loaded, $class;
624 }
625
a13e21ab 626 return @loaded;
9e81ba44 627}
628
6680c772 629no Moose;
e5ecd5bc 630__PACKAGE__->meta->make_immutable;
631
059c085b 632=head2 meta
633
634Provided by Moose
635
2f381252 636=head1 AUTHORS
1abd6db7 637
2f381252 638Catalyst Contributors, see Catalyst.pm
1abd6db7 639
640=head1 COPYRIGHT
641
642This program is free software, you can redistribute it and/or modify it under
643the same terms as Perl itself.
644
645=cut
646
6471;