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