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