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