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