#! /usr/bin/perl -w

# vim:syntax=perl

use strict;

use vars qw/ %reports @report_order %inputs $cfg $dlf_info $keep_temp_dlf /;

use lib '/usr/local/share/perl5';

use Lire::ReportConfig;
use Lire::Program qw/ :msg tempfile $PROG $LR_ID /;
use Lire::DataTypes qw/ :basic :special :misc /;
use Lire::XMLUtils qw/xml_encode/;
use Lire::AsciiDlf::AsciiDlfFactory;
use Lire::AsciiDlf::DlfInfo;

use POSIX qw/ strftime /;
use Symbol;
use Fcntl;

BEGIN {
    $keep_temp_dlf = 0;
    $ENV{LR_KEEP_TEMP_DLF} ||= "no";

    if ( check_bool( $ENV{LR_KEEP_TEMP_DLF}) ) {
	$keep_temp_dlf = eval_bool( $ENV{LR_KEEP_TEMP_DLF} );
    } else {
	lr_warn( "invalid boolean value in LR_KEEP_TEMP_DLF: ",
		 $ENV{LR_KEEP_TEMP_DLF} );
    }
}

my $debug = 0;
sub debug {
    $debug and lr_debug($_[0]);
}

sub load_report_cfg {
    my ( $superservice, $report_cfg ) = @_;

    my $factory = new Lire::AsciiDlf::AsciiDlfFactory;
    debug( "created Lire::AsciiDlf::AsciiDlfFactory object" );

    $cfg = eval { new_from_file Lire::ReportConfig( $superservice, $report_cfg,
						    $factory)};
    if ($@) {
        debug( "troubles when executing new_from_file Lire::ReportConfig " .
            "'$superservice', '$report_cfg', '$factory'" );
        lr_err( $@ );
    }
    debug( "load_report_cfg: executed new_from_file Lire::ReportConfig on " .
        "'$superservice', '$report_cfg' and a factory" );

    # Merge the sections' filter specifications
    $cfg->merge_filters;

    # Fill %reports and @report_orders based on the config file
    foreach my $section ( $cfg->sections ) {
	my @reports = $section->reports;
	foreach my $r ( @reports ) {
	    push @report_order, $r->key;
	    $reports{$r->key} = $r;
	}
    }
}

sub count_dlf_records {
    my ( $schema, $fh ) = @_;

    lr_info( "Analysis DLF file" );
    my $time_field = $schema->timestamp_field->name;
    $dlf_info = eval { new Lire::AsciiDlf::DlfInfo( $schema, $fh ) };
    lr_err( $@ ) if $@;

    if ( ! $dlf_info->is_field_available( $time_field ) ) {
	lr_info( "DLF contains ", $dlf_info->record_count, " records; start and end time are unavailable" );
    } else {
	lr_info( "DLF contains ", $dlf_info->record_count, " records; starts on ", 
		 strftime( "%Y-%m-%d %H:%M:%S", localtime $dlf_info->start_time ),
		 "; ends on ", 
		 strftime( "%Y-%m-%d %H:%M:%S", localtime $dlf_info->end_time ),
	       );
    }
    my @dlf_unavail = $dlf_info->unavail_fields;
    lr_info( "unavailable DLF fields: ", join( ", ", @dlf_unavail) )
      if @dlf_unavail;
}


#
# This function initializes the %inputs data structure which sorts the
# report specifications according to schema.
#
# The inputs hash contains one entry for each different schema that is 
# going to be used by all report specifications.
# 
# Consult the add_schema_to_input function for a description of the
# inputs' entry data structure.
sub sort_reports {
    my ($superservice) = @_;
    %inputs = ();

    debug ("sort_reports: sorting reports according to DLF input");

    # Sorts the reports according to input schema
    foreach my $report ( @report_order) {
	my $report_spec = $reports{$report};
	my $input  = add_schema_to_input( $report_spec->schema );
	push @{$input->{reports}}, $report;
    }
}

#
# This function initializes reports and determine their needed sort
# order.
sub init_reports {
    my ($input) = @_;

    my @reports = @{$input->{reports}};
    debug ("init_reports: initializing reports '@reports'");

    # Initialize the reports
    return unless check_reports_fields( $input, { reports => $input->{reports},
						  filters => {}
						} );

    foreach my $report ( @reports ) {
	lr_info( "initializing report '$report'");
	my $report_spec = $reports{$report};
	next unless defined $report_spec;
	eval {
	    $report_spec->init_report( $input->{dlf_info} );
	    add_report_to_input( $input, $report_spec );
	};
	if ( $@ ) {
	    lr_warn( "$@\n$report will be skipped");
	    delete $reports{$report};
	    $report_spec->mark_missing( "failed: $@" );
	}
    }
}

