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