blob: 9f0069d688ac556444bc00ee9a8ac4750cd45fe2 [file] [log] [blame]
Jake Farrellb5a18a12012-10-09 01:10:43 +00001# encoding: UTF-8
Jake Farrell6f0f5272012-01-31 03:39:30 +00002#
3# Licensed to the Apache Software Foundation (ASF) under one
4# or more contributor license agreements. See the NOTICE file
5# distributed with this work for additional information
6# regarding copyright ownership. The ASF licenses this file
7# to you under the Apache License, Version 2.0 (the
8# "License"); you may not use this file except in compliance
9# with the License. You may obtain a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing,
14# software distributed under the License is distributed on an
15# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16# KIND, either express or implied. See the License for the
17# specific language governing permissions and limitations
18# under the License.
19#
20
Jake Farrell6f0f5272012-01-31 03:39:30 +000021
22module Thrift
23 class LookaheadReader
24 def initialize(trans)
25 @trans = trans
26 @hasData = false
27 @data = nil
28 end
29
30 def read
31 if @hasData
32 @hasData = false
33 else
34 @data = @trans.read(1)
35 end
36
37 return @data
38 end
39
40 def peek
41 if !@hasData
42 @data = @trans.read(1)
43 end
44 @hasData = true
45 return @data
46 end
47 end
48
49 #
50 # Class to serve as base JSON context and as base class for other context
51 # implementations
52 #
53 class JSONContext
Jens Geyer3bb141d2013-08-14 21:33:16 +020054 @@kJSONElemSeparator = ','
Jake Farrell6f0f5272012-01-31 03:39:30 +000055 #
56 # Write context data to the trans. Default is to do nothing.
57 #
58 def write(trans)
59 end
60
61 #
62 # Read context data from the trans. Default is to do nothing.
63 #
64 def read(reader)
65 end
66
67 #
68 # Return true if numbers need to be escaped as strings in this context.
69 # Default behavior is to return false.
70 #
71 def escapeNum
72 return false
73 end
74 end
75
76 # Context class for object member key-value pairs
77 class JSONPairContext < JSONContext
Jens Geyer3bb141d2013-08-14 21:33:16 +020078 @@kJSONPairSeparator = ':'
79
Jake Farrell6f0f5272012-01-31 03:39:30 +000080 def initialize
81 @first = true
82 @colon = true
83 end
84
85 def write(trans)
86 if (@first)
87 @first = false
88 @colon = true
89 else
90 trans.write(@colon ? @@kJSONPairSeparator : @@kJSONElemSeparator)
91 @colon = !@colon
92 end
93 end
94
95 def read(reader)
96 if (@first)
97 @first = false
98 @colon = true
99 else
100 ch = (@colon ? @@kJSONPairSeparator : @@kJSONElemSeparator)
101 @colon = !@colon
102 JsonProtocol::read_syntax_char(reader, ch)
103 end
104 end
105
106 # Numbers must be turned into strings if they are the key part of a pair
107 def escapeNum
108 return @colon
109 end
110 end
111
112 # Context class for lists
113 class JSONListContext < JSONContext
114
115 def initialize
116 @first = true
117 end
118
119 def write(trans)
120 if (@first)
121 @first = false
122 else
123 trans.write(@@kJSONElemSeparator)
124 end
125 end
126
127 def read(reader)
128 if (@first)
129 @first = false
130 else
131 JsonProtocol::read_syntax_char(reader, @@kJSONElemSeparator)
132 end
133 end
134 end
135
136 class JsonProtocol < BaseProtocol
Jens Geyer3bb141d2013-08-14 21:33:16 +0200137
138 @@kJSONObjectStart = '{'
139 @@kJSONObjectEnd = '}'
140 @@kJSONArrayStart = '['
141 @@kJSONArrayEnd = ']'
142 @@kJSONNewline = '\n'
143 @@kJSONBackslash = '\\'
144 @@kJSONStringDelimiter = '"'
145
146 @@kThriftVersion1 = 1
147
148 @@kThriftNan = "NaN"
149 @@kThriftInfinity = "Infinity"
150 @@kThriftNegativeInfinity = "-Infinity"
151
Jake Farrell6f0f5272012-01-31 03:39:30 +0000152 def initialize(trans)
153 super(trans)
154 @context = JSONContext.new
155 @contexts = Array.new
156 @reader = LookaheadReader.new(trans)
157 end
158
159 def get_type_name_for_type_id(id)
160 case id
161 when Types::BOOL
162 "tf"
163 when Types::BYTE
164 "i8"
165 when Types::I16
166 "i16"
167 when Types::I32
168 "i32"
169 when Types::I64
170 "i64"
171 when Types::DOUBLE
172 "dbl"
173 when Types::STRING
174 "str"
175 when Types::STRUCT
176 "rec"
177 when Types::MAP
178 "map"
179 when Types::SET
180 "set"
181 when Types::LIST
182 "lst"
183 else
184 raise NotImplementedError
185 end
186 end
187
188 def get_type_id_for_type_name(name)
189 if (name == "tf")
190 result = Types::BOOL
191 elsif (name == "i8")
192 result = Types::BYTE
193 elsif (name == "i16")
194 result = Types::I16
195 elsif (name == "i32")
196 result = Types::I32
197 elsif (name == "i64")
198 result = Types::I64
199 elsif (name == "dbl")
200 result = Types::DOUBLE
201 elsif (name == "str")
202 result = Types::STRING
203 elsif (name == "rec")
204 result = Types::STRUCT
205 elsif (name == "map")
206 result = Types::MAP
207 elsif (name == "set")
208 result = Types::SET
209 elsif (name == "lst")
210 result = Types::LIST
211 else
212 result = Types::STOP
213 end
214 if (result == Types::STOP)
215 raise NotImplementedError
216 end
217 return result
218 end
219
220 # Static helper functions
221
222 # Read 1 character from the trans and verify that it is the expected character ch.
223 # Throw a protocol exception if it is not.
224 def self.read_syntax_char(reader, ch)
225 ch2 = reader.read
226 if (ch2 != ch)
227 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Expected \'#{ch}\' got \'#{ch2}\'.")
228 end
229 end
230
231 # Return true if the character ch is in [-+0-9.Ee]; false otherwise
232 def is_json_numeric(ch)
233 case ch
234 when '+', '-', '.', '0' .. '9', 'E', "e"
235 return true
236 else
237 return false
238 end
239 end
240
241 def push_context(context)
242 @contexts.push(@context)
243 @context = context
244 end
245
246 def pop_context
247 @context = @contexts.pop
248 end
249
250 # Write the character ch as a JSON escape sequence ("\u00xx")
251 def write_json_escape_char(ch)
252 trans.write('\\u')
253 ch_value = ch[0]
254 if (ch_value.kind_of? String)
255 ch_value = ch.bytes.first
256 end
257 trans.write(ch_value.to_s(16).rjust(4,'0'))
258 end
259
260 # Write the character ch as part of a JSON string, escaping as appropriate.
261 def write_json_char(ch)
262 # This table describes the handling for the first 0x30 characters
263 # 0 : escape using "\u00xx" notation
264 # 1 : just output index
265 # <other> : escape using "\<other>" notation
266 kJSONCharTable = [
267 # 0 1 2 3 4 5 6 7 8 9 A B C D E F
268 0, 0, 0, 0, 0, 0, 0, 0,'b','t','n', 0,'f','r', 0, 0, # 0
269 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, # 1
270 1, 1,'"', 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, # 2
271 ]
272
273 ch_value = ch[0]
274 if (ch_value.kind_of? String)
275 ch_value = ch.bytes.first
276 end
277 if (ch_value >= 0x30)
278 if (ch == @@kJSONBackslash) # Only special character >= 0x30 is '\'
279 trans.write(@@kJSONBackslash)
280 trans.write(@@kJSONBackslash)
281 else
282 trans.write(ch)
283 end
284 else
285 outCh = kJSONCharTable[ch_value];
286 # Check if regular character, backslash escaped, or JSON escaped
287 if outCh.kind_of? String
288 trans.write(@@kJSONBackslash)
289 trans.write(outCh)
290 elsif outCh == 1
291 trans.write(ch)
292 else
293 write_json_escape_char(ch)
294 end
295 end
296 end
297
298 # Write out the contents of the string str as a JSON string, escaping characters as appropriate.
299 def write_json_string(str)
300 @context.write(trans)
301 trans.write(@@kJSONStringDelimiter)
302 str.split('').each do |ch|
303 write_json_char(ch)
304 end
305 trans.write(@@kJSONStringDelimiter)
306 end
307
308 # Write out the contents of the string as JSON string, base64-encoding
309 # the string's contents, and escaping as appropriate
310 def write_json_base64(str)
311 @context.write(trans)
312 trans.write(@@kJSONStringDelimiter)
313 write_json_string([str].pack("m"))
314 trans.write(@@kJSONStringDelimiter)
315 end
316
317 # Convert the given integer type to a JSON number, or a string
318 # if the context requires it (eg: key in a map pair).
319 def write_json_integer(num)
320 @context.write(trans)
321 escapeNum = @context.escapeNum
322 if (escapeNum)
323 trans.write(@@kJSONStringDelimiter)
324 end
325 trans.write(num.to_s);
326 if (escapeNum)
327 trans.write(@@kJSONStringDelimiter)
328 end
329 end
330
331 # Convert the given double to a JSON string, which is either the number,
332 # "NaN" or "Infinity" or "-Infinity".
333 def write_json_double(num)
334 @context.write(trans)
335 # Normalize output of boost::lexical_cast for NaNs and Infinities
336 special = false;
337 if (num.nan?)
338 special = true;
339 val = @@kThriftNan;
340 elsif (num.infinite?)
341 special = true;
342 val = @@kThriftInfinity;
343 if (num < 0.0)
344 val = @@kThriftNegativeInfinity;
345 end
346 else
347 val = num.to_s
348 end
349
350 escapeNum = special || @context.escapeNum
351 if (escapeNum)
352 trans.write(@@kJSONStringDelimiter)
353 end
354 trans.write(val)
355 if (escapeNum)
356 trans.write(@@kJSONStringDelimiter)
357 end
358 end
359
360 def write_json_object_start
361 @context.write(trans)
362 trans.write(@@kJSONObjectStart)
363 push_context(JSONPairContext.new);
364 end
365
366 def write_json_object_end
367 pop_context
368 trans.write(@@kJSONObjectEnd)
369 end
370
371 def write_json_array_start
372 @context.write(trans)
373 trans.write(@@kJSONArrayStart)
374 push_context(JSONListContext.new);
375 end
376
377 def write_json_array_end
378 pop_context
379 trans.write(@@kJSONArrayEnd)
380 end
381
382 def write_message_begin(name, type, seqid)
383 write_json_array_start
384 write_json_integer(@@kThriftVersion1)
385 write_json_string(name)
386 write_json_integer(type)
387 write_json_integer(seqid)
388 end
389
390 def write_message_end
391 write_json_array_end
392 end
393
394 def write_struct_begin(name)
395 write_json_object_start
396 end
397
398 def write_struct_end
399 write_json_object_end
400 end
401
402 def write_field_begin(name, type, id)
403 write_json_integer(id)
404 write_json_object_start
405 write_json_string(get_type_name_for_type_id(type))
406 end
407
408 def write_field_end
409 write_json_object_end
410 end
411
412 def write_field_stop; nil; end
413
414 def write_map_begin(ktype, vtype, size)
415 write_json_array_start
416 write_json_string(get_type_name_for_type_id(ktype))
417 write_json_string(get_type_name_for_type_id(vtype))
418 write_json_integer(size)
419 write_json_object_start
420 end
421
422 def write_map_end
423 write_json_object_end
424 write_json_array_end
425 end
426
427 def write_list_begin(etype, size)
428 write_json_array_start
429 write_json_string(get_type_name_for_type_id(etype))
430 write_json_integer(size)
431 end
432
433 def write_list_end
434 write_json_array_end
435 end
436
437 def write_set_begin(etype, size)
438 write_json_array_start
439 write_json_string(get_type_name_for_type_id(etype))
440 write_json_integer(size)
441 end
442
443 def write_set_end
444 write_json_array_end
445 end
446
447 def write_bool(bool)
448 write_json_integer(bool ? 1 : 0)
449 end
450
451 def write_byte(byte)
452 write_json_integer(byte)
453 end
454
455 def write_i16(i16)
456 write_json_integer(i16)
457 end
458
459 def write_i32(i32)
460 write_json_integer(i32)
461 end
462
463 def write_i64(i64)
464 write_json_integer(i64)
465 end
466
467 def write_double(dub)
468 write_json_double(dub)
469 end
470
471 def write_string(str)
472 write_json_string(str)
473 end
474
475 def write_binary(str)
476 write_json_base64(str)
477 end
478
479 ##
480 # Reading functions
481 ##
482
483 # Reads 1 byte and verifies that it matches ch.
484 def read_json_syntax_char(ch)
485 JsonProtocol::read_syntax_char(@reader, ch)
486 end
487
488 # Decodes the four hex parts of a JSON escaped string character and returns
Jake Farrellb5a18a12012-10-09 01:10:43 +0000489 # the character via out.
490 #
491 # Note - this only supports Unicode characters in the BMP (U+0000 to U+FFFF);
492 # characters above the BMP are encoded as two escape sequences (surrogate pairs),
493 # which is not yet implemented
Jake Farrell6f0f5272012-01-31 03:39:30 +0000494 def read_json_escape_char
Jake Farrell6f0f5272012-01-31 03:39:30 +0000495 str = @reader.read
496 str += @reader.read
Jake Farrellb5a18a12012-10-09 01:10:43 +0000497 str += @reader.read
498 str += @reader.read
499 if RUBY_VERSION >= '1.9'
500 str.hex.chr(Encoding::UTF_8)
501 else
502 str.hex.chr
503 end
Jake Farrell6f0f5272012-01-31 03:39:30 +0000504 end
505
506 # Decodes a JSON string, including unescaping, and returns the string via str
507 def read_json_string(skipContext = false)
508 # This string's characters must match up with the elements in escape_char_vals.
509 # I don't have '/' on this list even though it appears on www.json.org --
Jens Geyer1fb68472013-12-26 18:55:33 +0100510 # it is not in the RFC -> it is. See RFC 4627
511 escape_chars = "\"\\/bfnrt"
Jake Farrell6f0f5272012-01-31 03:39:30 +0000512
513 # The elements of this array must match up with the sequence of characters in
514 # escape_chars
515 escape_char_vals = [
Jens Geyer1fb68472013-12-26 18:55:33 +0100516 '"', '\\', '/', '\b', '\f', '\n', '\r', '\t',
Jake Farrell6f0f5272012-01-31 03:39:30 +0000517 ]
518
519 if !skipContext
520 @context.read(@reader)
521 end
522 read_json_syntax_char(@@kJSONStringDelimiter)
523 ch = ""
524 str = ""
525 while (true)
526 ch = @reader.read
527 if (ch == @@kJSONStringDelimiter)
528 break
529 end
530 if (ch == @@kJSONBackslash)
531 ch = @reader.read
532 if (ch == 'u')
533 ch = read_json_escape_char
534 else
535 pos = escape_chars.index(ch);
536 if (pos.nil?) # not found
537 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Expected control char, got \'#{ch}\'.")
538 end
539 ch = escape_char_vals[pos]
540 end
541 end
542 str += ch
543 end
544 return str
545 end
546
547 # Reads a block of base64 characters, decoding it, and returns via str
548 def read_json_base64
549 read_json_string.unpack("m")[0]
550 end
551
552 # Reads a sequence of characters, stopping at the first one that is not
553 # a valid JSON numeric character.
554 def read_json_numeric_chars
555 str = ""
556 while (true)
557 ch = @reader.peek
558 if (!is_json_numeric(ch))
559 break;
560 end
561 ch = @reader.read
562 str += ch
563 end
564 return str
565 end
566
567 # Reads a sequence of characters and assembles them into a number,
568 # returning them via num
569 def read_json_integer
570 @context.read(@reader)
571 if (@context.escapeNum)
572 read_json_syntax_char(@@kJSONStringDelimiter)
573 end
574 str = read_json_numeric_chars
575
576 begin
577 num = Integer(str);
578 rescue
579 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Expected numeric value; got \"#{str}\"")
580 end
581
582 if (@context.escapeNum)
583 read_json_syntax_char(@@kJSONStringDelimiter)
584 end
585
586 return num
587 end
588
589 # Reads a JSON number or string and interprets it as a double.
590 def read_json_double
591 @context.read(@reader)
592 num = 0
593 if (@reader.peek == @@kJSONStringDelimiter)
594 str = read_json_string(true)
595 # Check for NaN, Infinity and -Infinity
596 if (str == @@kThriftNan)
597 num = (+1.0/0.0)/(+1.0/0.0)
598 elsif (str == @@kThriftInfinity)
599 num = +1.0/0.0
600 elsif (str == @@kThriftNegativeInfinity)
601 num = -1.0/0.0
602 else
603 if (!@context.escapeNum)
604 # Raise exception -- we should not be in a string in this case
605 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Numeric data unexpectedly quoted")
606 end
607 begin
608 num = Float(str)
609 rescue
610 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Expected numeric value; got \"#{str}\"")
611 end
612 end
613 else
614 if (@context.escapeNum)
615 # This will throw - we should have had a quote if escapeNum == true
616 read_json_syntax_char(@@kJSONStringDelimiter)
617 end
618 str = read_json_numeric_chars
619 begin
620 num = Float(str)
621 rescue
622 raise ProtocolException.new(ProtocolException::INVALID_DATA, "Expected numeric value; got \"#{str}\"")
623 end
624 end
625 return num
626 end
627
628 def read_json_object_start
629 @context.read(@reader)
630 read_json_syntax_char(@@kJSONObjectStart)
631 push_context(JSONPairContext.new)
632 nil
633 end
634
635 def read_json_object_end
636 read_json_syntax_char(@@kJSONObjectEnd)
637 pop_context
638 nil
639 end
640
641 def read_json_array_start
642 @context.read(@reader)
643 read_json_syntax_char(@@kJSONArrayStart)
644 push_context(JSONListContext.new)
645 nil
646 end
647
648 def read_json_array_end
649 read_json_syntax_char(@@kJSONArrayEnd)
650 pop_context
651 nil
652 end
653
654 def read_message_begin
655 read_json_array_start
656 version = read_json_integer
657 if (version != @@kThriftVersion1)
658 raise ProtocolException.new(ProtocolException::BAD_VERSION, 'Message contained bad version.')
659 end
660 name = read_json_string
661 message_type = read_json_integer
662 seqid = read_json_integer
663 [name, message_type, seqid]
664 end
665
666 def read_message_end
667 read_json_array_end
668 nil
669 end
670
671 def read_struct_begin
672 read_json_object_start
673 nil
674 end
675
676 def read_struct_end
677 read_json_object_end
678 nil
679 end
680
681 def read_field_begin
682 # Check if we hit the end of the list
683 ch = @reader.peek
684 if (ch == @@kJSONObjectEnd)
685 field_type = Types::STOP
686 else
687 field_id = read_json_integer
688 read_json_object_start
689 field_type = get_type_id_for_type_name(read_json_string)
690 end
691 [nil, field_type, field_id]
692 end
693
694 def read_field_end
695 read_json_object_end
696 end
697
698 def read_map_begin
699 read_json_array_start
700 key_type = get_type_id_for_type_name(read_json_string)
701 val_type = get_type_id_for_type_name(read_json_string)
702 size = read_json_integer
703 read_json_object_start
704 [key_type, val_type, size]
705 end
706
707 def read_map_end
708 read_json_object_end
709 read_json_array_end
710 end
711
712 def read_list_begin
713 read_json_array_start
714 [get_type_id_for_type_name(read_json_string), read_json_integer]
715 end
716
717 def read_list_end
718 read_json_array_end
719 end
720
721 def read_set_begin
722 read_json_array_start
Roger Meier772b2b12013-01-19 21:04:12 +0100723 [get_type_id_for_type_name(read_json_string), read_json_integer]
Jake Farrell6f0f5272012-01-31 03:39:30 +0000724 end
725
726 def read_set_end
727 read_json_array_end
728 end
729
730 def read_bool
731 byte = read_byte
732 byte != 0
733 end
734
735 def read_byte
736 read_json_integer
737 end
738
739 def read_i16
740 read_json_integer
741 end
742
743 def read_i32
744 read_json_integer
745 end
746
747 def read_i64
748 read_json_integer
749 end
750
751 def read_double
752 read_json_double
753 end
754
755 def read_string
756 read_json_string
757 end
758
759 def read_binary
760 read_json_base64
761 end
762 end
763
764 class JsonProtocolFactory < BaseProtocolFactory
765 def get_protocol(trans)
766 return Thrift::JsonProtocol.new(trans)
767 end
768 end
769end