Merge lp:~elmo/ubuntu/oneiric/libstomp-ruby/bug-707317 into lp:ubuntu/oneiric/libstomp-ruby

Proposed by James Troup
Status: Merged
Merged at revision: 5
Proposed branch: lp:~elmo/ubuntu/oneiric/libstomp-ruby/bug-707317
Merge into: lp:ubuntu/oneiric/libstomp-ruby
Diff against target: 5091 lines (+3522/-1302)
39 files modified
.gitignore (+5/-0)
.pc/.version (+0/-1)
.pc/applied-patches (+0/-2)
.pc/case_statement_compatible_1.9.2.patch/lib/stomp.rb (+0/-422)
.pc/getc_returns_a_string_1.9.patch/lib/stomp.rb (+0/-416)
CHANGELOG.rdoc (+84/-0)
LICENSE (+202/-0)
README.rdoc (+107/-0)
Rakefile (+77/-0)
bin/catstomp (+55/-0)
bin/stompcat (+56/-0)
debian/changelog (+23/-0)
debian/control (+4/-4)
debian/patches/case_statement_compatible_1.9.2.patch (+0/-23)
debian/patches/getc_returns_a_string_1.9.patch (+0/-27)
debian/patches/series (+0/-2)
debian/rules (+12/-0)
debian/source/format (+0/-1)
examples/consumer.rb (+19/-0)
examples/logexamp.rb (+50/-0)
examples/publisher.rb (+17/-0)
examples/slogger.rb (+100/-0)
lib/stomp.rb (+7/-404)
lib/stomp/client.rb (+340/-0)
lib/stomp/connection.rb (+559/-0)
lib/stomp/errors.rb (+33/-0)
lib/stomp/ext/hash.rb (+24/-0)
lib/stomp/message.rb (+68/-0)
lib/stomp/version.rb (+8/-0)
spec/client_shared_examples.rb (+69/-0)
spec/client_spec.rb (+312/-0)
spec/connection_spec.rb (+365/-0)
spec/message_spec.rb (+56/-0)
spec/spec_helper.rb (+6/-0)
stomp.gemspec (+66/-0)
test/test_client.rb (+364/-0)
test/test_connection.rb (+278/-0)
test/test_helper.rb (+38/-0)
test/test_message.rb (+118/-0)
To merge this branch: bzr merge lp:~elmo/ubuntu/oneiric/libstomp-ruby/bug-707317
Reviewer Review Type Date Requested Status
Dave Walker (community) Approve
James Troup (community) Needs Resubmitting
Stefano Rivera Needs Fixing
Ubuntu branches Pending
Review via email: mp+67463@code.launchpad.net

Description of the change

New upstream version, required for mcollective and also fixes LP
#707317 and Debian #598564.

To post a comment you must log in.
Revision history for this message
Stefano Rivera (stefanor) wrote :

I know little about Debian ruby packaging. But I can spot some issues:
* The version number really should be 1.1.9-0ubuntu1.
* We shouldn't change source format just because we don't have any packages.
* E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/catstomp
  E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/stompcat

review: Needs Fixing
Revision history for this message
James Troup (elmo) wrote :

Stefano Rivera <email address hidden> writes:

> * The version number really should be 1.1.9-0ubuntu1.

Even if I NMU it into Debian? ;-)

> * We shouldn't change source format just because we don't have any
> packages.

I assume you mean patches? If so, sorry, but why not? There are no
upstream changes anymore so making it source format 1 makes it easier to
backport to older distributions and AFAICS doesn't have any negative
side effects. What am I missing?

> * E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/catstomp
> E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/stompcat

Well, it has an indirect dependency via libstomp-ruby1.8 |
libstomp-ruby1.9.1. I guess I can make it an explicit dependency, if
you want.

--
James

Revision history for this message
Stefano Rivera (stefanor) wrote :

> Even if I NMU it into Debian? ;-)

Then the changelog would say "Non-maintainer upload." :)

> I assume you mean patches? If so, sorry, but why not?

Yeah, that's what I meant. Just because we don't like to deviate unnecessarily from Debian. I wouldn't change source format in an NMU either (but then I guess I'm pretty conservative when touching other people's packages in Debian).

One of the advantages of 3.0 (quilt) is that you don't need to add a patch system to add a patch neatly, it's already there.

IIRC 3.0 (quilt) is supported all the way back to etch, so backporting shouldn't be a big deal either.

The indirect dependency is fine with me (esp. as I prefer not deviating unnecessarily in Ubuntu). The python helpers provide dependencies on python I'd have expected the same for ruby, but as I said I'm completely unfamiliar with it.

Revision history for this message
Dave Walker (davewalker) wrote :

Hi James,

I agree that dropping the patch system is a bit of a pain if we need to introduce patches; particularly for SRU's where *adding* a patch system is frowned upon (traditionally favouring directly applying the patches!). I also thought there was a drive to try and get Debian packages to 3.0 ($something).

Anyway, *I* would favour trying to stay with 3.0 (quilt).. However, I don't care that strongly. If you NMU this to Debian then i care even less; as i'll sync whatever you put there making this branch obsolete.

This is currently blocking mcollective installation on Oneiric, so please update on what you want to do; and lets get it done.

Thanks.

review: Needs Information
Revision history for this message
Chris Halse Rogers (raof) wrote :

I'm marking this as In Progress to drop it off the sponsorship queue - it sounds like this is going to be resolved by NMUing this package into Debian and then syncing to Ubuntu. If that's the case, this merge request can be deleted.

If that's not the case, then the comments above apply :)

Revision history for this message
James Troup (elmo) wrote :

> I know little about Debian ruby packaging. But I can spot some issues:
> * The version number really should be 1.1.9-0ubuntu1.
> * We shouldn't change source format just because we don't have any packages.
> * E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/catstomp
> E: libstomp-ruby: ruby-script-but-no-ruby-dep usr/bin/stompcat

I've pushed r10 which fixes all of the above issues.

review: Needs Resubmitting
Revision history for this message
Dave Walker (davewalker) wrote :

