ce57809161bc1758f7f16d33330855052f8bb25f
[kivitendo-erp.git] / SL / Helper / Csv / Dispatcher.pm
1 package SL::Helper::Csv::Dispatcher;
2
3 use strict;
4
5 use Data::Dumper;
6 use Carp;
7 use Scalar::Util qw(weaken);
8 use Rose::Object::MakeMethods::Generic scalar => [ qw(
9   _specs _errors
10 ) ];
11
12 use SL::Helper::Csv::Error;
13
14 sub new {
15   my ($class, $parent) = @_;
16   my $self = bless { }, $class;
17
18   weaken($self->{_csv} = $parent);
19   $self->_errors([]);
20
21   return $self;
22 }
23
24 sub dispatch {
25   my ($self, $line) = @_;
26
27   my $class = $self->_class_by_line($line);
28   croak 'no class given' unless $class;
29
30   eval "require " . $class;
31   my $obj = $class->new;
32
33   my $specs = $self->_specs_by_line($line);
34   for my $spec (@{ $specs }) {
35     $self->apply($obj, $spec, $line->{$spec->{key}});
36   }
37
38   return $obj;
39 }
40
41 # return class for given line
42 # if only one profile is given, return this profiles class
43 # if more than one profile is given, identify class by first
44 # column???
45 sub _class_by_line {
46   my ($self, $line) = @_;
47
48   my $class;
49   if ($self->_csv->is_multiplexed) {
50     foreach my $p (@{ $self->_csv->profile }) {
51       my $row_ident = $p->{row_ident};
52       if ($line->{datatype} eq $row_ident) {
53         $class = $p->{class};
54         last;
55       }
56     }
57   } else {
58     $class = $self->_csv->profile->[0]->{class};
59   }
60
61   return $class;
62 }
63
64 sub _specs_by_line {
65   my ($self, $line) = @_;
66
67   my $spec;
68   my $i = 0;
69   if ($self->_csv->is_multiplexed) {
70     foreach my $p (@{ $self->_csv->profile }) {
71       my $row_ident = $p->{row_ident};
72       if ($line->{datatype} eq $row_ident) {
73         $spec = $self->_specs->[$i];
74         last;
75       }
76       $i++;
77     }
78   } else {
79     $spec = $self->_specs->[0];
80   }
81
82   return $spec;
83 }
84
85
86 sub apply {
87   my ($self, $obj, $spec, $value) = @_;
88   return unless $value;
89
90   for my $step (@{ $spec->{steps} }) {
91     my ($acc, $class, $index) = @$step;
92     if ($class) {
93
94       # autovifify
95       if (defined $index) {
96         if (! $obj->$acc || !$obj->$acc->[$index]) {
97           my @objects = $obj->$acc;
98           $obj->$acc(@objects, map { $class->new } 0 .. $index - @objects);
99         }
100         $obj = $obj->$acc->[$index];
101       } else {
102         if (! $obj->$acc) {
103           $obj->$acc($class->new);
104         }
105         $obj = $obj->$acc;
106       }
107
108     } else {
109       $obj->$acc($value);
110     }
111   }
112 }
113
114 sub is_known {
115   my ($self, $col) = @_;
116   return grep { $col eq $_->{key} } $self->_specs;
117 }
118
119 sub parse_profile {
120   my ($self, %params) = @_;
121
122   my $profile;
123   my $class;
124   my $header;
125   my @specs;
126
127   my $i = 0;
128   foreach my $h (@{ $self->_csv->header }) {
129     $header = $h;
130     if ($self->_csv->profile) {
131       $profile = $self->_csv->profile->[$i]->{profile};
132       $class   = $self->_csv->profile->[$i]->{class};
133     }
134
135     my $spec = $self->_parse_profile(profile => $profile,
136                                      class   => $class,
137                                      header  => $header);
138     push @specs, $spec;
139     $i++;
140   }
141
142   $self->_specs(\@specs);
143
144   return ! $self->errors;
145 }
146
147 sub _parse_profile {
148   my ($self, %params) = @_;
149
150   my $profile = $params{profile};
151   my $class   = $params{class};
152   my $header  = $params{header};
153
154   my @specs;
155
156   for my $col (@$header) {
157     next unless $col;
158     if ($self->_csv->strict_profile) {
159       if (exists $profile->{$col}) {
160         push @specs, $self->make_spec($col, $profile->{$col}, $class);
161       } else {
162         $self->unknown_column($col, undef);
163       }
164     } else {
165       if (exists $profile->{$col}) {
166         push @specs, $self->make_spec($col, $profile->{$col}, $class);
167       } else {
168         push @specs, $self->make_spec($col, $col, $class);
169       }
170     }
171   }
172
173   $self->_csv->_push_error($self->errors);
174
175   return \@specs;
176 }
177
178 sub make_spec {
179   my ($self, $col, $path, $cur_class) = @_;
180
181   my $spec = { key => $col, steps => [] };
182
183   return unless $path;
184
185   return unless $cur_class;
186
187   for my $step_index ( split /\.(?!\d)/, $path ) {
188     my ($step, $index) = split /\./, $step_index;
189     if ($cur_class->can($step)) {
190       if (my $rel = $cur_class->meta->relationship($step)) { #a
191         if ($index && ! $rel->isa('Rose::DB::Object::Metadata::Relationship::OneToMany')) {
192           $self->_push_error([
193             $path,
194             undef,
195             "Profile path error. Indexed relationship is not OneToMany around here: '$step_index'",
196             undef,
197             0,
198           ]);
199           return;
200         } else {
201           my $next_class = $cur_class->meta->relationship($step)->class;
202           push @{ $spec->{steps} }, [ $step, $next_class, $index ];
203           $cur_class = $next_class;
204           eval "require $cur_class; 1" or die "could not load class '$cur_class'";
205         }
206       } else { # simple dispatch
207         push @{ $spec->{steps} }, [ $step ];
208         last;
209       }
210     } else {
211       $self->unknown_column($col, $path);
212     }
213   }
214
215   return $spec;
216 }
217
218 sub unknown_column {
219   my ($self, $col, $path) = @_;
220   return if $self->_csv->ignore_unknown_columns;
221
222   $self->_push_error([
223     $col,
224     undef,
225     "header field '$col' is not recognized",
226     undef,
227     0,
228   ]);
229 }
230
231 sub _csv {
232   $_[0]->{_csv};
233 }
234
235 sub errors {
236   @{ $_[0]->_errors }
237 }
238
239 sub _push_error {
240   my ($self, @errors) = @_;
241   my @new_errors = ($self->errors, map { SL::Helper::Csv::Error->new(@$_) } @errors);
242   $self->_errors(\@new_errors);
243 }
244
245 1;