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   83573 use 5.005; # need the four-argument form of substr()
  5         40  
15 5     5   32 use strict;
  5         9  
  5         131  
16 5     5   27 use warnings;
  5         8  
  5         433  
17             our $VERSION = "2.5";
18             our $REVISION = sprintf("%d.%02d", q$Revision: 2.4 $ =~ /(\d+)\.(\d+)/);
19              
20             #
21             # used modules
22             #
23              
24 5     5   2383 use Encode qw();
  5         42528  
  5         138  
25 5     5   1944 use No::Worries::Die qw(dief);
  5         80316  
  5         41  
26 5     5   637 use No::Worries::Export qw(export_control);
  5         13  
  5         32  
27 5     5   2873 use No::Worries::Log qw(log_debug);
  5         186963  
  5         42  
28 5     5   715 use Params::Validate qw(validate validate_pos :types);
  5         12  
  5         862  
29              
30             #
31             # constants
32             #
33              
34 5     5   37 use constant I_COMMAND => 0;
  5         11  
  5         316  
35 5     5   34 use constant I_HEADERS => 1;
  5         11  
  5         253  
36 5     5   29 use constant I_BODY => 2; # stored as reference
  5         13  
  5         25396  
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       19 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         5 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 3434 my($class, %option, $object);
206              
207 20 100       42 if ($Net::STOMP::Client::NoParamsValidation) {
208 11         35 ($class, %option) = @_;
209             } else {
210 9         19 $class = shift(@_);
211 9 100       566 %option = validate(@_, \%new_options) if @_;
212             }
213 18 100       104 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     51 $option{"command"} ||= "SEND";
220 17   100     50 $option{"headers"} ||= {};
221 17   100     40 $option{"body_reference"} ||= \ "";
222 17         47 $object = [ @option{ qw(command headers body_reference) } ];
223 17         97 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         9 $self = shift(@_);
234 4 50       38 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 1061 my($self, $value);
279              
280 9         17 $self = shift(@_);
281 9 50       30 return(${ $self->[I_BODY] }) if @_ == 0;
  9         50  
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 26 my($self, $key, $value);
305              
306 8         14 $self = shift(@_);
307 8         15 $key = $_[0];
308 8 50 33     35 if (defined($key) and ref($key) eq "") {
309 8 50       23 if (@_ == 1) {
    0          
310             # get
311 8         39 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 126604 my($bufref, %option, $state, $index, $buflen, $eol, $tmp);
353              
354             #
355             # setup
356             #
357 44 100       110 if ($Net::STOMP::Client::NoParamsValidation) {
358 11         26 ($bufref, %option) = @_;
359             } else {
360 33 50       98 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
361 33         59 $bufref = shift(@_);
362 33 100       547 %option = validate(@_, \%parse_options) if @_;
363             }
364 44   100     190 $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       97 unless (exists($state->{before_len})) {
371 44 100       72 return(0) unless ${$bufref} =~ /^[\r\n]*[^\r\n]/g;
  44         241  
372 35         67 $state->{before_len} = pos(${$bufref}) - 1;
  35         87  
373             }
374             #
375             # command: everything up to the first EOL
376             #
377 35 50       96 unless (exists($state->{command_len})) {
378 35         65 $state->{command_idx} = $state->{before_len};
379 35         43 $index = index(${$bufref}, "\n", $state->{command_idx});
  35         81  
380 35 100       90 return(0) if $index < 0;
381 33         54 $state->{command_len} = $index - $state->{command_idx};
382 33 100       48 if (substr(${$bufref}, $index - 1, 1) eq "\r") {
  33         99  
383 3         6 $state->{command_len}--;
384 3         5 $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         64 $state->{header_idx} = $state->{command_idx} + $state->{command_len};
394 33         51 $eol = $state->{command_eol};
395 33         53 $tmp = $state->{header_idx} + $eol;
396 33         46 while (1) {
397 56         72 $index = index(${$bufref}, "\n", $tmp);
  56         96  
398 56 100       109 return(0) if $index < 0;
399 54 100 66     147 if ($index == $tmp) {
    100          
400 25         61 $state->{header_eol} = $eol + 1;
401 25         44 last;
402             } elsif ($index == $tmp + 1
403 6         24 and substr(${$bufref}, $tmp, 1) eq "\r") {
404 6         13 $state->{header_eol} = $eol + 2;
405 6         14 last;
406             }
407 23 100       29 $eol = substr(${$bufref}, $index - 1, 1) eq "\r" ? 2 : 1;
  23         52  
408 23         34 $tmp = $index + 1;
409             }
410 31         50 $index -= $state->{header_eol} - 1;
411 31 100       61 if ($index == $state->{header_idx}) {
412             # empty header
413 12         20 $state->{header_len} = 0;
414             } else {
415             # non-empty header
416 19         30 $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       75 $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         45 $buflen = length(${$bufref});
  31         54  
428             $state->{body_idx} = $state->{header_idx} + $state->{header_len}
429 31         91 + $state->{header_eol};
430 31 100       59 if (exists($state->{content_length})) {
431             # length is known
432             return(0)
433 3 50       14 if $buflen < $state->{body_idx} + $state->{content_length} + 1;
434 3         8 $state->{body_len} = $state->{content_length};
435 3         5 $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         42 $index = index(${$bufref}, "\0", $state->{body_idx});
  28         47  
440 28 100       69 return(0) if $index < 0;
441 24         42 $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         49 $state->{after_idx} = $state->{body_idx} + $state->{body_len} + 1;
449 26         40 $state->{after_len} = 0;
450 26         68 while ($buflen > $state->{after_idx} + $state->{after_len}) {
451 13         47 $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         20 $state->{after_len}++;
454             }
455 26         47 $state->{total_len} = $state->{after_idx} + $state->{after_len};
456             # so far so good ;-)
457 26         81 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 187 my($bufref, %option, $check, $state, $key, $val, $errors, $tmp, %frame);
484              
485             #
486             # setup
487             #
488 11 50       33 if ($Net::STOMP::Client::NoParamsValidation) {
489 0         0 ($bufref, %option) = @_;
490             } else {
491 11 50       63 validate_pos(@_, { type => SCALARREF }) unless ref($_[0]) eq "SCALAR";
492 11         23 $bufref = shift(@_);
493 11 50       198 %option = validate(@_, \%decode_options) if @_;
494             }
495 11   50     251 $option{debug} ||= "";
496 11   50     39 $state = $option{state} || {};
497 11 50       34 $option{strict} = $StrictEncode unless defined($option{strict});
498 11   50     22 $option{version} ||= "1.0";
499 11 50       26 $check = $option{strict} ? Encode::FB_CROAK : Encode::FB_DEFAULT;
500             #
501             # frame parsing
502             #
503             {
504 11         13 local $Net::STOMP::Client::NoParamsValidation = 1;
  11         21  
505 11         29 $tmp = parse($bufref, state => $state);
506             }
507 11 50       23 return(0) unless $tmp;
508             #
509             # frame debugging
510             #
511 11 50       25 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       23 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       32 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         13 substr(${$bufref}, $state->{command_idx}, $state->{command_len});
  11         32  
528             dief("invalid command: %s", $frame{"command"})
529 11 50       79 unless $frame{"command"} =~ $new_options{"command"}{"regex"};
530             #
531             # frame decoding (headers)
532             #
533 11 50       27 if ($state->{header_len}) {
534 11         23 $frame{"headers"} = {};
535 11         19 $tmp = substr(${$bufref}, $state->{header_idx}, $state->{header_len});
  11         41  
536 11 100       43 if ($option{version} ge "1.1") {
537             # STOMP >=1.1 behavior: the header is assumed to be UTF-8 encoded
538 7         24 $tmp = Encode::decode("UTF-8", $tmp, $check);
539             }
540 11 100       533 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       57 if ($line =~ /^($_HeaderNameRE)\s*:\s*(.*?)\s*$/o) {
547 5         27 $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         27 foreach my $line (split(/\n/, $tmp)) {
559 8 50       43 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       17 $key =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  0         0  
565 8 50       35 $val =~ s/(\\.)/$_DecMap1{$1}||$errors++/eg;
  3         19  
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       12 if ($line =~ /^([^\r\n\:]+):([^\r\n\:]*)$/) {
578 2         8 ($key, $val, $errors) = ($1, $2, 0);
579             } else {
580 0         0 dief("invalid header: %s", $line)
581             }
582 2 0       5 $key =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
583 2 0       4 $val =~ s/(\\.)/$_DecMap2{$1}||$errors++/eg;
  0         0  
584 2 50       6 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       37 if ($state->{body_len}) {
594 11         14 $tmp = substr(${$bufref}, $state->{body_idx}, $state->{body_len});
  11         27  
595 11 50 66     42 if ($option{version} ge "1.1" and $frame{"headers"}) {
596             # STOMP >=1.1 behavior: the body may be encoded
597 7         25 $val = _encoding($frame{"headers"}{"content-type"});
598 7 100       18 if ($val) {
599 1         4 $tmp = Encode::decode($val, $tmp, $check);
600             }
601             }
602 11         66 $frame{"body_reference"} = \$tmp;
603             }
604             #
605             # so far so good
606             #
607 11         16 substr(${$bufref}, 0, $state->{total_len}, "");
  11         32  
608 11         18 %{ $state } = ();
  11         31  
609 11         19 local $Net::STOMP::Client::NoParamsValidation = 1;
610 11         47 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   19 my($pkg, %exported);
806              
807 3         9 $pkg = shift(@_);
808 3         13 grep($exported{$_}++, qw(demessagify));
809 3         20 export_control(scalar(caller()), $pkg, \%exported, @_);
810             }
811              
812             1;
813              
814             __END__