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