File Coverage

blib/lib/Apache2/Controller/Dispatch/Simple.pm
Criterion Covered Total %
statement 15 15 100.0
branch n/a
condition n/a
subroutine 5 5 100.0
pod n/a
total 20 20 100.0


line stmt bran cond sub pod time code
1             package Apache2::Controller::Dispatch::Simple;
2              
3             =head1 NAME
4              
5             Apache2::Controller::Dispatch::Simple - simple dispatch mechanism for A2C
6              
7             =head1 VERSION
8              
9             Version 1.001.001
10              
11             =cut
12              
13 1     1   1927 use version;
  1         3  
  1         6  
14             our $VERSION = version->new('1.001.001');
15              
16             =head1 SYNOPSIS
17              
18            
19             SetHandler modperl
20             PerlInitHandler MyApp::Dispatch
21            
22              
23             # lib/MyApp::Dispatch:
24              
25             package MyApp::Dispatch;
26             use base qw(
27             Apache2::Controller::Dispatch::Simple
28             );
29              
30             # return hash reference from dispatch_map()
31             sub dispatch_map { {
32             foo => 'MyApp::C::Foo',
33             'foo/bar' => 'MyApp::C::Foo::Bar',
34             } }
35              
36             =head1 DESCRIPTION
37              
38             Implements find_controller() for Apache2::Controller::Dispatch with
39             a simple URI-to-controller module mapping. Your URI's are the keys
40             of the C<< dispatch_map() >> hash in your base package, and the values are
41             the Apache2::Controller modules to which those URI's should be dispatched.
42              
43             This dispatches URI's in a case-insensitive fashion. It searches from
44             longest known path to shortest. For a site with many controllers and
45             paths, a trie could possibly be more efficient. Consider that implementation
46             for another Dispatch plugin module.
47              
48             =head1 METHODS
49              
50             =cut
51              
52 1     1   90 use strict;
  1         2  
  1         33  
53 1     1   5 use warnings FATAL => 'all';
  1         2  
  1         37  
54 1     1   5 use English '-no_match_vars';
  1         2  
  1         7  
55              
56 1     1   499 use base qw( Apache2::Controller::Dispatch );
  1         2  
  1         143  
