Updating branch to current revision
[catagits/Catalyst-Runtime.git] / trunk / lib / Catalyst / Dispatcher.pm
CommitLineData
e28a6876 1package Catalyst::Dispatcher;
2
3use Moose;
4use Class::MOP;
5with 'MooseX::Emulate::Class::Accessor::Fast';
6
7use Catalyst::Exception;
8use Catalyst::Utils;
9use Catalyst::Action;
10use Catalyst::ActionContainer;
11use Catalyst::DispatchType::Default;
12use Catalyst::DispatchType::Index;
13use Catalyst::Utils;
14use Text::SimpleTable;
15use Tree::Simple;
16use Tree::Simple::Visitor::FindByPath;
17
18use namespace::clean -except => 'meta';
19
20# Refactoring note:
21# do these belong as package vars or should we build these via a builder method?
22# See Catalyst-Plugin-Server for them being added to, which should be much less ugly.
23
24# Preload these action types
25our @PRELOAD = qw/Index Path Regex/;
26
27# Postload these action types
28our @POSTLOAD = qw/Default/;
29
30# Note - see back-compat methods at end of file.
31has _tree => (is => 'rw', builder => '_build__tree');
32has dispatch_types => (is => 'rw', default => sub { [] }, required => 1, lazy => 1);
33has _registered_dispatch_types => (is => 'rw', default => sub { {} }, required => 1, lazy => 1);
34has _method_action_class => (is => 'rw', default => 'Catalyst::Action');
35has _action_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
36has _container_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
37
38my %dispatch_types = ( pre => \@PRELOAD, post => \@POSTLOAD );
39foreach my $type (keys %dispatch_types) {
40 has $type . "load_dispatch_types" => (
41 is => 'rw', required => 1, lazy => 1, default => sub { $dispatch_types{$type} },
42 traits => ['MooseX::Emulate::Class::Accessor::Fast::Meta::Role::Attribute'], # List assignment is CAF style
43 );
44}
45
46=head1 NAME
47
48Catalyst::Dispatcher - The Catalyst Dispatcher
49
50=head1 SYNOPSIS
51
52See L<Catalyst>.
53
54=head1 DESCRIPTION
55
56This is the class that maps public urls to actions in your Catalyst
57application based on the attributes you set.
58
59=head1 METHODS
60
61=head2 new
62
63Construct a new dispatcher.
64
65=cut
66
67sub _build__tree {
68 my ($self) = @_;
69
70 my $container =
71 Catalyst::ActionContainer->new( { part => '/', actions => {} } );
72
73 return Tree::Simple->new($container, Tree::Simple->ROOT);
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
96=head2 $self->dispatch($c)
97
98Delegate the dispatch to the action that matched the url, or return a
99message about unknown resource
100
101=cut
102
103sub dispatch {
104 my ( $self, $c ) = @_;
105 if ( my $action = $c->action ) {
106 $c->forward( join( '/', '', $action->namespace, '_DISPATCH' ) );
107 }
108 else {
109 my $path = $c->req->path;
110 my $error = $path
111 ? qq/Unknown resource "$path"/
112 : "No default action defined";
113 $c->log->error($error) if $c->debug;
114 $c->error($error);
115 }
116}
117
118# $self->_command2action( $c, $command [, \@arguments ] )
119# $self->_command2action( $c, $command [, \@captures, \@arguments ] )
120# Search for an action, from the command and returns C<($action, $args, $captures)> on
121# success. Returns C<(0)> on error.
122
123sub _command2action {
124 my ( $self, $c, $command, @extra_params ) = @_;
125
126 unless ($command) {
127 $c->log->debug('Nothing to go to') if $c->debug;
128 return 0;
129 }
130
131 my (@args, @captures);
132
133 if ( ref( $extra_params[-2] ) eq 'ARRAY' ) {
134 @captures = @{ splice @extra_params, -2, 1 };
135 }
136
137 if ( ref( $extra_params[-1] ) eq 'ARRAY' ) {
138 @args = @{ pop @extra_params }
139 } else {
140 # this is a copy, it may take some abuse from
141 # ->_invoke_as_path if the path had trailing parts
142 @args = @{ $c->request->arguments };
143 }
144
145 my $action;
146
147 # go to a string path ("/foo/bar/gorch")
148 # or action object
149 if (blessed($command) && $command->isa('Catalyst::Action')) {
150 $action = $command;
151 }
152 else {
153 $action = $self->_invoke_as_path( $c, "$command", \@args );
154 }
155
156 # go to a component ( "MyApp::*::Foo" or $c->component("...")
157 # - a path or an object)
158 unless ($action) {
159 my $method = @extra_params ? $extra_params[0] : "process";
160 $action = $self->_invoke_as_component( $c, $command, $method );
161 }
162
163 return $action, \@args, \@captures;
164}
165
166=head2 $self->visit( $c, $command [, \@arguments ] )
167
168Documented in L<Catalyst>
169
170=cut
171
172sub visit {
173 my $self = shift;
174 $self->_do_visit('visit', @_);
175}
176
177sub _do_visit {
178 my $self = shift;
179 my $opname = shift;
180 my ( $c, $command ) = @_;
181 my ( $action, $args, $captures ) = $self->_command2action(@_);
182 my $error = qq/Couldn't $opname("$command"): /;
183
184 if (!$action) {
185 $error .= qq/Couldn't $opname to command "$command": /
186 .qq/Invalid action or component./;
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 an :Action of 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) {
201 $c->error($error);
202 $c->log->debug($error) if $c->debug;
203 return 0;
204 }
205
206 $action = $self->expand_action($action);
207
208 local $c->request->{arguments} = $args;
209 local $c->request->{captures} = $captures;
210 local $c->{namespace} = $action->{'namespace'};
211 local $c->{action} = $action;
212
213 $self->dispatch($c);
214}
215
216=head2 $self->go( $c, $command [, \@arguments ] )
217
218Documented in L<Catalyst>
219
220=cut
221
222sub go {
223 my $self = shift;
224 $self->_do_visit('go', @_);
225 Catalyst::Exception::Go->throw;
226}
227
228=head2 $self->forward( $c, $command [, \@arguments ] )
229
230Documented in L<Catalyst>
231
232=cut
233
234sub forward {
235 my $self = shift;
236 no warnings 'recursion';
237 $self->_do_forward(forward => @_);
238}
239
240sub _do_forward {
241 my $self = shift;
242 my $opname = shift;
243 my ( $c, $command ) = @_;
244 my ( $action, $args, $captures ) = $self->_command2action(@_);
245
246 if (!$action) {
247 my $error .= qq/Couldn't $opname to command "$command": /
248 .qq/Invalid action or component./;
249 $c->error($error);
250 $c->log->debug($error) if $c->debug;
251 return 0;
252 }
253
254
255 local $c->request->{arguments} = $args;
256 no warnings 'recursion';
257 $action->dispatch( $c );
258
259 return $c->state;
260}
261
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 Catalyst::Exception::Detach->throw;
272}
273
274sub _action_rel2abs {
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;
284}
285
286sub _invoke_as_path {
287 my ( $self, $c, $rel_path, $args ) = @_;
288
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 }
306}
307
308sub _find_component {
309 my ( $self, $c, $component ) = @_;
310
311 # fugly, why doesn't ->component('MyApp') work?
312 return $c if ($component eq blessed($c));
313
314 return blessed($component)
315 ? $component
316 : $c->component($component);
317}
318
319sub _invoke_as_component {
320 my ( $self, $c, $component_or_class, $method ) = @_;
321
322 my $component = $self->_find_component($c, $component_or_class);
323 my $component_class = blessed $component || return 0;
324
325 if (my $code = $component_class->can('action_for')) {
326 my $possible_action = $component->$code($method);
327 return $possible_action if $possible_action;
328 }
329
330 if ( my $code = $component_class->can($method) ) {
331 return $self->_method_action_class->new(
332 {
333 name => $method,
334 code => $code,
335 reverse => "$component_class->$method",
336 class => $component_class,
337 namespace => Catalyst::Utils::class2prefix(
338 $component_class, ref($c)->config->{case_sensitive}
339 ),
340 }
341 );
342 }
343 else {
344 my $error =
345 qq/Couldn't forward to "$component_class". Does not implement "$method"/;
346 $c->error($error);
347 $c->log->debug($error)
348 if $c->debug;
349 return 0;
350 }
351}
352
353=head2 $self->prepare_action($c)
354
355Find an dispatch type that matches $c->req->path, and set args from it.
356
357=cut
358
359sub prepare_action {
360 my ( $self, $c ) = @_;
361 my $req = $c->req;
362 my $path = $req->path;
363 my @path = split /\//, $req->path;
364 $req->args( \my @args );
365
366 unshift( @path, '' ); # Root action
367
368 DESCEND: while (@path) {
369 $path = join '/', @path;
370 $path =~ s#^/+##;
371
372 # Check out dispatch types to see if any will handle the path at
373 # this level
374
375 foreach my $type ( @{ $self->dispatch_types } ) {
376 last DESCEND if $type->match( $c, $path );
377 }
378
379 # If not, move the last part path to args
380 my $arg = pop(@path);
381 $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
382 unshift @args, $arg;
383 }
384
385 s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$req->captures||[]};
386
387 $c->log->debug( 'Path is "' . $req->match . '"' )
388 if ( $c->debug && defined $req->match && length $req->match );
389
390 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
391 if ( $c->debug && @args );
392}
393
394=head2 $self->get_action( $action, $namespace )
395
396returns a named action from a given namespace.
397
398=cut
399
400sub get_action {
401 my ( $self, $name, $namespace ) = @_;
402 return unless $name;
403
404 $namespace = join( "/", grep { length } split '/', ( defined $namespace ? $namespace : "" ) );
405
406 return $self->_action_hash->{"${namespace}/${name}"};
407}
408
409=head2 $self->get_action_by_path( $path );
410
411Returns the named action by its full private path.
412
413=cut
414
415sub get_action_by_path {
416 my ( $self, $path ) = @_;
417 $path =~ s/^\///;
418 $path = "/$path" unless $path =~ /\//;
419 $self->_action_hash->{$path};
420}
421
422=head2 $self->get_actions( $c, $action, $namespace )
423
424=cut
425
426sub get_actions {
427 my ( $self, $c, $action, $namespace ) = @_;
428 return [] unless $action;
429
430 $namespace = join( "/", grep { length } split '/', $namespace || "" );
431
432 my @match = $self->get_containers($namespace);
433
434 return map { $_->get_action($action) } @match;
435}
436
437=head2 $self->get_containers( $namespace )
438
439Return all the action containers for a given namespace, inclusive
440
441=cut
442
443sub get_containers {
444 my ( $self, $namespace ) = @_;
445 $namespace ||= '';
446 $namespace = '' if $namespace eq '/';
447
448 my @containers;
449
450 if ( length $namespace ) {
451 do {
452 push @containers, $self->_container_hash->{$namespace};
453 } while ( $namespace =~ s#/[^/]+$## );
454 }
455
456 return reverse grep { defined } @containers, $self->_container_hash->{''};
457}
458
459=head2 $self->uri_for_action($action, \@captures)
460
461Takes a Catalyst::Action object and action parameters and returns a URI
462part such that if $c->req->path were this URI part, this action would be
463dispatched to with $c->req->captures set to the supplied arrayref.
464
465If the action object is not available for external dispatch or the dispatcher
466cannot determine an appropriate URI, this method will return undef.
467
468=cut
469
470sub uri_for_action {
471 my ( $self, $action, $captures) = @_;
472 $captures ||= [];
473 foreach my $dispatch_type ( @{ $self->dispatch_types } ) {
474 my $uri = $dispatch_type->uri_for_action( $action, $captures );
475 return( $uri eq '' ? '/' : $uri )
476 if defined($uri);
477 }
478 return undef;
479}
480
481=head2 expand_action
482
483expand an action into a full representation of the dispatch.
484mostly useful for chained, other actions will just return a
485single action.
486
487=cut
488
489sub expand_action {
490 my ($self, $action) = @_;
491
492 foreach my $dispatch_type (@{ $self->dispatch_types }) {
493 my $expanded = $dispatch_type->expand_action($action);
494 return $expanded if $expanded;
495 }
496
497 return $action;
498}
499
500=head2 $self->register( $c, $action )
501
502Make sure all required dispatch types for this action are loaded, then
503pass the action to our dispatch types so they can register it if required.
504Also, set up the tree with the action containers.
505
506=cut
507
508sub register {
509 my ( $self, $c, $action ) = @_;
510
511 my $registered = $self->_registered_dispatch_types;
512
513 #my $priv = 0; #seems to be unused
514 foreach my $key ( keys %{ $action->attributes } ) {
515 next if $key eq 'Private';
516 my $class = "Catalyst::DispatchType::$key";
517 unless ( $registered->{$class} ) {
518 # FIXME - Some error checking and re-throwing needed here, as
519 # we eat exceptions loading dispatch types.
520 eval { Class::MOP::load_class($class) };
521 push( @{ $self->dispatch_types }, $class->new ) unless $@;
522 $registered->{$class} = 1;
523 }
524 }
525
526 my @dtypes = @{ $self->dispatch_types };
527 my @normal_dtypes;
528 my @low_precedence_dtypes;
529
530 for my $type ( @dtypes ) {
531 if ($type->_is_low_precedence) {
532 push @low_precedence_dtypes, $type;
533 } else {
534 push @normal_dtypes, $type;
535 }
536 }
537
538 # Pass the action to our dispatch types so they can register it if reqd.
539 my $was_registered = 0;
540 foreach my $type ( @normal_dtypes ) {
541 $was_registered = 1 if $type->register( $c, $action );
542 }
543
544 if (not $was_registered) {
545 foreach my $type ( @low_precedence_dtypes ) {
546 $type->register( $c, $action );
547 }
548 }
549
550 my $namespace = $action->namespace;
551 my $name = $action->name;
552
553 my $container = $self->_find_or_create_action_container($namespace);
554
555 # Set the method value
556 $container->add_action($action);
557
558 $self->_action_hash->{"$namespace/$name"} = $action;
559 $self->_container_hash->{$namespace} = $container;
560}
561
562sub _find_or_create_action_container {
563 my ( $self, $namespace ) = @_;
564
565 my $tree ||= $self->_tree;
566
567 return $tree->getNodeValue unless $namespace;
568
569 my @namespace = split '/', $namespace;
570 return $self->_find_or_create_namespace_node( $tree, @namespace )
571 ->getNodeValue;
572}
573
574sub _find_or_create_namespace_node {
575 my ( $self, $parent, $part, @namespace ) = @_;
576
577 return $parent unless $part;
578
579 my $child =
580 ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
581
582 unless ($child) {
583 my $container = Catalyst::ActionContainer->new($part);
584 $parent->addChild( $child = Tree::Simple->new($container) );
585 }
586
587 $self->_find_or_create_namespace_node( $child, @namespace );
588}
589
590=head2 $self->setup_actions( $class, $context )
591
592Loads all of the preload dispatch types, registers their actions and then
593loads all of the postload dispatch types, and iterates over the tree of
594actions, displaying the debug information if appropriate.
595
596=cut
597
598sub setup_actions {
599 my ( $self, $c ) = @_;
600
601 my @classes =
602 $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
603 @{ $self->_registered_dispatch_types }{@classes} = (1) x @classes;
604
605 foreach my $comp ( values %{ $c->components } ) {
606 $comp->register_actions($c) if $comp->can('register_actions');
607 }
608
609 $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
610
611 return unless $c->debug;
612 $self->_display_action_tables($c);
613}
614
615sub _display_action_tables {
616 my ($self, $c) = @_;
617
618 my $avail_width = Catalyst::Utils::term_width() - 12;
619 my $col1_width = ($avail_width * .25) < 20 ? 20 : int($avail_width * .25);
620 my $col2_width = ($avail_width * .50) < 36 ? 36 : int($avail_width * .50);
621 my $col3_width = $avail_width - $col1_width - $col2_width;
622 my $privates = Text::SimpleTable->new(
623 [ $col1_width, 'Private' ], [ $col2_width, 'Class' ], [ $col3_width, 'Method' ]
624 );
625
626 my $has_private = 0;
627 my $walker = sub {
628 my ( $walker, $parent, $prefix ) = @_;
629 $prefix .= $parent->getNodeValue || '';
630 $prefix .= '/' unless $prefix =~ /\/$/;
631 my $node = $parent->getNodeValue->actions;
632
633 for my $action ( keys %{$node} ) {
634 my $action_obj = $node->{$action};
635 next
636 if ( ( $action =~ /^_.*/ )
637 && ( !$c->config->{show_internal_actions} ) );
638 $privates->row( "$prefix$action", $action_obj->class, $action );
639 $has_private = 1;
640 }
641
642 $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
643 };
644
645 $walker->( $walker, $self->_tree, '' );
646 $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
647 if $has_private;
648
649 # List all public actions
650 $_->list($c) for @{ $self->dispatch_types };
651}
652
653sub _load_dispatch_types {
654 my ( $self, @types ) = @_;
655
656 my @loaded;
657 # Preload action types
658 for my $type (@types) {
659 # first param is undef because we cannot get the appclass
660 my $class = Catalyst::Utils::resolve_namespace(undef, 'Catalyst::DispatchType', $type);
661
662 eval { Class::MOP::load_class($class) };
663 Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
664 if $@;
665 push @{ $self->dispatch_types }, $class->new;
666
667 push @loaded, $class;
668 }
669
670 return @loaded;
671}
672
673=head2 $self->dispatch_type( $type )
674
675Get the DispatchType object of the relevant type, i.e. passing C<$type> of
676C<Chained> would return a L<Catalyst::DispatchType::Chained> object (assuming
677of course it's being used.)
678
679=cut
680
681sub dispatch_type {
682 my ($self, $name) = @_;
683
684 # first param is undef because we cannot get the appclass
685 $name = Catalyst::Utils::resolve_namespace(undef, 'Catalyst::DispatchType', $name);
686
687 for (@{ $self->dispatch_types }) {
688 return $_ if ref($_) eq $name;
689 }
690 return undef;
691}
692
693use Moose;
694
695# 5.70 backwards compatibility hacks.
696
697# Various plugins (e.g. Plugin::Server and Plugin::Authorization::ACL)
698# need the methods here which *should* be private..
699
700# You should be able to use get_actions or get_containers appropriately
701# instead of relying on these methods which expose implementation details
702# of the dispatcher..
703#
704# IRC backlog included below, please come ask if this doesn't work for you.
705#
706# <@t0m> 5.80, the state of. There are things in the dispatcher which have
707# been deprecated, that we yell at anyone for using, which there isn't
708# a good alternative for yet..
709# <@mst> er, get_actions/get_containers provides that doesn't it?
710# <@mst> DispatchTypes are loaded on demand anyway
711# <@t0m> I'm thinking of things like _tree which is aliased to 'tree' with
712# warnings otherwise shit breaks.. We're issuing warnings about the
713# correct set of things which you shouldn't be calling..
714# <@mst> right
715# <@mst> basically, I don't see there's a need for a replacement for anything
716# <@mst> it was never a good idea to call ->tree
717# <@mst> nothingmuch was the only one who did AFAIK
718# <@mst> and he admitted it was a hack ;)
719
720# See also t/lib/TestApp/Plugin/AddDispatchTypes.pm
721
722# Alias _method_name to method_name, add a before modifier to warn..
723foreach my $public_method_name (qw/
724 tree
725 registered_dispatch_types
726 method_action_class
727 action_hash
728 container_hash
729 /) {
730 my $private_method_name = '_' . $public_method_name;
731 my $meta = __PACKAGE__->meta; # Calling meta method here fine as we happen at compile time.
732 $meta->add_method($public_method_name, $meta->get_method($private_method_name));
733 {
734 my %package_hash; # Only warn once per method, per package. These are infrequent enough that
735 # I haven't provided a way to disable them, patches welcome.
736 $meta->add_before_method_modifier($public_method_name, sub {
737 my $class = caller(2);
738 chomp($class);
739 $package_hash{$class}++ || do {
740 warn("Class $class is calling the deprecated method\n"
741 . " Catalyst::Dispatcher::$public_method_name,\n"
742 . " this will be removed in Catalyst 5.9X\n");
743 };
744 });
745 }
746}
747# End 5.70 backwards compatibility hacks.
748
749__PACKAGE__->meta->make_immutable;
750
751=head2 meta
752
753Provided by Moose
754
755=head1 AUTHORS
756
757Catalyst Contributors, see Catalyst.pm
758
759=head1 COPYRIGHT
760
761This library is free software. You can redistribute it and/or modify it under
762the same terms as Perl itself.
763
764=cut
765
7661;