#!/usr/bin/perl -I perl_lib/

# TODO: syntax in XSLT to check required constants are SET

# License: GPL
# Copyright: University of Southampton 2011
# Author: Christopher Gutteridge; http://id.ecs.soton.ac.uk/person/1248


use strict;
use warnings;

use Getopt::Long;

my %options = ( 
	config=>[], 	set=>[], 	delineator=>[], 
	include=>[],    process=>[],	carry=>[],
	noise=>1, 	in=>[], 	out=>undef, 
	xslt=>undef, 	format=>undef,	worksheet=>undef, 
	"skip-rows"=>undef,"skip-cols"=>undef,
);

Getopt::Long::Configure("permute");

my $show_help;
my $show_version;
my $verbose;
my $quiet;
GetOptions( 
	'help|?' => \$show_help,
	'version' => \$show_version,

	'verbose+' => \$verbose,
	'quiet' => \$quiet,

	'in=s' => $options{"in"},
	'out=s' => \$options{"out"},
	'xslt=s' => \$options{"xslt"},
	'format=s' => \$options{"format"},
	'worksheet=i' => \$options{"worksheet"},
	'skip-rows=i' => \$options{"skip-rows"},
	'skip-cols=i' => \$options{"skip-cols"},

	'config=s' => $options{"config"},
	'include=s' => $options{"include"},
	'set=s' => $options{"set"},
	'delineator=s' => $options{"delineator"},
	'carry=s' => $options{"carry"},
	'process=s' => $options{"process"},
) || show_usage();
#use Data::Dumper;print Dumper( \%options );exit;

show_version() if $show_version;

show_help() if $show_help;

show_usage() if( scalar @ARGV != 0 ); 

$options{noise} = 0 if( $quiet );
$options{noise} = 1+$verbose if( $verbose );

my $grinder = new Grinder( %options );

$grinder->grind();

exit;

		  ############################################################

sub show_version
{
	print STDERR "Grinder: version XYZ\n";	
	exit;
}

sub show_usage
{
	print STDERR <<END;
Usage: $0 [OPTION]... [--in filename|url] [--out filename] [--xslt filename]
Usage: $0 [OPTION]... [--config filename] 

$0 --help for more information.
END
	exit 1;
}

sub show_help
{
	print STDERR <<END;
Usage: $0 [OPTION]... [--in filename] [--out filename] [--xslt filename]
Usage: $0 [OPTION]... [--config filename] 

 --config <filename>    Load options from a config file, although command
                         line options override.

 --in <filename|url>    File to load spreadsheet from. Default "-" (stdin),
                         or if it starts with http: or https: then it is 
                         treaded as a URL.

 --format <format>      Format of spreadsheet.
                         csv (default) - Comma separated values
                         tsv - Tab separated values
                         psv - Pipe character separated values (eg. biztalk)
                         excel - Excel document (.xsl NOT .xslx)
                         colon - Colon-separated (eg. passwd)

 --worksheet <number>   For multi sheet files, which sheet (default 1)

 --skip-rows <number>   Ignore the this number of rows before looking for the
                         headings.

 --skip-cols <number>   Ignore this number of columns on every line.

 --out <filename>       File to output result to. Default "-" (stdout)

 --xslt <filename>      If specified, run the XML generated throught this 
                         XSLT transform and output that.

 --set <x>=<y>          Set a variable in the intermediate stage XML, sets 
                         <set name='x'>y</set>. This overrides values set in
                         config file(s) but is overridden by values set 
                         using *SET in the input data.

 --delineator <x>=<y>   Set a character <y> to use to split data in cells 
                         in a column with heading <x>.

 --carry <x>            Set a column which should have the value carried 
                         over to following rows if it is empty in those
                         rows.

 --process <x>=<y>,...  Add attributes to the XML cell with heading <x>.
                         attributes include "tag" and "md5" and "sha1",
                         "mbox_sha1sum", "ftag", "fname". You can use a *
                         in the <x> section to match zero or more characters.
                          
                               
 --include <filename>   Include the XML from <filename> at the top of the XML
                         output from the XSLT. Has no affect if xslt is not
                         set. Also assumes that the XSLT will add a 
                         <!--TOP--> to the XML it produces that can be used 
                         as the hook to insert the <filename> data.

 --verbose              Include more debug information. May be repeated for 
                         even more information.

 --quiet                Supress even normal warnings (but not errors).

 --help                 Show this help and exit.

 --version              Show the Grinder version and exit.

END
    exit;
}


		  ############################################################
		  ############################################################
		  ############################################################


