package Apache2::Controller::Dispatch::Simple; =head1 NAME Apache2::Controller::Dispatch::Simple - simple dispatch mechanism for A2C =head1 VERSION Version 1.000.100 =cut use version; our $VERSION = version->new('1.000.100'); =head1 SYNOPSIS SetHandler modperl PerlInitHandler MyApp::Dispatch # lib/MyApp::Dispatch: package MyApp::Dispatch; use base qw( Apache2::Controller::Dispatch::Simple ); # return hash reference from dispatch_map() sub dispatch_map { { foo => 'MyApp::C::Foo', 'foo/bar' => 'MyApp::C::Foo::Bar', } } =head1 DESCRIPTION Implements find_controller() for Apache2::Controller::Dispatch with a simple URI-to-controller module mapping. Your URI's are the keys of the C<< dispatch_map() >> hash in your base package, and the values are the Apache2::Controller modules to which those URI's should be dispatched. This dispatches URI's in a case-insensitive fashion. It searches from longest known path to shortest. For a site with many controllers and paths, a trie could possibly be more efficient. Consider that implementation for another Dispatch plugin module. =head1 METHODS =cut use strict; use warnings FATAL => 'all'; use English '-no_match_vars'; use base qw( Apache2::Controller::Dispatch ); use Apache2::Controller::X; use Apache2::Controller::Funk qw( controller_allows_method check_allowed_method ); use Log::Log4perl qw(:easy); use YAML::Syck; my %search_uris = ( ); my %uri_lengths = ( ); # return, for the class, the dispatch_map hash, uri_length map, & search uri list sub _get_class_info { my ($self) = @_; my $class = $self->{class}; my $dispatch_map = $self->get_dispatch_map(); my ($uri_length_map, $search_uri_list) = (); if (exists $uri_lengths{$class}) { $uri_length_map = $uri_lengths{$class}; $search_uri_list = $search_uris{$class}; } else { # search dispatch uri keys from longest to shortest my @uris = keys %{$dispatch_map}; a2cx "Upper case characters not allowed in $class dispatch_map " ."when using ".__PACKAGE__." to dispatch URIs." if grep m/ \p{IsUpper} /mxs, @uris; $uri_length_map = $uri_lengths{$class} = { }; $uri_length_map->{$_} = length $_ for @uris; $search_uri_list = $search_uris{$class} = [ sort { $uri_length_map->{$b} <=> $uri_length_map->{$a} } @uris ]; DEBUG(sub{"search_uris:".Dump(\%search_uris)}); DEBUG(sub{"uri_lengths:".Dump(\%uri_lengths)}); } return ($dispatch_map, $uri_length_map, $search_uri_list); } =head2 find_controller Find the controller and method for a given URI from the data set in the dispatch class module. =cut sub find_controller { my ($self) = @_; my $class = $self->{class}; my ($dispatch_map, $uri_length_map, $search_uri_list) = $self->_get_class_info(); # figure out what most-specific path matches this URI. my $r = $self->{r}; my $location = $r->location(); my $uri = $r->uri(); DEBUG(sub{Dump({ uri => $uri, location => $location, })}); $uri = substr $uri, length $location; DEBUG("uri becomes '$uri'"); if ($uri) { # trim duplicate /'s $uri =~ s{ /{2,} }{/}mxsg; # chop leading / $uri = substr($uri, 1) if substr($uri, 0, 1) eq '/'; } else { # 'default' is the default URI for top-level requests $uri = 'default'; } my $uri_len = length $uri; my $uri_lc = lc $uri; my ($controller, $method, $relative_uri) = (); my @path_args = (); SEARCH_URI: for my $search_uri ( grep $uri_length_map->{$_} <= $uri_len, @{$search_uri_list} ) { my $len = $uri_length_map->{$search_uri}; my $fragment = substr $uri_lc, 0, $len; DEBUG("search_uri '$search_uri', len $len, fragment '$fragment'"); if ($fragment eq $search_uri) { DEBUG("fragment match found: '$fragment'"); # if next character in URI is not / or end of string, this is not it, # only a partial (/foo/barrybonds/stats should not match /foo/bar) my $next_char = substr $uri, $len, 1; if ($next_char && $next_char ne '/') { DEBUG("only partial match. next SEARCH_URI..."); next SEARCH_URI; } $controller = $dispatch_map->{$search_uri} || a2cx "No controller assigned in $class dispatch map for $search_uri."; # extract the method and the rest of the path args from the uri if ($next_char) { my $rest_of_uri = substr $uri, $len + 1; my $first_arg; ($first_arg, @path_args) = split '/', $rest_of_uri; DEBUG("rest_of_uri '$rest_of_uri'"); DEBUG("first_arg '$first_arg'"); DEBUG(sub {Dump(\@path_args)}); # if the first field in the rest of the uri is a valid method, # assume that is the thing to use. if ( defined $first_arg && controller_allows_method($controller, $first_arg) ) { $method = $first_arg; } # else use the 'default' method else { $method = 'default'; unshift @path_args, $first_arg if defined $first_arg; } $relative_uri = $search_uri; } last SEARCH_URI; } } DEBUG($controller ? "Found controller '$controller'" : "no controller found"); DEBUG($method ? "Found method '$method'" : "no method found"); if (!$controller) { DEBUG("No controller found. Using default module from dispatch map."); $controller = $dispatch_map->{default} || a2cx "No 'default' controller assigned in $class dispatch map."; my $first_arg; ($first_arg, @path_args) = split '/', $uri; if (controller_allows_method($controller => $first_arg)) { $method = $first_arg; } else { $method = 'default'; unshift @path_args, $first_arg; } } a2cx "No controller module found." if !$controller; $method ||= 'default'; $relative_uri ||= ''; check_allowed_method($controller, $method); DEBUG(sub {Dump({ apache_location => $r->location(), apache_uri => $r->uri(), my_uri => $uri, controller => $controller, method => $method, path_args => \@path_args, relative_uri => $relative_uri, })}); my $pnotes_a2c = $r->pnotes->{a2c} ||= { }; $pnotes_a2c->{method} = $method; $pnotes_a2c->{relative_uri} = $relative_uri; $pnotes_a2c->{controller} = $controller; $pnotes_a2c->{path_args} = \@path_args; return $controller; } =head1 SEE ALSO L L L =head1 AUTHOR Mark Hedges, C =head1 COPYRIGHT AND LICENSE Copyright 2008 Mark Hedges. CPAN: markle This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself. This software is provided as-is, with no warranty and no guarantee of fitness for any particular purpose. =cut 1;