kivitendo/SL/Helper/Csv.pm @ f9f7b56e
2f6ebd89 | Sven Schöling | package SL::Helper::Csv;
|
||
use strict;
|
||||
use warnings;
|
||||
use Carp;
|
||||
use IO::File;
|
||||
use Text::CSV;
|
||||
use Params::Validate qw(:all);
|
||||
use Rose::Object::MakeMethods::Generic scalar => [ qw(
|
||||
f9f7b56e | Sven Schöling | file encoding sep_char quote_char escape_char header dispatch class
|
||
8fba112b | Sven Schöling | numberformat dateformat _io _csv _objects _parsed _data _errors
|
||
2f6ebd89 | Sven Schöling | ) ];
|
||
# public interface
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my %params = validate(@_, {
|
||||
sep_char => { default => ';' },
|
||||
quote_char => { default => '"' },
|
||||
8fba112b | Sven Schöling | escape_char => { default => '"' },
|
||
2f6ebd89 | Sven Schöling | header => { type => ARRAYREF, optional => 1 },
|
||
f9f7b56e | Sven Schöling | dispatch => { type => HASHREF, optional => 1 },
|
||
2f6ebd89 | Sven Schöling | file => 1,
|
||
encoding => 0,
|
||||
class => 0,
|
||||
numberformat => 0,
|
||||
dateformat => 0,
|
||||
});
|
||||
my $self = bless {}, $class;
|
||||
$self->$_($params{$_}) for keys %params;
|
||||
$self->_io(IO::File->new);
|
||||
$self->_csv(Text::CSV->new({
|
||||
binary => 1,
|
||||
8fba112b | Sven Schöling | sep_char => $self->sep_char,
|
||
quote_char => $self->quote_char,
|
||||
escape_char => $self->escape_char,
|
||||
2f6ebd89 | Sven Schöling | |||
}));
|
||||
8fba112b | Sven Schöling | $self->_errors([]);
|
||
2f6ebd89 | Sven Schöling | |||
return $self;
|
||||
}
|
||||
sub parse {
|
||||
my ($self, %params) = @_;
|
||||
$self->_open_file;
|
||||
return unless $self->_check_header;
|
||||
return unless $self->_parse_data;
|
||||
$self->_parsed(1);
|
||||
return $self;
|
||||
}
|
||||
sub get_data {
|
||||
$_[0]->_data;
|
||||
}
|
||||
sub get_objects {
|
||||
my ($self, %params) = @_;
|
||||
croak 'no class given' unless $self->class;
|
||||
croak 'must parse first' unless $self->_parsed;
|
||||
$self->_make_objects unless $self->_objects;
|
||||
return wantarray ? @{ $self->_objects } : $self->_objects;
|
||||
}
|
||||
8fba112b | Sven Schöling | sub errors {
|
||
@{ $_[0]->_errors }
|
||||
}
|
||||
f9f7b56e | Sven Schöling | sub check_header {
|
||
$_[0]->_check_header;
|
||||
}
|
||||
2f6ebd89 | Sven Schöling | # private stuff
|
||
sub _open_file {
|
||||
my ($self, %params) = @_;
|
||||
$self->encoding($self->_guess_encoding) if !$self->encoding;
|
||||
$self->_io->open($self->file, '<' . $self->_encode_layer)
|
||||
or die "could not open file " . $self->file;
|
||||
return $self->_io;
|
||||
}
|
||||
sub _check_header {
|
||||
my ($self, %params) = @_;
|
||||
return $self->header if $self->header;
|
||||
my $header = $self->_csv->getline($self->_io);
|
||||
$self->header($header);
|
||||
}
|
||||
f9f7b56e | Sven Schöling | sub _check_header_for_class {
|
||
my ($self, %params) = @_;
|
||||
my @errors;
|
||||
return unless $self->class;
|
||||
return $self->header;
|
||||
for my $method (@{ $self->header }) {
|
||||
next if $self->class->can($self->_real_method($method));
|
||||
push @errors, [
|
||||
$method,
|
||||
undef,
|
||||
"header field $method is not recognized",
|
||||
undef,
|
||||
0,
|
||||
];
|
||||
}
|
||||
$self->_push_error(@errors);
|
||||
return ! @errors;
|
||||
}
|
||||
2f6ebd89 | Sven Schöling | sub _parse_data {
|
||
my ($self, %params) = @_;
|
||||
8fba112b | Sven Schöling | my (@data, @errors);
|
||
2f6ebd89 | Sven Schöling | |||
$self->_csv->column_names(@{ $self->header });
|
||||
8fba112b | Sven Schöling | while (1) {
|
||
my $row = $self->_csv->getline($self->_io);
|
||||
last if $self->_csv->eof;
|
||||
if ($row) {
|
||||
my %hr;
|
||||
@hr{@{ $self->header }} = @$row;
|
||||
push @data, \%hr;
|
||||
} else {
|
||||
push @errors, [
|
||||
$self->_csv->error_input,
|
||||
$self->_csv->error_diag,
|
||||
$self->_io->input_line_number,
|
||||
];
|
||||
}
|
||||
}
|
||||
2f6ebd89 | Sven Schöling | |||
$self->_data(\@data);
|
||||
f9f7b56e | Sven Schöling | $self->_push_error(@errors);
|
||
8fba112b | Sven Schöling | |||
f9f7b56e | Sven Schöling | return ! @errors;
|
||
2f6ebd89 | Sven Schöling | }
|
||
sub _encode_layer {
|
||||
':encoding(' . $_[0]->encoding . ')';
|
||||
}
|
||||
sub _make_objects {
|
||||
my ($self, %params) = @_;
|
||||
my @objs;
|
||||
eval "require " . $self->class;
|
||||
local $::myconfig{numberformat} = $self->numberformat if $self->numberformat;
|
||||
local $::myconfig{dateformat} = $self->dateformat if $self->dateformat;
|
||||
for my $line (@{ $self->_data }) {
|
||||
push @objs, $self->class->new(
|
||||
map {
|
||||
f9f7b56e | Sven Schöling | $self->_real_method($_) => $line->{$_}
|
||
2f6ebd89 | Sven Schöling | } grep { $_ } keys %$line
|
||
);
|
||||
}
|
||||
$self->_objects(\@objs);
|
||||
}
|
||||
f9f7b56e | Sven Schöling | sub _real_method {
|
||
my ($self, $arg) = @_;
|
||||
($self->dispatch && $self->dispatch->{$arg}) || $arg;
|
||||
}
|
||||
2f6ebd89 | Sven Schöling | sub _guess_encoding {
|
||
# won't fix
|
||||
'utf-8';
|
||||
}
|
||||
f9f7b56e | Sven Schöling | sub _push_error {
|
||
my ($self, @errors) = @_;
|
||||
my @new_errors = ($self->errors, @errors);
|
||||
$self->_errors(\@new_errors);
|
||||
}
|
||||
2f6ebd89 | Sven Schöling | |||
1;
|
||||
__END__
|
||||
8fba112b | Sven Schöling | =encoding utf-8
|
||
2f6ebd89 | Sven Schöling | =head1 NAME
|
||
SL::Helper::Csv - take care of csv file uploads
|
||||
=head1 SYNOPSIS
|
||||
use SL::Helper::Csv;
|
||||
my $csv = SL::Helper::Csv->new(
|
||||
file => \$::form->{upload_file},
|
||||
encoding => 'utf-8', # undef means utf8
|
||||
sep_char => ',', # default ';'
|
||||
quote_char => ''', # default '"'
|
||||
header => [qw(id text sellprice word)] # see later
|
||||
f9f7b56e | Sven Schöling | dispatch => { sellprice => 'sellprice_as_number' }
|
||
2f6ebd89 | Sven Schöling | class => 'SL::DB::CsvLine', # if present, map lines to this
|
||
)
|
||||
my $status = $csv->parse;
|
||||
8fba112b | Sven Schöling | my $hrefs = $csv->get_data;
|
||
2f6ebd89 | Sven Schöling | my @objects = $scv->get_objects;
|
||
=head1 DESCRIPTION
|
||||
See Synopsis.
|
||||
Text::CSV offeres already good functions to get lines out of a csv file, but in
|
||||
most cases you will want those line to be parsed into hashes or even objects,
|
||||
so this model just skips ahead and gives you objects.
|
||||
f9f7b56e | Sven Schöling | Encoding autodetection is not easy, and should not be trusted. Try to avoid it
|
||
if possible.
|
||||
2f6ebd89 | Sven Schöling | |||
=head1 METHODS
|
||||
=over 4
|
||||
=item C<new> PARAMS
|
||||
Standard constructor. You can use this to set most of the data.
|
||||
=item C<parse>
|
||||
Do the actual work. Will return true ($self actually) if success, undef if not.
|
||||
=item C<get_objects>
|
||||
Parse the data into objects and return those.
|
||||
8fba112b | Sven Schöling | This method will return list or arrayref depending on context.
|
||
2f6ebd89 | Sven Schöling | =item C<get_data>
|
||
Returns an arrayref of the raw lines as hashrefs.
|
||||
8fba112b | Sven Schöling | =item C<errors>
|
||
Return all errors that came up druing parsing. See error handling for detailed
|
||||
information.
|
||||
=back
|
||||
=head1 PARAMS
|
||||
=over 4
|
||||
2f6ebd89 | Sven Schöling | =item C<file>
|
||
The file which contents are to be read. Can be a name of a physical file or a
|
||||
scalar ref for memory data.
|
||||
=item C<encoding>
|
||||
f9f7b56e | Sven Schöling | Encoding of the CSV file. Note that this module does not do any encoding
|
||
guessing. Know what your data ist. Defaults to utf-8.
|
||||
2f6ebd89 | Sven Schöling | |||
=item C<sep_char>
|
||||
=item C<quote_char>
|
||||
8fba112b | Sven Schöling | =item C<escape_char>
|
||
2f6ebd89 | Sven Schöling | Same as in L<Text::CSV>
|
||
=item C<header> \@FIELDS
|
||||
f9f7b56e | Sven Schöling | Can be an array of columns, in this case the first line is not used as a
|
||
2f6ebd89 | Sven Schöling | header. Empty header fields will be ignored in objects.
|
||
f9f7b56e | Sven Schöling | =item C<dispatch> \%ACCESSORS
|
||
2f6ebd89 | Sven Schöling | |||
May be used to map header fields to custom accessors. Example:
|
||||
{ listprice => listprice_as_number }
|
||||
In this case C<listprice_as_number> will be used to read in values from the
|
||||
C<listprice> column.
|
||||
=item C<class>
|
||||
If present, the line will be handed to the new sub of this class,
|
||||
and the return value used instead of the line itself.
|
||||
=back
|
||||
8fba112b | Sven Schöling | =head1 ERROR HANDLING
|
||
After parsing a file all errors will be accumulated into C<errors>.
|
||||
Each entry is an arrayref with the following structure:
|
||||
[
|
||||
offending raw input,
|
||||
f9f7b56e | Sven Schöling | Text::CSV error code if T:C error, 0 else,
|
||
error diagnostics,
|
||||
8fba112b | Sven Schöling | position in line,
|
||
estimated line in file,
|
||||
]
|
||||
Note that the last entry can be off, but will give an estimate.
|
||||
=head1 CAVEATS
|
||||
=over 4
|
||||
=item *
|
||||
sep_char, quote_char, and escape_char are passed to Text::CSV on creation.
|
||||
Changing them later has no effect currently.
|
||||
=item *
|
||||
Encoding errors are not dealt with properly.
|
||||
=back
|
||||
2f6ebd89 | Sven Schöling | |||
f9f7b56e | Sven Schöling | =head1 TODO
|
||
Dispatch to child objects, like this:
|
||||
$csv = SL::Helper::Csv->new(
|
||||
file => ...
|
||||
class => SL::DB::Part,
|
||||
dispatch => [
|
||||
makemodel => {
|
||||
make_1 => make,
|
||||
model_1 => model,
|
||||
},
|
||||
makemodel => {
|
||||
make_2 => make,
|
||||
model_2 => model,
|
||||
},
|
||||
]
|
||||
);
|
||||
2f6ebd89 | Sven Schöling | =head1 AUTHOR
|
||
8fba112b | Sven Schöling | Sven Schöling E<lt>s.schoeling@linet-services.deE<gt>
|
||
2f6ebd89 | Sven Schöling | =cut
|