stomp_out 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,580 @@
1
+ # Copyright (c) 2015 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module StompOut
23
+
24
+ # Abstract base class for STOMP client for use with an existing server connection, such
25
+ # as a WebSocket. Derived classes are responsible for supplying the following functions:
26
+ # send_data(data) - send data over connection
27
+ # on_connected(frame, session_id, server_name) - handle notification that now connected
28
+ # on_message(frame, destination, message, content_type, message_id, ack_id) - handle message
29
+ # received from server
30
+ # on_receipt(frame, receipt_id) - handle notification that a request was successfully
31
+ # handled by server
32
+ # on_error(frame, message, details, receipt_id) - handle notification from server
33
+ # that a request failed and that the connection should be closed
34
+ #
35
+ class Client
36
+
37
+ SUPPORTED_VERSIONS = ["1.0", "1.1", "1.2"]
38
+
39
+ ACK_SETTINGS = {
40
+ "1.0" => ["auto", "client"],
41
+ "1.1" => ["auto", "client", "client-individual"],
42
+ "1.2" => ["auto", "client", "client-individual"]
43
+ }
44
+
45
+ SERVER_COMMANDS = [:connected, :message, :receipt, :error]
46
+
47
+ MIN_SEND_HEARTBEAT = 5000
48
+
49
+ attr_reader :version, :session_id, :server_name, :host, :heartbeat
50
+
51
+ # Create STOMP client
52
+ #
53
+ # @option options [String] :host to which client wishes to connect; if not using virtual hosts,
54
+ # recommended setting is the host name that the socket in use was connected against,
55
+ # or any name of client's choosing; defaults to "stomp"
56
+ # @option options [Boolean] :receipt enabled for all requests except connect; disabled
57
+ # by default but can still enable on individual requests
58
+ # @option options [Boolean] :auto_json encode/decode "application/json" content-type
59
+ # @option options [Integer] :min_send_interval in msec that this client can guarantee;
60
+ # defaults to MIN_SEND_HEARTBEAT
61
+ def initialize(options = {})
62
+ @options = options
63
+ @host = @options[:host] || "stomp"
64
+ @parser = StompOut::Parser.new
65
+ @ack_id = 0
66
+ @message_ids = {} # ack ID is key
67
+ @subscribe_id = 0
68
+ @subscribes = {} # destination is key
69
+ @transaction_id = 0
70
+ @transaction_ids = []
71
+ @receipt = options[:receipt]
72
+ @receipt_id = 0
73
+ @receipted_frames = {} # receipt-id is key
74
+ @connected = false
75
+ end
76
+
77
+ # List active subscriptions
78
+ #
79
+ # @return [Array<String>] subscription destinations
80
+ def subscriptions
81
+ @subscribes.keys
82
+ end
83
+
84
+ # List active transactions
85
+ #
86
+ # @return [Array<String>] transaction IDs
87
+ def transactions
88
+ @transaction_ids
89
+ end
90
+
91
+ # Determine whether connected to STOMP server
92
+ #
93
+ # @return [Boolean] true if connected, otherwise false
94
+ def connected?
95
+ !!@connected
96
+ end
97
+
98
+ # Report to client that an error was encountered locally
99
+ # Not intended for use by end user of this class
100
+ #
101
+ # @param [Exception, String] error being reported
102
+ #
103
+ # @return [TrueClass] always true
104
+ def report_error(error)
105
+ details = ""
106
+ if error.is_a?(ProtocolError) || error.is_a?(ApplicationError)
107
+ message = error.message
108
+ elsif error.is_a?(Exception)
109
+ message = "#{error.class}: #{error.message}"
110
+ details = error.backtrace.join("\n") if error.respond_to?(:backtrace)
111
+ else
112
+ message = error.to_s
113
+ end
114
+ frame = Frame.new("ERROR", {"message" => message}, details)
115
+ on_error(frame, message, details, receipt_id = nil)
116
+ true
117
+ end
118
+
119
+ # Process data received over connection from server
120
+ #
121
+ # @param [String] data to be processed
122
+ #
123
+ # @return [TrueClass] always true
124
+ def receive_data(data)
125
+ @parser << data
126
+ process_frames
127
+ @heartbeat.received_data if @heartbeat
128
+ true
129
+ rescue StandardError => e
130
+ report_error(e)
131
+ end
132
+
133
+ ##################################
134
+ ## STOMP client subclass functions
135
+ ##################################
136
+
137
+ # Send data over connection to server
138
+ #
139
+ # @param [String] data that is STOMP encoded
140
+ #
141
+ # @return [TrueClass] always true
142
+ def send_data(data)
143
+ raise "Not implemented"
144
+ end
145
+
146
+ # Handle notification that now connected to server
147
+ #
148
+ # @param [Frame] frame received from server
149
+ # @param [String] session_id uniquely identifying the given STOMP session
150
+ # @param [String, NilClass] server_name in form "<name>/<version>" with
151
+ # "/<version>" being optional; nil if not provided by server
152
+ #
153
+ # @return [TrueClass] always true
154
+ def on_connected(frame, session_id, server_name)
155
+ raise "Not implemented"
156
+ end
157
+
158
+ # Handle message received from server
159
+ #
160
+ # @param [Frame] frame received from server
161
+ # @param [String] destination to which the message was sent
162
+ # @param [Object] message body; if content_type is "application/json"
163
+ # and :auto_json client option specified the message is JSON decoded
164
+ # @param [String] content_type of message in MIME terms, e.g., "text/plain"
165
+ # @param [String] message_id uniquely identifying message
166
+ # @param [String, NilClass] ack_id to be used when acknowledging message
167
+ # to server if acknowledgement enabled
168
+ #
169
+ # @return [TrueClass] always true
170
+ def on_message(frame, destination, message, content_type, message_id, ack_id)
171
+ raise "Not implemented"
172
+ end
173
+
174
+ # Handle notification that a request was successfully handled by server
175
+ #
176
+ # @param [Frame] frame received from server
177
+ # @param [String] receipt_id identifying request completed (client request
178
+ # functions optionally return a receipt_id)
179
+ #
180
+ # @return [TrueClass] always true
181
+ def on_receipt(frame, receipt_id)
182
+ raise "Not implemented"
183
+ end
184
+
185
+ # Handle notification from server that a request failed and that the connection
186
+ # should be closed
187
+ #
188
+ # @param [Frame] frame received from server
189
+ # @param [String] error message
190
+ # @param [String, NilClass] details about the error, e.g., the frame that failed
191
+ # @param [String, NilClass] receipt_id identifying request that failed (Client
192
+ # functions optionally return a receipt_id)
193
+ #
194
+ # @return [TrueClass] always true
195
+ def on_error(frame, error, details, receipt_id)
196
+ raise "Not implemented"
197
+ end
198
+
199
+ ########################
200
+ ## STOMP client commands
201
+ ########################
202
+
203
+ # Connect to server
204
+ #
205
+ # @param [Integer, NilClass] heartbeat rate in milliseconds that is desired;
206
+ # defaults to no heartbeat; not usable unless eventmachine gem available
207
+ # @param [String, NilClass] login name for authentication with server; defaults
208
+ # to no authentication, although this may not be acceptable to server
209
+ # @param [String, NilClass] passcode for authentication
210
+ # @param [Hash, NilClass] headers that are application specific
211
+ #
212
+ # @return [TrueClass] always true
213
+ #
214
+ # @raise [ProtocolError] already connected
215
+ # @raise [ApplicationError] eventmachine not available
216
+ def connect(heartbeat = nil, login = nil, passcode = nil, headers = nil)
217
+ raise ProtocolError, "Already connected" if @connected
218
+ headers ||= {}
219
+ headers["accept-version"] = SUPPORTED_VERSIONS.join(",")
220
+ headers["host"] = @host
221
+ if heartbeat
222
+ raise ApplicationError.new("Heartbeat not usable without eventmachine") unless Heartbeat.usable?
223
+ headers["heart-beat"] = "#{@options[:min_send_interval] || MIN_SEND_HEARTBEAT},#{heartbeat}"
224
+ end
225
+ if login
226
+ headers["login"] = login
227
+ headers["passcode"] = passcode
228
+ end
229
+ send_frame("CONNECT", headers)
230
+ true
231
+ end
232
+
233
+ # Send message to given destination
234
+ #
235
+ # @param [String] destination for message
236
+ # @param [String] message being sent
237
+ # @param [String, NilClass] content_type of message body in MIME format;
238
+ # optionally JSON-encodes body automatically if "application/json";
239
+ # defaults to "plain/text"
240
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
241
+ # @param [String, NilClass] transaction_id for transaction into which this command
242
+ # is to be included; defaults to no transaction
243
+ # @param [Hash] headers that are application specific, e.g., "message-id"
244
+ #
245
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
246
+ #
247
+ # @raise [ProtocolError] not connected
248
+ def message(destination, message, content_type = nil, receipt = nil, transaction_id = nil, headers = nil)
249
+ raise ProtocolError.new("Not connected") unless @connected
250
+ headers ||= {}
251
+ headers["destination"] = destination
252
+ frame = send_frame("SEND", headers, message, content_type, receipt, transaction_id)
253
+ frame.headers["receipt"]
254
+ end
255
+
256
+ # Register to listen to a given destination
257
+ #
258
+ # @param [String] destination of interest
259
+ # @param [String, NilClass] ack setting: "auto", "client", or "client-individual";
260
+ # defaults to "auto"
261
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
262
+ # @param [Hash, NilClass] headers that are application specific
263
+ #
264
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
265
+ #
266
+ # @raise [ProtocolError] not connected, invalid ack setting
267
+ # @raise [ApplicationError] duplicate subscription
268
+ def subscribe(destination, ack = nil, receipt = nil, headers = nil)
269
+ raise ProtocolError.new("Not connected") unless @connected
270
+ raise ApplicationError.new("Already subscribed to '#{destination}'") if @subscribes[destination]
271
+ raise ProtocolError.new("Invalid 'ack' setting") if ack && !ACK_SETTINGS[@version].include?(ack)
272
+ @subscribes[destination] = {:id => (@subscribe_id += 1).to_s, :ack => ack}
273
+ headers ||= {}
274
+ headers["destination"] = destination
275
+ headers["id"] = @subscribe_id.to_s
276
+ headers["ack"] = ack if ack
277
+ frame = send_frame("SUBSCRIBE", headers, body = nil, content_type = nil, receipt)
278
+ frame.headers["receipt"]
279
+ end
280
+
281
+ # Remove an existing subscription
282
+ #
283
+ # @param [String] destination no longer of interest
284
+ #
285
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
286
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
287
+ # @param [Hash, NilClass] headers that are application specific
288
+ #
289
+ # @raise [ProtocolError] not connected
290
+ # @raise [ApplicationError] subscription not found
291
+ def unsubscribe(destination, receipt = nil, headers = nil)
292
+ raise ProtocolError.new("Not connected") unless @connected
293
+ subscribe = @subscribes.delete(destination)
294
+ raise ApplicationError.new("Subscription to '#{destination}' not found") if subscribe.nil?
295
+ headers ||= {}
296
+ headers["id"] = subscribe[:id]
297
+ headers["destination"] = destination if @version == "1.0"
298
+ frame = send_frame("UNSUBSCRIBE", headers, body = nil, content_type = nil, receipt)
299
+ frame.headers["receipt"]
300
+ end
301
+
302
+ # Acknowledge consumption of a message from a subscription
303
+ #
304
+ # @param [String] ack_id identifying message being acknowledged
305
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
306
+ # @param [String, NilClass] transaction_id for transaction into which this command
307
+ # is to be included; defaults to no transaction
308
+ # @param [Hash, NilClass] headers that are application specific
309
+ #
310
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
311
+ #
312
+ # @raise [ProtocolError] not connected
313
+ # @raise [ApplicationError] message for ack not found
314
+ def ack(ack_id, receipt = nil, transaction_id = nil, headers = nil)
315
+ raise ProtocolError.new("Not connected") unless @connected
316
+ message_id = @message_ids.delete(ack_id)
317
+ headers ||= {}
318
+ if @version == "1.0"
319
+ raise ApplicationError.new("No message was received with ack #{ack_id}") if message_id.nil?
320
+ headers["message-id"] = message_id
321
+ frame = send_frame("ACK", headers, body = nil, content_type = nil, receipt, transaction_id)
322
+ else
323
+ headers["id"] = ack_id.to_s
324
+ frame = send_frame("ACK", headers, body = nil, content_type = nil, receipt, transaction_id)
325
+ end
326
+ frame.headers["receipt"]
327
+ end
328
+
329
+ # Tell the server that a message was not consumed
330
+ #
331
+ # @param [String] ack_id identifying message being negatively acknowledged
332
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
333
+ # @param [String, NilClass] transaction_id for transaction into which this command
334
+ # is to be included; defaults to no transaction
335
+ # @param [Hash, NilClass] headers that are application specific
336
+ #
337
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
338
+ #
339
+ # @raise [ProtocolError] nack not supported, not connected
340
+ def nack(ack_id, receipt = nil, transaction_id = nil, headers = nil)
341
+ raise ProtocolError.new("Command 'nack' not supported") if @version == "1.0"
342
+ raise ProtocolError.new("Not connected") unless @connected
343
+ @message_ids.delete(ack_id)
344
+ headers ||= {}
345
+ headers["id"] = ack_id.to_s
346
+ frame = send_frame("NACK", headers, body = nil, content_type = nil, receipt, transaction_id)
347
+ frame.headers["receipt"]
348
+ end
349
+
350
+ # Start a transaction
351
+ #
352
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
353
+ #
354
+ # @return [Array<String>] transaction ID and receipt ID if receipt enabled
355
+ # @param [Hash, NilClass] headers that are application specific
356
+ #
357
+ # @raise [ProtocolError] not connected
358
+ def begin(receipt = nil, headers = nil)
359
+ raise ProtocolError.new("Not connected") unless @connected
360
+ id = (@transaction_id += 1).to_s
361
+ headers ||= {}
362
+ headers["transaction"] = id.to_s
363
+ frame = send_frame("BEGIN", headers, body = nil, content_type = nil, receipt)
364
+ @transaction_ids << id
365
+ [id, frame.headers["receipt"]]
366
+ end
367
+
368
+ # Commit a transaction
369
+ #
370
+ # @param [String] transaction_id uniquely identifying transaction
371
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
372
+ # @param [Hash, NilClass] headers that are application specific
373
+ #
374
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
375
+ #
376
+ # @raise [ProtocolError] not connected
377
+ # @raise [ApplicationError] transaction not found
378
+ def commit(id, receipt = nil, headers = nil)
379
+ raise ProtocolError.new("Not connected") unless @connected
380
+ raise ApplicationError.new("Transaction #{id} not found") unless @transaction_ids.delete(id.to_s)
381
+ headers ||= {}
382
+ headers["transaction"] = id.to_s
383
+ frame = send_frame("COMMIT", headers, body = nil, content_type = nil, receipt)
384
+ frame.headers["receipt"]
385
+ end
386
+
387
+ # Roll back a transaction
388
+ #
389
+ # @param [String] id uniquely identifying transaction
390
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
391
+ # @param [Hash, NilClass] headers that are application specific
392
+ #
393
+ # @return [String, NilClass] receipt ID if enabled, otherwise nil
394
+ #
395
+ # @raise [ProtocolError] not connected
396
+ # @raise [ApplicationError] transaction not found
397
+ def abort(id, receipt = nil, headers = nil)
398
+ raise ProtocolError.new("Not connected") unless @connected
399
+ raise ApplicationError.new("Transaction #{id} not found") unless @transaction_ids.delete(id.to_s)
400
+ headers ||= {}
401
+ headers["transaction"] = id.to_s
402
+ frame = send_frame("ABORT", headers, body = nil, content_type = nil, receipt)
403
+ frame.headers["receipt"]
404
+ end
405
+
406
+ # Disconnect from the server
407
+ # Client is expected to close its connection after calling this function
408
+ # If receipt is requested, it may not be received before frame is reset
409
+ #
410
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
411
+ # @param [Hash, NilClass] headers that are application specific
412
+ #
413
+ # @return [String, NilClass] receipt ID if enabled and connected, otherwise nil
414
+ #
415
+ # @raise [ProtocolError] not connected
416
+ def disconnect(receipt = nil, headers = nil)
417
+ raise ProtocolError.new("Not connected") unless @connected
418
+ frame = send_frame("DISCONNECT", headers, body = nil, content_type = nil, receipt)
419
+ @heartbeat.stop if @heartbeat
420
+ @connected = false
421
+ frame.headers["receipt"]
422
+ end
423
+
424
+ protected
425
+
426
+ ########################
427
+ ## STOMP Server Commands
428
+ ########################
429
+
430
+ # Handle notification from server that now connected at STOMP protocol level
431
+ #
432
+ # @param [Frame] frame received from server
433
+ # @param [String, NilClass] body for frame after optional decoding
434
+ #
435
+ # @return [TrueClass] always true
436
+ def receive_connected(frame, body)
437
+ @version = frame.headers["version"] || "1.0"
438
+ @session_id = frame.headers["session"]
439
+ @server_name = frame.headers["server"]
440
+ if frame.headers["heart-beat"]
441
+ @heartbeat = Heartbeat.new(self, frame.headers["heart-beat"])
442
+ @heartbeat.start
443
+ end
444
+ @connected = true
445
+ on_connected(frame, @session_id, @server_name)
446
+ true
447
+ end
448
+
449
+ # Handle message from server
450
+ # Attempt to decode body if not text
451
+ #
452
+ # @param [Frame] frame received from server
453
+ # @param [String, NilClass] body for frame after optional decoding
454
+ #
455
+ # @return [TrueClass] always true
456
+ #
457
+ # @raise [ApplicationError] subscription not found, subscription does not
458
+ # match destination, duplicate ack ID
459
+ def receive_message(frame, body)
460
+ required = {"destination" => [], "message-id" => [], "subscription" => ["1.0"]}
461
+ destination, message_id, subscribe_id = frame.require(@version, required)
462
+ if (subscribe = @subscribes[destination])
463
+ if subscribe[:id] != subscribe_id && @version != "1.0"
464
+ raise ApplicationError.new("Subscription does not match destination '#{destination}'", frame)
465
+ end
466
+ ack_id = nil
467
+ if subscribe[:ack] != "auto"
468
+ # Create ack ID if there is none so that user of this class can always rely
469
+ # on its use for ack/nack and then correspondingly track message IDs so that
470
+ # convert back to ack ID when needed
471
+ ack_id = frame.require(@version, "ack" => ["1.0", "1.1"])
472
+ ack_id ||= (@ack_id += 1).to_s
473
+ if (message_id2 = @message_ids[ack_id])
474
+ raise ApplicationError.new("Duplicate ack #{ack_id} for messages #{message_id2} and #{message_id}", frame)
475
+ end
476
+ @message_ids[ack_id] = message_id
477
+ end
478
+ else
479
+ raise ApplicationError.new("Subscription to '#{destination}' not found", frame)
480
+ end
481
+ content_type = frame.headers["content-type"] || "text/plain"
482
+ on_message(frame, destination, body, content_type, message_id, ack_id)
483
+ true
484
+ end
485
+
486
+ # Handle receipt acknowledgement from server for frame sent earlier
487
+ #
488
+ # @param [Frame] frame received from server
489
+ # @param [String, NilClass] body for frame after optional decoding
490
+ #
491
+ # @return [TrueClass] always true
492
+ #
493
+ # @raise [ProtocolError] missing header
494
+ # @raise [ApplicationError] request for receipt not found
495
+ def receive_receipt(frame, body)
496
+ id = frame.require(@version, "receipt-id" => [])
497
+ raise ApplicationError.new("Request not found matching receipt #{id}") if @receipted_frames.delete(id).nil?
498
+ on_receipt(frame, id)
499
+ end
500
+
501
+ # Handle error reported by server
502
+ #
503
+ # @param [Frame] frame received from server
504
+ # @param [String, NilClass] body for frame after optional decoding
505
+ #
506
+ # @return [TrueClass] always true
507
+ def receive_error(frame, body)
508
+ on_error(frame, frame.headers["message"], body, frame.headers["receipt-id"])
509
+ true
510
+ end
511
+
512
+ ##########################
513
+ ## STOMP Support Functions
514
+ ##########################
515
+
516
+ # Process all complete frames that have been received
517
+ #
518
+ # @return [TrueClass] always true
519
+ def process_frames
520
+ while (frame = @parser.next) do process_frame(frame) end
521
+ true
522
+ end
523
+
524
+ # Process frame received from server
525
+ # Optionally JSON-decode body if "content-type" is "application/json"
526
+ #
527
+ # @param [Frame] frame received; body updated on return if is decoded
528
+ #
529
+ # @return [TrueClass] always true
530
+ #
531
+ # @raise [ProtocolError] unhandled frame
532
+ def process_frame(frame)
533
+ command = frame.command.downcase.to_sym
534
+ raise ProtocolError.new("Unhandled frame: #{frame.command}", frame) unless SERVER_COMMANDS.include?(command)
535
+ if (body = frame.body) && !body.empty? && frame.headers["content-type"] == "application/json" && @options[:auto_json]
536
+ body = JSON.load(body)
537
+ end
538
+ send(("receive_" + command.to_s).to_sym, frame, body)
539
+ end
540
+
541
+ # Send frame to server
542
+ # Optionally JSON-encode body if "content-type" is "application/json"
543
+ #
544
+ # @param [String] command name
545
+ # @param [Hash, NilClass] headers for frame; others added if there is a body
546
+ # @param [String, NilClass] body of message
547
+ # @param [String, NilClass] content_type per MIME; defaults to "text/plain"
548
+ # @param [Boolean, NilClass] receipt enabled (or'd with global setting)
549
+ # @param [String, NilClass] transaction_id uniquely identifying transaction
550
+ #
551
+ # @return [Frame] frame sent
552
+ #
553
+ # @raise [ApplicationError] transaction not found
554
+ def send_frame(command, headers = nil, body = nil, content_type = nil, receipt = nil, transaction_id = nil)
555
+ headers ||= {}
556
+ if body && !body.empty?
557
+ headers["content-type"] = content_type || "text/plain"
558
+ body = JSON.dump(body) if content_type == "application/json" && @options[:auto_json]
559
+ headers["content-length"] = body.size.to_s
560
+ else
561
+ body = ""
562
+ end
563
+ if transaction_id
564
+ transaction_id = transaction_id.to_s
565
+ raise ApplicationError.new("Transaction not found") unless @transaction_ids.index(transaction_id)
566
+ headers["transaction"] = transaction_id
567
+ end
568
+ frame = StompOut::Frame.new(command, headers, body)
569
+ if (receipt || @receipt) && command != "CONNECT"
570
+ receipt_id = frame.headers["receipt"] = (@receipt_id += 1).to_s
571
+ @receipted_frames[receipt_id] = frame
572
+ end
573
+ send_data(frame.to_s)
574
+ @heartbeat.sent_data if @heartbeat
575
+ frame
576
+ end
577
+
578
+ end # Client
579
+
580
+ end # StompOut
@@ -0,0 +1,67 @@
1
+ # Copyright (c) 2015 RightScale Inc
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining
4
+ # a copy of this software and associated documentation files (the
5
+ # "Software"), to deal in the Software without restriction, including
6
+ # without limitation the rights to use, copy, modify, merge, publish,
7
+ # distribute, sublicense, and/or sell copies of the Software, and to
8
+ # permit persons to whom the Software is furnished to do so, subject to
9
+ # the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be
12
+ # included in all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
22
+ module StompOut
23
+
24
+ # Exception for STOMP protocol violations
25
+ class ProtocolError < RuntimeError
26
+
27
+ # [Hash] Headers to be included in an ERROR response
28
+ attr_reader :headers
29
+
30
+ # [String, NilClass] Contents of "receipt" header in frame causing error
31
+ attr_reader :receipt
32
+
33
+ # [Frame, NilClass] Frame for which error occurred
34
+ attr_reader :frame
35
+
36
+ # Create exception
37
+ #
38
+ # @param [String] message describing error
39
+ # @param [Frame, NilClass] frame that caused error
40
+ # @param [Hash, NilClass] headers to be included in an ERROR response
41
+ def initialize(message, frame = nil, headers = nil)
42
+ @frame = frame
43
+ @headers = headers || {}
44
+ super(message)
45
+ end
46
+
47
+ end
48
+
49
+ # Exception for application level STOMP protocol violations, i.e.,
50
+ # for any additional rules that the application applying STOMP imposes
51
+ class ApplicationError < RuntimeError
52
+
53
+ # [Frame, NilClass] Frame for which error occurred
54
+ attr_reader :frame
55
+
56
+ # Create exception
57
+ #
58
+ # @param [String] message describing error
59
+ # @param [Frame, NilClass] frame that caused error
60
+ def initialize(message, frame = nil)
61
+ @frame = frame
62
+ super(message)
63
+ end
64
+
65
+ end
66
+
67
+ end # StompOut