stomp 1.1.3 → 1.1.4

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