| line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
|
1
|
|
|
|
|
|
|
# This code is part of Perl distribution Business-CAMT version 0.14. |
|
2
|
|
|
|
|
|
|
# The POD got stripped from this file by OODoc version 3.05. |
|
3
|
|
|
|
|
|
|
# For contributors see file ChangeLog. |
|
4
|
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
# This software is copyright (c) 2024-2025 by Mark Overmeer. |
|
6
|
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
# This is free software; you can redistribute it and/or modify it under |
|
8
|
|
|
|
|
|
|
# the same terms as the Perl 5 programming language system itself. |
|
9
|
|
|
|
|
|
|
# SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later |
|
10
|
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
#oodist: *** DO NOT USE THIS VERSION FOR PRODUCTION *** |
|
12
|
|
|
|
|
|
|
#oodist: This file contains OODoc-style documentation which will get stripped |
|
13
|
|
|
|
|
|
|
#oodist: during its release in the distribution. You can use this file for |
|
14
|
|
|
|
|
|
|
#oodist: testing, however the code of this development version may be broken! |
|
15
|
|
|
|
|
|
|
|
|
16
|
|
|
|
|
|
|
# https://www.betaalvereniging.nl/wp-content/uploads/IG-Bank-to-Customer-Statement-CAMT-053-v1-1.pdf |
|
17
|
|
|
|
|
|
|
|
|
18
|
|
|
|
|
|
|
package Business::CAMT;{ |
|
19
|
|
|
|
|
|
|
our $VERSION = '0.14'; |
|
20
|
|
|
|
|
|
|
} |
|
21
|
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
|
|
23
|
1
|
|
|
1
|
|
1197
|
use strict; |
|
|
1
|
|
|
|
|
5
|
|
|
|
1
|
|
|
|
|
29
|
|
|
24
|
1
|
|
|
1
|
|
3
|
use warnings; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
43
|
|
|
25
|
1
|
|
|
1
|
|
4
|
use utf8; |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
8
|
|
|
26
|
|
|
|
|
|
|
|
|
27
|
1
|
|
|
1
|
|
25
|
use Log::Report 'business-camt'; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
5
|
|
|
28
|
|
|
|
|
|
|
|
|
29
|
1
|
|
|
1
|
|
709
|
use Path::Class (); |
|
|
1
|
|
|
|
|
30709
|
|
|
|
1
|
|
|
|
|
42
|
|
|
30
|
1
|
|
|
1
|
|
12
|
use XML::LibXML (); |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
13
|
|
|
31
|
1
|
|
|
1
|
|
5
|
use XML::Compile::Cache (); |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
26
|
|
|
32
|
1
|
|
|
1
|
|
7
|
use Scalar::Util qw/blessed/; |
|
|
1
|
|
|
|
|
2
|
|
|
|
1
|
|
|
|
|
88
|
|
|
33
|
1
|
|
|
1
|
|
5
|
use List::Util qw/first/; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
64
|
|
|
34
|
1
|
|
|
1
|
|
6
|
use XML::Compile::Util qw/pack_type/; |
|
|
1
|
|
|
|
|
3
|
|
|
|
1
|
|
|
|
|
47
|
|
|
35
|
|
|
|
|
|
|
|
|
36
|
1
|
|
|
1
|
|
5
|
use Business::CAMT::Message (); |
|
|
1
|
|
|
|
|
1
|
|
|
|
1
|
|
|
|
|
2520
|
|
|
37
|
|
|
|
|
|
|
|
|
38
|
|
|
|
|
|
|
my $urnbase = 'urn:iso:std:iso:20022:tech:xsd'; |
|
39
|
|
|
|
|
|
|
my $moddir = Path::Class::File->new(__FILE__)->dir; |
|
40
|
|
|
|
|
|
|
my $xsddir = $moddir->subdir('CAMT', 'xsd'); |
|
41
|
|
|
|
|
|
|
my $tagdir = $moddir->subdir('CAMT', 'tags'); |
|
42
|
0
|
|
|
0
|
|
|
sub _rootElement($) { pack_type $_[1], 'Document' } # $ns parameter |
|
43
|
|
|
|
|
|
|
|
|
44
|
|
|
|
|
|
|
# The XSD filename is like camt.052.001.12.xsd. camt.052.001.* is |
|
45
|
|
|
|
|
|
|
# expected to be incompatible with camt.052.002.*, but *.12.xsd can |
|
46
|
|
|
|
|
|
|
# usually parse *.11.xsd |
|
47
|
|
|
|
|
|
|
my %xsd_files; |
|
48
|
|
|
|
|
|
|
|
|
49
|
|
|
|
|
|
|
# Translations from abbreviated XML tags to longer names, loaded on |
|
50
|
|
|
|
|
|
|
# demand. |
|
51
|
|
|
|
|
|
|
my $tagtable; |
|
52
|
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
|
|
54
|
|
|
|
|
|
|
sub new(%) |
|
55
|
0
|
|
|
0
|
1
|
|
{ my ($class, %args) = @_; |
|
56
|
0
|
|
|
|
|
|
(bless {}, $class)->init(\%args); |
|
57
|
|
|
|
|
|
|
} |
|
58
|
|
|
|
|
|
|
|
|
59
|
|
|
|
|
|
|
sub init($) |
|
60
|
0
|
|
|
0
|
0
|
|
{ my ($self, $args) = @_; |
|
61
|
|
|
|
|
|
|
|
|
62
|
|
|
|
|
|
|
# Collect the names of all CAMT schemes in this distribution |
|
63
|
0
|
|
0
|
|
|
|
foreach my $f (grep !$_->is_dir && $_->basename =~ /\.xsd$/, $xsddir->children) |
|
64
|
0
|
0
|
|
|
|
|
{ $f->basename =~ /^camt\.([0-9]{3}\.[0-9]{3})\.([0-9]+)\.xsd$/ or panic $f; |
|
65
|
0
|
|
|
|
|
|
$xsd_files{$1}{$2} = $f->stringify; |
|
66
|
|
|
|
|
|
|
} |
|
67
|
|
|
|
|
|
|
|
|
68
|
0
|
|
0
|
|
|
|
$self->{BC_rule} = delete $args->{match_schema} || 'NEWER'; |
|
69
|
0
|
|
0
|
|
|
|
$self->{BC_big} = delete $args->{big_numbers} || 0; |
|
70
|
0
|
|
0
|
|
|
|
$self->{BC_long} = delete $args->{long_tagnames} || 0; |
|
71
|
0
|
|
|
|
|
|
$self->{RC_schemas} = XML::Compile::Cache->new; |
|
72
|
|
|
|
|
|
|
|
|
73
|
0
|
|
|
|
|
|
$self; |
|
74
|
|
|
|
|
|
|
} |
|
75
|
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
#-------------------- |
|
77
|
|
|
|
|
|
|
|
|
78
|
0
|
|
|
0
|
1
|
|
sub schemas() { $_[0]->{RC_schemas} } |
|
79
|
|
|
|
|
|
|
|
|
80
|
|
|
|
|
|
|
#-------------------- |
|
81
|
|
|
|
|
|
|
|
|
82
|
|
|
|
|
|
|
sub read($%) |
|
83
|
0
|
|
|
0
|
1
|
|
{ my ($self, $src, %args) = @_; |
|
84
|
|
|
|
|
|
|
|
|
85
|
0
|
0
|
0
|
|
|
|
my $dom |
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
86
|
|
|
|
|
|
|
= ! ref $src ? XML::LibXML->load_xml($src =~ /\<.*\>/ ? (string => $src) : (location => $src)) |
|
87
|
|
|
|
|
|
|
: $src->isa('IO::Handle') || $src->isa('GLOB') ? XML::LibXML->load_xml(IO => $src) |
|
88
|
|
|
|
|
|
|
: $src->isa('XML::LibXML::Node') ? $src |
|
89
|
|
|
|
|
|
|
: error "Unrecognized input"; |
|
90
|
|
|
|
|
|
|
|
|
91
|
0
|
0
|
|
|
|
|
my $xml = $dom->isa('XML::LibXML::Document') ? $dom->documentElement : $dom; |
|
92
|
|
|
|
|
|
|
|
|
93
|
0
|
|
|
|
|
|
my $ns = $xml->namespaceURI; |
|
94
|
0
|
0
|
|
|
|
|
my ($set, $version) = $ns =~ m!^\Q$urnbase\E:camt\.([0-9]{3}\.[0-9]{3})\.([0-9]+)$! |
|
95
|
|
|
|
|
|
|
or error __"Not a CAMT file."; |
|
96
|
|
|
|
|
|
|
|
|
97
|
0
|
0
|
|
|
|
|
my $versions = $xsd_files{$set} |
|
98
|
|
|
|
|
|
|
or error __"Not a supported CAMT message type."; |
|
99
|
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
my $xsd_version = $self->matchSchema($set, $version, rule => $args{match_schema}) |
|
101
|
0
|
0
|
|
|
|
|
or error __"No compatible schema version available."; |
|
102
|
|
|
|
|
|
|
|
|
103
|
0
|
0
|
|
|
|
|
if($xsd_version != $version) |
|
104
|
0
|
|
|
|
|
|
{ trace "Using $set schema version $xsd_version to read a version $version message."; |
|
105
|
0
|
|
|
|
|
|
$ns = "$urnbase:camt.$set.$xsd_version"; |
|
106
|
0
|
|
|
|
|
|
$xml->setNamespaceDeclURI('', $ns); |
|
107
|
|
|
|
|
|
|
} |
|
108
|
|
|
|
|
|
|
|
|
109
|
0
|
|
|
|
|
|
my $reader = $self->schemaReader($set, $xsd_version, $ns); |
|
110
|
|
|
|
|
|
|
|
|
111
|
0
|
|
|
|
|
|
Business::CAMT::Message->fromData( |
|
112
|
|
|
|
|
|
|
set => $set, |
|
113
|
|
|
|
|
|
|
version => $xsd_version, |
|
114
|
|
|
|
|
|
|
data => $reader->($xml), |
|
115
|
|
|
|
|
|
|
camt => $self, |
|
116
|
|
|
|
|
|
|
); |
|
117
|
|
|
|
|
|
|
} |
|
118
|
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
|
|
120
|
|
|
|
|
|
|
sub fromHASH($%) |
|
121
|
0
|
|
|
0
|
1
|
|
{ my ($self, $data, %args) = @_; |
|
122
|
0
|
0
|
|
|
|
|
my $type = $args{type} or panic; |
|
123
|
0
|
0
|
|
|
|
|
my ($set, $version) = $type =~ /^(?:camt\.)?([0-9]+\.[0-9]+)\.([0-9]+)$/ |
|
124
|
|
|
|
|
|
|
or error __x"Unknown message type '{type}'", type => $type; |
|
125
|
|
|
|
|
|
|
|
|
126
|
0
|
|
|
|
|
|
Business::CAMT::Message->fromData( |
|
127
|
|
|
|
|
|
|
set => $set, |
|
128
|
|
|
|
|
|
|
version => $version, |
|
129
|
|
|
|
|
|
|
data => $data, |
|
130
|
|
|
|
|
|
|
camt => $self, |
|
131
|
|
|
|
|
|
|
); |
|
132
|
|
|
|
|
|
|
} |
|
133
|
|
|
|
|
|
|
|
|
134
|
|
|
|
|
|
|
|
|
135
|
|
|
|
|
|
|
sub create($$%) |
|
136
|
0
|
|
|
0
|
1
|
|
{ my ($self, $type, $data) = @_; |
|
137
|
0
|
0
|
|
|
|
|
my ($set, $version) = $type =~ /^(?:camt\.)?([0-9]+\.[0-9]+)\.([0-9]+)$/ |
|
138
|
|
|
|
|
|
|
or error __x"Unknown message type '{type}'", type => $type; |
|
139
|
|
|
|
|
|
|
|
|
140
|
0
|
|
|
|
|
|
Business::CAMT::Message->create( |
|
141
|
|
|
|
|
|
|
set => $set, |
|
142
|
|
|
|
|
|
|
version => $version, |
|
143
|
|
|
|
|
|
|
data => $data, |
|
144
|
|
|
|
|
|
|
camt => $self, |
|
145
|
|
|
|
|
|
|
); |
|
146
|
|
|
|
|
|
|
} |
|
147
|
|
|
|
|
|
|
|
|
148
|
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
sub write($$%) |
|
150
|
0
|
|
|
0
|
1
|
|
{ my ($self, $fn, $msg, %args) = @_; |
|
151
|
|
|
|
|
|
|
|
|
152
|
0
|
|
|
|
|
|
my $set = $msg->set; |
|
153
|
0
|
0
|
|
|
|
|
my $versions = $xsd_files{$set} |
|
154
|
|
|
|
|
|
|
or error __x"Message set '{set}' is unsupported.", set => $set; |
|
155
|
|
|
|
|
|
|
|
|
156
|
0
|
|
|
|
|
|
my @versions = sort { $a <=> $b } keys %$versions; |
|
|
0
|
|
|
|
|
|
|
|
157
|
0
|
|
|
|
|
|
my $version = $msg->version; |
|
158
|
0
|
0
|
|
|
|
|
grep $version eq $_, @versions |
|
159
|
|
|
|
|
|
|
or error __x"Schema version {version} is not available, pick from {versions}.", version => $version, versions => \@versions; |
|
160
|
|
|
|
|
|
|
|
|
161
|
0
|
|
|
|
|
|
my $ns = "$urnbase:camt.$set.$version"; |
|
162
|
0
|
|
|
|
|
|
my $writer = $self->schemaWriter($set, $version, $ns); |
|
163
|
|
|
|
|
|
|
|
|
164
|
0
|
|
|
|
|
|
my $doc = XML::LibXML::Document->new('1.0', 'UTF-8'); |
|
165
|
0
|
|
|
|
|
|
my $xml = $writer->($doc, $msg); |
|
166
|
0
|
|
|
|
|
|
$doc->setDocumentElement($xml); |
|
167
|
|
|
|
|
|
|
|
|
168
|
0
|
0
|
|
|
|
|
if(ref $fn eq 'GLOB') { $doc->toFH($fn, 1) } else { $doc->toFile($fn, 1) } |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
169
|
|
|
|
|
|
|
|
|
170
|
0
|
|
|
|
|
|
$xml; |
|
171
|
|
|
|
|
|
|
} |
|
172
|
|
|
|
|
|
|
|
|
173
|
|
|
|
|
|
|
#-------------------- |
|
174
|
|
|
|
|
|
|
|
|
175
|
|
|
|
|
|
|
sub _loadXsd($$) |
|
176
|
0
|
|
|
0
|
|
|
{ my ($self, $set, $version) = @_; |
|
177
|
0
|
|
|
|
|
|
my $file = $xsd_files{$set}{$version}; |
|
178
|
0
|
0
|
|
|
|
|
$self->{BC_loaded}{$file}++ or $self->schemas->importDefinitions($file); |
|
179
|
|
|
|
|
|
|
} |
|
180
|
|
|
|
|
|
|
|
|
181
|
|
|
|
|
|
|
my %msg_readers; |
|
182
|
|
|
|
|
|
|
sub schemaReader($$$) |
|
183
|
0
|
|
|
0
|
1
|
|
{ my ($self, $set, $version, $ns) = @_; |
|
184
|
0
|
|
0
|
|
|
|
my $r = $self->{BC_r} ||= {}; |
|
185
|
0
|
0
|
|
|
|
|
return $r->{$ns} if $r->{$ns}; |
|
186
|
|
|
|
|
|
|
|
|
187
|
0
|
|
|
|
|
|
$self->_loadXsd($set, $version); |
|
188
|
|
|
|
|
|
|
|
|
189
|
|
|
|
|
|
|
$r->{$ns} = $self->schemas->compile( |
|
190
|
|
|
|
|
|
|
READER => $self->_rootElement($ns), |
|
191
|
|
|
|
|
|
|
sloppy_floats => !$self->{BC_big}, |
|
192
|
0
|
0
|
|
|
|
|
key_rewrite => $self->{BC_long} ? $self->tag2fullnameTable : undef, |
|
193
|
|
|
|
|
|
|
); |
|
194
|
|
|
|
|
|
|
} |
|
195
|
|
|
|
|
|
|
|
|
196
|
|
|
|
|
|
|
|
|
197
|
|
|
|
|
|
|
sub schemaWriter($$$) |
|
198
|
0
|
|
|
0
|
1
|
|
{ my ($self, $set, $version, $ns) = @_; |
|
199
|
0
|
|
0
|
|
|
|
my $w = $self->{BC_w} ||= {}; |
|
200
|
0
|
0
|
|
|
|
|
return $w->{$ns} if $w->{$ns}; |
|
201
|
|
|
|
|
|
|
|
|
202
|
0
|
|
|
|
|
|
$self->_loadXsd($set, $version); |
|
203
|
|
|
|
|
|
|
$w->{$ns} = $self->schemas->compile( |
|
204
|
|
|
|
|
|
|
WRITER => $self->_rootElement($ns), |
|
205
|
|
|
|
|
|
|
sloppy_floats => !$self->{BC_big}, |
|
206
|
0
|
0
|
|
|
|
|
key_rewrite => $self->{BC_long} ? $self->tag2fullnameTable : undef, |
|
207
|
|
|
|
|
|
|
ignore_unused_tags => qr/^_attrs$/, |
|
208
|
|
|
|
|
|
|
prefixes => { $ns => '' }, |
|
209
|
|
|
|
|
|
|
); |
|
210
|
|
|
|
|
|
|
} |
|
211
|
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
|
|
213
|
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
# called with ($set, $version, \@available_versions) |
|
215
|
0
|
|
|
0
|
|
|
sub _exact { first { $_[1] eq $_ } @{$_[2]} } |
|
|
0
|
|
|
0
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
|
|
217
|
|
|
|
|
|
|
my %rules = ( |
|
218
|
|
|
|
|
|
|
EXACT => \&_exact, |
|
219
|
|
|
|
|
|
|
NEWER => sub { (grep $_ >= $_[1], @{$_[2]})[0] }, |
|
220
|
|
|
|
|
|
|
NEWEST => sub { _exact(@_) || ($_[1] <= $_[2][-1] ? $_[2][-1] : undef) }, |
|
221
|
|
|
|
|
|
|
ANY => sub { _exact(@_) || $_[2][-1] }, |
|
222
|
|
|
|
|
|
|
); |
|
223
|
|
|
|
|
|
|
|
|
224
|
|
|
|
|
|
|
sub matchSchema($$%) |
|
225
|
0
|
|
|
0
|
1
|
|
{ my ($self, $set, $version, %args) = @_; |
|
226
|
0
|
0
|
|
|
|
|
my $versions = $xsd_files{$set} or panic "Unknown set $set"; |
|
227
|
|
|
|
|
|
|
|
|
228
|
0
|
|
0
|
|
|
|
my $ruler = $args{rule} ||= $self->{BC_rule}; |
|
229
|
0
|
0
|
|
|
|
|
my $rule = ref $ruler eq 'CODE' ? $ruler : $rules{$ruler} |
|
|
|
0
|
|
|
|
|
|
|
230
|
|
|
|
|
|
|
or error __x"Unknown schema match rule '{rule}'.", rule => $ruler; |
|
231
|
|
|
|
|
|
|
|
|
232
|
0
|
|
|
|
|
|
$rule->($set, $version, [ sort { $a <=> $b } keys %$versions ]); |
|
|
0
|
|
|
|
|
|
|
|
233
|
|
|
|
|
|
|
} |
|
234
|
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
|
|
236
|
|
|
|
|
|
|
sub knownVersions(;$) |
|
237
|
0
|
|
|
0
|
1
|
|
{ my ($self, $set) = @_; |
|
238
|
0
|
|
|
|
|
|
my @s; |
|
239
|
0
|
0
|
|
|
|
|
foreach my $s ($set ? $set : sort keys %xsd_files) |
|
240
|
0
|
|
|
|
|
|
{ push @s, map "camt.$s.$_", sort {$a <=> $b} keys %{$xsd_files{$s}}; |
|
|
0
|
|
|
|
|
|
|
|
|
0
|
|
|
|
|
|
|
|
241
|
|
|
|
|
|
|
} |
|
242
|
0
|
|
|
|
|
|
@s; |
|
243
|
|
|
|
|
|
|
} |
|
244
|
|
|
|
|
|
|
|
|
245
|
|
|
|
|
|
|
|
|
246
|
|
|
|
|
|
|
sub fullname2tagTable() |
|
247
|
0
|
|
|
0
|
1
|
|
{ my $self = shift; |
|
248
|
0
|
|
0
|
|
|
|
$self->{BC_toAbbr} ||= +{ reverse %{$self->tag2fullnameTable} }; |
|
|
0
|
|
|
|
|
|
|
|
249
|
|
|
|
|
|
|
} |
|
250
|
|
|
|
|
|
|
|
|
251
|
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
sub tag2fullnameTable() |
|
253
|
0
|
|
|
0
|
1
|
|
{ my $self = shift; |
|
254
|
|
|
|
|
|
|
$self->{BC_toLong} ||= +{ |
|
255
|
0
|
|
0
|
|
|
|
map split(/,/, $_, 2), grep !/,$/, $tagdir->file('index.csv')->slurp(chomp => 1) |
|
256
|
|
|
|
|
|
|
}; |
|
257
|
|
|
|
|
|
|
} |
|
258
|
|
|
|
|
|
|
|
|
259
|
|
|
|
|
|
|
#-------------------- |
|
260
|
|
|
|
|
|
|
|
|
261
|
|
|
|
|
|
|
1; |