File Coverage

blib/lib/Business/Edifact/Interchange.pm
Criterion Covered Total %
statement 114 133 85.7
branch 20 34 58.8
condition 1 2 50.0
subroutine 17 19 89.4
pod 13 13 100.0
total 165 201 82.0


line stmt bran cond sub pod time code
1             package Business::Edifact::Interchange;
2              
3 6     6   187921 use warnings;
  6         16  
  6         222  
4 6     6   38 use strict;
  6         11  
  6         203  
5 6     6   297 use 5.010;
  6         27  
  6         289  
6 6     6   35 use Carp;
  6         12  
  6         941  
7 6     6   7477 use Encode;
  6         104744  
  6         1082  
8 6     6   5595 use Business::Edifact::Message;
  6         21  
  6         21209  
9              
10             =head1 NAME
11              
12             Business::Edifact::Interchange - Parse Edifact Messages For Book Ordering
13              
14             =head1 VERSION
15              
16             Version 0.07
17              
18             =cut
19              
20             our $VERSION = '0.07';
21              
22             # UNOA and UNOB "correspond to the basic ascii sets of iso 646 and iso 6937"
23             # Version 4 of edifact should extend this to unicode
24             my %encoding_map = (
25             'UNOA' => 'ascii',
26             'UNOB' => 'ascii',
27             'UNOC' => 'iso-8859-1',
28             'UNOD' => 'iso-8859-2',
29             'UNOE' => 'iso-8859-5',
30             'UNOF' => 'iso-8859-7',
31             );
32              
33             =head1 SYNOPSIS
34              
35             This is a support module for EDI ordering modules being developed for the
36             Koha and Evergreen OS Library Management Systems
37              
38             use Business::Edifact::Interchange;
39              
40             my $foo = Business::Edifact::Interchange->new();
41             $foo->parse($edifact_message);
42             or
43             $foo->parse_file($filename);
44             ...
45              
46             The standards for using Edifact in Library Book Supply are available from
47              
48             www.editeur.org
49              
50              
51             =head1 SUBROUTINES/METHODS
52              
53             =head2 new
54              
55             Create an Business::Edifact::Interchange object
56              
57             =cut
58              
59             sub new {
60 8     8 1 4362 my $class = shift;
61 8         20 my $print_trace = shift;
62              
63 8         22 my $self = {};
64              
65 8         28 bless $self, $class;
66 8         26 return $self;
67             }
68              
69             =head2 parse
70              
71             parse the edifact interchange passed in the message
72              
73             =cut
74              
75             sub parse {
76 7     7 1 17 my $self = shift;
77 7         14 my $doc = shift;
78 7         92 $self->{separator} = {
79             component => q{\:},
80             data => q{\+},
81             decimal => q{.},
82             release => q{\?},
83             reserved => q{ },
84             segment => q{\'},
85             };
86 7         17 $self->{sep_class} = '\:\+\'\?';
87 7 50       113 if ( $doc =~ s/^UNA// ) { # optional
88 7         27 my $element = substr $doc, 0, 6, q{};
89 7         31 $self->read_service_string_advice($element);
90             }
91 7         1240 my @segments =
92             split /(?{separator}->{release})$self->{separator}->{segment} */,
93             $doc;
94 7         62 $self->{interchange} = [];
95 7         24 $self->{messages} = [];
96 7         46 $self->{msg_cnt} = 0;
97 7 50       43 if ( $segments[0] =~ m/^UNB/ ) {
98              
99 7         16 my $hdr = shift @segments;
100 7         121 my @hdr_fields =
101             split /(?{separator}->{release})$self->{separator}->{data}/,
102             $hdr;
103 7         15 push @{ $self->{interchange} },
  7         53  
104             $self->interchange_header( @hdr_fields[ 1 .. $#hdr_fields ] );
105             }
106             else {
107 0         0 croak 'Interchange does not begin with an Interchange header';
108             }
109 7         14 my $current_msg;
110 7         29 while ( my $segment = shift @segments ) {
111 1362         8914 my ( $tag, @data ) =
112             split /(?{separator}->{release})$self->{separator}->{data}/,
113             $segment;
114 1362 100       3317 if ( $tag =~ /UNH/ ) {
115 8         50 $current_msg = $self->message_header(@data);
116 8         33 next;
117             }
118 1354 100       2493 if ( $tag =~ /UNT/ ) {
119 8         33 $self->message_trailer( $current_msg, @data );
120 8         26 $current_msg = undef;
121 8         32 next;
122             }
123 1346 100       2337 if ( $tag =~ /^UNZ/ ) {
124 6         25 $self->interchange_trailer(@data);
125 6         22 next;
126             }
127 1340 50       2125 if ( $tag =~ /^UNG/ ) {
128 0         0 $self->message_group_header(@data);
129 0         0 next;
130             }
131 1340 50       2150 if ( $tag =~ /^UNE/ ) {
132 0         0 $self->message_group_trailer(@data);
133 0         0 next;
134             }
135              
136 1340         2595 $self->user_data_segment( $current_msg, $tag, @data );
137             }
138 7         37 return;
139             }
140              
141             =head2 parse_file
142              
143             Reads an edifact message from a file and parses it
144             Will strip the lineendings added to files by some suppliers
145              
146             =cut
147              
148             sub parse_file {
149 6     6 1 41 my $self = shift;
150 6         127 my $filename = shift;
151 6   50     29 $filename ||= 'SampleQuote3.txt';
152              
153 6 50       382 open my $fh, '<', $filename or croak "Cannot open $filename : $!";
154 6         1122 my @lines = <$fh>;
155 6         148 close $fh;
156              
157 6         22 for (@lines) {
158 1315         1282 chomp;
159 1315         2840 s/\r$//;
160             }
161 6         13 my $msg;
162 6 50       57 if ( @lines == 1 ) {
    50          
163 0         0 $msg = $lines[0];
164             }
165             elsif ( @lines > 1 ) {
166 6         175 $msg = join q{}, @lines;
167             }
168 6 50       24 if ($msg) {
169 6         29 $self->parse($msg);
170             }
171 6         163 return;
172             }
173              
174             =head2 user_data_segment
175              
176             internal method for handling message data segments
177             pass to the current Business::Edifact::Message object for fuller passing
178              
179             =cut
180              
181             sub user_data_segment {
182 1340     1340 1 3045 my ( $self, $msg, $tag, @data ) = @_;
183              
184 1340         2279 my $d = $self->split_components(@data);
185              
186             # un release data
187 1340         3755 $msg->add_segment( $tag, $d );
188              
189 1340         6135 return;
190             }
191              
192             =head2 message_header
193              
194             create a new Business::Edifact::Message object
195              
196             =cut
197              
198             sub message_header {
199 8     8 1 24 my ( $self, @data ) = @_;
200              
201 8         25 my $msg = Business::Edifact::Message->new( $self->split_components(@data) );
202              
203 8         24 return $msg;
204             }
205              
206             =head2 message_trailer
207              
208             End message add completed message to my messages array
209              
210             =cut
211              
212             sub message_trailer {
213 8     8 1 22 my ( $self, $msg, @data ) = @_;
214              
215 8         20 $self->{msg_cnt}++;
216 8         18 push @{ $self->{messages} }, $msg;
  8         20  
217 8         15 return;
218             }
219              
220             =head2 interchange_trailer
221              
222             internal method to parse and validate the
223             interchange trailer
224              
225             =cut
226              
227             sub interchange_trailer {
228 6     6 1 22 my ( $self, @data ) = @_;
229              
230 6 50       28 if ( $data[0] != $self->{msg_cnt} ) {
231 0         0 carp "Message count error trailer says $data[0] I counted "
232             . $self->{msg_cnt};
233             }
234 6 50       29 if ( $data[1] ne $self->{control_ref} ) {
235              
236 0         0 carp 'Error mismatched control refs Header:'
237             . $self->{control_ref}
238             . ' Trailer:'
239             . $data[1];
240             }
241              
242 6         11 return;
243             }
244              
245             =head2 read_service_string_advice
246              
247             internal method to parse the service string advice
248             and set the separator values for the interchange
249             accordingly
250              
251             =cut
252              
253             sub read_service_string_advice {
254 7     7 1 14 my $self = shift;
255 7         14 my $ssa = shift;
256              
257             # The six characters represent
258             # component data element separator :
259             # Data element sep +
260             # Decimal notation .
261             # Release Indicator ?
262             # Reseved (space)
263             # Segment terminator '
264 7 50       25 if ( $ssa eq q{:+.? '} ) {
265              
266             # 'Standard Service String Advice';
267 7         18 return;
268             }
269             else {
270              
271             #'Non standard Service String Advice';
272 0         0 my @char = unpack 'C6', $ssa;
273 0         0 foreach (@char) {
274 0         0 $_ = quotemeta $_;
275             }
276 0         0 $self->{separator} = {
277             component => $char[0],
278             data => $char[1],
279             decimal => $char[2],
280             release => $char[3],
281             reserved => $char[4],
282             segment => $char[5],
283             };
284 0         0 $self->{sep_class} = join q{}, $char[0], $char[1], $char[3], $char[5];
285             }
286 0         0 return;
287             }
288              
289             =head2 split_components
290              
291             internal method to split data field into components
292              
293             =cut
294              
295             sub split_components {
296 1355     1355 1 2312 my ( $self, @data ) = @_;
297 1355         1819 my $d_arr = [];
298 1355         1986 for my $data_field (@data) {
299 3438         14977 my @components =
300             split
301             /(?{separator}->{release})$self->{separator}->{component}/,
302             $data_field;
303 3438         5466 foreach (@components) {
304 6196         16587 s/$self->{separator}->{release}([$self->{sep_class}])/$1/g;
305              
306             # convert data to utf-8
307 6196         19742 $_ = $self->{enc}->decode($_);
308             }
309 3438         3773 push @{$d_arr}, \@components;
  3438         8834  
310             }
311 1355         2942 return $d_arr;
312             }
313              
314             =head2 interchange_header
315              
316             Internal method to parse the interchange header
317              
318             =cut
319              
320             sub interchange_header {
321 7     7 1 22 my ( $self, @hdr ) = @_;
322 7         19 $self->{control_ref} = $hdr[4];
323 7         14 my $charencoding = 'iso-8859-1';
324 7         22 my $syntax_id = substr $hdr[0], 0, 4;
325 7 50       31 if ( exists $encoding_map{$syntax_id} ) {
326 7         21 $charencoding = $encoding_map{$syntax_id};
327             }
328 7         44 $self->{enc} = find_encoding($charencoding);
329 7 50       188 croak qq(encoding "$charencoding" not found) unless ref $self->{enc};
330 7         31 my $interchange_header = $self->split_components(@hdr);
331              
332             # syntax identifier :: Syntax_id a4 'UNO'[ABC]:Syntax_version
333             # interchange_sender :: Sender_id:id_code_qualifier[:Address]
334             # interchange_recipient :: Recepient_id:code_qualifier[:Address]
335             # DateTime of Prep : YYMMDD:HHMM
336             # Interchange Control Ref
337             # Password
338             # Application Ref
339             # [Priority Code]
340             # [Ack Request]
341             # [Comm Agreement ID]
342             # [Test Indicator [ 1 == a test]]
343 7         33 return $interchange_header;
344             }
345              
346             =head2 message_group_header
347              
348             internal method to parse the message group header
349             (Currently a nop )
350              
351             =cut
352              
353             sub message_group_header {
354 0     0 1 0 my ( $self, @data ) = @_;
355              
356             #TBD parse data
357             #say 'message_group_header';
358              
359 0         0 return;
360             }
361              
362             =head2 message_group_trailer
363              
364             internal method to parse the message group trailer
365             (Currently a nop )
366              
367             =cut
368              
369             sub message_group_trailer {
370 0     0 1 0 my ( $self, @data ) = @_;
371              
372             #TBD parse data
373             #say 'message_group_trailer';
374              
375 0         0 return;
376             }
377              
378             =head2 messages
379              
380             Returns and array_ref of Edifact::Message objects representing
381             the contents of the interchange
382              
383             =cut
384              
385             sub messages {
386 7     7 1 42 my $self = shift;
387 7 50       30 if ( exists $self->{messages} ) {
388 7         23 return $self->{messages};
389             }
390 0           return;
391             }
392              
393             =head1 WARNINGS
394              
395             At present this is tested for quotes. Beware suppliers' interpretation of the
396             Edifact Standard can vary considerably. (And the standard is large enough to
397             allow considerable leeway on this). Its intended to expand this module based on
398             practical experience.
399              
400             =head1 AUTHOR
401              
402             Colin Campbell, C<< >>
403              
404             =head1 BUGS
405              
406             Please report any bugs or feature requests to C, or through
407             the web interface at L. I will be notified, and then you'll
408             automatically be notified of progress on your bug as I make changes.
409              
410              
411              
412              
413             =head1 SUPPORT
414              
415             You can find documentation for this module with the perldoc command.
416              
417             perldoc Business::Edifact::Interchange
418              
419              
420             =head1 ACKNOWLEDGEMENTS
421              
422              
423             =head1 LICENSE AND COPYRIGHT
424              
425             Copyright 2011-2014 Colin Campbell.
426              
427             This program is free software; you can redistribute it and/or modify it
428             under the terms of either: the GNU General Public License as published
429             by the Free Software Foundation; or the Artistic License.
430              
431             See http://dev.perl.org/licenses/ for more information.
432              
433              
434             =cut
435              
436             1; # End of Business::Edifact::Interchange