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