stomp_out 0.1.0

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