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