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