simrpc 0.1 → 0.2

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.
@@ -0,0 +1,171 @@
1
+ # simrpc message module
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ require 'qpid'
27
+ require 'socket'
28
+ require 'semaphore'
29
+
30
+ module Simrpc
31
+
32
+ # The QpidAdapter module implements the simrpc qpid subsystem, providing
33
+ # a convenient way to access qpid constructs
34
+ module QpidAdapter
35
+
36
+ # Simrpc::Qpid::Node class, represents an enpoint on a qpid
37
+ # network which has its own exchange and queue which it listens on
38
+ class Node
39
+ private
40
+ # helper method to generate a random id
41
+ def gen_uuid
42
+ ["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
43
+ Array.new(16) {|x| rand(0xff) }
44
+ end
45
+
46
+ public
47
+ # a node can have children nodes mapped to by keys
48
+ attr_accessor :children
49
+
50
+ # node always has a node id
51
+ attr_reader :node_id
52
+
53
+ # create the qpid base connection with the specified broker / port
54
+ # or config file. Then establish exchange and queue and start listening
55
+ # for requests.
56
+ #
57
+ # specify :broker and :port arguments to directly connect to those
58
+ # specify :config argument to use that yml file
59
+ # specify MOTEL_AMQP_CONF environment variable to use that yml file
60
+ # specify :id parameter to set id, else it will be set to a uuid just created
61
+ def initialize(args = {})
62
+ # if no id specified generate a new uuid
63
+ @node_id = args[:id].nil? ? gen_uuid : args[:id]
64
+
65
+ # we generate a random session id
66
+ @session_id = gen_uuid
67
+
68
+ # get the broker/port
69
+ broker = args[:broker].nil? ? "localhost" : args[:broker]
70
+ port = args[:port].nil? ? 5672 : args[:port]
71
+
72
+ if (broker.nil? || port.nil?) && args.has_key?(:config)
73
+ config =
74
+ amqpconfig = YAML::load(File.open(args[:config]))
75
+ broker = amqpconfig["broker"] if broker.nil?
76
+ port = amqpconfig["port"] if port.nil?
77
+ end
78
+
79
+ ### create underlying tcp connection
80
+ @conn = Qpid::Connection.new(TCPSocket.new(broker,port))
81
+ @conn.start
82
+
83
+ ### connect to qpid broker
84
+ @ssn = @conn.session(@session_id)
85
+
86
+ @children = {}
87
+
88
+ @accept_lock = Semaphore.new(1)
89
+
90
+ # qpid constructs that will be created for node
91
+ @exchange = args[:exchange].nil? ? @node_id.to_s + "-exchange" : args[:exchange]
92
+ @queue = args[:queue].nil? ? @node_id.to_s + "-queue" : args[:queue]
93
+ @local_queue = args[:local_queue].nil? ? @node_id.to_s + "-local-queue" : args[:local_queue]
94
+ @routing_key = @queue
95
+
96
+ Logger.warn "creating qpid exchange #{@exchange} queue #{@queue} binding_key #{@routing_key}"
97
+
98
+ if @ssn.exchange_query(@exchange).not_found
99
+ @ssn.exchange_declare(@exchange, :type => "direct")
100
+ end
101
+
102
+ if @ssn.queue_query(@queue).queue.nil?
103
+ @ssn.queue_declare(@queue)
104
+ end
105
+
106
+ @ssn.exchange_bind(:exchange => @exchange,
107
+ :queue => @queue,
108
+ :binding_key => @routing_key)
109
+ end
110
+
111
+ # Instruct Node to start accepting requests asynchronously and immediately return.
112
+ # handler must be callable and take node, msg, respond_to arguments, corresponding to
113
+ # 'self', the message received', and the routing_key which to send any response.
114
+ def async_accept(&handler)
115
+ # TODO permit a QpidNode to accept messages from multiple exchanges/queues
116
+ @accept_lock.wait
117
+
118
+ # subscribe to the queue
119
+ @ssn.message_subscribe(:destination => @local_queue,
120
+ :queue => @queue,
121
+ :accept_mode => @ssn.message_accept_mode.none)
122
+ @incoming = @ssn.incoming(@local_queue)
123
+ @incoming.start
124
+
125
+ Logger.warn "listening for messages on #{@queue}"
126
+
127
+ # start receiving messages
128
+ @incoming.listen{ |msg|
129
+ Logger.info "queue #{@queue} received message #{msg.body.to_s.size} #{msg.body}"
130
+ reply_to = msg.get(:message_properties).reply_to.routing_key
131
+ handler.call(self, msg.body, reply_to)
132
+ }
133
+ end
134
+
135
+ # block until accept operation is complete
136
+ def join
137
+ @accept_lock.wait
138
+ end
139
+
140
+ # instructs QpidServer to stop accepting, blocking
141
+ # untill all accepting operations have terminated
142
+ def terminate
143
+ Logger.warn "terminating qpid session"
144
+ unless @incoming.nil?
145
+ @incoming.stop
146
+ @incoming.close
147
+ @accept_lock.signal
148
+ end
149
+ @ssn.close
150
+ # TODO undefine the @queue/@exchange
151
+ end
152
+
153
+ # send a message to the specified routing_key
154
+ def send_message(routing_key, message)
155
+ dp = @ssn.delivery_properties(:routing_key => routing_key)
156
+ mp = @ssn.message_properties( :content_type => "text/plain")
157
+ rp = @ssn.message_properties( :reply_to =>
158
+ @ssn.reply_to(@exchange, @routing_key))
159
+ msg = Qpid::Message.new(dp, mp, rp, message.to_s)
160
+
161
+ Logger.warn "sending qpid message #{msg.body} to #{routing_key}"
162
+
163
+ # send it
164
+ @ssn.message_transfer(:message => msg)
165
+ end
166
+
167
+ end
168
+
169
+ end # module QpidAdapter
170
+
171
+ end # module Simrpc
@@ -0,0 +1,400 @@
1
+ # simrpc schema module
2
+ #
3
+ # Copyright (C) 2010 Mohammed Morsi <movitto@yahoo.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person
6
+ # obtaining a copy of this software and associated documentation
7
+ # files (the "Software"), to deal in the Software without
8
+ # restriction, including without limitation the rights to use,
9
+ # copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the
11
+ # Software is furnished to do so, subject to the following
12
+ # conditions:
13
+ #
14
+ # The above copyright notice and this permission notice shall be
15
+ # included in all copies or substantial portions of the Software.
16
+ #
17
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
18
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
19
+ # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
20
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
21
+ # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22
+ # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23
+ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ require 'rexml/document'
27
+
28
+ # FIXME store lengths in binary instead of ascii string
29
+
30
+ module Simrpc
31
+
32
+ # schema module defines classes / methods to define
33
+ # a simrpc schema / load it from an xml file
34
+ module Schema
35
+
36
+ Types = [ :str, :int, :float, :bool, :obj, :array ]
37
+
38
+ # return true if type is a primitive, else false
39
+ def is_primitive?(type)
40
+ return [:str, :int, :float, :bool].include?(type)
41
+ end
42
+ module_function :is_primitive?
43
+
44
+ # convert primitive type from string
45
+ def primitive_from_str(type, str)
46
+ if type == :str
47
+ return str
48
+
49
+ elsif type == :int
50
+ return str.to_i
51
+
52
+ elsif type == :float
53
+ return str.to_f
54
+
55
+ elsif type == :bool
56
+ return str == "true"
57
+
58
+ end
59
+ end
60
+ module_function :primitive_from_str
61
+
62
+ # Data field defintion containing a type, name, and value.
63
+ # Optinally an associated class or type may be given.
64
+ # Associated only valid for :obj and :array types, and
65
+ # must be set to associated ClassDef or Type (:array only)
66
+ class DataFieldDef
67
+ attr_accessor :type, :name, :associated
68
+
69
+ # indicates this data field should be ignored if it has a null value
70
+ attr_accessor :ignore_null
71
+
72
+ def initialize(args = {})
73
+ @type = args[:type] unless args[:type].nil?
74
+ @name = args[:name] unless args[:name].nil?
75
+ @associated = args[:associated] unless args[:associated].nil?
76
+ @ignore_null = !args[:ignore_null].nil? && args[:ignore_null]
77
+ end
78
+
79
+ # helper method to lookup and return the specified class name in the
80
+ # specified schema
81
+ def find_class_def(cl_name, schema_def)
82
+ return schema_def.classes.find { |cl| cl.name == cl_name.to_s } unless cl_name.nil? || schema_def.nil?
83
+ nil
84
+ end
85
+
86
+ # helper method to lookup and return the class definition corresponding to the associated
87
+ # attribte in the specified schema def
88
+ def associated_class_def(schema_def)
89
+ find_class_def(@associated, schema_def) unless @associated.nil?
90
+ end
91
+
92
+ # convert given value of this data field into a string. Provide schema_def
93
+ # for :obj or :array data fields associated w/ a non-primitive type. converted_classes
94
+ # is a recursive helper array used/maintained internally
95
+ def to_s(value, schema_def = nil, converted_classes = [])
96
+ if value.nil?
97
+ return ""
98
+ elsif Schema::is_primitive?(@type)
99
+ return value.to_s
100
+
101
+ elsif type == :array
102
+ str = "%04d" % value.size
103
+ value.each { |val|
104
+ if Schema::is_primitive?(@associated)
105
+ str += "%04d" % val.to_s.size
106
+ str += val.to_s
107
+ else
108
+ cl_def = associated_class_def(schema_def)
109
+ unless cl_def.nil?
110
+ cl_s = cl_def.to_s(val, schema_def, converted_classes)
111
+ str += "%04d" % cl_s.size
112
+ str += cl_s
113
+ end
114
+ end
115
+ }
116
+ return str
117
+
118
+ # elsif @type == :map # TODO
119
+
120
+ elsif type == :obj
121
+ cl_name = ""
122
+ cl_def = associated_class_def(schema_def)
123
+
124
+ # if associated class isn't specified, store the class name,
125
+ # providing for generic object support
126
+ if cl_def.nil?
127
+ cl_name = value.class.to_s.demodulize
128
+ cl_def = find_class_def(cl_name, schema_def)
129
+ raise InvalidSchemaClass.new("cannot find #{cl_name} in schema") if cl_def.nil?
130
+ cl_name = "%04d" % cl_name.size + cl_name
131
+ end
132
+
133
+ return cl_name + cl_def.to_s(value, schema_def, converted_classes)
134
+ end
135
+ end
136
+
137
+ # convert given string representation of this data field into its original value.
138
+ # Provide schema_def for :obj or :array data fields associated w/ non-primitive types
139
+ # # coverted_classes is a recursive helper array used/maintained internally
140
+ def from_s(str, schema_def = nil, converted_classes = [])
141
+ if str == ""
142
+ return nil
143
+
144
+ elsif Schema::is_primitive?(@type)
145
+ return Schema::primitive_from_str(@type, str)
146
+
147
+ elsif @type == :array
148
+ res = []
149
+ cl_def = associated_class_def(schema_def) unless Schema::is_primitive?(@associated)
150
+ alen = str[0...4].to_i
151
+ apos = 4
152
+ (0...alen).each { |i|
153
+ elen = str[apos...apos+4].to_i
154
+ parsed = str[apos+4...apos+4+elen]
155
+ if Schema::is_primitive?(@associated)
156
+ p = Schema::primitive_from_str(@associated, parsed)
157
+ res.push p
158
+ else
159
+ res.push cl_def.from_s(parsed, schema_def, converted_classes)
160
+ end
161
+ apos = apos+4+elen
162
+ }
163
+ return res
164
+
165
+ # elsif @type == :map # TODO
166
+
167
+ elsif @type == :obj
168
+ cl_def = associated_class_def(schema_def)
169
+
170
+ # if associated class isn't specified, parse the class name,
171
+ # providing for generic object support
172
+ if cl_def.nil?
173
+ cnlen = str[0...4].to_i
174
+ cname = str[4...cnlen+4]
175
+ str = str[cnlen+4...str.size]
176
+ cl_def = find_class_def(cname, schema_def)
177
+ raise InvalidSchemaClass.new("cannot find #{cname} in schema") if cl_def.nil?
178
+ end
179
+
180
+ return cl_def.from_s(str, schema_def, converted_classes)
181
+
182
+ end
183
+ end
184
+
185
+ end
186
+
187
+ # A class definition, containing data members.
188
+ # Right now we build into this the assumption that
189
+ # the 'name' attribute will share the same name as
190
+ # the actual class name which data will be mapped to / from
191
+ # and accessors exist on it corresponding to the names of
192
+ # each of the members
193
+ class ClassDef
194
+ # class name
195
+ # array of DataFieldDef
196
+ # base class name
197
+ attr_accessor :name, :members, :inherits
198
+
199
+ def initialize
200
+ @members = []
201
+ end
202
+
203
+ def base_class_def(schema_def)
204
+ return schema_def.classes.find { |cl| cl.name == inherits.to_s } unless inherits.nil? || schema_def.nil?
205
+ end
206
+
207
+ # convert value instance of class represented by this ClassDef
208
+ # into a string. schema_def must be provided if this ClassDef
209
+ # contains any associated class members. converted_classes is
210
+ # a recursive helper array used internally
211
+ def to_s(value, schema_def = nil, converted_classes = [])
212
+ return "O" + ("%04d" % converted_classes.index(value)) if converted_classes.include? value # if we already converted the class, store 'O' + its index
213
+ converted_classes.push value
214
+
215
+ # just encode each member w/ length
216
+ str = "I" # NEED to have something here incase the length of the first member is the same as the ascii character for 'O'
217
+ unless value.nil?
218
+ @members.each { |member|
219
+ mval = value.send(member.name.intern) if value.respond_to? member.name.intern
220
+ #mval = value.method(member.name.intern).call # invoke member getter
221
+ mstr = member.to_s(mval, schema_def, converted_classes)
222
+ mlen = "%04d" % mstr.size
223
+ #unless mstr == "" && member.ignore_null
224
+ str += mlen + mstr
225
+ #end
226
+ }
227
+
228
+ # encode and append base class
229
+ base_class = base_class_def(schema_def)
230
+ until base_class.nil?
231
+ base_class.members.each { |member|
232
+ mval = value.send(member.name.intern) if value.respond_to? member.name.intern
233
+ mstr = member.to_s(mval, schema_def, converted_classes)
234
+ mlen = "%04d" % mstr.size
235
+ str += mlen + mstr
236
+ }
237
+ base_class = base_class.base_class_def(schema_def)
238
+ end
239
+ end
240
+
241
+ return str
242
+ end
243
+
244
+ # convert string instance of class represented by this ClassDef
245
+ # into actual class instance. schema_def must be provided if this
246
+ # ClassDef contains any associated class members.
247
+ # The converted_classes recursive helper array is used internally.
248
+ def from_s(str, schema_def = nil, converted_classes = [])
249
+ return nil if str == "I"
250
+
251
+ if str[0] == "O" # if we already converted the class, simply return that
252
+ return converted_classes[str[1...5].to_i]
253
+ end
254
+
255
+ # construct an instance of the class
256
+ cl = Object.module_eval("::#{@name}", __FILE__, __LINE__)
257
+ obj = cl.new if cl.respond_to? :new
258
+ obj = cl.instance if obj.nil? && cl.respond_to?(:instance)
259
+ raise InvalidSchemaClass.new("cannot create schema class #{@name}") if obj.nil?
260
+
261
+ # decode each member
262
+ mpos = 1 # start at 1 to skip the 'I'
263
+ @members.each { |member|
264
+ mlen = str[mpos...mpos+4].to_i
265
+ parsed = str[mpos+4...mpos+4+mlen]
266
+ parsed_o = member.from_s(parsed, schema_def, converted_classes)
267
+ unless parsed_o.nil? && member.ignore_null
268
+ member_method = (member.name + "=").intern
269
+ obj.send(member_method, parsed_o) if obj.respond_to? member_method # invoke member setter
270
+ end
271
+ mpos = mpos+4+mlen
272
+ }
273
+
274
+ # decode base object from string
275
+ base_class = base_class_def(schema_def)
276
+ until base_class.nil?
277
+ base_class.members.each { |member|
278
+ mlen = str[mpos...mpos+4].to_i
279
+ parsed = str[mpos+4...mpos+4+mlen]
280
+ parsed_o = member.from_s(parsed, schema_def, converted_classes)
281
+ unless parsed_o.nil? && member.ignore_null
282
+ member_method = (member.name + "=").intern
283
+ obj.send(member_method, parsed_o) if obj.respond_to? member_method # invoke member setter
284
+ end
285
+ mpos = mpos+4+mlen
286
+ }
287
+
288
+ base_class = base_class.base_class_def(schema_def)
289
+ end
290
+
291
+ return obj
292
+ end
293
+ end
294
+
295
+ # method definition, containing parameters
296
+ # and return values. May optionally have
297
+ # a handler to be invoked when a remote
298
+ # entity invokes this method
299
+ class MethodDef
300
+ attr_accessor :name
301
+
302
+ # both are arrays of DataFieldDef
303
+ attr_accessor :parameters, :return_values
304
+
305
+ # should be a callable entity that takes
306
+ # the specified parameters, and returns
307
+ # the specified return values
308
+ attr_accessor :handler
309
+
310
+ def initialize
311
+ @parameters = []
312
+ @return_values = []
313
+ end
314
+ end
315
+
316
+ # schema defintion including all defined classes and methods
317
+ class SchemaDef
318
+ # array of ClassDef
319
+ attr_accessor :classes
320
+
321
+ # array of MethodDef
322
+ attr_accessor :methods
323
+
324
+ def initialize
325
+ @classes = []
326
+ @methods = []
327
+ end
328
+ end
329
+
330
+ # parse classes, methods, and data out of a xml definition
331
+ class Parser
332
+
333
+ # Parse and return a SchemaDef.
334
+ # Specify :schema argument containing xml schema to parse
335
+ # or :file containing location of file containing xml schema
336
+ def self.parse(args = {})
337
+ if(!args[:schema].nil?)
338
+ schema = args[:schema]
339
+ elsif(!args[:file].nil?)
340
+ schema = File.new(args[:file], "r")
341
+ end
342
+
343
+ schema_def = SchemaDef.new
344
+
345
+ unless schema.nil? || schema == ""
346
+ doc = REXML::Document.new(schema)
347
+ # grab each method definition
348
+ doc.elements.each('schema/method') do |ele|
349
+ method = MethodDef.new
350
+ method.name = ele.attributes["name"]
351
+ Logger.debug "parsed schema method #{method.name}"
352
+
353
+ ele.elements.each("param") do |param|
354
+ param = _parse_data_def(param)
355
+ method.parameters.push param
356
+ Logger.debug " parameter #{param.name}"
357
+ end
358
+
359
+ ele.elements.each("return_value") do |rv|
360
+ rv = _parse_data_def(rv)
361
+ method.return_values.push rv
362
+ Logger.debug " return_value #{rv.name}"
363
+ end
364
+
365
+ schema_def.methods.push method
366
+ end
367
+
368
+ # grab each class definition
369
+ doc.elements.each('schema/class') do |ele|
370
+ cl = ClassDef.new
371
+ cl.name = ele.attributes["name"]
372
+ cl.inherits = ele.attributes["inherits"]
373
+ Logger.debug "parsed schema class #{cl.name}"
374
+
375
+ ele.elements.each("member") do |mem|
376
+ mem = _parse_data_def(mem)
377
+ cl.members.push mem
378
+ Logger.debug " member #{mem.name}"
379
+ end
380
+
381
+ schema_def.classes.push cl
382
+ end
383
+ end
384
+ return schema_def
385
+ end
386
+
387
+ private
388
+ # helper method to parse a DataFieldDef out of an Element
389
+ def self._parse_data_def(element)
390
+ data_field = DataFieldDef.new
391
+ data_field.type = element.attributes["type"].intern
392
+ data_field.name = element.attributes["name"]
393
+ data_field.associated = element.attributes["associated"].intern unless element.attributes["associated"].nil?
394
+ data_field.ignore_null = true if element.attributes.include? "ignore_null"
395
+ return data_field
396
+ end
397
+ end
398
+
399
+ end # module Schema
400
+ end # module Simrpc