sub add_schema_to_input {
    my ( $schema ) = @_;

    my $id = $schema->id;
    return $inputs{$id} if exists $inputs{$id};

    my $fh;
    my $filename;
    my $info;
    if ( $schema->isa( "Lire::DerivedSchema" ) ) {
	my $base = $schema->base;
	add_schema_to_input( $base );

	if ( $keep_temp_dlf ) {
	    ($fh, $filename) = tempfile( "$PROG.$LR_ID.$id.XXXXXX",
					 SUFFIX => '.dlf' );
	} else {
	    $fh = tempfile();
	}
	push @{$inputs{$base->id}{derived}}, $id;
    } elsif ( $schema->isa( "Lire::ExtendedSchema" ) ) {
	my $base = $schema->base;
        add_schema_to_input( $base );

	if ( $keep_temp_dlf ) {
	    ($fh, $filename) = tempfile( "$PROG.$LR_ID.$id.XXXXXX",
					 SUFFIX => '.dlf' );
	} else {
	    $fh = tempfile();
	}
	push @{$inputs{$base->id}{extensions}}, $id;
    } elsif ( $schema->isa( "Lire::DlfSchema" ) ) {
	$fh = \*DLF;
	$info = $dlf_info;
    }

    $inputs{$id} =
      { 
       # The name of the schema
       name       => $id,

       # The file handle that contains the DLF
       fh	   => $fh,

       # The filename of the DLF.
       filename   => $filename,

       # The list of reports that are computed on this DLF input
       reports	  => [],

       # An array used to group together report that share the same sort 
       # order on the input. This array contains hashes containing:
       #
       # - the fields on which to sort
       #    sort_fields => [], 
       # - the report's keys that don't have a filter expression
       #    reports	=> [],
       # - a hash used to group together report specs that share a common
       #   filter expression.
       #    filters    => {},
       #
       # This element is created by the init_reports function
       sort_orders => [],

       # The Lire::AsciiDlf::DlfInfo object for this DLF source
       dlf_info	    => $info,

       # Input sources that are derived using the
       # Lire::AsciiDlf::ExtendedFieldsCreator interface
       extensions => [],

       # Input sources that are derived using the
       # Lire::AsciiDlf::DerivedRecordsCreator interface
       derived	   => [], 
      }
}

sub add_report_to_input {
    my ( $input, $report_spec ) = @_;

    my $report = $report_spec->key();

    # Put the report at the appropriate place in the data structure
    my %uniq;
    my @sfields = grep { if (exists $uniq{$_}) { 0 } else {$uniq{$_} = 1;} }
      $report_spec->calc_spec->dlf_sort_fields();

    # Report specification that doesn't need a sorted input will use
    # the reports and filters element of the input record
    my $report_struct = undef;
    my $filter_spec = $report_spec->filter_spec;
    # Try to find an existing entry for those sort fields. An entry is 
    # considered compatible if they share a common sort fields prefix. 
    # For example, we can use the same sorted input for report 
    # specifications that need a sorted input in the following order:
    # time; time, client_host; time, client_host, http_result
    foreach my $e ( @{$input->{sort_orders}}) {
	my $order = is_sort_order_compatible($e->{sort_fields}, \@sfields);
	next unless defined $order;
	    
	$e->{sort_fields} = $order;
	$report_struct = $e;
	last;
    }
    unless ( $report_struct ) {
	$report_struct ||= {
			    sort_fields => \@sfields,
			    reports     => [],
			    filters     => {},
			   };
	push @{$input->{sort_orders}}, $report_struct;
    }

    if ( defined $filter_spec ) {
	my $filter = add_filter_report( $report_struct, $filter_spec );
	push @{$filter->{reports}}, $report;
    } else {
	push @{$report_struct->{reports}}, $report;
    }
}

sub add_filter_report {
    my ( $rstruct, $filter_spec ) = @_;

    my $filter_id = $filter_spec->id();
    return $rstruct->{filters}{$filter_id}
      if exists $rstruct->{filters}{$filter_id};

    $rstruct->{filters}{$filter_id} = { name    => $filter_id,
					filter  => $filter_spec->compile(),
					reports => [],
				      };
}

# Return the longest array when an array is a prefix of the other.
# Return undef when the arrays aren't equal or a prefix of one another.
sub is_sort_order_compatible {
    my ( $sfield1, $sfield2 ) = @_;

    # Sfield1 should always contain the shortest array
    ($sfield1, $sfield2) = ($sfield2, $sfield1) if @$sfield2 < @$sfield1;
    for ( my $i=0; $i < @$sfield1; $i++ ) {
	return undef unless $sfield1->[$i] eq $sfield2->[$i];
    }

    # sfield1 is a prefix of sfield2
    return $sfield2;
}

sub cancel_input {
    my ($input) = @_;

    lr_notice( "cancelling reports of the '$input->{name}' schema" );
    cancel_reports( $input );
    delete $inputs{$input->{name}};

    foreach my $i ( (@{$input->{extensions}}, @{$input->{derived}} ) )
    {
	cancel_input( $i );
    }
}

sub cancel_reports {
    my ( $report_struct ) = @_;

    foreach my $r ( @{$report_struct->{reports}} ) {
	lr_notice( "report '$r' will be skipped because of unavailable input");
	next unless $reports{$r};
	$reports{$r}->mark_missing( "unavailable input" );
	delete $reports{$r};
    }

    foreach my $f ( values %{$report_struct->{filters}} ) {
	foreach my $r ( @{$f->{reports}} ) {
	    lr_notice( "report '$r' will be skipped because of unavailable input");
	    next unless $reports{$r};
	    $reports{$r}->mark_missing( "unavailable input" );
	    delete $reports{$r};
	}
    }
}

sub create_input_source {
    my ( $input ) = @_;

    my $schema = Lire::DlfSchema::load_schema( $input->{name} );
    return unless UNIVERSAL::isa( $schema, "Lire::DerivedSchema" ) ||
      UNIVERSAL::isa( $schema, "Lire::ExtendedSchema" );

    my $base = $inputs{$schema->base->id};

    # Verify that we have the required fields are available
    my @missings;
    foreach my $f ( @{$schema->required_fields} ) {
	push @missings, $f
	  unless $base->{dlf_info}->is_field_available( $f );
    }

    if (@missings) {
	lr_notice( "can't compute schema '", $schema->id, 
		   "' because some fields are unavailable: ", 
		   join( ", ", @missings) );
	cancel_input( $input );
	return 0;
    }

    # Make sure that source file handle is at the start
    seek $base->{fh}, 0, 0
      or lr_err( "error rewinding source DLF $base->{name}: $!" );

    eval {
	if ( UNIVERSAL::isa( $schema, "Lire::ExtendedSchema" ) ) {
	    create_extended_input( $base, $input );
	} elsif ( UNIVERSAL::isa( $schema, "Lire::DerivedSchema" ) ) {
	    create_derived_input( $base, $input );
	}
    };
    if ( $@ ) {
	lr_warn( $@ );
	cancel_input( $input );
    }

    seek $input->{fh}, 0, 0
      or lr_err( "seek: $!" );
}

sub create_derived_input {
    my ( $base, $input ) = @_;
    lr_info( "creating derived DLF source '$input->{name}' ",
	     "from '$base->{name}'" );

    my $schema	    = Lire::DlfSchema::load_schema( $input->{name} );
    my $base_schema = Lire::DlfSchema::load_schema( $base->{name} );

    lr_debug( "will keep temporary DLF for schema ", $schema->id,
	      " in file ", $input->{filename} )
      if ($keep_temp_dlf);

    my $out_fh = $input->{fh};
    my $writer_cb = sub {
	# Escape fields
	foreach my $f ( @{$_[0]} ) {
	    Lire::DlfSchema::ascii_dlf_escape_field( $f );
	}

	print $out_fh join( " ", @{$_[0]} ), "\n";
    };

    my $module = $schema->module;
    $module->init_computation( $base->{dlf_info}, $writer_cb );

    my $line;
    my $count = 0;
    my $fh = $base->{fh};
    my $field_count = $base_schema->field_count;
    my $rtotal = $base->{dlf_info}->record_count;
    while (defined($line = <$fh>)) {
	chomp $line;
	my $dlf = [split /\s+/, $line];
        if (@$dlf != $field_count) {
            debug( "create_derived_input: choking on line '$line'" );
            lr_crit( "DLF record has ", scalar @$dlf, " fields when it ",
		 "should have ", $field_count );
        }

	$module->dlf_record( $dlf, $writer_cb );
	$count++;

	lr_info( sprintf( "%.2f%%", $count*100 / $rtotal ),
		 " of DLF source '$input->{name}' completed" )
	  unless $count % 10_000;
    }

    $module->end_computation( $writer_cb );
    $input->{dlf_info} = new Lire::AsciiDlf::DlfInfo( $schema, $out_fh );
    lr_info( $input->{dlf_info}->record_count,
	     " records in '$input->{name}' derived schema" );
}

