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