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