stomper 1.0.0 → 2.0.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.
Files changed (152) hide show
  1. data/.gitignore +5 -0
  2. data/{spec/spec.opts → .rspec} +0 -2
  3. data/Gemfile +4 -0
  4. data/LICENSE +201 -201
  5. data/README.md +130 -0
  6. data/Rakefile +5 -0
  7. data/examples/basic.rb +38 -0
  8. data/examples/events.rb +54 -0
  9. data/features/acking_messages.feature +147 -0
  10. data/features/disconnecting.feature +12 -0
  11. data/features/establish_connection.feature +44 -0
  12. data/features/protocol_version_negotiation.feature +61 -0
  13. data/features/receipts.feature +72 -0
  14. data/features/scopes.feature +32 -0
  15. data/features/secure_connections.feature +38 -0
  16. data/features/send_and_message.feature +28 -0
  17. data/features/steps/acking_messages_steps.rb +39 -0
  18. data/features/steps/disconnecting_steps.rb +8 -0
  19. data/features/steps/establish_connection_steps.rb +74 -0
  20. data/features/steps/frame_transmission_steps.rb +35 -0
  21. data/features/steps/protocol_version_negotiation_steps.rb +15 -0
  22. data/features/steps/receipts_steps.rb +79 -0
  23. data/features/steps/scopes_steps.rb +52 -0
  24. data/features/steps/secure_connections_steps.rb +41 -0
  25. data/features/steps/send_and_message_steps.rb +35 -0
  26. data/features/steps/subscribing_steps.rb +36 -0
  27. data/features/steps/threaded_receiver_steps.rb +8 -0
  28. data/features/steps/transactions_steps.rb +0 -0
  29. data/features/subscribing.feature +151 -0
  30. data/features/support/env.rb +11 -0
  31. data/features/support/header_helpers.rb +12 -0
  32. data/features/support/ssl/README +6 -0
  33. data/features/support/ssl/broker_cert.csr +17 -0
  34. data/features/support/ssl/broker_cert.pem +72 -0
  35. data/features/support/ssl/broker_key.pem +27 -0
  36. data/features/support/ssl/client_cert.csr +17 -0
  37. data/features/support/ssl/client_cert.pem +72 -0
  38. data/features/support/ssl/client_key.pem +27 -0
  39. data/features/support/ssl/demoCA/cacert.pem +17 -0
  40. data/features/support/ssl/demoCA/index.txt +2 -0
  41. data/features/support/ssl/demoCA/index.txt.attr +1 -0
  42. data/features/support/ssl/demoCA/index.txt.attr.old +1 -0
  43. data/features/support/ssl/demoCA/index.txt.old +1 -0
  44. data/features/support/ssl/demoCA/newcerts/01.pem +72 -0
  45. data/features/support/ssl/demoCA/newcerts/02.pem +72 -0
  46. data/features/support/ssl/demoCA/private/cakey.pem +17 -0
  47. data/features/support/ssl/demoCA/serial +1 -0
  48. data/features/support/ssl/demoCA/serial.old +1 -0
  49. data/features/support/test_stomp_server.rb +150 -0
  50. data/features/threaded_receiver.feature +11 -0
  51. data/features/transactions.feature +66 -0
  52. data/lib/stomper.rb +30 -20
  53. data/lib/stomper/connection.rb +442 -102
  54. data/lib/stomper/errors.rb +59 -0
  55. data/lib/stomper/extensions.rb +10 -0
  56. data/lib/stomper/extensions/common.rb +258 -0
  57. data/lib/stomper/extensions/events.rb +213 -0
  58. data/lib/stomper/extensions/heartbeat.rb +101 -0
  59. data/lib/stomper/extensions/scoping.rb +56 -0
  60. data/lib/stomper/frame.rb +54 -0
  61. data/lib/stomper/frame_serializer.rb +217 -0
  62. data/lib/stomper/headers.rb +15 -0
  63. data/lib/stomper/receipt_manager.rb +36 -0
  64. data/lib/stomper/receivers.rb +7 -0
  65. data/lib/stomper/receivers/threaded.rb +71 -0
  66. data/lib/stomper/scopes.rb +9 -0
  67. data/lib/stomper/scopes/header_scope.rb +49 -0
  68. data/lib/stomper/scopes/receipt_scope.rb +44 -0
  69. data/lib/stomper/scopes/transaction_scope.rb +109 -0
  70. data/lib/stomper/sockets.rb +66 -28
  71. data/lib/stomper/subscription_manager.rb +79 -0
  72. data/lib/stomper/support.rb +68 -0
  73. data/lib/stomper/support/1.8/frame_serializer.rb +53 -0
  74. data/lib/stomper/support/1.8/headers.rb +183 -0
  75. data/lib/stomper/support/1.9/frame_serializer.rb +64 -0
  76. data/lib/stomper/support/1.9/headers.rb +172 -0
  77. data/lib/stomper/support/ruby.rb +13 -0
  78. data/lib/stomper/uris.rb +49 -0
  79. data/lib/stomper/version.rb +7 -0
  80. data/spec/spec_helper.rb +13 -9
  81. data/spec/stomper/connection_spec.rb +712 -0
  82. data/spec/stomper/extensions/common_spec.rb +187 -0
  83. data/spec/stomper/extensions/events_spec.rb +78 -0
  84. data/spec/stomper/extensions/heartbeat_spec.rb +103 -0
  85. data/spec/stomper/extensions/scoping_spec.rb +21 -0
  86. data/spec/stomper/frame_serializer_1.8_spec.rb +318 -0
  87. data/spec/stomper/frame_serializer_spec.rb +316 -0
  88. data/spec/stomper/frame_spec.rb +36 -0
  89. data/spec/stomper/headers_spec.rb +224 -0
  90. data/spec/stomper/receipt_manager_spec.rb +91 -0
  91. data/spec/stomper/receivers/threaded_spec.rb +116 -0
  92. data/spec/stomper/scopes/header_scope_spec.rb +42 -0
  93. data/spec/stomper/scopes/receipt_scope_spec.rb +51 -0
  94. data/spec/stomper/scopes/transaction_scope_spec.rb +183 -0
  95. data/spec/stomper/sockets_spec.rb +113 -0
  96. data/spec/stomper/subscription_manager_spec.rb +107 -0
  97. data/spec/stomper/support_spec.rb +69 -0
  98. data/spec/stomper/uris_spec.rb +54 -0
  99. data/spec/stomper_spec.rb +9 -0
  100. data/spec/support/custom_argument_matchers.rb +57 -0
  101. data/spec/support/existential_frame_matchers.rb +19 -0
  102. data/spec/support/frame_header_matchers.rb +10 -0
  103. data/stomper.gemspec +30 -0
  104. metadata +272 -97
  105. data/AUTHORS +0 -21
  106. data/CHANGELOG +0 -20
  107. data/README.rdoc +0 -120
  108. data/lib/stomper/client.rb +0 -34
  109. data/lib/stomper/frame_reader.rb +0 -73
  110. data/lib/stomper/frame_writer.rb +0 -21
  111. data/lib/stomper/frames.rb +0 -39
  112. data/lib/stomper/frames/abort.rb +0 -10
  113. data/lib/stomper/frames/ack.rb +0 -25
  114. data/lib/stomper/frames/begin.rb +0 -11
  115. data/lib/stomper/frames/client_frame.rb +0 -89
  116. data/lib/stomper/frames/commit.rb +0 -10
  117. data/lib/stomper/frames/connect.rb +0 -10
  118. data/lib/stomper/frames/connected.rb +0 -30
  119. data/lib/stomper/frames/disconnect.rb +0 -10
  120. data/lib/stomper/frames/error.rb +0 -21
  121. data/lib/stomper/frames/message.rb +0 -48
  122. data/lib/stomper/frames/receipt.rb +0 -19
  123. data/lib/stomper/frames/send.rb +0 -10
  124. data/lib/stomper/frames/server_frame.rb +0 -38
  125. data/lib/stomper/frames/subscribe.rb +0 -42
  126. data/lib/stomper/frames/unsubscribe.rb +0 -19
  127. data/lib/stomper/open_uri_interface.rb +0 -41
  128. data/lib/stomper/receipt_handlers.rb +0 -23
  129. data/lib/stomper/receiptor.rb +0 -38
  130. data/lib/stomper/subscriber.rb +0 -76
  131. data/lib/stomper/subscription.rb +0 -128
  132. data/lib/stomper/subscriptions.rb +0 -95
  133. data/lib/stomper/threaded_receiver.rb +0 -59
  134. data/lib/stomper/transaction.rb +0 -185
  135. data/lib/stomper/transactor.rb +0 -50
  136. data/lib/stomper/uri.rb +0 -55
  137. data/spec/client_spec.rb +0 -29
  138. data/spec/connection_spec.rb +0 -22
  139. data/spec/frame_reader_spec.rb +0 -37
  140. data/spec/frame_writer_spec.rb +0 -27
  141. data/spec/frames/client_frame_spec.rb +0 -66
  142. data/spec/frames/indirect_frame_spec.rb +0 -45
  143. data/spec/frames/server_frame_spec.rb +0 -85
  144. data/spec/open_uri_interface_spec.rb +0 -132
  145. data/spec/receiptor_spec.rb +0 -35
  146. data/spec/shared_connection_examples.rb +0 -79
  147. data/spec/subscriber_spec.rb +0 -77
  148. data/spec/subscription_spec.rb +0 -157
  149. data/spec/subscriptions_spec.rb +0 -145
  150. data/spec/threaded_receiver_spec.rb +0 -33
  151. data/spec/transaction_spec.rb +0 -139
  152. data/spec/transactor_spec.rb +0 -46