package Grinder;

use strict;
use warnings;

# set = {} or [] or $
# delineator = {} or [] or $
# carry = {} or [] or $
# process = {} or [] or $
# include = [] or $

# in (default -)
# out (default -)
# xslt
# format (default csv)
# worksheet (default 1), excel format only
# skip-rows 
# skip-cols 
# config = [] or $
# noise (default 1)

sub new
{
	my( $class, %options ) = @_;

	my $self = bless { 
		set => {}, 
		delineator => {},
		carry => {},
		process => {},
		include => [],
	}, $class;

	# normal options
	foreach my $opt_key ( keys %options )
	{
		next if( $opt_key =~ m/^(set|delineator|process|carry)$/ );
		if( $opt_key eq "include" ) 
		{
			if( ref( $options{$opt_key} ) eq "" ) 
			{
				push @{$self->{$opt_key}}, $options{$opt_key};
			}
			else
			{
				push @{$self->{$opt_key}}, @{$options{$opt_key}};
			}
			next;
		}
		$self->{$opt_key} = $options{$opt_key};
	}

	if( $options{config} )
	{
		foreach my $config_file ( @{$options{config}} )
		{
			my $config_fh = $self->open_input_file( $config_file );
			my $line_no = 0;
			while( my $line = readline( $config_fh ) )
			{
				$line_no++;	
				chomp $line;
				next if( $line =~ m/^\s*$/ );
				next if( $line =~ m/^\s*#/ );
				if( $line =~ m/^\s*([^:]+):\s*(.*)$/ )
				{
					my( $left, $right ) = ( $1, $2 );
					$right=~s/\s*$//;
					if( $left =~ m/^(set|delineator|process|carry)$/ )
					{
						my( $id, $value ) = split( /=/, $right );
						$value = 1 if !defined $value;
						$value =~ s/<space>/ /g; #ugh!
						$self->{$left}->{$id} = $value;
					}
					elsif( $left eq "include" || $left eq "in" )
					{
						push @{$self->{$left}}, $right;
					}
					elsif( !defined $options{$left} )
					{
						# second definition in a config over-writes
						# but does not over-write passed-in option
						$self->{$left} = $right;
					}
					next;
				}
						
				$self->message( "Syntax error in config file '$config_file' at line #$line_no.", 1 );
				$self->message( "Line #$line_no: $line", 2 );
			}

			close( $config_fh );
		}
	}

	#  passed-in options overrides config for process type fields
	foreach my $opt_key ( keys %options )
	{
		my $v = $options{$opt_key};
		if( $opt_key =~ m/^(set|delineator|process|carry)$/ )
		{
			$v = [ $v ] if( ref( $v ) eq "" );	
			if( ref( $v ) eq "ARRAY" )
			{
				foreach my $touple ( @{$v} )
				{
					my( $id, $value ) = split( /=/, $touple );
					$value = 1 if !defined $value;
					$self->{$opt_key}->{$id} = $value;
				}
			}
			else
			{
				foreach my $id ( keys %{$v} )
				{
					$self->{$opt_key}->{$id} = $v->{$id};
				}
			}
			next;
		}
	}

	$self->{format} = "csv" unless defined $self->{format};
	$self->{noise} = 1 unless defined $self->{noise};
	$self->{in} = "-" unless defined $self->{in};
	$self->{out} = "-" unless defined $self->{out};
	$self->{tmp_dir} = "/tmp" unless defined $self->{tmp_dir};
	$self->{xslt_proc} = '/usr/bin/xsltproc @@XSL@@ @@XML@@' unless defined $self->{xslt_proc};

	return $self;
}

sub open_input_file
{
	my( $self, $filename ) = @_;

	my $fh;

	if( $filename eq "-" )
	{
		$fh = *STDIN;
		binmode( $fh, ":utf8" );
	}
	else
	{
		open( $fh, "<:utf8", $filename ) || $self->error( "Failed to open '$filename' for reading: $!" );
	}
	$self->message( "Opened '$filename' for reading.", 2 );

	return $fh;
}

sub open_output_file
{
	my( $self, $filename ) = @_;

	my $fh;

	if( $filename eq "-" )
	{
		$fh = *STDOUT;
		binmode( $fh, ":utf8" );
	}
	else
	{
		open( $fh, ">:utf8", $filename ) || $self->error( "Failed to open '$filename' for writing: $!" );
	}
	$self->message( "Opened '$filename' for writing.", 2 );

	return $fh;
}

sub close_file
{
	my( $self, $file_handle, $filename ) = @_;

	if( !close( $file_handle ) ) 
	{
		$self->message( "Could not close a file handle on $filename: $!", 1 );
	}
	$self->message( "Closed '$filename'.", 2 );
}

sub unlink_file
{
	my( $self, $filename ) = @_;

	if( !unlink( $filename ) ) 
	{
		$self->message( "Could not unlink $filename: $!", 1 );
	}
	$self->message( "Unlinked '$filename'.", 2 );
}


sub debug
{
	my( $self ) = @_;

	use Data::Dumper;
	print STDERR Dumper( $self );
}

sub error
{
	my( $self, $msg ) = @_;

	print STDERR "Grinder Error: $msg\n";
	exit 1;
}

sub message
{
	my( $self, $msg, $priority ) = @_;

	$priority = 1 unless defined $priority;
	if( $priority <= $self->{noise} )
	{
		print STDERR "$msg\n";
	}
}

sub grind
{
	my( $self ) = @_;

	my $xml_file;
	if( defined $self->{xslt} && $self->{xslt} ne "" )
	{
		$xml_file = $self->{tmp_dir}."/grinder.$$.xml";
	}
	else
	{
		$xml_file = $self->{out};
	}

	my $xml_out_fh = $self->open_output_file( $xml_file );

	$self->{parse} = { rows=>0, set=>{} };	

	my @lt = localtime;
	my @gmt = gmtime;

	$lt[5]+=1900; $lt[4]+=1;
	$gmt[5]+=1900; $gmt[4]+=1;
	my $offset = ($lt[1]+$lt[2]*60)-($gmt[1]+$gmt[2]*60);

	$self->{parse}->{set}->{_timestamp} = sprintf( '%04d-%02d-%02dT%02d:%02d:%02d%s%02d:%02d', 
		$lt[5],$lt[4],$lt[3],$lt[2],$lt[1],$lt[0], $offset<0?"":"+", $offset/60, $offset % 60);
	foreach my $k ( keys %{ $self->{set} } ) { $self->{parse}->{set}->{$k} = $self->{set}->{$k}; }

	my $ins = $self->{in};
	if( ref($ins) ne "ARRAY" ) { $ins = [$ins]; }

	foreach my $in ( @{$ins} )
	{
		if( $in =~ m/[; ]/ )
		{
			my @parts = split( /[ ;]/, $in );
			$in = { file=>shift @parts };
			foreach my $part ( @parts )
			{
				my( $k, $v ) = split( '=', $part );
				$in->{$k} = $v;
			}
		}
	}

	my $xmlns = { ""=>"http://purl.org/openorg/grinder/ns/" };
	foreach my $in ( @{$ins} )
	{
		if( ref( $in ) eq "HASH" )
		{
			if( defined $in->{namespace} )
			{
				$in->{prefix} = $in->{namespace};
				$xmlns->{$in->{prefix}} = "http://purl.org/openorg/grinder/ns/".$in->{namespace}."/";
			}
		}
	}

	# Output Header
	$self->send_xml_header( $xml_out_fh, $xmlns );

	# could take extra options to over-ride current ones, esp in & out

	foreach my $in ( @{$ins} )
	{
		my $in_fh;
		my $in_file = $in;
		my $format = $self->{format};
		my $worksheet = $self->{worksheet};
		$self->{parse}->{"skip-rows"} = $self->{"skip-rows"};
		$self->{parse}->{"skip-cols"} = $self->{"skip-cols"};
		my $prefix = "";
		if( ref( $in ) eq "HASH" )
		{
			$in_file = $in->{file};
			$format = $in->{format} if( defined $in->{format} );
			$prefix = $in->{prefix} if( defined $in->{prefix} );
			$worksheet = $in->{worksheet} if( defined $in->{worksheet} );
			$self->{parse}->{"skip-rows"} = $in->{"skip-rows"} if( defined $in->{"skip-rows"} );
			$self->{parse}->{"skip-cols"} = $in->{"skip-cols"} if( defined $in->{"skip-cols"} );
		}
		$self->{parse}->{"filename"} = $in_file;

		if( $in =~ m/^https?:/ )
		{
			$in_file = $self->{tmp_dir}."/grinder.in$$.xml";
			if( ! eval 'use LWP::UserAgent; 1;' ) 
			{
				$self->error( "Failed to load Perl Module: LWP::UserAgent: $@" );
			}	
			my $ua = LWP::UserAgent->new;
	
			my $req = HTTP::Request->new( GET => $in );
			my $res = $ua->request( $req );
			if( !$res )
			{
				$self->error( "Failed to load URL '".$in_file."': ".$res->status_line );
			}	
			my $in_file_out_fh = $self->open_output_file( $in_file ); # shake it all about
			print { $in_file_out_fh } $res->content;
			$self->close_file( $in_file_out_fh, $in_file );
		}
		$in_fh = $self->open_input_file( $in_file );
	
		delete $self->{parse}->{fields};	
		delete $self->{parse}->{fields_names};	
		if( $format eq "excel" ) { $self->process_excel( $in_fh, $xml_out_fh, $prefix, $worksheet ); }
		elsif( $format eq "csv" ) { $self->process_csv( $in_fh, $xml_out_fh, $prefix ); }
		elsif( $format eq "tsv" ) { $self->process_tsv( $in_fh, $xml_out_fh, $prefix ); }
		elsif( $format eq "colon" ) { $self->process_colon( $in_fh, $xml_out_fh, $prefix ); }
		elsif( $format eq "psv" ) { $self->process_psv( $in_fh, $xml_out_fh, $prefix ); }
		else { $self->error( "Unknown format '$format'" ); }

		$self->close_file( $in_fh, $in_file );
		# delete tmp file if input was from a URL
		if( $in =~ m/^https?:/ )
		{
			$self->unlink_file( $in_file );
		}
	}

	# Output end of XML file
	for my $set_id ( keys %{$self->{parse}->{set}} )
	{
		my $value = $self->{parse}->{set}->{$set_id};
		$value =~ s/&/&amp;/g;
		$value =~ s/>/&gt;/g;
		$value =~ s/</&lt;/g;
		$value =~ s/"/&quot;/g;
		print { $xml_out_fh } "  <set name='$set_id'>$value</set>\n";
	}

	$self->send_xml_footer( $xml_out_fh );

	$self->close_file( $xml_out_fh, $xml_file );


	# If no XSLT then we are done
	if( !defined $self->{xslt} || $self->{xslt} eq "" )
	{
		exit;
	}


	# Read include files
	my @lines = ();
	foreach my $inc_file ( @{$self->{include}} )
	{
		my $inc_fh = $self->open_input_file( $inc_file );
		while( my $line = readline( $inc_fh ) ) { push @lines, $line; }
		$self->close_file( $inc_fh, $inc_file );
	}
	my $include = join( '', @lines );


	# Process XSLT
	
	my $cmd = $self->{xslt_proc};
	$cmd =~ s/\@\@XML\@\@/$xml_file/g;
	$cmd =~ s/\@\@XSL\@\@/$self->{xslt}/g;

	my $xsltproc;
	open( $xsltproc, "-|:utf8", $cmd ) || $self->error( "Failed to exec '$cmd': $!" );
	$self->message( "Executed '$cmd' for writing.", 2 );

	my $out_fh = $self->open_output_file( $self->{out} );
	
	while( my $line = readline( $xsltproc ) )
	{
		$line =~ s/<!--TOP-->/$include/g;
		print { $out_fh } $line;
	}	

	$self->close_file( $out_fh, $self->{out} );	
	$self->close_file( $xsltproc, $cmd );	

	$self->unlink_file( $xml_file );	
}

sub process_row
{
	my( $self, $out, $cells, $prefix ) = @_;

	if( defined $self->{parse}->{"skip-rows"} && $self->{parse}->{"skip-rows"} > 0 )
	{
		$self->{parse}->{"skip-rows"}--;
		return;
	}

	if( defined $self->{parse}->{"skip-cols"} && $self->{parse}->{"skip-cols"} )
	{
		for( 1..$self->{parse}->{"skip-cols"} ) { shift @{$cells}; }
	}

	# Skip empty rows
	my $empty = 1;
	foreach my $cell ( @{$cells} )
	{
		if( defined $cell && $cell ne "" ) { $empty = 0; last; }
	}
	return if( $empty );

	# *STAR directive fields
	if( defined $cells->[0] && substr( $cells->[0],0,1 ) eq "*" )
	{
		if( $cells->[0] eq "*SET" )
		{
			$self->{parse}->{set}->{$cells->[1]} = $cells->[2];
		}
		elsif( $cells->[0] eq "*COMMENT" )
		{
			;
		}
		else
		{
			$self->message( "Unknown * directive: ".$cells->[0] );
		}
		return;
	}

	# Read headings
	if( !defined $self->{parse}->{fields} )
	{
		my $fields = {};
		foreach my $cell ( @{$cells} )
		{
			if( !defined $cell ) 
			{
				push @{$self->{parse}->{fields}}, undef;
				next;
			}
			$cell =~ s/^\s+//;
			$cell =~ s/\s+$//;
			my $name = $cell;

			$cell =~ s/\s/-/g;
			$cell =~ s/[^-_a-zA-Z0-9]//g;
			$cell = lc $cell;
			if( $cell eq "" )
			{
				push @{$self->{parse}->{fields}}, undef;
				next;
			}
			$cell =~ s/^[^A-Z0-9]*//i;
			$cell =~ s/--+/-/g;
			if( $cell eq "" ) { $cell = "empty-heading";  }
			if( $cell =~ m/^\d/ ) { $cell = "n".$cell; }
			if( defined $fields->{$cell} )
			{
				my $n = 2;
				while( defined $fields->{"$cell-$n"} ) { $n++; }
				$cell = "$cell-$n";
			}
			push @{$self->{parse}->{fields}}, $cell;
			$self->{parse}->{field_names}->{$cell} = $name;
			$fields->{$cell} = 1;
		}

		return;
	}

	my $p = $prefix.":";
	if( $prefix eq "" ) { $p = ""; }
	my $filename = $self->{parse}->{filename};
	$filename =~ s/&/&amp;/g;
	$filename =~ s/>/&gt;/g;
	$filename =~ s/</&lt;/g;
	$filename =~ s/"/&quot;/g;
	print { $out } "  <${p}row filename='$filename'>\n";
	my $this_row = {};
	for my $i ( 0..(scalar @{$self->{parse}->{fields}} - 1 ) )
	{
		my $field = $self->{parse}->{fields}->[$i];
		if( !defined $field ) { $field = "COL-".($i+1); }
	
		my $value = $cells->[$i];
		if( $self->{carry}->{$p.$field} && (!defined $value || $value =~ m/^\s*$/) )
		{
			$value = $self->{parse}->{prev_row}->{$p.$field};
		}
		$this_row->{$p.$field} = $value;

		next if !defined $value;

		$value =~ s/^\s+//;
		$value =~ s/\s+$//;

		my @values;
		if( $self->{delineator}->{$p.$field} )
		{
			my $del = $self->{delineator}->{$p.$field};
			@values = split( /\s*$del\s*/, $value );
		}
		else
		{
			@values = ( $value );
		}

		VALUE: foreach my $value ( @values )
		{
			my $attrs = "";
			EXP: foreach my $key ( keys %{$self->{process}} )
			{
				my $exp = $key;
				$exp =~ s/\*/\.*/g;
				if( "$p$field" !~ m/^$exp$/ ) { next EXP; }
				TYPE: foreach my $type ( split /,/, $self->{process}->{$key} )
				{
					if( $type eq "fname" )
					{
						my $tagname = $self->{parse}->{field_names}->{$field};
						$attrs.=" fname='$tagname'";	
						next;
					}
					if( $type eq "ftag" )
					{
						my $tagname = $self->{parse}->{field_names}->{$field};
						$tagname =~ s/\b([a-z])/\u$1/g;
						$tagname =~ s/[^a-zA-Z0-9-_]//g;
						$attrs.=" ftag='$tagname'";	
						next;
					}
					if( $value eq "" ) { next TYPE; }
					if( $type eq "md5" )
					{
						use Digest::MD5 qw(md5 md5_hex md5_base64);;
						$attrs.=" md5='".md5_hex($value)."'";
					}
					elsif( $type eq "sha1" )
					{
						use Digest::SHA1 qw(sha1_hex);
						$attrs.=" sha1='".sha1_hex($value)."'";
					}
					elsif( $type eq "mbox_sha1sum" )
					{
						use Digest::SHA1 qw(sha1_hex);
						$attrs.=" mbox_sha1sum='".sha1_hex("mailto:$value")."'";
					}
					elsif( $type eq "tag" )
					{
						my $tagname = $value;
						$tagname =~ s/\b([a-z])/\u$1/g;
						$tagname =~ s/[^a-zA-Z0-9-_]//g;
						$attrs.=" tag='$tagname'";
					}
					else
					{
						die "Unknown process type: '$type'";
					}
				}
			}
			$value =~ s/&/&amp;/g;
			$value =~ s/>/&gt;/g;
			$value =~ s/</&lt;/g;
			$value =~ s/"/&quot;/g;
			print { $out } "	<${p}$field$attrs>$value</${p}$field>\n";
		}
		$self->{parse}->{rows}++;
	}
	print { $out } "  </${p}row>\n";

	$self->{parse}->{prev_row} = $this_row;	
}

sub send_xml_footer
{
	my( $self, $out ) = @_;

	print { $out } <<END;
</grinder-data>
END
}

sub send_xml_header
{
	my( $self, $out, $xmlns ) = @_;

	print { $out } <<END;
<?xml version="1.0" encoding='utf-8'?>
<grinder-data 
END
	foreach my $prefix ( keys %{$xmlns} )
	{
		my $attr = "xmlns:$prefix";
		if( $prefix eq "" ) { $attr = "xmlns"; }
		print { $out } "    $attr=\"".$xmlns->{$prefix}."\"\n";
	}
	print { $out } ">\n";
}
	
sub process_excel
{
	my( $self, $in, $out, $prefix, $worksheet_number ) = @_;

	if( ! eval 'use Spreadsheet::ParseExcel; 1;' ) 
	{
		$self->error( "Failed to load Perl Module: Spreadsheet::ParseExcel: $@" );
	}	
	my $parser = Spreadsheet::ParseExcel->new();
	my $workbook = $parser->parse( $in );
	if ( !defined $workbook ) 
	{
		$self->error( "Failed to parse Excel file: ".$parser->error() );
	}

	$worksheet_number = 1 unless defined $worksheet_number;
	my @worksheets = $workbook->worksheets();
	if( !defined $worksheets[$worksheet_number-1] )
	{
		$self->error( "Workbook does not have a worksheet #$worksheet_number" );
	}
	my $worksheet = $worksheets[$worksheet_number-1];

	my ( $row_min, $row_max ) = $worksheet->row_range();
	my ( $col_min, $col_max ) = $worksheet->col_range();

	for my $row ( $row_min .. $row_max ) 
	{
		my @cells = ();
		for my $col ( 0 .. $col_max ) 
		{
			my $cell = $worksheet->get_cell( $row, $col );
			my $v;
			if( $cell ) { $v =  $cell->value; }
			push @cells, $v;
		}
		$self->process_row( $out, \@cells, $prefix );
	}
}

sub process_tsv
{
	my( $self, $in, $out, $prefix ) = @_;

	while( my $line = readline( $in ) )
	{
		chomp $line;

		$self->process_row( $out, [ split /\t/, $line ], $prefix );
	}
}

sub process_psv
{
	my( $self, $in, $out, $prefix ) = @_;

	while( my $line = readline( $in ) )
	{
		chomp $line;

		$self->process_row( $out, [ split /\|/, $line ], $prefix );
	}
}

sub process_colon
{
	my( $self, $in, $out, $prefix ) = @_;

	while( my $line = readline( $in ) )
	{
		chomp $line;

		$self->process_row( $out, [ split /:/, $line ], $prefix );
	}
}

sub process_csv
{
	my( $self, $in, $out, $prefix ) = @_;

	if( ! eval 'use Text::CSV; 1;' ) 
	{
		$self->error( "Failed to load Perl Module: Text::CSV" );
	}	

        my $csv = Text::CSV->new ({ binary => 1});

        while (my $row = $csv->getline( $in )) 
	{
		if( !$row )
		{
			my $err = $csv->error_input;
			$self->message( "Failed to parse line: $err", 1 );
			next;
		}
		
		$self->process_row( $out, $row, $prefix );
	}
}

