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   83496 use 5.005; # need the four-argument form of substr()
  5         41  
15 5     5   30 use strict;
  5         10  
  5         131  
16 5     5   26 use warnings;
  5         11  
  5         495  
17             our $VERSION = "2.4";
18             our $REVISION = sprintf("%d.%02d", q$Revision: 2.4 $ =~ /(\d+)\.(\d+)/);
19              
20             #
21             # used modules
22             #
23              
24 5     5   2326 use Encode qw();
  5         43659  
  5         149  
25 5     5   1893 use No::Worries::Die qw(dief);
  5         80927  
  5         39  
26 5     5   655 use No::Worries::Export qw(export_control);
  5         11  
  5         32  
27 5     5   2806 use No::Worries::Log qw(log_debug);
  5         181204  
  5         42  
28 5     5   676 use Params::Validate qw(validate validate_pos :types);
  5         14  
  5         828  
29              
30             #
31             # constants
32             #
33              
34 5     5   37 use constant I_COMMAND => 0;
  5         10  
  5         300  
35 5     5   34 use constant I_HEADERS => 1;
  5         10  
  5         216  
36 5     5   29 use constant I_BODY => 2; # stored as reference
  5         12  
  5         25507  
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   24 my($type) = @_;
86              
87 7 100       17 if ($type) {
88 2 100       18 if ($type =~ /^text\/[\w\-]+$/) {
    50          
89 1         4 return("UTF-8");
90             } elsif (";$type;" =~ /\;\s*charset=\"?([\w\-]+)\"?\s*\;/) {
91 0         0 return($1);
92             } else {
93 1         3 return(undef);
94             }
95             } else {
96 5         12 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 3594 my($class, %option, $object);
206              
207 20 100       47 if ($Net::STOMP::Client::NoParamsValidation) {
208 11         35 ($class, %option) = @_;
209             } else {
210 9         39 $class = shift(@_);
211 9 100       593 %option = validate(@_, \%new_options) if @_;
212             }
213 18 100       108 if (defined($option{"body"})) {
214             # handle the convenient body option
215             dief("options body and body_reference are " .
216 3 100       19 "mutually exclusive") if $option{"body_reference"};
217 2         7 $option{"body_reference"} = \ delete($option{"body"});
218             }
219 17   100     55 $option{"command"} ||= "SEND";
220 17   100     49 $option{"headers"} ||= {};
221 17   100     41 $option{"body_reference"} ||= \ "";
222 17         43 $object = [ @option{ qw(command headers body_reference) } ];
223 17         103 return(bless($object, $class));
224             }
225              
226             #
227             # standard getters and setters
228             #
229              
230             sub command : method {
231 4     4 1 19 my($self, $value);
232              
233 4         8 $self = shift(@_);
234 4 50       30 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 1139 my($self, $value);
279              
280 9         17 $self = shift(@_);
281 9 50       29 return(${ $self->[I_BODY] }) if @_ == 0;
  9         55  
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 27 my($self, $key, $value);
305              
306 8         16 $self = shift(@_);
307 8         13 $key = $_[0];
308 8 50 33     34 if (defined($key) and ref($key) eq "") {
309 8 50       21 if (@_ == 1) {
    0          
310             # get
311 8         43 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 127816 my($bufref, %option, $state, $index, $buflen, $eol, $tmp);
353              
354             #
355             # setup
356             #
357 44 100       101 if ($Net::STOMP::Client::NoParamsValidation) {
358 11         29 ($bufref, %option) = @_;
359             } else {
360 33 50       104 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
361 33         64 $bufref = shift(@_);
362 33 100       569 %option = validate(@_, \%parse_options) if @_;
363             }
364 44   100     193 $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       101 unless (exists($state->{before_len})) {
371 44 100       61 return(0) unless ${$bufref} =~ /^[\r\n]*[^\r\n]/g;
  44         257  
372 35         60 $state->{before_len} = pos(${$bufref}) - 1;
  35         90  
373             }
374             #
375             # command: everything up to the first EOL
376             #
377 35 50       98 unless (exists($state->{command_len})) {
378 35         76 $state->{command_idx} = $state->{before_len};
379 35         63 $index = index(${$bufref}, "\n", $state->{command_idx});
  35         86  
380 35 100       93 return(0) if $index < 0;
381 33         64 $state->{command_len} = $index - $state->{command_idx};
382 33 100       43 if (substr(${$bufref}, $index - 1, 1) eq "\r") {
  33         93  
383 3         6 $state->{command_len}--;
384 3         7 $state->{command_eol} = 2;
385             } else {
386 30         53 $state->{command_eol} = 1;
387             }
388             }
389             #
390             # header: everything up to the first double EOL
391             #
392 33 50       74 unless (exists($state->{header_len})) {
393 33         73 $state->{header_idx} = $state->{command_idx} + $state->{command_len};
394 33         49 $eol = $state->{command_eol};
395 33         59 $tmp = $state->{header_idx} + $eol;
396 33         47 while (1) {
397 56         74 $index = index(${$bufref}, "\n", $tmp);
  56         93  
398 56 100       116 return(0) if $index < 0;
399 54 100 66     144 if ($index == $tmp) {
    100          
400 25         44 $state->{header_eol} = $eol + 1;
401 25         43 last;
402             } elsif ($index == $tmp + 1
403 6         28 and substr(${$bufref}, $tmp, 1) eq "\r") {
404 6         12 $state->{header_eol} = $eol + 2;
405 6         11 last;
406             }
407 23 100       34 $eol = substr(${$bufref}, $index - 1, 1) eq "\r" ? 2 : 1;
  23         57  
408 23         31 $tmp = $index + 1;
409             }
410 31         52 $index -= $state->{header_eol} - 1;
411 31 100       64 if ($index == $state->{header_idx}) {
412             # empty header
413 12         23 $state->{header_len} = 0;
414             } else {
415             # non-empty header
416 19         36 $state->{header_idx} += $state->{command_eol};
417 19         37 $state->{header_len} = $index - $state->{header_idx};
418 19         52 $tmp = substr(${$bufref}, $state->{header_idx} - 1,
419 19         25 $state->{header_len} + 3);
420 19 100       67 $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         47 $buflen = length(${$bufref});
  31         54  
428             $state->{body_idx} = $state->{header_idx} + $state->{header_len}
429 31         93 + $state->{header_eol};
430 31 100       65 if (exists($state->{content_length})) {
431             # length is known
432             return(0)
433 3 50       13 if $buflen < $state->{body_idx} + $state->{content_length} + 1;
434 3         9 $state->{body_len} = $state->{content_length};
435 3         6 $tmp = substr(${$bufref}, $state->{body_idx} + $state->{body_len}, 1);
  3         8  
436 3 100       16 dief("missing NULL byte at end of frame") unless $tmp eq "\0";
437             } else {
438             # length is not known
439 28         47 $index = index(${$bufref}, "\0", $state->{body_idx});
  28         53  
440 28 100       69 return(0) if $index < 0;
441 24         45 $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         52 $state->{after_idx} = $state->{body_idx} + $state->{body_len} + 1;
449 26         38 $state->{after_len} = 0;
450 26         75 while ($buflen > $state->{after_idx} + $state->{after_len}) {
451 13         20 $tmp = substr(${$bufref}, $state->{after_idx} + $state->{after_len}, 1);
  13         31  
452 13 100 100     53 last unless $tmp eq "\r" or $tmp eq "\n";
453 9         19 $state->{after_len}++;
454             }
455 26         46 $state->{total_len} = $state->{after_idx} + $state->{after_len};
456             # so far so good ;-)
457 26         79 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 212 my($bufref, %option, $check, $state, $key, $val, $errors, $tmp, %frame);
484              
485             #
486             # setup
487             #
488 11 50       26 if ($Net::STOMP::Client::NoParamsValidation) {
489 0         0 ($bufref, %option) = @_;
490             } else {
491 11 50       38 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
492 11         21 $bufref = shift(@_);
493 11 50       194 %option = validate(@_, \%decode_options) if @_;
494             }
495 11   50     246 $option{debug} ||= "";
496 11   50     39 $state = $option{state} || {};
497 11 50       34 $option{strict} = $StrictEncode unless defined($option{strict});
498 11   50     26 $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         14 local $Net::STOMP::Client::NoParamsValidation = 1;
  11         19  
505 11         26 $tmp = parse($bufref, state => $state);
506             }
507 11 50       26 return(0) unless $tmp;
508             #
509             # frame debugging
510             #
511 11 50       24 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       22 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       21 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         15 substr(${$bufref}, $state->{command_idx}, $state->{command_len});
  11         32  
528             dief("invalid command: %s", $frame{"command"})
529 11 50       83 unless $frame{"command"} =~ $new_options{"command"}{"regex"};
530             #
531             # frame decoding (headers)
532             #
533 11 50       27 if ($state->{header_len}) {
534 11         24 $frame{"headers"} = {};
535 11         16 $tmp = substr(${$bufref}, $state->{header_idx}, $state->{header_len});
  11         25  
536 11 100       39 if ($option{version} ge "1.1") {
537             # STOMP >=1.1 behavior: the header is assumed to be UTF-8 encoded
538 7         26 $tmp = Encode::decode("UTF-8", $tmp, $check);
539             }
540 11 100       529 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         16 foreach my $line (split(/\n/, $tmp)) {
546 5 50       56 if ($line =~ /^($_HeaderNameRE)\s*:\s*(.*?)\s*$/o) {
547 5         23 $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         26 foreach my $line (split(/\n/, $tmp)) {
559 8 50       42 if ($line =~ /^([^\n\:]+):([^\n\:]*)$/) {
560 8         42 ($key, $val, $errors) = ($1, $2, 0);
561             } else {
562 0         0 dief("invalid header: %s", $line);
563             }
564 8 0       15 $key =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  0         0  
565 8 50       35 $val =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  3         18  
566 8 50       20 dief("invalid header: %s", $line) if $errors;
567             $frame{"headers"}{$key} = $val
568 8 100       36 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         11 foreach my $line (split(/\r?\n/, $tmp)) {
577 2 50       13 if ($line =~ /^([^\r\n\:]+):([^\r\n\:]*)$/) {
578 2         6 ($key, $val, $errors) = ($1, $2, 0);
579             } else {
580 0         0 dief("invalid header: %s", $line)
581             }
582 2 0       6 $key =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
583 2 0       6 $val =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
584 2 50       5 dief("invalid header: %s", $line) if $errors;
585             $frame{"headers"}{$key} = $val
586 2 50       9 unless exists($frame{"headers"}{$key});
587             }
588             }
589             }
590             #
591             # frame decoding (body)
592             #
593 11 50       27 if ($state->{body_len}) {
594 11         18 $tmp = substr(${$bufref}, $state->{body_idx}, $state->{body_len});
  11         25  
595 11 50 66     39 if ($option{version} ge "1.1" and $frame{"headers"}) {
596             # STOMP >=1.1 behavior: the body may be encoded
597 7         22 $val = _encoding($frame{"headers"}{"content-type"});
598 7 100       19 if ($val) {
599 1         4 $tmp = Encode::decode($val, $tmp, $check);
600             }
601             }
602 11         63 $frame{"body_reference"} = \$tmp;
603             }
604             #
605             # so far so good
606             #
607 11         18 substr(${$bufref}, 0, $state->{total_len}, "");
  11         53  
608 11         17 %{ $state } = ();
  11         32  
609 11         14 local $Net::STOMP::Client::NoParamsValidation = 1;
610 11         48 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   24 my($pkg, %exported);
806              
807 3         10 $pkg = shift(@_);
808 3         12 grep($exported{$_}++, qw(demessagify));
809 3         22 export_control(scalar(caller()), $pkg, \%exported, @_);
810             }
811              
812             1;
813              
814             __END__