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