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