Made forward sane again
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Dispatcher.pm
1 package Catalyst::Dispatcher;
2
3 use strict;
4 use base 'Class::Accessor::Fast';
5 use Catalyst::Exception;
6 use Catalyst::Utils;
7 use Catalyst::Action;
8 use Catalyst::ActionContainer;
9 use Catalyst::DispatchType::Default;
10 use Catalyst::DispatchType::Index;
11 use Text::SimpleTable;
12 use Tree::Simple;
13 use Tree::Simple::Visitor::FindByPath;
14
15 # Stringify to class
16 use overload '""' => sub { return ref shift }, fallback => 1;
17
18 __PACKAGE__->mk_accessors(
19     qw/tree dispatch_types registered_dispatch_types
20       method_action_class action_container_class/
21 );
22
23 # Preload these action types
24 our @PRELOAD = qw/Path Regex/;
25
26 # Postload these action types
27 our @POSTLOAD = qw/Index Default/;
28
29 =head1 NAME
30
31 Catalyst::Dispatcher - The Catalyst Dispatcher
32
33 =head1 SYNOPSIS
34
35 See L<Catalyst>.
36
37 =head1 DESCRIPTION
38
39 =head1 METHODS
40
41 =over 4
42
43 =item $self->detach( $c, $command [, \@arguments ] )
44
45 =cut
46
47 sub detach {
48     my ( $self, $c, $command, @args ) = @_;
49     $c->forward( $command, @args ) if $command;
50     die $Catalyst::DETACH;
51 }
52
53 =item $self->dispatch($c)
54
55 =cut
56
57 sub dispatch {
58     my ( $self, $c ) = @_;
59
60     if ( $c->action ) {
61         $c->forward( join( '/', '', $c->namespace, '_DISPATCH' ) );
62     }
63
64     else {
65         my $path  = $c->req->path;
66         my $error = $path
67           ? qq/Unknown resource "$path"/
68           : "No default action defined";
69         $c->log->error($error) if $c->debug;
70         $c->error($error);
71     }
72 }
73
74 =item $self->forward( $c, $command [, \@arguments ] )
75
76 =cut
77
78 sub forward {
79     my $self    = shift;
80     my $c       = shift;
81     my $command = shift;
82
83     unless ($command) {
84         $c->log->debug('Nothing to forward to') if $c->debug;
85         return 0;
86     }
87
88     my $arguments = ( ref( $_[-1] ) eq 'ARRAY' ) ? pop(@_) : $c->req->args;
89
90     my $result;
91
92     unless ( ref $command ) {
93         my $command_copy = $command;
94
95         unless ( $command_copy =~ s/^\/// ) {
96             my $namespace = $c->namespace;
97             $command_copy = "${namespace}/${command}";
98         }
99
100         unless ( $command_copy =~ /\// ) {
101             $result = $c->get_action( $command_copy, '/' );
102         }
103         else {
104             my @extra_args;
105           DESCEND: while ( $command_copy =~ s/^(.*)\/(\w+)$/$1/ ) {
106                 my $tail = $2;
107                 $result = $c->get_action( $tail, $1 );
108                 if ($result) {
109                     $command = $tail;
110                     push( @{$arguments}, @extra_args );
111                     last DESCEND;
112                 }
113                 unshift( @extra_args, $tail );
114             }
115         }
116     }
117     else {
118         $result = $command;
119     }
120
121     unless ($result) {
122
123         my $comp;
124
125         unless ( $comp = $c->component($command) ) {
126             my $error =
127 qq/Couldn't forward to command "$command". Invalid action or component./;
128             $c->error($error);
129             $c->log->debug($error) if $c->debug;
130             return 0;
131         }
132
133         my $class  = ref $comp;
134         my $method = shift || 'process';
135
136         if ( my $code = $class->can($method) ) {
137             my $action = $self->method_action_class->new(
138                 {
139                     name      => $method,
140                     code      => $code,
141                     reverse   => "$class->$method",
142                     class     => $class,
143                     namespace => $class,
144                 }
145             );
146             $result = $action;
147         }
148
149         else {
150             my $error =
151               qq/Couldn't forward to "$class". Does not implement "$method"/;
152             $c->error($error);
153             $c->log->debug($error)
154               if $c->debug;
155             return 0;
156         }
157
158     }
159
160     local $c->request->{arguments} = [ @{$arguments} ];
161
162     $result->execute($c);
163
164     return $c->state;
165 }
166
167 =item $self->prepare_action($c)
168
169 =cut
170
171 sub prepare_action {
172     my ( $self, $c ) = @_;
173     my $path = $c->req->path;
174     my @path = split /\//, $c->req->path;
175     $c->req->args( \my @args );
176
177     push( @path, '/' ) unless @path;    # Root action
178
179   DESCEND: while (@path) {
180         $path = join '/', @path;
181
182         $path = '' if $path eq '/';     # Root action
183
184         # Check out dispatch types to see if any will handle the path at
185         # this level
186
187         foreach my $type ( @{ $self->dispatch_types } ) {
188             last DESCEND if $type->match( $c, $path );
189         }
190
191         # If not, move the last part path to args
192
193         unshift @args, pop @path;
194     }
195
196     $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
197       if ( $c->debug && @args );
198 }
199
200 =item $self->get_action( $c, $action, $namespace )
201
202 =cut
203
204 sub get_action {
205     my ( $self, $c, $name, $namespace ) = @_;
206     return unless $name;
207     $namespace ||= '';
208     $namespace = '' if $namespace eq '/';
209
210     my @match = $self->get_containers($namespace);
211
212     return unless @match;
213
214     if ( my $action = $match[-1]->get_action( $c, $name ) ) {
215         return $action if $action->namespace eq $namespace;
216     }
217 }
218
219 =item $self->get_actions( $c, $action, $namespace )
220
221 =cut
222
223 sub get_actions {
224     my ( $self, $c, $action, $namespace ) = @_;
225     return [] unless $action;
226     $namespace ||= '';
227     $namespace = '' if $namespace eq '/';
228
229     my @match = $self->get_containers($namespace);
230
231     return map { $_->get_action( $c, $action ) } @match;
232 }
233
234 =item $self->get_containers( $namespace )
235
236 =cut
237
238 sub get_containers {
239     my ( $self, $namespace ) = @_;
240
241     # If the namespace is / just return the root ActionContainer
242
243     return ( $self->tree->getNodeValue )
244       if ( !$namespace || ( $namespace eq '/' ) );
245
246     # Use a visitor to recurse down the tree finding the ActionContainers
247     # for each namespace in the chain.
248
249     my $visitor = Tree::Simple::Visitor::FindByPath->new;
250     my @path = split( '/', $namespace );
251     $visitor->setSearchPath(@path);
252     $self->tree->accept($visitor);
253
254     my @match = $visitor->getResults;
255     @match = ( $self->tree ) unless @match;
256
257     if ( !defined $visitor->getResult ) {
258
259         # If we don't manage to match, the visitor doesn't return the last
260         # node is matched, so foo/bar/baz would only find the 'foo' node,
261         # not the foo and foo/bar nodes as it should. This does another
262         # single-level search to see if that's the case, and the 'last unless'
263         # should catch any failures - or short-circuit this if this *is* a
264         # bug in the visitor and gets fixed.
265
266         my $extra = $path[ ( scalar @match ) - 1 ];
267         last unless $extra;
268         $visitor->setSearchPath($extra);
269         $match[-1]->accept($visitor);
270         push( @match, $visitor->getResult ) if defined $visitor->getResult;
271     }
272
273     return map { $_->getNodeValue } @match;
274 }
275
276 sub register {
277     my ( $self, $c, $action ) = @_;
278
279     my $namespace = $action->namespace;
280     my $parent    = $self->tree;
281     my $visitor   = Tree::Simple::Visitor::FindByPath->new;
282
283     if ($namespace) {
284         for my $part ( split '/', $namespace ) {
285             $visitor->setSearchPath($part);
286             $parent->accept($visitor);
287             my $child = $visitor->getResult;
288
289             unless ($child) {
290
291                 # Create a new tree node and an ActionContainer to form
292                 # its value.
293
294                 my $container =
295                   Catalyst::ActionContainer->new(
296                     { part => $part, actions => {} } );
297                 $child = $parent->addChild( Tree::Simple->new($container) );
298                 $visitor->setSearchPath($part);
299                 $parent->accept($visitor);
300                 $child = $visitor->getResult;
301             }
302
303             $parent = $child;
304         }
305     }
306
307     # Set the method value
308     $parent->getNodeValue->actions->{ $action->name } = $action;
309
310     my $registered = $self->registered_dispatch_types;
311
312     foreach my $key ( keys %{ $action->attributes } ) {
313         my $class = "Catalyst::DispatchType::$key";
314         unless ( $registered->{$class} ) {
315             eval "require $class";
316             push( @{ $self->dispatch_types }, $class->new ) unless $@;
317             $registered->{$class} = 1;
318         }
319     }
320
321     # Pass the action to our dispatch types so they can register it if reqd.
322     foreach my $type ( @{ $self->dispatch_types } ) {
323         $type->register( $c, $action );
324     }
325 }
326
327 =item $self->setup_actions( $class, $component )
328
329 =cut
330
331 sub setup_actions {
332     my ( $self, $c ) = @_;
333
334     $self->dispatch_types( [] );
335     $self->registered_dispatch_types( {} );
336     $self->method_action_class('Catalyst::Action');
337     $self->action_container_class('Catalyst::ActionContainer');
338
339     # Preload action types
340     for my $type (@PRELOAD) {
341         my $class = "Catalyst::DispatchType::$type";
342         eval "require $class";
343         Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
344           if $@;
345         push @{ $self->dispatch_types }, $class->new;
346         $self->registered_dispatch_types->{$class} = 1;
347     }
348
349     # We use a tree
350     my $container =
351       Catalyst::ActionContainer->new( { part => '/', actions => {} } );
352     $self->tree( Tree::Simple->new( $container, Tree::Simple->ROOT ) );
353
354     $c->register_actions($c);
355
356     foreach my $comp ( values %{ $c->components } ) {
357         $comp->register_actions($c) if $comp->can('register_actions');
358     }
359
360     # Postload action types
361     for my $type (@POSTLOAD) {
362         my $class = "Catalyst::DispatchType::$type";
363         eval "require $class";
364         Catalyst::Exception->throw( message => qq/Couldn't load "$class"/ )
365           if $@;
366         push @{ $self->dispatch_types }, $class->new;
367     }
368
369     return unless $c->debug;
370
371     my $privates = Text::SimpleTable->new( [ 36, 'Private' ], [ 37, 'Class' ] );
372
373     my $has_private = 0;
374     my $walker = sub {
375         my ( $walker, $parent, $prefix ) = @_;
376         $prefix .= $parent->getNodeValue || '';
377         $prefix .= '/' unless $prefix =~ /\/$/;
378         my $node = $parent->getNodeValue->actions;
379
380         for my $action ( keys %{$node} ) {
381             my $action_obj = $node->{$action};
382             next
383               if ( ( $action =~ /^_.*/ )
384                 && ( !$c->config->{show_internal_actions} ) );
385             $privates->row( "$prefix$action", $action_obj->class );
386             $has_private = 1;
387         }
388
389         $walker->( $walker, $_, $prefix ) for $parent->getAllChildren;
390     };
391
392     $walker->( $walker, $self->tree, '' );
393     $c->log->debug( "Loaded Private actions:\n" . $privates->draw )
394       if ($has_private);
395
396     # List all public actions
397     $_->list($c) for @{ $self->dispatch_types };
398 }
399
400 =back
401
402 =head1 AUTHOR
403
404 Sebastian Riedel, C<sri@cpan.org>
405 Matt S Trout, C<mst@shadowcatsystems.co.uk>
406
407 =head1 COPYRIGHT
408
409 This program is free software, you can redistribute it and/or modify it under
410 the same terms as Perl itself.
411
412 =cut
413
414 1;