Separated execute and dispatch on Catalyst::Action
[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;
87b85407 11use Text::SimpleTable;
1abd6db7 12use Tree::Simple;
13use Tree::Simple::Visitor::FindByPath;
14
fbcc39ad 15# Stringify to class
16use overload '""' => sub { return ref shift }, fallback => 1;
17
49070d25 18__PACKAGE__->mk_accessors(
19 qw/tree dispatch_types registered_dispatch_types
e7bb8d33 20 method_action_class action_container_class
21 preload_dispatch_types postload_dispatch_types
a13e21ab 22 action_hash container_hash
9e81ba44 23 /
49070d25 24);
6d030e6f 25
26# Preload these action types
61a9002d 27our @PRELOAD = qw/Index Path Regex/;
1abd6db7 28
2d1d8f91 29# Postload these action types
61a9002d 30our @POSTLOAD = qw/Default/;
2d1d8f91 31
1abd6db7 32=head1 NAME
33
9c053379 34Catalyst::Dispatcher - The Catalyst Dispatcher
1abd6db7 35
36=head1 SYNOPSIS
37
38See L<Catalyst>.
39
40=head1 DESCRIPTION
41
4ab87e27 42This is the class that maps public urls to actions in your Catalyst
43application based on the attributes you set.
44
1abd6db7 45=head1 METHODS
46
76ddf86b 47=head2 new
4ab87e27 48
49Construct a new dispatcher.
50
e7bb8d33 51=cut
52
53sub new {
9e81ba44 54 my $self = shift;
e7bb8d33 55 my $class = ref($self) || $self;
9e81ba44 56
57 my $obj = $class->SUPER::new(@_);
58
e7bb8d33 59 # set the default pre- and and postloads
9e81ba44 60 $obj->preload_dispatch_types( \@PRELOAD );
e7bb8d33 61 $obj->postload_dispatch_types( \@POSTLOAD );
a13e21ab 62 $obj->action_hash( {} );
63 $obj->container_hash( {} );
64
65 # Create the root node of the tree
66 my $container =
67 Catalyst::ActionContainer->new( { part => '/', actions => {} } );
68 $obj->tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) );
69
9e81ba44 70 return $obj;
e7bb8d33 71}
72
73=head2 $self->preload_dispatch_types
74
75An arrayref of pre-loaded dispatchtype classes
76
77Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
78To use a custom class outside the regular C<Catalyst> namespace, prefix
79it with a C<+>, like so:
80
81 +My::Dispatch::Type
82
83=head2 $self->postload_dispatch_types
84
85An arrayref of post-loaded dispatchtype classes
86
87Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
88To use a custom class outside the regular C<Catalyst> namespace, prefix
89it with a C<+>, like so:
90
91 +My::Dispatch::Type
92
b5ecfcf0 93=head2 $self->detach( $c, $command [, \@arguments ] )
6ef62eb2 94
4ab87e27 95Documented in L<Catalyst>
96
6ef62eb2 97=cut
98
99sub detach {
fbcc39ad 100 my ( $self, $c, $command, @args ) = @_;
bd7d2e94 101 $c->forward( $command, @args ) if $command;
fbcc39ad 102 die $Catalyst::DETACH;
6ef62eb2 103}
104
b5ecfcf0 105=head2 $self->dispatch($c)
1abd6db7 106
4ab87e27 107Delegate the dispatch to the action that matched the url, or return a
108message about unknown resource
109
110
1abd6db7 111=cut
112
113sub dispatch {
fbcc39ad 114 my ( $self, $c ) = @_;
66e28e3f 115 if ( $c->action ) {
28591cd7 116 $c->forward( join( '/', '', $c->action->namespace, '_DISPATCH' ) );
fbcc39ad 117 }
118
119 else {
1abd6db7 120 my $path = $c->req->path;
121 my $error = $path
122 ? qq/Unknown resource "$path"/
123 : "No default action defined";
124 $c->log->error($error) if $c->debug;
125 $c->error($error);
126 }
127}
128
b5ecfcf0 129=head2 $self->forward( $c, $command [, \@arguments ] )
1abd6db7 130
4ab87e27 131Documented in L<Catalyst>
132
1abd6db7 133=cut
134
135sub forward {
e540158b 136 my ( $self, $c, $command ) = splice( @_, 0, 3 );
99fe1710 137
1abd6db7 138 unless ($command) {
139 $c->log->debug('Nothing to forward to') if $c->debug;
140 return 0;
141 }
99fe1710 142
e540158b 143 my $args = [ @{ $c->request->arguments } ];
99fe1710 144
e540158b 145 @$args = @{ pop @_ } if ( ref( $_[-1] ) eq 'ARRAY' );
99fe1710 146
adb53907 147 my $action = $self->_invoke_as_path( $c, $command, $args )
e540158b 148 || $self->_invoke_as_component( $c, $command, shift );
99fe1710 149
e540158b 150 unless ($action) {
151 my $error =
152 qq/Couldn't forward to command "$command": /
153 . qq/Invalid action or component./;
154 $c->error($error);
155 $c->log->debug($error) if $c->debug;
156 return 0;
157 }
bd7d2e94 158
e540158b 159 #push @$args, @_;
adb53907 160
e540158b 161 local $c->request->{arguments} = $args;
b8f669f3 162 $action->dispatch( $c );
99fe1710 163
1abd6db7 164 return $c->state;
165}
166
adb53907 167sub _action_rel2abs {
e540158b 168 my ( $self, $c, $path ) = @_;
169
170 unless ( $path =~ m#^/# ) {
171 my $namespace = $c->stack->[-1]->namespace;
172 $path = "$namespace/$path";
173 }
174
175 $path =~ s#^/##;
176 return $path;
adb53907 177}
178
179sub _invoke_as_path {
e540158b 180 my ( $self, $c, $rel_path, $args ) = @_;
181
182 return if ref $rel_path; # it must be a string
183
184 my $path = $self->_action_rel2abs( $c, $rel_path );
185
186 my ( $tail, @extra_args );
187 while ( ( $path, $tail ) = ( $path =~ m#^(?:(.*)/)?(\w+)?$# ) )
188 { # allow $path to be empty
189 if ( my $action = $c->get_action( $tail, $path ) ) {
190 push @$args, @extra_args;
191 return $action;
192 }
193 else {
194 return
195 unless $path
196 ; # if a match on the global namespace failed then the whole lookup failed
197 }
198
199 unshift @extra_args, $tail;
200 }
adb53907 201}
202
203sub _find_component_class {
e540158b 204 my ( $self, $c, $component ) = @_;
adb53907 205
e540158b 206 return ref($component)
207 || ref( $c->component($component) )
208 || $c->component($component);
adb53907 209}
210
211sub _invoke_as_component {
e540158b 212 my ( $self, $c, $component, $method ) = @_;
213
214 my $class = $self->_find_component_class( $c, $component ) || return 0;
215 $method ||= "process";
216
217 if ( my $code = $class->can($method) ) {
218 return $self->method_action_class->new(
219 {
220 name => $method,
221 code => $code,
222 reverse => "$class->$method",
223 class => $class,
224 namespace => Catalyst::Utils::class2prefix(
225 $class, $c->config->{case_sensitive}
226 ),
227 }
228 );
229 }
230 else {
231 my $error =
232 qq/Couldn't forward to "$class". Does not implement "$method"/;
233 $c->error($error);
234 $c->log->debug($error)
235 if $c->debug;
236 return 0;
237 }
adb53907 238}
239
b5ecfcf0 240=head2 $self->prepare_action($c)
fbcc39ad 241
4ab87e27 242Find an dispatch type that matches $c->req->path, and set args from it.
243
fbcc39ad 244=cut
245
246sub prepare_action {
247 my ( $self, $c ) = @_;
248 my $path = $c->req->path;
249 my @path = split /\//, $c->req->path;
250 $c->req->args( \my @args );
251
61a9002d 252 unshift( @path, '' ); # Root action
78d760bb 253
b96f127f 254 DESCEND: while (@path) {
fbcc39ad 255 $path = join '/', @path;
61a9002d 256 $path =~ s#^/##;
fbcc39ad 257
61a9002d 258 $path = '' if $path eq '/'; # Root action
78d760bb 259
22f3a8dd 260 # Check out dispatch types to see if any will handle the path at
261 # this level
262
78d760bb 263 foreach my $type ( @{ $self->dispatch_types } ) {
2633d7dc 264 last DESCEND if $type->match( $c, $path );
66e28e3f 265 }
b96f127f 266
22f3a8dd 267 # If not, move the last part path to args
4082e678 268 my $arg = pop(@path);
269 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
270 unshift @args, $arg;
fbcc39ad 271 }
272
e3a13771 273 $c->log->debug( 'Path is "' . $c->req->match . '"' )
274 if ( $c->debug && $c->req->match );
275
fbcc39ad 276 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
277 if ( $c->debug && @args );
278}
279
b5ecfcf0 280=head2 $self->get_action( $action, $namespace )
1abd6db7 281
4ab87e27 282returns a named action from a given namespace.
283
1abd6db7 284=cut
285
286sub get_action {
bcd1002b 287 my ( $self, $name, $namespace ) = @_;
79a3189a 288 return unless $name;
3d0d6d21 289
28928de9 290 $namespace = join( "/", grep { length } split '/', $namespace || "" );
99fe1710 291
a13e21ab 292 return $self->action_hash->{"$namespace/$name"};
1abd6db7 293}
294
34d28dfd 295=head2 $self->get_action_by_path( $path );
296
297Returns the named action by its full path.
3d0d6d21 298
34d28dfd 299=cut
3d0d6d21 300
301sub get_action_by_path {
302 my ( $self, $path ) = @_;
28928de9 303 $path = "/$path" unless $path =~ /\//;
3d0d6d21 304 $self->action_hash->{$path};
305}
306
b5ecfcf0 307=head2 $self->get_actions( $c, $action, $namespace )
a9dc674c 308
309=cut
310
311sub get_actions {
312 my ( $self, $c, $action, $namespace ) = @_;
313 return [] unless $action;
3d0d6d21 314
28928de9 315 $namespace = join( "/", grep { length } split '/', $namespace || "" );
a9dc674c 316
317 my @match = $self->get_containers($namespace);
318
684d10ed 319 return map { $_->get_action($action) } @match;
a9dc674c 320}
321
b5ecfcf0 322=head2 $self->get_containers( $namespace )
cfd04b0c 323
4ab87e27 324Return all the action containers for a given namespace, inclusive
325
cfd04b0c 326=cut
327
328sub get_containers {
329 my ( $self, $namespace ) = @_;
a13e21ab 330 $namespace ||= '';
331 $namespace = '' if $namespace eq '/';
cfd04b0c 332
a13e21ab 333 my @containers;
cfd04b0c 334
7f23827b 335 if ( length $namespace ) {
336 do {
337 push @containers, $self->container_hash->{$namespace};
338 } while ( $namespace =~ s#/[^/]+$## );
339 }
90ce41ba 340
a13e21ab 341 return reverse grep { defined } @containers, $self->container_hash->{''};
90ce41ba 342
a13e21ab 343 my @parts = split '/', $namespace;
cfd04b0c 344}
345
b5ecfcf0 346=head2 $self->register( $c, $action )
aad72cc9 347
4ab87e27 348Make sure all required dispatch types for this action are loaded, then
349pass the action to our dispatch types so they can register it if required.
350Also, set up the tree with the action containers.
351
aad72cc9 352=cut
353
79a3189a 354sub register {
355 my ( $self, $c, $action ) = @_;
356
694d15f1 357 my $registered = $self->registered_dispatch_types;
358
359 my $priv = 0;
360 foreach my $key ( keys %{ $action->attributes } ) {
361 $priv++ if $key eq 'Private';
362 my $class = "Catalyst::DispatchType::$key";
363 unless ( $registered->{$class} ) {
364 eval "require $class";
365 push( @{ $self->dispatch_types }, $class->new ) unless $@;
366 $registered->{$class} = 1;
367 }
368 }
369
370 # Pass the action to our dispatch types so they can register it if reqd.
01ce0928 371 my $reg = 0;
372 foreach my $type ( @{ $self->dispatch_types } ) {
373 $reg++ if $type->register( $c, $action );
694d15f1 374 }
375
376 return unless $reg + $priv;
377
79a3189a 378 my $namespace = $action->namespace;
a13e21ab 379 my $name = $action->name;
c7116517 380
a13e21ab 381 my $container = $self->find_or_create_action_container($namespace);
15e9b5dd 382
383 # Set the method value
a13e21ab 384 $container->add_action($action);
c7116517 385
a13e21ab 386 $self->action_hash->{"$namespace/$name"} = $action;
387 $self->container_hash->{$namespace} = $container;
15e9b5dd 388}
389
a13e21ab 390sub find_or_create_action_container {
391 my ( $self, $namespace ) = @_;
392
393 my $tree ||= $self->tree;
99fe1710 394
a13e21ab 395 return $tree->getNodeValue unless $namespace;
78d760bb 396
a13e21ab 397 my @namespace = split '/', $namespace;
398 return $self->_find_or_create_namespace_node( $tree, @namespace )
399 ->getNodeValue;
8505565b 400}
90ce41ba 401
8505565b 402sub _find_or_create_namespace_node {
a13e21ab 403 my ( $self, $parent, $part, @namespace ) = @_;
78d760bb 404
a13e21ab 405 return $parent unless $part;
8505565b 406
a13e21ab 407 my $child =
408 ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
8505565b 409
a13e21ab 410 unless ($child) {
411 my $container = Catalyst::ActionContainer->new($part);
412 $parent->addChild( $child = Tree::Simple->new($container) );
413 }
99fe1710 414
a13e21ab 415 $self->_find_or_create_namespace_node( $child, @namespace );
1abd6db7 416}
417
4ab87e27 418=head2 $self->setup_actions( $class, $context )
419
1abd6db7 420
421=cut
422
423sub setup_actions {
11bd4e3e 424 my ( $self, $c ) = @_;
99fe1710 425
6d030e6f 426 $self->dispatch_types( [] );
91d4abc5 427 $self->registered_dispatch_types( {} );
49070d25 428 $self->method_action_class('Catalyst::Action');
429 $self->action_container_class('Catalyst::ActionContainer');
12e28165 430
9e81ba44 431 my @classes =
432 $self->do_load_dispatch_types( @{ $self->preload_dispatch_types } );
433 @{ $self->registered_dispatch_types }{@classes} = (1) x @classes;
b96f127f 434
49070d25 435 foreach my $comp ( values %{ $c->components } ) {
436 $comp->register_actions($c) if $comp->can('register_actions');
1abd6db7 437 }
e494bd6b 438
9e81ba44 439 $self->do_load_dispatch_types( @{ $self->postload_dispatch_types } );
6d030e6f 440
11bd4e3e 441 return unless $c->debug;
99fe1710 442
684d10ed 443 my $privates = Text::SimpleTable->new(
dbf03873 444 [ 20, 'Private' ],
34d28dfd 445 [ 36, 'Class' ],
dbf03873 446 [ 12, 'Method' ]
684d10ed 447 );
99fe1710 448
87b85407 449 my $has_private = 0;
1abd6db7 450 my $walker = sub {
451 my ( $walker, $parent, $prefix ) = @_;
452 $prefix .= $parent->getNodeValue || '';
453 $prefix .= '/' unless $prefix =~ /\/$/;
b7aebc12 454 my $node = $parent->getNodeValue->actions;
99fe1710 455
78d760bb 456 for my $action ( keys %{$node} ) {
b7aebc12 457 my $action_obj = $node->{$action};
b0bb11ec 458 next
459 if ( ( $action =~ /^_.*/ )
460 && ( !$c->config->{show_internal_actions} ) );
684d10ed 461 $privates->row( "$prefix$action", $action_obj->class, $action );
87b85407 462 $has_private = 1;
1abd6db7 463 }
99fe1710 464
1abd6db7 465 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
466 };
99fe1710 467
1abd6db7 468 $walker->( $walker, $self->tree, '' );
11bd4e3e 469 $c->log->debug( "Loaded Private actions:\n" . $privates->draw )
49070d25 470 if ($has_private);
99fe1710 471
a9cbd748 472 # List all public actions
11bd4e3e 473 $_->list($c) for @{ $self->dispatch_types };
1abd6db7 474}
475
9e81ba44 476sub do_load_dispatch_types {
477 my ( $self, @types ) = @_;
478
479 my @loaded;
480
481 # Preload action types
482 for my $type (@types) {
483 my $class =
484 ( $type =~ /^\+(.*)$/ ) ? $1 : "Catalyst::DispatchType::${type}";
485 eval "require $class";
486 Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
487 if $@;
488 push @{ $self->dispatch_types }, $class->new;
489
490 push @loaded, $class;
491 }
492
a13e21ab 493 return @loaded;
9e81ba44 494}
495
1abd6db7 496=head1 AUTHOR
497
498Sebastian Riedel, C<sri@cpan.org>
158c88c0 499Matt S Trout, C<mst@shadowcatsystems.co.uk>
1abd6db7 500
501=head1 COPYRIGHT
502
503This program is free software, you can redistribute it and/or modify it under
504the same terms as Perl itself.
505
506=cut
507
5081;