stomper 0.3.0

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