File Coverage

blib/lib/Messaging/Message.pm
Criterion Covered Total %
statement 229 263 87.0
branch 97 116 83.6
condition 46 86 53.4
subroutine 45 49 91.8
pod 18 18 100.0
total 435 532 81.7


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   141114 use strict;
  5         32  
  5         168  
15 5     5   29 use warnings;
  5         11  
  5         475  
16             our $VERSION = "1.7";
17             our $REVISION = sprintf("%d.%02d", q$Revision: 1.25 $ =~ /(\d+)\.(\d+)/);
18              
19             #
20             # used modules
21             #
22              
23 5     5   1716 use Encode qw(encode decode FB_CROAK LEAVE_SRC);
  5         31942  
  5         423  
24 5     5   3334 use JSON qw();
  5         63853  
  5         249  
25 5     5   2369 use MIME::Base64 qw(encode_base64 decode_base64);
  5         4452  
  5         407  
26 5     5   2204 use No::Worries::Die qw(dief);
  5         94819  
  5         35  
27 5     5   595 use No::Worries::Export qw(export_control);
  5         13  
  5         29  
28 5     5   478 use Params::Validate qw(validate validate_pos :types);
  5         11  
  5         21774  
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   44 my($module) = @_;
61              
62 17 100       62 return if $_LoadedModule{$module};
63 4         304 eval("require $module"); ## no critic 'ProhibitStringyEval'
64 4 50       26 if ($@) {
65 0         0 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
66 0         0 dief("failed to load %s: %s", $module, $@);
67             } else {
68 4         15 $_LoadedModule{$module} = 1;
69             }
70             }
71              
72             #
73             # evaluate some code with fatal warnings
74             #
75              
76             sub _eval ($&;$) {
77 180     180   444 my($what, $code, $arg) = @_;
78              
79 180         279 eval {
80 180     0   826 local $SIG{__WARN__} = sub { die($_[0]) };
  0         0  
81 180         421 $code->($arg);
82             };
83 180 100       7797 return unless $@;
84 3         38 $@ =~ s/\s+at\s.+?\sline\s+\d+\.?$//;
85 3         13 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   31 my($object) = @_;
94              
95 15 50       81 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   163 $object->{"body"} = encode_base64($object->{"body"}, "");
99 15         78 });
100 15         58 $object->{"encoding"}{"base64"}++;
101             }
102              
103             sub _maybe_utf8_encode ($) {
104 3     3   6 my($object) = @_;
105 3         6 my($tmp);
106              
107             _eval("UTF-8 encoding", sub {
108 3     3   25 $tmp = encode("UTF-8", $object->{"body"}, FB_CROAK|LEAVE_SRC);
109 3         14 });
110 3 100       15 return if $tmp eq $object->{"body"};
111 1         2 $object->{"body"} = $tmp;
112 1         5 $object->{"encoding"}{"utf8"}++;
113             }
114              
115             sub _do_compress ($$) {
116 6     6   12 my($object, $algo) = @_;
117 6         11 my($compress, $tmp);
118              
119 6         8 $compress = \&{"Compress::$_CompressionModule{$algo}::compress"};
  6         31  
120             _eval("$_CompressionModule{$algo} compression", sub {
121 6     6   21 $tmp = $compress->(\$object->{"body"});
122 6         49 });
123 6         26 $object->{"body"} = $tmp;
124 6         22 $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 224     224 1 142079 my($class, %option, $body, $self);
162              
163 224         401 $class = shift(@_);
164 224 100       3024 %option = validate(@_, \%new_options) if @_;
165             dief("new(): options body and body_ref are mutually exclusive")
166 221 100 100     981 if exists($option{"body"}) and exists($option{"body_ref"});
167             # default message
168 220         351 $body = "";
169 220         672 $self = { "header" => {}, "body_ref" => \$body, "text" => 0 };
170             # handle options
171 220 100       525 $self->{"header"} = $option{"header"} if exists($option{"header"});
172 220 100       487 $self->{"body_ref"} = $option{"body_ref"} if exists($option{"body_ref"});
173 220 100       443 $self->{"body_ref"} = \$option{"body"} if exists($option{"body"});
174 220 100       513 $self->{"text"} = $option{"text"} ? 1 : 0 if exists($option{"text"});
    100          
175             # so far so good!
176 220         368 bless($self, $class);
177 220         937 return($self);
178             }
179              
180             #
181             # normal accessors
182             #
183              
184             sub header : method {
185 58     58 1 6202 my($self);
186              
187 58         119 $self = shift(@_);
188 58 50       374 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 572 my($self);
196              
197 2         4 $self = shift(@_);
198 2 100       12 return($self->{"body_ref"}) if @_ == 0;
199 1 50 33     11 validate_pos(@_, $new_options{"body_ref"})
      33        
200             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
201 1         3 $self->{"body_ref"} = $_[0];
202 1         2 return($self);
203             }
204              
205             sub text : method {
206 74     74 1 1541 my($self);
207              
208 74         116 $self = shift(@_);
209 74 100       198 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       4 $self->{"text"} = $_[0] ? 1 : 0;
213 1         3 return($self);
214             }
215              
216             #
217             # extra accessors
218             #
219              
220             sub header_field : method {
221 82     82 1 3442 my($self);
222              
223 82         123 $self = shift(@_);
224 82 100 66     371 if (@_ >= 1 and defined($_[0]) and ref($_[0]) eq "") {
      100        
225 80 100       283 return($self->{"header"}{$_[0]}) if @_ == 1;
226 2 100 33     19 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         463 validate_pos(@_, { type => SCALAR }, { type => SCALAR, optional => 1 });
233             }
234              
235             sub body : method {
236 50     50 1 217 my($self, $body);
237              
238 50         77 $self = shift(@_);
239 50 100       129 return(${ $self->{"body_ref"} }) if @_ == 0;
  49         153  
240 1 50 33     9 validate_pos(@_, $new_options{"body"})
      33        
241             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
242 1         3 $body = $_[0]; # copy
243 1         2 $self->{"body_ref"} = \$body;
244 1         3 return($self);
245             }
246              
247             #
248             # extra methods
249             #
250              
251             sub copy : method {
252 7     7 1 6380 my($self, %header, $body, $copy);
253              
254 7         15 $self = shift(@_);
255 7 50       18 validate_pos(@_) if @_;
256 7         10 %header = %{ $self->{"header"} }; # copy
  7         23  
257 7         8 $body = ${ $self->{"body_ref"} }; # copy
  7         16  
258             $copy = {
259             "header" => \%header,
260             "body_ref" => \$body,
261 7         27 "text" => $self->{"text"},
262             };
263 7         15 bless($copy, ref($self));
264 7         22 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   27 my($self, $object, $algo, $force, $len) = @_;
299              
300 10 50 66     55 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         8 _maybe_utf8_encode($object);
308 3         10 _do_compress($object, $algo);
309 3         11 _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   58 my($self, $object, $algo, $force, $len) = @_;
322              
323 12 50 66     66 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       60 $len *= 4/3 if $object->{"body"} =~ /[^\t\n\r\x20-\x7e]/;
330 3         26 _do_compress($object, $algo);
331 3         11 _maybe_base64_encode($object);
332 3 50       14 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         35 _maybe_base64_encode($object);
341             }
342             }
343              
344             sub jsonify : method {
345 38     38 1 142 my($self, %option, %object, $algo, $force, $len);
346              
347 38         61 $self = shift(@_);
348 38 100       146 %option = validate(@_, \%jsonify_options) if @_;
349 38 100 66     218 if ($option{"compression"} and $option{"compression"} =~ /^(\w+)(!?)$/) {
350 6         25 ($algo, $force) = ($1, $2);
351             }
352             # check compression availability
353 38 100       134 _require("Compress::$_CompressionModule{$algo}") if $algo;
354             # build the JSON object
355 38 100       117 $object{"text"} = JSON::true if $self->{"text"};
356 38 100       81 $object{"header"} = $self->{"header"} if keys(%{ $self->{"header"} });
  38         116  
357 38         58 $len = length(${ $self->{"body_ref"} });
  38         70  
358 38 100       89 return(\%object) unless $len;
359 22         32 $object{"body"} = ${ $self->{"body_ref"} };
  22         49  
360             # handle non-empty body
361 22 100       50 if ($self->{"text"}) {
362             # text body
363 10         29 _jsonify_text($self, \%object, $algo, $force, $len);
364             } else {
365             # binary body
366 12         37 _jsonify_binary($self, \%object, $algo, $force, $len);
367             }
368             # set the encoding string
369 15         87 $object{"encoding"} = join("+", sort(keys(%{ $object{"encoding"} })))
370 22 100       57 if $object{"encoding"};
371             # so far so good!
372 22         76 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 157 my($class, $object, $encoding, $self, $tmp, $len, $uncompress);
400              
401 74         139 $class = shift(@_);
402 74 100 33     539 validate_pos(@_, { type => HASHREF })
      66        
403             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "HASH";
404 73         1364 validate(@_, \%dejsonify_options);
405 70         363 $object = $_[0];
406 70   100     268 $encoding = $object->{"encoding"} || "";
407 70 100 100     508 dief("invalid encoding: %s", $encoding)
408             unless $encoding eq ""
409             or "${encoding}+" =~ /^((base64|utf8|$_CompressionAlgos)\+)+$/o;
410 69 100       352 _require("Compress::$_CompressionModule{$1}")
411             if $encoding =~ /($_CompressionAlgos)/o;
412             # construct the message
413 69         218 $self = $class->new();
414 69 100       264 $self->{"text"} = 1 if $object->{"text"};
415             $self->{"header"} = $object->{"header"}
416 69 100 100     338 if $object->{"header"} and keys(%{ $object->{"header"} });
  40         173  
417 69 100       160 if (exists($object->{"body"})) {
418 57         123 $tmp = $object->{"body"};
419 57 100       146 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         52 $len = length($tmp);
423 26 100       95 dief("invalid Base64 data: %s", $object->{"body"}) if $len % 4;
424 25         68 $len = $len * 3 / 4;
425 25         93 $len -= substr($tmp, -2) =~ tr/=/=/;
426             _eval("Base64 decoding", sub {
427 25     25   401 $tmp = decode_base64($tmp);
428 25         126 });
429 25 100       93 dief("invalid Base64 data: %s", $object->{"body"})
430             unless $len == length($tmp);
431             }
432 54 100       217 if ($encoding =~ /($_CompressionAlgos)/o) {
433             # body has been compressed
434 9         17 $uncompress = \&{"Compress::$_CompressionModule{$1}::uncompress"};
  9         74  
435             _eval("$_CompressionModule{$1} decompression", sub {
436 9     9   46 $tmp = $uncompress->(\$tmp);
437 9         61 });
438 9 50       44 dief("invalid $_CompressionModule{$1} compressed data!")
439             unless defined($tmp);
440             }
441 54 100       144 if ($encoding =~ /utf8/) {
442             # body has been UTF-8 encoded
443             _eval("UTF-8 decoding", sub {
444 4     4   19 $tmp = decode("UTF-8", $tmp, FB_CROAK);
445 4         21 });
446             }
447 54         113 $self->{"body_ref"} = \$tmp;
448             }
449             # so far so good!
450 66         289 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 6300 my($self, $tmp);
465              
466 7         16 $self = shift(@_);
467 7         15 $tmp = $self->jsonify(@_);
468             _eval("JSON encoding", sub {
469 7     7   67 $tmp = $_JSON->encode($tmp);
470 7         33 });
471 7         25 return($tmp);
472             }
473              
474             sub stringify_ref : method {
475 18     18 1 29 my($self, $tmp);
476              
477 18         26 $self = shift(@_);
478 18         37 $tmp = $self->jsonify(@_);
479             _eval("JSON encoding", sub {
480 18     18   195 $tmp = $_JSON->encode($tmp);
481 18         81 });
482 18         50 return(\$tmp);
483             }
484              
485             #
486             # destringify (= alternate constructor using the stringified representation)
487             #
488              
489             sub destringify : method {
490 36     36 1 83 my($class, $tmp);
491              
492 36         58 $class = shift(@_);
493 36 50 33     182 validate_pos(@_, { type => SCALAR })
      33        
494             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
495             _eval("JSON decoding", sub {
496 36     36   55 $tmp = $_JSON->decode(${ $_[0] });
  36         475  
497 36         159 }, \$_[0]);
498 34         122 return($class->dejsonify($tmp));
499             }
500              
501             sub destringify_ref : method {
502 33     33 1 65 my($class, $tmp);
503              
504 33         67 $class = shift(@_);
505 33 50 33     170 validate_pos(@_, { type => SCALARREF })
      33        
506             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
507             _eval("JSON decoding", sub {
508 33     33   60 $tmp = $_JSON->decode(${ $_[0] });
  33         1093  
509 33         207 }, $_[0]);
510 33         142 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 6382 my($self, $tmp);
525              
526 18         35 $self = shift(@_);
527 18         42 $tmp = $self->stringify_ref(@_);
528             _eval("UTF-8 encoding", sub {
529 18     18   28 $tmp = encode("UTF-8", ${ $tmp }, FB_CROAK);
  18         59  
530 18         64 });
531 18         68 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 6514 my($class, $tmp);
551              
552 28         48 $class = shift(@_);
553 28 50 33     147 validate_pos(@_, { type => SCALAR })
      33        
554             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "";
555 28 100       149 return($class->destringify($_[0])) unless $_[0] =~ /[^[:ascii:]]/;
556             _eval("UTF-8 decoding", sub {
557 4     4   7 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  4         18  
558 4         22 }, \$_[0]);
559 3         12 return($class->destringify($tmp));
560             }
561              
562             sub deserialize_ref : method {
563 35     35 1 96026 my($class, $tmp);
564              
565 35         89 $class = shift(@_);
566 35 50 33     306 validate_pos(@_, { type => SCALARREF })
      33        
567             unless @_ == 1 and defined($_[0]) and ref($_[0]) eq "SCALAR";
568 35 100       57 return($class->destringify_ref($_[0])) unless ${ $_[0] } =~ /[^[:ascii:]]/;
  35         803  
569             _eval("UTF-8 decoding", sub {
570 2     2   6 $tmp = decode("UTF-8", ${ $_[0] }, FB_CROAK|LEAVE_SRC);
  2         11  
571 2         19 }, $_[0]);
572 2         14 return($class->destringify($tmp));
573             }
574              
575             #
576             # export control
577             #
578              
579             sub import : method {
580 6     6   45 my($pkg, %exported);
581              
582 6         16 $pkg = shift(@_);
583 6         17 %exported = ("_require" => 1);
584 6         87 export_control(scalar(caller()), $pkg, \%exported, @_);
585             }
586              
587             1;
588              
589             __DATA__