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