data/lib/stomper.rb CHANGED
@@ -1,27 +1,37 @@
1
- require 'delegate'
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # For extensions to URI.parse for Stomp schemes.
2
4
  require 'uri'
3
- require 'io/wait'
5
+ # Primarily for CGI.parse
6
+ require 'cgi'
7
+ # Sockets are fairly important in all of this.
4
8
  require 'socket'
9
+ # As is openssl
10
+ require 'openssl'
11
+ # For IO#ready?
12
+ require 'io/wait'
13
+ # The socket helpers use this to delegate to the real sockets
14
+ require 'delegate'
15
+ # Threading and Mutex support
5
16
  require 'thread'
17
+ # Monitor support (prevent recursive dead locking)
6
18
  require 'monitor'
7
- require 'openssl'
8
- require 'stomper/uri'
9
- require 'stomper/frames'
10
- require 'stomper/frame_reader'
11
- require 'stomper/frame_writer'
12
- require 'stomper/sockets'
13
- require 'stomper/client'
14
- require 'stomper/transactor'
15
- require 'stomper/subscriber'
16
- require 'stomper/receiptor'
17
- require 'stomper/open_uri_interface'
18
- require 'stomper/threaded_receiver'
19
- require 'stomper/connection'
20
- require 'stomper/transaction'
21
- require 'stomper/subscription'
22
- require 'stomper/subscriptions'
23
- require 'stomper/receipt_handlers'
24
19
 
