File Coverage

blib/lib/Convert/Cisco.pm
Criterion Covered Total %
statement 24 99 24.2
branch 0 28 0.0
condition n/a
subroutine 8 13 61.5
pod 2 2 100.0
total 34 142 23.9


line stmt bran cond sub pod time code
1             #=============================================================
2            
3             package Convert::Cisco;
4            
5 1     1   22979 use warnings;
  1         2  
  1         42  
6 1     1   5 use strict;
  1         2  
  1         35  
7            
8 1     1   881 use FileHandle;
  1         13017  
  1         6  
9 1     1   470 use File::Basename;
  1         2  
  1         83  
10 1     1   1545 use Log::Log4perl qw(get_logger);
  1         82010  
  1         8  
11 1     1   941 use YAML qw(Dump Load);
  1         11265  
  1         72  
12 1     1   1445 use DateTime;
  1         239938  
  1         45  
13 1     1   1199 use XML::Writer;
  1         8013  
  1         1147  
14            
15             =head1 NAME
16            
17             Convert::Cisco - Module for converting Cisco billing records
18            
19             =head1 VERSION
20            
21             Version 0.06
22            
23             =cut
24            
25             our $VERSION = '0.06';
26            
27             =head1 SYNOPSIS
28            
29             Convert Cisco billing record binary files. The format is available on the
30             Cisco Website:
31            
32             http://www.cisco.com/univercd/cc/td/doc/product/access/sc/rel9/billinf/r9chap1.htm
33            
34             Module used to convert Cisco billing records into XML
35            
36             use Convert::Cisco;
37            
38             my $obj = Convert::Cisco->new(stylesheet=>"cisco.xsl");
39             $obj->to_xml("test.bin", "test.xml");
40            
41             =head1 FUNCTIONS
42            
43             =cut
44            
45             #-------------------------------------------------------------
46            
47             =head2 new
48            
49             Constructor method, has the following optional parameters:
50            
51             =over
52            
53             =item stylesheet
54            
55             Adds a xml-stylesheet processing instruction to the top of all converted XML files
56            
57            
58            
59             =item config
60            
61             Billing record configuration data expressed in YAML format. This option is normally
62             only used by the tests because it over-rides the modules field configuration.
63            
64             =back
65            
66             =cut
67            
68             sub new {
69 0     0 1   my $class = shift;
70 0           my (%args) = @_;
71 0           my $self = bless {stylesheet=>undef, config=>undef, %args}, $class;
72            
73             ### Load record configuration
74 0 0         if (defined $self->{config}) {
75 0           $self->{_config} = Load($self->{config});
76             }
77             else {
78 0           $self->{_config} = Load(join("\n", ));
79             }
80            
81             ### Default configuration
82 0 0         unless (exists $self->{_config}{"CDE Records"}{6003}) {
83 0           $self->{_config}{"CDE Records"}{6003} = {name=>"record_count", spec=>"N"};
84             }
85            
86 0           return $self;
87             }
88            
89             #-------------------------------------------------------------
90            
91             # _decodeCDB
92             #
93             # Returns the configured name for the CDB record
94             #
95            
96             sub _decodeCDB {
97 0     0     my ($self, $key) = @_;
98 0           my $log = get_logger;
99            
100 0 0         if (exists $self->{_config}{"CDB Names"}{$key}) {
101 0           return $self->{_config}{"CDB Names"}{$key};
102             }
103             else {
104 0           $log->warn("CDB not configured: ", $key);
105 0           return "UNKNOWN"
106             }
107             }
108            
109             #-------------------------------------------------------------
110            
111             # _decodeCDE
112             #
113             # Unpacks the CDE record based on the configured specification
114             #
115            
116             sub _decodeCDE {
117 0     0     my ($self, $key, $value) = @_;
118 0           my $log = get_logger;
119 0           my $decodeValue;
120             my $decodeName;
121 0           my $decodeValueUnformatted;
122            
123 0 0         if (exists $self->{_config}{"CDE Records"}{$key}) {
124            
125 0           $decodeName = $self->{_config}{"CDE Records"}{$key}{name};
126 0           my $spec = $self->{_config}{"CDE Records"}{$key}{spec};
127 0           my $format = $self->{_config}{"CDE Records"}{$key}{format};
128            
129             ### Handling for multi-part records
130 0 0         if (ref($spec) eq "ARRAY") {
131 0           $decodeValue = join("-", unpack(join(" ", @{$spec}), $value));
  0            
132             }
133             else {
134 0           $decodeValue = unpack($spec, $value);
135             }
136            
137             ### Optional output formatting
138 0 0         if (defined $format) {
139 0           $decodeValueUnformatted = $decodeValue;
140            
141 0 0         if ($format eq "epoch2datetime") {
    0          
142 0           $decodeValue = DateTime->from_epoch(epoch => $decodeValueUnformatted)->datetime;
143             }
144             elsif ($format eq "compoundEpoch2datetime") {
145 0           my @timeComponents = split("-", $decodeValueUnformatted);
146 0           $decodeValue = DateTime->from_epoch(epoch => $timeComponents[0])->datetime.".".$timeComponents[1];
147             }
148             else {
149 0           $log->warn("Unsupported format configured for CDE: $key");
150             }
151             }
152             }
153             else {
154 0           $log->warn("CDE not configured: ", $key);
155 0           $decodeName = "UNKNOWN";
156 0           $decodeValue = unpack("H*", $value);
157             }
158            
159 0           return ($decodeValue, $decodeName, $decodeValueUnformatted);
160             }
161            
162             #-------------------------------------------------------------
163            
164             # _stylesheetDecl
165             #
166             # Write XSL stylesheet declaration
167             #
168            
169             sub _stylesheetDecl {
170 0     0     my ($self, $writer) = @_;
171            
172 0 0         if (defined $self->{stylesheet}) {
173 0           $writer->pi('xml-stylesheet', sprintf('href="%s" type="%s"', $self->{stylesheet}, "text/xsl"));
174             }
175             }
176            
177             #-------------------------------------------------------------
178            
179             =head2 to_xml
180            
181             Converts a file into XML format. The current record format is:
182            
183            
184            
185             ..
186             ..
187            
188             0a
189             8090a3
190             02
191             ..
192             ..
193            
194             ..
195             ..
196            
197             B
198            
199             The XML format is subject to change and needs an associated XML DTD or Schema.
200            
201             =cut
202            
203             sub to_xml {
204 0     0 1   my ($self, $filename, $filename_output) = @_;
205 0           my $log = get_logger;
206            
207             ### Print the name of the file processed
208 0           $log->debug("Processing: ", $filename);
209            
210             ### Input file
211 0 0         my $infile = new FileHandle($filename, "r") or $log->logcroak("Cannot open $filename - $!");
212 0           binmode $infile;
213            
214             ### Output file
215 0 0         my $file = new FileHandle($filename_output, "w") or $log->logcroak("Cannot open $filename_output - $!");
216            
217             ### XML Writer object
218 0           my $writer = XML::Writer->new(OUTPUT => $file, DATA_MODE=>1, DATA_INDENT=>2);
219            
220             ### Write the start of the file
221 0           $writer->xmlDecl("UTF-8");
222 0           $self->_stylesheetDecl($writer);
223 0           $writer->startTag("cdrs");
224            
225             ### Convert the file into CSV format
226 0           my $bin;
227 0           my $i = 0;
228 0           my $recordCount = 0;
229            
230 0           while ($infile->read($bin, 4)) {
231 0           $i++;
232            
233             ### Read the Call Data Block
234 0           my ($cdbTag, $length) = unpack("n2", $bin);
235 0           $infile->read($bin, $length);
236            
237             ### Decode the Call Data Elements
238             # TLV format : Tag, Length, Value
239 0           my %cde = unpack("(n n/a*)*", $bin);
240            
241             ### Dump the CDB record
242 0           $log->debug("CDB Record:\n", { filter => \&Dump, value => [$cdbTag, \%cde] });
243            
244             ### Start the "cdb" block
245 0           $writer->startTag("cdb", tag => $cdbTag, name => $self->_decodeCDB($cdbTag));
246            
247 0           foreach my $cdeTag ( sort keys %cde ) {
248 0           my ($value, $name, $raw) = $self->_decodeCDE($cdeTag, $cde{$cdeTag});
249            
250 0 0         if (defined $raw) {
251 0           $writer->dataElement("cde", $value, tag=>$cdeTag, name=>$name, raw=>$raw);
252             }
253             else {
254 0           $writer->dataElement("cde", $value, tag=>$cdeTag, name=>$name);
255             }
256             }
257            
258             ### End "cdb" block
259 0           $writer->endTag("cdb");
260            
261             ### Read number of records from Footer record
262 0 0         if ($cdbTag == 1100) {
263 0           ($recordCount) = $self->_decodeCDE(6003, $cde{6003});
264             }
265             }
266            
267             ### Audit check
268 0 0         if ($i != $recordCount) {
269 0           $log->logcroak("Footer does not match number of records");
270             }
271            
272             ### Cleanup
273 0           $infile->close;
274 0           $writer->endTag("cdrs");
275 0           $file->print("\n");
276 0           $file->close;
277             }
278            
279             #-------------------------------------------------------------
280            
281             =head1 AUTHOR
282            
283             Mark O'Connor, C<< >>
284            
285             =head1 BUGS
286            
287             Please report any bugs or feature requests to
288             C, or through the web interface at
289             L.
290             I will be notified, and then you'll automatically be notified of progress on
291             your bug as I make changes.
292            
293             =head1 SUPPORT
294            
295             You can find documentation for this module with the perldoc command.
296            
297             perldoc Convert::Cisco
298            
299             You can also look for information at:
300            
301             =over 4
302            
303             =item * AnnoCPAN: Annotated CPAN documentation
304            
305             L
306            
307             =item * CPAN Ratings
308            
309             L
310            
311             =item * RT: CPAN's request tracker
312            
313             L
314            
315             =item * Search CPAN
316            
317             L
318            
319             =back
320            
321             =head1 ACKNOWLEDGEMENTS
322            
323             =head1 COPYRIGHT & LICENSE
324            
325             Copyright 2007 Mark O'Connor, all rights reserved.
326            
327             This program is free software; you can redistribute it and/or modify it
328             under the same terms as Perl itself.
329            
330             =cut
331            
332             1; # End of Convert::Cisco
333            
334             __DATA__