File Coverage

blib/lib/Messaging/Message.pm
Criterion Covered Total %
statement 231 263 87.8
branch 98 116 84.4
condition 45 86 52.3
subroutine 45 49 91.8
pod 18 18 100.0
total 437 532 82.1


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Messaging/Message.pm #
4             # #
5             # Description: abstraction of a message #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Messaging::Message;
14 5     5   87150 use strict;
  5         6  
  5         108  
15 5     5   16 use warnings;
  5         2  
  5         282  
16             our $VERSION = "1.6";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.25 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 5     5   1472 use Encode qw(encode decode FB_CROAK LEAVE_SRC);
  5         19653  
  5         292  
24 5     5   2706 use JSON qw();
  5         45762  
  5         122  
25 5     5   1972 use MIME::Base64 qw(encode_base64 decode_base64);
  5         2146  
  5         290  
26 5     5   1929 use No::Worries::Die qw(dief);
  5         60060  
  5         26  
27 5     5   348 use No::Worries::Export qw(export_control);
  5         5  
  5         21  
28 5     5   279 use Params::Validate qw(validate validate_pos :types);
  5         6  
  5         13473  
29              
30             #
31             # global variables
32             #
33              
34             our(
35             %_LoadedModule, # hash of successfully loaded modules
36             %_CompressionModule, # known compression modules
37             $_CompressionAlgos, # known compression algorithms
38             $_JSON, # JSON object
39             );
40              
41             %_CompressionModule = (
42             "lz4" => "LZ4",
43             "snappy" => "Snappy",
44             "zlib" => "Zlib",
45             );
46             $_CompressionAlgos = join("|", sort(keys(%_CompressionModule)));
47             $_JSON = JSON->new();
48              
49             #+++############################################################################
50             # #
51             # helper functions #
52             # #
53             #---############################################################################
54              
55             #
56             # make sure a module is loaded
57             #
58              
59             sub _require ($) {
60 17     17   22 my($module) = @_;
61              
62 17 100       50 return if $_LoadedModule{$module};
63 4         196 eval("require $module"); ## no critic 'ProhibitStringyEval'
64 4 100       17 if ($@) {
65 1         14 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
66 1         3 dief("failed to load %s: %s", $module, $@);
67             } else {
68 3         9 $_LoadedModule{$module} = 1;
69             }
70             }
71              
72             #
73             # evaluate some code with fatal warnings
74             #
75              
76             sub _eval ($&;$) {
77 180     180   186 my($what, $code, $arg) = @_;
78              
79 180         157 eval {
80 180     0   557 local $SIG{__WARN__} = sub { die($_[0]) };
  0         0  
81 180         257 $code->($arg);
82             };
83 180 100       7322 return unless $@;
84 3         27 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
85 3         8 dief("%s failed: %s", $what, $@);
86             }
87              
88             #
89             # helpers for body encoding and compression
90             #
91              
92             sub _maybe_base64_encode ($) {
93 15     15   10 my($object) = @_;
94              
95 15 50       53 return unless $object->{"body"} =~ /[^\t\n\r\x20-\x7e]/;
96             # only if it contains more than printable ASCII characters (plus \t \n \r)
97             _eval("Base64 encoding", sub {
98 15     15   71 $object->{"body"} = encode_base64($object->{"body"}, "");
99 15         49 });
100 15         34 $object->{"encoding"}{"base64"}++;
101             }
102              
103             sub _maybe_utf8_encode ($) {
104 3     3   4 my($object) = @_;
105 3         1 my($tmp);
106              
107             _eval("UTF-8 encoding", sub {
108 3     3   15 $tmp = encode("UTF-8", $object->{"body"}, FB_CROAK|LEAVE_SRC);
109 3         10 });
110 3 100       10 return if $tmp eq $object->{"body"};
111 1         1 $object->{"body"} = $tmp;
112 1         3 $object->{"encoding"}{"utf8"}++;
113             }
114              
115             sub _do_compress ($$) {
116 6     6   6 my($object, $algo) = @_;
117 6         2 my($compress, $tmp);
118              
119 6         6 $compress = \&{"Compress::$_CompressionModule{$algo}::compress"};
  6         18  
120             _eval("$_CompressionModule{$algo} compression", sub {
121 6     6   15 $tmp = $compress->(\$object->{"body"});
122 6         20 });
123 6         16 $object->{"body"} = $tmp;
124 6         15 $object->{"encoding"}{$algo}++;
125             }
126              
127             #+++############################################################################
128             # #
129             # object oriented interface #
130             # #
131             #---############################################################################
132              
133             #
134             # normal constructor
135             #
136              
137             my %new_options = (
138             "header" => {
139             type => HASHREF,
140             callbacks => {
141             "hash of strings" =>
142             sub { grep(!defined($_)||ref($_), values(%{$_[0]})) == 0 },
143             },
144             optional => 1,
145             },
146             "body" => {
147             type => SCALAR,
148             optional => 1,
149             },
150             "body_ref" => {
151             type => SCALARREF,
152             optional => 1,
153             },
154             "text" => {
155             type => BOOLEAN,
156             optional => 1,
157             },
158             );
159              
160             sub new : method {
161 223     223 1 48247 my($class, %option, $body, $self);
162              
163 223         223 $class = shift(@_);
164 223 100       2285 %option = validate(@_, \%new_options) if @_;
165             dief("new(): options body and body_ref are mutually exclusive")
166 220 100 66     616 if exists($option{"body"}) and exists($option{"body_ref"});
167             # default message
168 219         176 $body = "";
169 219         383 $self = { "header" => {}, "body_ref" => \$body, "text" => 0 };
170             # handle options
171 219 100       345 $self->{"header"} = $option{"header"} if exists($option{"header"});
172 219 100       314 $self->{"body_ref"} = $option{"body_ref"} if exists($option{"body_ref"});
173 219 100       275 $self->{"body_ref"} = \$option{"body"} if exists($option{"body"});
174 219 100       361 $self->{"text"} = $option{"text"} ? 1 : 0 if exists($option{"text"});
    100          
175             # so far so good!
176 219         221 bless($self, $class);
177 219         484 return($self);
178             }
179              
180             #
181             # normal accessors
182             #
183              
184             sub header : method {
185 58     58 1 3595 my($self);
186              
187 58         58 $self = shift(@_);
188 58 50       255 return($self->{"header"}) if @_ == 0;
189 0         0 validate_pos(@_, $new_options{"header"});
190 0         0 $self->{"header"} = $_[0];
191 0         0 return($self);
192             }
193              
194             sub body_ref : method {
195 2     2 1 246 my($self);
196              
197 2         2 $self = shift(@_);
198 2 100       7 return($self->{"body_ref"}) if @_ == 0;
199 1 50 33     8 validate_pos(@_, $new_options{"body_ref"})
      33        
200             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
201 1         2 $self->{"body_ref"} = $_[0];
202 1         2 return($self);
203             }
204              
205             sub text : method {
206 74     74 1 932 my($self);
207              
208 74         70 $self = shift(@_);
209 74 100       163 return($self->{"text"}) if @_ == 0;
210 1 50 33     8 validate_pos(@_, $new_options{"text"})
      33        
211             unless @_ == 1 and (not defined($_[0]) or ref($_[0]) eq "");
212 1 50       3 $self->{"text"} = $_[0] ? 1 : 0;
213 1         1 return($self);
214             }
215              
216             #
217             # extra accessors
218             #
219              
220             sub header_field : method {
221 82     82 1 1912 my($self);
222              
223 82         76 $self = shift(@_);
224 82 100 66     410 if (@_ >= 1 and defined($_[0]) and ref($_[0]) eq "") {
      100        
225 80 100       221 return($self->{"header"}{$_[0]}) if @_ == 1;
226 2 100 33     23 if (@_ == 2 and defined($_[1]) and ref($_[1]) eq "") {
      66        
227 1         4 $self->{"header"}{$_[0]} = $_[1];
228 1         2 return($self);
229             }
230             }
231             # so far so bad :-(
232 3         402 validate_pos(@_, { type => SCALAR }, { type => SCALAR, optional => 1 });
233             }
234              
235             sub body : method {
236 50     50 1 132 my($self, $body);
237              
238 50         43 $self = shift(@_);
239 50 100       78 return(${ $self->{"body_ref"} }) if @_ == 0;
  49         123  
240 1 50 33     8 validate_pos(@_, $new_options{"body"})
      33        
241             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
242 1         2 $body = $_[0]; # copy
243 1         2 $self->{"body_ref"} = \$body;
244 1         1 return($self);
245             }
246              
247             #
248             # extra methods
249             #
250              
251             sub copy : method {
252 7     7 1 4288 my($self, %header, $body, $copy);
253              
254 7         8 $self = shift(@_);
255 7 50       13 validate_pos(@_) if @_;
256 7         6 %header = %{ $self->{"header"} }; # copy
  7         17  
257 7         6 $body = ${ $self->{"body_ref"} }; # copy
  7         9  
258             $copy = {
259             "header" => \%header,
260             "body_ref" => \$body,
261 7         17 "text" => $self->{"text"},
262             };
263 7         9 bless($copy, ref($self));
264 7         9 return($copy);
265             }
266              
267             sub size : method {
268 0     0 1 0 my($self, $size, $key, $value);
269              
270 0         0 $self = shift(@_);
271 0 0       0 validate_pos(@_) if @_;
272 0         0 $size = 1 + length(${ $self->{"body_ref"} });
  0         0  
273 0         0 while (($key, $value) = each(%{ $self->{"header"} })) {
  0         0  
274 0         0 $size += 2 + length($key) + length($value);
275             }
276 0         0 return($size);
277             }
278              
279             #+++############################################################################
280             # #
281             # (de)jsonification #
282             # #
283             #---############################################################################
284              
285             #
286             # jsonify (= transform into a JSON object)
287             #
288              
289             my %jsonify_options = (
290             "compression" => {
291             type => SCALAR,
292             regex => qr/^($_CompressionAlgos)?!?$/o,
293             optional => 1,
294             },
295             );
296              
297             sub _jsonify_text ($$$$$) {
298 10     10   12 my($self, $object, $algo, $force, $len) = @_;
299              
300 10 50 66     45 if ($algo and $force) {
    100 66        
301             # always compress
302 0         0 _maybe_utf8_encode($object);
303 0         0 _do_compress($object, $algo);
304 0         0 _maybe_base64_encode($object);
305             } elsif ($algo and $len > 255) {
306             # maybe compress
307 3         4 _maybe_utf8_encode($object);
308 3         5 _do_compress($object, $algo);
309 3         4 _maybe_base64_encode($object);
310 3 50       11 if (length($object->{"body"}) >= $len) {
311             # not worth it
312 0         0 $object->{"body"} = ${ $self->{"body_ref"} };
  0         0  
313 0         0 delete($object->{"encoding"});
314             }
315             } else {
316             # do not compress
317             }
318             }
319              
320             sub _jsonify_binary ($$$$$) {
321 12     12   16 my($self, $object, $algo, $force, $len) = @_;
322              
323 12 50 66     53 if ($algo and $force) {
    100 66        
324             # always compress
325 0         0 _do_compress($object, $algo);
326 0         0 _maybe_base64_encode($object);
327             } elsif ($algo and $len > 255) {
328             # maybe compress
329 3 100       43 $len *= 4/3 if $object->{"body"} =~ /[^\t\n\r\x20-\x7e]/;
330 3         5 _do_compress($object, $algo);
331 3         5 _maybe_base64_encode($object);
332 3 50       11 if (length($object->{"body"}) >= $len) {
333             # not worth it
334 0         0 $object->{"body"} = ${ $self->{"body_ref"} };
  0         0  
335 0         0 delete($object->{"encoding"});
336 0         0 _maybe_base64_encode($object);
337             }
338             } else {
339             # do not compress
340 9         15 _maybe_base64_encode($object);
341             }
342             }
343              
344             sub jsonify : method {
345 38     38 1 58 my($self, %option, %object, $algo, $force, $len);
346              
347 38         34 $self = shift(@_);
348 38 100       110 %option = validate(@_, \%jsonify_options) if @_;
349 38 100 66     156 if ($option{"compression"} and $option{"compression"} =~ /^(\w+)(!?)$/) {
350 6         15 ($algo, $force) = ($1, $2);
351             }
352             # check compression availability
353 38 100       63 _require("Compress::$_CompressionModule{$algo}") if $algo;
354             # build the JSON object
355 38 100       77 $object{"text"} = JSON::true if $self->{"text"};
356 38 100       45 $object{"header"} = $self->{"header"} if keys(%{ $self->{"header"} });
  38         86  
357 38         25 $len = length(${ $self->{"body_ref"} });
  38         39  
358 38 100       70 return(\%object) unless $len;
359 22         13 $object{"body"} = ${ $self->{"body_ref"} };
  22         28  
360             # handle non-empty body
361 22 100       29 if ($self->{"text"}) {
362             # text body
363 10         15 _jsonify_text($self, \%object, $algo, $force, $len);
364             } else {
365             # binary body
366 12         19 _jsonify_binary($self, \%object, $algo, $force, $len);
367             }
368             # set the encoding string
369 15         55 $object{"encoding"} = join("+", sort(keys(%{ $object{"encoding"} })))
370 22 100       33 if $object{"encoding"};
371             # so far so good!
372 22         51 return(\%object);
373             }
374              
375             #
376             # dejsonify (= alternate constructor using the JSON object)
377             #
378              
379             my %dejsonify_options = (
380             "header" => $new_options{"header"},
381             "body" => {
382             type => SCALAR,
383             optional => 1,
384             },
385             "text" => {
386             type => OBJECT,
387             callbacks => {
388             "JSON::is_bool" => sub { JSON::is_bool($_[0]) },
389             },
390             optional => 1,
391             },
392             "encoding" => {
393             type => SCALAR,
394             optional => 1,
395             },
396             );
397              
398             sub dejsonify : method {
399 74     74 1 69 my($class, $object, $encoding, $self, $tmp, $len, $uncompress);
400              
401 74         67 $class = shift(@_);
402 74 100 33     523 validate_pos(@_, { type => HASHREF })
      66        
403             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "HASH";
404 73         1108 validate(@_, \%dejsonify_options);
405 70         257 $object = $_[0];
406 70   100     185 $encoding = $object->{"encoding"} || "";
407 70 100 100     347 dief("invalid encoding: %s", $encoding)
408             unless $encoding eq ""
409             or "${encoding}+" =~ /^((base64|utf8|$_CompressionAlgos)\+)+$/o;
410 69 100       240 _require("Compress::$_CompressionModule{$1}")
411             if $encoding =~ /($_CompressionAlgos)/o;
412             # construct the message
413 69         122 $self = $class->new();
414 69 100       247 $self->{"text"} = 1 if $object->{"text"};
415             $self->{"header"} = $object->{"header"}
416 69 100 100     205 if $object->{"header"} and keys(%{ $object->{"header"} });
  40         135  
417 69 100       106 if (exists($object->{"body"})) {
418 57         54 $tmp = $object->{"body"};
419 57 100       97 if ($encoding =~ /base64/) {
420             # body has been Base64 encoded, compute length to detect unexpected
421             # characters (this is because MIME::Base64 silently ignores them)
422 26         20 $len = length($tmp);
423 26 100       45 dief("invalid Base64 data: %s", $object->{"body"}) if $len % 4;
424 25         33 $len = $len * 3 / 4;
425 25         48 $len -= substr($tmp, -2) =~ tr/=/=/;
426             _eval("Base64 decoding", sub {
427 25     25   365 $tmp = decode_base64($tmp);
428 25         75 });
429 25 100       61 dief("invalid Base64 data: %s", $object->{"body"})
430             unless $len == length($tmp);
431             }
432 54 100       164 if ($encoding =~ /($_CompressionAlgos)/o) {
433             # body has been compressed
434 9         7 $uncompress = \&{"Compress::$_CompressionModule{$1}::uncompress"};
  9         35  
435             _eval("$_CompressionModule{$1} decompression", sub {
436 9     9   21 $tmp = $uncompress->(\$tmp);
437 9         30 });
438 9 50       26 dief("invalid $_CompressionModule{$1} compressed data!")
439             unless defined($tmp);
440             }
441 54 100       90 if ($encoding =~ /utf8/) {
442             # body has been UTF-8 encoded
443             _eval("UTF-8 decoding", sub {
444 4     4   17 $tmp = decode("UTF-8", $tmp, FB_CROAK);
445 4         14 });
446             }
447 54         66 $self->{"body_ref"} = \$tmp;
448             }
449             # so far so good!
450 66         277 return($self);
451             }
452              
453             #+++############################################################################
454             # #
455             # (de)stringification #
456             # #
457             #---############################################################################
458              
459             #
460             # stringify (= transform into a text string)
461             #
462              
463             sub stringify : method {
464 7     7 1 5302 my($self, $tmp);
465              
466 7         8 $self = shift(@_);
467 7         9 $tmp = $self->jsonify(@_);
468             _eval("JSON encoding", sub {
469 7     7   50 $tmp = $_JSON->encode($tmp);
470 7         19 });
471 7         18 return($tmp);
472             }
473              
474             sub stringify_ref : method {
475 18     18 1 16 my($self, $tmp);
476              
477 18         13 $self = shift(@_);
478 18         25 $tmp = $self->jsonify(@_);
479             _eval("JSON encoding", sub {
480 18     18   131 $tmp = $_JSON->encode($tmp);
481 18         47 });
482 18         31 return(\$tmp);
483             }
484              
485             #
486             # destringify (= alternate constructor using the stringified representation)
487             #
488              
489             sub destringify : method {
490 36     36 1 40 my($class, $tmp);
491              
492 36         33 $class = shift(@_);
493 36 50 33     164 validate_pos(@_, { type => SCALAR })
      33        
494             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
495             _eval("JSON decoding", sub {
496 36     36   26 $tmp = $_JSON->decode(${ $_[0] });
  36         354  
497 36         99 }, \$_[0]);
498 34         77 return($class->dejsonify($tmp));
499             }
500              
501             sub destringify_ref : method {
502 33     33 1 24 my($class, $tmp);
503              
504 33         49 $class = shift(@_);
505 33 50 33     180 validate_pos(@_, { type => SCALARREF })
      33        
506             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
507             _eval("JSON decoding", sub {
508 33     33   29 $tmp = $_JSON->decode(${ $_[0] });
  33         1131  
509 33         123 }, $_[0]);
510 33         97 return($class->dejsonify($tmp));
511             }
512              
513             #+++############################################################################
514             # #
515             # (de)serialization #
516             # #
517             #---############################################################################
518              
519             #
520             # serialize (= transform into a binary string)
521             #
522              
523             sub serialize : method {
524 18     18 1 4428 my($self, $tmp);
525              
526 18         19 $self = shift(@_);
527 18         26 $tmp = $self->stringify_ref(@_);
528             _eval("UTF-8 encoding", sub {
529 18     18   14 $tmp = encode("UTF-8", ${ $tmp }, FB_CROAK);
  18         52  
530 18         36 });
531 18         46 return($tmp);
532             }
533              
534             sub serialize_ref : method {
535 0     0 1 0 my($self, $tmp);
536              
537 0         0 $self = shift(@_);
538 0         0 $tmp = $self->stringify_ref(@_);
539             _eval("UTF-8 encoding", sub {
540 0     0   0 $tmp = encode("UTF-8", ${ $tmp }, FB_CROAK);
  0         0  
541 0         0 });
542 0         0 return(\$tmp);
543             }
544              
545             #
546             # deserialize (= alternate constructor using the serialized representation)
547             #
548              
549             sub deserialize : method {
550 28     28 1 3336 my($class, $tmp);
551              
552 28         28 $class = shift(@_);
553 28 50 33     152 validate_pos(@_, { type => SCALAR })
      33        
554             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
555 28 100       86 return($class->destringify($_[0])) unless $_[0] =~ /[^[:ascii:]]/;
556             _eval("UTF-8 decoding", sub {
557 4     4   2 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  4         15  
558 4         12 }, \$_[0]);
559 3         33 return($class->destringify($tmp));
560             }
561              
562             sub deserialize_ref : method {
563 35     35 1 59152 my($class, $tmp);
564              
565 35         50 $class = shift(@_);
566 35 50 33     261 validate_pos(@_, { type => SCALARREF })
      33        
567             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
568 35 100       38 return($class->destringify_ref($_[0])) unless ${ $_[0] } =~ /[^[:ascii:]]/;
  35         899  
569             _eval("UTF-8 decoding", sub {
570 2     2   3 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  2         9  
571 2         9 }, $_[0]);
572 2         7 return($class->destringify($tmp));
573             }
574              
575             #
576             # export control
577             #
578              
579             sub import : method {
580 6     6   26 my($pkg, %exported);
581              
582 6         8 $pkg = shift(@_);
583 6         11 %exported = ("_require" => 1);
584 6         18 export_control(scalar(caller()), $pkg, \%exported, @_);
585             }
586              
587             1;
588              
589             __DATA__