20
+ # Primary namespace of the stomper gem.
25
21
  module Stomper
26
- class MalformedFrameError < StandardError; end
27
22
  end
23
+
24
+ require 'stomper/version'
25
+ require 'stomper/errors'
26
+ require 'stomper/headers'
27
+ require 'stomper/sockets'
28
+ require 'stomper/frame'
29
+ require 'stomper/uris'
30
+ require 'stomper/frame_serializer'
31
+ require 'stomper/subscription_manager'
32
+ require 'stomper/receipt_manager'
33
+ require 'stomper/receivers'
34
+ require 'stomper/extensions'
35
+ require 'stomper/scopes'
36
+ require 'stomper/support'
37
+ require 'stomper/connection'
@@ -1,120 +1,460 @@
1
- module Stomper
2
- # A connection to a Stomp message broker. This class includes the Client,
3
- # Transactor, Subscriber, Receiptor and, in most cases, the ThreadedReceiver
4
- # modules.
5
- #
6
- # @see Client
7
- # @see Transactor
8
- # @see Subscriber
9
- # @see Receiptor
10
- # @see ThreadedReceiver
11
- class Connection
12
- attr_reader :uri
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # This class encapsulates a client connection to a message broker through the
4
+ # Stomp protocol. This class is also aliased as +Stomper::Client+
5
+ class Stomper::Connection
6
+ include ::Stomper::Extensions::Common
7
+ include ::Stomper::Extensions::Scoping
8
+ include ::Stomper::Extensions::Events
9
+ include ::Stomper::Extensions::Heartbeat
10
+
11
+ # The list of supported protocol versions
12
+ # @return [Array<String>]
13
+ PROTOCOL_VERSIONS = ['1.0', '1.1']
14
+
15
+ # The default configuration for connections. These settings have been
16
+ # deliberately left unfrozen to allow users to change defaults for all
17
+ # connections in one fell swoop.
18
+ # @return [{Symbol => Object}]
19
+ DEFAULT_CONFIG = {
20
+ :versions => ['1.0', '1.1'],
21
+ :heartbeats => [0, 0],
22
+ :host => nil,
23
+ :login => nil,
24
+ :passcode => nil,
25
+ :receiver_class => ::Stomper::Receivers::Threaded
26
+ }
13
27
 
