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