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