14
- class << self
15
- def connect(uri, opts={})
16
- connex = new(uri, opts)
17
- connex.connect
18
- connex
28
+ # The URI representation of the broker this connection is associated with
29
+ # @return [URI]
30
+ attr_reader :uri
31
+
32
+ # The CONNECTED frame sent by the broker during the connection handshake.
33
+ # @return [Stomper::Frame,nil]
34
+ attr_reader :connected_frame
35
+
36
+ # The protocol versions to allow for this connection
37
+ # @return [Array<String>]
38
+ attr_reader :versions
39
+
40
+ # The protocol version negotiated between the client and broker. Will be
41
+ # +nil+ until the connection has been established.
42
+ # @return [String,nil]
43
+ attr_reader :version
44
+
45
+ # The client-side heartbeat settings to allow for this connection
46
+ # @return [Array<Fixnum>]
47
+ attr_reader :heartbeats
48
+
49
+ # The negotiated heartbeat strategy. The first element is the maximum
50
+ # number of milliseconds that the client can go without transmitting
51
+ # data or a heartbeat (a zero indicates that a client does not need to
52
+ # send heartbeats.) The second elemenet is the maximum number of milliseconds
53
+ # a server will go without transmitting data or a heartbeat (a zero indicates
54
+ # that the server need not send any heartbeats.)
55
+ # @return [Array<Fixnum>]
56
+ attr_reader :heartbeating
57
+
58
+ # The SSL options to use if this connection is secure
59
+ # @return [{Symbol => Object}, nil]
60
+ attr_reader :ssl
61
+
62
+ # The host header value to send to the broker when connecting. This allows
63
+ # the client to inform the server which host it wishes to connect with
64
+ # when multiple brokers may share an IP address through virtual hosting.
65
+ # @return [String]
66
+ attr_reader :host
67
+
68
+ # The login header value to send to the broker when connecting.
69
+ # @return [String]
70
+ attr_reader :login
71
+
72
+ # The passcode header value to send to the broker when connecting.
73
+ # @return [String]
74
+ attr_reader :passcode
75
+
76
+ # The class to use when instantiating a new receiver for the connection.
77
+ # Defaults to {Stomper::Receivers::Threaded}
78
+ # @return [CLass]
79
+ attr_reader :receiver_class
80
+
81
+ # A timestamp set to the last time a frame was transmitted. Returns +nil+
82
+ # if no frames have been transmitted yet
83
+ # @return [Time,nil]
84
+ attr_reader :last_transmitted_at
85
+
86
+ # A timestamp set to the last time a frame was received. Returns +nil+
87
+ # if no frames have been received yet
88
+ # @return [Time,nil]
89
+ attr_reader :last_received_at
90
+
91
+ # The subscription manager. Maintains the list of destinations subscribed
92
+ # to as well as the callbacks to invoke when a MESSAGE frame is received
93
+ # on one of them.
94
+ # @return [Stomper::SubscriptionManager]
95
+ attr_reader :subscription_manager
96
+
97
+ # The receipt manager. Maintains the list of receipt IDs and the callbacks
98
+ # associated with them that will be invoked when any frame with a matching
99
+ # +receipt-id+ header is received.
100
+ # @return [Stomper::ReceiptManager]
101
+ attr_reader :receipt_manager
102
+
103
+
104
+ # Creates a connection to a broker specified by the suppled uri. The given
105
+ # uri will be resolved to a URI instance through +URI.parse+. The final URI object must
106
+ # provide a {::URI::STOMP#create_socket create_socket} method, or an error
107
+ # will be raised. Both {::URI::STOMP} and {::URI::STOMP_SSL} provide this
108
+ # method, so string URIs with a scheme of either "stomp" or "stomp+ssl" will
109
+ # work automatically.
110
+ # Most connection options can be supplied through query parameters specified in the URI or
111
+ # through an optional +Hash+ parameter. If the same option is configured in
112
+ # both the URI's parameters and the options hash, the options hash takes
113
+ # precedence. Certain options, those pertaining to SSL settings for
114
+ # instance, must be configured through the options hash.
115
+ #
116
+ # @param [String] uri a string representing the URI to the message broker's
117
+ # Stomp interface.
118
+ # @param [{Symbol => Object}] options additional options for the connection.
119
+ # @option options [Array<String>] :versions (['1.0', '1.1']) protocol versions
120
+ # this connection should allow.
121
+ # @option options [Array<Fixnum>] :heartbeats ([0, 0]) heartbeat timings for
122
+ # this connection in milliseconds (a zero indicates that heartbeating is
123
+ # not desired from the client or the broker)
124
+ # @option options [{Symbol => Object}] :ssl ({}) SSL specific options to
125
+ # pass on when creating an {Stomper::Sockets::SSL SSL connection}.
126
+ # @option options [String] :host (nil) Host name to pass as +host+ header
127
+ # on CONNECT frames (will use actual connection hostname if not set)
128
+ # @option options [String] :login (nil) Username to send as +login+ header
129
+ # for credential authenticated connections.
130
+ # @option options [String] :passcode (nil) Password to send as +passcode+ header
131
+ # for credential authenticated connections.
132
+ #
133
+ # @example Connecting to a broker on 'host.domain.tld' and a port of 12345
134
+ # con = Stomper::Connection.new('stomp://host.domain.tld:12345')
135
+ #
136
+ # @example Connecting with login credentials
137
+ # con = Stomper::Connection.new('stomp://username:secret@host.domain.tld')
138
+ #
139
+ # @example Connecting using Stomp protocol 1.1, sending client beats once per second, and no interest in server beats.
140
+ # con = Stomper::Connection.new('stomp://host/?versions=1.1&heartbeats=1000&heartbeats=0')
141
+ # con = Stomper::Connection.new('stomp://host', :versions => '1.1', :heartbeats => [1000, 0])
142
+ # # both result in:
143
+ # con.heartbeat #=> [1000, 0]
144
+ # con.versions #=> ['1.1']
145
+ #
146
+ # @example Repeated options in URI and options hash
147
+ # con = Stomper::Connection.new('stomp://host?versions=1.1&versions=1.0', :versions => '1.1')
148
+ # con.versions #=> '1.1'
149
+ # # In this case, the versions query parameter value +[1.1 , 1.0]+ is
150
+ # # overridden by the options hash setting +1.1+
151
+ def initialize(uri, options={})
152
+ @ssl = options.delete(:ssl) || {}
153
+ @uri = uri.is_a?(::URI) ? uri : ::URI.parse(uri)
154
+ config = ::Stomper::Support.keys_to_sym(::CGI.parse(@uri.query || '')).
155
+ merge(::Stomper::Support.keys_to_sym(options))
156
+ DEFAULT_CONFIG.each do |attr_name, def_val|
157
+ if config.key? attr_name
158
+ __send__ :"#{attr_name}=", config[attr_name]
159
+ elsif def_val
160
+ __send__ :"#{attr_name}=", def_val
19
161
  end
