stomp 1.1.3 → 1.1.4

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/CHANGELOG CHANGED
@@ -1,3 +1,18 @@
1
+ == 1.1.4 2010-21-01
2
+
3
+ * Added unreceive message method that sends the message back to its queue or to the
4
+ dead letter queue, depending on the :max_redeliveries option, similar to a13m one.
5
+ * Added environment variable option for running 'rake test' on any stomp server, using any port with any user.
6
+ * Added suppress_content_length header option for ActiveMQ knowing it is a text message (see:
7
+ http://juretta.com/log/2009/05/24/activemq-jms-stomp/)
8
+ * Fixed some bugs with Ruby 1.9 (concatenate string + exception)
9
+ * Major changes on message parsing feature
10
+ * Fixed bug with old socket not being closed when using failover
11
+ * Fixed broken poll method on Connection
12
+ * Fixed broken close method on Client
13
+ * Added connection_frame accessor
14
+ * Added disconnect receipt
15
+
1
16
  == 1.1.3 2009-24-11
2
17
 
3
18
  * Failover support
data/Rakefile CHANGED
@@ -44,6 +44,13 @@ Rake::RDocTask.new do |rd|
44
44
  rd.options = spec.rdoc_options
45
45
  end
46
46
 
47
+ desc "Rspec : run all with RCov"
48
+ Spec::Rake::SpecTask.new('spec:rcov') do |t|
49
+ t.spec_files = FileList['spec/**/*.rb']
50
+ t.rcov = true
51
+ t.rcov_opts = ['--exclude', 'gems', '--exclude', 'spec']
52
+ end
53
+
47
54
  desc "RSpec : run all"
48
55
  Spec::Rake::SpecTask.new('spec') do |t|
49
56
  t.spec_files = FileList['spec/**/*.rb']
@@ -13,12 +13,11 @@
13
13
  # See the License for the specific language governing permissions and
14
14
  # limitations under the License.
15
15
 
16
- require 'io/wait'
17
- require 'socket'
18
- require 'thread'
19
- require 'stomp/connection'
20
- require 'stomp/client'
21
- require 'stomp/message'
16
+ require File.join(File.dirname(__FILE__), 'stomp', 'ext', 'hash')
17
+ require File.join(File.dirname(__FILE__), 'stomp', 'connection')
18
+ require File.join(File.dirname(__FILE__), 'stomp', 'client')
19
+ require File.join(File.dirname(__FILE__), 'stomp', 'message')
20
+ require File.join(File.dirname(__FILE__), 'stomp', 'errors')
22
21
 
23
22
  module Stomp
24
23
  end
@@ -1,3 +1,5 @@
1
+ require 'thread'
2
+
1
3
  module Stomp
2
4
 
3
5
  # Typical Stomp client class. Uses a listener thread to receive frames
@@ -7,7 +9,7 @@ module Stomp
7
9
  # in that thread if you have much message volume.
8
10
  class Client
9
11
 
10
- attr_reader :login, :passcode, :host, :port, :reliable, :running, :parameters
12
+ attr_reader :login, :passcode, :host, :port, :reliable, :parameters
11
13
  alias :obj_send :send
12
14
 
13
15
  # A new Client object can be initialized using two forms:
@@ -40,7 +42,7 @@ module Stomp
40
42
  @login = first_host[:login]
41
43
  @passcode = first_host[:passcode]
42
44
  @host = first_host[:host]
43
- @port = first_host[:port] || default_port(first_host[:ssl])
45
+ @port = first_host[:port] || Connection::default_port(first_host[:ssl])
44
46
 
45
47
  @reliable = true
46
48
 
@@ -57,7 +59,7 @@ module Stomp
57
59
  @login = first_host[:login] = $4 || ""
58
60
  @passcode = first_host[:passcode] = $5 || ""
59
61
  @host = first_host[:host] = $6
60
- @port = first_host[:port] = $7.to_i || default_port(first_host[:ssl])
62
+ @port = first_host[:port] = $7.to_i || Connection::default_port(first_host[:ssl])
61
63
 
62
64
  options = $16 || ""
63
65
  parts = options.split(/&|=/)
@@ -168,7 +170,13 @@ module Stomp
168
170
  end
169
171
  @connection.ack message.headers['message-id'], headers
170
172
  end
171
-
173
+
174
+ # Unreceive a message, sending it back to its queue or to the DLQ
175
+ # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
176
+ #
177
+ def unreceive(message)
178
+ @connection.unreceive message
179
+ end
172
180
  # Send message to destination
173
181
  #
174
182
  # If a block is given a receipt will be requested and passed to the
@@ -181,6 +189,14 @@ module Stomp
181
189
  end
182
190
  @connection.send(destination, message, headers)
183
191
  end
192
+
193
+ def connection_frame
194
+ @connection.connection_frame
195
+ end
196
+
197
+ def disconnect_receipt
198
+ @connection.disconnect_receipt
199
+ end
184
200
 
185
201
  # Is this client open?
186
202
  def open?
@@ -193,9 +209,14 @@ module Stomp
193
209
  end
194
210
 
195
211
  # Close out resources in use by this client
196
- def close
197
- @connection.disconnect
198
- @running = false
212
+ def close headers={}
213
+ @listener_thread.exit
214
+ @connection.disconnect headers
215
+ end
216
+
217
+ # Check if the thread was created and isn't dead
218
+ def running
219
+ @listener_thread && !!@listener_thread.status
199
220
  end
200
221
 
201
222
  private
@@ -210,12 +231,6 @@ module Stomp
210
231
  id
211
232
  end
212
233
 
213
- def default_port(ssl)
214
- return 61612 if ssl
215
-
216
- 61613
217
- end
218
-
219
234
  def parse_hosts(url)
220
235
  hosts = []
221
236
 
@@ -257,15 +272,14 @@ module Stomp
257
272
  def start_listeners
258
273
  @listeners = {}
259
274
  @receipt_listeners = {}
260
- @running = true
261
275
  @replay_messages_by_txn = {}
262
276
 
263
277
  @listener_thread = Thread.start do
264
- while @running
265
- message = @connection.receive
278
+ while true
279
+ message = @connection.poll
266
280
  case
267
281
  when message.nil?
268
- break
282
+ sleep 0.1
269
283
  when message.command == 'MESSAGE'
270
284
  if listener = @listeners[message.headers['destination']]
271
285
  listener.call(message)
@@ -1,10 +1,20 @@
1
+ require 'socket'
2
+ require 'monitor'
3
+ require 'timeout'
4
+
1
5
  module Stomp
2
6
 
3
7
  # Low level connection which maps commands and supports
4
8
  # synchronous receives
5
9
  class Connection
6
-
10
+ attr_reader :connection_frame
11
+ attr_reader :disconnect_receipt
7
12
  alias :obj_send :send
13
+
14
+ def self.default_port(ssl)
15
+ ssl ? 61612 : 61613
16
+ end
17
+
8
18
  # A new Connection object accepts the following parameters:
9
19
  #
10
20
  # login (String, default : '')
@@ -45,6 +55,8 @@ module Stomp
45
55
  # stomp://user:pass@host.domain.tld:port
46
56
  #
47
57
  def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
58
+ @received_messages = []
59
+
48
60
  if login.is_a?(Hash)
49
61
  hashed_initialize(login)
50
62
  else
@@ -60,8 +72,7 @@ module Stomp
60
72
  end
61
73
 
62
74
  @transmit_semaphore = Mutex.new
63
- @read_semaphore = Mutex.new
64
- @socket_semaphore = Mutex.new
75
+ @read_semaphore = Monitor.new
65
76
 
66
77
  @subscriptions = {}
67
78
  @failure = nil
@@ -87,37 +98,28 @@ module Stomp
87
98
  end
88
99
 
89
100
  def socket
90
- # Need to look into why the following synchronize does not work.
91
- #@read_semaphore.synchronize do
92
-
93
- s = @socket;
94
-
95
- s = nil unless connected?
101
+ @read_semaphore.synchronize do
102
+ used_socket = @socket
103
+ used_socket = nil if closed?
96
104
 
97
- while s.nil? || !@failure.nil?
105
+ while used_socket.nil? || !@failure.nil?
98
106
  @failure = nil
99
107
  begin
100
- s = open_socket
101
- @closed = false
102
-
103
- headers = @connect_headers.clone
104
- headers[:login] = @login
105
- headers[:passcode] = @passcode
106
- _transmit(s, "CONNECT", headers)
107
- @connect = _receive(s)
108
- # replay any subscriptions.
109
- @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
108
+ used_socket = open_socket
109
+ # Open complete
110
+
111
+ connect(used_socket)
110
112
 
111
113
  @connection_attempts = 0
112
114
  rescue
113
- @failure = $!;
114
- s=nil;
115
+ @failure = $!
116
+ used_socket = nil
115
117
  raise unless @reliable
116
- $stderr.print "connect to #{@host} failed: " + $! +" will retry(##{@connection_attempts}) in #{@reconnect_delay}\n";
118
+ $stderr.print "connect to #{@host} failed: #{$!} will retry(##{@connection_attempts}) in #{@reconnect_delay}\n"
117
119
 
118
- raise "Max number of reconnection attempts reached" if max_reconnect_attempts?
120
+ raise Stomp::Error::MaxReconnectAttempts if max_reconnect_attempts?
119
121
 
120
- sleep(@reconnect_delay);
122
+ sleep(@reconnect_delay)
121
123
 
122
124
  @connection_attempts += 1
123
125
 
@@ -127,86 +129,28 @@ module Stomp
127
129
  end
128
130
  end
129
131
  end
130
- @socket = s
131
- return s;
132
- #end
133
- end
134
-
135
- def connected?
136
- begin
137
- test_socket = TCPSocket.open @host, @port
138
- test_socket.close
139
- open?
140
- rescue
141
- false
132
+ @socket = used_socket
142
133
  end
143
134
  end
144
-
145
- def close_socket
146
- begin
147
- @socket.close
148
- rescue
149
- #Ignoring if already closed
150
- end
151
-
152
- @closed = true
153
- end
154
-
155
- def open_socket
156
- return TCPSocket.open @host, @port unless @ssl
157
-
158
- ssl_socket
159
- end
160
-
161
- def ssl_socket
162
- require 'openssl' unless defined?(OpenSSL)
163
-
164
- ctx = OpenSSL::SSL::SSLContext.new
165
-
166
- # For client certificate authentication:
167
- # key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
168
- # ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
169
- # ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
170
-
171
- # For server certificate authentication:
172
- # truststores = OpenSSL::X509::Store.new
173
- # truststores.add_file("#{key_path}/client.ts")
174
- # ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
175
- # ctx.cert_store = truststores
176
-
177
- ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
178
-
179
- tcp_socket = TCPSocket.new @host, @port
180
- ssl = OpenSSL::SSL::SSLSocket.new(tcp_socket, ctx)
181
- ssl.connect
182
- ssl
183
- end
184
135
 
185
136
  def refine_params(params)
186
- params = uncamelized_sym_keys(params)
137
+ params = params.uncamelize_and_symbolize_keys
187
138
 
188
- {
139
+ default_params = {
140
+ :connect_headers => {},
141
+ # Failover parameters
189
142
  :initial_reconnect_delay => 0.01,
190
143
  :max_reconnect_delay => 30.0,
191
144
  :use_exponential_back_off => true,
192
145
  :back_off_multiplier => 2,
193
146
  :max_reconnect_attempts => 0,
194
147
  :randomize => false,
195
- :connect_headers => {},
196
148
  :backup => false,
197
149
  :timeout => -1
198
- }.merge(params)
199
-
200
- end
201
-
202
- def uncamelized_sym_keys(params)
203
- uncamelized = {}
204
- params.each_pair do |key, value|
205
- key = key.to_s.split(/(?=[A-Z])/).join('_').downcase.to_sym
206
- uncamelized[key] = value
207
- end
150
+ }
208
151
 
209
- uncamelized
152
+ default_params.merge(params)
153
+
210
154
  end
211
155
 
212
156
  def change_host
@@ -218,18 +162,12 @@ module Stomp
218
162
 
219
163
  @ssl = current_host[:ssl]
220
164
  @host = current_host[:host]
221
- @port = current_host[:port] || default_port(@ssl)
165
+ @port = current_host[:port] || Connection::default_port(@ssl)
222
166
  @login = current_host[:login] || ""
223
167
  @passcode = current_host[:passcode] || ""
224
168
 
225
169
  end
226
170
 
227
- def default_port(ssl)
228
- return 61612 if ssl
229
-
230
- 61613
231
- end
232
-
233
171
  def max_reconnect_attempts?
234
172
  !(@parameters.nil? || @parameters[:max_reconnect_attempts].nil?) && @parameters[:max_reconnect_attempts] != 0 && @connection_attempts > @parameters[:max_reconnect_attempts]
235
173
  end
@@ -303,16 +241,56 @@ module Stomp
303
241
 
304
242
  # Send message to destination
305
243
  #
244
+ # To disable content length header ( :suppress_content_length => true )
306
245
  # Accepts a transaction header ( :transaction => 'some_transaction_id' )
307
246
  def send(destination, message, headers = {})
308
247
  headers[:destination] = destination
309
248
  transmit("SEND", headers, message)
310
249
  end
250
+
251
+ # Send a message back to the source or to the dead letter queue
252
+ #
253
+ # Accepts a dead letter queue option ( :dead_letter_queue => "/queue/DLQ" )
254
+ # Accepts a limit number of redeliveries option ( :max_redeliveries => 6 )
255
+ def unreceive(message, options = {})
256
+ options = { :dead_letter_queue => "/queue/DLQ", :max_redeliveries => 6 }.merge options
257
+ # Lets make sure all keys are symbols
258
+ message.headers = message.headers.symbolize_keys
259
+
260
+ retry_count = message.headers[:retry_count].to_i || 0
261
+ message.headers[:retry_count] = retry_count + 1
262
+ transaction_id = "transaction-#{message.headers[:'message-id']}-#{retry_count}"
263
+
264
+ begin
265
+ self.begin transaction_id
266
+
267
+ if client_ack?(message)
268
+ self.ack(message.headers[:'message-id'], :transaction => transaction_id)
269
+ end
270
+
271
+ if retry_count <= options[:max_redeliveries]
272
+ self.send(message.headers[:destination], message.body, message.headers.merge(:transaction => transaction_id))
273
+ else
274
+ # Poison ack, sending the message to the DLQ
275
+ self.send(options[:dead_letter_queue], message.body, message.headers.merge(:transaction => transaction_id, :persistent => true))
276
+ end
277
+ self.commit transaction_id
278
+ rescue Exception => exception
279
+ self.abort transaction_id
280
+ raise exception
281
+ end
282
+ end
283
+
284
+ def client_ack?(message)
285
+ headers = @subscriptions[message.headers[:destination]]
286
+ !headers.nil? && headers[:ack] == "client"
287
+ end
311
288
 
312
289
  # Close this connection
313
290
  def disconnect(headers = {})
314
291
  transmit("DISCONNECT", headers)
315
-
292
+ headers = headers.symbolize_keys
293
+ @disconnect_receipt = receive if headers[:receipt]
316
294
  close_socket
317
295
  end
318
296
 
@@ -321,7 +299,7 @@ module Stomp
321
299
  def poll
322
300
  @read_semaphore.synchronize do
323
301
  return nil if @socket.nil? || !@socket.ready?
324
- return receive
302
+ receive
325
303
  end
326
304
  end
327
305
 
@@ -330,93 +308,172 @@ module Stomp
330
308
  # The recive my fail so we may need to retry.
331
309
  while TRUE
332
310
  begin
333
- s = socket
334
- return _receive(s)
311
+ used_socket = socket
312
+ return _receive(used_socket)
335
313
  rescue
336
- @failure = $!;
314
+ @failure = $!
337
315
  raise unless @reliable
338
- $stderr.print "receive failed: " + $!;
316
+ $stderr.print "receive failed: #{$!}"
339
317
  end
340
318
  end
341
319
  end
342
320
 
343
321
  def receive
344
- super_result = __old_receive()
322
+ super_result = __old_receive
345
323
  if super_result.nil? && @reliable
346
324
  $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
347
325
  @socket = nil
348
- super_result = __old_receive()
326
+ super_result = __old_receive
349
327
  end
350
328
  return super_result
351
329
  end
352
330
 
353
331
  private
354
332
 
355
- def _receive( s )
356
- line = ' '
333
+ def _receive( read_socket )
357
334
  @read_semaphore.synchronize do
358
- line = s.gets while line =~ /^\s*$/
335
+ line = read_socket.gets
359
336
  return nil if line.nil?
360
337
 
361
- message = Message.new do |m|
362
- m.command = line.chomp
363
- m.headers = {}
364
- until (line = s.gets.chomp) == ''
365
- k = (line.strip[0, line.strip.index(':')]).strip
366
- v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
367
- m.headers[k] = v
368
- end
338
+ # If the reading hangs for more than 5 seconds, abort the parsing process
339
+ Timeout::timeout(5, Stomp::Error::PacketParsingTimeout) do
340
+ # Reads the beginning of the message until it runs into a empty line
341
+ message_header = ''
342
+ begin
343
+ message_header += line
344
+ line = read_socket.gets
345
+ end until line =~ /^\s?\n$/
346
+
347
+ # Checks if it includes content_length header
348
+ content_length = message_header.match /content-length\s?:\s?(\d+)\s?\n/
349
+ message_body = ''
369
350
 
370
- if (m.headers['content-length'])
371
- m.body = s.read m.headers['content-length'].to_i
372
- c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
373
- raise "Invalid content length received" unless c == 0
351
+ # If it does, reads the specified amount of bytes
352
+ char = ''
353
+ if content_length
354
+ message_body = read_socket.read content_length[1].to_i
355
+ raise Stomp::Error::InvalidMessageLength unless parse_char(read_socket.getc) == "\0"
356
+ # Else reads, the rest of the message until the first \0
374
357
  else
375
- m.body = ''
376
- if RUBY_VERSION > '1.9'
377
- until (c = s.getc.ord) == 0
378
- m.body << c.chr
379
- end
380
- else
381
- until (c = s.getc) == 0
382
- m.body << c.chr
383
- end
384
- end
358
+ message_body += char while read_socket.ready? && (char = parse_char(read_socket.getc)) != "\0"
385
359
  end
386
- #c = s.getc
387
- #raise "Invalid frame termination received" unless c == 10
388
- end # message
389
- return message
390
360
 
361
+ # If the buffer isn't empty, reads the next char and returns it to the buffer
362
+ # unless it's a \n
363
+ if read_socket.ready?
364
+ last_char = read_socket.getc
365
+ read_socket.ungetc(last_char) if parse_char(last_char) != "\n"
366
+ end
367
+
368
+ # Adds the excluded \n and \0 and tries to create a new message with it
369
+ Message.new(message_header + "\n" + message_body + "\0")
370
+ end
391
371
  end
392
372
  end
393
373
 
374
+ def parse_char(char)
375
+ RUBY_VERSION > '1.9' ? char : char.chr
376
+ end
377
+
394
378
  def transmit(command, headers = {}, body = '')
395
379
  # The transmit may fail so we may need to retry.
396
380
  while TRUE
397
381
  begin
398
- s = socket
399
- _transmit(s, command, headers, body)
382
+ used_socket = socket
383
+ _transmit(used_socket, command, headers, body)
400
384
  return
401
385
  rescue
402
- @failure = $!;
386
+ @failure = $!
403
387
  raise unless @reliable
404
- $stderr.print "transmit to #{@host} failed: " + $!+"\n";
388
+ $stderr.print "transmit to #{@host} failed: #{$!}\n"
405
389
  end
406
390
  end
407
391
  end
408
392
 
409
- def _transmit(s, command, headers = {}, body = '')
393
+ def _transmit(used_socket, command, headers = {}, body = '')
410
394
  @transmit_semaphore.synchronize do
411
- s.puts command
412
- headers.each {|k,v| s.puts "#{k}:#{v}" }
413
- s.puts "content-length: #{body.length}"
414
- s.puts "content-type: text/plain; charset=UTF-8"
415
- s.puts
416
- s.write body
417
- s.write "\0"
395
+ # ActiveMQ interprets every message as a BinaryMessage
396
+ # if content_length header is included.
397
+ # Using :suppress_content_length => true will suppress this behaviour
398
+ # and ActiveMQ will interpret the message as a TextMessage.
399
+ # For more information refer to http://juretta.com/log/2009/05/24/activemq-jms-stomp/
400
+ suppress_content_length = headers.delete :suppress_content_length
401
+ headers['content-length'] = "#{body.length}" unless suppress_content_length
402
+
403
+ used_socket.puts command
404
+ headers.each {|k,v| used_socket.puts "#{k}:#{v}" }
405
+ used_socket.puts "content-type: text/plain; charset=UTF-8"
406
+ used_socket.puts
407
+ used_socket.write body
408
+ used_socket.write "\0"
418
409
  end
419
410
  end
411
+
412
+ def open_tcp_socket
413
+ tcp_socket = TCPSocket.open @host, @port
414
+ def tcp_socket.ready?
415
+ r,w,e = IO.select([self],nil,nil,0)
416
+ ! r.nil?
417
+ end
418
+
419
+ tcp_socket
420
+ end
421
+
422
+ def open_ssl_socket
423
+ require 'openssl' unless defined?(OpenSSL)
424
+ ctx = OpenSSL::SSL::SSLContext.new
425
+
426
+ # For client certificate authentication:
427
+ # key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
428
+ # ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
429
+ # ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
430
+
431
+ # For server certificate authentication:
432
+ # truststores = OpenSSL::X509::Store.new
433
+ # truststores.add_file("#{key_path}/client.ts")
434
+ # ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
435
+ # ctx.cert_store = truststores
436
+
437
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
438
+
439
+ ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
440
+ def ssl.ready?
441
+ ! @rbuffer.empty? || @io.ready?
442
+ end
443
+ ssl.connect
444
+ ssl
445
+ end
446
+
447
+ def close_socket
448
+ begin
449
+ @socket.close
450
+ rescue
451
+ #Ignoring if already closed
452
+ end
453
+
454
+ @closed = true
455
+ end
456
+
457
+ def open_socket
458
+ used_socket = @ssl ? open_ssl_socket : open_tcp_socket
459
+ # try to close the old connection if any
460
+ close_socket
461
+
462
+ @closed = false
463
+
464
+ used_socket
465
+ end
466
+
467
+ def connect(used_socket)
468
+ headers = @connect_headers.clone
469
+ headers[:login] = @login
470
+ headers[:passcode] = @passcode
471
+ _transmit(used_socket, "CONNECT", headers)
472
+ @connection_frame = _receive(used_socket)
473
+ @disconnect_receipt = nil
474
+ # replay any subscriptions.
475
+ @subscriptions.each { |k,v| _transmit(used_socket, "SUBSCRIBE", v) }
476
+ end
420
477
 
421
478
  end
422
479
 
@@ -0,0 +1,27 @@
1
+ module Stomp
2
+ module Error
3
+ class InvalidFormat < RuntimeError
4
+ def message
5
+ "Invalid message - invalid format"
6
+ end
7
+ end
8
+
9
+ class InvalidMessageLength < RuntimeError
10
+ def message
11
+ "Invalid content length received"
12
+ end
13
+ end
14
+
15
+ class PacketParsingTimeout < RuntimeError
16
+ def message
17
+ "Packet parsing timeout"
18
+ end
19
+ end
20
+
21
+ class MaxReconnectAttempts < RuntimeError
22
+ def message
23
+ "Maximum number of reconnection attempts reached"
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ class ::Hash
2
+ def uncamelize_and_symbolize_keys
3
+ self.uncamelize_and_stringify_keys.symbolize_keys
4
+ end
5
+
6
+ def uncamelize_and_stringify_keys
7
+ uncamelized = {}
8
+ self.each_pair do |key, value|
9
+ new_key = key.to_s.split(/(?=[A-Z])/).join('_').downcase
10
+ uncamelized[new_key] = value
11
+ end
12
+
13
+ uncamelized
14
+ end
15
+
16
+ def symbolize_keys
17
+ symbolized = {}
18
+ self.each_pair do |key, value|
19
+ symbolized[key.to_sym] = value
20
+ end
21
+
22
+ symbolized
23
+ end unless self.respond_to? :symbolize_keys
24
+ end
@@ -2,16 +2,48 @@ module Stomp
2
2
 
3
3
  # Container class for frames, misnamed technically
4
4
  class Message
5
- attr_accessor :headers, :body, :command
5
+ attr_accessor :command, :headers, :body, :original
6
6
 
7
- def initialize
8
- yield(self) if block_given?
7
+ def initialize(message)
8
+ # Set default empty values
9
+ self.command = ''
10
+ self.headers = {}
11
+ self.body = ''
12
+ self.original = message
13
+ return self if is_blank?(message)
14
+
15
+ # Parse the format of the received stomp message
16
+ parse = message.match /^(CONNECTED|MESSAGE|RECEIPT|ERROR)\n(.*?)\n\n(.*)\0\n?$/m
17
+ raise Stomp::Error::InvalidFormat if parse.nil?
18
+
19
+ # Set the message values
20
+ self.command = parse[1]
21
+ self.headers = {}
22
+ parse[2].split("\n").map do |value|
23
+ parsed_value = value.match /^([\w|-]*):(.*)$/
24
+ self.headers[parsed_value[1].strip] = parsed_value[2].strip if parsed_value
25
+ end
26
+
27
+ body_length = -1
28
+ if self.headers['content-length']
29
+ body_length = self.headers['content-length'].to_i
30
+ raise Stomp::Error::InvalidMessageLength if parse[3].length != body_length
31
+ end
32
+ self.body = parse[3][0..body_length]
9
33
  end
10
34
 
11
35
  def to_s
12
36
  "<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
13
37
  end
38
+
39
+ def empty?
40
+ is_blank?(command) && is_blank?(headers) && is_blank?(body)
41
+ end
42
+
43
+ private
44
+ def is_blank?(value)
45
+ value.nil? || (value.respond_to?(:empty?) && value.empty?)
46
+ end
14
47
  end
15
48
 
16
49
  end
17
-
@@ -1,27 +1,28 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
2
 
3
3
  class TestClient < Test::Unit::TestCase
4
-
4
+ include TestBase
5
+
5
6
  def setup
6
- @client = Stomp::Client.new("test", "user", "localhost", 61613)
7
+ @client = Stomp::Client.new(user, passcode, host, port)
7
8
  end
8
9
 
9
10
  def teardown
10
- @client.close
11
+ @client.close if @client # allow tests to close
11
12
  end
12
13
 
13
- def message_text
14
- "test_client#" + name()
15
- end
14
+ def test_ack_api_works
15
+ @client.send destination, message_text, {:suppress_content_length => true}
16
16
 
17
- def destination
18
- "/queue/test/ruby/client/" + name()
19
- end
17
+ received = nil
18
+ @client.subscribe(destination, {:ack => 'client'}) {|msg| received = msg}
19
+ sleep 0.01 until received
20
+ assert_equal message_text, received.body
20
21
 
21
- def test_subscribe_requires_block
22
- assert_raise(RuntimeError) do
23
- @client.subscribe destination
24
- end
22
+ receipt = nil
23
+ @client.acknowledge(received) {|r| receipt = r}
24
+ sleep 0.01 until receipt
25
+ assert_not_nil receipt.headers['receipt-id']
25
26
  end
26
27
 
27
28
  def test_asynch_subscribe
@@ -33,20 +34,6 @@ class TestClient < Test::Unit::TestCase
33
34
  assert_equal message_text, received.body
34
35
  end
35
36
 
36
- def test_ack_api_works
37
- @client.send destination, message_text
38
-
39
- received = nil
40
- @client.subscribe(destination, :ack => 'client') {|msg| received = msg}
41
- sleep 0.01 until received
42
- assert_equal message_text, received.body
43
-
44
- receipt = nil
45
- @client.acknowledge(received) {|r| receipt = r}
46
- sleep 0.01 until receipt
47
- assert_not_nil receipt.headers['receipt-id']
48
- end
49
-
50
37
  # BROKEN
51
38
  def test_noack
52
39
  @client.send destination, message_text
@@ -59,7 +46,7 @@ class TestClient < Test::Unit::TestCase
59
46
 
60
47
  # was never acked so should be resent to next client
61
48
 
62
- @client = Stomp::Client.new("test", "user", "localhost", 61613)
49
+ @client = Stomp::Client.new(user, passcode, host, port)
63
50
  received = nil
64
51
  @client.subscribe(destination) {|msg| received = msg}
65
52
  sleep 0.01 until received
@@ -78,6 +65,16 @@ class TestClient < Test::Unit::TestCase
78
65
  assert_equal message_text, message.body
79
66
  end
80
67
 
68
+ def test_disconnect_receipt
69
+ @client.close :receipt => "xyz789"
70
+ assert_nothing_raised {
71
+ assert_not_nil(@client.disconnect_receipt, "should have a receipt")
72
+ assert_equal(@client.disconnect_receipt.headers['receipt-id'],
73
+ "xyz789", "receipt sent and received should match")
74
+ }
75
+ @client = nil
76
+ end
77
+
81
78
  def test_send_then_sub
82
79
  @client.send destination, message_text
83
80
  message = nil
@@ -87,6 +84,12 @@ class TestClient < Test::Unit::TestCase
87
84
  assert_equal message_text, message.body
88
85
  end
89
86
 
87
+ def test_subscribe_requires_block
88
+ assert_raise(RuntimeError) do
89
+ @client.subscribe destination
90
+ end
91
+ end
92
+
90
93
  def test_transactional_send
91
94
  @client.begin 'tx1'
92
95
  @client.send destination, message_text, :transaction => 'tx1'
@@ -142,18 +145,6 @@ class TestClient < Test::Unit::TestCase
142
145
  @client.commit 'tx2'
143
146
  end
144
147
 
145
- def test_unsubscribe
146
- message = nil
147
- client = Stomp::Client.new("test", "user", "localhost", 61613, true)
148
- client.subscribe(destination, :ack => 'client') { |m| message = m }
149
- @client.send destination, message_text
150
- Timeout::timeout(4) do
151
- sleep 0.01 until message
152
- end
153
- client.unsubscribe destination # was throwing exception on unsub at one point
154
-
155
- end
156
-
157
148
  def test_transaction_with_client_side_redelivery
158
149
  @client.send destination, message_text
159
150
 
@@ -177,6 +168,29 @@ class TestClient < Test::Unit::TestCase
177
168
  @client.acknowledge message, :transaction => 'tx2'
178
169
  @client.commit 'tx2'
179
170
  end
171
+
172
+ def test_connection_frame
173
+ assert_not_nil @client.connection_frame
174
+ end
180
175
 
176
+ def test_unsubscribe
177
+ message = nil
178
+ client = Stomp::Client.new(user, passcode, host, port, true)
179
+ client.subscribe(destination, :ack => 'client') { |m| message = m }
180
+ @client.send destination, message_text
181
+ Timeout::timeout(4) do
182
+ sleep 0.01 until message
183
+ end
184
+ client.unsubscribe destination # was throwing exception on unsub at one point
185
+
186
+ end
181
187
 
188
+ private
189
+ def message_text
190
+ "test_client#" + name
191
+ end
192
+
193
+ def destination
194
+ "/queue/test/ruby/client/" + name
195
+ end
182
196
  end
@@ -1,38 +1,14 @@
1
1
  require File.join(File.dirname(__FILE__), 'test_helper.rb')
2
2
 
3
3
  class TestStomp < Test::Unit::TestCase
4
-
4
+ include TestBase
5
+
5
6
  def setup
6
- @conn = Stomp::Connection.open("test", "user", "localhost", 61613)
7
+ @conn = Stomp::Connection.open(user, passcode, host, port)
7
8
  end
8
9
 
9
10
  def teardown
10
- @conn.disconnect
11
- end
12
-
13
- def make_destination
14
- "/queue/test/ruby/stomp/" + name()
15
- end
16
-
17
- def _test_transaction
18
- @conn.subscribe make_destination
19
-
20
- # Drain the destination.
21
- sleep 0.01 while
22
- sleep 0.01 while @conn.poll!=nil
23
-
24
- @conn.begin "tx1"
25
- @conn.send make_destination, "txn message", 'transaction' => "tx1"
26
-
27
- @conn.send make_destination, "first message"
28
-
29
- sleep 0.01
30
- msg = @conn.receive
31
- assert_equal "first message", msg.body
32
-
33
- @conn.commit "tx1"
34
- msg = @conn.receive
35
- assert_equal "txn message", msg.body
11
+ @conn.disconnect if @conn # allow tests to disconnect
36
12
  end
37
13
 
38
14
  def test_connection_exists
@@ -52,6 +28,16 @@ class TestStomp < Test::Unit::TestCase
52
28
  assert_equal "abc", msg.headers['receipt-id']
53
29
  end
54
30
 
31
+ def test_disconnect_receipt
32
+ @conn.disconnect :receipt => "abc123"
33
+ assert_nothing_raised {
34
+ assert_not_nil(@conn.disconnect_receipt, "should have a receipt")
35
+ assert_equal(@conn.disconnect_receipt.headers['receipt-id'],
36
+ "abc123", "receipt sent and received should match")
37
+ }
38
+ @conn = nil
39
+ end
40
+
55
41
  def test_client_ack_with_symbol
56
42
  @conn.subscribe make_destination, :ack => :client
57
43
  @conn.send make_destination, "test_stomp#test_client_ack_with_symbol"
@@ -91,5 +77,57 @@ class TestStomp < Test::Unit::TestCase
91
77
  msg = @conn.receive
92
78
  assert_match /^<Stomp::Message headers=/ , msg.to_s
93
79
  end
80
+
81
+ def test_connection_frame
82
+ assert_not_nil @conn.connection_frame
83
+ end
84
+
85
+ def test_messages_with_multipleLine_ends
86
+ @conn.subscribe make_destination
87
+ @conn.send make_destination, "a\n\n"
88
+ @conn.send make_destination, "b\n\na\n\n"
89
+
90
+ msg_a = @conn.receive
91
+ msg_b = @conn.receive
92
+
93
+ assert_equal "a\n\n", msg_a.body
94
+ assert_equal "b\n\na\n\n", msg_b.body
95
+ end
96
+
97
+ def test_send_two_messages
98
+ @conn.subscribe make_destination
99
+ @conn.send make_destination, "a\0"
100
+ @conn.send make_destination, "b\0"
101
+ msg_a = @conn.receive
102
+ msg_b = @conn.receive
103
+
104
+ assert_equal "a\0", msg_a.body
105
+ assert_equal "b\0", msg_b.body
106
+ end
107
+
108
+ private
109
+ def make_destination
110
+ "/queue/test/ruby/stomp/" + name
111
+ end
112
+
113
+ def _test_transaction
114
+ @conn.subscribe make_destination
115
+
116
+ # Drain the destination.
117
+ sleep 0.01 while
118
+ sleep 0.01 while @conn.poll!=nil
119
+
120
+ @conn.begin "tx1"
121
+ @conn.send make_destination, "txn message", 'transaction' => "tx1"
122
+
123
+ @conn.send make_destination, "first message"
124
+
125
+ sleep 0.01
126
+ msg = @conn.receive
127
+ assert_equal "first message", msg.body
94
128
 
129
+ @conn.commit "tx1"
130
+ msg = @conn.receive
131
+ assert_equal "txn message", msg.body
132
+ end
95
133
  end
@@ -1,5 +1,24 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
+
1
3
  require 'test/unit'
2
4
  require 'timeout'
3
5
  require 'stomp'
4
- $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
6
+
7
+ # Helper routines
8
+ module TestBase
9
+ def user
10
+ ENV['STOMP_USER'] || "test"
11
+ end
12
+ def passcode
13
+ ENV['STOMP_PASSCODE'] || "user"
14
+ end
15
+ # Get host
16
+ def host
17
+ ENV['STOMP_HOST'] || "localhost"
18
+ end
19
+ # Get port
20
+ def port
21
+ (ENV['STOMP_PORT'] || 61613).to_i
22
+ end
23
+ end
5
24
 
metadata CHANGED
@@ -1,16 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stomp
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 1.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brian McCallister
8
8
  - Marius Mathiesen
9
+ - Thiago Morello
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
13
 
13
- date: 2009-12-03 00:00:00 -02:00
14
+ date: 2010-02-09 00:00:00 -02:00
14
15
  default_executable:
15
16
  dependencies: []
16
17
 
@@ -18,6 +19,7 @@ description: Ruby client for the Stomp messaging protocol
18
19
  email:
19
20
  - brianm@apache.org
20
21
  - marius@stones.com
22
+ - morellon@gmail.com
21
23
  executables:
22
24
  - catstomp
23
25
  - stompcat
@@ -36,6 +38,8 @@ files:
36
38
  - lib/stomp/client.rb
37
39
  - lib/stomp/connection.rb
38
40
  - lib/stomp/message.rb
41
+ - lib/stomp/errors.rb
42
+ - lib/stomp/ext/hash.rb
39
43
  - test/test_client.rb
40
44
  - test/test_connection.rb
41
45
  - test/test_helper.rb