sub create_extended_input {
    my ( $base, $input ) = @_;

    my $schema	    = Lire::DlfSchema::load_schema( $input->{name} );
    my $base_schema = Lire::DlfSchema::load_schema( $base->{name} );
    lr_info( "creating extended DLF source '$input->{name}' ",
		 "from '$base->{name}'" );
    lr_debug( "will keep temporary DLF for schema ", $schema->id,
	      " in file ", $input->{filename} )
      if ($keep_temp_dlf);

    my $module = $schema->module;
    $module->init_computation( $base->{dlf_info} );

    my $line;
    my $count = 0;
    my $fh	= $base->{fh};
    my $out_fh	= $input->{fh};
    my $field_count = $base_schema->field_count;
    my $rtotal	= $base->{dlf_info}->record_count;
    while (defined($line = <$fh>)) {
	chomp $line;
	my $dlf = [split /\s+/, $line];
        if (@$dlf != $field_count) {
            debug( "create_extended_input: choking on line '$line'" );
            lr_crit( "DLF record has ", scalar @$dlf, " fields when it ",
		     "should have ", $field_count );
        }

	my $fields = $module->create_extended_fields( $dlf );
	foreach my $f ( @$fields ) {
	    Lire::DlfSchema::ascii_dlf_escape_field( $f );
	}

	print $out_fh join( " ", @$dlf, @$fields ), "\n";

	$count++;
	lr_info( sprintf( "%.2f%%", $count*100 / $rtotal ),
		 " of DLF source '$input->{name}' completed" )
	  unless $count % 10_000;
    }

    $module->end_computation;
    $input->{dlf_info} = $base->{dlf_info}->new_extended( $schema, $out_fh );
    lr_info( $input->{dlf_info}->record_count,
	     " records in '$input->{name}' extended schema" );
}

sub create_sorted_input {
    my ( $input, $sorted_fields ) = @_;

    unless (@$sorted_fields ) {
	# No sort needed
	seek $input->{fh}, 0, 0
	  or die "can't rewind source DLF $input->{name}\n";
	return $input->{fh};
    }

    lr_info( "creating DLF source '$input->{name}' sorted on fields: ", 
	     join ", ", @$sorted_fields  );

    my $schema = Lire::DlfSchema::load_schema( $input->{name} );

    # Build sort command line
    my @cmd = ( "sort", "-t", " " );
    foreach my $f ( @$sorted_fields ) {
	my $opt = "";
	my $field = $schema->field( $f );
	$opt .= "n" if is_numeric_type( $field->type );
	my $pos = $field->pos + 1; # sort(1) -k takes 1 indexed argument
	push @cmd, "-k",  $pos . "," . $pos . $opt;
    }

    my ($fh, $filename);
    if ( $keep_temp_dlf ) {
	($fh, $filename) = tempfile( "$PROG.$LR_ID.$input->{name}.sort.XXXXXX",
					 SUFFIX => '.dlf' );
	lr_debug( "will keep temporary DLF for sorted schema ", $schema->id,
		  " in file ", $filename )
	  if ($keep_temp_dlf);
    } else {
	$fh = tempfile();
    }

    seek $input->{fh}, 0, 0
      or die "can't rewind source DLF $input->{name}\n";

    my $child_pid = fork;
    die "error forking: $!\n" unless defined $child_pid;
    if ( $child_pid == 0 ) {
	# Clear the environment
	# This makes sure that we use POSIX C language sort order
	local %ENV = ( PATH => $ENV{PATH},
		       HOME => $ENV{HOME},
		       TMPDIR => $ENV{TMPDIR},
		     );
	# Child
	open ( STDIN, "<&" . fileno $input->{fh} )
	  or die "error redirecting stdin: $!\n" ;
	open ( STDOUT, ">&" . fileno $fh )
	  or die "error redirecting stdout: $!\n";
	exec @cmd
	  or die "exec failed: $!\n";
    }
    # We let dlf_out open, so that the caller can seek it to the beginning
    waitpid $child_pid, 0
      or die "waitpid failed: $!\n";
    die "sort failed (exit with $?)\n" if $?;

    seek $fh, 0, 0
      or die "error rewinding sorted DLF: $!\n";

    return $fh;
}