20
- alias_method :open, :connect
21
162
  end
163
+ @host ||= (@uri.host||'localhost')
164
+ @login ||= (@uri.user || '')
165
+ @passcode ||= (@uri.password || '')
166
+ @connected = false
167
+ @heartbeating = [0,0]
168
+ @last_transmitted_at = @last_received_at = nil
169
+ @subscription_manager = ::Stomper::SubscriptionManager.new(self)
170
+ @receipt_manager = ::Stomper::ReceiptManager.new(self)
171
+ @connecting = false
172
+ @disconnecting = false
173
+
174
+ on_connected do |cf, con|
175
+ unless connected?
176
+ @connecting = false
177
+ @disconnecting = false
178
+ @version = (cf[:version].nil?||cf[:version].empty?) ? '1.0' : cf[:version]
179
+ unless @versions.include?(@version)
180
+ close
181
+ raise ::Stomper::Errors::UnsupportedProtocolVersionError,
182
+ "broker requested '#{@version}', client allows: #{@versions.inspect}"
183
+ end
184
+ c_x, c_y = @heartbeats
185
+ s_x, s_y = (cf[:'heart-beat'] || '0,0').split(',').map do |v|
186
+ vi = v.to_i
187
+ vi > 0 ? vi : 0
188
+ end
189
+ @heartbeating = [ (c_x == 0||s_y == 0 ? 0 : [c_x,s_y].max),
190
+ (c_y == 0||s_x == 0 ? 0 : [c_y,s_x].max) ]
22
191
 
23
- # Creates a new connection to the Stomp broker specified by +uri+.
24
- # The +uri+ parameter may be either a URI object, or something that can
25
- # be parsed by URI.parse, such as a string.
26
- # Some examples of acceptable +uri+ forms include:
27
- # [stomp:///] Connection will be made to 'localhost' on port 61613 with no login credentials.
28
- # [stomp+ssl:///] Same as above, but connection will be made on port 61612 and wrapped by SSL.
29
- # [stomp://user:pass@host.tld] Connection will be made to 'host.tld', authenticating as 'user' with a password of 'pass'.
30
- # [stomp://user:pass@host.tld:86753] Connection will be made to 'host.tld' on port 86753, authenticating as above.
31
- # [stomp://host.tld:86753] Connection will be made to 'host.tld' on port 86753, with no authentication.
32
- #
33
- # In order to wrap the connection with SSL, the schema of +uri+ must be 'stomp+ssl';
34
- # however, if SSL is not required, the schema is essentially ignored.
35
- # The default port for the 'stomp+ssl' schema is 61612, all other schemas
36
- # default to port 61613.
37
- def initialize(uri, opts={})
38
- if opts.delete(:threaded_receiver) { true }
39
- extend ::Stomper::ThreadedReceiver
192
+ extend_for_protocol
40
193
  end
