File Coverage

blib/lib/Net/STOMP/Client/Frame.pm
Criterion Covered Total %
statement 197 369 53.3
branch 82 206 39.8
condition 19 66 28.7
subroutine 19 28 67.8
pod 12 12 100.0
total 329 681 48.3


line stmt bran cond sub pod time code
1             #+##############################################################################
2             # #
3             # File: Net/STOMP/Client/Frame.pm #
4             # #
5             # Description: Frame support for Net::STOMP::Client #
6             # #
7             #-##############################################################################
8              
9             #
10             # module definition
11             #
12              
13             package Net::STOMP::Client::Frame;
14 5     5   21274 use 5.005; # need the four-argument form of substr()
  5         11  
15 5     5   15 use strict;
  5         4  
  5         72  
16 5     5   14 use warnings;
  5         7  
  5         277  
17             our $VERSION = "2.3";
18             our $REVISION = sprintf("%d.%02d", q$Revision: 2.4 $ =~ /(\d+)\.(\d+)/);
19              
20             #
21             # used modules
22             #
23              
24 5     5   1816 use Encode qw();
  5         27324  
  5         97  
25 5     5   1487 use No::Worries::Die qw(dief);
  5         48279  
  5         29  
26 5     5   400 use No::Worries::Export qw(export_control);
  5         6  
  5         21  
27 5     5   2112 use No::Worries::Log qw(log_debug);
  5         77388  
  5         30  
28 5     5   472 use Params::Validate qw(validate validate_pos :types);
  5         5  
  5         641  
29              
30             #
31             # constants
32             #
33              
34 5     5   21 use constant I_COMMAND => 0;
  5         7  
  5         209  
35 5     5   36 use constant I_HEADERS => 1;
  5         6  
  5         167  
36 5     5   14 use constant I_BODY => 2; # stored as reference
  5         6  
  5         15400  
