released 5.21
[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 }
170 else { $c->state( &$code( $class, $c, @{ $c->req->args } ) ) }
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 || '';
470 my $hostname = $c->req->hostname || '';
471 my $address = $c->req->address || '';
06e1b616 472
0556eb49 473 $c->log->debug(qq/"$method" request for "$path" from $hostname($address)/)
474 if $c->debug;
99fe1710 475
06e1b616 476 if ( $c->request->method eq 'POST' and $c->request->content_length ) {
477
478 if ( $c->req->content_type eq 'application/x-www-form-urlencoded' ) {
479 $c->prepare_parameters;
480 }
481 elsif ( $c->req->content_type eq 'multipart/form-data' ) {
482 $c->prepare_parameters;
483 $c->prepare_uploads;
484 }
485 else {
486 $c->prepare_body;
487 }
488 }
489
490 if ( $c->request->method eq 'GET' ) {
491 $c->prepare_parameters;
492 }
c85ff642 493
494 if ( $c->debug && keys %{ $c->req->params } ) {
0f7ecc53 495 my $t = Text::ASCIITable->new;
496 $t->setCols( 'Key', 'Value' );
0822f9a4 497 $t->setColWidth( 'Key', 37, 1 );
498 $t->setColWidth( 'Value', 36, 1 );
f78172f1 499 for my $key ( sort keys %{ $c->req->params } ) {
6d1ab915 500 my $param = $c->req->params->{$key};
501 my $value = defined($param) ? $param : '';
cd677e12 502 $t->addRow( $key, $value );
c85ff642 503 }
0f7ecc53 504 $c->log->debug( 'Parameters are', $t->draw );
c85ff642 505 }
99fe1710 506
fc7ec1d9 507 return $c;
508}
509
23f9d934 510=item $c->prepare_action
fc7ec1d9 511
ca39d576 512Prepare action.
fc7ec1d9 513
514=cut
515
516sub prepare_action {
517 my $c = shift;
518 my $path = $c->req->path;
519 my @path = split /\//, $c->req->path;
520 $c->req->args( \my @args );
99fe1710 521
fc7ec1d9 522 while (@path) {
7833fdfc 523 $path = join '/', @path;
0169d3a8 524 if ( my $result = ${ $c->get_action($path) }[0] ) {
fc7ec1d9 525
526 # It's a regex
527 if ($#$result) {
7e5adedd 528 my $match = $result->[1];
529 my @snippets = @{ $result->[2] };
81f6fc50 530 $c->log->debug(
531 qq/Requested action is "$path" and matched "$match"/)
fc7ec1d9 532 if $c->debug;
533 $c->log->debug(
534 'Snippets are "' . join( ' ', @snippets ) . '"' )
535 if ( $c->debug && @snippets );
536 $c->req->action($match);
537 $c->req->snippets( \@snippets );
538 }
99fe1710 539
fc7ec1d9 540 else {
541 $c->req->action($path);
81f6fc50 542 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
fc7ec1d9 543 }
99fe1710 544
fc7ec1d9 545 $c->req->match($path);
fc7ec1d9 546 last;
547 }
548 unshift @args, pop @path;
549 }
99fe1710 550
fc7ec1d9 551 unless ( $c->req->action ) {
ac733264 552 $c->req->action('default');
87e67021 553 $c->req->match('');
fc7ec1d9 554 }
99fe1710 555
5783a9a5 556 $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
557 if ( $c->debug && @args );
fc7ec1d9 558}
559
06e1b616 560=item $c->prepare_body
561
562Prepare message body.
563
564=cut
565
566sub prepare_body { }
567
c9afa5fc 568=item $c->prepare_connection
0556eb49 569
ca39d576 570Prepare connection.
0556eb49 571
572=cut
573
574sub prepare_connection { }
575
c9afa5fc 576=item $c->prepare_cookies
fc7ec1d9 577
ca39d576 578Prepare cookies.
fc7ec1d9 579
580=cut
581
6dc87a0f 582sub prepare_cookies {
583 my $c = shift;
584
585 if ( my $header = $c->request->header('Cookie') ) {
586 $c->req->cookies( { CGI::Cookie->parse($header) } );
587 }
588}
fc7ec1d9 589
23f9d934 590=item $c->prepare_headers
fc7ec1d9 591
ca39d576 592Prepare headers.
fc7ec1d9 593
594=cut
595
596sub prepare_headers { }
597
23f9d934 598=item $c->prepare_parameters
fc7ec1d9 599
ca39d576 600Prepare parameters.
fc7ec1d9 601
602=cut
603
604sub prepare_parameters { }
605
23f9d934 606=item $c->prepare_path
fc7ec1d9 607
ca39d576 608Prepare path and base.
fc7ec1d9 609
610=cut
611
612sub prepare_path { }
613
23f9d934 614=item $c->prepare_request
fc7ec1d9 615
ca39d576 616Prepare the engine request.
fc7ec1d9 617
618=cut
619
620sub prepare_request { }
621
23f9d934 622=item $c->prepare_uploads
fc7ec1d9 623
ca39d576 624Prepare uploads.
fc7ec1d9 625
626=cut
627
628sub prepare_uploads { }
629
c9afa5fc 630=item $c->run
631
ca39d576 632Starts the engine.
c9afa5fc 633
634=cut
635
636sub run { }
637
61b1e958 638=item $c->request
fc7ec1d9 639
ca39d576 640=item $c->req
23f9d934 641
ca39d576 642Returns a C<Catalyst::Request> object.
fc7ec1d9 643
ca39d576 644 my $req = $c->req;
61b1e958 645
646=item $c->response
647
ca39d576 648=item $c->res
649
fc7ec1d9 650Returns a C<Catalyst::Response> object.
651
652 my $res = $c->res;
653
23f9d934 654=item $class->setup
fc7ec1d9 655
ca39d576 656Setup.
fc7ec1d9 657
658 MyApp->setup;
659
660=cut
661
662sub setup {
663 my $self = shift;
664 $self->setup_components;
665 if ( $self->debug ) {
666 my $name = $self->config->{name} || 'Application';
667 $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
668 }
669}
670
23f9d934 671=item $class->setup_components
fc7ec1d9 672
ca39d576 673Setup components.
fc7ec1d9 674
675=cut
676
677sub setup_components {
678 my $self = shift;
66294129 679
b18987fe 680 # Components
681 my $class = ref $self || $self;
682 eval <<"";
683 package $class;
684 import Module::Pluggable::Fast
685 name => '_components',
686 search => [
687 '$class\::Controller', '$class\::C',
688 '$class\::Model', '$class\::M',
689 '$class\::View', '$class\::V'
690 ];
3245f607 691
b18987fe 692 if ( my $error = $@ ) {
693 chomp $error;
694 die qq/Couldn't load components "$error"/;
695 }
99fe1710 696
b18987fe 697 $self->components( {} );
698 my @comps;
699 for my $comp ( $self->_components($self) ) {
700 $self->components->{ ref $comp } = $comp;
701 push @comps, $comp;
3245f607 702 }
b18987fe 703
5fbed090 704 my $t = Text::ASCIITable->new( { hide_HeadRow => 1, hide_HeadLine => 1 } );
705 $t->setCols('Class');
706 $t->setColWidth( 'Class', 75, 1 );
3245f607 707 $t->addRow($_) for sort keys %{ $self->components };
5fbed090 708 $self->log->debug( 'Loaded components', $t->draw )
709 if ( @{ $t->{tbl_rows} } && $self->debug );
99fe1710 710
b18987fe 711 $self->setup_actions( [ $self, @comps ] );
fc7ec1d9 712}
713
63b763c5 714=item $c->state
715
716Contains the return value of the last executed action.
717
23f9d934 718=item $c->stash
fc7ec1d9 719
ca39d576 720Returns a hashref containing all your data.
fc7ec1d9 721
722 $c->stash->{foo} ||= 'yada';
723 print $c->stash->{foo};
724
725=cut
726
727sub stash {
728 my $self = shift;
e88fa058 729 if (@_) {
c19e2f4a 730 my $stash = @_ > 1 ? {@_} : $_[0];
fc7ec1d9 731 while ( my ( $key, $val ) = each %$stash ) {
732 $self->{stash}->{$key} = $val;
733 }
734 }
735 return $self->{stash};
736}
737
23f9d934 738=back
739
fc7ec1d9 740=head1 AUTHOR
741
742Sebastian Riedel, C<sri@cpan.org>
743
744=head1 COPYRIGHT
745
746This program is free software, you can redistribute it and/or modify it under
747the same terms as Perl itself.
748
749=cut
750
7511;