41
- @uri = (uri.is_a?(URI) && uri) || URI.parse(uri)
42
- raise ArgumentError, 'Expected URI schema to be one of stomp or stomp+ssl' unless @uri.respond_to?(:create_socket)
43
- @connected = false
44
- @writer = @reader = nil
45
194
  end
46
-
47
-
48
- # Connects to the broker specified by the +uri+ attribute.
49
- # By default, this method is invoked when a new Stomper::Connection
50
- # is created.
51
- #
52
- # See also: new
53
- def connect
54
- @connected = false
55
- @socket = @uri.create_socket
56
- transmit Stomper::Frames::Connect.new(@uri.user, @uri.password)
57
- @connected = receive.instance_of?(Stomper::Frames::Connected)
195
+
196
+ on_disconnect do |df, con|
197
+ @disconnecting = true
198
+ close unless df[:receipt]
58
199
  end
59
-
60
- # Returns true when there is an open connection
61
- # established to the broker.
62
- def connected?
63
- @connected && @socket && !@socket.closed?
200
+ end
201
+
202
+ # Sets the protocol versions that are acceptable for this connection.
203
+ # This method accepts version numbers as:
204
+ # - A single string value (eg: '1.0')
205
+ # - An array of string values (eg: ['1.1', '1.0'])
206
+ # @overload versions=(version)
207
+ # Sets the acceptable versions to a single supplied version.
208
+ # @param [String] version the protocol version to accept
209
+ # @overload versions=(versions)
210
+ # Sets the acceptable versions to the list provided
211
+ # @param [Array<String>] versions list of acceptable protocol versions
212
+ # @return [Array<String>] acceptable protocol versions
213
+ # @raise [Stomper::Errors::UnsupportedProtocolVersionError] if none of the
214
+ # versions provided are supported by this library
215
+ def versions=(vers)
216
+ vers = [vers] unless vers.is_a?(Array)
217
+ @versions = PROTOCOL_VERSIONS.select { |v| vers.include? v }
218
+ if @versions.empty?
219
+ raise ::Stomper::Errors::UnsupportedProtocolVersionError, "no supported protocol versions in #{vers.inspect}"
64
220
  end
65
-
66
- # Transmits a Stomper::Frames::Disconnect frame to the broker
67
- # then terminates the connection by invoking +close+.
68
- def disconnect
69
- begin
70
- @connected = false
71
- transmit(Stomper::Frames::Disconnect.new)
72
- ensure
73
- close_socket
221
+ @versions
222
+ end
223
+
224
+ # Sets the client-side heartbeat settings to allow for this connection.
225
+ # The first element specifies the smallest number of milliseconds between
226
+ # heartbeats the client can guarantee, a value of 0 indicates that the client
227
+ # cannot send heartbeats. The second element specifies the
228
+ # duration in milliseconds between heartbeats that the client would like
229
+ # to receive from the server, a value of 0 indicates that the client does not
230
+ # want to receive server heartbeats. Both values are converted to integers,
231
+ # and negative values are replaced with 0.
232
+ #
233
+ # @param [Array<Fixnum>] beats
234
+ def heartbeats=(beats)
235
+ @heartbeats = beats[0..1].map { |b| bi = b.to_i; bi > 0 ? bi : 0 }
236
+ end
237
+
238
+ # Sets the host header value to use when connecting to the server. This
239
+ # provides the client with the ability to specify a specific broker that
240
+ # resides on a server that supports virtual hosts.
241
+ #
242
+ # @param [String] val
243
+ def host=(val); @host = (val.is_a?(Array) ? val.first : val).to_s; end
244
+
245
+ # Sets the login header value to use when connecting to the server.
246
+ #
247
+ # @param [String] val
248
+ def login=(val); @login = (val.is_a?(Array) ? val.first : val).to_s; end
249
+
250
+ # Sets the passcode header value to use when connecting to the server.
251
+ #
252
+ # @param [String] val
253
+ def passcode=(val)
254
+ @passcode = (val.is_a?(Array) ? val.first : val).to_s
255
+ end
256
+
257
+ # Sets the class to use when a receiver needs to be created
258
+ # @see #start
259
+ # @see #stop
260
+ # @see Stomper::Receivers::Threaded
261
+ def receiver_class=(val)
262
+ @receiver_class = ::Stomper::Support.constantize(val.is_a?(Array) ?
263
+ val.first : val)
264
+ end
265
+
266
+ # Establishes a connection to the broker. After the socket connection is
267
+ # established, a CONNECT/STOMP frame will be sent to the broker and a frame
268
+ # will be read from the TCP stream. If the frame is a CONNECTED frame, the
269
+ # connection has been established and you're ready to go, otherwise the
270
+ # socket will be closed and an error will be raised.
271
+ def connect(headers={})
272
+ @socket = @uri.create_socket(@ssl)
273
+ @serializer = ::Stomper::FrameSerializer.new(@socket)
274
+ m_headers = {
275
+ :'accept-version' => @versions.join(','),
276
+ :host => @host,
277
+ :'heart-beat' => @heartbeats.join(','),
278
+ :login => @login,
279
+ :passcode => @passcode
280
+ }
281
+ @connecting = true
282
+ transmit create_frame('CONNECT', headers, m_headers)
283
+ receive.tap do |f|
284
+ if f.command == 'CONNECTED'
285
+ @connected_frame = f
286
+ else
287
+ close
288
+ raise ::Stomper::Errors::ConnectFailedError, 'broker did not send CONNECTED frame'
74
289
  end
