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