Fixed auto dispatcher
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
CommitLineData
fc7ec1d9 1package Catalyst::Engine;
2
3use strict;
424b2705 4use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
d70195d8 5use attributes ();
fc7ec1d9 6use UNIVERSAL::require;
6dc87a0f 7use CGI::Cookie;
fc7ec1d9 8use Data::Dumper;
9use HTML::Entities;
10use HTTP::Headers;
11use Time::HiRes qw/gettimeofday tv_interval/;
0f7ecc53 12use Text::ASCIITable;
fc7ec1d9 13use Catalyst::Request;
146554c5 14use Catalyst::Request::Upload;
fc7ec1d9 15use Catalyst::Response;
16
17require Module::Pluggable::Fast;
18
99fe1710 19# For pretty dumps
fc7ec1d9 20$Data::Dumper::Terse = 1;
21
1abd6db7 22__PACKAGE__->mk_classdata('components');
b768faa3 23__PACKAGE__->mk_accessors(qw/request response state/);
fc7ec1d9 24
fc7ec1d9 25*comp = \&component;
26*req = \&request;
27*res = \&response;
28
06e1b616 29# For backwards compatibility
30*finalize_output = \&finalize_body;
31
99fe1710 32# For statistics
fc7ec1d9 33our $COUNT = 1;
34our $START = time;
35
36=head1 NAME
37
38Catalyst::Engine - The Catalyst Engine
39
40=head1 SYNOPSIS
41
42See L<Catalyst>.
43
44=head1 DESCRIPTION
45
23f9d934 46=head1 METHODS
fc7ec1d9 47
23f9d934 48=over 4
49
23f9d934 50=item $c->benchmark($coderef)
fc7ec1d9 51
52Takes a coderef with arguments and returns elapsed time as float.
53
54 my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
55 $c->log->info( sprintf "Processing took %f seconds", $elapsed );
56
57=cut
58
59sub benchmark {
60 my $c = shift;
61 my $code = shift;
62 my $time = [gettimeofday];
63 my @return = &$code(@_);
64 my $elapsed = tv_interval $time;
65 return wantarray ? ( $elapsed, @return ) : $elapsed;
66}
67
23f9d934 68=item $c->comp($name)
69
70=item $c->component($name)
fc7ec1d9 71
72Get a component object by name.
73
74 $c->comp('MyApp::Model::MyModel')->do_stuff;
75
76Regex search for a component.
77
78 $c->comp('mymodel')->do_stuff;
79
80=cut
81
82sub component {
3245f607 83 my $c = shift;
99fe1710 84
3245f607 85 if ( @_ ) {
99fe1710 86
3245f607 87 my $name = shift;
88
89 if ( my $component = $c->components->{$name} ) {
90 return $component;
91 }
92
93 else {
94 for my $component ( keys %{ $c->components } ) {
95 return $c->components->{$component} if $component =~ /$name/i;
96 }
fc7ec1d9 97 }
98 }
99fe1710 99
3245f607 100 return sort keys %{ $c->components };
fc7ec1d9 101}
102
a554cc3b 103=item $c->error
23f9d934 104
a554cc3b 105=item $c->error($error, ...)
23f9d934 106
a554cc3b 107=item $c->error($arrayref)
fc7ec1d9 108
a554cc3b 109Returns an arrayref containing error messages.
fc7ec1d9 110
a554cc3b 111 my @error = @{ $c->error };
fc7ec1d9 112
113Add a new error.
114
a554cc3b 115 $c->error('Something bad happened');
fc7ec1d9 116
117=cut
118
a554cc3b 119sub error {
fc7ec1d9 120 my $c = shift;
a554cc3b 121 my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
122 push @{ $c->{error} }, @$error;
123 return $c->{error};
fc7ec1d9 124}
125
6dc87a0f 126=item $c->execute($class, $coderef)
127
128Execute a coderef in given class and catch exceptions.
129Errors are available via $c->error.
130
131=cut
132
133sub execute {
134 my ( $c, $class, $code ) = @_;
91571b7b 135 $class = $c->components->{$class} || $class;
6dc87a0f 136 $c->state(0);
39de91b0 137 my $callsub = ( caller(1) )[3];
99fe1710 138
6dc87a0f 139 eval {
140 if ( $c->debug )
141 {
142 my $action = $c->actions->{reverse}->{"$code"};
143 $action = "/$action" unless $action =~ /\-\>/;
fb13403c 144 $action = "-> $action" if $callsub =~ /forward$/;
6dc87a0f 145 my ( $elapsed, @state ) =
146 $c->benchmark( $code, $class, $c, @{ $c->req->args } );
0f7ecc53 147 push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
6dc87a0f 148 $c->state(@state);
149 }
150 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) ) }
151 };
99fe1710 152
6dc87a0f 153 if ( my $error = $@ ) {
b9ffe28b 154
155 unless ( ref $error ) {
156 chomp $error;
157 $error = qq/Caught exception "$error"/;
158 }
159
6dc87a0f 160 $c->log->error($error);
b9ffe28b 161 $c->error($error);
6dc87a0f 162 $c->state(0);
163 }
164 return $c->state;
165}
166
23f9d934 167=item $c->finalize
fc7ec1d9 168
ca39d576 169Finalize request.
fc7ec1d9 170
171=cut
172
173sub finalize {
174 my $c = shift;
23f9d934 175
6dc87a0f 176 $c->finalize_cookies;
177
49490aab 178 if ( my $location = $c->response->redirect ) {
23f9d934 179 $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
6dc87a0f 180 $c->response->header( Location => $location );
e7c0c583 181 $c->response->status(302) if $c->response->status !~ /^3\d\d$/;
6dc87a0f 182 }
183
969647fd 184 if ( $#{ $c->error } >= 0 ) {
185 $c->finalize_error;
23f9d934 186 }
187
d7945f32 188 if ( !$c->response->body && $c->response->status !~ /^(1|3)\d\d$/ ) {
969647fd 189 $c->finalize_error;
190 }
fc7ec1d9 191
d7945f32 192 if ( $c->response->body && !$c->response->content_length ) {
193 use bytes; # play safe with a utf8 aware perl
194 $c->response->content_length( length $c->response->body );
fc7ec1d9 195 }
969647fd 196
fc7ec1d9 197 my $status = $c->finalize_headers;
06e1b616 198 $c->finalize_body;
fc7ec1d9 199 return $status;
200}
201
06e1b616 202=item $c->finalize_body
203
204Finalize body.
205
206=cut
207
208sub finalize_body { }
209
6dc87a0f 210=item $c->finalize_cookies
211
212Finalize cookies.
213
214=cut
215
216sub finalize_cookies {
217 my $c = shift;
218
219 while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
220 my $cookie = CGI::Cookie->new(
221 -name => $name,
222 -value => $cookie->{value},
223 -expires => $cookie->{expires},
224 -domain => $cookie->{domain},
225 -path => $cookie->{path},
226 -secure => $cookie->{secure} || 0
227 );
228
229 $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
230 }
231}
232
969647fd 233=item $c->finalize_error
234
ca39d576 235Finalize error.
969647fd 236
237=cut
238
239sub finalize_error {
240 my $c = shift;
241
242 $c->res->headers->content_type('text/html');
243 my $name = $c->config->{name} || 'Catalyst Application';
244
245 my ( $title, $error, $infos );
246 if ( $c->debug ) {
247 $error = join '<br/>', @{ $c->error };
248 $error ||= 'No output';
249 $title = $name = "$name on Catalyst $Catalyst::VERSION";
250 my $req = encode_entities Dumper $c->req;
251 my $res = encode_entities Dumper $c->res;
252 my $stash = encode_entities Dumper $c->stash;
253 $infos = <<"";
254<br/>
255<b><u>Request</u></b><br/>
256<pre>$req</pre>
257<b><u>Response</u></b><br/>
258<pre>$res</pre>
259<b><u>Stash</u></b><br/>
260<pre>$stash</pre>
261
262 }
263 else {
264 $title = $name;
265 $error = '';
266 $infos = <<"";
267<pre>
268(en) Please come back later
269(de) Bitte versuchen sie es spaeter nocheinmal
270(nl) Gelieve te komen later terug
271(no) Vennligst prov igjen senere
272(fr) Veuillez revenir plus tard
273(es) Vuelto por favor mas adelante
274(pt) Voltado por favor mais tarde
275(it) Ritornato prego più successivamente
276</pre>
277
278 $name = '';
279 }
e060fe05 280 $c->res->body( <<"" );
969647fd 281<html>
282<head>
283 <title>$title</title>
284 <style type="text/css">
285 body {
286 font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
287 Tahoma, Arial, helvetica, sans-serif;
288 color: #ddd;
289 background-color: #eee;
290 margin: 0px;
291 padding: 0px;
292 }
293 div.box {
294 background-color: #ccc;
295 border: 1px solid #aaa;
296 padding: 4px;
297 margin: 10px;
298 -moz-border-radius: 10px;
299 }
300 div.error {
301 background-color: #977;
302 border: 1px solid #755;
303 padding: 8px;
304 margin: 4px;
305 margin-bottom: 10px;
306 -moz-border-radius: 10px;
307 }
308 div.infos {
309 background-color: #797;
310 border: 1px solid #575;
311 padding: 8px;
312 margin: 4px;
313 margin-bottom: 10px;
314 -moz-border-radius: 10px;
315 }
316 div.name {
317 background-color: #779;
318 border: 1px solid #557;
319 padding: 8px;
320 margin: 4px;
321 -moz-border-radius: 10px;
322 }
323 </style>
324</head>
325<body>
326 <div class="box">
327 <div class="error">$error</div>
328 <div class="infos">$infos</div>
329 <div class="name">$name</div>
330 </div>
331</body>
332</html>
333
334}
335
23f9d934 336=item $c->finalize_headers
fc7ec1d9 337
ca39d576 338Finalize headers.
fc7ec1d9 339
340=cut
341
342sub finalize_headers { }
343
4f81d9ba 344=item $c->handler( $class, $engine )
fc7ec1d9 345
ca39d576 346Handles the request.
fc7ec1d9 347
348=cut
349
6dc87a0f 350sub handler {
351 my ( $class, $engine ) = @_;
fc7ec1d9 352
353 # Always expect worst case!
354 my $status = -1;
355 eval {
d41516b2 356 my @stats = ();
99fe1710 357
fc7ec1d9 358 my $handler = sub {
6dc87a0f 359 my $c = $class->prepare($engine);
d41516b2 360 $c->{stats} = \@stats;
63b763c5 361 $c->dispatch;
fc7ec1d9 362 return $c->finalize;
363 };
99fe1710 364
fc7ec1d9 365 if ( $class->debug ) {
366 my $elapsed;
367 ( $elapsed, $status ) = $class->benchmark($handler);
368 $elapsed = sprintf '%f', $elapsed;
6ddb9f01 369 my $av = sprintf '%.3f', ( $elapsed == 0 ? '??' : (1 / $elapsed) );
0f7ecc53 370 my $t = Text::ASCIITable->new;
371 $t->setCols( 'Action', 'Time' );
3f36a3a3 372 $t->setColWidth( 'Action', 64, 1 );
373 $t->setColWidth( 'Time', 9, 1 );
0822f9a4 374
cd677e12 375 for my $stat (@stats) { $t->addRow( $stat->[0], $stat->[1] ) }
0f7ecc53 376 $class->log->info( "Request took $elapsed" . "s ($av/s)",
377 $t->draw );
fc7ec1d9 378 }
379 else { $status = &$handler }
99fe1710 380
fc7ec1d9 381 };
99fe1710 382
fc7ec1d9 383 if ( my $error = $@ ) {
384 chomp $error;
385 $class->log->error(qq/Caught exception in engine "$error"/);
386 }
99fe1710 387
fc7ec1d9 388 $COUNT++;
389 return $status;
390}
391
3f822a28 392=item $c->prepare($engine)
fc7ec1d9 393
a554cc3b 394Turns the engine-specific request( Apache, CGI ... )
395into a Catalyst context .
fc7ec1d9 396
397=cut
398
399sub prepare {
4f81d9ba 400 my ( $class, $engine ) = @_;
99fe1710 401
fc7ec1d9 402 my $c = bless {
403 request => Catalyst::Request->new(
404 {
405 arguments => [],
406 cookies => {},
407 headers => HTTP::Headers->new,
408 parameters => {},
bfde09a2 409 secure => 0,
fc7ec1d9 410 snippets => [],
411 uploads => {}
412 }
413 ),
414 response => Catalyst::Response->new(
bfde09a2 415 {
03222156 416 body => '',
bfde09a2 417 cookies => {},
418 headers => HTTP::Headers->new,
03222156 419 status => 200
bfde09a2 420 }
fc7ec1d9 421 ),
b768faa3 422 stash => {},
423 state => 0
fc7ec1d9 424 }, $class;
99fe1710 425
fc7ec1d9 426 if ( $c->debug ) {
427 my $secs = time - $START || 1;
428 my $av = sprintf '%.3f', $COUNT / $secs;
1a0250cb 429 $c->log->debug('**********************************');
fc7ec1d9 430 $c->log->debug("* Request $COUNT ($av/s) [$$]");
1a0250cb 431 $c->log->debug('**********************************');
fc7ec1d9 432 $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
433 }
99fe1710 434
4f81d9ba 435 $c->prepare_request($engine);
bfde09a2 436 $c->prepare_connection;
ac733264 437 $c->prepare_headers;
1a80619d 438 $c->prepare_cookies;
bfde09a2 439 $c->prepare_path;
06e1b616 440 $c->prepare_action;
99fe1710 441
0556eb49 442 my $method = $c->req->method || '';
443 my $path = $c->req->path || '';
444 my $hostname = $c->req->hostname || '';
445 my $address = $c->req->address || '';
06e1b616 446
0556eb49 447 $c->log->debug(qq/"$method" request for "$path" from $hostname($address)/)
448 if $c->debug;
99fe1710 449
06e1b616 450 if ( $c->request->method eq 'POST' and $c->request->content_length ) {
451
452 if ( $c->req->content_type eq 'application/x-www-form-urlencoded' ) {
453 $c->prepare_parameters;
454 }
455 elsif ( $c->req->content_type eq 'multipart/form-data' ) {
456 $c->prepare_parameters;
457 $c->prepare_uploads;
458 }
459 else {
460 $c->prepare_body;
461 }
462 }
463
464 if ( $c->request->method eq 'GET' ) {
465 $c->prepare_parameters;
466 }
c85ff642 467
468 if ( $c->debug && keys %{ $c->req->params } ) {
0f7ecc53 469 my $t = Text::ASCIITable->new;
470 $t->setCols( 'Key', 'Value' );
0822f9a4 471 $t->setColWidth( 'Key', 37, 1 );
472 $t->setColWidth( 'Value', 36, 1 );
f78172f1 473 for my $key ( sort keys %{ $c->req->params } ) {
6d1ab915 474 my $param = $c->req->params->{$key};
475 my $value = defined($param) ? $param : '';
cd677e12 476 $t->addRow( $key, $value );
c85ff642 477 }
0f7ecc53 478 $c->log->debug( 'Parameters are', $t->draw );
c85ff642 479 }
99fe1710 480
fc7ec1d9 481 return $c;
482}
483
23f9d934 484=item $c->prepare_action
fc7ec1d9 485
ca39d576 486Prepare action.
fc7ec1d9 487
488=cut
489
490sub prepare_action {
491 my $c = shift;
492 my $path = $c->req->path;
493 my @path = split /\//, $c->req->path;
494 $c->req->args( \my @args );
99fe1710 495
fc7ec1d9 496 while (@path) {
7833fdfc 497 $path = join '/', @path;
0169d3a8 498 if ( my $result = ${ $c->get_action($path) }[0] ) {
fc7ec1d9 499
500 # It's a regex
501 if ($#$result) {
7e5adedd 502 my $match = $result->[1];
503 my @snippets = @{ $result->[2] };
81f6fc50 504 $c->log->debug(
505 qq/Requested action is "$path" and matched "$match"/)
fc7ec1d9 506 if $c->debug;
507 $c->log->debug(
508 'Snippets are "' . join( ' ', @snippets ) . '"' )
509 if ( $c->debug && @snippets );
510 $c->req->action($match);
511 $c->req->snippets( \@snippets );
512 }
99fe1710 513
fc7ec1d9 514 else {
515 $c->req->action($path);
81f6fc50 516 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
fc7ec1d9 517 }
99fe1710 518
fc7ec1d9 519 $c->req->match($path);
fc7ec1d9 520 last;
521 }
522 unshift @args, pop @path;
523 }
99fe1710 524
fc7ec1d9 525 unless ( $c->req->action ) {
ac733264 526 $c->req->action('default');
87e67021 527 $c->req->match('');
fc7ec1d9 528 }
99fe1710 529
5783a9a5 530 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
531 if ( $c->debug && @args );
fc7ec1d9 532}
533
06e1b616 534=item $c->prepare_body
535
536Prepare message body.
537
538=cut
539
540sub prepare_body { }
541
c9afa5fc 542=item $c->prepare_connection
0556eb49 543
ca39d576 544Prepare connection.
0556eb49 545
546=cut
547
548sub prepare_connection { }
549
c9afa5fc 550=item $c->prepare_cookies
fc7ec1d9 551
ca39d576 552Prepare cookies.
fc7ec1d9 553
554=cut
555
6dc87a0f 556sub prepare_cookies {
557 my $c = shift;
558
559 if ( my $header = $c->request->header('Cookie') ) {
560 $c->req->cookies( { CGI::Cookie->parse($header) } );
561 }
562}
fc7ec1d9 563
23f9d934 564=item $c->prepare_headers
fc7ec1d9 565
ca39d576 566Prepare headers.
fc7ec1d9 567
568=cut
569
570sub prepare_headers { }
571
23f9d934 572=item $c->prepare_parameters
fc7ec1d9 573
ca39d576 574Prepare parameters.
fc7ec1d9 575
576=cut
577
578sub prepare_parameters { }
579
23f9d934 580=item $c->prepare_path
fc7ec1d9 581
ca39d576 582Prepare path and base.
fc7ec1d9 583
584=cut
585
586sub prepare_path { }
587
23f9d934 588=item $c->prepare_request
fc7ec1d9 589
ca39d576 590Prepare the engine request.
fc7ec1d9 591
592=cut
593
594sub prepare_request { }
595
23f9d934 596=item $c->prepare_uploads
fc7ec1d9 597
ca39d576 598Prepare uploads.
fc7ec1d9 599
600=cut
601
602sub prepare_uploads { }
603
3245f607 604=item $c->retrieve_components
605
606Retrieve Components.
607
608=cut
609
610sub retrieve_components {
611 my $self = shift;
612
613 my $class = ref $self || $self;
614 eval <<"";
615 package $class;
616 import Module::Pluggable::Fast
617 name => '_components',
618 search => [
619 '$class\::Controller', '$class\::C',
620 '$class\::Model', '$class\::M',
621 '$class\::View', '$class\::V'
622 ],
623 require => 1;
624
625 if ( my $error = $@ ) {
626 chomp $error;
627 die qq/Couldn't load components "$error"/;
628 }
629
89acad38 630 return $self->_components;
3245f607 631}
632
c9afa5fc 633=item $c->run
634
ca39d576 635Starts the engine.
c9afa5fc 636
637=cut
638
639sub run { }
640
61b1e958 641=item $c->request
fc7ec1d9 642
ca39d576 643=item $c->req
23f9d934 644
ca39d576 645Returns a C<Catalyst::Request> object.
fc7ec1d9 646
ca39d576 647 my $req = $c->req;
61b1e958 648
649=item $c->response
650
ca39d576 651=item $c->res
652
fc7ec1d9 653Returns a C<Catalyst::Response> object.
654
655 my $res = $c->res;
656
23f9d934 657=item $class->setup
fc7ec1d9 658
ca39d576 659Setup.
fc7ec1d9 660
661 MyApp->setup;
662
663=cut
664
665sub setup {
666 my $self = shift;
667 $self->setup_components;
668 if ( $self->debug ) {
669 my $name = $self->config->{name} || 'Application';
670 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
671 }
672}
673
23f9d934 674=item $class->setup_components
fc7ec1d9 675
ca39d576 676Setup components.
fc7ec1d9 677
678=cut
679
680sub setup_components {
681 my $self = shift;
3245f607 682
b18987fe 683 # Components
684 my $class = ref $self || $self;
685 eval <<"";
686 package $class;
687 import Module::Pluggable::Fast
688 name => '_components',
689 search => [
690 '$class\::Controller', '$class\::C',
691 '$class\::Model', '$class\::M',
692 '$class\::View', '$class\::V'
693 ];
3245f607 694
b18987fe 695 if ( my $error = $@ ) {
696 chomp $error;
697 die qq/Couldn't load components "$error"/;
698 }
99fe1710 699
b18987fe 700 $self->components( {} );
701 my @comps;
702 for my $comp ( $self->_components($self) ) {
703 $self->components->{ ref $comp } = $comp;
704 push @comps, $comp;
3245f607 705 }
b18987fe 706
5fbed090 707 my $t = Text::ASCIITable->new( { hide_HeadRow => 1, hide_HeadLine => 1 } );
708 $t->setCols('Class');
709 $t->setColWidth( 'Class', 75, 1 );
3245f607 710 $t->addRow($_) for sort keys %{ $self->components };
5fbed090 711 $self->log->debug( 'Loaded components', $t->draw )
712 if ( @{ $t->{tbl_rows} } && $self->debug );
99fe1710 713
b18987fe 714 $self->setup_actions( [ $self, @comps ] );
fc7ec1d9 715}
716
63b763c5 717=item $c->state
718
719Contains the return value of the last executed action.
720
23f9d934 721=item $c->stash
fc7ec1d9 722
ca39d576 723Returns a hashref containing all your data.
fc7ec1d9 724
725 $c->stash->{foo} ||= 'yada';
726 print $c->stash->{foo};
727
728=cut
729
730sub stash {
731 my $self = shift;
c19e2f4a 732 if ( @_ ) {
733 my $stash = @_ > 1 ? {@_} : $_[0];
fc7ec1d9 734 while ( my ( $key, $val ) = each %$stash ) {
735 $self->{stash}->{$key} = $val;
736 }
737 }
738 return $self->{stash};
739}
740
23f9d934 741=back
742
fc7ec1d9 743=head1 AUTHOR
744
745Sebastian Riedel, C<sri@cpan.org>
746
747=head1 COPYRIGHT
748
749This program is free software, you can redistribute it and/or modify it under
750the same terms as Perl itself.
751
752=cut
753
7541;