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