Fixed table debug output
[catagits/Catalyst-Runtime.git] / lib / Catalyst / Engine.pm
1 package Catalyst::Engine;
2
3 use strict;
4 use base qw/Class::Data::Inheritable Class::Accessor::Fast/;
5 use attributes ();
6 use UNIVERSAL::require;
7 use CGI::Cookie;
8 use Data::Dumper;
9 use HTML::Entities;
10 use HTTP::Headers;
11 use Time::HiRes qw/gettimeofday tv_interval/;
12 use Text::ASCIITable;
13 use Catalyst::Exception;
14 use Catalyst::Request;
15 use Catalyst::Request::Upload;
16 use Catalyst::Response;
17 use Catalyst::Utils;
18
19 require Module::Pluggable::Fast;
20
21 # For pretty dumps
22 $Data::Dumper::Terse = 1;
23
24 __PACKAGE__->mk_classdata('components');
25 __PACKAGE__->mk_accessors(qw/counter depth request response state/);
26
27 *comp = \&component;
28 *req  = \&request;
29 *res  = \&response;
30
31 # For backwards compatibility
32 *finalize_output = \&finalize_body;
33
34 # For statistics
35 our $COUNT     = 1;
36 our $START     = time;
37 our $RECURSION = 1000;
38 our $DETACH    = "catalyst_detach\n";
39
40 =head1 NAME
41
42 Catalyst::Engine - The Catalyst Engine
43
44 =head1 SYNOPSIS
45
46 See L<Catalyst>.
47
48 =head1 DESCRIPTION
49
50 =head1 METHODS
51
52 =over 4
53
54 =item $c->benchmark($coderef)
55
56 Takes a coderef with arguments and returns elapsed time as float.
57
58     my ( $elapsed, $status ) = $c->benchmark( sub { return 1 } );
59     $c->log->info( sprintf "Processing took %f seconds", $elapsed );
60
61 =cut
62
63 sub benchmark {
64     my $c       = shift;
65     my $code    = shift;
66     my $time    = [gettimeofday];
67     my @return  = &$code(@_);
68     my $elapsed = tv_interval $time;
69     return wantarray ? ( $elapsed, @return ) : $elapsed;
70 }
71
72 =item $c->comp($name)
73
74 =item $c->component($name)
75
76 Get a component object by name.
77
78     $c->comp('MyApp::Model::MyModel')->do_stuff;
79
80 Regex search for a component.
81
82     $c->comp('mymodel')->do_stuff;
83
84 =cut
85
86 sub component {
87     my $c = shift;
88
89     if (@_) {
90
91         my $name = shift;
92
93         if ( my $component = $c->components->{$name} ) {
94             return $component;
95         }
96
97         else {
98             for my $component ( keys %{ $c->components } ) {
99                 return $c->components->{$component} if $component =~ /$name/i;
100             }
101         }
102     }
103
104     return sort keys %{ $c->components };
105 }
106
107 =item $c->counter
108
109 Returns a hashref containing coderefs and execution counts.
110 (Needed for deep recursion detection)
111
112 =item $c->depth
113
114 Returns the actual forward depth.
115
116 =item $c->error
117
118 =item $c->error($error, ...)
119
120 =item $c->error($arrayref)
121
122 Returns an arrayref containing error messages.
123
124     my @error = @{ $c->error };
125
126 Add a new error.
127
128     $c->error('Something bad happened');
129
130 =cut
131
132 sub error {
133     my $c = shift;
134     my $error = ref $_[0] eq 'ARRAY' ? $_[0] : [@_];
135     push @{ $c->{error} }, @$error;
136     return $c->{error};
137 }
138
139 =item $c->execute($class, $coderef)
140
141 Execute a coderef in given class and catch exceptions.
142 Errors are available via $c->error.
143
144 =cut
145
146 sub execute {
147     my ( $c, $class, $code ) = @_;
148     $class = $c->components->{$class} || $class;
149     $c->state(0);
150     my $callsub = ( caller(1) )[3];
151
152     my $action = '';
153     if ( $c->debug ) {
154         $action = $c->actions->{reverse}->{"$code"};
155         $action = "/$action" unless $action =~ /\-\>/;
156         $c->counter->{"$code"}++;
157
158         if ( $c->counter->{"$code"} > $RECURSION ) {
159             my $error = qq/Deep recursion detected in "$action"/;
160             $c->log->error($error);
161             $c->error($error);
162             $c->state(0);
163             return $c->state;
164         }
165
166         $action = "-> $action" if $callsub =~ /forward$/;
167     }
168
169     $c->{depth}++;
170     eval {
171         if ( $c->debug )
172         {
173             my ( $elapsed, @state ) =
174               $c->benchmark( $code, $class, $c, @{ $c->req->args } );
175             push @{ $c->{stats} }, [ $action, sprintf( '%fs', $elapsed ) ];
176             $c->state(@state);
177         }
178         else { $c->state( &$code( $class, $c, @{ $c->req->args } ) || 0 ) }
179     };
180     $c->{depth}--;
181
182     if ( my $error = $@ ) {
183
184         if ( $error eq $DETACH ) { die $DETACH if $c->{depth} > 1 }
185         else {
186             unless ( ref $error ) {
187                 chomp $error;
188                 $error = qq/Caught exception "$error"/;
189             }
190
191             $c->log->error($error);
192             $c->error($error);
193             $c->state(0);
194         }
195     }
196     return $c->state;
197 }
198
199 =item $c->finalize
200
201 Finalize request.
202
203 =cut
204
205 sub finalize {
206     my $c = shift;
207
208     $c->finalize_cookies;
209
210     if ( my $location = $c->response->redirect ) {
211         $c->log->debug(qq/Redirecting to "$location"/) if $c->debug;
212         $c->response->header( Location => $location );
213         $c->response->status(302) if $c->response->status !~ /^3\d\d$/;
214     }
215
216     if ( $#{ $c->error } >= 0 ) {
217         $c->finalize_error;
218     }
219
220     if ( !$c->response->body && $c->response->status == 200 ) {
221         $c->finalize_error;
222     }
223
224     if ( $c->response->body && !$c->response->content_length ) {
225         $c->response->content_length( bytes::length( $c->response->body ) );
226     }
227
228     if ( $c->response->status =~ /^(1\d\d|[23]04)$/ ) {
229         $c->response->headers->remove_header("Content-Length");
230         $c->response->body('');
231     }
232
233     if ( $c->request->method eq 'HEAD' ) {
234         $c->response->body('');
235     }
236
237     my $status = $c->finalize_headers;
238     $c->finalize_body;
239     return $status;
240 }
241
242 =item $c->finalize_output
243
244 <obsolete>, see finalize_body
245
246 =item $c->finalize_body
247
248 Finalize body.
249
250 =cut
251
252 sub finalize_body { }
253
254 =item $c->finalize_cookies
255
256 Finalize cookies.
257
258 =cut
259
260 sub finalize_cookies {
261     my $c = shift;
262
263     while ( my ( $name, $cookie ) = each %{ $c->response->cookies } ) {
264         my $cookie = CGI::Cookie->new(
265             -name    => $name,
266             -value   => $cookie->{value},
267             -expires => $cookie->{expires},
268             -domain  => $cookie->{domain},
269             -path    => $cookie->{path},
270             -secure  => $cookie->{secure} || 0
271         );
272
273         $c->res->headers->push_header( 'Set-Cookie' => $cookie->as_string );
274     }
275 }
276
277 =item $c->finalize_error
278
279 Finalize error.
280
281 =cut
282
283 sub finalize_error {
284     my $c = shift;
285
286     $c->res->headers->content_type('text/html');
287     my $name = $c->config->{name} || 'Catalyst Application';
288
289     my ( $title, $error, $infos );
290     if ( $c->debug ) {
291         $error = join '<br/>', @{ $c->error };
292         $error ||= 'No output';
293         $title = $name = "$name on Catalyst $Catalyst::VERSION";
294         my $req   = encode_entities Dumper $c->req;
295         my $res   = encode_entities Dumper $c->res;
296         my $stash = encode_entities Dumper $c->stash;
297         $infos = <<"";
298 <br/>
299 <b><u>Request</u></b><br/>
300 <pre>$req</pre>
301 <b><u>Response</u></b><br/>
302 <pre>$res</pre>
303 <b><u>Stash</u></b><br/>
304 <pre>$stash</pre>
305
306     }
307     else {
308         $title = $name;
309         $error = '';
310         $infos = <<"";
311 <pre>
312 (en) Please come back later
313 (de) Bitte versuchen sie es spaeter nocheinmal
314 (nl) Gelieve te komen later terug
315 (no) Vennligst prov igjen senere
316 (fr) Veuillez revenir plus tard
317 (es) Vuelto por favor mas adelante
318 (pt) Voltado por favor mais tarde
319 (it) Ritornato prego piĆ¹ successivamente
320 </pre>
321
322         $name = '';
323     }
324     $c->res->body( <<"" );
325 <html>
326 <head>
327     <title>$title</title>
328     <style type="text/css">
329         body {
330             font-family: "Bitstream Vera Sans", "Trebuchet MS", Verdana,
331                          Tahoma, Arial, helvetica, sans-serif;
332             color: #ddd;
333             background-color: #eee;
334             margin: 0px;
335             padding: 0px;
336         }
337         div.box {
338             background-color: #ccc;
339             border: 1px solid #aaa;
340             padding: 4px;
341             margin: 10px;
342             -moz-border-radius: 10px;
343         }
344         div.error {
345             background-color: #977;
346             border: 1px solid #755;
347             padding: 8px;
348             margin: 4px;
349             margin-bottom: 10px;
350             -moz-border-radius: 10px;
351         }
352         div.infos {
353             background-color: #797;
354             border: 1px solid #575;
355             padding: 8px;
356             margin: 4px;
357             margin-bottom: 10px;
358             -moz-border-radius: 10px;
359         }
360         div.name {
361             background-color: #779;
362             border: 1px solid #557;
363             padding: 8px;
364             margin: 4px;
365             -moz-border-radius: 10px;
366         }
367     </style>
368 </head>
369 <body>
370     <div class="box">
371         <div class="error">$error</div>
372         <div class="infos">$infos</div>
373         <div class="name">$name</div>
374     </div>
375 </body>
376 </html>
377
378 }
379
380 =item $c->finalize_headers
381
382 Finalize headers.
383
384 =cut
385
386 sub finalize_headers { }
387
388 =item $c->handler( $class, @arguments )
389
390 Handles the request.
391
392 =cut
393
394 sub handler {
395     my ( $class, @arguments ) = @_;
396
397     # Always expect worst case!
398     my $status = -1;
399     eval {
400         my @stats = ();
401
402         my $handler = sub {
403             my $c = $class->prepare(@arguments);
404             $c->{stats} = \@stats;
405             $c->dispatch;
406             return $c->finalize;
407         };
408
409         if ( $class->debug ) {
410             my $elapsed;
411             ( $elapsed, $status ) = $class->benchmark($handler);
412             $elapsed = sprintf '%f', $elapsed;
413             my $av = sprintf '%.3f',
414               ( $elapsed == 0 ? '??' : ( 1 / $elapsed ) );
415             my $t = Text::ASCIITable->new;
416             $t->setCols( 'Action', 'Time' );
417             $t->setColWidth( 'Action', 64, 1 );
418             $t->setColWidth( 'Time',   9,  1 );
419
420             for my $stat (@stats) { $t->addRow( $stat->[0], $stat->[1] ) }
421             $class->log->info( "Request took ${elapsed}s ($av/s)\n" . $t->draw );
422         }
423         else { $status = &$handler }
424
425     };
426
427     if ( my $error = $@ ) {
428         chomp $error;
429         $class->log->error(qq/Caught exception in engine "$error"/);
430     }
431
432     $COUNT++;
433     return $status;
434 }
435
436 =item $c->prepare(@arguments)
437
438 Turns the engine-specific request( Apache, CGI ... )
439 into a Catalyst context .
440
441 =cut
442
443 sub prepare {
444     my ( $class, @arguments ) = @_;
445
446     my $c = bless {
447         counter => {},
448         depth   => 0,
449         request => Catalyst::Request->new(
450             {
451                 arguments  => [],
452                 cookies    => {},
453                 headers    => HTTP::Headers->new,
454                 parameters => {},
455                 secure     => 0,
456                 snippets   => [],
457                 uploads    => {}
458             }
459         ),
460         response => Catalyst::Response->new(
461             {
462                 body    => '',
463                 cookies => {},
464                 headers => HTTP::Headers->new( 'Content-Length' => 0 ),
465                 status  => 200
466             }
467         ),
468         stash => {},
469         state => 0
470     }, $class;
471
472     if ( $c->debug ) {
473         my $secs = time - $START || 1;
474         my $av = sprintf '%.3f', $COUNT / $secs;
475         $c->log->debug('**********************************');
476         $c->log->debug("* Request $COUNT ($av/s) [$$]");
477         $c->log->debug('**********************************');
478         $c->res->headers->header( 'X-Catalyst' => $Catalyst::VERSION );
479     }
480
481     $c->prepare_request(@arguments);
482     $c->prepare_connection;
483     $c->prepare_headers;
484     $c->prepare_cookies;
485     $c->prepare_path;
486     $c->prepare_action;
487
488     my $method  = $c->req->method  || '';
489     my $path    = $c->req->path    || '';
490     my $address = $c->req->address || '';
491
492     $c->log->debug(qq/"$method" request for "$path" from $address/)
493       if $c->debug;
494
495     if ( $c->request->method eq 'POST' and $c->request->content_length ) {
496
497         if ( $c->req->content_type eq 'application/x-www-form-urlencoded' ) {
498             $c->prepare_parameters;
499         }
500         elsif ( $c->req->content_type eq 'multipart/form-data' ) {
501             $c->prepare_parameters;
502             $c->prepare_uploads;
503         }
504         else {
505             $c->prepare_body;
506         }
507     }
508
509     if ( $c->request->method eq 'GET' ) {
510         $c->prepare_parameters;
511     }
512
513     if ( $c->debug && keys %{ $c->req->params } ) {
514         my $t = Text::ASCIITable->new;
515         $t->setCols( 'Key', 'Value' );
516         $t->setColWidth( 'Key',   37, 1 );
517         $t->setColWidth( 'Value', 36, 1 );
518         for my $key ( sort keys %{ $c->req->params } ) {
519             my $param = $c->req->params->{$key};
520             my $value = defined($param) ? $param : '';
521             $t->addRow( $key, $value );
522         }
523         $c->log->debug( "Parameters are:\n" . $t->draw );
524     }
525
526     return $c;
527 }
528
529 =item $c->prepare_action
530
531 Prepare action.
532
533 =cut
534
535 sub prepare_action {
536     my $c    = shift;
537     my $path = $c->req->path;
538     my @path = split /\//, $c->req->path;
539     $c->req->args( \my @args );
540
541     while (@path) {
542         $path = join '/', @path;
543         if ( my $result = ${ $c->get_action($path) }[0] ) {
544
545             # It's a regex
546             if ($#$result) {
547                 my $match    = $result->[1];
548                 my @snippets = @{ $result->[2] };
549                 $c->log->debug(
550                     qq/Requested action is "$path" and matched "$match"/)
551                   if $c->debug;
552                 $c->log->debug(
553                     'Snippets are "' . join( ' ', @snippets ) . '"' )
554                   if ( $c->debug && @snippets );
555                 $c->req->action($match);
556                 $c->req->snippets( \@snippets );
557             }
558
559             else {
560                 $c->req->action($path);
561                 $c->log->debug(qq/Requested action is "$path"/) if $c->debug;
562             }
563
564             $c->req->match($path);
565             last;
566         }
567         unshift @args, pop @path;
568     }
569
570     unless ( $c->req->action ) {
571         $c->req->action('default');
572         $c->req->match('');
573     }
574
575     $c->log->debug( 'Arguments are "' . join( '/', @args ) . '"' )
576       if ( $c->debug && @args );
577 }
578
579 =item $c->prepare_body
580
581 Prepare message body.
582
583 =cut
584
585 sub prepare_body { }
586
587 =item $c->prepare_connection
588
589 Prepare connection.
590
591 =cut
592
593 sub prepare_connection { }
594
595 =item $c->prepare_cookies
596
597 Prepare cookies.
598
599 =cut
600
601 sub prepare_cookies {
602     my $c = shift;
603
604     if ( my $header = $c->request->header('Cookie') ) {
605         $c->req->cookies( { CGI::Cookie->parse($header) } );
606     }
607 }
608
609 =item $c->prepare_headers
610
611 Prepare headers.
612
613 =cut
614
615 sub prepare_headers { }
616
617 =item $c->prepare_parameters
618
619 Prepare parameters.
620
621 =cut
622
623 sub prepare_parameters { }
624
625 =item $c->prepare_path
626
627 Prepare path and base.
628
629 =cut
630
631 sub prepare_path { }
632
633 =item $c->prepare_request
634
635 Prepare the engine request.
636
637 =cut
638
639 sub prepare_request { }
640
641 =item $c->prepare_uploads
642
643 Prepare uploads.
644
645 =cut
646
647 sub prepare_uploads { }
648
649 =item $c->run
650
651 Starts the engine.
652
653 =cut
654
655 sub run { }
656
657 =item $c->request
658
659 =item $c->req
660
661 Returns a C<Catalyst::Request> object.
662
663     my $req = $c->req;
664
665 =item $c->response
666
667 =item $c->res
668
669 Returns a C<Catalyst::Response> object.
670
671     my $res = $c->res;
672
673 =item $class->setup
674
675 Setup.
676
677     MyApp->setup;
678
679 =cut
680
681 sub setup {
682     my $self = shift;
683
684     # Initialize our data structure
685     $self->components( {} );
686
687     $self->setup_components;
688
689     if ( $self->debug ) {
690         my $t = Text::ASCIITable->new;
691         $t->setOptions( 'hide_HeadRow',  1 );
692         $t->setOptions( 'hide_HeadLine', 1 );
693         $t->setCols('Class');
694         $t->setColWidth( 'Class', 75, 1 );
695         $t->addRow($_) for sort keys %{ $self->components };
696         $self->log->debug( "Loaded components:\n" . $t->draw )
697           if ( @{ $t->{tbl_rows} } );
698     }
699
700     # Add our self to components, since we are also a component
701     $self->components->{$self} = $self;
702
703     $self->setup_actions;
704
705     if ( $self->debug ) {
706         my $name = $self->config->{name} || 'Application';
707         $self->log->info("$name powered by Catalyst $Catalyst::VERSION");
708     }
709 }
710
711 =item $class->setup_components
712
713 Setup components.
714
715 =cut
716
717 sub setup_components {
718     my $self = shift;
719
720     my $callback = sub {
721         my ( $component, $context ) = @_;
722
723         unless ( $component->isa('Catalyst::Base') ) {
724             return $component;
725         }
726
727         my $suffix = Catalyst::Utils::class2classsuffix($component);
728         my $config = $self->config->{$suffix} || {};
729
730         my $instance;
731
732         eval { $instance = $component->new( $context, $config ); };
733
734         if ( my $error = $@ ) {
735             
736             chomp $error;
737             
738             Catalyst::Exception->throw( 
739                 message => qq/Couldn't instantiate component "$component", "$error"/
740             );
741         }
742
743         return $instance;
744     };
745
746     eval {
747         Module::Pluggable::Fast->import(
748             name   => '_components',
749             search => [
750                 "$self\::Controller", "$self\::C",
751                 "$self\::Model",      "$self\::M",
752                 "$self\::View",       "$self\::V"
753             ],
754             callback => $callback
755         );
756     };
757
758     if ( my $error = $@ ) {
759         
760         chomp $error;
761         
762         Catalyst::Exception->throw( 
763             message => qq/Couldn't load components "$error"/ 
764         );
765     }
766
767     for my $component ( $self->_components($self) ) {
768         $self->components->{ ref $component || $component } = $component;
769     }
770 }
771
772 =item $c->state
773
774 Contains the return value of the last executed action.
775
776 =item $c->stash
777
778 Returns a hashref containing all your data.
779
780     $c->stash->{foo} ||= 'yada';
781     print $c->stash->{foo};
782
783 =cut
784
785 sub stash {
786     my $self = shift;
787     if (@_) {
788         my $stash = @_ > 1 ? {@_} : $_[0];
789         while ( my ( $key, $val ) = each %$stash ) {
790             $self->{stash}->{$key} = $val;
791         }
792     }
793     return $self->{stash};
794 }
795
796 =back
797
798 =head1 AUTHOR
799
800 Sebastian Riedel, C<sri@cpan.org>
801
802 =head1 COPYRIGHT
803
804 This program is free software, you can redistribute it and/or modify it under
805 the same terms as Perl itself.
806
807 =cut
808
809 1;