Thanks for addressing this, uploading now!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file '.gitignore'
--- .gitignore 1970-01-01 00:00:00 +0000
+++ .gitignore 2011-07-31 16:48:34 +0000
@@ -0,0 +1,5 @@
1pkg/*
2doc/*
3coverage/*
4*.gem
5
06
=== added directory '.pc'
=== removed directory '.pc'
=== added file '.pc/.version'
--- .pc/.version 1970-01-01 00:00:00 +0000
+++ .pc/.version 2011-07-31 16:48:34 +0000
@@ -0,0 +1,1 @@
12
02
=== removed file '.pc/.version'
--- .pc/.version 2010-09-28 00:25:39 +0000
+++ .pc/.version 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
12
20
=== removed file '.pc/applied-patches'
--- .pc/applied-patches 2010-09-28 00:25:39 +0000
+++ .pc/applied-patches 1970-01-01 00:00:00 +0000
@@ -1,2 +0,0 @@
1getc_returns_a_string_1.9.patch
2case_statement_compatible_1.9.2.patch
30
=== removed directory '.pc/case_statement_compatible_1.9.2.patch'
=== removed directory '.pc/case_statement_compatible_1.9.2.patch/lib'
=== removed file '.pc/case_statement_compatible_1.9.2.patch/lib/stomp.rb'
--- .pc/case_statement_compatible_1.9.2.patch/lib/stomp.rb 2010-09-28 00:25:39 +0000
+++ .pc/case_statement_compatible_1.9.2.patch/lib/stomp.rb 1970-01-01 00:00:00 +0000
@@ -1,422 +0,0 @@
1# Copyright 2005-2006 Brian McCallister
2# Copyright 2006 LogicBlaze Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16require 'io/wait'
17require 'socket'
18require 'thread'
19
20module Stomp
21
22 # Low level connection which maps commands and supports
23 # synchronous receives
24 class Connection
25
26 def Connection.open(login = "", passcode = "", host='localhost', port=61613, reliable=FALSE, reconnectDelay=5)
27 Connection.new login, passcode, host, port, reliable, reconnectDelay
28 end
29
30 # Create a connection, requires a login and passcode.
31 # Can accept a host (default is localhost), and port
32 # (default is 61613) to connect to
33 def initialize(login, passcode, host='localhost', port=61613, reliable=false, reconnectDelay=5)
34 @host = host
35 @port = port
36 @login = login
37 @passcode = passcode
38 @transmit_semaphore = Mutex.new
39 @read_semaphore = Mutex.new
40 @socket_semaphore = Mutex.new
41 @reliable = reliable
42 @reconnectDelay = reconnectDelay
43 @closed = FALSE
44 @subscriptions = {}
45 @failure = NIL
46 socket
47 end
48
49 def socket
50 # Need to look into why the following synchronize does not work.
51 #@read_semaphore.synchronize do
52 s = @socket;
53 while s == NIL or @failure != NIL
54 @failure = NIL
55 begin
56 s = TCPSocket.open @host, @port
57 _transmit(s, "CONNECT", {:login => @login, :passcode => @passcode})
58 @connect = _receive(s)
59 # replay any subscriptions.
60 @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
61 rescue
62 @failure = $!;
63 s=NIL;
64 raise unless @reliable
65 $stderr.print "connect failed: " + $! +" will retry in #{@reconnectDelay}\n";
66 sleep(@reconnectDelay);
67 end
68 end
69 @socket = s
70 return s;
71 #end
72 end
73
74 # Is this connection open?
75 def open?
76 !@closed
77 end
78
79 # Is this connection closed?
80 def closed?
81 @closed
82 end
83
84 # Begin a transaction, requires a name for the transaction
85 def begin name, headers={}
86 headers[:transaction] = name
87 transmit "BEGIN", headers
88 end
89
90 # Acknowledge a message, used then a subscription has specified
91 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
92 #
93 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
94 def ack message_id, headers={}
95 headers['message-id'] = message_id
96 transmit "ACK", headers
97 end
98
99 # Commit a transaction by name
100 def commit name, headers={}
101 headers[:transaction] = name
102 transmit "COMMIT", headers
103 end
104
105 # Abort a transaction by name
106 def abort name, headers={}
107 headers[:transaction] = name
108 transmit "ABORT", headers
109 end
110
111 # Subscribe to a destination, must specify a name
112 def subscribe(name, headers = {}, subId=NIL)
113 headers[:destination] = name
114 transmit "SUBSCRIBE", headers
115
116 # Store the sub so that we can replay if we reconnect.
117 if @reliable
118 subId = name if subId==NIL
119 @subscriptions[subId]=headers
120 end
121 end
122
123 # Unsubscribe from a destination, must specify a name
124 def unsubscribe(name, headers = {}, subId=NIL)
125 headers[:destination] = name
126 transmit "UNSUBSCRIBE", headers
127 if @reliable
128 subId = name if subId==NIL
129 @subscriptions.delete(subId)
130 end
131 end
132
133 # Send message to destination
134 #
135 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
136 def send(destination, message, headers={})
137 headers[:destination] = destination
138 transmit "SEND", headers, message
139 end
140
141 # Close this connection
142 def disconnect(headers = {})
143 transmit "DISCONNECT", headers
144 end
145
146 # Return a pending message if one is available, otherwise
147 # return nil
148 def poll
149 @read_semaphore.synchronize do
150 return nil if @socket==NIL or !@socket.ready?
151 return receive
152 end
153 end
154
155 # Receive a frame, block until the frame is received
156 def __old_receive
157 # The recive my fail so we may need to retry.
158 while TRUE
159 begin
160 s = socket
161 return _receive(s)
162 rescue
163 @failure = $!;
164 raise unless @reliable
165 $stderr.print "receive failed: " + $!;
166 end
167 end
168 end
169
170 def receive
171 super_result = __old_receive()
172 if super_result.nil? && @reliable
173 $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
174 @socket = nil
175 super_result = __old_receive()
176 end
177 return super_result
178 end
179
180 private
181 def _receive( s )
182 line = ' '
183 @read_semaphore.synchronize do
184 line = s.gets while line =~ /^\s*$/
185 return NIL if line == NIL
186 Message.new do |m|
187 m.command = line.chomp
188 m.headers = {}
189 until (line = s.gets.chomp) == ''
190 k = (line.strip[0, line.strip.index(':')]).strip
191 v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
192 m.headers[k] = v
193 end
194
195 if (m.headers['content-length'])
196 m.body = s.read m.headers['content-length'].to_i
197 c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
198 raise "Invalid content length received" unless c == 0
199 else
200 m.body = ''
201 if RUBY_VERSION > '1.9'
202 until (c = s.getc.ord) == 0
203 m.body << c.chr
204 end
205 else
206 until (c = s.getc) == 0
207 m.body << c.chr
208 end
209 end
210 end
211 #c = s.getc
212 #raise "Invalid frame termination received" unless c == 10
213 end
214 end
215 end
216
217 private
218 def transmit(command, headers={}, body='')
219 # The transmit my fail so we may need to retry.
220 while TRUE
221 begin
222 s = socket
223 _transmit(s, command, headers, body)
224 return
225 rescue
226 @failure = $!;
227 raise unless @reliable
228 $stderr.print "transmit failed: " + $!+"\n";
229 end
230 end
231 end
232
233 private
234 def _transmit(s, command, headers={}, body='')
235 @transmit_semaphore.synchronize do
236 s.puts command
237 headers.each {|k,v| s.puts "#{k}:#{v}" }
238 s.puts "content-length: #{body.length}"
239 s.puts "content-type: text/plain; charset=UTF-8"
240 s.puts
241 s.write body
242 s.write "\0"
243 end
244 end
245 end
246
247 # Container class for frames, misnamed technically
248 class Message
249 attr_accessor :headers, :body, :command
250
251 def initialize
252 yield(self) if block_given?
253 end
254
255 def to_s
256 "<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
257 end
258 end
259
260 # Typical Stomp client class. Uses a listener thread to receive frames
261 # from the server, any thread can send.
262 #
263 # Receives all happen in one thread, so consider not doing much processing
264 # in that thread if you have much message volume.
265 class Client
266
267 # Accepts a username (default ""), password (default ""),
268 # host (default localhost), and port (default 61613)
269 def initialize user="", pass="", host="localhost", port=61613, reliable=false
270 if user =~ /stomp:\/\/(\w+):(\d+)/
271 user = ""
272 pass = ""
273 host = $1
274 port = $2
275 reliable = false
276 elsif user =~ /stomp:\/\/(\w+):(\w+)@(\w+):(\d+)/
277 user = $1
278 pass = $2
279 host = $3
280 port = $4
281 reliable = false
282 end
283
284 @id_mutex = Mutex.new
285 @ids = 1
286 @connection = Connection.open user, pass, host, port, reliable
287 @listeners = {}
288 @receipt_listeners = {}
289 @running = true
290 @replay_messages_by_txn = Hash.new
291 @listener_thread = Thread.start do
292 while @running
293 message = @connection.receive
294 case
295 when message == NIL:
296 break
297 when message.command == 'MESSAGE':
298 if listener = @listeners[message.headers['destination']]
299 listener.call(message)
300 end
301 when message.command == 'RECEIPT':
302 if listener = @receipt_listeners[message.headers['receipt-id']]
303 listener.call(message)
304 end
305 end
306 end
307 end
308 end
309
310 # Join the listener thread for this client,
311 # generally used to wait for a quit signal
312 def join
313 @listener_thread.join
314 end
315
316 # Accepts a username (default ""), password (default ""),
317 # host (default localhost), and port (default 61613)
318 def self.open user="", pass="", host="localhost", port=61613, reliable=false
319 Client.new user, pass, host, port, reliable
320 end
321
322 # Begin a transaction by name
323 def begin name, headers={}
324 @connection.begin name, headers
325 end
326
327 # Abort a transaction by name
328 def abort name, headers={}
329 @connection.abort name, headers
330
331 # lets replay any ack'd messages in this transaction
332 replay_list = @replay_messages_by_txn[name]
333 if replay_list
334 replay_list.each do |message|
335 if listener = @listeners[message.headers['destination']]
336 listener.call(message)
337 end
338 end
339 end
340 end
341
342 # Commit a transaction by name
343 def commit name, headers={}
344 txn_id = headers[:transaction]
345 @replay_messages_by_txn.delete(txn_id)
346 @connection.commit name, headers
347 end
348
349 # Subscribe to a destination, must be passed a block
350 # which will be used as a callback listener
351 #
352 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
353 def subscribe destination, headers={}
354 raise "No listener given" unless block_given?
355 @listeners[destination] = lambda {|msg| yield msg}
356 @connection.subscribe destination, headers
357 end
358
359 # Unsubecribe from a channel
360 def unsubscribe name, headers={}
361 @connection.unsubscribe name, headers
362 @listeners[name] = nil
363 end
364
365 # Acknowledge a message, used then a subscription has specified
366 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
367 #
368 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
369 def acknowledge message, headers={}
370 txn_id = headers[:transaction]
371 if txn_id
372 # lets keep around messages ack'd in this transaction in case we rollback
373 replay_list = @replay_messages_by_txn[txn_id]
374 if replay_list == nil
375 replay_list = []
376 @replay_messages_by_txn[txn_id] = replay_list
377 end
378 replay_list << message
379 end
380 if block_given?
381 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
382 end
383 @connection.ack message.headers['message-id'], headers
384 end
385
386 # Send message to destination
387 #
388 # If a block is given a receipt will be requested and passed to the
389 # block on receipt
390 #
391 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
392 def send destination, message, headers = {}
393 if block_given?
394 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
395 end
396 @connection.send destination, message, headers
397 end
398
399 # Is this client open?
400 def open?
401 @connection.open?
402 end
403
404 # Close out resources in use by this client
405 def close
406 @connection.disconnect
407 @running = false
408 end
409
410 private
411 def register_receipt_listener listener
412 id = -1
413 @id_mutex.synchronize do
414 id = @ids.to_s
415 @ids = @ids.succ
416 end
417 @receipt_listeners[id] = listener
418 id
419 end
420
421 end
422end
4230
=== removed directory '.pc/getc_returns_a_string_1.9.patch'
=== removed directory '.pc/getc_returns_a_string_1.9.patch/lib'
=== removed file '.pc/getc_returns_a_string_1.9.patch/lib/stomp.rb'
--- .pc/getc_returns_a_string_1.9.patch/lib/stomp.rb 2010-09-28 00:25:39 +0000
+++ .pc/getc_returns_a_string_1.9.patch/lib/stomp.rb 1970-01-01 00:00:00 +0000
@@ -1,416 +0,0 @@
1# Copyright 2005-2006 Brian McCallister
2# Copyright 2006 LogicBlaze Inc.
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16require 'io/wait'
17require 'socket'
18require 'thread'
19
20module Stomp
21
22 # Low level connection which maps commands and supports
23 # synchronous receives
24 class Connection
25
26 def Connection.open(login = "", passcode = "", host='localhost', port=61613, reliable=FALSE, reconnectDelay=5)
27 Connection.new login, passcode, host, port, reliable, reconnectDelay
28 end
29
30 # Create a connection, requires a login and passcode.
31 # Can accept a host (default is localhost), and port
32 # (default is 61613) to connect to
33 def initialize(login, passcode, host='localhost', port=61613, reliable=false, reconnectDelay=5)
34 @host = host
35 @port = port
36 @login = login
37 @passcode = passcode
38 @transmit_semaphore = Mutex.new
39 @read_semaphore = Mutex.new
40 @socket_semaphore = Mutex.new
41 @reliable = reliable
42 @reconnectDelay = reconnectDelay
43 @closed = FALSE
44 @subscriptions = {}
45 @failure = NIL
46 socket
47 end
48
49 def socket
50 # Need to look into why the following synchronize does not work.
51 #@read_semaphore.synchronize do
52 s = @socket;
53 while s == NIL or @failure != NIL
54 @failure = NIL
55 begin
56 s = TCPSocket.open @host, @port
57 _transmit(s, "CONNECT", {:login => @login, :passcode => @passcode})
58 @connect = _receive(s)
59 # replay any subscriptions.
60 @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
61 rescue
62 @failure = $!;
63 s=NIL;
64 raise unless @reliable
65 $stderr.print "connect failed: " + $! +" will retry in #{@reconnectDelay}\n";
66 sleep(@reconnectDelay);
67 end
68 end
69 @socket = s
70 return s;
71 #end
72 end
73
74 # Is this connection open?
75 def open?
76 !@closed
77 end
78
79 # Is this connection closed?
80 def closed?
81 @closed
82 end
83
84 # Begin a transaction, requires a name for the transaction
85 def begin name, headers={}
86 headers[:transaction] = name
87 transmit "BEGIN", headers
88 end
89
90 # Acknowledge a message, used then a subscription has specified
91 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
92 #
93 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
94 def ack message_id, headers={}
95 headers['message-id'] = message_id
96 transmit "ACK", headers
97 end
98
99 # Commit a transaction by name
100 def commit name, headers={}
101 headers[:transaction] = name
102 transmit "COMMIT", headers
103 end
104
105 # Abort a transaction by name
106 def abort name, headers={}
107 headers[:transaction] = name
108 transmit "ABORT", headers
109 end
110
111 # Subscribe to a destination, must specify a name
112 def subscribe(name, headers = {}, subId=NIL)
113 headers[:destination] = name
114 transmit "SUBSCRIBE", headers
115
116 # Store the sub so that we can replay if we reconnect.
117 if @reliable
118 subId = name if subId==NIL
119 @subscriptions[subId]=headers
120 end
121 end
122
123 # Unsubscribe from a destination, must specify a name
124 def unsubscribe(name, headers = {}, subId=NIL)
125 headers[:destination] = name
126 transmit "UNSUBSCRIBE", headers
127 if @reliable
128 subId = name if subId==NIL
129 @subscriptions.delete(subId)
130 end
131 end
132
133 # Send message to destination
134 #
135 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
136 def send(destination, message, headers={})
137 headers[:destination] = destination
138 transmit "SEND", headers, message
139 end
140
141 # Close this connection
142 def disconnect(headers = {})
143 transmit "DISCONNECT", headers
144 end
145
146 # Return a pending message if one is available, otherwise
147 # return nil
148 def poll
149 @read_semaphore.synchronize do
150 return nil if @socket==NIL or !@socket.ready?
151 return receive
152 end
153 end
154
155 # Receive a frame, block until the frame is received
156 def __old_receive
157 # The recive my fail so we may need to retry.
158 while TRUE
159 begin
160 s = socket
161 return _receive(s)
162 rescue
163 @failure = $!;
164 raise unless @reliable
165 $stderr.print "receive failed: " + $!;
166 end
167 end
168 end
169
170 def receive
171 super_result = __old_receive()
172 if super_result.nil? && @reliable
173 $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
174 @socket = nil
175 super_result = __old_receive()
176 end
177 return super_result
178 end
179
180 private
181 def _receive( s )
182 line = ' '
183 @read_semaphore.synchronize do
184 line = s.gets while line =~ /^\s*$/
185 return NIL if line == NIL
186 Message.new do |m|
187 m.command = line.chomp
188 m.headers = {}
189 until (line = s.gets.chomp) == ''
190 k = (line.strip[0, line.strip.index(':')]).strip
191 v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
192 m.headers[k] = v
193 end
194
195 if (m.headers['content-length'])
196 m.body = s.read m.headers['content-length'].to_i
197 c = s.getc
198 raise "Invalid content length received" unless c == 0
199 else
200 m.body = ''
201 until (c = s.getc) == 0
202 m.body << c.chr
203 end
204 end
205 #c = s.getc
206 #raise "Invalid frame termination received" unless c == 10
207 end
208 end
209 end
210
211 private
212 def transmit(command, headers={}, body='')
213 # The transmit my fail so we may need to retry.
214 while TRUE
215 begin
216 s = socket
217 _transmit(s, command, headers, body)
218 return
219 rescue
220 @failure = $!;
221 raise unless @reliable
222 $stderr.print "transmit failed: " + $!+"\n";
223 end
224 end
225 end
226
227 private
228 def _transmit(s, command, headers={}, body='')
229 @transmit_semaphore.synchronize do
230 s.puts command
231 headers.each {|k,v| s.puts "#{k}:#{v}" }
232 s.puts "content-length: #{body.length}"
233 s.puts "content-type: text/plain; charset=UTF-8"
234 s.puts
235 s.write body
236 s.write "\0"
237 end
238 end
239 end
240
241 # Container class for frames, misnamed technically
242 class Message
243 attr_accessor :headers, :body, :command
244
245 def initialize
246 yield(self) if block_given?
247 end
248
249 def to_s
250 "<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
251 end
252 end
253
254 # Typical Stomp client class. Uses a listener thread to receive frames
255 # from the server, any thread can send.
256 #
257 # Receives all happen in one thread, so consider not doing much processing
258 # in that thread if you have much message volume.
259 class Client
260
261 # Accepts a username (default ""), password (default ""),
262 # host (default localhost), and port (default 61613)
263 def initialize user="", pass="", host="localhost", port=61613, reliable=false
264 if user =~ /stomp:\/\/(\w+):(\d+)/
265 user = ""
266 pass = ""
267 host = $1
268 port = $2
269 reliable = false
270 elsif user =~ /stomp:\/\/(\w+):(\w+)@(\w+):(\d+)/
271 user = $1
272 pass = $2
273 host = $3
274 port = $4
275 reliable = false
276 end
277
278 @id_mutex = Mutex.new
279 @ids = 1
280 @connection = Connection.open user, pass, host, port, reliable
281 @listeners = {}
282 @receipt_listeners = {}
283 @running = true
284 @replay_messages_by_txn = Hash.new
285 @listener_thread = Thread.start do
286 while @running
287 message = @connection.receive
288 case
289 when message == NIL:
290 break
291 when message.command == 'MESSAGE':
292 if listener = @listeners[message.headers['destination']]
293 listener.call(message)
294 end
295 when message.command == 'RECEIPT':
296 if listener = @receipt_listeners[message.headers['receipt-id']]
297 listener.call(message)
298 end
299 end
300 end
301 end
302 end
303
304 # Join the listener thread for this client,
305 # generally used to wait for a quit signal
306 def join
307 @listener_thread.join
308 end
309
310 # Accepts a username (default ""), password (default ""),
311 # host (default localhost), and port (default 61613)
312 def self.open user="", pass="", host="localhost", port=61613, reliable=false
313 Client.new user, pass, host, port, reliable
314 end
315
316 # Begin a transaction by name
317 def begin name, headers={}
318 @connection.begin name, headers
319 end
320
321 # Abort a transaction by name
322 def abort name, headers={}
323 @connection.abort name, headers
324
325 # lets replay any ack'd messages in this transaction
326 replay_list = @replay_messages_by_txn[name]
327 if replay_list
328 replay_list.each do |message|
329 if listener = @listeners[message.headers['destination']]
330 listener.call(message)
331 end
332 end
333 end
334 end
335
336 # Commit a transaction by name
337 def commit name, headers={}
338 txn_id = headers[:transaction]
339 @replay_messages_by_txn.delete(txn_id)
340 @connection.commit name, headers
341 end
342
343 # Subscribe to a destination, must be passed a block
344 # which will be used as a callback listener
345 #
346 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
347 def subscribe destination, headers={}
348 raise "No listener given" unless block_given?
349 @listeners[destination] = lambda {|msg| yield msg}
350 @connection.subscribe destination, headers
351 end
352
353 # Unsubecribe from a channel
354 def unsubscribe name, headers={}
355 @connection.unsubscribe name, headers
356 @listeners[name] = nil
357 end
358
359 # Acknowledge a message, used then a subscription has specified
360 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
361 #
362 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
363 def acknowledge message, headers={}
364 txn_id = headers[:transaction]
365 if txn_id
366 # lets keep around messages ack'd in this transaction in case we rollback
367 replay_list = @replay_messages_by_txn[txn_id]
368 if replay_list == nil
369 replay_list = []
370 @replay_messages_by_txn[txn_id] = replay_list
371 end
372 replay_list << message
373 end
374 if block_given?
375 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
376 end
377 @connection.ack message.headers['message-id'], headers
378 end
379
380 # Send message to destination
381 #
382 # If a block is given a receipt will be requested and passed to the
383 # block on receipt
384 #
385 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
386 def send destination, message, headers = {}
387 if block_given?
388 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
389 end
390 @connection.send destination, message, headers
391 end
392
393 # Is this client open?
394 def open?
395 @connection.open?
396 end
397
398 # Close out resources in use by this client
399 def close
400 @connection.disconnect
401 @running = false
402 end
403
404 private
405 def register_receipt_listener listener
406 id = -1
407 @id_mutex.synchronize do
408 id = @ids.to_s
409 @ids = @ids.succ
410 end
411 @receipt_listeners[id] = listener
412 id
413 end
414
415 end
416end
4170
=== added file 'CHANGELOG.rdoc'
--- CHANGELOG.rdoc 1970-01-01 00:00:00 +0000
+++ CHANGELOG.rdoc 2011-07-31 16:48:34 +0000
@@ -0,0 +1,84 @@
1== 1.1.9 2011-15-06
2
3* Support wildcard destinations
4* Handle subscribe with string or symbol ID
5* Check for duplicate subscriptions in spec tests
6* Support AMQ and Apollo servers in uinit tests
7* Correct UTF-8 (Unicode) content-length calcualtion in Ruby 1.9
8* Send of a nil body causes exception
9* Add optional callback logging. See the examples install directory, files logexamp.rb and slogger.rb
10* Correct date stamps in this file
11
12== 1.1.8 2011-16-03
13
14* Set KEEPALIVE on connection socket options
15* Attempt to support JRuby more robustly (poll remains broken)
16* Switch to ruby supplied IO#ready?
17* Test enhancements for suppress_content_length header
18* Miscellaneous small documentation updates
19* Add parse_timeout parameter for use with hashed logins
20* Allow connection to hosts with a - (dash) in the host name
21* Add limit parameter to thread joins
22
23== 1.1.7 2011-09-01
24
25* Binary parse of raw STOMP frame
26* Fix broken tests on Ruby 1.9.2
27
28== 1.1.6 2010-10-06
29
30* Fixed multi-thread app hanging
31
32== 1.1.5 2010-17-03
33
34* Added publish method (send is now deprecated)
35* Changes on Rake File
36* Added original_destination header to unreceive
37* suppress content length header is send on the message for future handling (like unreceive)
38
39== 1.1.4 2010-21-01
40
41* Added unreceive message method that sends the message back to its queue or to the
42 dead letter queue, depending on the :max_redeliveries option, similar to a13m one.
43* Added environment variable option for running 'rake test' on any stomp server, using any port with any user.
44* Added suppress_content_length header option for ActiveMQ knowing it is a text message (see:
45 http://juretta.com/log/2009/05/24/activemq-jms-stomp/)
46* Fixed some bugs with Ruby 1.9 (concatenate string + exception)
47* Major changes on message parsing feature
48* Fixed bug with old socket not being closed when using failover
49* Fixed broken poll method on Connection
50* Fixed broken close method on Client
51* Added connection_frame accessor
52* Added disconnect receipt
53
54== 1.1.3 2009-24-11
55
56* Failover support
57* SSL support
58* Stomp::Connection and Stomp::Client accept a hash on their constructor
59
60== 1.1 2009-27-02
61
62* Ruby 1.9 Support
63* Add support for connect_headers, to control the CONNECT command.
64* Refactored lib dir to separate concerns.
65* Better test coverage
66* General code cleanup.
67
68== 1.0.6 2008-05-08
69
70* Whitespace cleanup
71* Refactored Rakefile and added stomp.gemspec for GitHub friendliness.
72* Added .gitignore file
73* Refactored layout of lib dir to separate concerns
74* Cleanup of initializers, and provide Client accessors for reading values (for testing)
75* Removed test/test_url_* files as they only differed from the test_client.rb in their
76 setup. Super UnDry. Added URL tests to cover stomp URL as param.
77* Created initial RSpec specs which stub/mock objects and should not require a running
78 Stomp server instance.
79
80== v1.0.5
81
82SVN rev 86 clone from http://svn.codehaus.org/stomp/ruby/trunk
83
84git-svn-id: http://svn.codehaus.org/stomp/ruby/trunk@86 fd4e7336-3dff-0310-b68a-b6615a75f13b
085
=== added file 'LICENSE'
--- LICENSE 1970-01-01 00:00:00 +0000
+++ LICENSE 2011-07-31 16:48:34 +0000
@@ -0,0 +1,202 @@
1
2 Apache License
3 Version 2.0, January 2004
4 http://www.apache.org/licenses/
5
6 TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
8 1. Definitions.
9
10 "License" shall mean the terms and conditions for use, reproduction,
11 and distribution as defined by Sections 1 through 9 of this document.
12
13 "Licensor" shall mean the copyright owner or entity authorized by
14 the copyright owner that is granting the License.
15
16 "Legal Entity" shall mean the union of the acting entity and all
17 other entities that control, are controlled by, or are under common
18 control with that entity. For the purposes of this definition,
19 "control" means (i) the power, direct or indirect, to cause the
20 direction or management of such entity, whether by contract or
21 otherwise, or (ii) ownership of fifty percent (50%) or more of the
22 outstanding shares, or (iii) beneficial ownership of such entity.
23
24 "You" (or "Your") shall mean an individual or Legal Entity
25 exercising permissions granted by this License.
26
27 "Source" form shall mean the preferred form for making modifications,
28 including but not limited to software source code, documentation
29 source, and configuration files.
30
31 "Object" form shall mean any form resulting from mechanical
32 transformation or translation of a Source form, including but
33 not limited to compiled object code, generated documentation,
34 and conversions to other media types.
35
36 "Work" shall mean the work of authorship, whether in Source or
37 Object form, made available under the License, as indicated by a
38 copyright notice that is included in or attached to the work
39 (an example is provided in the Appendix below).
40
41 "Derivative Works" shall mean any work, whether in Source or Object
42 form, that is based on (or derived from) the Work and for which the
43 editorial revisions, annotations, elaborations, or other modifications
44 represent, as a whole, an original work of authorship. For the purposes
45 of this License, Derivative Works shall not include works that remain
46 separable from, or merely link (or bind by name) to the interfaces of,
47 the Work and Derivative Works thereof.
48
49 "Contribution" shall mean any work of authorship, including
50 the original version of the Work and any modifications or additions
51 to that Work or Derivative Works thereof, that is intentionally
52 submitted to Licensor for inclusion in the Work by the copyright owner
53 or by an individual or Legal Entity authorized to submit on behalf of
54 the copyright owner. For the purposes of this definition, "submitted"
55 means any form of electronic, verbal, or written communication sent
56 to the Licensor or its representatives, including but not limited to
57 communication on electronic mailing lists, source code control systems,
58 and issue tracking systems that are managed by, or on behalf of, the
59 Licensor for the purpose of discussing and improving the Work, but
60 excluding communication that is conspicuously marked or otherwise
61 designated in writing by the copyright owner as "Not a Contribution."
62
63 "Contributor" shall mean Licensor and any individual or Legal Entity
64 on behalf of whom a Contribution has been received by Licensor and
65 subsequently incorporated within the Work.
66
67 2. Grant of Copyright License. Subject to the terms and conditions of
68 this License, each Contributor hereby grants to You a perpetual,
69 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70 copyright license to reproduce, prepare Derivative Works of,
71 publicly display, publicly perform, sublicense, and distribute the
72 Work and such Derivative Works in Source or Object form.
73
74 3. Grant of Patent License. Subject to the terms and conditions of
75 this License, each Contributor hereby grants to You a perpetual,
76 worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77 (except as stated in this section) patent license to make, have made,
78 use, offer to sell, sell, import, and otherwise transfer the Work,
79 where such license applies only to those patent claims licensable
80 by such Contributor that are necessarily infringed by their
81 Contribution(s) alone or by combination of their Contribution(s)
82 with the Work to which such Contribution(s) was submitted. If You
83 institute patent litigation against any entity (including a
84 cross-claim or counterclaim in a lawsuit) alleging that the Work
85 or a Contribution incorporated within the Work constitutes direct
86 or contributory patent infringement, then any patent licenses
87 granted to You under this License for that Work shall terminate
88 as of the date such litigation is filed.
89
90 4. Redistribution. You may reproduce and distribute copies of the
91 Work or Derivative Works thereof in any medium, with or without
92 modifications, and in Source or Object form, provided that You
93 meet the following conditions:
94
95 (a) You must give any other recipients of the Work or
96 Derivative Works a copy of this License; and
97
98 (b) You must cause any modified files to carry prominent notices
99 stating that You changed the files; and
100
101 (c) You must retain, in the Source form of any Derivative Works
102 that You distribute, all copyright, patent, trademark, and
103 attribution notices from the Source form of the Work,
104 excluding those notices that do not pertain to any part of
105 the Derivative Works; and
106
107 (d) If the Work includes a "NOTICE" text file as part of its
108 distribution, then any Derivative Works that You distribute must
109 include a readable copy of the attribution notices contained
110 within such NOTICE file, excluding those notices that do not
111 pertain to any part of the Derivative Works, in at least one
112 of the following places: within a NOTICE text file distributed
113 as part of the Derivative Works; within the Source form or
114 documentation, if provided along with the Derivative Works; or,
115 within a display generated by the Derivative Works, if and
116 wherever such third-party notices normally appear. The contents
117 of the NOTICE file are for informational purposes only and
118 do not modify the License. You may add Your own attribution
119 notices within Derivative Works that You distribute, alongside
120 or as an addendum to the NOTICE text from the Work, provided
121 that such additional attribution notices cannot be construed
122 as modifying the License.
123
124 You may add Your own copyright statement to Your modifications and
125 may provide additional or different license terms and conditions
126 for use, reproduction, or distribution of Your modifications, or
127 for any such Derivative Works as a whole, provided Your use,
128 reproduction, and distribution of the Work otherwise complies with
129 the conditions stated in this License.
130
131 5. Submission of Contributions. Unless You explicitly state otherwise,
132 any Contribution intentionally submitted for inclusion in the Work
133 by You to the Licensor shall be under the terms and conditions of
134 this License, without any additional terms or conditions.
135 Notwithstanding the above, nothing herein shall supersede or modify
136 the terms of any separate license agreement you may have executed
137 with Licensor regarding such Contributions.
138
139 6. Trademarks. This License does not grant permission to use the trade
140 names, trademarks, service marks, or product names of the Licensor,
141 except as required for reasonable and customary use in describing the
142 origin of the Work and reproducing the content of the NOTICE file.
143
144 7. Disclaimer of Warranty. Unless required by applicable law or
145 agreed to in writing, Licensor provides the Work (and each
146 Contributor provides its Contributions) on an "AS IS" BASIS,
147 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148 implied, including, without limitation, any warranties or conditions
149 of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150 PARTICULAR PURPOSE. You are solely responsible for determining the
151 appropriateness of using or redistributing the Work and assume any
152 risks associated with Your exercise of permissions under this License.
153
154 8. Limitation of Liability. In no event and under no legal theory,
155 whether in tort (including negligence), contract, or otherwise,
156 unless required by applicable law (such as deliberate and grossly
157 negligent acts) or agreed to in writing, shall any Contributor be
158 liable to You for damages, including any direct, indirect, special,
159 incidental, or consequential damages of any character arising as a
160 result of this License or out of the use or inability to use the
161 Work (including but not limited to damages for loss of goodwill,
162 work stoppage, computer failure or malfunction, or any and all
163 other commercial damages or losses), even if such Contributor
164 has been advised of the possibility of such damages.
165
166 9. Accepting Warranty or Additional Liability. While redistributing
167 the Work or Derivative Works thereof, You may choose to offer,
168 and charge a fee for, acceptance of support, warranty, indemnity,
169 or other liability obligations and/or rights consistent with this
170 License. However, in accepting such obligations, You may act only
171 on Your own behalf and on Your sole responsibility, not on behalf
172 of any other Contributor, and only if You agree to indemnify,
173 defend, and hold each Contributor harmless for any liability
174 incurred by, or claims asserted against, such Contributor by reason
175 of your accepting any such warranty or additional liability.
176
177 END OF TERMS AND CONDITIONS
178
179 APPENDIX: How to apply the Apache License to your work.
180
181 To apply the Apache License to your work, attach the following
182 boilerplate notice, with the fields enclosed by brackets "[]"
183 replaced with your own identifying information. (Don't include
184 the brackets!) The text should be enclosed in the appropriate
185 comment syntax for the file format. We also recommend that a
186 file or class name and description of purpose be included on the
187 same "printed page" as the copyright notice for easier
188 identification within third-party archives.
189
190 Copyright [yyyy] [name of copyright owner]
191
192 Licensed under the Apache License, Version 2.0 (the "License");
193 you may not use this file except in compliance with the License.
194 You may obtain a copy of the License at
195
196 http://www.apache.org/licenses/LICENSE-2.0
197
198 Unless required by applicable law or agreed to in writing, software
199 distributed under the License is distributed on an "AS IS" BASIS,
200 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 See the License for the specific language governing permissions and
202 limitations under the License.
0203
=== added file 'README.rdoc'
--- README.rdoc 1970-01-01 00:00:00 +0000
+++ README.rdoc 2011-07-31 16:48:34 +0000
@@ -0,0 +1,107 @@
1==README
2
3* (http://gitorious.org/projects/stomp/)
4* (https://github.com/morellon/stomp/)
5* (http://stomp.rubyforge.org/)
6
7===Overview
8
9An implementation of the Stomp protocol for Ruby. See:
10
11* [STOMP 1.0] (http://stomp.codehaus.org/Protocol)
12* [STOMP 1.0 and 1.1 Draft] (http://stomp.github.com/index.html)
13
14
15===Example Usage
16
17 client = Stomp::Client.new("test", "user", "localhost", 61613)
18 client.send("/my/queue", "hello world!")
19 client.subscribe("/my/queue") do |msg|
20 p msg
21 end
22
23===Failover + SSL Example URL Usage
24
25 options = "initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false"
26
27 #remotehost1 uses SSL, remotehost2 doesn't
28 client = Stomp::Client.new("failover:(stomp+ssl://login1:passcode1@remotehost1:61612,stomp://login2:passcode2@remotehost2:61613)?#{options}")
29
30 client.send("/my/queue", "hello world!")
31 client.subscribe("/my/queue") do |msg|
32 p msg
33 end
34
35===Hash Login Example Usage
36
37 hash = {
38 :hosts => [
39 {:login => "login1", :passcode => "passcode1", :host => "remotehost1", :port => 61612, :ssl => true},
40 {:login => "login2", :passcode => "passcode2", :host => "remotehost2", :port => 61613, :ssl => false},
41
42 ],
43 # These are the default parameters, don't need to be set
44 :initial_reconnect_delay => 0.01,
45 :max_reconnect_delay => 30.0,
46 :use_exponential_back_off => true,
47 :back_off_multiplier => 2,
48 :max_reconnect_attempts => 0,
49 :randomize => false,
50 :backup => false,
51 :timeout => -1,
52 :connect_headers => {},
53 :parse_timeout => 5,
54 :logger => nil,
55 }
56
57 # for client
58 client = Stomp::Client.new(hash)
59
60 # for connection
61 connection = Stomp::Connection.new(hash)
62
63
64===Contact info
65
66Up until March 2009 the project was maintained and primarily developed by Brian McCallister.
67
68The project is now maintained by Johan Sørensen <johan@johansorensen.com> and others.
69
70===Source Code
71
72 https://github.com/morellon/stomp/
73 http://gitorious.org/projects/stomp/
74 http://github.com/js/stomp/
75
76===Project urls
77
78Project Home :
79
80 http://gitorious.org/projects/stomp/
81 http://rubyforge.org/projects/stomp/
82
83Stomp Protocol Info :
84
85 http://stomp.github.com/index.html
86 http://stomp.codehaus.org/Protocol
87
88= Contributors
89
90The following people have contributed to Stomp:
91
92* Brian McCaliister
93* Glenn Rempe <glenn@rempe.us>
94* jstrachan
95* Marius Mathiesen <marius.mathiesen@gmail.com>
96* Johan S√∏rensen <johan@johansorensen.com>
97* Thiago Morello <morellon@gmail.com>
98* Guy M. Allard
99* kookster
100* Tony Garnock-Jones <tonyg@lshift.net>
101* chirino
102* Stefan Saasen
103* Neil Wilson
104* Dinesh Majrekar
105* Kiall Mac Innes
106* Rob Skaggs
107
0108
=== added file 'Rakefile'
--- Rakefile 1970-01-01 00:00:00 +0000
+++ Rakefile 2011-07-31 16:48:34 +0000
@@ -0,0 +1,77 @@
1# Copyright 2005-2006 Brian McCallister
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14$:.unshift(File.dirname(__FILE__) + "/lib")
15require 'rubygems'
16require 'rake'
17require 'rake/testtask'
18require 'rspec/core/rake_task'
19require "stomp/version"
20
21begin
22 require "hanna/rdoctask"
23rescue LoadError => e
24 require "rdoc/task"
25end
26
27begin
28 require 'jeweler'
29 Jeweler::Tasks.new do |gem|
30 gem.name = "stomp"
31 gem.version = Stomp::Version::STRING
32 gem.summary = %Q{Ruby client for the Stomp messaging protocol}
33 gem.description = %Q{Ruby client for the Stomp messaging protocol}
34 gem.email = ["brianm@apache.org", 'marius@stones.com', 'morellon@gmail.com',
35 'allard.guy.m@gmail.com' ]
36 gem.homepage = "https://rubygems.org/gems/stomp"
37 gem.authors = ["Brian McCallister", 'Marius Mathiesen', 'Thiago Morello',
38 'Guy M. Allard']
39 gem.add_development_dependency "rspec", '>= 2.3'
40 end
41 Jeweler::GemcutterTasks.new
42rescue LoadError
43 puts "Jeweler not available. Install it with: gem install jeweler"
44end
45
46desc 'Run the specs'
47RSpec::Core::RakeTask.new(:spec) do |t|
48 t.rspec_opts = ['--colour']
49 t.pattern = 'spec/**/*_spec.rb'
50end
51
52desc "Rspec : run all with RCov"
53RSpec::Core::RakeTask.new('spec:rcov') do |t|
54 t.pattern = 'spec/**/*_spec.rb'
55 t.rcov = true
56 t.rcov_opts = ['--exclude', 'gems', '--exclude', 'spec']
57end
58
59Rake::RDocTask.new do |rdoc|
60 rdoc.main = "README.rdoc"
61 rdoc.rdoc_dir = "doc"
62 rdoc.title = "Stomp"
63 rdoc.options += %w[ --line-numbers --inline-source --charset utf-8 ]
64 rdoc.rdoc_files.include("README.rdoc", "CHANGELOG.rdoc")
65 rdoc.rdoc_files.include("lib/**/*.rb")
66end
67
68Rake::TestTask.new do |t|
69 t.libs << "test"
70 t.test_files = FileList['test/test*.rb']
71 t.verbose = true
72end
73
74task :default => :spec
75
76
77
078
=== added directory 'bin'
=== added file 'bin/catstomp'
--- bin/catstomp 1970-01-01 00:00:00 +0000
+++ bin/catstomp 2011-07-31 16:48:34 +0000
@@ -0,0 +1,55 @@
1#!/usr/bin/env ruby
2#
3# Copyright 2006 LogicBlaze Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17begin; require 'rubygems'; rescue; end
18require 'stomp'
19
20#
21# This simple script is inspired by the netcat utility. It allows you to send
22# input into this process to stomp destination.
23#
24# Usage: catstomp (destination-name)
25#
26# Example: ls | catstomp /topic/foo
27# Would send the output of the ls command to the stomp destination /topic/foo
28#
29begin
30
31 @port = 61613
32 @host = "localhost"
33 @user = ENV["STOMP_USER"];
34 @password = ENV["STOMP_PASSWORD"]
35
36 @host = ENV["STOMP_HOST"] if ENV["STOMP_HOST"] != nil
37 @port = ENV["STOMP_PORT"] if ENV["STOMP_PORT"] != nil
38
39 @destination = "/topic/default"
40 @destination = $*[0] if $*[0] != nil
41
42 $stderr.print "Connecting to stomp://#{@host}:#{@port} as #{@user}\n"
43 @conn = Stomp::Connection.open(@user, @password, @host, @port, true)
44 $stderr.print "Sending input to #{@destination}\n"
45
46 @headers = {'persistent'=>'false'}
47 @headers['reply-to'] = $*[1] if $*[1] != nil
48
49 STDIN.each_line { |line|
50 @conn.send @destination, line, @headers
51 }
52
53rescue
54end
55
056
=== added file 'bin/stompcat'
--- bin/stompcat 1970-01-01 00:00:00 +0000
+++ bin/stompcat 2011-07-31 16:48:34 +0000
@@ -0,0 +1,56 @@
1#!/usr/bin/env ruby
2#
3# Copyright 2006 LogicBlaze Inc.
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16#
17begin; require 'rubygems'; rescue; end
18require 'stomp'
19
20#
21# This simple script is inspired by the netcat utility. It allows you to receive
22# data from a stomp destination and output it.
23#
24# Usage: stompcat (destination-name)
25#
26# Example: stompcat /topic/foo
27# Would display output that arrives at the /topic/foo stomp destination
28#
29begin
30
31 @port = 61613
32 @host = "localhost"
33 @user = ENV["STOMP_USER"];
34 @password = ENV["STOMP_PASSWORD"]
35
36 @host = ENV["STOMP_HOST"] if ENV["STOMP_HOST"] != nil
37 @port = ENV["STOMP_PORT"] if ENV["STOMP_PORT"] != nil
38
39 @destination = "/topic/default"
40 @destination = $*[0] if $*[0] != nil
41
42 $stderr.print "Connecting to stomp://#{@host}:#{@port} as #{@user}\n"
43 @conn = Stomp::Connection.open(@user, @password, @host, @port, true)
44 $stderr.print "Getting output from #{@destination}\n"
45
46 @conn.subscribe(@destination, { :ack =>"client" })
47 while true
48 @msg = @conn.receive
49 $stdout.print @msg.body
50 $stdout.flush
51 @conn.ack @msg.headers["message-id"]
52 end
53
54rescue
55end
56
057
=== modified file 'debian/changelog'
--- debian/changelog 2010-09-28 00:25:39 +0000
+++ debian/changelog 2011-07-31 16:48:34 +0000
@@ -1,3 +1,26 @@
1libstomp-ruby (1.1.9-0ubuntu1) oneiric; urgency=low
2
3 * New upstream release (Closes: #598564, LP: #707317).
4
5 * debian/patches/case_statement_compatible_1.9.2.patch,
6 debian/patches/getc_returns_a_string_1.9.patch: obsolete, removed.
7 * debian/patches/series: update accordingly.
8
9 * debian/rules (common-binary-indep): deal with the catstomp and
10 stompcat binaries by putting them in libstomp-ruby instead of the
11 (ruby-)versioned packages and fixing their hash-bang line to use the
12 generic /usr/bin/ruby binary/symlink.
13
14 * debian/control: have libstomp-ruby depend on libstomp-ruby1.8 or
15 libstomp-ruby1.9.1. Make libstomp-ruby$ver suggest libstomp-ruby.
16 Update the package description to describe libstomp-ruby's new
17 purpose.
18
19 * debian/control: have libstomp-ruby explicitly depend on ruby for
20 catstomp and stompcat to make lintian happy.
21
22 -- James Troup <james.troup@canonical.com> Sun, 31 Jul 2011 17:14:07 +0100
23
1libstomp-ruby (1.0.4-3) unstable; urgency=low24libstomp-ruby (1.0.4-3) unstable; urgency=low
225
3 * Team upload.26 * Team upload.
427
=== modified file 'debian/control'
--- debian/control 2010-09-28 00:25:39 +0000
+++ debian/control 2011-07-31 16:48:34 +0000
@@ -12,7 +12,7 @@
1212
13Package: libstomp-ruby13Package: libstomp-ruby
14Architecture: all14Architecture: all
15Depends: libstomp-ruby1.8, ${misc:Depends}15Depends: libstomp-ruby1.8 | libstomp-ruby1.9.1, ruby, ${misc:Depends}
16Suggests: libstomp-ruby-doc16Suggests: libstomp-ruby-doc
17Description: Ruby bindings for the stomp messaging protocol17Description: Ruby bindings for the stomp messaging protocol
18 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)18 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)
@@ -21,13 +21,12 @@
21 implementation, and Stomp::Client, which is designed as a higher21 implementation, and Stomp::Client, which is designed as a higher
22 level convenience API.22 level convenience API.
23 .23 .
24 This package is a dependency package, which depends on the package24 This package contains the catstomp and stompcat binaries.
25 containing actual Ruby stomp libraries for the default Ruby version
26 (currently 1.8).
2725
28Package: libstomp-ruby1.826Package: libstomp-ruby1.8
29Architecture: all27Architecture: all
30Depends: ${shlibs:Depends}, ${misc:Depends}, ruby1.828Depends: ${shlibs:Depends}, ${misc:Depends}, ruby1.8
29Suggests: libstomp-ruby
31Description: Ruby 1.8 bindings for the stomp messaging protocol30Description: Ruby 1.8 bindings for the stomp messaging protocol
32 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)31 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)
33 type systems. This library provides two useful interfaces, a low-32 type systems. This library provides two useful interfaces, a low-
@@ -40,6 +39,7 @@
40Package: libstomp-ruby1.9.139Package: libstomp-ruby1.9.1
41Architecture: all40Architecture: all
42Depends: ${shlibs:Depends}, ${misc:Depends}, ruby1.9.141Depends: ${shlibs:Depends}, ${misc:Depends}, ruby1.9.1
42Suggests: libstomp-ruby
43Description: Ruby 1.9.1 bindings for the stomp messaging protocol43Description: Ruby 1.9.1 bindings for the stomp messaging protocol
44 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)44 Stomp is a text-oriented wire protocol for messaging (MOM/MQ/JMS)
45 type systems. This library provides two useful interfaces, a low-45 type systems. This library provides two useful interfaces, a low-
4646
=== added directory 'debian/patches'
=== removed directory 'debian/patches'
=== removed file 'debian/patches/case_statement_compatible_1.9.2.patch'
--- debian/patches/case_statement_compatible_1.9.2.patch 2010-09-28 00:25:39 +0000
+++ debian/patches/case_statement_compatible_1.9.2.patch 1970-01-01 00:00:00 +0000
@@ -1,23 +0,0 @@
1From: Marius Mathiesen
2Description: Making 1.9 compatible
3Origin: upstream, http://github.com/js/stomp/commit/a778661ce9c074ae5b415658d17dd2639f5c9c05
4Debian-Bug: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=593079
5--- a/lib/stomp.rb
6+++ b/lib/stomp.rb
7@@ -286,13 +286,13 @@ module Stomp
8 while @running
9 message = @connection.receive
10 case
11- when message == NIL:
12+ when message == NIL
13 break
14- when message.command == 'MESSAGE':
15+ when message.command == 'MESSAGE'
16 if listener = @listeners[message.headers['destination']]
17 listener.call(message)
18 end
19- when message.command == 'RECEIPT':
20+ when message.command == 'RECEIPT'
21 if listener = @receipt_listeners[message.headers['receipt-id']]
22 listener.call(message)
23 end
240
=== removed file 'debian/patches/getc_returns_a_string_1.9.patch'
--- debian/patches/getc_returns_a_string_1.9.patch 2010-09-28 00:25:39 +0000
+++ debian/patches/getc_returns_a_string_1.9.patch 1970-01-01 00:00:00 +0000
@@ -1,27 +0,0 @@
1From: Johan Sørensen <johan@johansorensen.com>
2Subject: Further Ruby 1.9 support: IO#getc returns a string, instead of a char in 1.9
3Origin: upstream, http://github.com/js/stomp/commit/2971a7922f64052c5b308f2a4a92080d7c8b046b
4--- a/lib/stomp.rb
5+++ b/lib/stomp.rb
6@@ -194,12 +194,18 @@ module Stomp
7
8 if (m.headers['content-length'])
9 m.body = s.read m.headers['content-length'].to_i
10- c = s.getc
11+ c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
12 raise "Invalid content length received" unless c == 0
13 else
14 m.body = ''
15- until (c = s.getc) == 0
16- m.body << c.chr
17+ if RUBY_VERSION > '1.9'
18+ until (c = s.getc.ord) == 0
19+ m.body << c.chr
20+ end
21+ else
22+ until (c = s.getc) == 0
23+ m.body << c.chr
24+ end
25 end
26 end
27 #c = s.getc
280
=== added file 'debian/patches/series'
--- debian/patches/series 1970-01-01 00:00:00 +0000
+++ debian/patches/series 2011-07-31 16:48:34 +0000
@@ -0,0 +1,1 @@
1
02
=== removed file 'debian/patches/series'
--- debian/patches/series 2010-09-28 00:25:39 +0000
+++ debian/patches/series 1970-01-01 00:00:00 +0000
@@ -1,2 +0,0 @@
1getc_returns_a_string_1.9.patch
2case_statement_compatible_1.9.2.patch
30
=== modified file 'debian/rules'
--- debian/rules 2009-01-26 20:38:53 +0000
+++ debian/rules 2011-07-31 16:48:34 +0000
@@ -2,3 +2,15 @@
22
3include /usr/share/cdbs/1/rules/debhelper.mk3include /usr/share/cdbs/1/rules/debhelper.mk
4include /usr/share/ruby-pkg-tools/1/class/ruby-setup-rb.mk4include /usr/share/ruby-pkg-tools/1/class/ruby-setup-rb.mk
5
6common-binary-indep::
7 # Deal with the catstomp and stompcat binaries by putting them
8 # into libstomp-ruby and fixing them to use /usr/bin/ruby
9 install -d -m 755 -o root -g root debian/libstomp-ruby/usr/
10 cp -a debian/$(firstword $(DEB_RUBY_REAL_LIB_PACKAGES))/usr/bin/ debian/libstomp-ruby/usr/bin/
11 for script in `find debian/libstomp-ruby/usr/bin/ -type f`; do \
12 sed -i -e "1s%.*%#! /usr/bin/ruby%" $$script ; \
13 done
14 for pkg in $(DEB_RUBY_REAL_LIB_PACKAGES); do \
15 rm -fr debian/$$pkg/usr/bin/; \
16 done
517
=== added directory 'debian/source'
=== removed directory 'debian/source'
=== added file 'debian/source/format'
--- debian/source/format 1970-01-01 00:00:00 +0000
+++ debian/source/format 2011-07-31 16:48:34 +0000
@@ -0,0 +1,1 @@
13.0 (quilt)
02
=== removed file 'debian/source/format'
--- debian/source/format 2010-06-12 00:08:49 +0000
+++ debian/source/format 1970-01-01 00:00:00 +0000
@@ -1,1 +0,0 @@
13.0 (quilt)
20
=== added directory 'examples'
=== added file 'examples/consumer.rb'
--- examples/consumer.rb 1970-01-01 00:00:00 +0000
+++ examples/consumer.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,19 @@
1require 'rubygems'
2require 'stomp'
3
4
5client = Stomp::Client.new("failover://(stomp://:@localhost:61613,stomp://:@remotehost:61613)?initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false")
6puts "Subscribing ronaldo"
7client.subscribe("/queue/ronaldo", {:ack => "client", "activemq.prefetchSize" => 1, "activemq.exclusive" => true }) do |msg|
8 File.open("file", "a") do |f|
9 f.write(msg.body)
10 f.write("\n----------------\n")
11 end
12
13 client.acknowledge(msg)
14end
15
16loop do
17 sleep(1)
18 puts "."
19end
020
=== added file 'examples/logexamp.rb'
--- examples/logexamp.rb 1970-01-01 00:00:00 +0000
+++ examples/logexamp.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,50 @@
1require 'rubygems'
2require 'stomp'
3require 'logger' # for the 'local' logger
4#
5$:.unshift(File.dirname(__FILE__))
6#
7require 'slogger'
8#
9# A STOMP client program which uses the callback logging facility.
10#
11llog = Logger::new(STDOUT)
12llog.level = Logger::DEBUG
13llog.debug "LE Starting"
14
15# //////////////////////////////////////////////////////////////////////////////
16mylog = Slogger::new # The client provided STOMP callback logger
17
18# //////////////////////////////////////////////////////////////////////////////
19user = ENV['STOMP_USER'] ? ENV['STOMP_USER'] : 'guest'
20password = ENV['STOMP_PASSWORD'] ? ENV['STOMP_PASSWORD'] : 'guestpw'
21host = ENV['STOMP_HOST'] ? ENV['STOMP_HOST'] : 'localhost'
22port = ENV['STOMP_PORT'] ? ENV['STOMP_PORT'].to_i : 61613
23# //////////////////////////////////////////////////////////////////////////////
24# A hash type connect *MUST* be used to enable callback logging.
25# //////////////////////////////////////////////////////////////////////////////
26hash = { :hosts => [
27 {:login => user, :passcode => password, :host => 'noonehome', :port => 2525},
28 {:login => user, :passcode => password, :host => host, :port => port},
29 ],
30 :logger => mylog, # This enables callback logging!
31 :max_reconnect_attempts => 5,
32 }
33
34# //////////////////////////////////////////////////////////////////////////////
35# For a Connection:
36conn = Stomp::Connection.new(hash)
37conn.disconnect
38# //////////////////////////////////////////////////////////////////////////////
39llog.debug "LE Connection processing complete"
40
41# //////////////////////////////////////////////////////////////////////////////
42# For a Client:
43conn = Stomp::Client.new(hash)
44conn.close
45# //////////////////////////////////////////////////////////////////////////////
46# llog.debug "LE Client processing complete"
47
48# //////////////////////////////////////////////////////////////////////////////
49llog.debug "LE Ending"
50
051
=== added file 'examples/publisher.rb'
--- examples/publisher.rb 1970-01-01 00:00:00 +0000
+++ examples/publisher.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,17 @@
1require 'rubygems'
2require 'stomp'
3
4#client = Stomp::Client.new("", "", "localhost", 61613)
5
6client = Stomp::Client.new("failover://(stomp://:@localhost:61613,stomp://:@remotehost:61613)?initialReconnectDelay=5000&randomize=false&useExponentialBackOff=false")
7message = "ronaldo #{ARGV[0]}"
8
9for i in (1..300)
10 puts "Sending message"
11 client.send("/queue/ronaldo", "#{i}: #{message}", {:persistent => true})
12 puts "(#{Time.now})Message sent: #{i}"
13 sleep 1
14end
15
16
17
018
=== added file 'examples/slogger.rb'
--- examples/slogger.rb 1970-01-01 00:00:00 +0000
+++ examples/slogger.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,100 @@
1=begin
2
3Example STOMP call back logger class.
4
5Optional callback methods:
6
7 on_connecting: connection starting
8 on_connected: successful connect
9 on_connectfail: unsuccessful connect (will usually be retried)
10 on_disconnect: successful disconnect
11
12 on_miscerr: on miscellaneous xmit/recv errors
13
14All methods are optional, at the user's requirements.
15
16If a method is not provided, it is not called (of course.)
17
18IMPORTANT NOTE: call back logging methods *MUST* not raise exceptions,
19otherwise the underlying STOMP connection will fail in mysterious ways.
20
21Callback parameters: are a copy of the @parameters instance variable for
22the Stomp::Connection.
23
24=end
25
26require 'logger' # use the standard Ruby logger .....
27
28class Slogger
29 #
30 def initialize(init_parms = nil)
31 @log = Logger::new(STDOUT) # User preference
32 @log.level = Logger::DEBUG # User preference
33 @log.info("Logger initialization complete.")
34 end
35
36 # Log connecting events
37 def on_connecting(parms)
38 begin
39 @log.debug "Connecting: #{info(parms)}"
40 rescue
41 @log.debug "Connecting oops"
42 end
43 end
44
45 # Log connected events
46 def on_connected(parms)
47 begin
48 @log.debug "Connected: #{info(parms)}"
49 rescue
50 @log.debug "Connected oops"
51 end
52 end
53
54 # Log connectfail events
55 def on_connectfail(parms)
56 begin
57 @log.debug "Connect Fail #{info(parms)}"
58 rescue
59 @log.debug "Connect Fail oops"
60 end
61 end
62
63 # Log disconnect events
64 def on_disconnect(parms)
65 begin
66 @log.debug "Disconnected #{info(parms)}"
67 rescue
68 @log.debug "Disconnected oops"
69 end
70 end
71
72
73 # Log miscellaneous errors
74 def on_miscerr(parms, errstr)
75 begin
76 @log.debug "Miscellaneous Error #{info(parms)}"
77 @log.debug "Miscellaneous Error String #{errstr}"
78 rescue
79 @log.debug "Miscellaneous Error oops"
80 end
81 end
82
83 private
84
85 def info(parms)
86 #
87 # Available in the Hash:
88 # parms[:cur_host]
89 # parms[:cur_port]
90 # parms[:cur_login]
91 # parms[:cur_passcode]
92 # parms[:cur_ssl]
93 # parms[:cur_recondelay]
94 # parms[:cur_parseto]
95 # parms[:cur_conattempts]
96 #
97 "Host: #{parms[:cur_host]}, Port: #{parms[:cur_port]}, Login: Port: #{parms[:cur_login]}, Passcode: #{parms[:cur_passcode]}"
98 end
99end # of class
100
0101
=== removed file 'lib/._stomp.rb'
1Binary files lib/._stomp.rb 2009-01-26 20:38:53 +0000 and lib/._stomp.rb 1970-01-01 00:00:00 +0000 differ102Binary files lib/._stomp.rb 2009-01-26 20:38:53 +0000 and lib/._stomp.rb 1970-01-01 00:00:00 +0000 differ
=== added directory 'lib/stomp'
=== modified file 'lib/stomp.rb'
--- lib/stomp.rb 2010-09-28 00:25:39 +0000
+++ lib/stomp.rb 2011-07-31 16:48:34 +0000
@@ -13,410 +13,13 @@
13# See the License for the specific language governing permissions and13# See the License for the specific language governing permissions and
14# limitations under the License.14# limitations under the License.
1515
16require 'io/wait'16require 'stomp/ext/hash'
17require 'socket'17require 'stomp/connection'
18require 'thread'18require 'stomp/client'
19require 'stomp/message'
20require 'stomp/version'
21require 'stomp/errors'
1922
20module Stomp23module Stomp
21
22 # Low level connection which maps commands and supports
23 # synchronous receives
24 class Connection
25
26 def Connection.open(login = "", passcode = "", host='localhost', port=61613, reliable=FALSE, reconnectDelay=5)
27 Connection.new login, passcode, host, port, reliable, reconnectDelay
28 end
29
30 # Create a connection, requires a login and passcode.
31 # Can accept a host (default is localhost), and port
32 # (default is 61613) to connect to
33 def initialize(login, passcode, host='localhost', port=61613, reliable=false, reconnectDelay=5)
34 @host = host
35 @port = port
36 @login = login
37 @passcode = passcode
38 @transmit_semaphore = Mutex.new
39 @read_semaphore = Mutex.new
40 @socket_semaphore = Mutex.new
41 @reliable = reliable
42 @reconnectDelay = reconnectDelay
43 @closed = FALSE
44 @subscriptions = {}
45 @failure = NIL
46 socket
47 end
48
49 def socket
50 # Need to look into why the following synchronize does not work.
51 #@read_semaphore.synchronize do
52 s = @socket;
53 while s == NIL or @failure != NIL
54 @failure = NIL
55 begin
56 s = TCPSocket.open @host, @port
57 _transmit(s, "CONNECT", {:login => @login, :passcode => @passcode})
58 @connect = _receive(s)
59 # replay any subscriptions.
60 @subscriptions.each { |k,v| _transmit(s, "SUBSCRIBE", v) }
61 rescue
62 @failure = $!;
63 s=NIL;
64 raise unless @reliable
65 $stderr.print "connect failed: " + $! +" will retry in #{@reconnectDelay}\n";
66 sleep(@reconnectDelay);
67 end
68 end
69 @socket = s
70 return s;
71 #end
72 end
73
74 # Is this connection open?
75 def open?
76 !@closed
77 end
78
79 # Is this connection closed?
80 def closed?
81 @closed
82 end
83
84 # Begin a transaction, requires a name for the transaction
85 def begin name, headers={}
86 headers[:transaction] = name
87 transmit "BEGIN", headers
88 end
89
90 # Acknowledge a message, used then a subscription has specified
91 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
92 #
93 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
94 def ack message_id, headers={}
95 headers['message-id'] = message_id
96 transmit "ACK", headers
97 end
98
99 # Commit a transaction by name
100 def commit name, headers={}
101 headers[:transaction] = name
102 transmit "COMMIT", headers
103 end
104
105 # Abort a transaction by name
106 def abort name, headers={}
107 headers[:transaction] = name
108 transmit "ABORT", headers
109 end
110
111 # Subscribe to a destination, must specify a name
112 def subscribe(name, headers = {}, subId=NIL)
113 headers[:destination] = name
114 transmit "SUBSCRIBE", headers
115
116 # Store the sub so that we can replay if we reconnect.
117 if @reliable
118 subId = name if subId==NIL
119 @subscriptions[subId]=headers
120 end
121 end
122
123 # Unsubscribe from a destination, must specify a name
124 def unsubscribe(name, headers = {}, subId=NIL)
125 headers[:destination] = name
126 transmit "UNSUBSCRIBE", headers
127 if @reliable
128 subId = name if subId==NIL
129 @subscriptions.delete(subId)
130 end
131 end
132
133 # Send message to destination
134 #
135 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
136 def send(destination, message, headers={})
137 headers[:destination] = destination
138 transmit "SEND", headers, message
139 end
140
141 # Close this connection
142 def disconnect(headers = {})
143 transmit "DISCONNECT", headers
144 end
145
146 # Return a pending message if one is available, otherwise
147 # return nil
148 def poll
149 @read_semaphore.synchronize do
150 return nil if @socket==NIL or !@socket.ready?
151 return receive
152 end
153 end
154
155 # Receive a frame, block until the frame is received
156 def __old_receive
157 # The recive my fail so we may need to retry.
158 while TRUE
159 begin
160 s = socket
161 return _receive(s)
162 rescue
163 @failure = $!;
164 raise unless @reliable
165 $stderr.print "receive failed: " + $!;
166 end
167 end
168 end
169
170 def receive
171 super_result = __old_receive()
172 if super_result.nil? && @reliable
173 $stderr.print "connection.receive returning EOF as nil - resetting connection.\n"
174 @socket = nil
175 super_result = __old_receive()
176 end
177 return super_result
178 end
179
180 private
181 def _receive( s )
182 line = ' '
183 @read_semaphore.synchronize do
184 line = s.gets while line =~ /^\s*$/
185 return NIL if line == NIL
186 Message.new do |m|
187 m.command = line.chomp
188 m.headers = {}
189 until (line = s.gets.chomp) == ''
190 k = (line.strip[0, line.strip.index(':')]).strip
191 v = (line.strip[line.strip.index(':') + 1, line.strip.length]).strip
192 m.headers[k] = v
193 end
194
195 if (m.headers['content-length'])
196 m.body = s.read m.headers['content-length'].to_i
197 c = RUBY_VERSION > '1.9' ? s.getc.ord : s.getc
198 raise "Invalid content length received" unless c == 0
199 else
200 m.body = ''
201 if RUBY_VERSION > '1.9'
202 until (c = s.getc.ord) == 0
203 m.body << c.chr
204 end
205 else
206 until (c = s.getc) == 0
207 m.body << c.chr
208 end
209 end
210 end
211 #c = s.getc
212 #raise "Invalid frame termination received" unless c == 10
213 end
214 end
215 end
216
217 private
218 def transmit(command, headers={}, body='')
219 # The transmit my fail so we may need to retry.
220 while TRUE
221 begin
222 s = socket
223 _transmit(s, command, headers, body)
224 return
225 rescue
226 @failure = $!;
227 raise unless @reliable
228 $stderr.print "transmit failed: " + $!+"\n";
229 end
230 end
231 end
232
233 private
234 def _transmit(s, command, headers={}, body='')
235 @transmit_semaphore.synchronize do
236 s.puts command
237 headers.each {|k,v| s.puts "#{k}:#{v}" }
238 s.puts "content-length: #{body.length}"
239 s.puts "content-type: text/plain; charset=UTF-8"
240 s.puts
241 s.write body
242 s.write "\0"
243 end
244 end
245 end
246
247 # Container class for frames, misnamed technically
248 class Message
249 attr_accessor :headers, :body, :command
250
251 def initialize
252 yield(self) if block_given?
253 end
254
255 def to_s
256 "<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
257 end
258 end
259
260 # Typical Stomp client class. Uses a listener thread to receive frames
261 # from the server, any thread can send.
262 #
263 # Receives all happen in one thread, so consider not doing much processing
264 # in that thread if you have much message volume.
265 class Client
266
267 # Accepts a username (default ""), password (default ""),
268 # host (default localhost), and port (default 61613)
269 def initialize user="", pass="", host="localhost", port=61613, reliable=false
270 if user =~ /stomp:\/\/(\w+):(\d+)/
271 user = ""
272 pass = ""
273 host = $1
274 port = $2
275 reliable = false
276 elsif user =~ /stomp:\/\/(\w+):(\w+)@(\w+):(\d+)/
277 user = $1
278 pass = $2
279 host = $3
280 port = $4
281 reliable = false
282 end
283
284 @id_mutex = Mutex.new
285 @ids = 1
286 @connection = Connection.open user, pass, host, port, reliable
287 @listeners = {}
288 @receipt_listeners = {}
289 @running = true
290 @replay_messages_by_txn = Hash.new
291 @listener_thread = Thread.start do
292 while @running
293 message = @connection.receive
294 case
295 when message == NIL
296 break
297 when message.command == 'MESSAGE'
298 if listener = @listeners[message.headers['destination']]
299 listener.call(message)
300 end
301 when message.command == 'RECEIPT'
302 if listener = @receipt_listeners[message.headers['receipt-id']]
303 listener.call(message)
304 end
305 end
306 end
307 end
308 end
309
310 # Join the listener thread for this client,
311 # generally used to wait for a quit signal
312 def join
313 @listener_thread.join
314 end
315
316 # Accepts a username (default ""), password (default ""),
317 # host (default localhost), and port (default 61613)
318 def self.open user="", pass="", host="localhost", port=61613, reliable=false
319 Client.new user, pass, host, port, reliable
320 end
321
322 # Begin a transaction by name
323 def begin name, headers={}
324 @connection.begin name, headers
325 end
326
327 # Abort a transaction by name
328 def abort name, headers={}
329 @connection.abort name, headers
330
331 # lets replay any ack'd messages in this transaction
332 replay_list = @replay_messages_by_txn[name]
333 if replay_list
334 replay_list.each do |message|
335 if listener = @listeners[message.headers['destination']]
336 listener.call(message)
337 end
338 end
339 end
340 end
341
342 # Commit a transaction by name
343 def commit name, headers={}
344 txn_id = headers[:transaction]
345 @replay_messages_by_txn.delete(txn_id)
346 @connection.commit name, headers
347 end
348
349 # Subscribe to a destination, must be passed a block
350 # which will be used as a callback listener
351 #
352 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
353 def subscribe destination, headers={}
354 raise "No listener given" unless block_given?
355 @listeners[destination] = lambda {|msg| yield msg}
356 @connection.subscribe destination, headers
357 end
358
359 # Unsubecribe from a channel
360 def unsubscribe name, headers={}
361 @connection.unsubscribe name, headers
362 @listeners[name] = nil
363 end
364
365 # Acknowledge a message, used then a subscription has specified
366 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
367 #
368 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
369 def acknowledge message, headers={}
370 txn_id = headers[:transaction]
371 if txn_id
372 # lets keep around messages ack'd in this transaction in case we rollback
373 replay_list = @replay_messages_by_txn[txn_id]
374 if replay_list == nil
375 replay_list = []
376 @replay_messages_by_txn[txn_id] = replay_list
377 end
378 replay_list << message
379 end
380 if block_given?
381 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
382 end
383 @connection.ack message.headers['message-id'], headers
384 end
385
386 # Send message to destination
387 #
388 # If a block is given a receipt will be requested and passed to the
389 # block on receipt
390 #
391 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
392 def send destination, message, headers = {}
393 if block_given?
394 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
395 end
396 @connection.send destination, message, headers
397 end
398
399 # Is this client open?
400 def open?
401 @connection.open?
402 end
403
404 # Close out resources in use by this client
405 def close
406 @connection.disconnect
407 @running = false
408 end
409
410 private
411 def register_receipt_listener listener
412 id = -1
413 @id_mutex.synchronize do
414 id = @ids.to_s
415 @ids = @ids.succ
416 end
417 @receipt_listeners[id] = listener
418 id
419 end
420
421 end
422end24end
25
42326
=== added file 'lib/stomp/client.rb'
--- lib/stomp/client.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/client.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,340 @@
1require 'thread'
2require 'digest/sha1'
3
4module Stomp
5
6 # Typical Stomp client class. Uses a listener thread to receive frames
7 # from the server, any thread can send.
8 #
9 # Receives all happen in one thread, so consider not doing much processing
10 # in that thread if you have much message volume.
11 class Client
12
13 attr_reader :login, :passcode, :host, :port, :reliable, :parameters
14
15 #alias :obj_send :send
16
17 # A new Client object can be initialized using two forms:
18 #
19 # Standard positional parameters:
20 # login (String, default : '')
21 # passcode (String, default : '')
22 # host (String, default : 'localhost')
23 # port (Integer, default : 61613)
24 # reliable (Boolean, default : false)
25 #
26 # e.g. c = Client.new('login', 'passcode', 'localhost', 61613, true)
27 #
28 # Stomp URL :
29 # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
30 #
31 # stomp://host:port
32 # stomp://host.domain.tld:port
33 # stomp://login:passcode@host:port
34 # stomp://login:passcode@host.domain.tld:port
35 #
36 def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
37
38 # Parse stomp:// URL's or set params
39 if login.is_a?(Hash)
40 @parameters = login
41
42 first_host = @parameters[:hosts][0]
43
44 @login = first_host[:login]
45 @passcode = first_host[:passcode]
46 @host = first_host[:host]
47 @port = first_host[:port] || Connection::default_port(first_host[:ssl])
48
49 @reliable = true
50
51 elsif login =~ /^stomp:\/\/#{url_regex}/ # e.g. stomp://login:passcode@host:port or stomp://host:port
52 @login = $2 || ""
53 @passcode = $3 || ""
54 @host = $4
55 @port = $5.to_i
56 @reliable = false
57 elsif login =~ /^failover:(\/\/)?\(stomp(\+ssl)?:\/\/#{url_regex}(,stomp(\+ssl)?:\/\/#{url_regex}\))+(\?(.*))?$/ # e.g. failover://(stomp://login1:passcode1@localhost:61616,stomp://login2:passcode2@remotehost:61617)?option1=param
58
59 first_host = {}
60 first_host[:ssl] = !$2.nil?
61 @login = first_host[:login] = $4 || ""
62 @passcode = first_host[:passcode] = $5 || ""
63 @host = first_host[:host] = $6
64 @port = first_host[:port] = $7.to_i || Connection::default_port(first_host[:ssl])
65
66 options = $16 || ""
67 parts = options.split(/&|=/)
68 options = Hash[*parts]
69
70 hosts = [first_host] + parse_hosts(login)
71
72 @parameters = {}
73 @parameters[:hosts] = hosts
74
75 @parameters.merge! filter_options(options)
76
77 @reliable = true
78 else
79 @login = login
80 @passcode = passcode
81 @host = host
82 @port = port.to_i
83 @reliable = reliable
84 end
85
86 check_arguments!
87
88 @id_mutex = Mutex.new
89 @ids = 1
90
91 if @parameters
92 @connection = Connection.new(@parameters)
93 else
94 @connection = Connection.new(@login, @passcode, @host, @port, @reliable)
95 end
96
97 start_listeners
98
99 end
100
101 # Syntactic sugar for 'Client.new' See 'initialize' for usage.
102 def self.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false)
103 Client.new(login, passcode, host, port, reliable)
104 end
105
106 # Join the listener thread for this client,
107 # generally used to wait for a quit signal
108 def join(limit = nil)
109 @listener_thread.join(limit)
110 end
111
112 # Begin a transaction by name
113 def begin(name, headers = {})
114 @connection.begin(name, headers)
115 end
116
117 # Abort a transaction by name
118 def abort(name, headers = {})
119 @connection.abort(name, headers)
120
121 # lets replay any ack'd messages in this transaction
122 replay_list = @replay_messages_by_txn[name]
123 if replay_list
124 replay_list.each do |message|
125 if listener = find_listener(message)
126 listener.call(message)
127 end
128 end
129 end
130 end
131
132 # Commit a transaction by name
133 def commit(name, headers = {})
134 txn_id = headers[:transaction]
135 @replay_messages_by_txn.delete(txn_id)
136 @connection.commit(name, headers)
137 end
138
139 # Subscribe to a destination, must be passed a block
140 # which will be used as a callback listener
141 #
142 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
143 def subscribe(destination, headers = {})
144 raise "No listener given" unless block_given?
145 # use subscription id to correlate messages to subscription. As described in
146 # the SUBSCRIPTION section of the protocol: http://stomp.codehaus.org/Protocol.
147 # If no subscription id is provided, generate one.
148 set_subscription_id_if_missing(destination, headers)
149 if @listeners[headers[:id]]
150 raise "attempting to subscribe to a queue with a previous subscription"
151 end
152 @listeners[headers[:id]] = lambda {|msg| yield msg}
153 @connection.subscribe(destination, headers)
154 end
155
156 # Unsubecribe from a channel
157 def unsubscribe(name, headers = {})
158 set_subscription_id_if_missing(name, headers)
159 @connection.unsubscribe(name, headers)
160 @listeners[headers[:id]] = nil
161 end
162
163 # Acknowledge a message, used when a subscription has specified
164 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
165 #
166 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
167 def acknowledge(message, headers = {})
168 txn_id = headers[:transaction]
169 if txn_id
170 # lets keep around messages ack'd in this transaction in case we rollback
171 replay_list = @replay_messages_by_txn[txn_id]
172 if replay_list.nil?
173 replay_list = []
174 @replay_messages_by_txn[txn_id] = replay_list
175 end
176 replay_list << message
177 end
178 if block_given?
179 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
180 end
181 @connection.ack message.headers['message-id'], headers
182 end
183
184 # Unreceive a message, sending it back to its queue or to the DLQ
185 #
186 def unreceive(message, options = {})
187 @connection.unreceive(message, options)
188 end
189
190 # Publishes message to destination
191 #
192 # If a block is given a receipt will be requested and passed to the
193 # block on receipt
194 #
195 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
196 def publish(destination, message, headers = {})
197 if block_given?
198 headers['receipt'] = register_receipt_listener lambda {|r| yield r}
199 end
200 @connection.publish(destination, message, headers)
201 end
202
203 def obj_send(*args)
204 __send__(*args)
205 end
206
207 def send(*args)
208 warn("This method is deprecated and will be removed on the next release. Use 'publish' instead")
209 publish(*args)
210 end
211
212 def connection_frame
213 @connection.connection_frame
214 end
215
216 def disconnect_receipt
217 @connection.disconnect_receipt
218 end
219
220 # Is this client open?
221 def open?
222 @connection.open?
223 end
224
225 # Is this client closed?
226 def closed?
227 @connection.closed?
228 end
229
230 # Close out resources in use by this client
231 def close headers={}
232 @listener_thread.exit
233 @connection.disconnect headers
234 end
235
236 # Check if the thread was created and isn't dead
237 def running
238 @listener_thread && !!@listener_thread.status
239 end
240
241 private
242 # Set a subscription id in the headers hash if one does not already exist.
243 # For simplicities sake, all subscriptions have a subscription ID.
244 # setting an id in the SUBSCRIPTION header is described in the stomp protocol docs:
245 # http://stomp.codehaus.org/Protocol
246 def set_subscription_id_if_missing(destination, headers)
247 headers[:id] = headers[:id] ? headers[:id] : headers['id']
248 if headers[:id] == nil
249 headers[:id] = Digest::SHA1.hexdigest(destination)
250 end
251 end
252
253 def register_receipt_listener(listener)
254 id = -1
255 @id_mutex.synchronize do
256 id = @ids.to_s
257 @ids = @ids.succ
258 end
259 @receipt_listeners[id] = listener
260 id
261 end
262
263 # e.g. login:passcode@host:port or host:port
264 def url_regex
265 '(([\w\.\-]*):(\w*)@)?([\w\.\-]+):(\d+)'
266 end
267
268 def parse_hosts(url)
269 hosts = []
270
271 host_match = /stomp(\+ssl)?:\/\/(([\w\.]*):(\w*)@)?([\w\.]+):(\d+)\)/
272 url.scan(host_match).each do |match|
273 host = {}
274 host[:ssl] = !match[0].nil?
275 host[:login] = match[2] || ""
276 host[:passcode] = match[3] || ""
277 host[:host] = match[4]
278 host[:port] = match[5].to_i
279
280 hosts << host
281 end
282
283 hosts
284 end
285
286 def check_arguments!
287 raise ArgumentError if @host.nil? || @host.empty?
288 raise ArgumentError if @port.nil? || @port == '' || @port < 1 || @port > 65535
289 raise ArgumentError unless @reliable.is_a?(TrueClass) || @reliable.is_a?(FalseClass)
290 end
291
292 def filter_options(options)
293 new_options = {}
294 new_options[:initial_reconnect_delay] = (options["initialReconnectDelay"] || 10).to_f / 1000 # In ms
295 new_options[:max_reconnect_delay] = (options["maxReconnectDelay"] || 30000 ).to_f / 1000 # In ms
296 new_options[:use_exponential_back_off] = !(options["useExponentialBackOff"] == "false") # Default: true
297 new_options[:back_off_multiplier] = (options["backOffMultiplier"] || 2 ).to_i
298 new_options[:max_reconnect_attempts] = (options["maxReconnectAttempts"] || 0 ).to_i
299 new_options[:randomize] = options["randomize"] == "true" # Default: false
300 new_options[:backup] = false # Not implemented yet: I'm using a master X slave solution
301 new_options[:timeout] = -1 # Not implemented yet: a "timeout(5) do ... end" would do the trick, feel free
302
303 new_options
304 end
305
306 def find_listener(message)
307 subscription_id = message.headers['subscription']
308 if subscription_id == nil
309 # For backward compatibility, some messages may already exist with no
310 # subscription id, in which case we can attempt to synthesize one.
311 set_subscription_id_if_missing(message.headers['destination'], message.headers)
312 subscription_id = message.headers['id']
313 end
314 @listeners[subscription_id]
315 end
316
317 def start_listeners
318 @listeners = {}
319 @receipt_listeners = {}
320 @replay_messages_by_txn = {}
321
322 @listener_thread = Thread.start do
323 while true
324 message = @connection.receive
325 if message.command == 'MESSAGE'
326 if listener = find_listener(message)
327 listener.call(message)
328 end
329 elsif message.command == 'RECEIPT'
330 if listener = @receipt_listeners[message.headers['receipt-id']]
331 listener.call(message)
332 end
333 end
334 end
335 end
336
337 end
338 end
339end
340
0341
=== added file 'lib/stomp/connection.rb'
--- lib/stomp/connection.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/connection.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,559 @@
1require 'socket'
2require 'timeout'
3require 'io/wait'
4
5module Stomp
6
7 # Low level connection which maps commands and supports
8 # synchronous receives
9 class Connection
10 attr_reader :connection_frame
11 attr_reader :disconnect_receipt
12 #alias :obj_send :send
13
14 def self.default_port(ssl)
15 ssl ? 61612 : 61613
16 end
17
18 # A new Connection object accepts the following parameters:
19 #
20 # login (String, default : '')
21 # passcode (String, default : '')
22 # host (String, default : 'localhost')
23 # port (Integer, default : 61613)
24 # reliable (Boolean, default : false)
25 # reconnect_delay (Integer, default : 5)
26 #
27 # e.g. c = Connection.new("username", "password", "localhost", 61613, true)
28 #
29 # Hash:
30 #
31 # hash = {
32 # :hosts => [
33 # {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
34 # {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
35 # ],
36 # :initial_reconnect_delay => 0.01,
37 # :max_reconnect_delay => 30.0,
38 # :use_exponential_back_off => true,
39 # :back_off_multiplier => 2,
40 # :max_reconnect_attempts => 0,
41 # :randomize => false,
42 # :backup => false,
43 # :timeout => -1,
44 # :connect_headers => {},
45 # :parse_timeout => 5,
46 # :logger => nil,
47 # }
48 #
49 # e.g. c = Connection.new(hash)
50 #
51 # TODO
52 # Stomp URL :
53 # A Stomp URL must begin with 'stomp://' and can be in one of the following forms:
54 #
55 # stomp://host:port
56 # stomp://host.domain.tld:port
57 # stomp://user:pass@host:port
58 # stomp://user:pass@host.domain.tld:port
59 #
60 def initialize(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
61 @received_messages = []
62
63 if login.is_a?(Hash)
64 hashed_initialize(login)
65 else
66 @host = host
67 @port = port
68 @login = login
69 @passcode = passcode
70 @reliable = reliable
71 @reconnect_delay = reconnect_delay
72 @connect_headers = connect_headers
73 @ssl = false
74 @parameters = nil
75 @parse_timeout = 5 # To override, use hashed parameters
76 @logger = nil # To override, use hashed parameters
77 end
78
79 # Use Mutexes: only one lock per each thread
80 # Revert to original implementation attempt
81 @transmit_semaphore = Mutex.new
82 @read_semaphore = Mutex.new
83 @socket_semaphore = Mutex.new
84
85 @subscriptions = {}
86 @failure = nil
87 @connection_attempts = 0
88
89 socket
90 end
91
92 def hashed_initialize(params)
93
94 @parameters = refine_params(params)
95 @reliable = true
96 @reconnect_delay = @parameters[:initial_reconnect_delay]
97 @connect_headers = @parameters[:connect_headers]
98 @parse_timeout = @parameters[:parse_timeout]
99 @logger = @parameters[:logger]
100 #sets the first host to connect
101 change_host
102 if @logger && @logger.respond_to?(:on_connecting)
103 @logger.on_connecting(log_params)
104 end
105 end
106
107 # Syntactic sugar for 'Connection.new' See 'initialize' for usage.
108 def Connection.open(login = '', passcode = '', host = 'localhost', port = 61613, reliable = false, reconnect_delay = 5, connect_headers = {})
109 Connection.new(login, passcode, host, port, reliable, reconnect_delay, connect_headers)
110 end
111
112 def socket
113 @socket_semaphore.synchronize do
114 used_socket = @socket
115 used_socket = nil if closed?
116
117 while used_socket.nil? || !@failure.nil?
118 @failure = nil
119 begin
120 used_socket = open_socket
121 # Open complete
122
123 connect(used_socket)
124 if @logger && @logger.respond_to?(:on_connected)
125 @logger.on_connected(log_params)
126 end
127 @connection_attempts = 0
128 rescue
129 @failure = $!
130 used_socket = nil
131 raise unless @reliable
132 if @logger && @logger.respond_to?(:on_connectfail)
133 @logger.on_connectfail(log_params)
134 else
135 $stderr.print "connect to #{@host} failed: #{$!} will retry(##{@connection_attempts}) in #{@reconnect_delay}\n"
136 end
137 raise Stomp::Error::MaxReconnectAttempts if max_reconnect_attempts?
138
139 sleep(@reconnect_delay)
140
141 @connection_attempts += 1
142
143 if @parameters
144 change_host
145 increase_reconnect_delay
146 end
147 end
148 end
149 @socket = used_socket
150 end
151 end
152
153 def refine_params(params)
154 params = params.uncamelize_and_symbolize_keys
155
156 default_params = {
157 :connect_headers => {},
158 # Failover parameters
159 :initial_reconnect_delay => 0.01,
160 :max_reconnect_delay => 30.0,
161 :use_exponential_back_off => true,
162 :back_off_multiplier => 2,
163 :max_reconnect_attempts => 0,
164 :randomize => false,
165 :backup => false,
166 :timeout => -1,
167 # Parse Timeout
168 :parse_timeout => 5
169 }
170
171 default_params.merge(params)
172
173 end
174
175 def change_host
176 @parameters[:hosts] = @parameters[:hosts].sort_by { rand } if @parameters[:randomize]
177
178 # Set first as master and send it to the end of array
179 current_host = @parameters[:hosts].shift
180 @parameters[:hosts] << current_host
181
182 @ssl = current_host[:ssl]
183 @host = current_host[:host]
184 @port = current_host[:port] || Connection::default_port(@ssl)
185 @login = current_host[:login] || ""
186 @passcode = current_host[:passcode] || ""
187
188 end
189
190 def max_reconnect_attempts?
191 !(@parameters.nil? || @parameters[:max_reconnect_attempts].nil?) && @parameters[:max_reconnect_attempts] != 0 && @connection_attempts >= @parameters[:max_reconnect_attempts]
192 end
193
194 def increase_reconnect_delay
195
196 @reconnect_delay *= @parameters[:back_off_multiplier] if @parameters[:use_exponential_back_off]
197 @reconnect_delay = @parameters[:max_reconnect_delay] if @reconnect_delay > @parameters[:max_reconnect_delay]
198
199 @reconnect_delay
200 end
201
202 # Is this connection open?
203 def open?
204 !@closed
205 end
206
207 # Is this connection closed?
208 def closed?
209 @closed
210 end
211
212 # Begin a transaction, requires a name for the transaction
213 def begin(name, headers = {})
214 headers[:transaction] = name
215 transmit("BEGIN", headers)
216 end
217
218 # Acknowledge a message, used when a subscription has specified
219 # client acknowledgement ( connection.subscribe "/queue/a", :ack => 'client'g
220 #
221 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
222 def ack(message_id, headers = {})
223 headers['message-id'] = message_id
224 transmit("ACK", headers)
225 end
226
227 # Commit a transaction by name
228 def commit(name, headers = {})
229 headers[:transaction] = name
230 transmit("COMMIT", headers)
231 end
232
233 # Abort a transaction by name
234 def abort(name, headers = {})
235 headers[:transaction] = name
236 transmit("ABORT", headers)
237 end
238
239 # Subscribe to a destination, must specify a name
240 def subscribe(name, headers = {}, subId = nil)
241 headers[:destination] = name
242 transmit("SUBSCRIBE", headers)
243
244 # Store the sub so that we can replay if we reconnect.
245 if @reliable
246 subId = name if subId.nil?
247 @subscriptions[subId] = headers
248 end
249 end
250
251 # Unsubscribe from a destination, must specify a name
252 def unsubscribe(name, headers = {}, subId = nil)
253 headers[:destination] = name
254 transmit("UNSUBSCRIBE", headers)
255 if @reliable
256 subId = name if subId.nil?
257 @subscriptions.delete(subId)
258 end
259 end
260
261 # Publish message to destination
262 #
263 # To disable content length header ( :suppress_content_length => true )
264 # Accepts a transaction header ( :transaction => 'some_transaction_id' )
265 def publish(destination, message, headers = {})
266 headers[:destination] = destination
267 transmit("SEND", headers, message)
268 end
269
270 def obj_send(*args)
271 __send__(*args)
272 end
273
274 def send(*args)
275 warn("This method is deprecated and will be removed on the next release. Use 'publish' instead")
276 publish(*args)
277 end
278
279 # Send a message back to the source or to the dead letter queue
280 #
281 # Accepts a dead letter queue option ( :dead_letter_queue => "/queue/DLQ" )
282 # Accepts a limit number of redeliveries option ( :max_redeliveries => 6 )
283 # Accepts a force client acknowledgement option (:force_client_ack => true)
284 def unreceive(message, options = {})
285 options = { :dead_letter_queue => "/queue/DLQ", :max_redeliveries => 6 }.merge options
286 # Lets make sure all keys are symbols
287 message.headers = message.headers.symbolize_keys
288
289 retry_count = message.headers[:retry_count].to_i || 0
290 message.headers[:retry_count] = retry_count + 1
291 transaction_id = "transaction-#{message.headers[:'message-id']}-#{retry_count}"
292 message_id = message.headers.delete(:'message-id')
293
294 begin
295 self.begin transaction_id
296
297 if client_ack?(message) || options[:force_client_ack]
298 self.ack(message_id, :transaction => transaction_id)
299 end
300
301 if retry_count <= options[:max_redeliveries]
302 self.publish(message.headers[:destination], message.body, message.headers.merge(:transaction => transaction_id))
303 else
304 # Poison ack, sending the message to the DLQ
305 self.publish(options[:dead_letter_queue], message.body, message.headers.merge(:transaction => transaction_id, :original_destination => message.headers[:destination], :persistent => true))
306 end
307 self.commit transaction_id
308 rescue Exception => exception
309 self.abort transaction_id
310 raise exception
311 end
312 end
313
314 def client_ack?(message)
315 headers = @subscriptions[message.headers[:destination]]
316 !headers.nil? && headers[:ack] == "client"
317 end
318
319 # Close this connection
320 def disconnect(headers = {})
321 transmit("DISCONNECT", headers)
322 headers = headers.symbolize_keys
323 @disconnect_receipt = receive if headers[:receipt]
324 if @logger && @logger.respond_to?(:on_disconnect)
325 @logger.on_disconnect(log_params)
326 end
327 close_socket
328 end
329
330 # Return a pending message if one is available, otherwise
331 # return nil
332 def poll
333 # No need for a read lock here. The receive method eventually fullfills
334 # that requirement.
335 return nil if @socket.nil? || !@socket.ready?
336 receive
337 end
338
339 # Receive a frame, block until the frame is received
340 def __old_receive
341 # The recive my fail so we may need to retry.
342 while TRUE
343 begin
344 used_socket = socket
345 return _receive(used_socket)
346 rescue
347 @failure = $!
348 raise unless @reliable
349 errstr = "receive failed: #{$!}"
350 if @logger && @logger.respond_to?(:on_miscerr)
351 @logger.on_miscerr(log_params, errstr)
352 else
353 $stderr.print errstr
354 end
355 end
356 end
357 end
358
359 def receive
360 super_result = __old_receive
361 if super_result.nil? && @reliable
362 errstr = "connection.receive returning EOF as nil - resetting connection.\n"
363 if @logger && @logger.respond_to?(:on_miscerr)
364 @logger.on_miscerr(log_params, errstr)
365 else
366 $stderr.print errstr
367 end
368 @socket = nil
369 super_result = __old_receive
370 end
371 return super_result
372 end
373
374 private
375
376 def _receive( read_socket )
377 @read_semaphore.synchronize do
378 line = read_socket.gets
379
380 return nil if line.nil?
381
382 # If the reading hangs for more than X seconds, abort the parsing process.
383 # X defaults to 5. Override allowed in connection hash parameters.
384 Timeout::timeout(@parse_timeout, Stomp::Error::PacketParsingTimeout) do
385 # Reads the beginning of the message until it runs into a empty line
386 message_header = ''
387 begin
388 message_header += line
389 line = read_socket.gets
390 end until line =~ /^\s?\n$/
391
392 # Checks if it includes content_length header
393 content_length = message_header.match /content-length\s?:\s?(\d+)\s?\n/
394 message_body = ''
395
396 # If it does, reads the specified amount of bytes
397 char = ''
398 if content_length
399 message_body = read_socket.read content_length[1].to_i
400 raise Stomp::Error::InvalidMessageLength unless parse_char(read_socket.getc) == "\0"
401 # Else reads, the rest of the message until the first \0
402 else
403 message_body += char while (char = parse_char(read_socket.getc)) != "\0"
404 end
405
406 # If the buffer isn't empty, reads trailing new lines.
407 # Note: experiments with JRuby seem to show that .ready? never
408 # returns true. This means that this code to drain trailing new
409 # lines never runs using JRuby.
410 while read_socket.ready?
411 last_char = read_socket.getc
412 break unless last_char
413 if parse_char(last_char) != "\n"
414 read_socket.ungetc(last_char)
415 break
416 end
417 end
418 # And so, a JRuby hack. Remove any new lines at the start of the
419 # next buffer.
420 message_header.gsub!(/^\n?/, "")
421
422 # Adds the excluded \n and \0 and tries to create a new message with it
423 Message.new(message_header + "\n" + message_body + "\0")
424 end
425 end
426 end
427
428 def parse_char(char)
429 RUBY_VERSION > '1.9' ? char : char.chr
430 end
431
432 def transmit(command, headers = {}, body = '')
433 # The transmit may fail so we may need to retry.
434 while TRUE
435 begin
436 used_socket = socket
437 _transmit(used_socket, command, headers, body)
438 return
439 rescue Stomp::Error::MaxReconnectAttempts => e
440 raise
441 rescue
442 @failure = $!
443 raise unless @reliable
444 errstr = "transmit to #{@host} failed: #{$!}\n"
445 if @logger && @logger.respond_to?(:on_miscerr)
446 @logger.on_miscerr(log_params, errstr)
447 else
448 $stderr.print errstr
449 end
450 end
451 end
452 end
453
454 def _transmit(used_socket, command, headers = {}, body = '')
455 @transmit_semaphore.synchronize do
456 # Handle nil body
457 body = '' if body.nil?
458 # The content-length should be expressed in bytes.
459 # Ruby 1.8: String#length => # of bytes; Ruby 1.9: String#length => # of characters
460 # With Unicode strings, # of bytes != # of characters. So, use String#bytesize when available.
461 body_length_bytes = body.respond_to?(:bytesize) ? body.bytesize : body.length
462
463 # ActiveMQ interprets every message as a BinaryMessage
464 # if content_length header is included.
465 # Using :suppress_content_length => true will suppress this behaviour
466 # and ActiveMQ will interpret the message as a TextMessage.
467 # For more information refer to http://juretta.com/log/2009/05/24/activemq-jms-stomp/
468 # Lets send this header in the message, so it can maintain state when using unreceive
469 headers['content-length'] = "#{body_length_bytes}" unless headers[:suppress_content_length]
470
471 used_socket.puts command
472 headers.each {|k,v| used_socket.puts "#{k}:#{v}" }
473 used_socket.puts "content-type: text/plain; charset=UTF-8"
474 used_socket.puts
475 used_socket.write body
476 used_socket.write "\0"
477 end
478 end
479
480 def open_tcp_socket
481 tcp_socket = TCPSocket.open @host, @port
482
483 tcp_socket
484 end
485
486 def open_ssl_socket
487 require 'openssl' unless defined?(OpenSSL)
488 ctx = OpenSSL::SSL::SSLContext.new
489
490 # For client certificate authentication:
491 # key_path = ENV["STOMP_KEY_PATH"] || "~/stomp_keys"
492 # ctx.cert = OpenSSL::X509::Certificate.new("#{key_path}/client.cer")
493 # ctx.key = OpenSSL::PKey::RSA.new("#{key_path}/client.keystore")
494
495 # For server certificate authentication:
496 # truststores = OpenSSL::X509::Store.new
497 # truststores.add_file("#{key_path}/client.ts")
498 # ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
499 # ctx.cert_store = truststores
500
501 ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE
502
503 ssl = OpenSSL::SSL::SSLSocket.new(open_tcp_socket, ctx)
504 def ssl.ready?
505 ! @rbuffer.empty? || @io.ready?
506 end
507 ssl.connect
508 ssl
509 end
510
511 def close_socket
512 begin
513 @socket.close
514 rescue
515 #Ignoring if already closed
516 end
517
518 @closed = true
519 end
520
521 def open_socket
522 used_socket = @ssl ? open_ssl_socket : open_tcp_socket
523 # try to close the old connection if any
524 close_socket
525
526 @closed = false
527 # Use keepalive
528 used_socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true)
529 used_socket
530 end
531
532 def connect(used_socket)
533 headers = @connect_headers.clone
534 headers[:login] = @login
535 headers[:passcode] = @passcode
536 _transmit(used_socket, "CONNECT", headers)
537 @connection_frame = _receive(used_socket)
538 @disconnect_receipt = nil
539 # replay any subscriptions.
540 @subscriptions.each { |k,v| _transmit(used_socket, "SUBSCRIBE", v) }
541 end
542
543 def log_params
544 lparms = @parameters.clone
545 lparms[:cur_host] = @host
546 lparms[:cur_port] = @port
547 lparms[:cur_login] = @login
548 lparms[:cur_passcode] = @passcode
549 lparms[:cur_ssl] = @ssl
550 lparms[:cur_recondelay] = @reconnect_delay
551 lparms[:cur_parseto] = @parse_timeout
552 lparms[:cur_conattempts] = @connection_attempts
553 #
554 lparms
555 end
556 end
557
558end
559
0560
=== added file 'lib/stomp/errors.rb'
--- lib/stomp/errors.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/errors.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,33 @@
1module Stomp
2 module Error
3 class InvalidFormat < RuntimeError
4 def message
5 "Invalid message - invalid format"
6 end
7 end
8
9 class InvalidServerCommand < RuntimeError
10 def message
11 "Invalid command from server"
12 end
13 end
14
15 class InvalidMessageLength < RuntimeError
16 def message
17 "Invalid content length received"
18 end
19 end
20
21 class PacketParsingTimeout < RuntimeError
22 def message
23 "Packet parsing timeout"
24 end
25 end
26
27 class MaxReconnectAttempts < RuntimeError
28 def message
29 "Maximum number of reconnection attempts reached"
30 end
31 end
32 end
33end
034
=== added directory 'lib/stomp/ext'
=== added file 'lib/stomp/ext/hash.rb'
--- lib/stomp/ext/hash.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/ext/hash.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,24 @@
1class ::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.method_defined?(:symbolize_keys)
24end
0\ No newline at end of file25\ No newline at end of file
126
=== added file 'lib/stomp/message.rb'
--- lib/stomp/message.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/message.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,68 @@
1module Stomp
2
3 # Container class for frames, misnamed technically
4 class Message
5 attr_accessor :command, :headers, :body, :original
6
7 @@allowed_commands = [ 'CONNECTED', 'MESSAGE', 'RECEIPT', 'ERROR' ]
8
9 def initialize(frame)
10 # p frame
11 # Set default empty values
12 self.command = ''
13 self.headers = {}
14 self.body = ''
15 self.original = frame
16 return self if is_blank?(frame)
17 # Figure out where individual parts of the frame begin and end.
18 command_index = frame.index("\n")
19 raise Stomp::Error::InvalidFormat, 'command index' unless command_index
20 #
21 headers_index = frame.index("\n\n", command_index+1)
22 raise Stomp::Error::InvalidFormat, 'headers index' unless headers_index
23 #
24 lastnull_index = frame.rindex("\0")
25 raise Stomp::Error::InvalidFormat, 'lastnull index' unless lastnull_index
26
27 # Extract working copies of each frame part
28 work_command = frame[0..command_index-1]
29 raise Stomp::Error::InvalidServerCommand, "invalid command: #{work_command.inspect}" unless @@allowed_commands.include?(work_command)
30 #
31 work_headers = frame[command_index+1..headers_index-1]
32 raise Stomp::Error::InvalidFormat, 'nil headers' unless work_headers
33 #
34 work_body = frame[headers_index+2..lastnull_index-1]
35 raise Stomp::Error::InvalidFormat, 'nil body' unless work_body
36 # Set the frame values
37 self.command = work_command
38 work_headers.split("\n").map do |value|
39 parsed_value = value.match /^([\w|-]*):(.*)$/
40 raise Stomp::Error::InvalidFormat, 'parsed header value' unless parsed_value
41 self.headers[parsed_value[1].strip] = parsed_value[2].strip if parsed_value
42 end
43
44 body_length = -1
45
46 if self.headers['content-length']
47 body_length = self.headers['content-length'].to_i
48 raise Stomp::Error::InvalidMessageLength if work_body.length != body_length
49 end
50 self.body = work_body[0..body_length]
51 end
52
53 def to_s
54 "<Stomp::Message headers=#{headers.inspect} body='#{body}' command='#{command}' >"
55 end
56
57 def empty?
58 is_blank?(command) && is_blank?(headers) && is_blank?(body)
59 end
60
61 private
62 def is_blank?(value)
63 value.nil? || (value.respond_to?(:empty?) && value.empty?)
64 end
65 end
66
67end
68
069
=== added file 'lib/stomp/version.rb'
--- lib/stomp/version.rb 1970-01-01 00:00:00 +0000
+++ lib/stomp/version.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,8 @@
1module Stomp
2 module Version #:nodoc: all
3 MAJOR = 1
4 MINOR = 1
5 PATCH = 9
6 STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
7 end
8end
09
=== added directory 'spec'
=== added file 'spec/client_shared_examples.rb'
--- spec/client_shared_examples.rb 1970-01-01 00:00:00 +0000
+++ spec/client_shared_examples.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,69 @@
1require 'spec_helper'
2
3shared_examples_for "standard Client" do
4
5 before(:each) do
6 @destination = "/queue/test/ruby/client"
7 @message_text = "test_client-#{Time.now.to_i}"
8 end
9
10 describe "the closed? method" do
11 it "should be false when the connection is open" do
12 @mock_connection.stub!(:closed?).and_return(false)
13 @client.closed?.should == false
14 end
15
16 it "should be true when the connection is closed" do
17 @mock_connection.stub!(:closed?).and_return(true)
18 @client.closed?.should == true
19 end
20 end
21
22 describe "the open? method" do
23 it "should be true when the connection is open" do
24 @mock_connection.stub!(:open?).and_return(true)
25 @client.open?.should == true
26 end
27
28 it "should be false when the connection is closed" do
29 @mock_connection.stub!(:open?).and_return(false)
30 @client.open?.should == false
31 end
32 end
33
34 describe "the subscribe method" do
35
36 before(:each) do
37 @mock_connection.stub!(:subscribe).and_return(true)
38 end
39
40 it "should raise RuntimeError if not passed a block" do
41 lambda {
42 @client.subscribe(@destination)
43 }.should raise_error
44 end
45
46 it "should not raise an error when passed a block" do
47 lambda {
48 @client.subscribe(@destination) {|msg| received = msg}
49 }.should_not raise_error
50 end
51
52 it "should raise RuntimeError on duplicate subscriptions" do
53 lambda {
54 @client.subscribe(@destination)
55 @client.subscribe(@destination)
56 }.should raise_error
57 end
58
59 it "should raise RuntimeError with duplicate id headers" do
60 lambda {
61 @client.subscribe(@destination, {'id' => 'abcdef'})
62 @client.subscribe(@destination, {'id' => 'abcdef'})
63 }.should raise_error
64 end
65
66 end
67
68end
69
070
=== added file 'spec/client_spec.rb'
--- spec/client_spec.rb 1970-01-01 00:00:00 +0000
+++ spec/client_spec.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,312 @@
1require 'spec_helper'
2require 'client_shared_examples'
3
4
5describe Stomp::Client do
6
7 before(:each) do
8 @mock_connection = mock('connection')
9 Stomp::Connection.stub!(:new).and_return(@mock_connection)
10 end
11
12 describe "(created with no params)" do
13
14 before(:each) do
15 @client = Stomp::Client.new
16 end
17
18 it "should not return any errors" do
19 lambda {
20 @client = Stomp::Client.new
21 }.should_not raise_error
22 end
23
24 it "should not return any errors when created with the open constructor" do
25 lambda {
26 @client = Stomp::Client.open
27 }.should_not raise_error
28 end
29
30 it_should_behave_like "standard Client"
31
32 end
33
34 describe "(created with invalid params)" do
35
36 it "should return ArgumentError if host is nil" do
37 lambda {
38 @client = Stomp::Client.new('login', 'passcode', nil)
39 }.should raise_error
40 end
41
42 it "should return ArgumentError if host is empty" do
43 lambda {
44 @client = Stomp::Client.new('login', 'passcode', '')
45 }.should raise_error
46 end
47
48 it "should return ArgumentError if port is nil" do
49 lambda {
50 @client = Stomp::Client.new('login', 'passcode', 'localhost', nil)
51 }.should raise_error
52 end
53
54 it "should return ArgumentError if port is < 1" do
55 lambda {
56 @client = Stomp::Client.new('login', 'passcode', 'localhost', 0)
57 }.should raise_error
58 end
59
60 it "should return ArgumentError if port is > 65535" do
61 lambda {
62 @client = Stomp::Client.new('login', 'passcode', 'localhost', 65536)
63 }.should raise_error
64 end
65
66 it "should return ArgumentError if port is empty" do
67 lambda {
68 @client = Stomp::Client.new('login', 'passcode', 'localhost', '')
69 }.should raise_error
70 end
71
72 it "should return ArgumentError if reliable is something other than true or false" do
73 lambda {
74 @client = Stomp::Client.new('login', 'passcode', 'localhost', '12345', 'foo')
75 }.should raise_error
76 end
77
78 end
79
80
81 describe "(created with positional params)" do
82
83 before(:each) do
84 @client = Stomp::Client.new('testlogin', 'testpassword', 'localhost', '12345', false)
85 end
86
87 it "should properly parse the URL provided" do
88 @client.login.should eql('testlogin')
89 @client.passcode.should eql('testpassword')
90 @client.host.should eql('localhost')
91 @client.port.should eql(12345)
92 @client.reliable.should be_false
93 end
94
95 it_should_behave_like "standard Client"
96
97 end
98
99 describe "(created with non-authenticating stomp:// URL and non-TLD host)" do
100
101 before(:each) do
102 @client = Stomp::Client.new('stomp://foobar:12345')
103 end
104
105 it "should properly parse the URL provided" do
106 @client.login.should eql('')
107 @client.passcode.should eql('')
108 @client.host.should eql('foobar')
109 @client.port.should eql(12345)
110 @client.reliable.should be_false
111 end
112
113 it_should_behave_like "standard Client"
114
115 end
116
117 describe "(created with non-authenticating stomp:// URL and a host with a '-')" do
118
119 before(:each) do
120 @client = Stomp::Client.new('stomp://foo-bar:12345')
121 end
122
123 it "should properly parse the URL provided" do
124 @client.login.should eql('')
125 @client.passcode.should eql('')
126 @client.host.should eql('foo-bar')
127 @client.port.should eql(12345)
128 @client.reliable.should be_false
129 end
130
131 it_should_behave_like "standard Client"
132
133 end
134
135 describe "(created with authenticating stomp:// URL and non-TLD host)" do
136
137 before(:each) do
138 @client = Stomp::Client.new('stomp://test-login:testpasscode@foobar:12345')
139 end
140
141 it "should properly parse the URL provided" do
142 @client.login.should eql('test-login')
143 @client.passcode.should eql('testpasscode')
144 @client.host.should eql('foobar')
145 @client.port.should eql(12345)
146 @client.reliable.should be_false
147 end
148
149 it_should_behave_like "standard Client"
150
151 end
152
153 describe "(created with authenticating stomp:// URL and a host with a '-')" do
154
155 before(:each) do
156 @client = Stomp::Client.new('stomp://test-login:testpasscode@foo-bar:12345')
157 end
158
159 it "should properly parse the URL provided" do
160 @client.login.should eql('test-login')
161 @client.passcode.should eql('testpasscode')
162 @client.host.should eql('foo-bar')
163 @client.port.should eql(12345)
164 @client.reliable.should be_false
165 end
166
167 it_should_behave_like "standard Client"
168
169 end
170
171 describe "(created with non-authenticating stomp:// URL and TLD host)" do
172
173 before(:each) do
174 @client = Stomp::Client.new('stomp://host.foobar.com:12345')
175 end
176
177 after(:each) do
178 end
179
180 it "should properly parse the URL provided" do
181 @client.login.should eql('')
182 @client.passcode.should eql('')
183 @client.host.should eql('host.foobar.com')
184 @client.port.should eql(12345)
185 @client.reliable.should be_false
186 end
187
188 it_should_behave_like "standard Client"
189
190 end
191
192 describe "(created with authenticating stomp:// URL and non-TLD host)" do
193
194 before(:each) do
195 @client = Stomp::Client.new('stomp://testlogin:testpasscode@host.foobar.com:12345')
196 end
197
198 it "should properly parse the URL provided" do
199 @client.login.should eql('testlogin')
200 @client.passcode.should eql('testpasscode')
201 @client.host.should eql('host.foobar.com')
202 @client.port.should eql(12345)
203 @client.reliable.should be_false
204 end
205
206 it_should_behave_like "standard Client"
207
208 end
209
210 describe "(created with failover URL)" do
211 before(:each) do
212 #default options
213 @parameters = {
214 :initial_reconnect_delay => 0.01,
215 :max_reconnect_delay => 30.0,
216 :use_exponential_back_off => true,
217 :back_off_multiplier => 2,
218 :max_reconnect_attempts => 0,
219 :randomize => false,
220 :backup => false,
221 :timeout => -1
222 }
223 end
224 it "should properly parse a URL with failover://" do
225 url = "failover://(stomp://login1:passcode1@localhost:61616,stomp://login2:passcode2@remotehost:61617)"
226
227 @parameters[:hosts] = [
228 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
229 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
230 ]
231
232 Stomp::Connection.should_receive(:new).with(@parameters)
233
234 client = Stomp::Client.new(url)
235 client.parameters.should == @parameters
236 end
237
238 it "should properly parse a URL with failover:" do
239 url = "failover:(stomp://login1:passcode1@localhost:61616,stomp://login2:passcode2@remotehost1:61617),stomp://login3:passcode3@remotehost2:61618)"
240
241 @parameters[:hosts] = [
242 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
243 {:login => "login2", :passcode => "passcode2", :host => "remotehost1", :port => 61617, :ssl => false},
244 {:login => "login3", :passcode => "passcode3", :host => "remotehost2", :port => 61618, :ssl => false}
245 ]
246
247 Stomp::Connection.should_receive(:new).with(@parameters)
248
249 client = Stomp::Client.new(url)
250 client.parameters.should == @parameters
251 end
252
253 it "should properly parse a URL without user and password" do
254 url = "failover:(stomp://localhost:61616,stomp://remotehost:61617)"
255
256 @parameters[:hosts] = [
257 {:login => "", :passcode => "", :host => "localhost", :port => 61616, :ssl => false},
258 {:login => "", :passcode => "", :host => "remotehost", :port => 61617, :ssl => false}
259 ]
260
261 Stomp::Connection.should_receive(:new).with(@parameters)
262
263 client = Stomp::Client.new(url)
264 client.parameters.should == @parameters
265 end
266
267 it "should properly parse a URL with user and/or password blank" do
268 url = "failover:(stomp://:@localhost:61616,stomp://:@remotehost:61617)"
269
270 @parameters[:hosts] = [
271 {:login => "", :passcode => "", :host => "localhost", :port => 61616, :ssl => false},
272 {:login => "", :passcode => "", :host => "remotehost", :port => 61617, :ssl => false}
273 ]
274
275 Stomp::Connection.should_receive(:new).with(@parameters)
276
277 client = Stomp::Client.new(url)
278 client.parameters.should == @parameters
279 end
280
281 it "should properly parse a URL with the options query" do
282 query = "initialReconnectDelay=5000&maxReconnectDelay=60000&useExponentialBackOff=false&backOffMultiplier=3"
283 query += "&maxReconnectAttempts=4&randomize=true&backup=true&timeout=10000"
284
285 url = "failover:(stomp://login1:passcode1@localhost:61616,stomp://login2:passcode2@remotehost:61617)?#{query}"
286
287 #backup and timeout are not implemented yet
288 @parameters = {
289 :initial_reconnect_delay => 5.0,
290 :max_reconnect_delay => 60.0,
291 :use_exponential_back_off => false,
292 :back_off_multiplier => 3,
293 :max_reconnect_attempts => 4,
294 :randomize => true,
295 :backup => false,
296 :timeout => -1
297 }
298
299 @parameters[:hosts] = [
300 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
301 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
302 ]
303
304 Stomp::Connection.should_receive(:new).with(@parameters)
305
306 client = Stomp::Client.new(url)
307 client.parameters.should == @parameters
308 end
309
310 end
311
312end
0313
=== added file 'spec/connection_spec.rb'
--- spec/connection_spec.rb 1970-01-01 00:00:00 +0000
+++ spec/connection_spec.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,365 @@
1# encoding: UTF-8
2require 'spec_helper'
3
4describe Stomp::Connection do
5
6 before(:each) do
7 @parameters = {
8 :hosts => [
9 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
10 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
11 ],
12 :initial_reconnect_delay => 0.01,
13 :max_reconnect_delay => 30.0,
14 :use_exponential_back_off => true,
15 :back_off_multiplier => 2,
16 :max_reconnect_attempts => 0,
17 :randomize => false,
18 :backup => false,
19 :timeout => -1,
20 :parse_timeout => 5,
21 :connect_headers => {}
22 }
23
24 #POG:
25 class Stomp::Connection
26 def _receive( s )
27 end
28 end
29
30 # clone() does a shallow copy, we want a deep one so we can garantee the hosts order
31 normal_parameters = Marshal::load(Marshal::dump(@parameters))
32
33 @tcp_socket = mock(:tcp_socket, :close => nil, :puts => nil, :write => nil, :setsockopt => nil)
34 TCPSocket.stub!(:open).and_return @tcp_socket
35 @connection = Stomp::Connection.new(normal_parameters)
36 end
37
38 describe "(created using a hash)" do
39 it "should uncamelize and symbolize the main hash keys" do
40 used_hash = {
41 "hosts" => [
42 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false},
43 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false}
44 ],
45 "initialReconnectDelay" => 0.01,
46 "maxReconnectDelay" => 30.0,
47 "useExponentialBackOff" => true,
48 "backOffMultiplier" => 2,
49 "maxReconnectAttempts" => 0,
50 "randomize" => false,
51 "backup" => false,
52 "timeout" => -1,
53 "parse_timeout" => 5,
54 }
55
56 @connection = Stomp::Connection.new(used_hash)
57 @connection.instance_variable_get(:@parameters).should == @parameters
58 end
59
60 it "should be reliable" do
61 @connection.instance_variable_get(:@reliable).should be_true
62 end
63 it "should start with first host in array" do
64 @connection.instance_variable_get(:@host).should == "localhost"
65 end
66
67 it "should change host to next one with randomize false" do
68 @connection.change_host
69 @connection.instance_variable_get(:@host).should == "remotehost"
70 end
71
72 it "should use default port (61613) if none is given" do
73 hash = {:hosts => [{:login => "login2", :passcode => "passcode2", :host => "remotehost", :ssl => false}]}
74 @connection = Stomp::Connection.new hash
75 @connection.instance_variable_get(:@port).should == 61613
76 end
77
78 context "when dealing with content-length header" do
79 it "should not suppress it when receiving :suppress_content_length => false" do
80 @tcp_socket.should_receive(:puts).with("content-length:7")
81 @connection.publish "/queue", "message", :suppress_content_length => false
82 end
83
84 it "should not suppress it when :suppress_content_length is nil" do
85 @tcp_socket.should_receive(:puts).with("content-length:7")
86 @connection.publish "/queue", "message"
87 end
88
89 it "should suppress it when receiving :suppress_content_length => true" do
90 @tcp_socket.should_not_receive(:puts).with("content-length:7")
91 @connection.publish "/queue", "message", :suppress_content_length => true
92 end
93
94 it "should get the correct byte length when dealing with Unicode characters" do
95 @tcp_socket.should_receive(:puts).with("content-length:18")
96 @connection.publish "/queue", "сообщение" # 'сообщение' is 'message' in Russian
97 end
98 end
99
100 describe "when unacknowledging a message" do
101
102 before :each do
103 @message = Stomp::Message.new(nil)
104 @message.body = "message body"
105 @message.headers = {"destination" => "/queue/original", "message-id" => "ID"}
106
107 @transaction_id = "transaction-#{@message.headers["message-id"]}-0"
108
109 @retry_headers = {
110 :destination => @message.headers["destination"],
111 :transaction => @transaction_id,
112 :retry_count => 1
113 }
114 end
115
116 it "should use a transaction" do
117 @connection.should_receive(:begin).with(@transaction_id).ordered
118 @connection.should_receive(:commit).with(@transaction_id).ordered
119 @connection.unreceive @message
120 end
121
122 it "should acknowledge the original message if ack mode is client" do
123 @connection.should_receive(:ack).with(@message.headers["message-id"], :transaction => @transaction_id)
124 @connection.subscribe(@message.headers["destination"], :ack => "client")
125 @connection.unreceive @message
126 end
127
128 it "should acknowledge the original message if forced" do
129 @connection.subscribe(@message.headers["destination"])
130 @connection.should_receive(:ack)
131 @connection.unreceive(@message, :force_client_ack => true)
132 end
133
134 it "should not acknowledge the original message if ack mode is not client or it did not subscribe to the queue" do
135 @connection.subscribe(@message.headers["destination"], :ack => "client")
136 @connection.should_receive(:ack)
137 @connection.unreceive @message
138
139 # At this time the message headers are symbolized
140 @connection.unsubscribe(@message.headers[:destination])
141 @connection.should_not_receive(:ack)
142 @connection.unreceive @message
143 @connection.subscribe(@message.headers[:destination], :ack => "individual")
144 @connection.unreceive @message
145 end
146
147 it "should send the message back to the queue it came" do
148 @connection.subscribe(@message.headers["destination"], :ack => "client")
149 @connection.should_receive(:publish).with(@message.headers["destination"], @message.body, @retry_headers)
150 @connection.unreceive @message
151 end
152
153 it "should increment the retry_count header" do
154 @message.headers["retry_count"] = 4
155 @connection.unreceive @message
156 @message.headers[:retry_count].should == 5
157 end
158
159 it "should not send the message to the dead letter queue as persistent if redeliveries equal max redeliveries" do
160 max_redeliveries = 5
161 dead_letter_queue = "/queue/Dead"
162
163 @message.headers["retry_count"] = max_redeliveries
164 transaction_id = "transaction-#{@message.headers["message-id"]}-#{@message.headers["retry_count"]}"
165 @retry_headers = @retry_headers.merge :transaction => transaction_id, :retry_count => @message.headers["retry_count"] + 1
166 @connection.should_receive(:publish).with(@message.headers["destination"], @message.body, @retry_headers)
167 @connection.unreceive @message, :dead_letter_queue => dead_letter_queue, :max_redeliveries => max_redeliveries
168 end
169
170 it "should send the message to the dead letter queue as persistent if max redeliveries have been reached" do
171 max_redeliveries = 5
172 dead_letter_queue = "/queue/Dead"
173
174 @message.headers["retry_count"] = max_redeliveries + 1
175 transaction_id = "transaction-#{@message.headers["message-id"]}-#{@message.headers["retry_count"]}"
176 @retry_headers = @retry_headers.merge :persistent => true, :transaction => transaction_id, :retry_count => @message.headers["retry_count"] + 1, :original_destination=> @message.headers["destination"]
177 @connection.should_receive(:publish).with(dead_letter_queue, @message.body, @retry_headers)
178 @connection.unreceive @message, :dead_letter_queue => dead_letter_queue, :max_redeliveries => max_redeliveries
179 end
180
181 it "should rollback the transaction and raise the exception if happened during transaction" do
182 @connection.should_receive(:publish).and_raise "Error"
183 @connection.should_receive(:abort).with(@transaction_id)
184 lambda {@connection.unreceive @message}.should raise_error("Error")
185 end
186
187 end
188
189 describe "when sending a nil message body" do
190 it "should should not raise an error" do
191 @connection = Stomp::Connection.new("niluser", "nilpass", "localhost", 61613)
192 lambda {
193 @connection.publish("/queue/nilq", nil)
194 }.should_not raise_error
195 end
196 end
197
198 describe "when using ssl" do
199
200 # Mocking ruby's openssl extension, so we can test without requiring openssl
201 module ::OpenSSL
202 module SSL
203 VERIFY_NONE = 0
204
205 class SSLSocket
206 end
207
208 class SSLContext
209 attr_accessor :verify_mode
210 end
211 end
212 end
213
214 before(:each) do
215 ssl_parameters = {:hosts => [{:login => "login2", :passcode => "passcode2", :host => "remotehost", :ssl => true}]}
216 @ssl_socket = mock(:ssl_socket, :puts => nil, :write => nil, :setsockopt => nil)
217
218 TCPSocket.should_receive(:open).and_return @tcp_socket
219 OpenSSL::SSL::SSLSocket.should_receive(:new).and_return(@ssl_socket)
220 @ssl_socket.should_receive(:connect)
221
222 @connection = Stomp::Connection.new ssl_parameters
223 end
224
225 it "should use ssl socket if ssl use is enabled" do
226 @connection.instance_variable_get(:@socket).should == @ssl_socket
227 end
228
229 it "should use default port for ssl (61612) if none is given" do
230 @connection.instance_variable_get(:@port).should == 61612
231 end
232
233 end
234
235 describe "when called to increase reconnect delay" do
236 it "should exponentialy increase when use_exponential_back_off is true" do
237 @connection.increase_reconnect_delay.should == 0.02
238 @connection.increase_reconnect_delay.should == 0.04
239 @connection.increase_reconnect_delay.should == 0.08
240 end
241 it "should not increase when use_exponential_back_off is false" do
242 @parameters[:use_exponential_back_off] = false
243 @connection = Stomp::Connection.new(@parameters)
244 @connection.increase_reconnect_delay.should == 0.01
245 @connection.increase_reconnect_delay.should == 0.01
246 end
247 it "should not increase when max_reconnect_delay is reached" do
248 @parameters[:initial_reconnect_delay] = 8.0
249 @connection = Stomp::Connection.new(@parameters)
250 @connection.increase_reconnect_delay.should == 16.0
251 @connection.increase_reconnect_delay.should == 30.0
252 end
253
254 it "should change to next host on socket error" do
255 @connection.instance_variable_set(:@failure, "some exception")
256 #retries the same host
257 TCPSocket.should_receive(:open).and_raise "exception"
258 #tries the new host
259 TCPSocket.should_receive(:open).and_return @tcp_socket
260
261 @connection.socket
262 @connection.instance_variable_get(:@host).should == "remotehost"
263 end
264
265 it "should use default options if those where not given" do
266 expected_hash = {
267 :hosts => [
268 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false},
269 # Once connected the host is sent to the end of array
270 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false}
271 ],
272 :initial_reconnect_delay => 0.01,
273 :max_reconnect_delay => 30.0,
274 :use_exponential_back_off => true,
275 :back_off_multiplier => 2,
276 :max_reconnect_attempts => 0,
277 :randomize => false,
278 :backup => false,
279 :timeout => -1,
280 :parse_timeout => 5,
281 :connect_headers => {}
282 }
283
284 used_hash = {
285 :hosts => [
286 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false},
287 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false}
288 ]
289 }
290
291 @connection = Stomp::Connection.new(used_hash)
292 @connection.instance_variable_get(:@parameters).should == expected_hash
293 end
294
295 it "should use the given options instead of default ones" do
296 used_hash = {
297 :hosts => [
298 {:login => "login2", :passcode => "passcode2", :host => "remotehost", :port => 61617, :ssl => false},
299 {:login => "login1", :passcode => "passcode1", :host => "localhost", :port => 61616, :ssl => false}
300 ],
301 :initial_reconnect_delay => 5.0,
302 :max_reconnect_delay => 100.0,
303 :use_exponential_back_off => false,
304 :back_off_multiplier => 3,
305 :max_reconnect_attempts => 10,
306 :randomize => true,
307 :backup => false,
308 :timeout => -1,
309 :parse_timeout => 20,
310 :connect_headers => {:lerolero => "ronaldo"},
311 :dead_letter_queue => "queue/Error",
312 :max_redeliveries => 10
313 }
314
315 @connection = Stomp::Connection.new(used_hash)
316 received_hash = @connection.instance_variable_get(:@parameters)
317
318 #Using randomize we can't assure the hosts order
319 received_hash.delete(:hosts)
320 used_hash.delete(:hosts)
321
322 received_hash.should == used_hash
323 end
324
325 end
326
327 end
328
329 describe "when closing a socket" do
330 it "should close the tcp connection" do
331 @tcp_socket.should_receive(:close)
332 @connection.obj_send(:close_socket).should be_true
333 end
334 it "should ignore exceptions" do
335 @tcp_socket.should_receive(:close).and_raise "exception"
336 @connection.obj_send(:close_socket).should be_true
337 end
338 end
339
340 describe "when checking if max reconnect attempts have been reached" do
341 it "should return false if not using failover" do
342 host = @parameters[:hosts][0]
343 @connection = Stomp::Connection.new(host[:login], host[:passcode], host[:host], host[:port], reliable = true, 5, connect_headers = {})
344 @connection.instance_variable_set(:@connection_attempts, 10000)
345 @connection.max_reconnect_attempts?.should be_false
346 end
347 it "should return false if max_reconnect_attempts = 0" do
348 @connection.instance_variable_set(:@connection_attempts, 10000)
349 @connection.max_reconnect_attempts?.should be_false
350 end
351 it "should return true if connection attempts > max_reconnect_attempts" do
352 limit = 10000
353 @parameters[:max_reconnect_attempts] = limit
354 @connection = Stomp::Connection.new(@parameters)
355
356 @connection.instance_variable_set(:@connection_attempts, limit-1)
357 @connection.max_reconnect_attempts?.should be_false
358
359 @connection.instance_variable_set(:@connection_attempts, limit)
360 @connection.max_reconnect_attempts?.should be_true
361
362 end
363 end
364end
365
0366
=== added file 'spec/message_spec.rb'
--- spec/message_spec.rb 1970-01-01 00:00:00 +0000
+++ spec/message_spec.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,56 @@
1require 'spec_helper'
2
3describe Stomp::Message do
4
5 context 'when initializing a new message' do
6
7 context 'with invalid parameters' do
8 it 'should return an empty message when receiving an empty string or nil parameter' do
9 message = Stomp::Message.new('')
10 message.should be_empty
11 end
12
13 it 'should raise Stomp::Error::InvalidFormat when receiving a invalid formated message' do
14 lambda{ Stomp::Message.new('any invalid format') }.should raise_error(Stomp::Error::InvalidFormat)
15 end
16 end
17
18 context 'with valid parameters' do
19 subject do
20 @message = ["CONNECTED\n", "session:host_address\n", "\n", "body value\n", "\000\n"]
21 Stomp::Message.new(@message.join)
22 end
23
24 it 'should parse the headers' do
25 subject.headers.should == {'session' => 'host_address'}
26 end
27
28 it 'should parse the body' do
29 subject.body.should == @message[3]
30 end
31
32 it 'should parse the command' do
33 subject.command.should == @message[0].chomp
34 end
35 end
36
37 context 'with multiple line ends on the body' do
38 subject do
39 @message = ["CONNECTED\n", "session:host_address\n", "\n", "body\n\n value\n\n\n", "\000\n"]
40 Stomp::Message.new(@message.join)
41 end
42
43 it 'should parse the headers' do
44 subject.headers.should == {'session' => 'host_address'}
45 end
46
47 it 'should parse the body' do
48 subject.body.should == @message[3]
49 end
50
51 it 'should parse the command' do
52 subject.command.should == @message[0].chomp
53 end
54 end
55 end
56end
057
=== added file 'spec/spec_helper.rb'
--- spec/spec_helper.rb 1970-01-01 00:00:00 +0000
+++ spec/spec_helper.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,6 @@
1require 'rspec'
2dir = File.dirname(__FILE__)
3lib_path = File.expand_path("#{dir}/../lib")
4$LOAD_PATH.unshift lib_path unless $LOAD_PATH.include?(lib_path)
5
6require 'stomp'
07
=== added file 'stomp.gemspec'
--- stomp.gemspec 1970-01-01 00:00:00 +0000
+++ stomp.gemspec 2011-07-31 16:48:34 +0000
@@ -0,0 +1,66 @@
1# Generated by jeweler
2# DO NOT EDIT THIS FILE DIRECTLY
3# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4# -*- encoding: utf-8 -*-
5
6Gem::Specification.new do |s|
7 s.name = %q{stomp}
8 s.version = "1.1.9"
9
10 s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11 s.authors = [%q{Brian McCallister}, %q{Marius Mathiesen}, %q{Thiago Morello}, %q{Guy M. Allard}]
12 s.date = %q{2011-06-15}
13 s.description = %q{Ruby client for the Stomp messaging protocol}
14 s.email = [%q{brianm@apache.org}, %q{marius@stones.com}, %q{morellon@gmail.com}, %q{allard.guy.m@gmail.com}]
15 s.executables = [%q{catstomp}, %q{stompcat}]
16 s.extra_rdoc_files = [
17 "LICENSE",
18 "README.rdoc"
19 ]
20 s.files = [
21 "CHANGELOG.rdoc",
22 "LICENSE",
23 "README.rdoc",
24 "Rakefile",
25 "bin/catstomp",
26 "bin/stompcat",
27 "examples/consumer.rb",
28 "examples/logexamp.rb",
29 "examples/publisher.rb",
30 "examples/slogger.rb",
31 "lib/stomp.rb",
32 "lib/stomp/client.rb",
33 "lib/stomp/connection.rb",
34 "lib/stomp/errors.rb",
35 "lib/stomp/ext/hash.rb",
36 "lib/stomp/message.rb",
37 "lib/stomp/version.rb",
38 "spec/client_shared_examples.rb",
39 "spec/client_spec.rb",
40 "spec/connection_spec.rb",
41 "spec/message_spec.rb",
42 "spec/spec_helper.rb",
43 "stomp.gemspec",
44 "test/test_client.rb",
45 "test/test_connection.rb",
46 "test/test_helper.rb",
47 "test/test_message.rb"
48 ]
49 s.homepage = %q{https://rubygems.org/gems/stomp}
50 s.require_paths = [%q{lib}]
51 s.rubygems_version = %q{1.8.5}
52 s.summary = %q{Ruby client for the Stomp messaging protocol}
53
54 if s.respond_to? :specification_version then
55 s.specification_version = 3
56
57 if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
58 s.add_development_dependency(%q<rspec>, [">= 2.3"])
59 else
60 s.add_dependency(%q<rspec>, [">= 2.3"])
61 end
62 else
63 s.add_dependency(%q<rspec>, [">= 2.3"])
64 end
65end
66
067
=== added directory 'test'
=== added file 'test/test_client.rb'
--- test/test_client.rb 1970-01-01 00:00:00 +0000
+++ test/test_client.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,364 @@
1$:.unshift(File.dirname(__FILE__))
2
3require 'test_helper'
4
5class TestClient < Test::Unit::TestCase
6 include TestBase
7
8 def setup
9 @client = Stomp::Client.new(user, passcode, host, port)
10 # Multi_thread test data
11 @max_threads = 20
12 @max_msgs = 50
13 end
14
15 def teardown
16 @client.close if @client # allow tests to close
17 end
18
19 def test_ack_api_works
20 @client.publish destination, message_text, {:suppress_content_length => true}
21
22 received = nil
23 @client.subscribe(destination, {:ack => 'client'}) {|msg| received = msg}
24 sleep 0.01 until received
25 assert_equal message_text, received.body
26
27 receipt = nil
28 @client.acknowledge(received) {|r| receipt = r}
29 sleep 0.01 until receipt
30 assert_not_nil receipt.headers['receipt-id']
31 end
32
33 def test_asynch_subscribe
34 received = false
35 @client.subscribe(destination) {|msg| received = msg}
36 @client.publish destination, message_text
37 sleep 0.01 until received
38
39 assert_equal message_text, received.body
40 end
41
42 def test_noack
43 @client.publish destination, message_text
44
45 received = nil
46 @client.subscribe(destination, :ack => :client) {|msg| received = msg}
47 sleep 0.01 until received
48 assert_equal message_text, received.body
49 @client.close
50
51 # was never acked so should be resent to next client
52
53 @client = Stomp::Client.new(user, passcode, host, port)
54 received2 = nil
55 @client.subscribe(destination) {|msg| received2 = msg}
56 sleep 0.01 until received2
57
58 assert_equal message_text, received2.body
59 assert_equal received.body, received2.body
60 assert_equal received.headers['message-id'], received2.headers['message-id']
61 end
62
63 def test_receipts
64 receipt = false
65 @client.publish(destination, message_text) {|r| receipt = r}
66 sleep 0.1 until receipt
67
68 message = nil
69 @client.subscribe(destination) {|m| message = m}
70 sleep 0.1 until message
71 assert_equal message_text, message.body
72 end
73
74 def test_disconnect_receipt
75 @client.close :receipt => "xyz789"
76 assert_nothing_raised {
77 assert_not_nil(@client.disconnect_receipt, "should have a receipt")
78 assert_equal(@client.disconnect_receipt.headers['receipt-id'],
79 "xyz789", "receipt sent and received should match")
80 }
81 @client = nil
82 end
83
84 def test_publish_then_sub
85 @client.publish destination, message_text
86 message = nil
87 @client.subscribe(destination) {|m| message = m}
88 sleep 0.01 until message
89
90 assert_equal message_text, message.body
91 end
92
93 def test_subscribe_requires_block
94 assert_raise(RuntimeError) do
95 @client.subscribe destination
96 end
97 end
98
99 def test_transactional_publish
100 @client.begin 'tx1'
101 @client.publish destination, message_text, :transaction => 'tx1'
102 @client.commit 'tx1'
103
104 message = nil
105 @client.subscribe(destination) {|m| message = m}
106 sleep 0.01 until message
107
108 assert_equal message_text, message.body
109 end
110
111 def test_transaction_publish_then_rollback
112 @client.begin 'tx1'
113 @client.publish destination, "first_message", :transaction => 'tx1'
114 @client.abort 'tx1'
115
116 @client.begin 'tx1'
117 @client.publish destination, "second_message", :transaction => 'tx1'
118 @client.commit 'tx1'
119
120 message = nil
121 @client.subscribe(destination) {|m| message = m}
122 sleep 0.01 until message
123 assert_equal "second_message", message.body
124 end
125
126 def test_transaction_ack_rollback_with_new_client
127 @client.publish destination, message_text
128
129 @client.begin 'tx1'
130 message = nil
131 @client.subscribe(destination, :ack => 'client') {|m| message = m}
132 sleep 0.01 until message
133 assert_equal message_text, message.body
134 @client.acknowledge message, :transaction => 'tx1'
135 message = nil
136 @client.abort 'tx1'
137
138 # lets recreate the connection
139 teardown
140 setup
141 @client.subscribe(destination, :ack => 'client') {|m| message = m}
142
143 Timeout::timeout(4) do
144 sleep 0.01 until message
145 end
146 assert_not_nil message
147 assert_equal message_text, message.body
148
149 @client.begin 'tx2'
150 @client.acknowledge message, :transaction => 'tx2'
151 @client.commit 'tx2'
152 end
153
154 def test_raise_on_multiple_subscriptions_to_same_destination
155 subscribe_dest = destination
156 @client.subscribe(subscribe_dest) {|m| nil }
157 assert_raise(RuntimeError) do
158 @client.subscribe(subscribe_dest) {|m| nil }
159 end
160 end
161
162 def test_raise_on_multiple_subscriptions_to_same_id
163 subscribe_dest = destination
164 @client.subscribe(subscribe_dest, {'id' => 'myid'}) {|m| nil }
165 assert_raise(RuntimeError) do
166 @client.subscribe(subscribe_dest, {'id' => 'myid'}) {|m| nil }
167 end
168 end
169
170 def test_raise_on_multiple_subscriptions_to_same_id_mixed
171 subscribe_dest = destination
172 @client.subscribe(subscribe_dest, {'id' => 'myid'}) {|m| nil }
173 assert_raise(RuntimeError) do
174 @client.subscribe(subscribe_dest, {:id => 'myid'}) {|m| nil }
175 end
176 end
177
178 def test_asterisk_wildcard_subscribe
179 queue_base_name = destination
180 queue1 = queue_base_name + ".a"
181 queue2 = queue_base_name + ".b"
182 send_message = message_text
183 @client.publish queue1, send_message
184 @client.publish queue2, send_message
185 messages = []
186 @client.subscribe(queue_base_name + ".*", :ack => 'client') do |m|
187 messages << m
188 @client.acknowledge(m)
189 end
190 Timeout::timeout(4) do
191 sleep 0.1 while messages.size < 2
192 end
193
194 messages.each do |message|
195 assert_not_nil message
196 assert_equal send_message, message.body
197 end
198 results = [queue1, queue2].collect do |queue|
199 messages.any? do |message|
200 message_source = message.headers['destination']
201 message_source == queue
202 end
203 end
204 assert results.all?{|a| a == true }
205
206 end unless ENV['STOMP_NOWILD']
207
208 def test_greater_than_wildcard_subscribe
209 queue_base_name = destination + "."
210 queue1 = queue_base_name + "foo.a"
211 queue2 = queue_base_name + "bar.a"
212 queue3 = queue_base_name + "foo.b"
213 send_message = message_text
214 @client.publish queue1, send_message
215 @client.publish queue2, send_message
216 @client.publish queue3, send_message
217 messages = []
218 # should subscribe to all three queues
219 @client.subscribe(queue_base_name + ">", :ack => 'client') do |m|
220 messages << m
221 @client.acknowledge(m)
222 end
223 Timeout::timeout(4) do
224 sleep 0.1 while messages.size < 3
225 end
226
227 messages.each do |message|
228 assert_not_nil message
229 assert_equal send_message, message.body
230 end
231 # make sure that the messages received came from the expected queues
232 results = [queue1, queue2, queue3].collect do |queue|
233 messages.any? do |message|
234 message_source = message.headers['destination']
235 message_source == queue
236 end
237 end
238 assert results.all?{|a| a == true }
239 end unless ENV['STOMP_NOWILD'] || ENV['STOMP_APOLLO']
240
241 def test_transaction_with_client_side_redelivery
242 @client.publish destination, message_text
243
244 @client.begin 'tx1'
245 message = nil
246 @client.subscribe(destination, :ack => 'client') { |m| message = m }
247
248 sleep 0.1 while message.nil?
249
250 assert_equal message_text, message.body
251 @client.acknowledge message, :transaction => 'tx1'
252 message = nil
253 @client.abort 'tx1'
254
255 sleep 0.1 while message.nil?
256
257 assert_not_nil message
258 assert_equal message_text, message.body
259
260 @client.begin 'tx2'
261 @client.acknowledge message, :transaction => 'tx2'
262 @client.commit 'tx2'
263 end
264
265 def test_connection_frame
266 assert_not_nil @client.connection_frame
267 end
268
269 def test_unsubscribe
270 message = nil
271 dest = destination
272 to_send = message_text
273 client = Stomp::Client.new(user, passcode, host, port, true)
274 assert_nothing_raised {
275 client.subscribe(dest, :ack => 'client') { |m| message = m }
276 @client.publish dest, to_send
277 Timeout::timeout(4) do
278 sleep 0.01 until message
279 end
280 }
281 assert_equal to_send, message.body, "first body check"
282 assert_nothing_raised {
283 client.unsubscribe dest # was throwing exception on unsub at one point
284 client.close
285 }
286 # Same message should remain on the queue. Receive it again with ack=>auto.
287 message_copy = nil
288 client = Stomp::Client.new(user, passcode, host, port, true)
289 assert_nothing_raised {
290 client.subscribe(dest, :ack => 'auto') { |m| message_copy = m }
291 Timeout::timeout(4) do
292 sleep 0.01 until message_copy
293 end
294 }
295 assert_equal to_send, message_copy.body, "second body check"
296 assert_equal message.headers['message-id'], message_copy.headers['message-id'], "header check"
297 end
298
299 def test_thread_one_subscribe
300 msg = nil
301 dest = destination
302 Thread.new(@client) do |acli|
303 assert_nothing_raised {
304 acli.subscribe(dest) { |m| msg = m }
305 Timeout::timeout(4) do
306 sleep 0.01 until msg
307 end
308 }
309 end
310 #
311 @client.publish(dest, message_text)
312 sleep 1
313 assert_not_nil msg
314 end
315
316 def test_thread_multi_subscribe
317 #
318 lock = Mutex.new
319 msg_ctr = 0
320 dest = destination
321 1.upto(@max_threads) do |tnum|
322 # Threads within threads .....
323 Thread.new(@client) do |acli|
324 assert_nothing_raised {
325 acli.subscribe(dest) { |m|
326 msg = m
327 lock.synchronize do
328 msg_ctr += 1
329 end
330 # Simulate message processing
331 sleep 0.05
332 }
333 }
334 end
335 end
336 #
337 1.upto(@max_msgs) do |mnum|
338 msg = Time.now.to_s + " #{mnum}"
339 @client.publish(dest, message_text)
340 end
341 #
342 max_sleep = (RUBY_VERSION =~ /1\.8\.6/) ? 30 : 5
343 sleep_incr = 0.10
344 total_slept = 0
345 while true
346 break if @max_msgs == msg_ctr
347 total_slept += sleep_incr
348 break if total_slept > max_sleep
349 sleep sleep_incr
350 end
351 assert_equal @max_msgs, msg_ctr
352 end
353
354 private
355 def message_text
356 name = caller_method_name unless name
357 "test_client#" + name
358 end
359
360 def destination
361 name = caller_method_name unless name
362 qname = ENV['STOMP_APOLLO'] ? "/queue/test.ruby.stomp." + name : "/queue/test/ruby/stomp/" + name
363 end
364end
0365
=== added file 'test/test_connection.rb'
--- test/test_connection.rb 1970-01-01 00:00:00 +0000
+++ test/test_connection.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,278 @@
1$:.unshift(File.dirname(__FILE__))
2
3require 'test_helper'
4
5class TestStomp < Test::Unit::TestCase
6 include TestBase
7
8 def setup
9 @conn = Stomp::Connection.open(user, passcode, host, port)
10 # Data for multi_thread tests
11 @max_threads = 20
12 @max_msgs = 100
13 end
14
15 def teardown
16 @conn.disconnect if @conn # allow tests to disconnect
17 end
18
19 def test_connection_exists
20 assert_not_nil @conn
21 end
22
23 def test_no_length
24 @conn.subscribe make_destination
25 #
26 @conn.publish make_destination, "test_stomp#test_no_length",
27 { :suppress_content_length => true }
28 msg = @conn.receive
29 assert_equal "test_stomp#test_no_length", msg.body
30 #
31 @conn.publish make_destination, "test_stomp#test_\000_length",
32 { :suppress_content_length => true }
33 msg2 = @conn.receive
34 assert_equal "test_stomp#test_", msg2.body
35 end
36
37 def test_explicit_receive
38 @conn.subscribe make_destination
39 @conn.publish make_destination, "test_stomp#test_explicit_receive"
40 msg = @conn.receive
41 assert_equal "test_stomp#test_explicit_receive", msg.body
42 end
43
44 def test_receipt
45 @conn.subscribe make_destination, :receipt => "abc"
46 msg = @conn.receive
47 assert_equal "abc", msg.headers['receipt-id']
48 end
49
50 def test_disconnect_receipt
51 @conn.disconnect :receipt => "abc123"
52 assert_nothing_raised {
53 assert_not_nil(@conn.disconnect_receipt, "should have a receipt")
54 assert_equal(@conn.disconnect_receipt.headers['receipt-id'],
55 "abc123", "receipt sent and received should match")
56 }
57 @conn = nil
58 end
59
60 def test_client_ack_with_symbol
61 @conn.subscribe make_destination, :ack => :client
62 @conn.publish make_destination, "test_stomp#test_client_ack_with_symbol"
63 msg = @conn.receive
64 @conn.ack msg.headers['message-id']
65 end
66
67 def test_embedded_null
68 @conn.subscribe make_destination
69 @conn.publish make_destination, "a\0"
70 msg = @conn.receive
71 assert_equal "a\0" , msg.body
72 end
73
74 def test_connection_open?
75 assert_equal true , @conn.open?
76 @conn.disconnect
77 assert_equal false, @conn.open?
78 end
79
80 def test_connection_closed?
81 assert_equal false, @conn.closed?
82 @conn.disconnect
83 assert_equal true, @conn.closed?
84 end
85
86 def test_response_is_instance_of_message_class
87 @conn.subscribe make_destination
88 @conn.publish make_destination, "a\0"
89 msg = @conn.receive
90 assert_instance_of Stomp::Message , msg
91 end
92
93 def test_message_to_s
94 @conn.subscribe make_destination
95 @conn.publish make_destination, "a\0"
96 msg = @conn.receive
97 assert_match /^<Stomp::Message headers=/ , msg.to_s
98 end
99
100 def test_connection_frame
101 assert_not_nil @conn.connection_frame
102 end
103
104 def test_messages_with_multipleLine_ends
105 @conn.subscribe make_destination
106 @conn.publish make_destination, "a\n\n"
107 @conn.publish make_destination, "b\n\na\n\n"
108
109 msg_a = @conn.receive
110 msg_b = @conn.receive
111
112 assert_equal "a\n\n", msg_a.body
113 assert_equal "b\n\na\n\n", msg_b.body
114 end
115
116 def test_publish_two_messages
117 @conn.subscribe make_destination
118 @conn.publish make_destination, "a\0"
119 @conn.publish make_destination, "b\0"
120 msg_a = @conn.receive
121 msg_b = @conn.receive
122
123 assert_equal "a\0", msg_a.body
124 assert_equal "b\0", msg_b.body
125 end
126
127 def test_thread_hang_one
128 received = nil
129 Thread.new(@conn) do |amq|
130 while true
131 received = amq.receive
132 end
133 end
134 #
135 @conn.subscribe( make_destination )
136 message = Time.now.to_s
137 @conn.publish(make_destination, message)
138 sleep 1
139 assert_not_nil received
140 assert_equal message, received.body
141 end
142
143 def test_thread_poll_one
144 received = nil
145 max_sleep = (RUBY_VERSION =~ /1\.8\.6/) ? 5 : 1
146 Thread.new(@conn) do |amq|
147 while true
148 received = amq.poll
149 # One message is needed
150 Thread.exit if received
151 sleep max_sleep
152 end
153 end
154 #
155 @conn.subscribe( make_destination )
156 message = Time.now.to_s
157 @conn.publish(make_destination, message)
158 sleep max_sleep+1
159 assert_not_nil received
160 assert_equal message, received.body
161 end
162
163 def test_multi_thread_receive
164 lock = Mutex.new
165 msg_ctr = 0
166 dest = make_destination
167 #
168 1.upto(@max_threads) do |tnum|
169 Thread.new(@conn) do |amq|
170 while true
171 received = amq.receive
172 lock.synchronize do
173 msg_ctr += 1
174 end
175 # Simulate message processing
176 sleep 0.05
177 end
178 end
179 end
180 #
181 @conn.subscribe( dest )
182 1.upto(@max_msgs) do |mnum|
183 msg = Time.now.to_s + " #{mnum}"
184 @conn.publish(dest, msg)
185 end
186 #
187 max_sleep = (RUBY_VERSION =~ /1\.8\.6/) ? 30 : 5
188 sleep_incr = 0.10
189 total_slept = 0
190 while true
191 break if @max_msgs == msg_ctr
192 total_slept += sleep_incr
193 break if total_slept > max_sleep
194 sleep sleep_incr
195 end
196 assert_equal @max_msgs, msg_ctr
197 end
198
199 def test_multi_thread_poll
200 #
201 lock = Mutex.new
202 msg_ctr = 0
203 dest = make_destination
204 #
205 1.upto(@max_threads) do |tnum|
206 Thread.new(@conn) do |amq|
207 while true
208 received = amq.poll
209 if received
210 lock.synchronize do
211 msg_ctr += 1
212 end
213 # Simulate message processing
214 sleep 0.05
215 else
216 # Wait a bit for more work
217 sleep 0.05
218 end
219 end
220 end
221 end
222 #
223 @conn.subscribe( dest )
224 1.upto(@max_msgs) do |mnum|
225 msg = Time.now.to_s + " #{mnum}"
226 @conn.publish(dest, msg)
227 end
228 #
229 max_sleep = (RUBY_VERSION =~ /1\.8\.6/) ? 30 : 5
230 sleep_incr = 0.10
231 total_slept = 0
232 while true
233 break if @max_msgs == msg_ctr
234 total_slept += sleep_incr
235 break if total_slept > max_sleep
236 sleep sleep_incr
237 end
238 assert_equal @max_msgs, msg_ctr
239 end
240
241 def test_nil_body
242 dest = make_destination
243 assert_nothing_raised {
244 @conn.publish dest, nil
245 }
246 @conn.subscribe dest
247 msg = @conn.receive
248 assert_equal "", msg.body
249 end
250
251 private
252 def make_destination
253 name = caller_method_name unless name
254 qname = ENV['STOMP_APOLLO'] ? "/queue/test.ruby.stomp." + name : "/queue/test/ruby/stomp/" + name
255 end
256
257 def _test_transaction
258 @conn.subscribe make_destination
259
260 # Drain the destination.
261 sleep 0.01 while
262 sleep 0.01 while @conn.poll!=nil
263
264 @conn.begin "tx1"
265 @conn.publish make_destination, "txn message", 'transaction' => "tx1"
266
267 @conn.publish make_destination, "first message"
268
269 sleep 0.01
270 msg = @conn.receive
271 assert_equal "first message", msg.body
272
273 @conn.commit "tx1"
274 msg = @conn.receive
275 assert_equal "txn message", msg.body
276 end
277end
278
0279
=== added file 'test/test_helper.rb'
--- test/test_helper.rb 1970-01-01 00:00:00 +0000
+++ test/test_helper.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,38 @@
1$:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
2
3require 'test/unit'
4require 'timeout'
5require 'stomp'
6
7# Helper routines
8module 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 # Helper for minitest on 1.9
24 def caller_method_name
25 parse_caller(caller(2).first).last
26 end
27 # Helper for minitest on 1.9
28 def parse_caller(at)
29 if /^(.+?):(\d+)(?::in `(.*)')?/ =~ at
30 file = Regexp.last_match[1]
31 line = Regexp.last_match[2].to_i
32 method = Regexp.last_match[3]
33 method.gsub!(" ","_")
34 [file, line, method]
35 end
36 end
37end
38
039
=== added file 'test/test_message.rb'
--- test/test_message.rb 1970-01-01 00:00:00 +0000
+++ test/test_message.rb 2011-07-31 16:48:34 +0000
@@ -0,0 +1,118 @@
1$:.unshift(File.dirname(__FILE__))
2#
3# Test Ruby 1.8 with $KCODE='U'
4#
5require 'test_helper'
6#
7class TestMessageKcode < Test::Unit::TestCase
8 include TestBase
9 #
10 def setup
11 $KCODE = 'U' if RUBY_VERSION =~ /1\.8/
12 @conn = Stomp::Connection.open(user, passcode, host, port)
13 # Message body data
14 @messages = [
15 "normal text message",
16 "bad byte: \372",
17 "\004\b{\f:\tbody\"\001\207\004\b{\b:\016statusmsg\"\aOK:\017statuscodei\000:\tdata{\t:\voutput\"3Enabled, not running, last run 693 seconds ago:\frunningi\000:\fenabledi\006:\flastrunl+\aE\021\022M:\rsenderid\"\032xx.xx.xx.xx:\016requestid\"%849d647bbe3e421ea19ac9f947bbdde4:\020senderagent\"\fpuppetd:\016msgtarget\"%/topic/mcollective.puppetd.reply:\thash\"\001\257ZdQqtaDmmdD0jZinnEcpN+YbkxQDn8uuCnwsQdvGHau6d+gxnnfPLUddWRSb\nZNMs+sQUXgJNfcV1eVBn1H+Z8QQmzYXVDMqz7J43jmgloz5PsLVbN9K3PmX/\ngszqV/WpvIyAqm98ennWqSzpwMuiCC4q2Jr3s3Gm6bUJ6UkKXnY=\n:\fmsgtimel+\a\372\023\022M"
18 ]
19 #
20 end
21
22 def teardown
23 @conn.disconnect if @conn # allow tests to disconnect
24 end
25
26 # Various message bodies, including the failing test case reported
27 def test_kcode_001
The diff has been truncated for viewing.

Subscribers

People subscribed via source and target branches

to all changes: