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