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