75
290
  end
76
-
77
- alias_method :close, :disconnect
78
-
79
- # Transmits the a Stomp Frame to the connected Stomp broker by
80
- # way of an internal FrameWriter. If an exception is raised
81
- # during the transmission, the connection will be forcibly closed
82
- # and the exception will be propegated.
83
- def transmit(frame)
84
- begin
85
- @socket.transmit_frame(frame)
86
- frame
87
- rescue Exception => ioerr
88
- close_socket :lost_connection
89
- raise ioerr
291
+ @connected = true
292
+ trigger_event(:on_connection_established, self) if @connected
293
+ @connected_frame
294
+ end
295
+ alias :open :connect
296
+
297
+ class << self
298
+ # Creates a new connection and immediately connects it to the broker.
299
+ # @see #initialize
300
+ def connect(uri, options={})
301
+ conx = new(uri, options)
302
+ conx.connect
303
+ conx
304
+ end
305
+ alias :open :connect
306
+ end
307
+
308
+ # True if a connection with the broker has been established, false otherwise.
309
+ # @return [true,false]
310
+ def connected?
311
+ @connected && !@socket.closed?
312
+ end
313
+
314
+ # Creates an instance of the class given by {#receiver_class} and starts it.
315
+ # A call to {#connect} will be made if the connection has not been established.
316
+ # The class to instantiate can be overridden on a per connection basis, or
317
+ # for all connections by changing DEFAULT_CONFIG[:receiver_class]
318
+ # @param [{Object => String}] headers optional headers to pass to {#connect}
319
+ # if the connection has not yet been established.
320
+ # @return [self]
321
+ # @see #stop
322
+ # @see #connect
323
+ def start(headers={})
324
+ connect(headers) unless @connected
325
+ @receiver ||= receiver_class.new(self)
326
+ @receiver.start
327
+ self
328
+ end
329
+
330
+ # Stops the instantiated receiver and calls {#disconnect} if a connection
331
+ # has been established.
332
+ # @param [{Object => String}] headers optional headers to pass to {#disconnect}
333
+ # if the connection has been established.
334
+ # @return [self]
335
+ # @raise [Exception] if invoking +stop+ on the receiver raises an exception
336
+ # @see #start
337
+ # @see #disconnect
338
+ # @see Stomper::Receivers::Threaded#stop for an example of when a receiver
339
+ # may raise an exception when stopped.
340
+ def stop(headers={})
341
+ disconnect(headers) unless @disconnecting
342
+ @receiver && @receiver.stop
343
+ self
344
+ end
345
+
346
+ # Returns true if the receiver exists and is running.
347
+ def running?
348
+ @receiver && @receiver.running?
349
+ end
350
+
351
+ # Disconnects from the broker immediately. This is not a polite disconnect,
352
+ # meaning that no DISCONNECT frame is transmitted to the broker, the socket
353
+ # is shutdown and closed immediately. Calls to {#disconnect} invoke this
354
+ # method internally after the DISCONNECT frame has been transmitted. This
355
+ # method always triggers the
356
+ # {Stomper::Extensions::Events#on_connection_closed on_connection_closed} event
357
+ # and if +true+ is passed as a parameter,
358
+ # {Stomper::Extensions::Events#on_connection_terminated on_connection_terminated}
359
+ # will be triggered as well.
360
+ # @see #disconnect
361
+ # @see Stomper::Extensions::Events#on_connection_closed
362
+ # @see Stomper::Extensions::Events#on_connection_terminated
363
+ # @param [true,false] fire_terminated (false) If true, trigger
364
+ # {Stomper::Extensions::Events#on_connection_terminated}
365
+ def close(fire_terminated=false)
366
+ begin
367
+ trigger_event(:on_connection_terminated, self) if fire_terminated
368
+ ensure
369
+ unless @socket.closed?
370
+ @socket.shutdown(2) rescue nil
371
+ @socket.close rescue nil
90
372
  end
