simrpc 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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