1package Plack::App::URLMap; 2use strict; 3use warnings; 4use parent qw(Plack::Component); 5use constant DEBUG => $ENV{PLACK_URLMAP_DEBUG} ? 1 : 0; 6 7use Carp (); 8 9sub mount { shift->map(@_) } 10 11sub map { 12 my $self = shift; 13 my($location, $app) = @_; 14 15 my $host; 16 if ($location =~ m!^https?://(.*?)(/.*)!) { 17 $host = $1; 18 $location = $2; 19 } 20 21 if ($location !~ m!^/!) { 22 Carp::croak("Paths need to start with /"); 23 } 24 $location =~ s!/$!!; 25 26 push @{$self->{_mapping}}, [ $host, $location, qr/^\Q$location\E/, $app ]; 27} 28 29sub prepare_app { 30 my $self = shift; 31 # sort by path length 32 $self->{_sorted_mapping} = [ 33 map { [ @{$_}[2..5] ] } 34 sort { $b->[0] <=> $a->[0] || $b->[1] <=> $a->[1] } 35 map { [ ($_->[0] ? length $_->[0] : 0), length($_->[1]), @$_ ] } @{$self->{_mapping}}, 36 ]; 37} 38 39sub call { 40 my ($self, $env) = @_; 41 42 my $path_info = $env->{PATH_INFO}; 43 my $script_name = $env->{SCRIPT_NAME}; 44 45 my($http_host, $server_name) = @{$env}{qw( HTTP_HOST SERVER_NAME )}; 46 47 if ($http_host and my $port = $env->{SERVER_PORT}) { 48 $http_host =~ s/:$port$//; 49 } 50 51 for my $map (@{ $self->{_sorted_mapping} }) { 52 my($host, $location, $location_re, $app) = @$map; 53 my $path = $path_info; # copy 54 no warnings 'uninitialized'; 55 DEBUG && warn "Matching request (Host=$http_host Path=$path) and the map (Host=$host Path=$location)\n"; 56 next unless not defined $host or 57 $http_host eq $host or 58 $server_name eq $host; 59 next unless $location eq '' or $path =~ s!$location_re!!; 60 next unless $path eq '' or $path =~ m!^/!; 61 DEBUG && warn "-> Matched!\n"; 62 63 my $orig_path_info = $env->{PATH_INFO}; 64 my $orig_script_name = $env->{SCRIPT_NAME}; 65 66 $env->{PATH_INFO} = $path; 67 $env->{SCRIPT_NAME} = $script_name . $location; 68 return $self->response_cb($app->($env), sub { 69 $env->{PATH_INFO} = $orig_path_info; 70 $env->{SCRIPT_NAME} = $orig_script_name; 71 }); 72 } 73 74 DEBUG && warn "All matching failed.\n"; 75 76 return [404, [ 'Content-Type' => 'text/plain' ], [ "Not Found" ]]; 77} 78 791; 80 81__END__ 82 83=head1 NAME 84 85Plack::App::URLMap - Map multiple apps in different paths 86 87=head1 SYNOPSIS 88 89 use Plack::App::URLMap; 90 91 my $app1 = sub { ... }; 92 my $app2 = sub { ... }; 93 my $app3 = sub { ... }; 94 95 my $urlmap = Plack::App::URLMap->new; 96 $urlmap->map("/" => $app1); 97 $urlmap->map("/foo" => $app2); 98 $urlmap->map("http://bar.example.com/" => $app3); 99 100 my $app = $urlmap->to_app; 101 102=head1 DESCRIPTION 103 104Plack::App::URLMap is a PSGI application that can dispatch multiple 105applications based on URL path and host names (a.k.a "virtual hosting") 106and takes care of rewriting C<SCRIPT_NAME> and C<PATH_INFO> (See 107L</"HOW THIS WORKS"> for details). This module is inspired by 108Ruby's Rack::URLMap. 109 110=head1 METHODS 111 112=over 4 113 114=item map 115 116 $urlmap->map("/foo" => $app); 117 $urlmap->map("http://bar.example.com/" => $another_app); 118 119Maps URL path or an absolute URL to a PSGI application. The match 120order is sorted by host name length and then path length (longest strings 121first). 122 123URL paths need to match from the beginning and should match completely 124until the path separator (or the end of the path). For example, if you 125register the path C</foo>, it I<will> match with the request C</foo>, 126C</foo/> or C</foo/bar> but it I<won't> match with C</foox>. 127 128Mapping URLs with host names is also possible, and in that case the URL 129mapping works like a virtual host. 130 131Mappings will nest. If $app is already mapped to C</baz> it will 132match a request for C</foo/baz> but not C</foo>. See L</"HOW THIS 133WORKS"> for more details. 134 135=item mount 136 137Alias for C<map>. 138 139=item to_app 140 141 my $handler = $urlmap->to_app; 142 143Returns the PSGI application code reference. Note that the 144Plack::App::URLMap object is callable (by overloading the code 145dereference), so returning the object itself as a PSGI application 146should also work. 147 148=back 149 150=head1 PERFORMANCE 151 152If you C<map> (or C<mount> with Plack::Builder) N applications, 153Plack::App::URLMap will need to at most iterate through N paths to 154match incoming requests. 155 156It is a good idea to use C<map> only for a known, limited amount of 157applications, since mounting hundreds of applications could affect 158runtime request performance. 159 160=head1 DEBUGGING 161 162You can set the environment variable C<PLACK_URLMAP_DEBUG> to see how 163this application matches with the incoming request host names and 164paths. 165 166=head1 HOW THIS WORKS 167 168This application works by I<fixing> C<SCRIPT_NAME> and C<PATH_INFO> 169before dispatching the incoming request to the relocated 170applications. 171 172Say you have a Wiki application that takes C</index> and C</page/*> 173and makes a PSGI application C<$wiki_app> out of it, using one of 174supported web frameworks, you can put the whole application under 175C</wiki> by: 176 177 # MyWikiApp looks at PATH_INFO and handles /index and /page/* 178 my $wiki_app = sub { MyWikiApp->run(@_) }; 179 180 use Plack::App::URLMap; 181 my $app = Plack::App::URLMap->new; 182 $app->mount("/wiki" => $wiki_app); 183 184When a request comes in with C<PATH_INFO> set to C</wiki/page/foo>, 185the URLMap application C<$app> strips the C</wiki> part from 186C<PATH_INFO> and B<appends> that to C<SCRIPT_NAME>. 187 188That way, if the C<$app> is mounted under the root 189(i.e. C<SCRIPT_NAME> is C<"">) with standalone web servers like 190L<Starman>, C<SCRIPT_NAME> is now locally set to C</wiki> and 191C<PATH_INFO> is changed to C</page/foo> when C<$wiki_app> gets called. 192 193=head1 AUTHOR 194 195Tatsuhiko Miyagawa 196 197=head1 SEE ALSO 198 199L<Plack::Builder> 200 201=cut 202