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.
data/lib/simrpc.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # Implements a simple to use method based RPC for ruby
4
4
  # built upon Apache Qpid
5
5
  #
6
- # Copyright (C) 2009 Mohammed Morsi <movitto@yahoo.com>
6
+ # Copyright (c) 2010 Mohammed Morsi <movitto@yahoo.com>
7
7
  #
8
8
  # Permission is hereby granted, free of charge, to any person
9
9
  # obtaining a copy of this software and associated documentation
@@ -26,806 +26,13 @@
26
26
  # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27
27
  # OTHER DEALINGS IN THE SOFTWARE.
28
28
 
29
- require 'rexml/document'
30
- require 'logger'
29
+ lib = File.dirname(__FILE__)
31
30
 
32
- require 'qpid'
33
- require 'socket'
34
- require 'semaphore'
31
+ require lib + '/simrpc/common'
32
+ require lib + '/simrpc/exceptions'
33
+ require lib + '/simrpc/schema'
34
+ require lib + '/simrpc/message'
35
+ require lib + '/simrpc/qpid_adapter'
36
+ require lib + '/simrpc/node'
35
37
 
36
- require 'activesupport' # for inflector demodulize
37
-
38
- module Simrpc
39
-
40
- # Logger helper class
41
- class Logger
42
- private
43
- def self._instantiate_logger
44
- unless defined? @@logger
45
- @@logger = ::Logger.new(STDOUT)
46
- @@logger.level = ::Logger::FATAL # FATAL ERROR WARN INFO DEBUG
47
- end
48
- end
49
- public
50
- def self.method_missing(method_id, *args)
51
- _instantiate_logger
52
- @@logger.send(method_id, args)
53
- end
54
- end
55
-
56
- # schema module defines classes / methods to define
57
- # a simrpc schema / load it from an xml file
58
- module Schema
59
-
60
- Types = [ :str, :int, :float, :bool, :obj, :array ]
61
-
62
- # return true if type is a primitive, else false
63
- def is_primitive?(type)
64
- return [:str, :int, :float, :bool].include?(type)
65
- end
66
- module_function :is_primitive?
67
-
68
- # convert primitive type from string
69
- def primitive_from_str(type, str)
70
- if type == :str
71
- return str
72
-
73
- elsif type == :int
74
- return str.to_i
75
-
76
- elsif type == :float
77
- return str.to_f
78
-
79
- elsif type == :bool
80
- return str == "true"
81
-
82
- end
83
- end
84
- module_function :primitive_from_str
85
-
86
- # FIXME store lengths in binary instead of ascii string
87
-
88
- # Data field defintion containing a type, name, and value.
89
- # Optinally an associated class or type may be given.
90
- # Associated only valid for :obj and :array types, and
91
- # must be set to associated ClassDef or Type (:array only)
92
- class DataFieldDef
93
- attr_accessor :type, :name, :associated
94
-
95
- # indicates this data field should be ignored if it has a null value
96
- attr_accessor :ignore_null
97
-
98
- def initialize(args = {})
99
- @type = args[:type] unless args[:type].nil?
100
- @name = args[:name] unless args[:name].nil?
101
- @associated = args[:associated] unless args[:associated].nil?
102
- @ignore_null = !args[:ignore_null].nil? && args[:ignore_null]
103
- end
104
-
105
- # helper method to lookup and return the specified class name in the
106
- # specified schema
107
- def find_class_def(cl_name, schema_def)
108
- return schema_def.classes.find { |cl| cl.name == cl_name.to_s } unless cl_name.nil? || schema_def.nil?
109
- nil
110
- end
111
-
112
- # helper method to lookup and return the class definition corresponding to the associated
113
- # attribte in the specified schema def
114
- def associated_class_def(schema_def)
115
- find_class_def(@associated, schema_def) unless @associated.nil?
116
- end
117
-
118
- # convert given value of this data field into a string. Provide schema_def
119
- # for :obj or :array data fields associated w/ a non-primitive type. converted_classes
120
- # is a recursive helper array used/maintained internally
121
- def to_s(value, schema_def = nil, converted_classes = [])
122
- if value.nil?
123
- return ""
124
- elsif Schema::is_primitive?(@type)
125
- return value.to_s
126
-
127
- elsif type == :array
128
- str = "%04d" % value.size
129
- value.each { |val|
130
- if Schema::is_primitive?(@associated)
131
- str += "%04d" % val.to_s.size
132
- str += val.to_s
133
- else
134
- cl_def = associated_class_def(schema_def)
135
- unless cl_def.nil?
136
- cl_s = cl_def.to_s(val, schema_def, converted_classes)
137
- str += "%04d" % cl_s.size
138
- str += cl_s
139
- end
140
- end
141
- }
142
- return str
143
-
144
- # elsif @type == :map # TODO
145
-
146
- elsif type == :obj
147
- cl_name = ""
148
- cl_def = associated_class_def(schema_def)
149
-
150
- # if associated class isn't specified, store the class name,
151
- # providing for generic object support
152
- if cl_def.nil?
153
- cl_name = value.class.to_s.demodulize
154
- cl_def = find_class_def(cl_name, schema_def)
155
- cl_name = "%04d" % cl_name.size + cl_name
156
- end
157
-
158
- return cl_name + cl_def.to_s(value, schema_def, converted_classes) unless cl_def.nil?
159
-
160
- end
161
- end
162
-
163
- # convert given string representation of this data field into its original value.
164
- # Provide schema_def for :obj or :array data fields associated w/ non-primitive types
165
- # # coverted_classes is a recursive helper array used/maintained internally
166
- def from_s(str, schema_def = nil, converted_classes = [])
167
- if str == ""
168
- return nil
169
-
170
- elsif Schema::is_primitive?(@type)
171
- return Schema::primitive_from_str(@type, str)
172
-
173
- elsif @type == :array
174
- res = []
175
- cl_def = associated_class_def(schema_def) unless Schema::is_primitive?(@associated)
176
- alen = str[0...4].to_i
177
- apos = 4
178
- (0...alen).each { |i|
179
- elen = str[apos...apos+4].to_i
180
- parsed = str[apos+4...apos+4+elen]
181
- if Schema::is_primitive?(@associated)
182
- p = Schema::primitive_from_str(@associated, parsed)
183
- res.push p
184
- else
185
- res.push cl_def.from_s(parsed, schema_def, converted_classes)
186
- end
187
- apos = apos+4+elen
188
- }
189
- #parsed = str[4...4+len]
190
- return res
191
-
192
- # elsif @type == :map # TODO
193
-
194
- elsif @type == :obj
195
- cl_def = associated_class_def(schema_def)
196
-
197
- # if associated class isn't specified, parse the class name,
198
- # providing for generic object support
199
- if cl_def.nil?
200
- cnlen = str[0...4].to_i
201
- cname = str[4...cnlen+4]
202
- str = str[cnlen+4...str.size]
203
- cl_def = find_class_def(cname, schema_def)
204
- end
205
-
206
- return cl_def.from_s(str, schema_def, converted_classes) unless cl_def.nil?
207
-
208
- end
209
- end
210
-
211
- end
212
-
213
- # A class definition, containing data members.
214
- # Right now we build into this the assumption that
215
- # the 'name' attribute will share the same name as
216
- # the actual class name which data will be mapped to / from
217
- # and accessors exist on it corresponding to the names of
218
- # each of the members
219
- class ClassDef
220
- # class name
221
- # array of DataFieldDef
222
- attr_accessor :name, :members
223
-
224
- def initialize
225
- @members = []
226
- end
227
-
228
- # convert value instance of class represented by this ClassDef
229
- # into a string. schema_def must be provided if this ClassDef
230
- # contains any associated class members. converted_classes is
231
- # a recursive helper array used internally
232
- def to_s(value, schema_def = nil, converted_classes = [])
233
- return "O" + ("%04d" % converted_classes.index(value)) if converted_classes.include? value # if we already converted the class, store 'O' + its index
234
- converted_classes.push value
235
-
236
- # just encode each member w/ length
237
- str = ""
238
- unless value.nil?
239
- @members.each { |member|
240
- mval = value.send(member.name.intern)
241
- #mval = value.method(member.name.intern).call # invoke member getter
242
- mstr = member.to_s(mval, schema_def, converted_classes)
243
- mlen = "%04d" % mstr.size
244
- #unless mstr == "" && member.ignore_null
245
- str += mlen + mstr
246
- #end
247
- }
248
- end
249
- return str
250
- end
251
-
252
- # convert string instance of class represented by this ClassDef
253
- # into actual class instance. schema_def must be provided if this
254
- # ClassDef contains any associated class members. converted_classes
255
- # is a recurvice helper array used internally.
256
- def from_s(str, schema_def = nil, converted_classes = [])
257
- return nil if str == ""
258
-
259
- mpos = 0
260
- if str[mpos,1] == "O" # if we already converted the class, simply return that
261
- return converted_classes[str[1...5].to_i]
262
- end
263
-
264
- # construct an instance of the class
265
- obj = Object.module_eval("::#{@name}", __FILE__, __LINE__).new
266
- converted_classes.push obj
267
-
268
- # decode each member
269
- @members.each { |member|
270
- mlen = str[mpos...mpos+4].to_i
271
- parsed = str[mpos+4...mpos+4+mlen]
272
- parsed_o = member.from_s(parsed, schema_def, converted_classes)
273
- unless parsed_o.nil? && member.ignore_null
274
- obj.send(member.name + "=", parsed_o) # invoke member setter
275
- end
276
- mpos = mpos+4+mlen
277
- }
278
-
279
- return obj
280
- end
281
- end
282
-
283
- # method definition, containing parameters
284
- # and return values. May optionally have
285
- # a handler to be invoked when a remote
286
- # entity invokes this method
287
- class MethodDef
288
- attr_accessor :name
289
-
290
- # both are arrays of DataFieldDef
291
- attr_accessor :parameters, :return_values
292
-
293
- # should be a callable entity that takes
294
- # the specified parameters, and returns
295
- # the specified return values
296
- attr_accessor :handler
297
-
298
- def initialize
299
- @parameters = []
300
- @return_values = []
301
- end
302
- end
303
-
304
- # schema defintion including all defined classes and methods
305
- class SchemaDef
306
- # array of ClassDef
307
- attr_accessor :classes
308
-
309
- # array of MethodDef
310
- attr_accessor :methods
311
-
312
- def initialize
313
- @classes = []
314
- @methods = []
315
- end
316
- end
317
-
318
- # parse classes, methods, and data out of a xml definition
319
- class Parser
320
-
321
- # Parse and return a SchemaDef.
322
- # Specify :schema argument containing xml schema to parse
323
- # or :file containing location of file containing xml schema
324
- def self.parse(args = {})
325
- if(!args[:schema].nil?)
326
- schema = args[:schema]
327
- elsif(!args[:file].nil?)
328
- schema = File.new(args[:file], "r")
329
- end
330
-
331
- schema_def = SchemaDef.new
332
-
333
- unless schema.nil? || schema == ""
334
- doc = REXML::Document.new(schema)
335
- # grab each method definition
336
- doc.elements.each('schema/method') do |ele|
337
- method = MethodDef.new
338
- method.name = ele.attributes["name"]
339
- Logger.debug "parsed schema method #{method.name}"
340
-
341
- ele.elements.each("param") do |param|
342
- param = _parse_data_def(param)
343
- method.parameters.push param
344
- Logger.debug " parameter #{param.name}"
345
- end
346
-
347
- ele.elements.each("return_value") do |rv|
348
- rv = _parse_data_def(rv)
349
- method.return_values.push rv
350
- Logger.debug " return_value #{rv.name}"
351
- end
352
-
353
- schema_def.methods.push method
354
- end
355
-
356
- # grab each class definition
357
- doc.elements.each('schema/class') do |ele|
358
- cl = ClassDef.new
359
- cl.name = ele.attributes["name"]
360
- Logger.debug "parsed schema class #{cl.name}"
361
-
362
- ele.elements.each("member") do |mem|
363
- mem = _parse_data_def(mem)
364
- cl.members.push mem
365
- Logger.debug " member #{mem.name}"
366
- end
367
-
368
- schema_def.classes.push cl
369
- end
370
- end
371
- return schema_def
372
- end
373
-
374
- private
375
- # helper method to parse a DataFieldDef out of an Element
376
- def self._parse_data_def(element)
377
- data_field = DataFieldDef.new
378
- data_field.type = element.attributes["type"].intern
379
- data_field.name = element.attributes["name"]
380
- data_field.associated = element.attributes["associated"].intern unless element.attributes["associated"].nil?
381
- data_field.ignore_null = true if element.attributes.include? "ignore_null"
382
- return data_field
383
- end
384
- end
385
-
386
- end
387
-
388
- # the message module provides the Message definition,
389
- # including a header with routing info and a body
390
- # with any number of data fields
391
- module Message
392
-
393
- # Simrpc::Message formatter helper module
394
- class Formatter
395
-
396
- # helper method to format a data field,
397
- # prepending a fixed size to it
398
- def self.format_with_size(data)
399
- # currently size is set to a 8 digit int
400
- len = "%08d" % data.to_s.size
401
- len + data.to_s
402
- end
403
-
404
- # helper method to parse a data field
405
- # off the front of a data sequence, using the
406
- # formatted size. Returns parsed data field
407
- # and remaining data sequence. If optional
408
- # class is given, the from_s method will
409
- # be invoked w/ the parsed data field and
410
- # returned with the remaining data sequence
411
- # instead
412
- def self.parse_from_formatted(data, data_class = nil)
413
- len = data[0...8].to_i
414
- parsed = data[8...8+len]
415
- remaining = data[8+len...data.size]
416
- return parsed, remaining if data_class.nil?
417
- return data_class.from_s(parsed), remaining
418
- end
419
- end
420
-
421
- # a single field trasnmitted via a message,
422
- # containing a key / value pair
423
- class Field
424
- attr_accessor :name, :value
425
-
426
- def initialize(args = {})
427
- @name = args[:name].nil? ? "" : args[:name]
428
- @value = args[:value].nil? ? "" : args[:value]
429
- end
430
-
431
- def to_s
432
- Formatter::format_with_size(@name) + Formatter::format_with_size(@value)
433
- end
434
-
435
- def self.from_s(data)
436
- field = Field.new
437
- field.name, data = Formatter::parse_from_formatted(data)
438
- field.value, data = Formatter::parse_from_formatted(data)
439
- return field
440
- end
441
- end
442
-
443
- # header contains various descriptive properies
444
- # about a message
445
- class Header
446
- attr_accessor :type, :target
447
-
448
- def initialize(args = {})
449
- @type = args[:type].nil? ? "" : args[:type]
450
- @target = args[:target].nil? ? "" : args[:target]
451
- end
452
-
453
- def to_s
454
- Formatter::format_with_size(@type) + Formatter::format_with_size(@target)
455
- end
456
-
457
- def self.from_s(data)
458
- header = Header.new
459
- header.type, data = Formatter::parse_from_formatted(data)
460
- header.target, data = Formatter::parse_from_formatted(data)
461
- return header
462
- end
463
- end
464
-
465
- # body consists of a list of data fields
466
- class Body
467
- attr_accessor :fields
468
-
469
- def initialize
470
- @fields = []
471
- end
472
-
473
- def to_s
474
- s = ''
475
- @fields.each { |field|
476
- fs = field.to_s
477
- s += Formatter::format_with_size(fs)
478
- }
479
- return s
480
- end
481
-
482
- def self.from_s(data)
483
- body = Body.new
484
- while(data != "")
485
- field, data = Formatter::parse_from_formatted(data)
486
- field = Field.from_s field
487
- body.fields.push field
488
- end
489
- return body
490
- end
491
- end
492
-
493
- # message contains a header / body
494
- class Message
495
- attr_accessor :header, :body
496
-
497
- def initialize
498
- @header = Header.new
499
- @body = Body.new
500
- end
501
-
502
- def to_s
503
- Formatter::format_with_size(@header) + Formatter::format_with_size(@body)
504
- end
505
-
506
- def self.from_s(data)
507
- message = Message.new
508
- message.header, data = Formatter::parse_from_formatted(data, Header)
509
- message.body, data = Formatter::parse_from_formatted(data, Body)
510
- return message
511
- end
512
- end
513
-
514
- end
515
-
516
- # The QpidAdapter module implements the simrpc qpid subsystem, providing
517
- # a convenient way to access qpid constructs
518
- module QpidAdapter
519
-
520
- # Simrpc::Qpid::Node class, represents an enpoint on a qpid
521
- # network which has its own exchange and queue which it listens on
522
- class Node
523
- private
524
- # helper method to generate a random id
525
- def gen_uuid
526
- ["%02x"*4, "%02x"*2, "%02x"*2, "%02x"*2, "%02x"*6].join("-") %
527
- Array.new(16) {|x| rand(0xff) }
528
- end
529
-
530
- public
531
- # a node can have children nodes mapped to by keys
532
- attr_accessor :children
533
-
534
- # node always has a node id
535
- attr_reader :node_id
536
-
537
- # create the qpid base connection with the specified broker / port
538
- # or config file. Then establish exchange and queue and start listening
539
- # for requests.
540
- #
541
- # specify :broker and :port arguments to directly connect to those
542
- # specify :config argument to use that yml file
543
- # specify MOTEL_AMQP_CONF environment variable to use that yml file
544
- # specify :id parameter to set id, else it will be set to a uuid just created
545
- def initialize(args = {})
546
- # if no id specified generate a new uuid
547
- @node_id = args[:id].nil? ? gen_uuid : args[:id]
548
-
549
- # we generate a random session id
550
- @session_id = gen_uuid
551
-
552
- # get the broker/port
553
- broker = args[:broker].nil? ? "localhost" : args[:broker]
554
- port = args[:port].nil? ? 5672 : args[:port]
555
-
556
- if (broker.nil? || port.nil?) && args.has_key?(:config)
557
- config =
558
- amqpconfig = YAML::load(File.open(args[:config]))
559
- broker = amqpconfig["broker"] if broker.nil?
560
- port = amqpconfig["port"] if port.nil?
561
- end
562
-
563
- ### create underlying tcp connection
564
- @conn = Qpid::Connection.new(TCPSocket.new(broker,port))
565
- @conn.start
566
-
567
- ### connect to qpid broker
568
- @ssn = @conn.session(@session_id)
569
-
570
- @children = {}
571
-
572
- @accept_lock = Semaphore.new(1)
573
-
574
- # qpid constructs that will be created for node
575
- @exchange = args[:exchange].nil? ? @node_id.to_s + "-exchange" : args[:exchange]
576
- @queue = args[:queue].nil? ? @node_id.to_s + "-queue" : args[:queue]
577
- @local_queue = args[:local_queue].nil? ? @node_id.to_s + "-local-queue" : args[:local_queue]
578
- @routing_key = @queue
579
-
580
- Logger.warn "creating qpid exchange #{@exchange} queue #{@queue} binding_key #{@routing_key}"
581
-
582
- if @ssn.exchange_query(@exchange).not_found
583
- @ssn.exchange_declare(@exchange, :type => "direct")
584
- end
585
-
586
- if @ssn.queue_query(@queue).queue.nil?
587
- @ssn.queue_declare(@queue)
588
- end
589
-
590
- @ssn.exchange_bind(:exchange => @exchange,
591
- :queue => @queue,
592
- :binding_key => @routing_key)
593
- end
594
-
595
- # Instruct Node to start accepting requests asynchronously and immediately return.
596
- # handler must be callable and take node, msg, respond_to arguments, corresponding to
597
- # 'self', the message received', and the routing_key which to send any response.
598
- def async_accept(&handler)
599
- # TODO permit a QpidNode to accept messages from multiple exchanges/queues
600
- @accept_lock.wait
601
-
602
- # subscribe to the queue
603
- @ssn.message_subscribe(:destination => @local_queue,
604
- :queue => @queue,
605
- :accept_mode => @ssn.message_accept_mode.none)
606
- @incoming = @ssn.incoming(@local_queue)
607
- @incoming.start
608
-
609
- Logger.warn "listening for messages on #{@queue}"
610
-
611
- # start receiving messages
612
- @incoming.listen{ |msg|
613
- Logger.info "queue #{@queue} received message #{msg.body.to_s.size} #{msg.body}"
614
- reply_to = msg.get(:message_properties).reply_to.routing_key
615
- handler.call(self, msg.body, reply_to)
616
- }
617
- end
618
-
619
- # block until accept operation is complete
620
- def join
621
- @accept_lock.wait
622
- end
623
-
624
- # instructs QpidServer to stop accepting, blocking
625
- # untill all accepting operations have terminated
626
- def terminate
627
- Logger.warn "terminating qpid session"
628
- unless @incoming.nil?
629
- @incoming.stop
630
- @incoming.close
631
- @accept_lock.signal
632
- end
633
- @ssn.close
634
- # TODO undefine the @queue/@exchange
635
- end
636
-
637
- # send a message to the specified routing_key
638
- def send_message(routing_key, message)
639
- dp = @ssn.delivery_properties(:routing_key => routing_key)
640
- mp = @ssn.message_properties( :content_type => "text/plain")
641
- rp = @ssn.message_properties( :reply_to =>
642
- @ssn.reply_to(@exchange, @routing_key))
643
- msg = Qpid::Message.new(dp, mp, rp, message.to_s)
644
-
645
- Logger.warn "sending qpid message #{msg.body} to #{routing_key}"
646
-
647
- # send it
648
- @ssn.message_transfer(:message => msg)
649
- end
650
-
651
- end
652
-
653
- end
654
-
655
- # Simrpc Method Message Controller, generates and handles method messages
656
- class MethodMessageController
657
- public
658
- # initialize with a specified schema definition
659
- def initialize(schema_def)
660
- @schema_def = schema_def
661
- end
662
-
663
- # generate new new method message, setting the message
664
- # target to the specified method name, and setting the fields
665
- # on the message to the method arguments
666
- def generate(method_name, args)
667
- @schema_def.methods.each { |method|
668
- if method.name == method_name
669
- msg = Message::Message.new
670
- msg.header.type = 'request'
671
- msg.header.target = method.name
672
-
673
- # loop through each param, convering corresponding
674
- # argument to message field and adding it to msg
675
- i = 0
676
- method.parameters.each { |param|
677
- field = Message::Field.new
678
- field.name = param.name
679
- field.value = param.to_s(args[i], @schema_def)
680
- msg.body.fields.push field
681
- i += 1
682
- }
683
-
684
- return msg
685
- end
686
- }
687
- return nil
688
- end
689
-
690
- # should be invoked when a message is received,
691
- # takes a message, converts it into a method call, and calls the corresponding
692
- # handler in the provided schema. Takes return arguments and sends back to caller
693
- def message_received(node, message, reply_to)
694
- message = Message::Message::from_s(message)
695
- @schema_def.methods.each { |method|
696
-
697
- if method.name == message.header.target
698
- Logger.info "received method #{method.name} message "
699
-
700
- # for request messages, dispatch to method handler
701
- if message.header.type != 'response' && method.handler != nil
702
- # order the params
703
- params = []
704
- method.parameters.each { |data_field|
705
- value_field = message.body.fields.find { |f| f.name == data_field.name }
706
- params.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
707
- }
708
-
709
- Logger.info "invoking #{method.name} handler "
710
-
711
- # invoke method handler
712
- return_values = method.handler.call(*params) # FIXME handlers can't use 'return' as this will fall through here
713
- # FIXME throw a catch block around this call to catch all handler exceptions
714
- return_values = [return_values] unless return_values.is_a? Array
715
-
716
- # if method returns no values, do not return response
717
- unless method.return_values.size == 0
718
-
719
- # consruct and send response message using return values
720
- response = Message::Message.new
721
- response.header.type = 'response'
722
- response.header.target = method.name
723
- (0...method.return_values.size).each { |rvi|
724
- field = Message::Field.new
725
- field.name = method.return_values[rvi].name
726
- field_def = method.return_values.find { |rv| rv.name == field.name }
727
- field.value = field_def.to_s(return_values[rvi], @schema_def) unless field_def.nil? # TODO what if field_def is nil
728
- response.body.fields.push field
729
- }
730
- Logger.info "responding to #{reply_to}"
731
- node.send_message(reply_to, response)
732
-
733
- end
734
-
735
- # for response values just return converted return values
736
- else
737
- results = []
738
- method.return_values.each { |data_field|
739
- value_field = message.body.fields.find { |f| f.name == data_field.name }
740
- results.push data_field.from_s(value_field.value, @schema_def) unless value_field.nil? # TODO what if value_field is nil
741
- }
742
- return results
743
- end
744
- end
745
- }
746
- end
747
- end
748
-
749
- # Simrpc Node represents ths main api which to communicate and send/listen for data.
750
- class Node
751
-
752
- # Instantiate it w/ a specified id
753
- # or one will be autogenerated. Specify schema (or location) containing
754
- # data and methods which to invoke and/or handle. Optionally specify
755
- # a remote destination which to send new messages to. Automatically listens
756
- # for incoming messages.
757
- def initialize(args = {})
758
- @id = args[:id] if args.has_key? :id
759
- @schema = args[:schema]
760
- @schema_file = args[:schema_file]
761
- @destination = args[:destination]
762
-
763
- if !@schema.nil?
764
- @schema_def = Schema::Parser.parse(:schema => @schema)
765
- elsif !@schema_file.nil?
766
- @schema_def = Schema::Parser.parse(:file => @schema_file)
767
- end
768
- raise ArgumentError, "schema_def cannot be nil" if @schema_def.nil?
769
- @mmc = MethodMessageController.new(@schema_def)
770
- @message_lock = Semaphore.new(1)
771
- @message_lock.wait
772
-
773
- @qpid_node = QpidAdapter::Node.new(:id => @id)
774
- @qpid_node.async_accept { |node, msg, reply_to|
775
- results = @mmc.message_received(node, msg, reply_to)
776
- message_received(results)
777
- }
778
- end
779
-
780
- def id
781
- return @id unless @id.nil?
782
- return @qpid_node.node_id
783
- end
784
-
785
- # implements, message_received callback to be notified when qpid receives a message
786
- def message_received(results)
787
- @message_results = results
788
- @message_lock.signal
789
- end
790
-
791
- # wait until the node is no longer accepting messages
792
- def join
793
- @qpid_node.join
794
- end
795
-
796
- # add a handler which to invoke when an schema method is invoked
797
- def handle_method(method, &handler)
798
- @schema_def.methods.each { |smethod|
799
- if smethod.name == method.to_s
800
- smethod.handler = handler
801
- break
802
- end
803
- }
804
- end
805
-
806
- # send method request to remote destination w/ the specified args
807
- def send_method(method_name, destination, *args)
808
- # generate and send new method message
809
- msg = @mmc.generate(method_name, args)
810
- @qpid_node.send_message(destination + "-queue", msg)
811
-
812
- # FIXME race condition if response is received b4 wait is invoked
813
-
814
- # block if we are expecting return values
815
- if @schema_def.methods.find{|m| m.name == method_name}.return_values.size != 0
816
- @message_lock.wait # block until response received
817
-
818
- # return return values
819
- #@message_received.body.fields.collect { |f| f.value }
820
- return *@message_results
821
- end
822
- end
823
-
824
- # can invoke schema methods directly on Node instances, this will catch
825
- # them and send them onto the destination
826
- def method_missing(method_id, *args)
827
- send_method(method_id.to_s, @destination, *args)
828
- end
829
- end
830
-
831
- end # module Simrpc
38
+ require 'activesupport' # for inflector