sub check_reports_fields {
    my ( $input, $rstruct ) = @_;

    my $count = 0;
    foreach my $r ( ( @{$rstruct->{reports}},
		      map { @{$_->{reports}} } values %{$rstruct->{filters}} ))
    {
	my $report_spec = $reports{$r};
	next unless $report_spec;
	my %uniq = ();
	my @fields = grep { if (exists $uniq{$_}) { 0 } else {$uniq{$_} = 1;} }
	  $report_spec->needed_fields();
	my @missings = ();
	foreach my $f ( @fields ) {
	    push @missings, $f->name
	      unless $input->{dlf_info}->is_field_available( $f->name );
	}

	if (@missings) {
	    lr_notice( "report '$r' will be skipped because some ",
		       "required fields are unavailable: ",
		       join( ", ", @missings));
	    delete $reports{$r};
	    $report_spec->mark_missing( "missing fields: " . 
					join( ", ", @missings) );
	} else {
	    $count++;
	}
    }

    return $count;
}

sub compute_reports_from_source {
    my ( $input ) = @_;

    return unless $input;

    create_input_source( $input );

    # Check that the source wasn't cancelled
    return unless exists $inputs{$input->{name}};

    # Initialize the reports and determine the sort_orders
    init_reports( $input );

    # Compute all the reports
    foreach my $f ( @{$input->{sort_orders}} ) {
	compute_reports( $input, $f );
    }

    # Process derived and extended schema
    foreach my $i ( (@{$input->{extensions}}, @{$input->{derived}}) )
    {
	compute_reports_from_source( $inputs{$i} );
    }

    close $input->{fh};
}

# This compute all the reports in $rstruct which comes from the $input source
# $rstruct is an hash from the sort
sub compute_reports {
    my ($input, $rstruct ) = @_;

    return unless check_reports_fields( $input, $rstruct );

    my $fh = eval { create_sorted_input($input, $rstruct->{sort_fields} ) };
    if ( $@ ) {
	lr_warn( $@ );
	cancel_reports( $rstruct );
	return;
    }

    my $source_name;
    if ( @{$rstruct->{sort_fields}} ) {
	$source_name = "'$input->{name}' sorted on fields: " .
	  join ", ", @{$rstruct->{sort_fields}};
    } else {
	$source_name = "'$input->{name}' unsorted";
    }

    lr_info( "computing reports from DLF source ", $source_name );

    my $reports = $rstruct->{reports};
    my $filters = $rstruct->{filters};
    my $record_count = $input->{dlf_info}->record_count;

    my $line;
    my $count = 0;
    my $schema = Lire::DlfSchema::load_schema( $input->{name} );
    my $field_count = $schema->field_count;
    while (defined($line = <$fh>)) {
	chomp $line;

	$count++;

	lr_info( sprintf( "%.2f%%", $count*100 / $record_count ),
		 " of DLF source $source_name processed" )
	  unless $count % 10_000;
	my $dlf = [split /\s+/, $line];
        if (@$dlf != $field_count) {
            debug ( "compute_reports_from_source: choking on line '$line'" );
            lr_crit( "DLF record has ", scalar @$dlf, " fields when it ",
		 "should have ", $field_count );
        }

	# Compute reports which aren't using a filter
	foreach my $report ( @$reports ) {
	    my $report_spec = $reports{$report};
	    next unless defined $report_spec; # Skipped failed reports
	    eval {
		$report_spec->update_report( $dlf );
	    };
	    if ( $@ ) {
		lr_warn( "$@\n$report will be skipped");
		$report_spec->mark_missing( "failed: $@" );
		delete $reports{$report};
	    }
	}

	# Compute each reports using a filter
	foreach my $f ( values %$filters) {
	    next unless $f->{filter}->( $dlf );

	    foreach my $report ( @{$f->{reports}} ) {
		my $report_spec = $reports{$report};
		next unless defined $report_spec; # Skipped failed reports
		eval {
		    $report_spec->update_report( $dlf );
		};
		if ( $@ ) {
		    lr_warn( "$@\n$report will be skipped");
		    $report_spec->mark_missing( "failed: $@" );
		    delete $reports{$report};
		}
	    }
	}
    }
    lr_info( "processed $count records in DLF source $source_name" );

    # Finalize the reports
    foreach my $report ( (@$reports,
			  map { @{$_->{reports}} } values %$filters ))
    {
	my $report_spec = $reports{$report};
	next unless defined $report_spec; # Skipped failed reports

	lr_info( "completing report '$report'" );
	eval {
	    $report_spec->end_report();
	    $reports{$report} = $report_spec;
	};
	if ($@) {
	    lr_warn( "$@\n$report will be skipped");
	    $report_spec->mark_missing( "failed: $@" );
	    delete $reports{$report};
	}
    }

    # Only close the fh, if it is a sorted source
    # the main DLF fh is closed in compute_reports_from_source
    close $fh if @{$rstruct->{sort_fields}};
}

lr_err( "Usage: $PROG <superservice> <report_cfg_file> <dlf_file>" )
  unless @ARGV == 3;

my ( $superservice, $report_cfg, $dlf_file ) = @ARGV;

# Open the DLF file
open DLF, $dlf_file
  or lr_err( "can't open $dlf_file: $!" );

my $schema = eval { Lire::DlfSchema::load_schema( $superservice ) };
if ($@) {
    debug( "troubles excuting Lire::DlfSchema::load_schema on " .
        "'$superservice'" );
    lr_err( $@ );
}
debug( "executed Lire::DlfSchema::load_schema on '$superservice'" );

load_report_cfg( $superservice, $report_cfg );
debug( "executed load_report_cfg on '$superservice', '$report_cfg'" );

count_dlf_records( $schema, \*DLF );
debug( "executed count_dlf_records" );

sort_reports( $superservice );
debug( "executed sort_reports on '$superservice'" );

compute_reports_from_source( $inputs{$superservice} );

lr_info( "creating lire XML report..." );
my $report = $cfg->create_report( $dlf_info->start_time, $dlf_info->end_time );
$report->generator( "lr_dlf2xml(1)" );
lr_info( "writing lire XML report..." );
$report->write_report;

if ( $ENV{LR_ARCHIVE} ) {
    my $lr_time = strftime( "%Y%m%d%H%M%S", localtime $dlf_info->start_time ) 
      . "-" . strftime( "%Y%m%d%H%M%S", localtime $dlf_info->end_time );
    # Save timespan in the archive
    lr_info( "gonna run lr_db_store $LR_ID time_span $lr_time" );
    system( "lr_db_store", $LR_ID, "time_span", $lr_time );
    lr_err "lr_db_store failed"
      if ( $? ne 0 );
}
exit 0;

# Local Variables:
# mode: cperl
# End:

__END__

=pod

=head1 NAME

lr_dlf2xml - generate a XML report from a dlf file

=head1 SYNOPSIS 

B<lr_dlf2xml> I<superservice> I<report_cfg_file> I<dlffile>

=head1 DESCRIPTION

B<lr_dlf2xml> reads a dlf file, and prints a generated XML report to stdout.

It stores the dlf file's timespan in the Lire database, by running
lr_db_store(1).

It inspects the I<report_cfg_file> (e.g. .../etc/lire/email.cfg) to find
names of reports and associated settings.

The environment variable LR_ID is used in debug messages printed to stderr.
The directory stored in the  environment variable TMPDIR, as set in defaults,
is used to create tmp files in.

This script is called by lr_log2report(1).

=head1 SEE ALSO

lr_log2report(1), documentation in the Lire User Manual

=head1 VERSION

$Id: lr_dlf2xml.in,v 1.82 2002/07/19 16:00:13 flacoste Exp $

=head1 COPYRIGHT

Copyright (C) 2001, 2002 Stichting LogReport Foundation LogReport@LogReport.org
 
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
 
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.
 
You should have received a copy of the GNU General Public License
along with this program (see COPYING); if not, check with
http://www.gnu.org/copyleft/gpl.html or write to the Free Software 
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111, USA.

=head1 AUTHOR

Francis J. Lacoste <flacoste@logreport.org>

=cut


