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