37              
38             #
39             # global variables
40             #
41              
42             our(
43             # public
44             $DebugBodyLength, # the maximum length of body that will be debugged
45             $StrictEncode, # true if encoding/decoding operations should be strict
46             # private
47             $_HeaderNameRE, # regular expression matching a header name (STOMP 1.0)
48             %_EncMap1, # map to \-encode some chars in the header (STOMP 1.1)
49             %_DecMap1, # map to \-decode some chars in the header (STOMP 1.1)
50             $_EncSet1, # set of chars to encode in the header (STOMP 1.1)
51             %_EncMap2, # map to \-encode some chars in the header (STOMP >= 1.2)
52             %_DecMap2, # map to \-decode some chars in the header (STOMP >= 1.2)
53             $_EncSet2, # set of chars to encode in the header (STOMP >= 1.2)
54             );
55              
56             # public
57             $DebugBodyLength = 256;
58             $StrictEncode = undef;
59              
60             # private
61             $_HeaderNameRE = q/[_a-zA-Z0-9\-\.]+/;
62             %_EncMap1 = %_EncMap2 = (
63             "\r" => "\\r",
64             "\n" => "\\n",
65             ":" => "\\c",
66             "\\" => "\\\\",
67             );
68             delete($_EncMap1{"\r"}); # \r encoding is only for STOMP >= 1.2
69             %_DecMap1 = reverse(%_EncMap1);
70             $_EncSet1 = "[".join("", map(sprintf("\\x%02x", ord($_)), keys(%_EncMap1)))."]";
71             %_DecMap2 = reverse(%_EncMap2);
72             $_EncSet2 = "[".join("", map(sprintf("\\x%02x", ord($_)), keys(%_EncMap2)))."]";
73              
74             #+++############################################################################
75             # #
76             # helpers #
77             # #
78             #---############################################################################
79              
80             #
81             # helper to guess the encoding to use from the content type header
82             #
83              
84             sub _encoding ($) {
85 7     7   8 my($type) = @_;
86              
87 7 100       10 if ($type) {
88 2 100       15 if ($type =~ /^text\/[\w\-]+$/) {
    50          
89 1         2 return("UTF-8");
90             } elsif (";$type;" =~ /\;\s*charset=\"?([\w\-]+)\"?\s*\;/) {
91 0         0 return($1);
92             } else {
93 1         2 return(undef);
94             }
95             } else {
96 5         6 return(undef);
97             }
98             }
99              
100             #
101             # debugging helpers
102             #
103              
104             sub _debug_command ($$) {
105 0     0   0 my($what, $command) = @_;
106              
107 0         0 log_debug("%s %s frame", $what, $command);
108             }
109              
110             sub _debug_header ($) {
111 0     0   0 my($header) = @_;
112 0         0 my($offset, $length, $line, $char);
113              
114 0         0 $length = length($header);
115 0         0 $offset = 0;
116 0         0 while ($offset < $length) {
117 0         0 $line = "";
118 0         0 while (1) {
119 0         0 $char = ord(substr($header, $offset, 1));
120 0         0 $offset++;
121 0 0 0     0 if ($char == 0x0a) {
    0 0        
122 0         0 last;
123             } elsif (0x20 <= $char and $char <= 0x7e and $char != 0x25) {
124 0         0 $line .= sprintf("%c", $char);
125             } else {
126 0         0 $line .= sprintf("%%%02x", $char);
127             }
128 0 0       0 last if $offset == $length;
129             }
130 0         0 log_debug(" H %s", $line);
131             }
132             }
133              
134             sub _debug_body ($) {
135 0     0   0 my($body) = @_;
136 0         0 my($offset, $length, $line, $ascii, $char);
137              
138 0         0 $length = length($body);
139 0 0 0     0 if ($DebugBodyLength and $length > $DebugBodyLength) {
140 0         0 substr($body, $DebugBodyLength, $length - $DebugBodyLength, "");
141 0         0 $length = $DebugBodyLength;
142             }
143 0         0 $offset = 0;
144 0         0 while ($length > 0) {
145 0         0 $line = sprintf("%04x", $offset);
146 0         0 $ascii = "";
147 0         0 foreach my $index (0 .. 15) {
148 0 0       0 if (($index & 3) == 0) {
149 0         0 $line .= " ";
150 0         0 $ascii .= " ";
151             }
152 0 0       0 if ($index < $length) {
153 0         0 $char = ord(substr($body, $index, 1));
154 0         0 $line .= sprintf("%02x", $char);
155 0 0 0     0 $ascii .= sprintf("%c", (0x20 <= $char && $char <= 0x7e) ?
156             $char : 0x2e);
157             } else {
158 0         0 $line .= " ";
159 0         0 $ascii .= " ";
160             }
161             }
162 0         0 log_debug(" B %s %s", $line, $ascii);
163 0         0 $offset += 16;
164 0         0 $length -= 16;
165 0         0 substr($body, 0, 16, "");
166             }
167             }
168              
169             #+++############################################################################
170             # #
171             # object oriented interface #
172             # #
173             #---############################################################################
174              
175             #
176             # constructor
177             #
178             # notes:
179             # - $self->[I_COMMAND] defaults to SEND so it's always defined
180             # - $self->[I_HEADERS] defaults to {} so it's always set to a hash ref
181             # - $self->[I_BODY] defaults to \"" so it's always set to a scalar ref
182             #
183              
184             my %new_options = (
185             "command" => {
186             optional => 1,
187             type => SCALAR,
188             regex => qr/^[A-Z]{2,16}$/,
189             },
190             "headers" => {
191             optional => 1,
192             type => HASHREF,
193             },
194             "body_reference" => {
195             optional => 1,
196             type => SCALARREF,
197             },
198             "body" => {
199             optional => 1,
200             type => SCALAR,
201             },
202             );
203              
204             sub new : method {
205 20     20 1 3511 my($class, %option, $object);
206              
207 20 100       35 if ($Net::STOMP::Client::NoParamsValidation) {
208 11         23 ($class, %option) = @_;
209             } else {
210 9         14 $class = shift(@_);
211 9 100       646 %option = validate(@_, \%new_options) if @_;
212             }
213 18 100       138 if (defined($option{"body"})) {
214             # handle the convenient body option
215             dief("options body and body_reference are " .
216 3 100       15 "mutually exclusive") if $option{"body_reference"};
217 2         6 $option{"body_reference"} = \ delete($option{"body"});
218             }
219 17   100     43 $option{"command"} ||= "SEND";
220 17   100     38 $option{"headers"} ||= {};
221 17   100     31 $option{"body_reference"} ||= \ "";
222 17         32 $object = [ @option{ qw(command headers body_reference) } ];
223 17         75 return(bless($object, $class));
224             }
225              
226             #
227             # standard getters and setters
228             #
229              
230             sub command : method {
231 4     4 1 17 my($self, $value);
232              
233 4         18 $self = shift(@_);
234 4 50       20 return($self->[I_COMMAND]) if @_ == 0;
235 0         0 $value = $_[0];
236 0 0 0     0 if (@_ == 1 and defined($value) and ref($value) eq ""
      0        
      0        
237             and $value =~ $new_options{"command"}{"regex"}) {
238 0         0 $self->[I_COMMAND] = $value;
239 0         0 return($self);
240             }
241             # otherwise complain...
242 0         0 validate_pos(@_, $new_options{"command"});
243             }
244              
245             sub headers : method {
246 0     0 1 0 my($self, $value);
247              
248 0         0 $self = shift(@_);
249 0 0       0 return($self->[I_HEADERS]) if @_ == 0;
250 0         0 $value = $_[0];
251 0 0 0     0 if (@_ == 1 and ref($value) eq "HASH") {
252 0         0 $self->[I_HEADERS] = $value;
253 0         0 return($self);
254             }
255             # otherwise complain...
256 0         0 validate_pos(@_, $new_options{"headers"});
257             }
258              
259             sub body_reference : method {
260 0     0 1 0 my($self, $value);
261              
262 0         0 $self = shift(@_);
263 0 0       0 return($self->[I_BODY]) if @_ == 0;
264 0         0 $value = $_[0];
265 0 0 0     0 if (@_ == 1 and ref($value) eq "SCALAR") {
266 0         0 $self->[I_BODY] = $value;
267 0         0 return($self);
268             }
269             # otherwise complain...
270 0         0 validate_pos(@_, $new_options{"body_reference"});
271             }
272              
273             #
274             # convenient body getter and setter
275             #
276              
277             sub body : method {
278 9     9 1 1026 my($self, $value);
279              
280 9         11 $self = shift(@_);
281 9 50       17 return(${ $self->[I_BODY] }) if @_ == 0;
  9         37  
282 0         0 $value = $_[0];
283 0 0 0     0 if (@_ == 1 and defined($value) and ref($value) eq "") {
      0        
284 0         0 $self->[I_BODY] = \$value;
285 0         0 return($self);
286             }
287             # otherwise complain...
288 0         0 validate_pos(@_, $new_options{"body"});
289             }
290              
291             #
292             # convenient individual header getter and setter:
293             # - $frame->header($key): get
294             # - $frame->header($key, $value): set
295             # - $frame->header($key, undef): delete
296             #
297              
298             my @header_options = (
299             { optional => 0, type => SCALAR },
300             { optional => 1, type => SCALAR|UNDEF },
301             );
302              
303             sub header : method {
304 8     8 1 17 my($self, $key, $value);
305              
306 8         8 $self = shift(@_);
307 8         6 $key = $_[0];
308 8 50 33     31 if (defined($key) and ref($key) eq "") {
309 8 50       11 if (@_ == 1) {
    0          
310             # get
311 8         29 return($self->[I_HEADERS]{$key});
312             } elsif (@_ == 2) {
313 0         0 $value = $_[1];
314 0 0       0 if (defined($value)) {
315 0 0       0 if (ref($value) eq "") {
316             # set
317 0         0 $self->[I_HEADERS]{$key} = $value;
318 0         0 return($self);
319             }
320             } else {
321             # delete
322 0         0 delete($self->[I_HEADERS]{$key});
323 0         0 return($self);
324             }
325             }
326             }
327             # otherwise complain...
328 0         0 validate_pos(@_, @header_options);
329             }
330              
331             #+++############################################################################
332             # #
333             # parsing #
334             # #
335             #---############################################################################
336              
337             #
338             # parse the given buffer reference and return a hash of pointers to frame parts
339             # if the frame is complete or false otherwise; an optional hash can be given to
340             # represent state information from a previous parse on the exact same buffer
341             #
342             # note: for STOMP <1.2, we may miss a final \r in command or header as it would
343             # be part of the eol; up to the caller to be strict and check for its presence
344             # or to simply ignore this corner case for the sake of simplicity
345             #
346              
347             my %parse_options = (
348             state => { optional => 1, type => HASHREF },
349             );
350              
351             sub parse ($@) { ## no critic 'ProhibitExcessComplexity'
352 44     44 1 83613 my($bufref, %option, $state, $index, $buflen, $eol, $tmp);
353              
354             #
355             # setup
356             #
357 44 100       70 if ($Net::STOMP::Client::NoParamsValidation) {
358 11         19 ($bufref, %option) = @_;
359             } else {
360 33 50       92 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
361 33         29 $bufref = shift(@_);
362 33 100       427 %option = validate(@_, \%parse_options) if @_;
363             }
364 44   100     122 $state = $option{state} || {};
365             #
366             # before: allow 0 or more end-of-line characters
367             # (note: we allow \n and \r\n but also \r as EOL, this should not be a
368             # problem in practice)
369             #
370 44 50       72 unless (exists($state->{before_len})) {
371 44 100       50 return(0) unless ${$bufref} =~ /^[\r\n]*[^\r\n]/g;
  44         186  
372 35         27 $state->{before_len} = pos(${$bufref}) - 1;
  35         56  
373             }
374             #
375             # command: everything up to the first EOL
376             #
377 35 50       56 unless (exists($state->{command_len})) {
378 35         31 $state->{command_idx} = $state->{before_len};
379 35         26 $index = index(${$bufref}, "\n", $state->{command_idx});
  35         46  
380 35 100       97 return(0) if $index < 0;
381 33         34 $state->{command_len} = $index - $state->{command_idx};
382 33 100       20 if (substr(${$bufref}, $index - 1, 1) eq "\r") {
  33         61  
383 3         3 $state->{command_len}--;
384 3         4 $state->{command_eol} = 2;
385             } else {
386 30         30 $state->{command_eol} = 1;
387             }
388             }
389             #
390             # header: everything up to the first double EOL
391             #
392 33 50       45 unless (exists($state->{header_len})) {
393 33         35 $state->{header_idx} = $state->{command_idx} + $state->{command_len};
394 33         28 $eol = $state->{command_eol};
395 33         28 $tmp = $state->{header_idx} + $eol;
396 33         25 while (1) {
397 56         34 $index = index(${$bufref}, "\n", $tmp);
  56         45  
398 56 100       75 return(0) if $index < 0;
399 54 100 66     114 if ($index == $tmp) {
    100          
400 25         18 $state->{header_eol} = $eol + 1;
401 25         24 last;
402             } elsif ($index == $tmp + 1
403 6         17 and substr(${$bufref}, $tmp, 1) eq "\r") {
404 6         8 $state->{header_eol} = $eol + 2;
405 6         5 last;
406             }
407 23 100       14 $eol = substr(${$bufref}, $index - 1, 1) eq "\r" ? 2 : 1;
  23         39  
408 23         17 $tmp = $index + 1;
409             }
410 31         29 $index -= $state->{header_eol} - 1;
411 31 100       38 if ($index == $state->{header_idx}) {
412             # empty header
413 12         11 $state->{header_len} = 0;
414             } else {
415             # non-empty header
416 19         15 $state->{header_idx} += $state->{command_eol};
417 19         18 $state->{header_len} = $index - $state->{header_idx};
418 19         27 $tmp = substr(${$bufref}, $state->{header_idx} - 1,
419 19         16 $state->{header_len} + 3);
420 19 100       60 $state->{content_length} = $1
421             if $tmp =~ /\ncontent-length *: *(\d+) *\r?\n/;
422             }
423             }
424             #
425             # body: everything up to content-length bytes or the first NULL byte
426             #
427 31         25 $buflen = length(${$bufref});
  31         25  
428             $state->{body_idx} = $state->{header_idx} + $state->{header_len}
429 31         47 + $state->{header_eol};
430 31 100       41 if (exists($state->{content_length})) {
431             # length is known
432             return(0)
433 3 50       10 if $buflen < $state->{body_idx} + $state->{content_length} + 1;
434 3         4 $state->{body_len} = $state->{content_length};
435 3         3 $tmp = substr(${$bufref}, $state->{body_idx} + $state->{body_len}, 1);
  3         6  
436 3 100       11 dief("missing NULL byte at end of frame") unless $tmp eq "\0";
437             } else {
438             # length is not known
439 28         20 $index = index(${$bufref}, "\0", $state->{body_idx});
  28         29  
440 28 100       44 return(0) if $index < 0;
441 24         25 $state->{body_len} = $index - $state->{body_idx};
442             }
443             #
444             # after: allow 0 or more end-of-line characters
445             # (note: we allow \n and \r\n but also \r as EOL, this should not be a
446             # problem in practice)
447             #
448 26         26 $state->{after_idx} = $state->{body_idx} + $state->{body_len} + 1;
449 26         21 $state->{after_len} = 0;
450 26         41 while ($buflen > $state->{after_idx} + $state->{after_len}) {
451 13         13 $tmp = substr(${$bufref}, $state->{after_idx} + $state->{after_len}, 1);
  13         15  
452 13 100 100     43 last unless $tmp eq "\r" or $tmp eq "\n";
453 9         12 $state->{after_len}++;
454             }
455 26         25 $state->{total_len} = $state->{after_idx} + $state->{after_len};
456             # so far so good ;-)
457 26         52 return($state);
458             }
459              
460             #+++############################################################################
461             # #
462             # decoding #
463             # #
464             #---############################################################################
465              
466             #
467             # decode the given string reference and return a frame object if the frame is
468             # complete or false otherwise; take the same options as parse() plus debug
469             # and version
470             #
471             # side effect: in case a frame is successfully decoded, the given string is
472             # _modified_ to remove the corresponding encoded frame
473             #
474              
475             my %decode_options = (
476             debug => { optional => 1, type => UNDEF|SCALAR },
477             state => { optional => 1, type => HASHREF },
478             strict => { optional => 1, type => BOOLEAN },
479             version => { optional => 1, type => SCALAR, regex => qr/^1\.\d$/ },
480             );
481              
482             sub decode ($@) { ## no critic 'ProhibitExcessComplexity'
483 11     11 1 49 my($bufref, %option, $check, $state, $key, $val, $errors, $tmp, %frame);
484              
485             #
486             # setup
487             #
488 11 50       14 if ($Net::STOMP::Client::NoParamsValidation) {
489 0         0 ($bufref, %option) = @_;
490             } else {
491 11 50       25 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
492 11         10 $bufref = shift(@_);
493 11 50       166 %option = validate(@_, \%decode_options) if @_;
494             }
495 11   50     177 $option{debug} ||= "";
496 11   50     32 $state = $option{state} || {};
497 11 50       20 $option{strict} = $StrictEncode unless defined($option{strict});
498 11   50     16 $option{version} ||= "1.0";
499 11 50       22 $check = $option{strict} ? Encode::FB_CROAK : Encode::FB_DEFAULT;
500             #
501             # frame parsing
502             #
503             {
504 11         9 local $Net::STOMP::Client::NoParamsValidation = 1;
  11         9  
505 11         15 $tmp = parse($bufref, state => $state);
506             }
507 11 50       16 return(0) unless $tmp;
508             #
509             # frame debugging
510             #
511 11 50       19 if ($option{debug} =~ /\b(command|all)\b/) {
512 0         0 $tmp = substr(${$bufref}, $state->{command_idx}, $state->{command_len});
  0         0  
513 0         0 _debug_command("decoding", $tmp);
514             }
515 11 50       14 if ($option{debug} =~ /\b(header|all)\b/) {
516 0         0 $tmp = substr(${$bufref}, $state->{header_idx}, $state->{header_len});
  0         0  
517 0         0 _debug_header($tmp);
518             }
519 11 50       13 if ($option{debug} =~ /\b(body|all)\b/) {
520 0         0 $tmp = substr(${$bufref}, $state->{body_idx}, $state->{body_len});
  0         0  
521 0         0 _debug_body($tmp);
522             }
523             #
524             # frame decoding (command)
525             #
526             $frame{"command"} =
527 11         12 substr(${$bufref}, $state->{command_idx}, $state->{command_len});
  11         18  
528             dief("invalid command: %s", $frame{"command"})
529 11 50       53 unless $frame{"command"} =~ $new_options{"command"}{"regex"};
530             #
531             # frame decoding (headers)
532             #
533 11 50       15 if ($state->{header_len}) {
534 11         14 $frame{"headers"} = {};
535 11         9 $tmp = substr(${$bufref}, $state->{header_idx}, $state->{header_len});
  11         17  
536 11 100       20 if ($option{version} ge "1.1") {
537             # STOMP >=1.1 behavior: the header is assumed to be UTF-8 encoded
538 7         16 $tmp = Encode::decode("UTF-8", $tmp, $check);
539             }
540 11 100       323 if ($option{version} eq "1.0") {
    100          
541             # STOMP 1.0 behavior:
542             # - we arbitrarily restrict the header name as a safeguard
543             # - space surrounding the comma and at end of line is not significant
544             # - last header wins (not specified explicitly but reasonable default)
545 4         10 foreach my $line (split(/\n/, $tmp)) {
546 5 50       37 if ($line =~ /^($_HeaderNameRE)\s*:\s*(.*?)\s*$/o) {
547 5         20 $frame{"headers"}{$1} = $2;
548             } else {
549 0         0 dief("invalid header: %s", $line);
550             }
551             }
552             } elsif ($option{version} eq "1.1") {
553             # STOMP 1.1 behavior:
554             # - header names and values can contain any byte except \n or :
555             # - space is significant
556             # - only the first header entry should be used
557             # - handle backslash escaping
558 6         20 foreach my $line (split(/\n/, $tmp)) {
559 8 50       23 if ($line =~ /^([^\n\:]+):([^\n\:]*)$/) {
560 8         18 ($key, $val, $errors) = ($1, $2, 0);
561             } else {
562 0         0 dief("invalid header: %s", $line);
563             }
564 8 0       8 $key =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  0         0  
565 8 50       8 $val =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  3         12  
566 8 50       11 dief("invalid header: %s", $line) if $errors;
567             $frame{"headers"}{$key} = $val
568 8 100       27 unless exists($frame{"headers"}{$key});
569             }
570             } else {
571             # STOMP 1.2 behavior:
572             # - header names and values can contain any byte except \r or \n or :
573             # - space is significant
574             # - only the first header entry should be used
575             # - handle backslash escaping
576 1         7 foreach my $line (split(/\r?\n/, $tmp)) {
577 2 50       7 if ($line =~ /^([^\r\n\:]+):([^\r\n\:]*)$/) {
578 2         5 ($key, $val, $errors) = ($1, $2, 0);
579             } else {
580 0         0 dief("invalid header: %s", $line)
581             }
582 2 0       3 $key =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
583 2 0       2 $val =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
584 2 50       21 dief("invalid header: %s", $line) if $errors;
585             $frame{"headers"}{$key} = $val
586 2 50       7 unless exists($frame{"headers"}{$key});
587             }
588             }
589             }
590             #
591             # frame decoding (body)
592             #
593 11 50       16 if ($state->{body_len}) {
594 11         7 $tmp = substr(${$bufref}, $state->{body_idx}, $state->{body_len});
  11         21  
595 11 50 66     27 if ($option{version} ge "1.1" and $frame{"headers"}) {
596             # STOMP >=1.1 behavior: the body may be encoded
597 7         21 $val = _encoding($frame{"headers"}{"content-type"});
598 7 100       14 if ($val) {
599 1         7 $tmp = Encode::decode($val, $tmp, $check);
600             }
601             }
602 11         36 $frame{"body_reference"} = \$tmp;
603             }
604             #
605             # so far so good
606             #
607 11         7 substr(${$bufref}, 0, $state->{total_len}, "");
  11         19  
608 11         9 %{ $state } = ();
  11         19  
609 11         9 local $Net::STOMP::Client::NoParamsValidation = 1;
610 11         31 return(__PACKAGE__->new(%frame));
611             }
612              
613             #+++############################################################################
614             # #
615             # encoding #
616             # #
617             #---############################################################################
618              
619             #
620             # encode the given frame object and return a string reference; take the same
621             # options as decode() except state
622             #
623              
624             my %encode_options = (
625             debug => { optional => 1, type => UNDEF|SCALAR },
626             strict => { optional => 1, type => BOOLEAN },
627             version => { optional => 1, type => SCALAR, regex => qr/^1\.\d$/ },
628             );
629              
630             sub encode : method { ## no critic 'ProhibitExcessComplexity'
631 0     0 1 0 my($self, %option, $check, $header, $tmp);
632 0         0 my($body, $bodyref, $bodylen, $conlen, $key, $val);
633              
634             #
635             # setup
636             #
637 0 0       0 if ($Net::STOMP::Client::NoParamsValidation) {
638 0         0 ($self, %option) = @_;
639             } else {
640 0         0 $self = shift(@_);
641 0 0       0 %option = validate(@_, \%encode_options) if @_;
642             }
643 0   0     0 $option{debug} ||= "";
644 0 0       0 $option{strict} = $StrictEncode unless defined($option{strict});
645 0   0     0 $option{version} ||= "1.0";
646 0 0       0 $check = $option{strict} ? Encode::FB_CROAK : Encode::FB_DEFAULT;
647             #
648             # body encoding (must be done first because of the content-length header)
649             #
650 0 0       0 if ($option{version} ge "1.1") {
651 0         0 $tmp = _encoding($self->[I_HEADERS]{"content-type"});
652             } else {
653 0         0 $tmp = undef;
654             }
655 0 0       0 if ($tmp) {
656 0         0 $body = Encode::encode($tmp, ${ $self->[I_BODY] },
  0         0  
657             $check | Encode::LEAVE_SRC);
658 0         0 $bodyref = \$body;
659             } else {
660 0         0 $bodyref = $self->[I_BODY];
661             }
662 0         0 $bodylen = length(${ $bodyref });
  0         0  
663             #
664             # content-length header handling
665             #
666 0         0 $tmp = $self->[I_HEADERS]{"content-length"};
667 0 0       0 if (defined($tmp)) {
668             # content-length is defined: we use it unless it is the empty string
669             # (which means do not set the content-length even with a body)
670 0 0       0 $conlen = $tmp unless $tmp eq "";
671             } else {
672             # content-length is not defined (default behavior): we set it to the
673             # body length only if the body is not empty
674 0 0       0 $conlen = $bodylen unless $bodylen == 0;
675             }
676             #
677             # header encoding
678             #
679 0         0 $tmp = $self->[I_HEADERS];
680 0 0       0 if ($option{version} eq "1.0") {
    0          
681             # STOMP 1.0 behavior: no backslash escaping
682             $header = join("\n", map($_ . ":" . $tmp->{$_},
683 0         0 grep($_ ne "content-length", keys(%{ $tmp }))), "");
  0         0  
684             } elsif ($option{version} eq "1.1") {
685             # STOMP 1.1 behavior: backslash escaping
686 0         0 $header = "";
687 0         0 while (($key, $val) = each(%{ $tmp })) {
  0         0  
688 0 0       0 next if $key eq "content-length";
689 0         0 $key =~ s/($_EncSet1)/$_EncMap1{$1}/ego;
  0         0  
690 0         0 $val =~ s/($_EncSet1)/$_EncMap1{$1}/ego;
  0         0  
691 0         0 $header .= $key . ":" . $val . "\n";
692             }
693             } else {
694             # STOMP 1.2 behavior: backslash escaping
695 0         0 $header = "";
696 0         0 while (($key, $val) = each(%{ $tmp })) {
  0         0  
697 0 0       0 next if $key eq "content-length";
698 0         0 $key =~ s/($_EncSet2)/$_EncMap2{$1}/ego;
  0         0  
699 0         0 $val =~ s/($_EncSet2)/$_EncMap2{$1}/ego;
  0         0  
700 0         0 $header .= $key . ":" . $val . "\n";
701             }
702             }
703 0 0       0 $header .= "content-length:" . $conlen . "\n" if defined($conlen);
704 0 0       0 if ($option{version} ge "1.1") {
705             # STOMP >=1.1 behavior: the header must be UTF-8 encoded
706 0         0 $header = Encode::encode("UTF-8", $header, $check);
707             }
708             #
709             # frame debugging
710             #
711 0 0       0 if ($option{debug} =~ /\b(command|all)\b/) {
712 0         0 _debug_command("encoding", $self->[I_COMMAND]);
713             }
714 0 0       0 if ($option{debug} =~ /\b(header|all)\b/) {
715 0         0 _debug_header($header);
716             }
717 0 0       0 if ($option{debug} =~ /\b(body|all)\b/) {
718 0         0 _debug_body(${ $bodyref });
  0         0  
719             }
720             #
721             # assemble all the parts
722             #
723 0         0 $tmp = $self->[I_COMMAND] . "\n" . $header . "\n" . ${ $bodyref } . "\0";
  0         0  
724             # return a reference to the encoded frame
725 0         0 return(\$tmp);
726             }
727              
728             #
729             # FIXME: compatibility hack for Net::STOMP::Client 1.x (to be removed one day)
730             #
731              
732             sub check : method {
733 0     0 1 0 return(1);
734             }
735              
736             #+++############################################################################
737             # #
738             # integration with Messaging::Message #
739             # #
740             #---############################################################################
741              
742             #
743             # transform a frame into a message
744             #
745              
746             sub messagify : method {
747 0     0 1 0 my($self) = @_;
748              
749 0 0       0 unless ($Messaging::Message::VERSION) {
750 0         0 eval { require Messaging::Message };
  0         0  
751 0 0       0 dief("cannot load Messaging::Message: %s", $@) if $@;
752             }
753 0 0       0 return(Messaging::Message->new(
754             "header" => $self->headers(),
755             "body_ref" => $self->body_reference(),
756             "text" => _encoding($self->header("content-type")) ? 1 : 0,
757             ));
758             }
759              
760             #
761             # transform a message into a frame
762             #
763              
764             sub demessagify ($) {
765 0     0 1 0 my($message, $frame, $content_type);
766              
767             # FIXME: compatibility hack for Net::STOMP::Client 1.x (to be removed one day)
768 0 0 0     0 if (@_ == 1) {
    0          
769             # normal API, to become: my($message) = @_
770 0         0 $message = $_[0];
771             } elsif (@_ == 2 and $_[0] eq "Net::STOMP::Client::Frame") {
772             # old API, was a class method
773 0         0 shift(@_);
774 0         0 $message = $_[0];
775             }
776 0         0 validate_pos(@_, { isa => "Messaging::Message" });
777 0         0 $frame = __PACKAGE__->new(
778             "command" => "SEND",
779             "headers" => $message->header(),
780             "body_reference" => $message->body_ref(),
781             );
782             # handle the text attribute wrt the content-type header
783 0         0 $content_type = $frame->header("content-type");
784 0 0       0 if (defined($content_type)) {
785             # make sure the content-type is consistent with the message type
786 0 0       0 if (_encoding($content_type)) {
787 0 0       0 dief("unexpected text content-type for binary message: %s",
788             $content_type) unless $message->text();
789             } else {
790 0 0       0 dief("unexpected binary content-type for text message: %s",
791             $content_type) if $message->text();
792             }
793             } else {
794             # set a text content-type if it is missing (this is needed by STOMP >=1.1)
795 0 0       0 $frame->header("content-type", "text/unknown") if $message->text();
796             }
797 0         0 return($frame);
798             }
799              
800             #
801             # export control
802             #
803              
804             sub import : method {
805 3     3   13 my($pkg, %exported);
806              
807 3         4 $pkg = shift(@_);
808 3         7 grep($exported{$_}++, qw(demessagify));
809 3         11 export_control(scalar(caller()), $pkg, \%exported, @_);
810             }
811              
812             1;
813              
814             __END__