57              
58             use Apache2::Controller::X;
59             use Apache2::Controller::Funk qw( controller_allows_method check_allowed_method );
60              
61             use Log::Log4perl qw(:easy);
62             use YAML::Syck;
63              
64             my %search_uris = ( );
65             my %uri_lengths = ( );
66              
67             # return, for the class, the dispatch_map hash, uri_length map, & search uri list
68             sub _get_class_info {
69             my ($self) = @_;
70             my $class = $self->{class};
71             my $dispatch_map = $self->get_dispatch_map();
72             my ($uri_length_map, $search_uri_list) = ();
73             if (exists $uri_lengths{$class}) {
74             $uri_length_map = $uri_lengths{$class};
75             $search_uri_list = $search_uris{$class};
76             }
77             else {
78             # search dispatch uri keys from longest to shortest
79             my @uris = keys %{$dispatch_map};
80              
81             a2cx "Upper case characters not allowed in $class dispatch_map "
82             ."when using ".__PACKAGE__." to dispatch URIs."
83             if grep m/ \p{IsUpper} /mxs, @uris;
84              
85             $uri_length_map = $uri_lengths{$class} = { };
86             $uri_length_map->{$_} = length $_ for @uris;
87              
88             $search_uri_list = $search_uris{$class} = [
89             sort { $uri_length_map->{$b} <=> $uri_length_map->{$a} } @uris
90             ];
91              
92             DEBUG(sub{"search_uris:".Dump(\%search_uris)});
93             DEBUG(sub{"uri_lengths:".Dump(\%uri_lengths)});
94             }
95             return ($dispatch_map, $uri_length_map, $search_uri_list);
96             }
97              
98             =head2 find_controller
99              
100             Find the controller and method for a given URI from the data
101             set in the dispatch class module.
102              
103             =cut
104              
105             sub find_controller {
106             my ($self) = @_;
107              
108             my $class = $self->{class};
109              
110             my ($dispatch_map, $uri_length_map, $search_uri_list)
111             = $self->_get_class_info();
112              
113             # figure out what most-specific path matches this URI.
114             my $r = $self->{r};
115              
116             my $location = $r->location();
117              
118             my $uri = $r->uri();
119             DEBUG(sub{Dump({
120             uri => $uri,
121             location => $location,
122             })});
123              
124             $uri = substr $uri, length $location;
125              
126             DEBUG("uri becomes '$uri'");
127              
128             if ($uri) {
129             # trim duplicate /'s
130             $uri =~ s{ /{2,} }{/}mxsg;
131              
132             # chop leading /
133             $uri = substr($uri, 1) if substr($uri, 0, 1) eq '/';
134             }
135             else {
136             # 'default' is the default URI for top-level requests
137             $uri = 'default';
138             }
139             my $uri_len = length $uri;
140             my $uri_lc = lc $uri;
141              
142             my ($controller, $method, $relative_uri) = ();
143             my @path_args = ();
144              
145             SEARCH_URI:
146             for my $search_uri (
147             grep $uri_length_map->{$_} <= $uri_len, @{$search_uri_list}
148             ) {
149             my $len = $uri_length_map->{$search_uri};
150             my $fragment = substr $uri_lc, 0, $len;
151             DEBUG("search_uri '$search_uri', len $len, fragment '$fragment'");
152             if ($fragment eq $search_uri) {
153              
154             DEBUG("fragment match found: '$fragment'");
155              
156             # if next character in URI is not / or end of string, this is not it,
157             # only a partial (/foo/barrybonds/stats should not match /foo/bar)
158             my $next_char = substr $uri, $len, 1;
159             if ($next_char && $next_char ne '/') {
160             DEBUG("only partial match. next SEARCH_URI...");
161             next SEARCH_URI;
162             }
163              
164             $controller = $dispatch_map->{$search_uri}
165             || a2cx
166             "No controller assigned in $class dispatch map for $search_uri.";
167            
168             # extract the method and the rest of the path args from the uri
169             if ($next_char) {
170             my $rest_of_uri = substr $uri, $len + 1;
171             my $first_arg;
172             ($first_arg, @path_args) = split '/', $rest_of_uri;
173              
174             DEBUG sub { Dump({
175             rest_of_uri => $rest_of_uri,
176             first_arg => defined $first_arg
177             ? "'$first_arg'"
178             : '[undef]'
179             ,
180             path_args => \@path_args,
181             }) };
182              
183             # if the first field in the rest of the uri is a valid method,
184             # assume that is the thing to use.
185             if ( defined $first_arg
186             && controller_allows_method($controller, $first_arg)
187             ) {
188             $method = $first_arg;
189             }
190             # else use the 'default' method
191             else {
192             $method = 'default';
193             unshift @path_args, $first_arg if defined $first_arg;
194             }
195             $relative_uri = $search_uri;
196             }
197             last SEARCH_URI;
198             }
199             }
200              
201             DEBUG($controller ? "Found controller '$controller'" : "no controller found");
202             DEBUG($method ? "Found method '$method'" : "no method found");
203              
204             if (!$controller) {
205             DEBUG("No controller found. Using default module from dispatch map.");
206              
207             $controller = $dispatch_map->{default}
208             || a2cx "No 'default' controller assigned in $class dispatch map.";
209              
210             my $first_arg;
211             ($first_arg, @path_args) = split '/', $uri;
212             if (controller_allows_method($controller => $first_arg)) {
213             $method = $first_arg;
214             }
215             else {
216             $method = 'default';
217             unshift @path_args, $first_arg;
218             }
219             }
220              
221             a2cx "No controller module found." if !$controller;
222              
223             $method ||= 'default';
224              
225             # relative_uri can be blank. i must have introduced a regression before
226             # when trying to set it to $uri if it was blank. that resulted in
227             # 'default/default.html' in Apache2::Controller:Render::Template tests.
228              
229             check_allowed_method($controller, $method);
230              
231             DEBUG(sub {Dump({
232             apache_location => $r->location(),
233             apache_uri => $r->uri(),
234             my_uri => $uri,
235             controller => $controller,
236             method => $method,
237             path_args => \@path_args,
238             relative_uri => $relative_uri,
239             })});
240              
241             my $pnotes_a2c = $r->pnotes->{a2c} ||= { };
242              
243             $pnotes_a2c->{method} = $method;
244             $pnotes_a2c->{relative_uri} = $relative_uri;
245             $pnotes_a2c->{controller} = $controller;
246             $pnotes_a2c->{path_args} = \@path_args;
247              
248             return $controller;
249             }
250              
251             =head1 SEE ALSO
252              
253             L
254              
255             L
256              
257             L
258              
259             =head1 AUTHOR
260              
261             Mark Hedges, C
262              
263             =head1 COPYRIGHT AND LICENSE
264              
265             Copyright 2008-2010 Mark Hedges. CPAN: markle
266              
267             This library is free software; you can redistribute it and/or modify
268             it under the same terms as Perl itself.
269              
270             This software is provided as-is, with no warranty
271             and no guarantee of fitness
272             for any particular purpose.
273              
274             =cut
275              
276              
277             1;