373
+ @connected = false
91
374
  end
92
-
93
- # Receives the next Stomp Frame from an internal FrameReader that
94
- # wraps the underlying Stomp broker connection. If an exception is raised
95
- # during the fetch, the connection will be forcibly closed and the exception
96
- # will be propegated.
97
- def receive()
375
+ trigger_event(:on_connection_closed, self)
376
+ end
377
+
378
+ # Transmits a frame to the broker. This is a low-level method used internally
379
+ # by the more user friendly interface.
380
+ # @param [Stomper::Frame] frame
381
+ def transmit(frame)
382
+ trigger_event(:on_connection_died, self) if dead?
383
+ trigger_event(:before_transmitting, frame, self)
384
+ trigger_before_transmitted_frame(frame, self)
385
+ begin
386
+ @serializer.write_frame(frame).tap do
387
+ @last_transmitted_at = Time.now
388
+ trigger_event(:after_transmitting, frame, self)
389
+ trigger_transmitted_frame(frame, self)
390
+ end
391
+ rescue ::IOError, ::SystemCallError
392
+ close(true)
393
+ raise
394
+ end
395
+ end
396
+
397
+ # Receives a frame from the broker.
398
+ # @return [Stomper::Frame]
399
+ def receive
400
+ trigger_event(:on_connection_died, self) if dead?
401
+ if alive? || @connecting
402
+ trigger_event(:before_receiving, nil, self)
98
403
  begin
99
- @socket.receive_frame
100
- rescue Exception => ioerr
101
- close_socket :lost_connection
102
- raise ioerr
404
+ @serializer.read_frame.tap do |f|
405
+ if f.nil?
406
+ close(true) if @connected
407
+ else
408
+ @last_received_at = Time.now
409
+ trigger_event(:after_receiving, f, self)
410
+ trigger_received_frame(f, self)
411
+ end
412
+ end
413
+ rescue ::IOError, ::SystemCallError
414
+ close(true)
415
+ raise
103
416
  end
104
417
  end
105
-
106
- # Immediately closes the connection to the broker, without the
107
- # formality of sending a Disconnect frame.
108
- #
109
- # See also: disconnect
110
- def close_socket(conx_state = :disconnected)
111
- @socket.shutdown(2)
112
- @socket.close if @socket
418
+ end
419
+
420
+ # Receives a frame from the broker if there is data to be read from the
421
+ # underlying socket. If there is no data available for reading from the
422
+ # socket, +nil+ is returned.
423
+ # @note While this method will not block if there is no data ready for reading,
424
+ # if any data is available it will block until a complete frame has been read.
425
+ # @return [Stomper::Frame, nil]
426
+ def receive_nonblock
427
+ trigger_event(:on_connection_died, self) if dead?
428
+ trigger_event(:before_receiving, self)
429
+ if @socket.ready?
430
+ @serializer.read_frame.tap do |f|
431
+ trigger_event(:after_receiving, self, f)
432
+ trigger_received_frame(f, self)
433
+ end
113
434
  end
114
-
115
- include ::Stomper::Client
116
- include ::Stomper::Transactor
117
- include ::Stomper::Subscriber
118
- include ::Stomper::Receiptor
435
+ end
436
+
437
+ # Duration in milliseconds since a frame has been transmitted to the broker.
438
+ # @return [Fixnum]
439
+ def duration_since_transmitted
440
+ @last_transmitted_at && ((Time.now - @last_transmitted_at)*1000).to_i
441
+ end
442
+
443
+ # Duration in milliseconds since a frame has been received from the broker.
444
+ # @return [Fixnum]
445
+ def duration_since_received
446
+ @last_received_at && ((Time.now - @last_received_at)*1000).to_i
447
+ end
448
+
449
+ private
450
+ def extend_for_protocol
451
+ ::Stomper::Extensions::Common.extend_by_protocol_version(self, @version)
452
+ ::Stomper::Extensions::Heartbeat.extend_by_protocol_version(self, @version)
453
+ @serializer.extend_for_protocol @version
454
+ self
119
455
  end
120
456
  end
457
+
458
+ # Alias Stomper::Client to Stomper::Connection
459
+ ::Stomper::Client = ::Stomper::Connection
460
+