a couple more details
[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
068c0898 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 ) = @_;
66e28e3f 111 if ( $c->action ) {
28591cd7 112 $c->forward( join( '/', '', $c->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
e72f8f51 173 local $c->request->{arguments} = \@args;
b8f669f3 174 $action->dispatch( $c );
99fe1710 175
1abd6db7 176 return $c->state;
177}
178
adb53907 179sub _action_rel2abs {
e540158b 180 my ( $self, $c, $path ) = @_;
181
182 unless ( $path =~ m#^/# ) {
183 my $namespace = $c->stack->[-1]->namespace;
184 $path = "$namespace/$path";
185 }
186
187 $path =~ s#^/##;
188 return $path;
adb53907 189}
190
191sub _invoke_as_path {
e540158b 192 my ( $self, $c, $rel_path, $args ) = @_;
193
e540158b 194 my $path = $self->_action_rel2abs( $c, $rel_path );
195
196 my ( $tail, @extra_args );
197 while ( ( $path, $tail ) = ( $path =~ m#^(?:(.*)/)?(\w+)?$# ) )
198 { # allow $path to be empty
199 if ( my $action = $c->get_action( $tail, $path ) ) {
200 push @$args, @extra_args;
201 return $action;
202 }
203 else {
204 return
205 unless $path
206 ; # if a match on the global namespace failed then the whole lookup failed
207 }
208
209 unshift @extra_args, $tail;
210 }
adb53907 211}
212
213sub _find_component_class {
e540158b 214 my ( $self, $c, $component ) = @_;
adb53907 215
e540158b 216 return ref($component)
217 || ref( $c->component($component) )
218 || $c->component($component);
adb53907 219}
220
221sub _invoke_as_component {
e540158b 222 my ( $self, $c, $component, $method ) = @_;
223
224 my $class = $self->_find_component_class( $c, $component ) || return 0;
e540158b 225
226 if ( my $code = $class->can($method) ) {
059c085b 227 return $self->_method_action_class->new(
e540158b 228 {
229 name => $method,
230 code => $code,
231 reverse => "$class->$method",
232 class => $class,
233 namespace => Catalyst::Utils::class2prefix(
234 $class, $c->config->{case_sensitive}
235 ),
236 }
237 );
238 }
239 else {
240 my $error =
241 qq/Couldn't forward to "$class". Does not implement "$method"/;
242 $c->error($error);
243 $c->log->debug($error)
244 if $c->debug;
245 return 0;
246 }
adb53907 247}
248
b5ecfcf0 249=head2 $self->prepare_action($c)
fbcc39ad 250
4ab87e27 251Find an dispatch type that matches $c->req->path, and set args from it.
252
fbcc39ad 253=cut
254
255sub prepare_action {
256 my ( $self, $c ) = @_;
257 my $path = $c->req->path;
258 my @path = split /\//, $c->req->path;
259 $c->req->args( \my @args );
260
61a9002d 261 unshift( @path, '' ); # Root action
78d760bb 262
b96f127f 263 DESCEND: while (@path) {
fbcc39ad 264 $path = join '/', @path;
61a9002d 265 $path =~ s#^/##;
fbcc39ad 266
61a9002d 267 $path = '' if $path eq '/'; # Root action
78d760bb 268
22f3a8dd 269 # Check out dispatch types to see if any will handle the path at
270 # this level
271
059c085b 272 foreach my $type ( @{ $self->_dispatch_types } ) {
2633d7dc 273 last DESCEND if $type->match( $c, $path );
66e28e3f 274 }
b96f127f 275
22f3a8dd 276 # If not, move the last part path to args
4082e678 277 my $arg = pop(@path);
278 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
279 unshift @args, $arg;
fbcc39ad 280 }
281
cccc8f68 282 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$c->req->captures||[]};
66d7ad40 283
e3a13771 284 $c->log->debug( 'Path is "' . $c->req->match . '"' )
285 if ( $c->debug && $c->req->match );
286
fbcc39ad 287 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
288 if ( $c->debug && @args );
289}
290
b5ecfcf0 291=head2 $self->get_action( $action, $namespace )
1abd6db7 292
4ab87e27 293returns a named action from a given namespace.
294
1abd6db7 295=cut
296
297sub get_action {
bcd1002b 298 my ( $self, $name, $namespace ) = @_;
79a3189a 299 return unless $name;
3d0d6d21 300
28928de9 301 $namespace = join( "/", grep { length } split '/', $namespace || "" );
99fe1710 302
059c085b 303 return $self->_action_hash->{"$namespace/$name"};
1abd6db7 304}
305
068c0898 306=head2 $self->get_action_by_path( $path );
307
308Returns the named action by its full path.
3d0d6d21 309
068c0898 310=cut
3d0d6d21 311
312sub get_action_by_path {
313 my ( $self, $path ) = @_;
ea0e58d9 314 $path =~ s/^\///;
28928de9 315 $path = "/$path" unless $path =~ /\//;
059c085b 316 $self->_action_hash->{$path};
3d0d6d21 317}
318
b5ecfcf0 319=head2 $self->get_actions( $c, $action, $namespace )
a9dc674c 320
321=cut
322
323sub get_actions {
324 my ( $self, $c, $action, $namespace ) = @_;
325 return [] unless $action;
3d0d6d21 326
28928de9 327 $namespace = join( "/", grep { length } split '/', $namespace || "" );
a9dc674c 328
329 my @match = $self->get_containers($namespace);
330
684d10ed 331 return map { $_->get_action($action) } @match;
a9dc674c 332}
333
b5ecfcf0 334=head2 $self->get_containers( $namespace )
cfd04b0c 335
4ab87e27 336Return all the action containers for a given namespace, inclusive
337
cfd04b0c 338=cut
339
340sub get_containers {
341 my ( $self, $namespace ) = @_;
a13e21ab 342 $namespace ||= '';
343 $namespace = '' if $namespace eq '/';
cfd04b0c 344
a13e21ab 345 my @containers;
cfd04b0c 346
7f23827b 347 if ( length $namespace ) {
348 do {
059c085b 349 push @containers, $self->_container_hash->{$namespace};
7f23827b 350 } while ( $namespace =~ s#/[^/]+$## );
351 }
90ce41ba 352
059c085b 353 return reverse grep { defined } @containers, $self->_container_hash->{''};
90ce41ba 354
a13e21ab 355 my @parts = split '/', $namespace;
cfd04b0c 356}
357
ea0e58d9 358=head2 $self->uri_for_action($action, \@captures)
359
360Takes a Catalyst::Action object and action parameters and returns a URI
361part such that if $c->req->path were this URI part, this action would be
362dispatched to with $c->req->captures set to the supplied arrayref.
363
364If the action object is not available for external dispatch or the dispatcher
365cannot determine an appropriate URI, this method will return undef.
366
367=cut
368
369sub uri_for_action {
370 my ( $self, $action, $captures) = @_;
371 $captures ||= [];
059c085b 372 foreach my $dispatch_type ( @{ $self->_dispatch_types } ) {
ea0e58d9 373 my $uri = $dispatch_type->uri_for_action( $action, $captures );
81e75875 374 return( $uri eq '' ? '/' : $uri )
375 if defined($uri);
ea0e58d9 376 }
377 return undef;
378}
379
b5ecfcf0 380=head2 $self->register( $c, $action )
aad72cc9 381
4ab87e27 382Make sure all required dispatch types for this action are loaded, then
383pass the action to our dispatch types so they can register it if required.
384Also, set up the tree with the action containers.
385
aad72cc9 386=cut
387
79a3189a 388sub register {
389 my ( $self, $c, $action ) = @_;
390
059c085b 391 my $registered = $self->_registered_dispatch_types;
694d15f1 392
393 my $priv = 0;
394 foreach my $key ( keys %{ $action->attributes } ) {
9a6ecf4f 395 next if $key eq 'Private';
694d15f1 396 my $class = "Catalyst::DispatchType::$key";
397 unless ( $registered->{$class} ) {
068c0898 398 #eval "require $class";
5fb67d52 399 #some error checking rethrowing here wouldn't hurt.
068c0898 400 eval { Class::MOP::load_class($class) };
059c085b 401 push( @{ $self->_dispatch_types }, $class->new ) unless $@;
694d15f1 402 $registered->{$class} = 1;
403 }
404 }
405
406 # Pass the action to our dispatch types so they can register it if reqd.
059c085b 407 foreach my $type ( @{ $self->_dispatch_types } ) {
9a6ecf4f 408 $type->register( $c, $action );
694d15f1 409 }
410
79a3189a 411 my $namespace = $action->namespace;
a13e21ab 412 my $name = $action->name;
c7116517 413
ad5e4650 414 my $container = $self->_find_or_create_action_container($namespace);
15e9b5dd 415
416 # Set the method value
a13e21ab 417 $container->add_action($action);
c7116517 418
059c085b 419 $self->_action_hash->{"$namespace/$name"} = $action;
420 $self->_container_hash->{$namespace} = $container;
15e9b5dd 421}
422
ad5e4650 423sub _find_or_create_action_container {
a13e21ab 424 my ( $self, $namespace ) = @_;
425
059c085b 426 my $tree ||= $self->_tree;
99fe1710 427
a13e21ab 428 return $tree->getNodeValue unless $namespace;
78d760bb 429
a13e21ab 430 my @namespace = split '/', $namespace;
431 return $self->_find_or_create_namespace_node( $tree, @namespace )
432 ->getNodeValue;
8505565b 433}
90ce41ba 434
8505565b 435sub _find_or_create_namespace_node {
a13e21ab 436 my ( $self, $parent, $part, @namespace ) = @_;
78d760bb 437
a13e21ab 438 return $parent unless $part;
8505565b 439
a13e21ab 440 my $child =
441 ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
8505565b 442
a13e21ab 443 unless ($child) {
444 my $container = Catalyst::ActionContainer->new($part);
445 $parent->addChild( $child = Tree::Simple->new($container) );
446 }
99fe1710 447
a13e21ab 448 $self->_find_or_create_namespace_node( $child, @namespace );
1abd6db7 449}
450
4ab87e27 451=head2 $self->setup_actions( $class, $context )
452
1abd6db7 453
454=cut
455
456sub setup_actions {
11bd4e3e 457 my ( $self, $c ) = @_;
99fe1710 458
059c085b 459 $self->_dispatch_types( [] );
460 $self->_registered_dispatch_types( {} );
461 $self->_method_action_class('Catalyst::Action');
462 $self->_action_container_class('Catalyst::ActionContainer');
12e28165 463
9e81ba44 464 my @classes =
ad5e4650 465 $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
059c085b 466 @{ $self->_registered_dispatch_types }{@classes} = (1) x @classes;
b96f127f 467
49070d25 468 foreach my $comp ( values %{ $c->components } ) {
469 $comp->register_actions($c) if $comp->can('register_actions');
1abd6db7 470 }
e494bd6b 471
ad5e4650 472 $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
6d030e6f 473
11bd4e3e 474 return unless $c->debug;
99fe1710 475
684d10ed 476 my $privates = Text::SimpleTable->new(
dbf03873 477 [ 20, 'Private' ],
34d28dfd 478 [ 36, 'Class' ],
dbf03873 479 [ 12, 'Method' ]
684d10ed 480 );
99fe1710 481
87b85407 482 my $has_private = 0;
1abd6db7 483 my $walker = sub {
484 my ( $walker, $parent, $prefix ) = @_;
485 $prefix .= $parent->getNodeValue || '';
486 $prefix .= '/' unless $prefix =~ /\/$/;
b7aebc12 487 my $node = $parent->getNodeValue->actions;
99fe1710 488
78d760bb 489 for my $action ( keys %{$node} ) {
b7aebc12 490 my $action_obj = $node->{$action};
b0bb11ec 491 next
492 if ( ( $action =~ /^_.*/ )
493 && ( !$c->config->{show_internal_actions} ) );
684d10ed 494 $privates->row( "$prefix$action", $action_obj->class, $action );
87b85407 495 $has_private = 1;
1abd6db7 496 }
99fe1710 497
1abd6db7 498 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
499 };
99fe1710 500
059c085b 501 $walker->( $walker, $self->_tree, '' );
1cf0345b 502 $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
503 if $has_private;
99fe1710 504
a9cbd748 505 # List all public actions
059c085b 506 $_->list($c) for @{ $self->_dispatch_types };
1abd6db7 507}
508
ad5e4650 509sub _load_dispatch_types {
9e81ba44 510 my ( $self, @types ) = @_;
511
512 my @loaded;
513
514 # Preload action types
515 for my $type (@types) {
516 my $class =
517 ( $type =~ /^\+(.*)$/ ) ? $1 : "Catalyst::DispatchType::${type}";
068c0898 518 #eval "require $class";
519 eval { Class::MOP::load_class($class) };
9e81ba44 520 Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
521 if $@;
059c085b 522 push @{ $self->_dispatch_types }, $class->new;
9e81ba44 523
524 push @loaded, $class;
525 }
526
a13e21ab 527 return @loaded;
9e81ba44 528}
529
059c085b 530=head2 meta
531
532Provided by Moose
533
1abd6db7 534=head1 AUTHOR
535
536Sebastian Riedel, C<sri@cpan.org>
158c88c0 537Matt S Trout, C<mst@shadowcatsystems.co.uk>
1abd6db7 538
539=head1 COPYRIGHT
540
541This program is free software, you can redistribute it and/or modify it under
542the same terms as Perl itself.
543
544=cut
545
5461;