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