Renaming all the attributes, as making them _private breaks multiple plugins. See...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Dispatcher.pm
CommitLineData
68a748b9 1package Catalyst::Dispatcher;
1abd6db7 2
059c085b 3use Moose;
068c0898 4use Class::MOP;
531f1ab6 5with 'MooseX::Emulate::Class::Accessor::Fast';
059c085b 6
a2f2cde9 7use Catalyst::Exception;
f05af9ba 8use Catalyst::Utils;
fbcc39ad 9use Catalyst::Action;
b7aebc12 10use Catalyst::ActionContainer;
b96f127f 11use Catalyst::DispatchType::Default;
bcccee4e 12use Catalyst::DispatchType::Index;
87b85407 13use Text::SimpleTable;
1abd6db7 14use Tree::Simple;
15use Tree::Simple::Visitor::FindByPath;
e72f8f51 16use Scalar::Util ();
1abd6db7 17
5fb12dbb 18#do these belong as package vars or should we build these via a builder method?
6d030e6f 19# Preload these action types
61a9002d 20our @PRELOAD = qw/Index Path Regex/;
1abd6db7 21
2d1d8f91 22# Postload these action types
61a9002d 23our @POSTLOAD = qw/Default/;
2d1d8f91 24
d91474f5 25# FIXME - All of these should be _private attributes, and should have public accessors
26# which warn about back-compat if you use them.
27has tree => (is => 'rw');
28has dispatch_types => (is => 'rw', default => sub { [] }, required => 1, lazy => 1);
29has registered_dispatch_types => (is => 'rw', default => sub { {} }, required => 1, lazy => 1);
30has method_action_class => (is => 'rw', default => 'Catalyst::Action');
31has action_container_class => (is => 'rw', default => 'Catalyst::ActionContainer');
32has action_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
33has container_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
34# END FIXME
8c80e4f8 35
5fb12dbb 36has preload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@PRELOAD] });
37has postload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@POSTLOAD] });
059c085b 38
083ee5d9 39# Wrap accessors so you can assign a list and it will capture a list ref.
40around qw/preload_dispatch_types postload_dispatch_types/ => sub {
41 my $orig = shift;
42 my $self = shift;
43 return $self->$orig([@_]) if (scalar @_ && ref $_[0] ne 'ARRAY');
44 return $self->$orig(@_);
45};
46
059c085b 47no Moose;
48
1abd6db7 49=head1 NAME
50
9c053379 51Catalyst::Dispatcher - The Catalyst Dispatcher
1abd6db7 52
53=head1 SYNOPSIS
54
55See L<Catalyst>.
56
57=head1 DESCRIPTION
58
4ab87e27 59This is the class that maps public urls to actions in your Catalyst
60application based on the attributes you set.
61
1abd6db7 62=head1 METHODS
63
ac5c933b 64=head2 new
4ab87e27 65
66Construct a new dispatcher.
67
e7bb8d33 68=cut
69
059c085b 70sub BUILD {
71 my ($self, $params) = @_;
9e81ba44 72
068c0898 73 my $container =
059c085b 74 Catalyst::ActionContainer->new( { part => '/', actions => {} } );
a13e21ab 75
d91474f5 76 $self->tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) );
e7bb8d33 77}
78
79=head2 $self->preload_dispatch_types
80
81An arrayref of pre-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
89=head2 $self->postload_dispatch_types
90
91An arrayref of post-loaded dispatchtype classes
92
93Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
94To use a custom class outside the regular C<Catalyst> namespace, prefix
95it with a C<+>, like so:
96
97 +My::Dispatch::Type
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")
e31b525c 148 # or action object
149 if (Scalar::Util::blessed($command) && $command->isa('Catalyst::Action')) {
150 $action = $command;
151 }
152 else {
153 $action = $self->_invoke_as_path( $c, "$command", \@args );
154 }
99fe1710 155
2f381252 156 # go to a component ( "MyApp::*::Foo" or $c->component("...")
157 # - a path or an object)
e72f8f51 158 unless ($action) {
159 my $method = @extra_params ? $extra_params[0] : "process";
160 $action = $self->_invoke_as_component( $c, $command, $method );
161 }
99fe1710 162
2f381252 163 return $action, \@args;
164}
165
ae0e35ee 166=head2 $self->visit( $c, $command [, \@arguments ] )
2f381252 167
168Documented in L<Catalyst>
169
170=cut
171
ae0e35ee 172sub visit {
2f381252 173 my $self = shift;
ae0e35ee 174 $self->_do_visit('visit', @_);
175}
176
177sub _do_visit {
178 my $self = shift;
179 my $opname = shift;
2f381252 180 my ( $c, $command ) = @_;
181 my ( $action, $args ) = $self->_command2action(@_);
ae0e35ee 182 my $error = qq/Couldn't $opname("$command"): /;
2f381252 183
ae0e35ee 184 if (!$action) {
3ea37672 185 $error .= qq/Couldn't $opname to command "$command": /
186 .qq/Invalid action or component./;
ae0e35ee 187 }
188 elsif (!defined $action->namespace) {
189 $error .= qq/Action has no namespace: cannot $opname() to a plain /
190 .qq/method or component, must be a :Action or some sort./
191 }
192 elsif (!$action->class->can('_DISPATCH')) {
193 $error .= qq/Action cannot _DISPATCH. /
194 .qq/Did you try to $opname() a non-controller action?/;
195 }
196 else {
197 $error = q();
198 }
199
200 if($error) {
2f381252 201 $c->error($error);
202 $c->log->debug($error) if $c->debug;
203 return 0;
204 }
205
52f71256 206 $action = $self->expand_action($action);
207
2f381252 208 local $c->request->{arguments} = $args;
ae0e35ee 209 local $c->{namespace} = $action->{'namespace'};
210 local $c->{action} = $action;
211
2f381252 212 $self->dispatch($c);
ae0e35ee 213}
214
215=head2 $self->go( $c, $command [, \@arguments ] )
216
217Documented in L<Catalyst>
218
219=cut
2f381252 220
ae0e35ee 221sub go {
222 my $self = shift;
223 $self->_do_visit('go', @_);
2f381252 224 die $Catalyst::GO;
225}
226
227=head2 $self->forward( $c, $command [, \@arguments ] )
228
229Documented in L<Catalyst>
230
231=cut
232
233sub forward {
234 my $self = shift;
3ea37672 235 $self->_do_forward(forward => @_);
236}
237
238sub _do_forward {
239 my $self = shift;
240 my $opname = shift;
2f381252 241 my ( $c, $command ) = @_;
242 my ( $action, $args ) = $self->_command2action(@_);
99fe1710 243
3ea37672 244 if (!$action) {
245 my $error .= qq/Couldn't $opname to command "$command": /
246 .qq/Invalid action or component./;
e540158b 247 $c->error($error);
248 $c->log->debug($error) if $c->debug;
249 return 0;
250 }
bd7d2e94 251
059c085b 252 no warnings 'recursion';
253
6680c772 254 my $orig_args = $c->request->arguments();
a9078a5a 255 $c->request->arguments($args);
b8f669f3 256 $action->dispatch( $c );
6680c772 257 $c->request->arguments($orig_args);
3ea37672 258
1abd6db7 259 return $c->state;
260}
261
3ea37672 262=head2 $self->detach( $c, $command [, \@arguments ] )
263
264Documented in L<Catalyst>
265
266=cut
267
268sub detach {
269 my ( $self, $c, $command, @args ) = @_;
270 $self->_do_forward(detach => $c, $command, @args ) if $command;
271 die $Catalyst::DETACH;
272}
273
adb53907 274sub _action_rel2abs {
e540158b 275 my ( $self, $c, $path ) = @_;
276
277 unless ( $path =~ m#^/# ) {
278 my $namespace = $c->stack->[-1]->namespace;
279 $path = "$namespace/$path";
280 }
281
282 $path =~ s#^/##;
283 return $path;
adb53907 284}
285
286sub _invoke_as_path {
e540158b 287 my ( $self, $c, $rel_path, $args ) = @_;
288
e540158b 289 my $path = $self->_action_rel2abs( $c, $rel_path );
290
291 my ( $tail, @extra_args );
292 while ( ( $path, $tail ) = ( $path =~ m#^(?:(.*)/)?(\w+)?$# ) )
293 { # allow $path to be empty
294 if ( my $action = $c->get_action( $tail, $path ) ) {
295 push @$args, @extra_args;
296 return $action;
297 }
298 else {
299 return
300 unless $path
301 ; # if a match on the global namespace failed then the whole lookup failed
302 }
303
304 unshift @extra_args, $tail;
305 }
adb53907 306}
307
308sub _find_component_class {
e540158b 309 my ( $self, $c, $component ) = @_;
adb53907 310
e540158b 311 return ref($component)
312 || ref( $c->component($component) )
313 || $c->component($component);
adb53907 314}
315
316sub _invoke_as_component {
e540158b 317 my ( $self, $c, $component, $method ) = @_;
318
319 my $class = $self->_find_component_class( $c, $component ) || return 0;
e540158b 320
321 if ( my $code = $class->can($method) ) {
d91474f5 322 return $self->method_action_class->new(
e540158b 323 {
324 name => $method,
325 code => $code,
326 reverse => "$class->$method",
327 class => $class,
328 namespace => Catalyst::Utils::class2prefix(
329 $class, $c->config->{case_sensitive}
330 ),
331 }
332 );
333 }
334 else {
335 my $error =
336 qq/Couldn't forward to "$class". Does not implement "$method"/;
337 $c->error($error);
338 $c->log->debug($error)
339 if $c->debug;
340 return 0;
341 }
adb53907 342}
343
b5ecfcf0 344=head2 $self->prepare_action($c)
fbcc39ad 345
4ab87e27 346Find an dispatch type that matches $c->req->path, and set args from it.
347
fbcc39ad 348=cut
349
350sub prepare_action {
351 my ( $self, $c ) = @_;
e63bdf38 352 my $req = $c->req;
353 my $path = $req->path;
354 my @path = split /\//, $req->path;
355 $req->args( \my @args );
fbcc39ad 356
61a9002d 357 unshift( @path, '' ); # Root action
78d760bb 358
b96f127f 359 DESCEND: while (@path) {
fbcc39ad 360 $path = join '/', @path;
61a9002d 361 $path =~ s#^/##;
fbcc39ad 362
61a9002d 363 $path = '' if $path eq '/'; # Root action
78d760bb 364
22f3a8dd 365 # Check out dispatch types to see if any will handle the path at
366 # this level
367
d91474f5 368 foreach my $type ( @{ $self->dispatch_types } ) {
2633d7dc 369 last DESCEND if $type->match( $c, $path );
66e28e3f 370 }
b96f127f 371
22f3a8dd 372 # If not, move the last part path to args
4082e678 373 my $arg = pop(@path);
374 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
375 unshift @args, $arg;
fbcc39ad 376 }
377
e63bdf38 378 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$req->captures||[]};
66d7ad40 379
e63bdf38 380 $c->log->debug( 'Path is "' . $req->match . '"' )
076bfad3 381 if ( $c->debug && defined $req->match && length $req->match );
e3a13771 382
fbcc39ad 383 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
384 if ( $c->debug && @args );
385}
386
b5ecfcf0 387=head2 $self->get_action( $action, $namespace )
1abd6db7 388
4ab87e27 389returns a named action from a given namespace.
390
1abd6db7 391=cut
392
393sub get_action {
bcd1002b 394 my ( $self, $name, $namespace ) = @_;
79a3189a 395 return unless $name;
3d0d6d21 396
2f381252 397 $namespace = join( "/", grep { length } split '/', ( defined $namespace ? $namespace : "" ) );
99fe1710 398
d91474f5 399 return $self->action_hash->{"${namespace}/${name}"};
1abd6db7 400}
401
ac5c933b 402=head2 $self->get_action_by_path( $path );
068c0898 403
ac5c933b 404Returns the named action by its full path.
3d0d6d21 405
068c0898 406=cut
3d0d6d21 407
408sub get_action_by_path {
409 my ( $self, $path ) = @_;
ea0e58d9 410 $path =~ s/^\///;
28928de9 411 $path = "/$path" unless $path =~ /\//;
d91474f5 412 $self->action_hash->{$path};
3d0d6d21 413}
414
b5ecfcf0 415=head2 $self->get_actions( $c, $action, $namespace )
a9dc674c 416
417=cut
418
419sub get_actions {
420 my ( $self, $c, $action, $namespace ) = @_;
421 return [] unless $action;
3d0d6d21 422
28928de9 423 $namespace = join( "/", grep { length } split '/', $namespace || "" );
a9dc674c 424
425 my @match = $self->get_containers($namespace);
426
684d10ed 427 return map { $_->get_action($action) } @match;
a9dc674c 428}
429
b5ecfcf0 430=head2 $self->get_containers( $namespace )
cfd04b0c 431
4ab87e27 432Return all the action containers for a given namespace, inclusive
433
cfd04b0c 434=cut
435
436sub get_containers {
437 my ( $self, $namespace ) = @_;
a13e21ab 438 $namespace ||= '';
439 $namespace = '' if $namespace eq '/';
cfd04b0c 440
a13e21ab 441 my @containers;
cfd04b0c 442
7f23827b 443 if ( length $namespace ) {
444 do {
d91474f5 445 push @containers, $self->container_hash->{$namespace};
7f23827b 446 } while ( $namespace =~ s#/[^/]+$## );
447 }
90ce41ba 448
d91474f5 449 return reverse grep { defined } @containers, $self->container_hash->{''};
90ce41ba 450
e63bdf38 451 #return (split '/', $namespace); # isnt this more clear?
a13e21ab 452 my @parts = split '/', $namespace;
cfd04b0c 453}
454
ea0e58d9 455=head2 $self->uri_for_action($action, \@captures)
456
457Takes a Catalyst::Action object and action parameters and returns a URI
458part such that if $c->req->path were this URI part, this action would be
459dispatched to with $c->req->captures set to the supplied arrayref.
460
461If the action object is not available for external dispatch or the dispatcher
462cannot determine an appropriate URI, this method will return undef.
463
464=cut
465
466sub uri_for_action {
467 my ( $self, $action, $captures) = @_;
468 $captures ||= [];
d91474f5 469 foreach my $dispatch_type ( @{ $self->dispatch_types } ) {
ea0e58d9 470 my $uri = $dispatch_type->uri_for_action( $action, $captures );
81e75875 471 return( $uri eq '' ? '/' : $uri )
472 if defined($uri);
ea0e58d9 473 }
474 return undef;
475}
476
ae0e35ee 477=head2 expand_action
478
479expand an action into a full representation of the dispatch.
480mostly useful for chained, other actions will just return a
481single action.
482
483=cut
484
52f71256 485sub expand_action {
486 my ($self, $action) = @_;
487
d91474f5 488 foreach my $dispatch_type (@{ $self->dispatch_types }) {
52f71256 489 my $expanded = $dispatch_type->expand_action($action);
490 return $expanded if $expanded;
491 }
492
493 return $action;
494}
495
b5ecfcf0 496=head2 $self->register( $c, $action )
aad72cc9 497
4ab87e27 498Make sure all required dispatch types for this action are loaded, then
499pass the action to our dispatch types so they can register it if required.
500Also, set up the tree with the action containers.
501
aad72cc9 502=cut
503
79a3189a 504sub register {
505 my ( $self, $c, $action ) = @_;
506
d91474f5 507 my $registered = $self->registered_dispatch_types;
694d15f1 508
e63bdf38 509 #my $priv = 0; #seems to be unused
694d15f1 510 foreach my $key ( keys %{ $action->attributes } ) {
9a6ecf4f 511 next if $key eq 'Private';
694d15f1 512 my $class = "Catalyst::DispatchType::$key";
513 unless ( $registered->{$class} ) {
5fb67d52 514 #some error checking rethrowing here wouldn't hurt.
068c0898 515 eval { Class::MOP::load_class($class) };
d91474f5 516 push( @{ $self->dispatch_types }, $class->new ) unless $@;
694d15f1 517 $registered->{$class} = 1;
518 }
519 }
520
521 # Pass the action to our dispatch types so they can register it if reqd.
d91474f5 522 foreach my $type ( @{ $self->dispatch_types } ) {
9a6ecf4f 523 $type->register( $c, $action );
694d15f1 524 }
525
79a3189a 526 my $namespace = $action->namespace;
a13e21ab 527 my $name = $action->name;
c7116517 528
ad5e4650 529 my $container = $self->_find_or_create_action_container($namespace);
15e9b5dd 530
531 # Set the method value
a13e21ab 532 $container->add_action($action);
c7116517 533
d91474f5 534 $self->action_hash->{"$namespace/$name"} = $action;
535 $self->container_hash->{$namespace} = $container;
15e9b5dd 536}
537
ad5e4650 538sub _find_or_create_action_container {
a13e21ab 539 my ( $self, $namespace ) = @_;
540
d91474f5 541 my $tree ||= $self->tree;
99fe1710 542
a13e21ab 543 return $tree->getNodeValue unless $namespace;
78d760bb 544
a13e21ab 545 my @namespace = split '/', $namespace;
546 return $self->_find_or_create_namespace_node( $tree, @namespace )
547 ->getNodeValue;
8505565b 548}
90ce41ba 549
8505565b 550sub _find_or_create_namespace_node {
a13e21ab 551 my ( $self, $parent, $part, @namespace ) = @_;
78d760bb 552
a13e21ab 553 return $parent unless $part;
8505565b 554
a13e21ab 555 my $child =
556 ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
8505565b 557
a13e21ab 558 unless ($child) {
559 my $container = Catalyst::ActionContainer->new($part);
560 $parent->addChild( $child = Tree::Simple->new($container) );
561 }
99fe1710 562
a13e21ab 563 $self->_find_or_create_namespace_node( $child, @namespace );
1abd6db7 564}
565
4ab87e27 566=head2 $self->setup_actions( $class, $context )
567
1abd6db7 568
569=cut
570
571sub setup_actions {
11bd4e3e 572 my ( $self, $c ) = @_;
99fe1710 573
12e28165 574
9e81ba44 575 my @classes =
ad5e4650 576 $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
d91474f5 577 @{ $self->registered_dispatch_types }{@classes} = (1) x @classes;
b96f127f 578
49070d25 579 foreach my $comp ( values %{ $c->components } ) {
580 $comp->register_actions($c) if $comp->can('register_actions');
1abd6db7 581 }
e494bd6b 582
ad5e4650 583 $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
6d030e6f 584
11bd4e3e 585 return unless $c->debug;
99fe1710 586
684d10ed 587 my $privates = Text::SimpleTable->new(
dbf03873 588 [ 20, 'Private' ],
34d28dfd 589 [ 36, 'Class' ],
dbf03873 590 [ 12, 'Method' ]
684d10ed 591 );
99fe1710 592
87b85407 593 my $has_private = 0;
1abd6db7 594 my $walker = sub {
595 my ( $walker, $parent, $prefix ) = @_;
596 $prefix .= $parent->getNodeValue || '';
597 $prefix .= '/' unless $prefix =~ /\/$/;
b7aebc12 598 my $node = $parent->getNodeValue->actions;
99fe1710 599
78d760bb 600 for my $action ( keys %{$node} ) {
b7aebc12 601 my $action_obj = $node->{$action};
b0bb11ec 602 next
603 if ( ( $action =~ /^_.*/ )
604 && ( !$c->config->{show_internal_actions} ) );
684d10ed 605 $privates->row( "$prefix$action", $action_obj->class, $action );
87b85407 606 $has_private = 1;
1abd6db7 607 }
99fe1710 608
1abd6db7 609 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
610 };
99fe1710 611
d91474f5 612 $walker->( $walker, $self->tree, '' );
1cf0345b 613 $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
614 if $has_private;
99fe1710 615
a9cbd748 616 # List all public actions
d91474f5 617 $_->list($c) for @{ $self->dispatch_types };
1abd6db7 618}
619
ad5e4650 620sub _load_dispatch_types {
9e81ba44 621 my ( $self, @types ) = @_;
622
623 my @loaded;
624
625 # Preload action types
626 for my $type (@types) {
627 my $class =
628 ( $type =~ /^\+(.*)$/ ) ? $1 : "Catalyst::DispatchType::${type}";
0fc2d522 629
068c0898 630 eval { Class::MOP::load_class($class) };
9e81ba44 631 Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
632 if $@;
d91474f5 633 push @{ $self->dispatch_types }, $class->new;
9e81ba44 634
635 push @loaded, $class;
636 }
637
a13e21ab 638 return @loaded;
9e81ba44 639}
640
6680c772 641no Moose;
e5ecd5bc 642__PACKAGE__->meta->make_immutable;
643
059c085b 644=head2 meta
645
646Provided by Moose
647
2f381252 648=head1 AUTHORS
1abd6db7 649
2f381252 650Catalyst Contributors, see Catalyst.pm
1abd6db7 651
652=head1 COPYRIGHT
653
654This program is free software, you can redistribute it and/or modify it under
655the same terms as Perl itself.
656
657=cut
658
6591;