Hack _invoke_as_component in a horrible way, so that it gives us back a path Action...
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Dispatcher.pm
1 package Catalyst::Dispatcher;
2
3 use Moose;
4 use Class::MOP;
5 with 'MooseX::Emulate::Class::Accessor::Fast';
6
7 use Catalyst::Exception;
8 use Catalyst::Utils;
9 use Catalyst::Action;
10 use Catalyst::ActionContainer;
11 use Catalyst::DispatchType::Default;
12 use Catalyst::DispatchType::Index;
13 use Catalyst::Utils;
14 use Text::SimpleTable;
15 use Tree::Simple;
16 use Tree::Simple::Visitor::FindByPath;
17
18 # Refactoring note:
19 # do these belong as package vars or should we build these via a builder method?
20 # See Catalyst-Plugin-Server for them being added to, which should be much less ugly.
21
22 # Preload these action types
23 our @PRELOAD = qw/Index Path Regex/;
24
25 # Postload these action types
26 our @POSTLOAD = qw/Default/;
27
28 # Note - see back-compat methods at end of file.
29 has _tree => (is => 'rw');
30 has _dispatch_types => (is => 'rw', default => sub { [] }, required => 1, lazy => 1);
31 has _registered_dispatch_types => (is => 'rw', default => sub { {} }, required => 1, lazy => 1);
32 has _method_action_class => (is => 'rw', default => 'Catalyst::Action');
33 has _action_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
34 has _container_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
35 has preload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@PRELOAD] });
36
37 has postload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@POSTLOAD] });
38
39 # Wrap accessors so you can assign a list and it will capture a list ref.
40 around 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
47 no Moose;
48
49 =head1 NAME
50
51 Catalyst::Dispatcher - The Catalyst Dispatcher
52
53 =head1 SYNOPSIS
54
55 See L<Catalyst>.
56
57 =head1 DESCRIPTION
58
59 This is the class that maps public urls to actions in your Catalyst
60 application based on the attributes you set.
61
62 =head1 METHODS
63
64 =head2 new 
65
66 Construct a new dispatcher.
67
68 =cut
69
70 sub BUILD {
71   my ($self, $params) = @_;
72
73   my $container =
74     Catalyst::ActionContainer->new( { part => '/', actions => {} } );
75
76   $self->_tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) );
77 }
78
79 =head2 $self->preload_dispatch_types
80
81 An arrayref of pre-loaded dispatchtype classes
82
83 Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
84 To use a custom class outside the regular C<Catalyst> namespace, prefix
85 it with a C<+>, like so:
86
87     +My::Dispatch::Type
88
89 =head2 $self->postload_dispatch_types
90
91 An arrayref of post-loaded dispatchtype classes
92
93 Entries are considered to be available as C<Catalyst::DispatchType::CLASS>
94 To use a custom class outside the regular C<Catalyst> namespace, prefix
95 it with a C<+>, like so:
96
97     +My::Dispatch::Type
98
99 =head2 $self->dispatch($c)
100
101 Delegate the dispatch to the action that matched the url, or return a
102 message about unknown resource
103
104 =cut
105
106 sub dispatch {
107     my ( $self, $c ) = @_;
108     if ( my $action = $c->action ) {
109         $c->forward( join( '/', '', $action->namespace, '_DISPATCH' ) );
110     }
111     else {
112         my $path  = $c->req->path;
113         my $error = $path
114           ? qq/Unknown resource "$path"/
115           : "No default action defined";
116         $c->log->error($error) if $c->debug;
117         $c->error($error);
118     }
119 }
120
121 # $self->_command2action( $c, $command [, \@arguments ] )
122 # $self->_command2action( $c, $command [, \@captures, \@arguments ] )
123 # Search for an action, from the command and returns C<($action, $args, $captures)> on
124 # success. Returns C<(0)> on error.
125
126 sub _command2action {
127     my ( $self, $c, $command, @extra_params ) = @_;
128
129     unless ($command) {
130         $c->log->debug('Nothing to go to') if $c->debug;
131         return 0;
132     }
133
134     my (@args, @captures);
135
136     if ( ref( $extra_params[-2] ) eq 'ARRAY' ) {
137         @captures = @{ pop @extra_params };
138     }
139
140     if ( ref( $extra_params[-1] ) eq 'ARRAY' ) {
141         @args = @{ pop @extra_params }
142     } else {
143         # this is a copy, it may take some abuse from
144         # ->_invoke_as_path if the path had trailing parts
145         @args = @{ $c->request->arguments };
146     }
147
148     my $action;
149
150     # go to a string path ("/foo/bar/gorch")
151     # or action object
152     if (blessed($command) && $command->isa('Catalyst::Action')) {
153         $action = $command;
154     }
155     else {
156         $action = $self->_invoke_as_path( $c, "$command", \@args );
157     }
158
159     # go to a component ( "MyApp::*::Foo" or $c->component("...")
160     # - a path or an object)
161     unless ($action) {
162         my $method = @extra_params ? $extra_params[0] : "process";
163         $action = $self->_invoke_as_component( $c, $command, $method );
164     }
165
166     return $action, \@args, \@captures;
167 }
168
169 =head2 $self->visit( $c, $command [, \@arguments ] )
170
171 Documented in L<Catalyst>
172
173 =cut
174
175 sub visit {
176     my $self = shift;
177     $self->_do_visit('visit', @_);
178 }
179
180 sub _do_visit {
181     my $self = shift;
182     my $opname = shift;
183     my ( $c, $command ) = @_;
184     my ( $action, $args, $captures ) = $self->_command2action(@_);
185     my $error = qq/Couldn't $opname("$command"): /;
186
187     if (!$action) {
188         $error .= qq/Couldn't $opname to command "$command": /
189                  .qq/Invalid action or component./;
190     }
191     elsif (!defined $action->namespace) {
192         $error .= qq/Action has no namespace: cannot $opname() to a plain /
193                  .qq/method or component, must be a :Action or some sort./
194     }
195     elsif (!$action->class->can('_DISPATCH')) {
196         $error .= qq/Action cannot _DISPATCH. /
197                  .qq/Did you try to $opname() a non-controller action?/;
198     }
199     else {
200         $error = q();
201     }
202
203     if($error) {
204         $c->error($error);
205         $c->log->debug($error) if $c->debug;
206         return 0;
207     }
208
209     $action = $self->expand_action($action);
210
211     local $c->request->{arguments} = $args;
212     local $c->request->{captures}  = $captures;
213     local $c->{namespace} = $action->{'namespace'};
214     local $c->{action} = $action;
215
216     $self->dispatch($c);
217 }
218
219 =head2 $self->go( $c, $command [, \@arguments ] )
220
221 Documented in L<Catalyst>
222
223 =cut
224
225 sub go {
226     my $self = shift;
227     $self->_do_visit('go', @_);
228     die $Catalyst::GO;
229 }
230
231 =head2 $self->forward( $c, $command [, \@arguments ] )
232
233 Documented in L<Catalyst>
234
235 =cut
236
237 sub forward {
238     my $self = shift;
239     $self->_do_forward(forward => @_);
240 }
241
242 sub _do_forward {
243     my $self = shift;
244     my $opname = shift;
245     my ( $c, $command ) = @_;
246     my ( $action, $args, $captures ) = $self->_command2action(@_);
247
248     if (!$action) {
249         my $error .= qq/Couldn't $opname to command "$command": /
250                     .qq/Invalid action or component./;
251         $c->error($error);
252         $c->log->debug($error) if $c->debug;
253         return 0;
254     }
255
256     no warnings 'recursion';
257
258     local $c->request->{arguments} = $args;
259     $action->dispatch( $c );
260
261     return $c->state;
262 }
263
264 =head2 $self->detach( $c, $command [, \@arguments ] )
265
266 Documented in L<Catalyst>
267
268 =cut
269
270 sub detach {
271     my ( $self, $c, $command, @args ) = @_;
272     $self->_do_forward(detach => $c, $command, @args ) if $command;
273     die $Catalyst::DETACH;
274 }
275
276 sub _action_rel2abs {
277     my ( $self, $c, $path ) = @_;
278
279     unless ( $path =~ m#^/# ) {
280         my $namespace = $c->stack->[-1]->namespace;
281         $path = "$namespace/$path";
282     }
283
284     $path =~ s#^/##;
285     return $path;
286 }
287
288 sub _invoke_as_path {
289     my ( $self, $c, $rel_path, $args ) = @_;
290
291     my $path = $self->_action_rel2abs( $c, $rel_path );
292
293     my ( $tail, @extra_args );
294     while ( ( $path, $tail ) = ( $path =~ m#^(?:(.*)/)?(\w+)?$# ) )
295     {                           # allow $path to be empty
296         if ( my $action = $c->get_action( $tail, $path ) ) {
297             push @$args, @extra_args;
298             return $action;
299         }
300         else {
301             return
302               unless $path
303               ; # if a match on the global namespace failed then the whole lookup failed
304         }
305
306         unshift @extra_args, $tail;
307     }
308 }
309
310 sub _find_component_class {
311     my ( $self, $c, $component ) = @_;
312
313     return ref($component)
314       || ref( $c->component($component) )
315       || $c->component($component);
316 }
317
318 sub _invoke_as_component {
319     my ( $self, $c, $component, $method ) = @_;
320
321     my $class = $self->_find_component_class( $c, $component ) || return 0;
322
323     ### XXX FIXME - Horrible hack to get proper action objects for
324     ###             controller paths..
325     if ($class =~ /::C(ontroller)?::/) {
326         my $possible_path = $class . '/' . $method;
327         $possible_path =~ s/.+::C(ontroller)?:://;
328         $possible_path =~ s|::|/|g;
329         $possible_path =~ tr/A-Z/a-z/;
330         my $possible_action = $self->_invoke_as_path( $c, '/' . $possible_path );
331         return $possible_action if $possible_action;
332     }
333
334     if ( my $code = $class->can($method) ) {
335         return $self->_method_action_class->new(
336             {
337                 name      => $method,
338                 code      => $code,
339                 reverse   => "$class->$method",
340                 class     => $class,
341                 namespace => Catalyst::Utils::class2prefix(
342                     $class, $c->config->{case_sensitive}
343                 ),
344             }
345         );
346     }
347     else {
348         my $error =
349           qq/Couldn't forward to "$class". Does not implement "$method"/;
350         $c->error($error);
351         $c->log->debug($error)
352           if $c->debug;
353         return 0;
354     }
355 }
356
357 =head2 $self->prepare_action($c)
358
359 Find an dispatch type that matches $c->req->path, and set args from it.
360
361 =cut
362
363 sub prepare_action {
364     my ( $self, $c ) = @_;
365     my $req = $c->req;
366     my $path = $req->path;
367     my @path = split /\//, $req->path;
368     $req->args( \my @args );
369
370     unshift( @path, '' );    # Root action
371
372   DESCEND: while (@path) {
373         $path = join '/', @path;
374         $path =~ s#^/##;
375
376         $path = '' if $path eq '/';    # Root action
377
378         # Check out dispatch types to see if any will handle the path at
379         # this level
380
381         foreach my $type ( @{ $self->_dispatch_types } ) {
382             last DESCEND if $type->match( $c, $path );
383         }
384
385         # If not, move the last part path to args
386         my $arg = pop(@path);
387         $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
388         unshift @args, $arg;
389     }
390
391     s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$req->captures||[]};
392
393     $c->log->debug( 'Path is "' . $req->match . '"' )
394       if ( $c->debug && defined $req->match && length $req->match );
395
396     $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
397       if ( $c->debug && @args );
398 }
399
400 =head2 $self->get_action( $action, $namespace )
401
402 returns a named action from a given namespace.
403
404 =cut
405
406 sub get_action {
407     my ( $self, $name, $namespace ) = @_;
408     return unless $name;
409
410     $namespace = join( "/", grep { length } split '/', ( defined $namespace ? $namespace : "" ) );
411
412     return $self->_action_hash->{"${namespace}/${name}"};
413 }
414
415 =head2 $self->get_action_by_path( $path ); 
416
417 Returns the named action by its full path. 
418
419 =cut
420
421 sub get_action_by_path {
422     my ( $self, $path ) = @_;
423     $path =~ s/^\///;
424     $path = "/$path" unless $path =~ /\//;
425     $self->_action_hash->{$path};
426 }
427
428 =head2 $self->get_actions( $c, $action, $namespace )
429
430 =cut
431
432 sub get_actions {
433     my ( $self, $c, $action, $namespace ) = @_;
434     return [] unless $action;
435
436     $namespace = join( "/", grep { length } split '/', $namespace || "" );
437
438     my @match = $self->get_containers($namespace);
439
440     return map { $_->get_action($action) } @match;
441 }
442
443 =head2 $self->get_containers( $namespace )
444
445 Return all the action containers for a given namespace, inclusive
446
447 =cut
448
449 sub get_containers {
450     my ( $self, $namespace ) = @_;
451     $namespace ||= '';
452     $namespace = '' if $namespace eq '/';
453
454     my @containers;
455
456     if ( length $namespace ) {
457         do {
458             push @containers, $self->_container_hash->{$namespace};
459         } while ( $namespace =~ s#/[^/]+$## );
460     }
461
462     return reverse grep { defined } @containers, $self->_container_hash->{''};
463
464     #return (split '/', $namespace); # isnt this more clear?
465     my @parts = split '/', $namespace;
466 }
467
468 =head2 $self->uri_for_action($action, \@captures)
469
470 Takes a Catalyst::Action object and action parameters and returns a URI
471 part such that if $c->req->path were this URI part, this action would be
472 dispatched to with $c->req->captures set to the supplied arrayref.
473
474 If the action object is not available for external dispatch or the dispatcher
475 cannot determine an appropriate URI, this method will return undef.
476
477 =cut
478
479 sub uri_for_action {
480     my ( $self, $action, $captures) = @_;
481     $captures ||= [];
482     foreach my $dispatch_type ( @{ $self->_dispatch_types } ) {
483         my $uri = $dispatch_type->uri_for_action( $action, $captures );
484         return( $uri eq '' ? '/' : $uri )
485             if defined($uri);
486     }
487     return undef;
488 }
489
490 =head2 expand_action
491
492 expand an action into a full representation of the dispatch.
493 mostly useful for chained, other actions will just return a
494 single action.
495
496 =cut
497
498 sub expand_action {
499     my ($self, $action) = @_;
500
501     foreach my $dispatch_type (@{ $self->_dispatch_types }) {
502         my $expanded = $dispatch_type->expand_action($action);
503         return $expanded if $expanded;
504     }
505
506     return $action;
507 }
508
509 =head2 $self->register( $c, $action )
510
511 Make sure all required dispatch types for this action are loaded, then
512 pass the action to our dispatch types so they can register it if required.
513 Also, set up the tree with the action containers.
514
515 =cut
516
517 sub register {
518     my ( $self, $c, $action ) = @_;
519
520     my $registered = $self->_registered_dispatch_types;
521
522     #my $priv = 0; #seems to be unused
523     foreach my $key ( keys %{ $action->attributes } ) {
524         next if $key eq 'Private';
525         my $class = "Catalyst::DispatchType::$key";
526         unless ( $registered->{$class} ) {
527             # FIXME - Some error checking and re-throwing needed here, as
528             #         we eat exceptions loading dispatch types.
529             eval { Class::MOP::load_class($class) };
530             push( @{ $self->_dispatch_types }, $class->new ) unless $@;
531             $registered->{$class} = 1;
532         }
533     }
534
535     # Pass the action to our dispatch types so they can register it if reqd.
536     foreach my $type ( @{ $self->_dispatch_types } ) {
537         $type->register( $c, $action );
538     }
539
540     my $namespace = $action->namespace;
541     my $name      = $action->name;
542
543     my $container = $self->_find_or_create_action_container($namespace);
544
545     # Set the method value
546     $container->add_action($action);
547
548     $self->_action_hash->{"$namespace/$name"} = $action;
549     $self->_container_hash->{$namespace} = $container;
550 }
551
552 sub _find_or_create_action_container {
553     my ( $self, $namespace ) = @_;
554
555     my $tree ||= $self->_tree;
556
557     return $tree->getNodeValue unless $namespace;
558
559     my @namespace = split '/', $namespace;
560     return $self->_find_or_create_namespace_node( $tree, @namespace )
561       ->getNodeValue;
562 }
563
564 sub _find_or_create_namespace_node {
565     my ( $self, $parent, $part, @namespace ) = @_;
566
567     return $parent unless $part;
568
569     my $child =
570       ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
571
572     unless ($child) {
573         my $container = Catalyst::ActionContainer->new($part);
574         $parent->addChild( $child = Tree::Simple->new($container) );
575     }
576
577     $self->_find_or_create_namespace_node( $child, @namespace );
578 }
579
580 =head2 $self->setup_actions( $class, $context )
581
582 Loads all of the preload dispatch types, registers their actions and then
583 loads all of the postload dispatch types, and iterates over the tree of
584 actions, displaying the debug information if appropriate.
585
586 =cut
587
588 sub setup_actions {
589     my ( $self, $c ) = @_;
590
591     my @classes =
592       $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
593     @{ $self->_registered_dispatch_types }{@classes} = (1) x @classes;
594
595     foreach my $comp ( values %{ $c->components } ) {
596         $comp->register_actions($c) if $comp->can('register_actions');
597     }
598
599     $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
600
601     return unless $c->debug;
602     $self->_display_action_tables($c);
603 }
604
605 sub _display_action_tables {
606     my ($self, $c) = @_;
607
608     my $column_width = Catalyst::Utils::term_width() - 20 - 36 - 12;
609     my $privates = Text::SimpleTable->new(
610         [ 20, 'Private' ], [ 36, 'Class' ], [ $column_width, 'Method' ]
611     );
612
613     my $has_private = 0;
614     my $walker = sub {
615         my ( $walker, $parent, $prefix ) = @_;
616         $prefix .= $parent->getNodeValue || '';
617         $prefix .= '/' unless $prefix =~ /\/$/;
618         my $node = $parent->getNodeValue->actions;
619
620         for my $action ( keys %{$node} ) {
621             my $action_obj = $node->{$action};
622             next
623               if ( ( $action =~ /^_.*/ )
624                 && ( !$c->config->{show_internal_actions} ) );
625             $privates->row( "$prefix$action", $action_obj->class, $action );
626             $has_private = 1;
627         }
628
629         $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
630     };
631
632     $walker->( $walker, $self->_tree, '' );
633     $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
634       if $has_private;
635
636     # List all public actions
637     $_->list($c) for @{ $self->_dispatch_types };
638 }
639
640 sub _load_dispatch_types {
641     my ( $self, @types ) = @_;
642
643     my @loaded;
644
645     # Preload action types
646     for my $type (@types) {
647         my $class =
648           ( $type =~ /^\+(.*)$/ ) ? $1 : "Catalyst::DispatchType::${type}";
649
650         eval { Class::MOP::load_class($class) };
651         Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
652           if $@;
653         push @{ $self->_dispatch_types }, $class->new;
654
655         push @loaded, $class;
656     }
657
658     return @loaded;
659 }
660
661 # Dont document this until someone else is happy with beaviour. Ash 2009/03/16
662 sub dispatch_type {
663     my ($self, $name) = @_;
664
665     unless ($name =~ s/^\+//) {
666         $name = "Catalyst::DispatchType::" . $name;
667     }
668
669     for (@{ $self->_dispatch_types }) {
670         return $_ if ref($_) eq $name;
671     }
672     return undef;
673 }
674
675 use Moose;
676
677 # 5.70 backwards compatibility hacks.
678
679 # Various plugins (e.g. Plugin::Server and Plugin::Authorization::ACL)
680 # need the methods here which *should* be private..
681
682 # However we can't really take them away until there is a sane API for
683 # building actions and configuring / introspecting the dispatcher.
684 # In 5.90, we should build that infrastructure, port the plugins which
685 # use it, and then take the crap below away.
686 # See also t/lib/TestApp/Plugin/AddDispatchTypes.pm
687
688 # Alias _method_name to method_name, add a before modifier to warn..
689 foreach my $public_method_name (qw/ 
690         tree 
691         dispatch_types 
692         registered_dispatch_types 
693         method_action_class  
694         action_hash 
695         container_hash
696     /) {
697     my $private_method_name = '_' . $public_method_name;
698     my $meta = __PACKAGE__->meta; # Calling meta method here fine as we happen at compile time.
699     $meta->add_method($public_method_name, $meta->get_method($private_method_name));
700     {
701         my %package_hash; # Only warn once per method, per package. These are infrequent enough that
702                           # I haven't provided a way to disable them, patches welcome.
703         $meta->add_before_method_modifier($public_method_name, sub {
704             my $class = blessed(shift);
705             $package_hash{$class}++ || do { 
706                 warn("Class $class is calling the deprecated method Catalyst::Dispatcher::$public_method_name,\n"
707                     . "this will be removed in Catalyst 5.9X");
708             };
709         });
710     }
711 }
712 # End 5.70 backwards compatibility hacks.
713
714 no Moose;
715 __PACKAGE__->meta->make_immutable;
716
717 =head2 meta
718
719 Provided by Moose
720
721 =head1 AUTHORS
722
723 Catalyst Contributors, see Catalyst.pm
724
725 =head1 COPYRIGHT
726
727 This program is free software, you can redistribute it and/or modify it under
728 the same terms as Perl itself.
729
730 =cut
731
732 1;