fd176156bbf349ca8ec8a9eec5969c59af5ed5ee
[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 Text::SimpleTable;
14 use Tree::Simple;
15 use Tree::Simple::Visitor::FindByPath;
16 use Scalar::Util ();
17
18 #do these belong as package vars or should we build these via a builder method?
19 # Preload these action types
20 our @PRELOAD = qw/Index Path Regex/;
21
22 # Postload these action types
23 our @POSTLOAD = qw/Default/;
24
25 # FIXME - All of these should be _private attributes, and should have public accessors
26 #         which warn about back-compat if you use them.
27 has tree => (is => 'rw');
28 has dispatch_types => (is => 'rw', default => sub { [] }, required => 1, lazy => 1);
29 has registered_dispatch_types => (is => 'rw', default => sub { {} }, required => 1, lazy => 1);
30 has method_action_class => (is => 'rw', default => 'Catalyst::Action');
31 has action_container_class => (is => 'rw', default => 'Catalyst::ActionContainer');
32 has action_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
33 has container_hash => (is => 'rw', required => 1, lazy => 1, default => sub { {} });
34 # END FIXME
35
36 has preload_dispatch_types => (is => 'rw', required => 1, lazy => 1, default => sub { [@PRELOAD] });
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
105 =cut
106
107 sub dispatch {
108     my ( $self, $c ) = @_;
109     if ( my $action = $c->action ) {
110         $c->forward( join( '/', '', $action->namespace, '_DISPATCH' ) );
111     }
112
113     else {
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
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.
126
127 sub _command2action {
128     my ( $self, $c, $command, @extra_params ) = @_;
129
130     unless ($command) {
131         $c->log->debug('Nothing to go to') if $c->debug;
132         return 0;
133     }
134
135     my @args;
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 (Scalar::Util::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;
164 }
165
166 =head2 $self->visit( $c, $command [, \@arguments ] )
167
168 Documented in L<Catalyst>
169
170 =cut
171
172 sub visit {
173     my $self = shift;
174     $self->_do_visit('visit', @_);
175 }
176
177 sub _do_visit {
178     my $self = shift;
179     my $opname = shift;
180     my ( $c, $command ) = @_;
181     my ( $action, $args ) = $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 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) {
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->{namespace} = $action->{'namespace'};
210     local $c->{action} = $action;
211
212     $self->dispatch($c);
213 }
214
215 =head2 $self->go( $c, $command [, \@arguments ] )
216
217 Documented in L<Catalyst>
218
219 =cut
220
221 sub go {
222     my $self = shift;
223     $self->_do_visit('go', @_);
224     die $Catalyst::GO;
225 }
226
227 =head2 $self->forward( $c, $command [, \@arguments ] )
228
229 Documented in L<Catalyst>
230
231 =cut
232
233 sub forward {
234     my $self = shift;
235     $self->_do_forward(forward => @_);
236 }
237
238 sub _do_forward {
239     my $self = shift;
240     my $opname = shift;
241     my ( $c, $command ) = @_;
242     my ( $action, $args ) = $self->_command2action(@_);
243
244     if (!$action) {
245         my $error .= qq/Couldn't $opname to command "$command": /
246                     .qq/Invalid action or component./;
247         $c->error($error);
248         $c->log->debug($error) if $c->debug;
249         return 0;
250     }
251
252     no warnings 'recursion';
253
254     my $orig_args = $c->request->arguments();
255     $c->request->arguments($args);
256     $action->dispatch( $c );
257     $c->request->arguments($orig_args);
258
259     return $c->state;
260 }
261
262 =head2 $self->detach( $c, $command [, \@arguments ] )
263
264 Documented in L<Catalyst>
265
266 =cut
267
268 sub detach {
269     my ( $self, $c, $command, @args ) = @_;
270     $self->_do_forward(detach => $c, $command, @args ) if $command;
271     die $Catalyst::DETACH;
272 }
273
274 sub _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
286 sub _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
308 sub _find_component_class {
309     my ( $self, $c, $component ) = @_;
310
311     return ref($component)
312       || ref( $c->component($component) )
313       || $c->component($component);
314 }
315
316 sub _invoke_as_component {
317     my ( $self, $c, $component, $method ) = @_;
318
319     my $class = $self->_find_component_class( $c, $component ) || return 0;
320
321     if ( my $code = $class->can($method) ) {
322         return $self->method_action_class->new(
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     }
342 }
343
344 =head2 $self->prepare_action($c)
345
346 Find an dispatch type that matches $c->req->path, and set args from it.
347
348 =cut
349
350 sub prepare_action {
351     my ( $self, $c ) = @_;
352     my $req = $c->req;
353     my $path = $req->path;
354     my @path = split /\//, $req->path;
355     $req->args( \my @args );
356
357     unshift( @path, '' );    # Root action
358
359   DESCEND: while (@path) {
360         $path = join '/', @path;
361         $path =~ s#^/##;
362
363         $path = '' if $path eq '/';    # Root action
364
365         # Check out dispatch types to see if any will handle the path at
366         # this level
367
368         foreach my $type ( @{ $self->dispatch_types } ) {
369             last DESCEND if $type->match( $c, $path );
370         }
371
372         # If not, move the last part path to args
373         my $arg = pop(@path);
374         $arg =~ s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg;
375         unshift @args, $arg;
376     }
377
378     s/%([0-9A-Fa-f]{2})/chr(hex($1))/eg for grep { defined } @{$req->captures||[]};
379
380     $c->log->debug( 'Path is "' . $req->match . '"' )
381       if ( $c->debug && defined $req->match && length $req->match );
382
383     $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
384       if ( $c->debug && @args );
385 }
386
387 =head2 $self->get_action( $action, $namespace )
388
389 returns a named action from a given namespace.
390
391 =cut
392
393 sub get_action {
394     my ( $self, $name, $namespace ) = @_;
395     return unless $name;
396
397     $namespace = join( "/", grep { length } split '/', ( defined $namespace ? $namespace : "" ) );
398
399     return $self->action_hash->{"${namespace}/${name}"};
400 }
401
402 =head2 $self->get_action_by_path( $path ); 
403
404 Returns the named action by its full path. 
405
406 =cut
407
408 sub get_action_by_path {
409     my ( $self, $path ) = @_;
410     $path =~ s/^\///;
411     $path = "/$path" unless $path =~ /\//;
412     $self->action_hash->{$path};
413 }
414
415 =head2 $self->get_actions( $c, $action, $namespace )
416
417 =cut
418
419 sub get_actions {
420     my ( $self, $c, $action, $namespace ) = @_;
421     return [] unless $action;
422
423     $namespace = join( "/", grep { length } split '/', $namespace || "" );
424
425     my @match = $self->get_containers($namespace);
426
427     return map { $_->get_action($action) } @match;
428 }
429
430 =head2 $self->get_containers( $namespace )
431
432 Return all the action containers for a given namespace, inclusive
433
434 =cut
435
436 sub get_containers {
437     my ( $self, $namespace ) = @_;
438     $namespace ||= '';
439     $namespace = '' if $namespace eq '/';
440
441     my @containers;
442
443     if ( length $namespace ) {
444         do {
445             push @containers, $self->container_hash->{$namespace};
446         } while ( $namespace =~ s#/[^/]+$## );
447     }
448
449     return reverse grep { defined } @containers, $self->container_hash->{''};
450
451     #return (split '/', $namespace); # isnt this more clear?
452     my @parts = split '/', $namespace;
453 }
454
455 =head2 $self->uri_for_action($action, \@captures)
456
457 Takes a Catalyst::Action object and action parameters and returns a URI
458 part such that if $c->req->path were this URI part, this action would be
459 dispatched to with $c->req->captures set to the supplied arrayref.
460
461 If the action object is not available for external dispatch or the dispatcher
462 cannot determine an appropriate URI, this method will return undef.
463
464 =cut
465
466 sub uri_for_action {
467     my ( $self, $action, $captures) = @_;
468     $captures ||= [];
469     foreach my $dispatch_type ( @{ $self->dispatch_types } ) {
470         my $uri = $dispatch_type->uri_for_action( $action, $captures );
471         return( $uri eq '' ? '/' : $uri )
472             if defined($uri);
473     }
474     return undef;
475 }
476
477 =head2 expand_action 
478
479 expand an action into a full representation of the dispatch.
480 mostly useful for chained, other actions will just return a
481 single action.
482
483 =cut
484
485 sub expand_action {
486     my ($self, $action) = @_;
487
488     foreach my $dispatch_type (@{ $self->dispatch_types }) {
489         my $expanded = $dispatch_type->expand_action($action);
490         return $expanded if $expanded;
491     }
492
493     return $action;
494 }
495
496 =head2 $self->register( $c, $action )
497
498 Make sure all required dispatch types for this action are loaded, then
499 pass the action to our dispatch types so they can register it if required.
500 Also, set up the tree with the action containers.
501
502 =cut
503
504 sub register {
505     my ( $self, $c, $action ) = @_;
506
507     my $registered = $self->registered_dispatch_types;
508
509     #my $priv = 0; #seems to be unused
510     foreach my $key ( keys %{ $action->attributes } ) {
511         next if $key eq 'Private';
512         my $class = "Catalyst::DispatchType::$key";
513         unless ( $registered->{$class} ) {
514             #some error checking rethrowing here wouldn't hurt.
515             eval { Class::MOP::load_class($class) };
516             push( @{ $self->dispatch_types }, $class->new ) unless $@;
517             $registered->{$class} = 1;
518         }
519     }
520
521     # Pass the action to our dispatch types so they can register it if reqd.
522     foreach my $type ( @{ $self->dispatch_types } ) {
523         $type->register( $c, $action );
524     }
525
526     my $namespace = $action->namespace;
527     my $name      = $action->name;
528
529     my $container = $self->_find_or_create_action_container($namespace);
530
531     # Set the method value
532     $container->add_action($action);
533
534     $self->action_hash->{"$namespace/$name"} = $action;
535     $self->container_hash->{$namespace} = $container;
536 }
537
538 sub _find_or_create_action_container {
539     my ( $self, $namespace ) = @_;
540
541     my $tree ||= $self->tree;
542
543     return $tree->getNodeValue unless $namespace;
544
545     my @namespace = split '/', $namespace;
546     return $self->_find_or_create_namespace_node( $tree, @namespace )
547       ->getNodeValue;
548 }
549
550 sub _find_or_create_namespace_node {
551     my ( $self, $parent, $part, @namespace ) = @_;
552
553     return $parent unless $part;
554
555     my $child =
556       ( grep { $_->getNodeValue->part eq $part } $parent->getAllChildren )[0];
557
558     unless ($child) {
559         my $container = Catalyst::ActionContainer->new($part);
560         $parent->addChild( $child = Tree::Simple->new($container) );
561     }
562
563     $self->_find_or_create_namespace_node( $child, @namespace );
564 }
565
566 =head2 $self->setup_actions( $class, $context )
567
568
569 =cut
570
571 sub setup_actions {
572     my ( $self, $c ) = @_;
573
574
575     my @classes =
576       $self->_load_dispatch_types( @{ $self->preload_dispatch_types } );
577     @{ $self->registered_dispatch_types }{@classes} = (1) x @classes;
578
579     foreach my $comp ( values %{ $c->components } ) {
580         $comp->register_actions($c) if $comp->can('register_actions');
581     }
582
583     $self->_load_dispatch_types( @{ $self->postload_dispatch_types } );
584
585     return unless $c->debug;
586
587     my $privates = Text::SimpleTable->new(
588         [ 20, 'Private' ],
589         [ 36, 'Class' ],
590         [ 12, 'Method' ]
591     );
592
593     my $has_private = 0;
594     my $walker = sub {
595         my ( $walker, $parent, $prefix ) = @_;
596         $prefix .= $parent->getNodeValue || '';
597         $prefix .= '/' unless $prefix =~ /\/$/;
598         my $node = $parent->getNodeValue->actions;
599
600         for my $action ( keys %{$node} ) {
601             my $action_obj = $node->{$action};
602             next
603               if ( ( $action =~ /^_.*/ )
604                 && ( !$c->config->{show_internal_actions} ) );
605             $privates->row( "$prefix$action", $action_obj->class, $action );
606             $has_private = 1;
607         }
608
609         $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
610     };
611
612     $walker->( $walker, $self->tree, '' );
613     $c->log->debug( "Loaded Private actions:\n" . $privates->draw . "\n" )
614       if $has_private;
615
616     # List all public actions
617     $_->list($c) for @{ $self->dispatch_types };
618 }
619
620 sub _load_dispatch_types {
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}";
629
630         eval { Class::MOP::load_class($class) };
631         Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
632           if $@;
633         push @{ $self->dispatch_types }, $class->new;
634
635         push @loaded, $class;
636     }
637
638     return @loaded;
639 }
640
641 no Moose;
642 __PACKAGE__->meta->make_immutable;
643
644 =head2 meta
645
646 Provided by Moose
647
648 =head1 AUTHORS
649
650 Catalyst Contributors, see Catalyst.pm
651
652 =head1 COPYRIGHT
653
654 This program is free software, you can redistribute it and/or modify it under
655 the same terms as Perl itself.
656
657 =cut
658
659 1;