#-- # Licensed to the Apache Software Foundation (ASF) under one # or more contributor license agreements. See the NOTICE file # distributed with this work for additional information # regarding copyright ownership. The ASF licenses this file # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. #++ module Qpid::Proton::Codec # +DataError+ is raised when an error occurs while encoding # or decoding data. class DataError < Exception; end # The +Data+ class provides an interface for decoding, extracting, # creating, and encoding arbitrary AMQP data. A +Data+ object # contains a tree of AMQP values. Leaf nodes in this tree correspond # to scalars in the AMQP type system such as INT or STRING. Interior # nodes in this tree correspond to compound values in the AMQP type # system such as *LIST*,*MAP*, *ARRAY*, or *DESCRIBED*. The root node # of the tree is the +Data+ object itself and can have an arbitrary # number of children. # # A +Data+ object maintains the notion of the current sibling node # and a current parent node. Siblings are ordered within their parent. # Values are accessed and/or added by using the #next, #prev, # #enter, and #exit methods to navigate to the desired location in # the tree and using the supplied variety of mutator and accessor # methods to access or add a value of the desired type. # # The mutator methods will always add a value _after_ the current node # in the tree. If the current node has a next sibling the mutator method # will overwrite the value on this node. If there is no current node # or the current node has no next sibling then one will be added. The # accessor methods always set the added/modified node to the current # node. The accessor methods read the value of the current node and do # not change which node is current. # # The following types of scalar values are supported: # # * NULL # * BOOL # * UBYTE # * BYTE # * USHORT # * SHORT # * UINT # * INT # * CHAR # * ULONG # * LONG # * TIMESTAMP # * FLOAT # * DOUBLE # * DECIMAL32 # * DECIMAL64 # * DECIMAL128 # * UUID # * BINARY # * STRING # * SYMBOL # # The following types of compound values are supported: # # * DESCRIBED # * ARRAY # * LIST # * MAP # class Data # Creates a new instance with the specified capacity. # # @param capacity [Fixnum, Object] The initial capacity or content. # def initialize(capacity = 16) if (!capacity.nil?) && (capacity.is_a?(Fixnum) || capacity.is_a?(Bignum)) @data = Cproton.pn_data(capacity) @free = true else @data = capacity @free = false end # destructor ObjectSpace.define_finalizer(self, self.class.finalize!(@data, @free)) end # @private def self.finalize!(data, free) proc { Cproton.pn_data_free(data) if free } end # @private def to_s tmp = Cproton.pn_string("") Cproton.pn_inspect(@data, tmp) result = Cproton.pn_string_get(tmp) Cproton.pn_free(tmp) return result end # Clears the object. # def clear Cproton.pn_data_clear(@data) end # Clears the current node and sets the parent to the root node. # # Clearing the current node sets it *before* the first node, calling # #next will advance to the first node. # def rewind Cproton.pn_data_rewind(@data) end # Advances the current node to its next sibling and returns its types. # # If there is no next sibling the current node remains unchanged # and nil is returned. # def next Cproton.pn_data_next(@data) end # Advances the current node to its previous sibling and returns its type. # # If there is no previous sibling then the current node remains unchanged # and nil is return. # def prev return Cproton.pn_data_prev(@data) ? type : nil end # Sets the parent node to the current node and clears the current node. # # Clearing the current node sets it _before_ the first child. # def enter Cproton.pn_data_enter(@data) end # Sets the current node to the parent node and the parent node to its own # parent. # def exit Cproton.pn_data_exit(@data) end # Returns the numeric type code of the current node. # # @return [Fixnum] The current node type. # @return [nil] If there is no current node. # def type_code dtype = Cproton.pn_data_type(@data) return (dtype == -1) ? nil : dtype end # Return the type object for the current node # # @param [Fixnum] The object type. # # @see #type_code # def type Mapping.for_code(type_code) end # Returns a representation of the data encoded in AMQP format. # # @return [String] The context of the Data as an AMQP data string. # # @example # # @data.string = "This is a test." # @encoded = @data.encode # # # @encoded now contains the text "This is a test." encoded for # # AMQP transport. # def encode buffer = "\0"*1024 loop do cd = Cproton.pn_data_encode(@data, buffer, buffer.length) if cd == Cproton::PN_OVERFLOW buffer *= 2 elsif cd >= 0 return buffer[0...cd] else check(cd) end end end # Decodes the first value from supplied AMQP data and returns the number # of bytes consumed. # # @param encoded [String] The encoded data. # # @example # # # SCENARIO: A string of encoded data, @encoded, contains the text # # of "This is a test." and is passed to an instance of Data # # for decoding. # # @data.decode(@encoded) # @data.string #=> "This is a test." # def decode(encoded) check(Cproton.pn_data_decode(@data, encoded, encoded.length)) end # Puts a list value. # # Elements may be filled by entering the list node and putting element # values. # # @example # # data = Qpid::Proton::Codec::Data.new # data.put_list # data.enter # data.int = 1 # data.int = 2 # data.int = 3 # data.exit # def put_list check(Cproton.pn_data_put_list(@data)) end # If the current node is a list, this returns the number of elements. # Otherwise, it returns zero. # # List elements can be accessed by entering the list. # # @example # # count = @data.list # @data.enter # (0...count).each # type = @data.next # puts "Value: #{@data.string}" if type == STRING # # ... process other node types # end def list Cproton.pn_data_get_list(@data) end # Puts a map value. # # Elements may be filled by entering the map node and putting alternating # key/value pairs. # # @example # # data = Qpid::Proton::Codec::Data.new # data.put_map # data.enter # data.string = "key" # data.string = "value" # data.exit # def put_map check(Cproton.pn_data_put_map(@data)) end # If the current node is a map, this returns the number of child # elements. Otherwise, it returns zero. # # Key/value pairs can be accessed by entering the map. # # @example # # count = @data.map # @data.enter # (0...count).each do # type = @data.next # puts "Key=#{@data.string}" if type == STRING # # ... process other key types # type = @data.next # puts "Value=#{@data.string}" if type == STRING # # ... process other value types # end # @data.exit def map Cproton.pn_data_get_map(@data) end # @private def get_map ::Hash.proton_data_get(self) end # Puts an array value. # # Elements may be filled by entering the array node and putting the # element values. The values must all be of the specified array element # type. # # If an array is *described* then the first child value of the array # is the descriptor and may be of any type. # # @param described [Boolean] True if the array is described. # @param element_type [Fixnum] The AMQP type for each element of the array. # # @example # # # create an array of integer values # data = Qpid::Proton::Codec::Data.new # data.put_array(false, INT) # data.enter # data.int = 1 # data.int = 2 # data.int = 3 # data.exit # # # create a described array of double values # data.put_array(true, DOUBLE) # data.enter # data.symbol = "array-descriptor" # data.double = 1.1 # data.double = 1.2 # data.double = 1.3 # data.exit # def put_array(described, element_type) check(Cproton.pn_data_put_array(@data, described, element_type.code)) end # If the current node is an array, returns a tuple of the element count, a # boolean indicating whether the array is described, and the type of each # element. Otherwise it returns +(0, false, nil). # # Array data can be accessed by entering the array. # # @example # # # get the details of thecurrent array # count, described, array_type = @data.array # # # enter the node # data.enter # # # get the next node # data.next # puts "Descriptor: #{data.symbol}" if described # (0...count).each do # @data.next # puts "Element: #{@data.string}" # end def array count = Cproton.pn_data_get_array(@data) described = Cproton.pn_data_is_array_described(@data) array_type = Cproton.pn_data_get_array_type(@data) return nil if array_type == -1 [count, described, Mapping.for_code(array_type) ] end # @private def get_array ::Array.proton_get(self) end # Puts a described value. # # A described node has two children, the descriptor and the value. # These are specified by entering the node and putting the # desired values. # # @example # # data = Qpid::Proton::Codec::Data.new # data.put_described # data.enter # data.symbol = "value-descriptor" # data.string = "the value" # data.exit # def put_described check(Cproton.pn_data_put_described(@data)) end # @private def get_described raise TypeError, "not a described type" unless self.described? self.enter self.next type = self.type descriptor = type.get(self) self.next type = self.type value = type.get(self) self.exit Qpid::Proton::Types::Described.new(descriptor, value) end # Checks if the current node is a described value. # # The described and value may be accessed by entering the described value. # # @example # # if @data.described? # @data.enter # puts "The symbol is #{@data.symbol}" # puts "The value is #{@data.string}" # end def described? Cproton.pn_data_is_described(@data) end # Puts a null value. # def null check(Cproton.pn_data_put_null(@data)) end # Utility method for Qpid::Proton::Codec::Mapping # # @private # def null=(value) null end # Puts an arbitrary object type. # # The Data instance will determine which AMQP type is appropriate and will # use that to encode the object. # # @param object [Object] The value. # def object=(object) Mapping.for_class(object.class).put(self, object) end # Gets the current node, based on how it was encoded. # # @return [Object] The current node. # def object type = self.type return nil if type.nil? type.get(data) end # Checks if the current node is null. # # @return [Boolean] True if the node is null. # def null? Cproton.pn_data_is_null(@data) end # Puts a boolean value. # # @param value [Boolean] The boolean value. # def bool=(value) check(Cproton.pn_data_put_bool(@data, value)) end # If the current node is a boolean, then it returns the value. Otherwise, # it returns false. # # @return [Boolean] The boolean value. # def bool Cproton.pn_data_get_bool(@data) end # Puts an unsigned byte value. # # @param value [Fixnum] The unsigned byte value. # def ubyte=(value) check(Cproton.pn_data_put_ubyte(@data, value)) end # If the current node is an unsigned byte, returns its value. Otherwise, # it returns 0. # # @return [Fixnum] The unsigned byte value. # def ubyte Cproton.pn_data_get_ubyte(@data) end # Puts a byte value. # # @param value [Fixnum] The byte value. # def byte=(value) check(Cproton.pn_data_put_byte(@data, value)) end # If the current node is an byte, returns its value. Otherwise, # it returns 0. # # @return [Fixnum] The byte value. # def byte Cproton.pn_data_get_byte(@data) end # Puts an unsigned short value. # # @param value [Fixnum] The unsigned short value # def ushort=(value) check(Cproton.pn_data_put_ushort(@data, value)) end # If the current node is an unsigned short, returns its value. Otherwise, # it returns 0. # # @return [Fixnum] The unsigned short value. # def ushort Cproton.pn_data_get_ushort(@data) end # Puts a short value. # # @param value [Fixnum] The short value. # def short=(value) check(Cproton.pn_data_put_short(@data, value)) end # If the current node is a short, returns its value. Otherwise, # returns a 0. # # @return [Fixnum] The short value. # def short Cproton.pn_data_get_short(@data) end # Puts an unsigned integer value. # # @param value [Fixnum] the unsigned integer value # def uint=(value) raise TypeError if value.nil? raise RangeError, "invalid uint: #{value}" if value < 0 check(Cproton.pn_data_put_uint(@data, value)) end # If the current node is an unsigned int, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The unsigned integer value. # def uint Cproton.pn_data_get_uint(@data) end # Puts an integer value. # # ==== Options # # * value - the integer value def int=(value) check(Cproton.pn_data_put_int(@data, value)) end # If the current node is an integer, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The integer value. # def int Cproton.pn_data_get_int(@data) end # Puts a character value. # # @param value [Fixnum] The character value. # def char=(value) check(Cproton.pn_data_put_char(@data, value)) end # If the current node is a character, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The character value. # def char Cproton.pn_data_get_char(@data) end # Puts an unsigned long value. # # @param value [Fixnum] The unsigned long value. # def ulong=(value) raise TypeError if value.nil? raise RangeError, "invalid ulong: #{value}" if value < 0 check(Cproton.pn_data_put_ulong(@data, value)) end # If the current node is an unsigned long, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The unsigned long value. # def ulong Cproton.pn_data_get_ulong(@data) end # Puts a long value. # # @param value [Fixnum] The long value. # def long=(value) check(Cproton.pn_data_put_long(@data, value)) end # If the current node is a long, returns its value. Otherwise, returns 0. # # @return [Fixnum] The long value. def long Cproton.pn_data_get_long(@data) end # Puts a timestamp value. # # @param value [Fixnum] The timestamp value. # def timestamp=(value) value = value.to_i if (!value.nil? && value.is_a?(Time)) check(Cproton.pn_data_put_timestamp(@data, value)) end # If the current node is a timestamp, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The timestamp value. # def timestamp Cproton.pn_data_get_timestamp(@data) end # Puts a float value. # # @param value [Float] The floating point value. # def float=(value) check(Cproton.pn_data_put_float(@data, value)) end # If the current node is a float, returns its value. Otherwise, # returns 0. # # @return [Float] The floating point value. # def float Cproton.pn_data_get_float(@data) end # Puts a double value. # # @param value [Float] The double precision floating point value. # def double=(value) check(Cproton.pn_data_put_double(@data, value)) end # If the current node is a double, returns its value. Otherwise, # returns 0. # # @return [Float] The double precision floating point value. # def double Cproton.pn_data_get_double(@data) end # Puts a decimal32 value. # # @param value [Fixnum] The decimal32 value. # def decimal32=(value) check(Cproton.pn_data_put_decimal32(@data, value)) end # If the current node is a decimal32, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The decimal32 value. # def decimal32 Cproton.pn_data_get_decimal32(@data) end # Puts a decimal64 value. # # @param value [Fixnum] The decimal64 value. # def decimal64=(value) check(Cproton.pn_data_put_decimal64(@data, value)) end # If the current node is a decimal64, returns its value. Otherwise, # it returns 0. # # @return [Fixnum] The decimal64 value. # def decimal64 Cproton.pn_data_get_decimal64(@data) end # Puts a decimal128 value. # # @param value [Fixnum] The decimal128 value. # def decimal128=(value) raise TypeError, "invalid decimal128 value: #{value}" if value.nil? value = value.to_s(16).rjust(32, "0") bytes = [] value.scan(/(..)/) {|v| bytes << v[0].to_i(16)} check(Cproton.pn_data_put_decimal128(@data, bytes)) end # If the current node is a decimal128, returns its value. Otherwise, # returns 0. # # @return [Fixnum] The decimal128 value. # def decimal128 value = "" Cproton.pn_data_get_decimal128(@data).each{|val| value += ("%02x" % val)} value.to_i(16) end # Puts a +UUID+ value. # # The UUID is expected to be in the format of a string or else a 128-bit # integer value. # # @param value [String, Numeric] A string or numeric representation of the UUID. # # @example # # # set a uuid value from a string value # require 'securerandom' # @data.uuid = SecureRandom.uuid # # # or # @data.uuid = "fd0289a5-8eec-4a08-9283-81d02c9d2fff" # # # set a uuid value from a 128-bit value # @data.uuid = 0 # sets to 00000000-0000-0000-0000-000000000000 # def uuid=(value) raise ::ArgumentError, "invalid uuid: #{value}" if value.nil? # if the uuid that was submitted was numeric value, then translated # it into a hex string, otherwise assume it was a string represtation # and attempt to decode it if value.is_a? Numeric value = "%032x" % value else raise ::ArgumentError, "invalid uuid: #{value}" if !valid_uuid?(value) value = (value[0, 8] + value[9, 4] + value[14, 4] + value[19, 4] + value[24, 12]) end bytes = [] value.scan(/(..)/) {|v| bytes << v[0].to_i(16)} check(Cproton.pn_data_put_uuid(@data, bytes)) end # If the current value is a +UUID+, returns its value. Otherwise, # it returns nil. # # @return [String] The string representation of the UUID. # def uuid value = "" Cproton.pn_data_get_uuid(@data).each{|val| value += ("%02x" % val)} value.insert(8, "-").insert(13, "-").insert(18, "-").insert(23, "-") end # Puts a binary value. # # A binary string is encoded as an ASCII 8-bit string value. This is in # contranst to other strings, which are treated as UTF-8 encoded. # # @param value [String] An arbitrary string value. # # @see #string= # def binary=(value) check(Cproton.pn_data_put_binary(@data, value)) end # If the current node is binary, returns its value. Otherwise, it returns # an empty string (""). # # @return [String] The binary string. # # @see #string # def binary Qpid::Proton::Types::BinaryString.new(Cproton.pn_data_get_binary(@data)) end # Puts a UTF-8 encoded string value. # # *NOTE:* A nil value is stored as an empty string rather than as a nil. # # @param value [String] The UTF-8 encoded string value. # # @see #binary= # def string=(value) check(Cproton.pn_data_put_string(@data, value)) end # If the current node is a string, returns its value. Otherwise, it # returns an empty string (""). # # @return [String] The UTF-8 encoded string. # # @see #binary # def string Qpid::Proton::Types::UTFString.new(Cproton.pn_data_get_string(@data)) end # Puts a symbolic value. # # @param value [String] The symbolic string value. # def symbol=(value) check(Cproton.pn_data_put_symbol(@data, value)) end # If the current node is a symbol, returns its value. Otherwise, it # returns an empty string (""). # # @return [String] The symbolic string value. # def symbol Cproton.pn_data_get_symbol(@data) end # Get the current value as a single object. # # @return [Object] The current node's object. # # @see #type_code # @see #type # def get type.get(self); end # Puts a new value with the given type into the current node. # # @param value [Object] The value. # @param type_code [Mapping] The value's type. # # @private # def put(value, type_code); type_code.put(self, value); end private def valid_uuid?(value) # ensure that the UUID is in the right format # xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx value =~ /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/ end # @private def check(err) if err < 0 raise DataError, "[#{err}]: #{Cproton.pn_data_error(@data)}" else return err end end end end