rack-amf 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +55 -0
- data/Rakefile +54 -0
- data/lib/amf/class_mapping.rb +210 -0
- data/lib/amf/common.rb +28 -0
- data/lib/amf/constants.rb +46 -0
- data/lib/amf/pure/deserializer.rb +353 -0
- data/lib/amf/pure/io_helpers.rb +94 -0
- data/lib/amf/pure/remoting.rb +120 -0
- data/lib/amf/pure/serializer.rb +218 -0
- data/lib/amf/pure.rb +14 -0
- data/lib/amf/values/array_collection.rb +9 -0
- data/lib/amf/values/messages.rb +133 -0
- data/lib/amf/values/typed_hash.rb +13 -0
- data/lib/amf/version.rb +9 -0
- data/lib/amf.rb +17 -0
- data/lib/rack/amf/application.rb +32 -0
- data/lib/rack/amf/request.rb +15 -0
- data/lib/rack/amf/response.rb +54 -0
- data/lib/rack/amf/service_manager.rb +35 -0
- data/lib/rack/amf.rb +17 -0
- data/spec/amf/class_mapping_set_spec.rb +34 -0
- data/spec/amf/class_mapping_spec.rb +109 -0
- data/spec/amf/deserializer_spec.rb +301 -0
- data/spec/amf/remoting_spec.rb +37 -0
- data/spec/amf/serializer_spec.rb +254 -0
- data/spec/amf/values/array_collection_spec.rb +6 -0
- data/spec/amf/values/messages_spec.rb +27 -0
- data/spec/rack/request_spec.rb +5 -0
- data/spec/rack/response_spec.rb +46 -0
- data/spec/rack/service_manager_spec.rb +26 -0
- data/spec/spec_helper.rb +24 -0
- metadata +97 -0
@@ -0,0 +1,353 @@
|
|
1
|
+
require 'amf/pure/io_helpers'
|
2
|
+
|
3
|
+
module AMF
|
4
|
+
module Pure
|
5
|
+
# Pure ruby deserializer
|
6
|
+
#--
|
7
|
+
# AMF0 deserializer, it switches over to AMF3 when it sees the switch flag
|
8
|
+
class Deserializer
|
9
|
+
def initialize
|
10
|
+
@ref_cache = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(source, type=nil)
|
14
|
+
source = StringIO.new(source) unless StringIO === source
|
15
|
+
type = read_int8 source unless type
|
16
|
+
case type
|
17
|
+
when AMF0_NUMBER_MARKER
|
18
|
+
read_number source
|
19
|
+
when AMF0_BOOLEAN_MARKER
|
20
|
+
read_boolean source
|
21
|
+
when AMF0_STRING_MARKER
|
22
|
+
read_string source
|
23
|
+
when AMF0_OBJECT_MARKER
|
24
|
+
read_object source
|
25
|
+
when AMF0_NULL_MARKER
|
26
|
+
nil
|
27
|
+
when AMF0_UNDEFINED_MARKER
|
28
|
+
nil
|
29
|
+
when AMF0_REFERENCE_MARKER
|
30
|
+
read_reference source
|
31
|
+
when AMF0_HASH_MARKER
|
32
|
+
read_hash source
|
33
|
+
when AMF0_STRICT_ARRAY_MARKER
|
34
|
+
read_array source
|
35
|
+
when AMF0_DATE_MARKER
|
36
|
+
read_date source
|
37
|
+
when AMF0_LONG_STRING_MARKER
|
38
|
+
read_string source, true
|
39
|
+
when AMF0_UNSUPPORTED_MARKER
|
40
|
+
nil
|
41
|
+
when AMF0_XML_MARKER
|
42
|
+
#read_xml source
|
43
|
+
when AMF0_TYPED_OBJECT_MARKER
|
44
|
+
read_typed_object source
|
45
|
+
when AMF0_AMF3_MARKER
|
46
|
+
AMF3Deserializer.new.deserialize(source)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
include AMF::Pure::ReadIOHelpers
|
52
|
+
|
53
|
+
def read_number source
|
54
|
+
res = read_double source
|
55
|
+
res.is_a?(Float)&&res.nan? ? nil : res # check for NaN and convert them to nil
|
56
|
+
end
|
57
|
+
|
58
|
+
def read_boolean source
|
59
|
+
read_int8(source) != 0
|
60
|
+
end
|
61
|
+
|
62
|
+
def read_string source, long=false
|
63
|
+
len = long ? read_word32_network(source) : read_word16_network(source)
|
64
|
+
source.read(len)
|
65
|
+
end
|
66
|
+
|
67
|
+
def read_object source
|
68
|
+
obj = {}
|
69
|
+
while true
|
70
|
+
key = read_string source
|
71
|
+
type = read_int8 source
|
72
|
+
break if type == AMF0_OBJECT_END_MARKER
|
73
|
+
obj[key.to_sym] = deserialize(source, type)
|
74
|
+
end
|
75
|
+
@ref_cache << obj
|
76
|
+
obj
|
77
|
+
end
|
78
|
+
|
79
|
+
def read_reference source
|
80
|
+
index = read_word16_network(source)
|
81
|
+
@ref_cache[index]
|
82
|
+
end
|
83
|
+
|
84
|
+
def read_hash source
|
85
|
+
len = read_word32_network(source) # Read and ignore length
|
86
|
+
|
87
|
+
# Read first pair
|
88
|
+
key = read_string source
|
89
|
+
type = read_int8 source
|
90
|
+
return [] if type == AMF0_OBJECT_END_MARKER
|
91
|
+
|
92
|
+
# We need to figure out whether this is a real hash, or whether some stupid serializer gave up
|
93
|
+
if key.to_i.to_s == key
|
94
|
+
# Array
|
95
|
+
obj = []
|
96
|
+
obj[key.to_i] = deserialize(source, type)
|
97
|
+
while true
|
98
|
+
key = read_string source
|
99
|
+
type = read_int8 source
|
100
|
+
break if type == AMF0_OBJECT_END_MARKER
|
101
|
+
obj[key.to_i] = deserialize(source, type)
|
102
|
+
end
|
103
|
+
else
|
104
|
+
# Hash
|
105
|
+
obj = {key.to_sym => deserialize(source, type)}
|
106
|
+
while true
|
107
|
+
key = read_string source
|
108
|
+
type = read_int8 source
|
109
|
+
break if type == AMF0_OBJECT_END_MARKER
|
110
|
+
obj[key.to_sym] = deserialize(source, type)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
@ref_cache << obj
|
114
|
+
obj
|
115
|
+
end
|
116
|
+
|
117
|
+
def read_array source
|
118
|
+
len = read_word32_network(source)
|
119
|
+
array = []
|
120
|
+
0.upto(len - 1) do
|
121
|
+
array << deserialize(source)
|
122
|
+
end
|
123
|
+
@ref_cache << array
|
124
|
+
array
|
125
|
+
end
|
126
|
+
|
127
|
+
def read_date source
|
128
|
+
seconds = read_double(source).to_f/1000
|
129
|
+
time = Time.at(seconds)
|
130
|
+
tz = read_word16_network(source) # Unused
|
131
|
+
time
|
132
|
+
end
|
133
|
+
|
134
|
+
def read_typed_object source
|
135
|
+
class_name = read_string source
|
136
|
+
props = read_object source
|
137
|
+
@ref_cache.pop
|
138
|
+
|
139
|
+
obj = ClassMapper.get_ruby_obj class_name
|
140
|
+
ClassMapper.populate_ruby_obj obj, props, {}
|
141
|
+
@ref_cache << obj
|
142
|
+
obj
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# AMF3 implementation of deserializer, loaded automatically by the AMF0
|
147
|
+
# deserializer when needed
|
148
|
+
class AMF3Deserializer
|
149
|
+
def initialize
|
150
|
+
@string_cache = []
|
151
|
+
@object_cache = []
|
152
|
+
@trait_cache = []
|
153
|
+
end
|
154
|
+
|
155
|
+
def deserialize(source, type=nil)
|
156
|
+
source = StringIO.new(source) unless StringIO === source
|
157
|
+
type = read_int8 source unless type
|
158
|
+
case type
|
159
|
+
when AMF3_UNDEFINED_MARKER
|
160
|
+
nil
|
161
|
+
when AMF3_NULL_MARKER
|
162
|
+
nil
|
163
|
+
when AMF3_FALSE_MARKER
|
164
|
+
false
|
165
|
+
when AMF3_TRUE_MARKER
|
166
|
+
true
|
167
|
+
when AMF3_INTEGER_MARKER
|
168
|
+
read_integer source
|
169
|
+
when AMF3_DOUBLE_MARKER
|
170
|
+
read_number source
|
171
|
+
when AMF3_STRING_MARKER
|
172
|
+
read_string source
|
173
|
+
when AMF3_XML_DOC_MARKER
|
174
|
+
#read_xml_string
|
175
|
+
when AMF3_DATE_MARKER
|
176
|
+
read_date source
|
177
|
+
when AMF3_ARRAY_MARKER
|
178
|
+
read_array source
|
179
|
+
when AMF3_OBJECT_MARKER
|
180
|
+
read_object source
|
181
|
+
when AMF3_XML_MARKER
|
182
|
+
#read_amf3_xml
|
183
|
+
when AMF3_BYTE_ARRAY_MARKER
|
184
|
+
#read_amf3_byte_array
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
private
|
189
|
+
include AMF::Pure::ReadIOHelpers
|
190
|
+
|
191
|
+
def read_integer source
|
192
|
+
n = 0
|
193
|
+
b = read_word8(source) || 0
|
194
|
+
result = 0
|
195
|
+
|
196
|
+
while ((b & 0x80) != 0 && n < 3)
|
197
|
+
result = result << 7
|
198
|
+
result = result | (b & 0x7f)
|
199
|
+
b = read_word8(source) || 0
|
200
|
+
n = n + 1
|
201
|
+
end
|
202
|
+
|
203
|
+
if (n < 3)
|
204
|
+
result = result << 7
|
205
|
+
result = result | b
|
206
|
+
else
|
207
|
+
#Use all 8 bits from the 4th byte
|
208
|
+
result = result << 8
|
209
|
+
result = result | b
|
210
|
+
|
211
|
+
#Check if the integer should be negative
|
212
|
+
if (result > MAX_INTEGER)
|
213
|
+
result -= (1 << 29)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
result
|
217
|
+
end
|
218
|
+
|
219
|
+
def read_number source
|
220
|
+
res = read_double source
|
221
|
+
res.is_a?(Float)&&res.nan? ? nil : res # check for NaN and convert them to nil
|
222
|
+
end
|
223
|
+
|
224
|
+
def read_string source
|
225
|
+
type = read_integer source
|
226
|
+
isReference = (type & 0x01) == 0
|
227
|
+
|
228
|
+
if isReference
|
229
|
+
reference = type >> 1
|
230
|
+
return @string_cache[reference]
|
231
|
+
else
|
232
|
+
length = type >> 1
|
233
|
+
#HACK needed for ['',''] array of empty strings
|
234
|
+
#It may be better to take one more parameter that
|
235
|
+
#would specify whether or not they expect us to return
|
236
|
+
#a string
|
237
|
+
str = "" #if stringRequest
|
238
|
+
if length > 0
|
239
|
+
str = source.read(length)
|
240
|
+
@string_cache << str
|
241
|
+
end
|
242
|
+
return str
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def read_array source
|
247
|
+
type = read_integer source
|
248
|
+
isReference = (type & 0x01) == 0
|
249
|
+
|
250
|
+
if isReference
|
251
|
+
reference = type >> 1
|
252
|
+
return @object_cache[reference]
|
253
|
+
else
|
254
|
+
length = type >> 1
|
255
|
+
propertyName = read_string source
|
256
|
+
if propertyName != ""
|
257
|
+
array = {}
|
258
|
+
@object_cache << array
|
259
|
+
begin
|
260
|
+
while(propertyName.length)
|
261
|
+
value = deserialize(source)
|
262
|
+
array[propertyName] = value
|
263
|
+
propertyName = read_string source
|
264
|
+
end
|
265
|
+
rescue Exception => e #end of object exception, because propertyName.length will be non existent
|
266
|
+
end
|
267
|
+
0.upto(length - 1) do |i|
|
268
|
+
array["" + i.to_s] = deserialize(source)
|
269
|
+
end
|
270
|
+
else
|
271
|
+
array = []
|
272
|
+
@object_cache << array
|
273
|
+
0.upto(length - 1) do
|
274
|
+
array << deserialize(source)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
array
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
def read_object source
|
282
|
+
type = read_integer source
|
283
|
+
isReference = (type & 0x01) == 0
|
284
|
+
|
285
|
+
if isReference
|
286
|
+
reference = type >> 1
|
287
|
+
return @object_cache[reference]
|
288
|
+
else
|
289
|
+
class_type = type >> 1
|
290
|
+
class_is_reference = (class_type & 0x01) == 0
|
291
|
+
|
292
|
+
if class_is_reference
|
293
|
+
reference = class_type >> 1
|
294
|
+
class_definition = @trait_cache[reference]
|
295
|
+
else
|
296
|
+
class_name = read_string source
|
297
|
+
externalizable = (class_type & 0x02) != 0
|
298
|
+
dynamic = (class_type & 0x04) != 0
|
299
|
+
attribute_count = class_type >> 3
|
300
|
+
|
301
|
+
class_attributes = []
|
302
|
+
attribute_count.times{class_attributes << read_string(source)} # Read class members
|
303
|
+
|
304
|
+
class_definition = {"class_name" => class_name,
|
305
|
+
"members" => class_attributes,
|
306
|
+
"externalizable" => externalizable,
|
307
|
+
"dynamic" => dynamic}
|
308
|
+
@trait_cache << class_definition
|
309
|
+
end
|
310
|
+
|
311
|
+
obj = ClassMapper.get_ruby_obj class_definition["class_name"]
|
312
|
+
@object_cache << obj
|
313
|
+
|
314
|
+
if class_definition['externalizable']
|
315
|
+
obj.externalized_data = deserialize(source)
|
316
|
+
else
|
317
|
+
props = {}
|
318
|
+
class_definition['members'].each do |key|
|
319
|
+
value = deserialize(source)
|
320
|
+
props[key.to_sym] = value
|
321
|
+
end
|
322
|
+
|
323
|
+
dynamic_props = nil
|
324
|
+
if class_definition['dynamic']
|
325
|
+
dynamic_props = {}
|
326
|
+
while (key = read_string source) && key.length != 0 do # read next key
|
327
|
+
value = deserialize(source)
|
328
|
+
dynamic_props[key.to_sym] = value
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
ClassMapper.populate_ruby_obj obj, props, dynamic_props
|
333
|
+
end
|
334
|
+
obj
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def read_date source
|
339
|
+
type = read_integer source
|
340
|
+
isReference = (type & 0x01) == 0
|
341
|
+
if isReference
|
342
|
+
reference = type >> 1
|
343
|
+
return @object_cache[reference]
|
344
|
+
else
|
345
|
+
seconds = read_double(source).to_f/1000
|
346
|
+
time = Time.at(seconds)
|
347
|
+
@object_cache << time
|
348
|
+
time
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
module AMF
|
2
|
+
module Pure
|
3
|
+
module ReadIOHelpers #:nodoc:
|
4
|
+
def read_int8 source
|
5
|
+
source.read(1).unpack('c').first
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_word8 source
|
9
|
+
source.read(1).unpack('C').first
|
10
|
+
end
|
11
|
+
|
12
|
+
def read_double source
|
13
|
+
source.read(8).unpack('G').first
|
14
|
+
end
|
15
|
+
|
16
|
+
def read_word16_network source
|
17
|
+
source.read(2).unpack('n').first
|
18
|
+
end
|
19
|
+
|
20
|
+
def read_int16_network source
|
21
|
+
str = source.read(2)
|
22
|
+
str.reverse! if byte_order_little? # swap bytes as native=little (and we want network)
|
23
|
+
str.unpack('s').first
|
24
|
+
end
|
25
|
+
|
26
|
+
def read_word32_network source
|
27
|
+
source.read(4).unpack('N').first
|
28
|
+
end
|
29
|
+
|
30
|
+
def byte_order
|
31
|
+
if [0x12345678].pack("L") == "\x12\x34\x56\x78"
|
32
|
+
:BigEndian
|
33
|
+
else
|
34
|
+
:LittleEndian
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def byte_order_little?
|
39
|
+
(byte_order == :LittleEndian) ? true : false;
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
module WriteIOHelpers #:nodoc:
|
44
|
+
def pack_integer(integer)
|
45
|
+
integer = integer & 0x1fffffff
|
46
|
+
if(integer < 0x80)
|
47
|
+
[integer].pack('c')
|
48
|
+
elsif(integer < 0x4000)
|
49
|
+
[integer >> 7 & 0x7f | 0x80].pack('c')+
|
50
|
+
[integer & 0x7f].pack('c')
|
51
|
+
elsif(integer < 0x200000)
|
52
|
+
[integer >> 14 & 0x7f | 0x80].pack('c') +
|
53
|
+
[integer >> 7 & 0x7f | 0x80].pack('c') +
|
54
|
+
[integer & 0x7f].pack('c')
|
55
|
+
else
|
56
|
+
[integer >> 22 & 0x7f | 0x80].pack('c')+
|
57
|
+
[integer >> 15 & 0x7f | 0x80].pack('c')+
|
58
|
+
[integer >> 8 & 0x7f | 0x80].pack('c')+
|
59
|
+
[integer & 0xff].pack('c')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def pack_double(double)
|
64
|
+
[double].pack('G')
|
65
|
+
end
|
66
|
+
|
67
|
+
def pack_int8(val)
|
68
|
+
[val].pack('c')
|
69
|
+
end
|
70
|
+
|
71
|
+
def pack_int16_network(val)
|
72
|
+
[val].pack('n')
|
73
|
+
end
|
74
|
+
|
75
|
+
def pack_word32_network(val)
|
76
|
+
str = [val].pack('L')
|
77
|
+
str.reverse! if byte_order_little? # swap bytes as native=little (and we want network)
|
78
|
+
str
|
79
|
+
end
|
80
|
+
|
81
|
+
def byte_order
|
82
|
+
if [0x12345678].pack("L") == "\x12\x34\x56\x78"
|
83
|
+
:BigEndian
|
84
|
+
else
|
85
|
+
:LittleEndian
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def byte_order_little?
|
90
|
+
(byte_order == :LittleEndian) ? true : false;
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require 'amf/pure/io_helpers'
|
2
|
+
|
3
|
+
module AMF
|
4
|
+
module Pure
|
5
|
+
# AMF request object wrapper, it is responsible for deserializing AMF requests
|
6
|
+
class Request
|
7
|
+
attr_reader :amf_version, :headers, :messages
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@amf_version = 0
|
11
|
+
@headers = []
|
12
|
+
@messages = []
|
13
|
+
end
|
14
|
+
|
15
|
+
def populate_from_stream stream
|
16
|
+
stream = StringIO.new(stream) unless StringIO === stream
|
17
|
+
|
18
|
+
# Read AMF version
|
19
|
+
@amf_version = read_word16_network stream
|
20
|
+
|
21
|
+
# Read in headers
|
22
|
+
header_count = read_word16_network stream
|
23
|
+
0.upto(header_count-1) do
|
24
|
+
name = stream.read(read_word16_network(stream))
|
25
|
+
must_understand = read_int8(stream) != 0
|
26
|
+
length = read_word32_network stream
|
27
|
+
data = AMF.deserialize stream
|
28
|
+
@headers << Header.new(name, must_understand, data)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read in messages
|
32
|
+
message_count = read_word16_network stream
|
33
|
+
0.upto(message_count-1) do
|
34
|
+
target_uri = stream.read(read_word16_network(stream))
|
35
|
+
response_uri = stream.read(read_word16_network(stream))
|
36
|
+
length = read_word32_network stream
|
37
|
+
data = AMF.deserialize stream
|
38
|
+
if data.is_a?(Array) && data.length == 1 && data[0].is_a?(::AMF::Values::AbstractMessage)
|
39
|
+
data = data[0]
|
40
|
+
end
|
41
|
+
@messages << Message.new(target_uri, response_uri, data)
|
42
|
+
end
|
43
|
+
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
include AMF::Pure::ReadIOHelpers
|
49
|
+
end
|
50
|
+
|
51
|
+
# AMF response object wrapper, it is responsible for serializing the AMF response
|
52
|
+
class Response
|
53
|
+
attr_accessor :amf_version, :headers, :messages
|
54
|
+
|
55
|
+
def initialize
|
56
|
+
@amf_version = 3
|
57
|
+
@headers = []
|
58
|
+
@messages = []
|
59
|
+
end
|
60
|
+
|
61
|
+
def serialize
|
62
|
+
stream = ""
|
63
|
+
|
64
|
+
# Write version
|
65
|
+
stream << pack_int16_network(@amf_version)
|
66
|
+
|
67
|
+
# Write headers
|
68
|
+
stream << pack_int16_network(@headers.length) # Header count
|
69
|
+
@headers.each do |h|
|
70
|
+
stream << pack_int16_network(h.name.length)
|
71
|
+
stream << h.name
|
72
|
+
stream << pack_int8(h.must_understand ? 1 : 0)
|
73
|
+
stream << pack_word32_network(-1)
|
74
|
+
stream << AMF.serialize(h.data, 0)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Write messages
|
78
|
+
stream << pack_int16_network(@messages.length) # Message count
|
79
|
+
@messages.each do |m|
|
80
|
+
stream << pack_int16_network(m.target_uri.length)
|
81
|
+
stream << m.target_uri
|
82
|
+
|
83
|
+
stream << pack_int16_network(m.response_uri.length)
|
84
|
+
stream << m.response_uri
|
85
|
+
|
86
|
+
stream << pack_word32_network(-1)
|
87
|
+
stream << AMF0_AMF3_MARKER if @amf_version == 3
|
88
|
+
stream << AMF.serialize(m.data, @amf_version)
|
89
|
+
end
|
90
|
+
|
91
|
+
stream
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
include AMF::Pure::WriteIOHelpers
|
96
|
+
end
|
97
|
+
|
98
|
+
# AMF::Request or AMF::Response header
|
99
|
+
class Header
|
100
|
+
attr_accessor :name, :must_understand, :data
|
101
|
+
|
102
|
+
def initialize name, must_understand, data
|
103
|
+
@name = name
|
104
|
+
@must_understand = must_understand
|
105
|
+
@data = data
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# AMF::Request or AMF::Response message
|
110
|
+
class Message
|
111
|
+
attr_accessor :target_uri, :response_uri, :data
|
112
|
+
|
113
|
+
def initialize target_uri, response_uri, data
|
114
|
+
@target_uri = target_uri
|
115
|
+
@response_uri = response_uri
|
116
|
+
@data = data
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|