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