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