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