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