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.
- data/AUTHORS +21 -0
- data/CHANGELOG +3 -0
- data/LICENSE +202 -0
- data/README.rdoc +68 -0
- data/lib/stomper.rb +15 -0
- data/lib/stomper/client.rb +300 -0
- data/lib/stomper/connection.rb +176 -0
- data/lib/stomper/frames.rb +24 -0
- data/lib/stomper/frames/abort.rb +14 -0
- data/lib/stomper/frames/ack.rb +29 -0
- data/lib/stomper/frames/begin.rb +14 -0
- data/lib/stomper/frames/client_frame.rb +86 -0
- data/lib/stomper/frames/commit.rb +14 -0
- data/lib/stomper/frames/connect.rb +15 -0
- data/lib/stomper/frames/connected.rb +27 -0
- data/lib/stomper/frames/disconnect.rb +13 -0
- data/lib/stomper/frames/error.rb +26 -0
- data/lib/stomper/frames/headers.rb +68 -0
- data/lib/stomper/frames/message.rb +44 -0
- data/lib/stomper/frames/receipt.rb +24 -0
- data/lib/stomper/frames/send.rb +14 -0
- data/lib/stomper/frames/server_frame.rb +48 -0
- data/lib/stomper/frames/subscribe.rb +47 -0
- data/lib/stomper/frames/unsubscribe.rb +23 -0
- data/lib/stomper/subscription.rb +128 -0
- data/lib/stomper/subscriptions.rb +95 -0
- data/lib/stomper/transaction.rb +180 -0
- data/spec/client_spec.rb +167 -0
- data/spec/connection_spec.rb +12 -0
- data/spec/frames/client_frame_spec.rb +142 -0
- data/spec/frames/headers_spec.rb +54 -0
- data/spec/frames/server_frame_spec.rb +86 -0
- data/spec/shared_connection_examples.rb +84 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/subscription_spec.rb +157 -0
- data/spec/subscriptions_spec.rb +148 -0
- data/spec/transaction_spec.rb +139 -0
- metadata +121 -0
@@ -0,0 +1,176 @@
|
|
1
|
+
module Stomper
|
2
|
+
# A low level connection to a Stomp message broker.
|
3
|
+
# Instances of Connection are not synchronized and thus not
|
4
|
+
# directly thread safe. This is a deliberate decision as instances of
|
5
|
+
# Stomper::Client are the preferred way of communicating with
|
6
|
+
# Stomp message broker services.
|
7
|
+
class Connection
|
8
|
+
attr_reader :uri
|
9
|
+
attr_reader :socket
|
10
|
+
|
11
|
+
# Creates a new connection to the Stomp broker specified by +uri+.
|
12
|
+
# The +uri+ parameter may be either a URI object, or something that can
|
13
|
+
# be parsed by URI.parse, such as a string.
|
14
|
+
# Some examples of acceptable +uri+ forms include:
|
15
|
+
# [stomp:///] Connection will be made to 'localhost' on port 61613 with no login credentials.
|
16
|
+
# [stomp+ssl:///] Same as above, but connection will be made on port 61612 and wrapped by SSL.
|
17
|
+
# [stomp://user:pass@host.tld] Connection will be made to 'host.tld', authenticating as 'user' with a password of 'pass'.
|
18
|
+
# [stomp://user:pass@host.tld:86753] Connection will be made to 'host.tld' on port 86753, authenticating as above.
|
19
|
+
# [stomp://host.tld:86753] Connection will be made to 'host.tld' on port 86753, with no authentication.
|
20
|
+
#
|
21
|
+
# In order to wrap the connection with SSL, the schema of +uri+ must be 'stomp+ssl';
|
22
|
+
# however, if SSL is not required, the schema is essentially ignored.
|
23
|
+
# The default port for the 'stomp+ssl' schema is 61612, all other schemas
|
24
|
+
# default to port 61613.
|
25
|
+
#
|
26
|
+
# The +opts+ parameter is a hash of options, and can include:
|
27
|
+
#
|
28
|
+
# [:connect_now] Immediately connect to the broker when a new instance is created (default: true)
|
29
|
+
def initialize(uri, opts = {})
|
30
|
+
connect_now = opts.delete(:connect_now) { true }
|
31
|
+
@uri = (uri.is_a?(URI) && uri) || URI.parse(uri)
|
32
|
+
@uri.port = (@uri.scheme == "stomp+ssl") ? 61612 : 61613 if @uri.port.nil?
|
33
|
+
@uri.host = 'localhost' if @uri.host.nil?
|
34
|
+
@uri.freeze
|
35
|
+
@use_ssl = (@uri.scheme == "stomp+ssl")
|
36
|
+
if @use_ssl
|
37
|
+
@ssl_context = OpenSSL::SSL::SSLContext.new
|
38
|
+
@ssl_context.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
39
|
+
end
|
40
|
+
@connected = false
|
41
|
+
connect if connect_now
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# Connects to the broker specified by the +uri+ attribute.
|
46
|
+
# By default, this method is invoked when a new Stomper::Connection
|
47
|
+
# is created.
|
48
|
+
#
|
49
|
+
# See also: new
|
50
|
+
def connect
|
51
|
+
s = TCPSocket.open(@uri.host, @uri.port)
|
52
|
+
if @use_ssl
|
53
|
+
s = OpenSSL::SSL::SSLSocket.new(s, @ssl_context)
|
54
|
+
s.sync_close = true
|
55
|
+
s.connect
|
56
|
+
end
|
57
|
+
@socket = s
|
58
|
+
transmit Stomper::Frames::Connect.new(@uri.user, @uri.password)
|
59
|
+
# Block until the first frame is received
|
60
|
+
connect_frame = receive(true)
|
61
|
+
@connected = connect_frame.instance_of?(Stomper::Frames::Connected)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns true when there is an open connection
|
65
|
+
# established to the broker.
|
66
|
+
def connected?
|
67
|
+
# FIXME: @socket.eof? appears to block or otherwise "wonk out", not sure
|
68
|
+
# why yet.
|
69
|
+
#!(@socket.closed? || @socket.eof?)
|
70
|
+
@connected && @socket && !@socket.closed?
|
71
|
+
end
|
72
|
+
|
73
|
+
# Immediately closes the connection to the broker.
|
74
|
+
#
|
75
|
+
# See also: disconnect
|
76
|
+
def close
|
77
|
+
@socket.close
|
78
|
+
ensure
|
79
|
+
@connected = false
|
80
|
+
end
|
81
|
+
|
82
|
+
# Transmits a Stomper::Frames::Disconnect frame to the broker
|
83
|
+
# then terminates the connection by invoking +close+.
|
84
|
+
#
|
85
|
+
# See also: close
|
86
|
+
def disconnect
|
87
|
+
transmit(Stomper::Frames::Disconnect.new)
|
88
|
+
ensure
|
89
|
+
close
|
90
|
+
end
|
91
|
+
|
92
|
+
# Converts an instance of Stomper::Frames::ClientFrame into
|
93
|
+
# a string conforming to the Stomp protocol and sends it
|
94
|
+
# to the broker.
|
95
|
+
def transmit(frame)
|
96
|
+
@socket.write(frame.to_stomp)
|
97
|
+
end
|
98
|
+
|
99
|
+
# Receives a single Stomper::Frames::ServerFrame from the broker.
|
100
|
+
# If the frame received is known to the Stomper library, an instance of
|
101
|
+
# the appropriate subclass will be returned (eg: Stomper::Frames::Message),
|
102
|
+
# otherwise an instance of Stomper::Frames::ServerFrame is returned with
|
103
|
+
# the +command+ attribute set to the frame type.
|
104
|
+
# If the +blocking+ parameter is set to true, +receive+ will
|
105
|
+
# block until there is a frame available from the server, otherwise if no frame
|
106
|
+
# is currently available, +nil+ is returned.
|
107
|
+
#
|
108
|
+
# If an incoming message is malformed (not terminated with a NULL (\0)
|
109
|
+
# character, or has an incorrectly specified +content+-+length+ header,
|
110
|
+
# this method will raise an exception. [Type the exception, don't rely on
|
111
|
+
# a basic RuntimeError or whatever the default is.]
|
112
|
+
def receive(blocking=false)
|
113
|
+
command = ''
|
114
|
+
while (ready? || blocking) && (command = @socket.gets)
|
115
|
+
command.chomp!
|
116
|
+
break if command.size > 0
|
117
|
+
end
|
118
|
+
# If we got a command, continue on, potentially blocking until
|
119
|
+
# the entire message is received, otherwise we bail out now.
|
120
|
+
return nil if command.nil? || command.size == 0
|
121
|
+
headers = {}
|
122
|
+
while (line = @socket.gets)
|
123
|
+
line.chomp!
|
124
|
+
break if line.size == 0
|
125
|
+
delim = line.index(':')
|
126
|
+
if delim
|
127
|
+
key = line[0..(delim-1)]
|
128
|
+
val = line[(delim+1)..-1]
|
129
|
+
headers[key] = val
|
130
|
+
end
|
131
|
+
end
|
132
|
+
body = nil
|
133
|
+
# Have we been given a content length?
|
134
|
+
if headers['content-length']
|
135
|
+
body = @socket.read(headers['content-length'].to_i)
|
136
|
+
raise "Invalid message terminator or content-length header" if socket_c_to_i(@socket.getc) != 0
|
137
|
+
else
|
138
|
+
body = ''
|
139
|
+
# We read until we find the first nil character
|
140
|
+
while (c = @socket.getc)
|
141
|
+
# Both Ruby 1.8 and 1.9 should support this even though the behavior
|
142
|
+
# of getc is different between the two. However, jruby is particular
|
143
|
+
# about this. And that sucks.
|
144
|
+
break if socket_c_to_i(c) == 0
|
145
|
+
body << socket_c_to_chr(c)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
# Messages should be forever immutable.
|
149
|
+
Stomper::Frames::ServerFrame.build(command, headers, body).freeze
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
def ready?
|
154
|
+
(@use_ssl) ? @socket.io.ready? : @socket.ready?
|
155
|
+
end
|
156
|
+
|
157
|
+
def socket_c_to_i(c)
|
158
|
+
if c.respond_to?(:ord)
|
159
|
+
def socket_c_to_i(char); char.ord; end
|
160
|
+
c.ord
|
161
|
+
else
|
162
|
+
def socket_c_to_i(char); char; end
|
163
|
+
c
|
164
|
+
end
|
165
|
+
end
|
166
|
+
def socket_c_to_chr(c)
|
167
|
+
if c.respond_to?(:chr)
|
168
|
+
def socket_c_to_chr(char); char.chr; end
|
169
|
+
c.chr
|
170
|
+
else
|
171
|
+
def socket_c_to_chr(char); char; end
|
172
|
+
c
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'stomper/frames/headers'
|
2
|
+
require 'stomper/frames/client_frame'
|
3
|
+
require 'stomper/frames/server_frame'
|
4
|
+
require 'stomper/frames/abort'
|
5
|
+
require 'stomper/frames/ack'
|
6
|
+
require 'stomper/frames/begin'
|
7
|
+
require 'stomper/frames/commit'
|
8
|
+
require 'stomper/frames/connect'
|
9
|
+
require 'stomper/frames/connected'
|
10
|
+
require 'stomper/frames/disconnect'
|
11
|
+
require 'stomper/frames/error'
|
12
|
+
require 'stomper/frames/message'
|
13
|
+
require 'stomper/frames/receipt'
|
14
|
+
require 'stomper/frames/send'
|
15
|
+
require 'stomper/frames/subscribe'
|
16
|
+
require 'stomper/frames/unsubscribe'
|
17
|
+
|
18
|
+
module Stomper
|
19
|
+
# This module holds all known encapsulations of
|
20
|
+
# frames that are part of the
|
21
|
+
# {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
22
|
+
module Frames
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates an "ACK" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Abort < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(transaction_id, headers={})
|
9
|
+
super('ABORT', headers)
|
10
|
+
@headers.transaction = transaction_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates an "ACK" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Ack < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(message_id, headers={})
|
9
|
+
super('ACK', headers)
|
10
|
+
@headers["message-id"] = message_id
|
11
|
+
end
|
12
|
+
|
13
|
+
# Creates a new Ack instance that corresponds to an acknowledgement
|
14
|
+
# of the supplied +message+, with any additional +headers+. The
|
15
|
+
# +message+ parameter may be an instance of Stomper::Frames::Message, or
|
16
|
+
# a message id. If +message+ is an instance of Stomper::Frames::Message
|
17
|
+
# and was exchanged as part of a transaction, the transaction header from
|
18
|
+
# +message+ will be injected into the newly created Ack object's headers.
|
19
|
+
def self.ack_for(message, headers = {})
|
20
|
+
if message.is_a?(Message)
|
21
|
+
headers['transaction'] = message.headers.transaction if message.headers.transaction
|
22
|
+
new(message.id, headers)
|
23
|
+
else
|
24
|
+
new(message.to_s)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a "BEGIN" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Begin < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(transaction_id, headers={})
|
9
|
+
super('BEGIN', headers)
|
10
|
+
@headers.transaction = transaction_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a client side frame for the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class ClientFrame
|
8
|
+
attr_reader :headers, :body, :command
|
9
|
+
|
10
|
+
# Creates a new ClientFrame instance with the specified +command+,
|
11
|
+
# +headers+ and +body+.
|
12
|
+
# If +headers+ includes a key of :generate_content_length, the
|
13
|
+
# associated value will determine if a 'content-length' header is automatically
|
14
|
+
# generated for this particular frame instance. This key can be
|
15
|
+
# specified in the +headers+ parameter of any of the subclasses of ClientFrame,
|
16
|
+
# and it will be interpretted in the same fashion.
|
17
|
+
def initialize(command, headers={}, body=nil)
|
18
|
+
@command = command
|
19
|
+
@generate_content_length = headers.delete(:generate_content_length)
|
20
|
+
@headers = Headers.new(headers)
|
21
|
+
@body = body
|
22
|
+
end
|
23
|
+
|
24
|
+
# If +bool+ false or nil, this frame instance will not attempt to
|
25
|
+
# automatically generate a content-length header. This is useful when
|
26
|
+
# dealing with ActiveMQ as a stomp message broker, which will treat incoming
|
27
|
+
# messages lacking a content-length header as +TextMessage+s and
|
28
|
+
# +BytesMessage+s if the header is present. For more information see:
|
29
|
+
# {Apache ActiveMQ - Stomp}[http://activemq.apache.org/stomp.html]
|
30
|
+
def generate_content_length=(bool)
|
31
|
+
@generate_content_length=bool
|
32
|
+
end
|
33
|
+
|
34
|
+
# If generate_content_length= has been called on this instance, then the
|
35
|
+
# value supplied there is returned here. Otherwise, we defer to the
|
36
|
+
# class method of the same name.
|
37
|
+
def generate_content_length?
|
38
|
+
@generate_content_length.nil? ? self.class.generate_content_length? : @generate_content_length
|
39
|
+
end
|
40
|
+
|
41
|
+
# Converts the frame instance into a valid string representation of the
|
42
|
+
# desired command according to the specifications of the
|
43
|
+
# {Stomp Protocol}[http://stomp.codehaus.org/Protocol]
|
44
|
+
#
|
45
|
+
# This is where the content-length header is generated if the frame
|
46
|
+
# has a body of non-zero length and generate_content_length? is true.
|
47
|
+
def to_stomp
|
48
|
+
@headers["content-length"] = str_size(@body) if @body && !@body.empty? && generate_content_length?
|
49
|
+
"#{@command}\n#{@headers.to_stomp}\n#{@body}\0"
|
50
|
+
end
|
51
|
+
|
52
|
+
class << self
|
53
|
+
# Sets a class level setting for determining if a content-length header
|
54
|
+
# should automatically be generated.
|
55
|
+
def generate_content_length=(bool)
|
56
|
+
@generate_content_length = bool
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns the value passed to the class level generate_content_length=
|
60
|
+
# method, or true if no value has been set (thus, defaults to true.)
|
61
|
+
#
|
62
|
+
# The precedence for resolving whether or not a content-length header
|
63
|
+
# is generated by the to_stomp method is: check the instance setting,
|
64
|
+
# if it has not been set, defer to the class setting, if it hasn't
|
65
|
+
# been set, default to true.
|
66
|
+
def generate_content_length?
|
67
|
+
if @generate_content_length.nil?
|
68
|
+
@generate_content_length = true
|
69
|
+
end
|
70
|
+
@generate_content_length
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
def str_size(str)
|
76
|
+
if str.respond_to?(:bytesize)
|
77
|
+
def str_size(strng); strng.bytesize; end
|
78
|
+
str.bytesize
|
79
|
+
else
|
80
|
+
def str_size(strng); strng.size; end
|
81
|
+
str.size
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a "COMMIT" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Commit < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(transaction_id, headers={})
|
9
|
+
super('COMMIT', headers)
|
10
|
+
@headers.transaction = transaction_id
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a "CONNECT" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Connect < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(username='', password='', headers={})
|
9
|
+
super('CONNECT', headers)
|
10
|
+
@headers.login = username
|
11
|
+
@headers.passcode = password
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a "CONNECTED" 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 Connected < Stomper::Frames::ServerFrame
|
8
|
+
# This class is a factory for incoming 'CONNECTED' commands.
|
9
|
+
factory_for :connected
|
10
|
+
|
11
|
+
# Builds a Connected frame instance with the supplied
|
12
|
+
# +headers+ and +body+
|
13
|
+
def initialize(headers, body)
|
14
|
+
super('CONNECTED', headers, body)
|
15
|
+
end
|
16
|
+
|
17
|
+
# A convenience method that returns the value of
|
18
|
+
# the session header, if it is set.
|
19
|
+
#
|
20
|
+
# This value can also be accessed as:
|
21
|
+
# frame.headers.session or frame.headers['session'] or frame.headers[:session]
|
22
|
+
def session
|
23
|
+
@headers.session
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates a "DISCONNECT" frame from the Stomp Protocol.
|
4
|
+
#
|
5
|
+
# See the {Stomp Protocol Specification}[http://stomp.codehaus.org/Protocol]
|
6
|
+
# for more details.
|
7
|
+
class Disconnect < Stomper::Frames::ClientFrame
|
8
|
+
def initialize(headers={})
|
9
|
+
super('DISCONNECT', headers)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Stomper
|
2
|
+
module Frames
|
3
|
+
# Encapsulates an "ERROR" 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 Error < Stomper::Frames::ServerFrame
|
8
|
+
# This class is a factory for all incoming ERROR frames.
|
9
|
+
factory_for :error
|
10
|
+
|
11
|
+
# Creates a new Error frame with the supplied +headers+ and +body+
|
12
|
+
def initialize(headers, body)
|
13
|
+
super('ERROR', headers, body)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the message responsible for the generation of this Error frame,
|
17
|
+
# if applicable.
|
18
|
+
#
|
19
|
+
# This is a convenience method for:
|
20
|
+
# frame.headers[:message], frame.headers['message'], or frame.headers.message
|
21
|
+
def message
|
22
|
+
@headers.message
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|