stomper 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates the headers attached to a Frame from the Stomper::Frames
4
+ # module. Instances of this class wrap a hash, but do so in a way
5
+ # to allow its values to be accessed by string, symbol, or method name,
6
+ # similar to an OpenStruct.
7
+ class Headers
8
+ # Creates a new Header instance, derived from the supplied hash, +hsh+.
9
+ def initialize(hsh = {})
10
+ @intern_head = hsh.inject({}) { |acc, (k,v)| acc[k.to_sym] = v; acc }
11
+ end
12
+
13
+ # Returns the 'id' header value, if it exists. Explicitly implemented
14
+ # because Object#id is a valid method by default.
15
+ def id
16
+ @intern_head[:id]
17
+ end
18
+
19
+ # Assigns the 'id' header value. Explicitly implemented because Object#id
20
+ # is a valid method, and we implemented +id+ explicitly so why not +id=+
21
+ def id=(id)
22
+ @intern_head[:id] = id
23
+ end
24
+
25
+ # Allows the headers to be accessed as though they were a Hash instance.
26
+ def [](idx)
27
+ @intern_head[idx.to_sym]
28
+ end
29
+
30
+ # Allows the headers to be assigned as though they were a Hash instance.
31
+ def []=(idx, val)
32
+ @intern_head[idx.to_sym] = val
33
+ end
34
+
35
+ def method_missing(meth, *args) # :nodoc:
36
+ raise TypeError, "can't modify frozen headers" if frozen?
37
+ meth_str = meth.to_s
38
+ ret = if meth_str =~ /=$/
39
+ raise ArgumentError, "setter #{meth_str} can only accept one value" if args.size != 1
40
+ meth_str.chop!
41
+ @intern_head[meth_str.to_sym] = args.first
42
+ else
43
+ raise ArgumentError, "getter #{meth_str} cannot accept any values" if args.size > 0
44
+ @intern_head[meth_str.to_sym]
45
+ end
46
+ _create_helpers(meth_str)
47
+ # Do the appropriate thing the first time around.
48
+ ret
49
+ end
50
+
51
+ # Converts the headers encapsulated by this object into a format that
52
+ # the Stomp Protocol expects them to be presented as.
53
+ def to_stomp
54
+ @intern_head.sort { |a, b| a.first.to_s <=> b.first.to_s }.inject("") do |acc, (k,v)|
55
+ acc << "#{k.to_s}:#{v}\n"
56
+ end
57
+ end
58
+
59
+ protected
60
+ def _create_helpers(meth)
61
+ return if self.respond_to?(meth)
62
+ meta = class << self; self; end
63
+ meta.send(:define_method, meth) { self[meth] }
64
+ meta.send(:define_method, :"#{meth}=") { |v| self[meth] = v }
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,44 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates a "MESSAGE" server side frame for the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class Message < Stomper::Frames::ServerFrame
8
+ # This class is the factory for all MESSAGE frames received.
9
+ factory_for :message
10
+
11
+ # Creates a new message frame with the given +headers+ and +body+
12
+ def initialize(headers, body)
13
+ super('MESSAGE', headers, body)
14
+ end
15
+
16
+ # Returns the message id generated by the stomp broker.
17
+ #
18
+ # This is a convenience method for:
19
+ # frame.headers[:'message-id'] or frame.headers['message-id']
20
+ def id
21
+ @headers[:'message-id']
22
+ end
23
+
24
+ # Returns the destination from which this message was delivered.
25
+ #
26
+ # This is a convenience method for:
27
+ # frame.headers.destination, frame.headers['destination'], or
28
+ # frame.headers[:destination]
29
+ def destination
30
+ @headers.destination
31
+ end
32
+
33
+ # Returns the name of the subscription which is responsible for the
34
+ # client having received this message.
35
+ #
36
+ # This is a convenience method for:
37
+ # frame.headers.subscription, frame.headers['subscription'] or
38
+ # frame.headers[:subscription]
39
+ def subscription
40
+ @headers.subscription
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates a "RECEIPT" server side frame for the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class Receipt < Stomper::Frames::ServerFrame
8
+ # This class is a factory for all RECEIPT commands received.
9
+ factory_for :receipt
10
+
11
+ # Creates a new Receipt frame with the supplied +headers+ and +body+
12
+ def initialize(headers, body)
13
+ super('RECEIPT', headers, body)
14
+ end
15
+
16
+ # Returns the 'receipt-id' header of the frame, which
17
+ # will correspond to the 'receipt' header of the message
18
+ # that caused this receipt to be sent by the stomp broker.
19
+ def for
20
+ @headers[:'receipt-id']
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates a "SEND" frame from the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class Send < Stomper::Frames::ClientFrame
8
+ def initialize(destination, body, headers={})
9
+ super('SEND', headers, body)
10
+ @headers.destination = destination
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates a server side frame for the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class ServerFrame
8
+ attr_reader :command, :headers, :body
9
+
10
+ # Creates a new server frame corresponding to the
11
+ # supplied +command+ with the given +headers+ and +body+.
12
+ def initialize(command, headers={}, body=nil)
13
+ @command = command
14
+ @headers = Headers.new(headers)
15
+ @body = body
16
+ end
17
+
18
+ class << self
19
+ # Provides a method for subclasses to register themselves
20
+ # as factories for particular stomp commands by passing a list
21
+ # of strings (or symbols) to this method. Each element in
22
+ # the list is interpretted as the command for which we will
23
+ # defer to the calling subclass to build.
24
+ def factory_for(*args)
25
+ @@registered_commands ||= {}
26
+ args.each do |command|
27
+ @@registered_commands[command.to_s.upcase] = self
28
+ end
29
+ end
30
+
31
+ # Builds a new ServerFrame instance by first checking to
32
+ # see if some subclass of ServerFrame has registered itself
33
+ # as a builder of the particular command. If so, a new
34
+ # instance of that subclass is created, otherwise a generic
35
+ # ServerFrame instance is created with its +command+ attribute
36
+ # set appropriately.
37
+ def build(command, headers, body)
38
+ command = command.to_s.upcase
39
+ if @@registered_commands.has_key?(command)
40
+ @@registered_commands[command].new(headers, body)
41
+ else
42
+ ServerFrame.new(command, headers, body)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,47 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates a "SUBSCRIBE" frame from the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class Subscribe < Stomper::Frames::ClientFrame
8
+ def initialize(destination, headers={})
9
+ super('SUBSCRIBE', headers)
10
+ @headers['destination'] = destination
11
+ @headers['ack'] ||= 'auto'
12
+ end
13
+
14
+ # Returns the ack mode of this subscription. (defaults to 'auto')
15
+ #
16
+ # This is a convenience method, and may also be accessed through
17
+ # frame.headers.ack or frame.headers[:ack] or frame.headers['ack']
18
+ def ack
19
+ @headers['ack']
20
+ end
21
+
22
+ # Returns the destination to which we are subscribing.
23
+ #
24
+ # This is a convenience method, and may also be accessed through
25
+ # frame.headers.destination or frame.headers[:destination] or frame.headers['destination']
26
+ def destination
27
+ @headers['destination']
28
+ end
29
+
30
+ # Returns the id of this subscription, if it has been set.
31
+ #
32
+ # This is a convenience method, and may also be accessed through
33
+ # frame.headers.id or frame.headers[:id] or frame.headers['id']
34
+ def id
35
+ @headers['id']
36
+ end
37
+
38
+ # Returns the selector header of this subscription, if it has been set.
39
+ #
40
+ # This is a convenience method, and may also be accessed through
41
+ # frame.headers.selector or frame.headers[:selector] or frame.headers['selector']
42
+ def selector
43
+ @headers['selector']
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ module Stomper
2
+ module Frames
3
+ # Encapsulates an "UNSUBSCRIBE" frame from the Stomp Protocol.
4
+ #
5
+ # See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
6
+ # for more details.
7
+ class Unsubscribe < Stomper::Frames::ClientFrame
8
+ def initialize(destination, headers={})
9
+ super('UNSUBSCRIBE', headers)
10
+ @headers['destination'] = destination
11
+ end
12
+
13
+ # Returns the id of the subscription being unsubscribed from, if it
14
+ # exists.
15
+ #
16
+ # This is a convenience method, and may also be accessed through
17
+ # frame.headers.id or frame.headers[:id] or frame.headers['id']
18
+ def id
19
+ @headers['id']
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,128 @@
1
+ module Stomper
2
+ # A representation of a subscription to a stomp broker destination. The
3
+ # attributes +id+, +destination+, +ack+ and +selector+ have the same
4
+ # semantic meaning as the headers of a Stomp "SUBSCRIBE" frame with the same
5
+ # name.
6
+ class Subscription
7
+ attr_reader :id, :destination, :ack, :selector
8
+
9
+ # Creates a new Subscription instance from the given parameters.
10
+ # The +destination_or_options+ parameter can either be a string
11
+ # specification of the destination, such as "/queue/target", or a hash
12
+ # corresponding to the headers of a "SUBSCRIBE" frame
13
+ # (eg: { :destination => "/queue/target", :id => "sub-001", ... })
14
+ #
15
+ # The optional +subscription_id+ parameter is a string corresponding
16
+ # to the name of this subscription. If this parameter is specified, it
17
+ # should be unique within the context of a given Stomper::Client, otherwise
18
+ # the behavior of the Stomper::Client#unsubscribe method may have unintended
19
+ # consequences.
20
+ #
21
+ # The optional +ack+ parameter specifies the mode that a client
22
+ # will use to acknowledge received messages and may be either :client or :auto.
23
+ # The default, :auto, does not require the client to notify the broker when
24
+ # it has received a message; however, setting +ack+ to :client will require
25
+ # each message received by this subscription to be acknowledged through the
26
+ # use of Stomper::Client#ack in order to ensure proper interaction between
27
+ # client and broker.
28
+ #
29
+ # The +selector+ parameter (again, optional) sets a SQL 92 selector for
30
+ # this subscription with the stomp broker as per the Stomp Protocol specification.
31
+ # Support of this functionality is entirely the responsibility of the broker,
32
+ # there is no client side filtering being done on incoming messages.
33
+ #
34
+ # When a message is "received" by an instance of Subscription, the supplied
35
+ # +block+ is inovked with the received message sent as a parameter.
36
+ #
37
+ # If no +subscription_id+ is specified, either explicitly or through a
38
+ # hash key of 'id' in +destination_or_options+, one may be automatically
39
+ # generated of the form "sub-#{Time.now.to_f}". The automatic generation
40
+ # of a subscription id occurs if and only if naive? returns false.
41
+ #
42
+ # While direct creation of Subscription instances is possible, the preferred
43
+ # method is for them to be constructed by a Stomper::Client through the use
44
+ # of the Stomper::Client#subscribe method.
45
+ #
46
+ # See also: naive?, Stomper::Client#subscribe, Stomper::Client#unsubscribe,
47
+ # Stomper::Client#ack
48
+ #
49
+ def initialize(destination_or_options, subscription_id=nil, ack=nil, selector=nil, &block)
50
+ if destination_or_options.is_a?(Hash)
51
+ options = Stomper::Frames::Headers.new(destination_or_options)
52
+ destination = options.destination
53
+ subscription_id ||= options.id
54
+ ack ||= options.ack
55
+ selector ||= options.selector
56
+ else
57
+ destination = destination_or_options.to_s
58
+ end
59
+ @id = subscription_id
60
+ @destination = destination
61
+ @ack = (ack || :auto).to_sym
62
+ @selector = selector
63
+ @call_back = block
64
+ @id ||= "sub-#{Time.now.to_f}" unless naive?
65
+ end
66
+
67
+ # Returns true if this subscription has no explicitly specified id,
68
+ # has no selector specified, and acknowledges messages through the :auto
69
+ # mode.
70
+ def naive?
71
+ @id.nil? && @selector.nil? && @ack == :auto
72
+ end
73
+
74
+ # Returns true if this subscription is responsible for a Stomper::Client
75
+ # instance receiving +message_frame+.
76
+ #
77
+ # See also: receives_for?, perform
78
+ def accepts?(message_frame)
79
+ receives_for?(message_frame.destination, message_frame.subscription)
80
+ end
81
+
82
+ # Returns true if this subscription is responsible for receiving
83
+ # messages for the given destination or subscription id, specified
84
+ # by +dest+ and +subid+ respectively.
85
+ #
86
+ # Note: if +subid+ is non-nil or this subscription is not naive?,
87
+ # then this method returns true if and only if the supplied +subid+ is
88
+ # equal to the +id+ of this subscription. Otherwise, the return value
89
+ # depends only upon the equality of +dest+ and this subscriptions +destination+
90
+ # attribute.
91
+ #
92
+ # See also: naive?
93
+ def receives_for?(dest, subid=nil)
94
+ if naive? && subid.nil?
95
+ @destination == dest
96
+ else
97
+ @id == subid
98
+ end
99
+ end
100
+
101
+ # Invokes the block associated with this subscription if
102
+ # this subscription accepts the supplied +message_frame+.
103
+ #
104
+ # See also: accepts?
105
+ def perform(message_frame)
106
+ @call_back.call(message_frame) if accepts?(message_frame)
107
+ end
108
+
109
+ # Converts this representation of a subscription into a
110
+ # Stomper::Frames::Subscribe client frame that can be transmitted
111
+ # to a stomp broker through a Stomper::Connection instance.
112
+ def to_subscribe
113
+ headers = { 'destination' => @destination, 'ack' => @ack.to_s }
114
+ headers['id'] = @id unless @id.nil?
115
+ headers['selector'] = @selector unless @selector.nil?
116
+ Stomper::Frames::Subscribe.new(@destination, headers)
117
+ end
118
+
119
+ # Converts this representation of a subscription into a
120
+ # Stomper::Frames::Unsubscribe client frame that can be transmitted
121
+ # to a stomp broker through a Stomper::Connection instance.
122
+ def to_unsubscribe
123
+ headers = { 'destination' => @destination }
124
+ headers['id'] = @id unless @id.nil?
125
+ Stomper::Frames::Unsubscribe.new(@destination, headers)
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,95 @@
1
+ module Stomper
2
+ # A Subscription collection class used internally by Stomper::Client to store
3
+ # its subscriptions. Instances of this class utilize synchronization making
4
+ # it safe to use in a multi-threaded context.
5
+ class Subscriptions
6
+ include Enumerable
7
+
8
+ # Creates a new Subscriptions container.
9
+ def initialize
10
+ @subs = []
11
+ @sub_lock = Mutex.new
12
+ end
13
+
14
+ # Adds the supplied subscription, +sub+, to the collection.
15
+ def <<(sub)
16
+ add(sub)
17
+ end
18
+
19
+ # Adds the supplied subscription, +sub+, to the collection.
20
+ def add(sub)
21
+ raise ArgumentError, "appended object must be a subscription" unless sub.is_a?(Subscription)
22
+ @sub_lock.synchronize { @subs << sub }
23
+ end
24
+
25
+ # Removes all Subscription objects from the collection that match
26
+ # the supplied destination, +dest+, and subscription id, +subid+.
27
+ # If +dest+ is a hash, the value referenced by the :destination key
28
+ # will be used as the destination, and +subid+ will be set to the value
29
+ # referenced by :id, unless it is explicitly set beforehand. If +dest+ is
30
+ # an instance of Subscription, the +destination+ attribute will be used
31
+ # as the destination, and +subid+ will be set to the +id+ attribute, unless
32
+ # explicitly set beforehand. The Subscription objects removed are all of
33
+ # those, and only those, for which the Stomper::Subscription#receives_for?
34
+ # method returns true given the destination and/or subscription id.
35
+ #
36
+ # This method returns an array of all the Subscription objects that were
37
+ # removed, or an empty array if none were removed.
38
+ #
39
+ # See also: Stomper::Subscription#receives_for?, Stomper::Client#unsubscribe
40
+ def remove(dest, subid=nil)
41
+ if dest.is_a?(Hash)
42
+ subid ||= dest[:id]
43
+ dest = dest[:destination]
44
+ elsif dest.is_a?(Subscription)
45
+ subid ||= dest.id
46
+ dest = dest.destination
47
+ end
48
+ _remove(dest, subid)
49
+ end
50
+
51
+ # Returns the number of Subscription objects within the container through
52
+ # the use of synchronization.
53
+ def size
54
+ @sub_lock.synchronize { @subs.size }
55
+ end
56
+
57
+ # Returns the first Subscription object within the container through
58
+ # the use of synchronization.
59
+ def first
60
+ @sub_lock.synchronize { @subs.first }
61
+ end
62
+
63
+ # Returns the last Subscription object within the container through
64
+ # the use of synchronization.
65
+ def last
66
+ @sub_lock.synchronize { @subs.last }
67
+ end
68
+
69
+ # Evaluates the supplied +block+ for each Subscription object
70
+ # within the container, or yields an Enumerator for the collection
71
+ # if no +block+ is given. As this method is synchronized, it is
72
+ # entirely possible to enter into a dead-lock if the supplied block
73
+ # in turn calls any other synchronized method of the container.
74
+ # [This could be remedied by creating a new array with
75
+ # the same Subscription objects currently contained, and performing
76
+ # the +each+ call on the new array. Give this some thought.]
77
+ def each(&block)
78
+ @sub_lock.synchronize { @subs.each(&block) }
79
+ end
80
+
81
+ # Passes the supplied +message+ to all Subscription objects within the
82
+ # collection through their Stomper::Subscription#perform method.
83
+ def perform(message)
84
+ @sub_lock.synchronize { @subs.each { |sub| sub.perform(message) } }
85
+ end
86
+
87
+ private
88
+ def _remove(dest, subid)
89
+ @sub_lock.synchronize do
90
+ to_remove, @subs = @subs.partition { |s| s.receives_for?(dest,subid) }
91
+ to_remove
92
+ end
93
+ end
94
+ end
95
+ end