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