stomper 0.3.0

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,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