stomper 1.0.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (152) hide show
  1. data/.gitignore +5 -0
  2. data/{spec/spec.opts → .rspec} +0 -2
  3. data/Gemfile +4 -0
  4. data/LICENSE +201 -201
  5. data/README.md +130 -0
  6. data/Rakefile +5 -0
  7. data/examples/basic.rb +38 -0
  8. data/examples/events.rb +54 -0
  9. data/features/acking_messages.feature +147 -0
  10. data/features/disconnecting.feature +12 -0
  11. data/features/establish_connection.feature +44 -0
  12. data/features/protocol_version_negotiation.feature +61 -0
  13. data/features/receipts.feature +72 -0
  14. data/features/scopes.feature +32 -0
  15. data/features/secure_connections.feature +38 -0
  16. data/features/send_and_message.feature +28 -0
  17. data/features/steps/acking_messages_steps.rb +39 -0
  18. data/features/steps/disconnecting_steps.rb +8 -0
  19. data/features/steps/establish_connection_steps.rb +74 -0
  20. data/features/steps/frame_transmission_steps.rb +35 -0
  21. data/features/steps/protocol_version_negotiation_steps.rb +15 -0
  22. data/features/steps/receipts_steps.rb +79 -0
  23. data/features/steps/scopes_steps.rb +52 -0
  24. data/features/steps/secure_connections_steps.rb +41 -0
  25. data/features/steps/send_and_message_steps.rb +35 -0
  26. data/features/steps/subscribing_steps.rb +36 -0
  27. data/features/steps/threaded_receiver_steps.rb +8 -0
  28. data/features/steps/transactions_steps.rb +0 -0
  29. data/features/subscribing.feature +151 -0
  30. data/features/support/env.rb +11 -0
  31. data/features/support/header_helpers.rb +12 -0
  32. data/features/support/ssl/README +6 -0
  33. data/features/support/ssl/broker_cert.csr +17 -0
  34. data/features/support/ssl/broker_cert.pem +72 -0
  35. data/features/support/ssl/broker_key.pem +27 -0
  36. data/features/support/ssl/client_cert.csr +17 -0
  37. data/features/support/ssl/client_cert.pem +72 -0
  38. data/features/support/ssl/client_key.pem +27 -0
  39. data/features/support/ssl/demoCA/cacert.pem +17 -0
  40. data/features/support/ssl/demoCA/index.txt +2 -0
  41. data/features/support/ssl/demoCA/index.txt.attr +1 -0
  42. data/features/support/ssl/demoCA/index.txt.attr.old +1 -0
  43. data/features/support/ssl/demoCA/index.txt.old +1 -0
  44. data/features/support/ssl/demoCA/newcerts/01.pem +72 -0
  45. data/features/support/ssl/demoCA/newcerts/02.pem +72 -0
  46. data/features/support/ssl/demoCA/private/cakey.pem +17 -0
  47. data/features/support/ssl/demoCA/serial +1 -0
  48. data/features/support/ssl/demoCA/serial.old +1 -0
  49. data/features/support/test_stomp_server.rb +150 -0
  50. data/features/threaded_receiver.feature +11 -0
  51. data/features/transactions.feature +66 -0
  52. data/lib/stomper.rb +30 -20
  53. data/lib/stomper/connection.rb +442 -102
  54. data/lib/stomper/errors.rb +59 -0
  55. data/lib/stomper/extensions.rb +10 -0
  56. data/lib/stomper/extensions/common.rb +258 -0
  57. data/lib/stomper/extensions/events.rb +213 -0
  58. data/lib/stomper/extensions/heartbeat.rb +101 -0
  59. data/lib/stomper/extensions/scoping.rb +56 -0
  60. data/lib/stomper/frame.rb +54 -0
  61. data/lib/stomper/frame_serializer.rb +217 -0
  62. data/lib/stomper/headers.rb +15 -0
  63. data/lib/stomper/receipt_manager.rb +36 -0
  64. data/lib/stomper/receivers.rb +7 -0
  65. data/lib/stomper/receivers/threaded.rb +71 -0
  66. data/lib/stomper/scopes.rb +9 -0
  67. data/lib/stomper/scopes/header_scope.rb +49 -0
  68. data/lib/stomper/scopes/receipt_scope.rb +44 -0
  69. data/lib/stomper/scopes/transaction_scope.rb +109 -0
  70. data/lib/stomper/sockets.rb +66 -28
  71. data/lib/stomper/subscription_manager.rb +79 -0
  72. data/lib/stomper/support.rb +68 -0
  73. data/lib/stomper/support/1.8/frame_serializer.rb +53 -0
  74. data/lib/stomper/support/1.8/headers.rb +183 -0
  75. data/lib/stomper/support/1.9/frame_serializer.rb +64 -0
  76. data/lib/stomper/support/1.9/headers.rb +172 -0
  77. data/lib/stomper/support/ruby.rb +13 -0
  78. data/lib/stomper/uris.rb +49 -0
  79. data/lib/stomper/version.rb +7 -0
  80. data/spec/spec_helper.rb +13 -9
  81. data/spec/stomper/connection_spec.rb +712 -0
  82. data/spec/stomper/extensions/common_spec.rb +187 -0
  83. data/spec/stomper/extensions/events_spec.rb +78 -0
  84. data/spec/stomper/extensions/heartbeat_spec.rb +103 -0
  85. data/spec/stomper/extensions/scoping_spec.rb +21 -0
  86. data/spec/stomper/frame_serializer_1.8_spec.rb +318 -0
  87. data/spec/stomper/frame_serializer_spec.rb +316 -0
  88. data/spec/stomper/frame_spec.rb +36 -0
  89. data/spec/stomper/headers_spec.rb +224 -0
  90. data/spec/stomper/receipt_manager_spec.rb +91 -0
  91. data/spec/stomper/receivers/threaded_spec.rb +116 -0
  92. data/spec/stomper/scopes/header_scope_spec.rb +42 -0
  93. data/spec/stomper/scopes/receipt_scope_spec.rb +51 -0
  94. data/spec/stomper/scopes/transaction_scope_spec.rb +183 -0
  95. data/spec/stomper/sockets_spec.rb +113 -0
  96. data/spec/stomper/subscription_manager_spec.rb +107 -0
  97. data/spec/stomper/support_spec.rb +69 -0
  98. data/spec/stomper/uris_spec.rb +54 -0
  99. data/spec/stomper_spec.rb +9 -0
  100. data/spec/support/custom_argument_matchers.rb +57 -0
  101. data/spec/support/existential_frame_matchers.rb +19 -0
  102. data/spec/support/frame_header_matchers.rb +10 -0
  103. data/stomper.gemspec +30 -0
  104. metadata +272 -97
  105. data/AUTHORS +0 -21
  106. data/CHANGELOG +0 -20
  107. data/README.rdoc +0 -120
  108. data/lib/stomper/client.rb +0 -34
  109. data/lib/stomper/frame_reader.rb +0 -73
  110. data/lib/stomper/frame_writer.rb +0 -21
  111. data/lib/stomper/frames.rb +0 -39
  112. data/lib/stomper/frames/abort.rb +0 -10
  113. data/lib/stomper/frames/ack.rb +0 -25
  114. data/lib/stomper/frames/begin.rb +0 -11
  115. data/lib/stomper/frames/client_frame.rb +0 -89
  116. data/lib/stomper/frames/commit.rb +0 -10
  117. data/lib/stomper/frames/connect.rb +0 -10
  118. data/lib/stomper/frames/connected.rb +0 -30
  119. data/lib/stomper/frames/disconnect.rb +0 -10
  120. data/lib/stomper/frames/error.rb +0 -21
  121. data/lib/stomper/frames/message.rb +0 -48
  122. data/lib/stomper/frames/receipt.rb +0 -19
  123. data/lib/stomper/frames/send.rb +0 -10
  124. data/lib/stomper/frames/server_frame.rb +0 -38
  125. data/lib/stomper/frames/subscribe.rb +0 -42
  126. data/lib/stomper/frames/unsubscribe.rb +0 -19
  127. data/lib/stomper/open_uri_interface.rb +0 -41
  128. data/lib/stomper/receipt_handlers.rb +0 -23
  129. data/lib/stomper/receiptor.rb +0 -38
  130. data/lib/stomper/subscriber.rb +0 -76
  131. data/lib/stomper/subscription.rb +0 -128
  132. data/lib/stomper/subscriptions.rb +0 -95
  133. data/lib/stomper/threaded_receiver.rb +0 -59
  134. data/lib/stomper/transaction.rb +0 -185
  135. data/lib/stomper/transactor.rb +0 -50
  136. data/lib/stomper/uri.rb +0 -55
  137. data/spec/client_spec.rb +0 -29
  138. data/spec/connection_spec.rb +0 -22
  139. data/spec/frame_reader_spec.rb +0 -37
  140. data/spec/frame_writer_spec.rb +0 -27
  141. data/spec/frames/client_frame_spec.rb +0 -66
  142. data/spec/frames/indirect_frame_spec.rb +0 -45
  143. data/spec/frames/server_frame_spec.rb +0 -85
  144. data/spec/open_uri_interface_spec.rb +0 -132
  145. data/spec/receiptor_spec.rb +0 -35
  146. data/spec/shared_connection_examples.rb +0 -79
  147. data/spec/subscriber_spec.rb +0 -77
  148. data/spec/subscription_spec.rb +0 -157
  149. data/spec/subscriptions_spec.rb +0 -145
  150. data/spec/threaded_receiver_spec.rb +0 -33
  151. data/spec/transaction_spec.rb +0 -139
  152. data/spec/transactor_spec.rb +0 -46
@@ -0,0 +1,101 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Provides the heart-beating interface for a {Stomper::Connection} object.
4
+ module Stomper::Extensions::Heartbeat
5
+ # Extends an object with any additional modules that are appropriate
6
+ # for the Stomp protocol being used.
7
+ def self.extend_by_protocol_version(instance, version)
8
+ if EXTEND_BY_VERSION[version]
9
+ EXTEND_BY_VERSION[version].each do |mod|
10
+ instance.extend mod
11
+ end
12
+ end
13
+ end
14
+
15
+ # By default, this method does nothing. If the established connection
16
+ # utilizes the Stomp 1.1 protocol, this method will be overridden by
17
+ # {Stomper::Protocols::V1_1::Heartbeating#beat}.
18
+ def beat; end
19
+
20
+ # By default, a connection is alive if it is connected.
21
+ # If the established connection utilizes the Stomp 1.1 protocol, this
22
+ # method will be overridden by {Stomper::Protocols::V1_1::Heartbeating#alive?}.
23
+ # @return [true,false]
24
+ # @see #dead?
25
+ def alive?
26
+ connected?
27
+ end
28
+
29
+ # A {Stomper::Connection connection} is dead if it is not +alive?+
30
+ # @return [true, false]
31
+ # @see #alive?
32
+ def dead?
33
+ !alive?
34
+ end
35
+
36
+ # Stomp Protocol 1.1 extensions to the heart-beating interface
37
+ module V1_1
38
+ # Send a heartbeat to the broker
39
+ def beat
40
+ transmit ::Stomper::Frame.new
41
+ end
42
+
43
+ # Stomp 1.1 {Stomper::Connection connections} are alive if they are
44
+ # +connected?+ and are meeting their negotiated heart-beating obligations.
45
+ # @return [true, false]
46
+ # @see #dead?
47
+ def alive?
48
+ connected? && client_alive? && broker_alive?
49
+ end
50
+
51
+ # Maximum number of milliseconds that can pass between frame / heartbeat
52
+ # transmissions before we consider the client to be dead.
53
+ # @return [Fixnum]
54
+ def heartbeat_client_limit
55
+ unless defined?(@heartbeat_client_limit)
56
+ @heartbeat_client_limit = heartbeating[0] > 0 ? (1.1 * heartbeating[0]) : 0
57
+ end
58
+ @heartbeat_client_limit
59
+ end
60
+
61
+ # Maximum number of milliseconds that can pass between frames / heartbeats
62
+ # received before we consider the broker to be dead.
63
+ # @return [Fixnum]
64
+ def heartbeat_broker_limit
65
+ unless defined?(@heartbeat_broker_limit)
66
+ @heartbeat_broker_limit = heartbeating[1] > 0 ? (1.1 * heartbeating[1]) : 0
67
+ end
68
+ @heartbeat_broker_limit
69
+ end
70
+
71
+ # Returns true if the client is alive. Client is alive if client heartbeating
72
+ # is disabled, or the number of milliseconds that have passed since last
73
+ # transmission is less than or equal to {#heartbeat_client_limit client} limit
74
+ # @return [true,false]
75
+ # @see #heartbeat_client_limit
76
+ # @see #broker_alive?
77
+ def client_alive?
78
+ # Consider some benchmarking to determine if this is faster than
79
+ # re-writing the method after its first invocation.
80
+ heartbeat_client_limit == 0 ||
81
+ duration_since_transmitted <= heartbeat_client_limit
82
+ end
83
+
84
+ # Returns true if the broker is alive. Broker is alive if broker heartbeating
85
+ # is disabled, or the number of milliseconds that have passed since last
86
+ # receiving is less than or equal to {#heartbeat_broker_limit broker} limit
87
+ # @return [true,false]
88
+ # @see #heartbeat_broker_limit
89
+ # @see #client_alive?
90
+ def broker_alive?
91
+ heartbeat_broker_limit == 0 ||
92
+ duration_since_received <= heartbeat_broker_limit
93
+ end
94
+ end
95
+
96
+ # A mapping between protocol versions and modules to include
97
+ EXTEND_BY_VERSION = {
98
+ '1.0' => [ ],
99
+ '1.1' => [ ::Stomper::Extensions::Heartbeat::V1_1 ]
100
+ }
101
+ end
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Provides the scoping interface for a {Stomper::Connection} object.
4
+ module Stomper::Extensions::Scoping
5
+ # Creates a new {Stomper::Scopes::TransactionScope} to perform
6
+ # a transaction. If a block is provided, all SEND, ACK, NACK, COMMIT and
7
+ # ABORT frames generated within the block are bound to the same transaction.
8
+ # Further, if an exception is raised within the block, the transaction is
9
+ # rolled back through an ABORT frame, otherwise it is automatically committed
10
+ # through a COMMIT frame. If a block is not provided, the transaction must
11
+ # be manually aborted or committed through the returned
12
+ # {Stomper::Scopes::TransactionScope} object.
13
+ # @param [{Symbol => Object}] headers
14
+ # @yield [tx] block is evaluated as a transaction
15
+ # @yieldparam [Stomper::Scopes::TransactionScope] tx
16
+ # @return [Stomper::Scopes::TransactionScope]
17
+ # @example Gonna need an example or two
18
+ def with_transaction(headers={}, &block)
19
+ create_scope(::Stomper::Scopes::TransactionScope, headers, block)
20
+ end
21
+
22
+ # Creates a new {Stomper::Scopes::ReceiptScope} using
23
+ # a supplied block as the receipt handler. If no block is provided, no
24
+ # receipt handler is created; however, all frames generated through this
25
+ # {Stomper::Scopes::ReceiptScope} will still request a RECEIPT
26
+ # from the broker.
27
+ # @param [{Symbol => Object}] headers
28
+ # @yield [receipt] callback invoked upon receiving the RECEIPT frame
29
+ # @yieldparam [Stomper::Frame] the received RECEIPT frame
30
+ # @return [Stomper::Scopes::ReceiptScope]
31
+ # @example Gonna need an example or two
32
+ # @see Stomper::Extensions::Events#on_receipt}
33
+ def with_receipt(headers={}, &block)
34
+ create_scope(::Stomper::Scopes::ReceiptScope, headers, block)
35
+ end
36
+
37
+ # Creates a new {Stomper::Scopes::HeaderScope} from the
38
+ # supplied hash of headers. If a block is provided, it will be invoked with
39
+ # with this {Stomper::Scopes::HeaderScope} as its only parameter.
40
+ # @param [{Symbol => Object}] headers
41
+ # @yield [header_scope] block is evaluated applying the specified headers to
42
+ # all frames generated within the block.
43
+ # @yieldparam [Stomper::Scopes::HeaderScope] header_scope
44
+ # @return [Stomper::Scopes::HeaderScope]
45
+ # @example Gonna need an example or two
46
+ def with_headers(headers, &block)
47
+ create_scope(::Stomper::Scopes::HeaderScope, headers, block)
48
+ end
49
+
50
+ def create_scope(klass, headers, callback)
51
+ klass.new(self, headers).tap do |scoped|
52
+ scoped.apply_to(callback)
53
+ end
54
+ end
55
+ private :create_scope
56
+ end
@@ -0,0 +1,54 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A generic encapsulation of a frame as specified by the Stomp protocol.
4
+ class Stomper::Frame
5
+ # The command name of this frame (CONNECTED, SEND, RECEIPT, etc.)
6
+ # @return [String]
7
+ attr_accessor :command
8
+
9
+ # The body of this frame
10
+ # @return [String] if a body has been set
11
+ attr_accessor :body
12
+
13
+ # The headers associated with this frame
14
+ # @return [Stomper::Headers]
15
+ attr_reader :headers
16
+
17
+ # Creates a new frame. The frame will be initialized with the optional
18
+ # +command+ name, a {Stomper::Headers headers} collection initialized
19
+ # with the optional +headers+ hash, and an optional body.
20
+ def initialize(command=nil, headers={}, body=nil)
21
+ @command = command
22
+ @headers = ::Stomper::Headers.new(headers)
23
+ @body = body
24
+ end
25
+
26
+ # Gets the header value paired with the supplied name. This is a convenient
27
+ # shortcut for `frame.headers[name]`.
28
+ #
29
+ # @param [Object] name the header name associated with the desired value
30
+ # @return [String] the value associated with the requested header name
31
+ # @see Stomper::Headers#[]
32
+ # @example
33
+ # frame['content-type'] #=> 'text/plain'
34
+ def [](name); @headers[name]; end
35
+
36
+ # Sets the header value paired with the supplied name. This is a convenient
37
+ # shortcut for `frame.headers[name] = val`.
38
+ #
39
+ # @param [Object] name the header name to associate with the supplied value
40
+ # @param [Object] val the value to associate with the supplied header name
41
+ # @return [String] the supplied value as a string, or `nil` if `nil` was supplied as the value.
42
+ # @see Stomper::Headers#[]=
43
+ # @example
44
+ # frame['content-type'] = 'text/plain' #=> 'text/plain'
45
+ # frame['other header'] = 42 #=> '42'
46
+ def []=(name, val); @headers[name] = val; end
47
+
48
+ # A convenience method for getting the 'content-type' header without
49
+ # any parameters.
50
+ # @return [String]
51
+ def content_type
52
+ @headers[:'content-type'] && @headers[:'content-type'].split(';').first || ''
53
+ end
54
+ end
@@ -0,0 +1,217 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # This class serializes Stomp frames to IO streams. Submodule mixins within
4
+ # this class are used to adjust the serialization behavior depending upon
5
+ # the Stomp Protocol version being used.
6
+ # @see Stomper::Support::Ruby1_8::FrameSerializer Implementation for Ruby 1.8.7
7
+ # @see Stomper::Support::Ruby1_9::FrameSerializer Implementation for Ruby 1.9
8
+ class Stomper::FrameSerializer
9
+ # The character that must be present at the end of every Stomp frame.
10
+ FRAME_TERMINATOR = "\000"
11
+
12
+ # Creates a new frame serializer that will read {Stomper::Frame frames} from
13
+ # and write {Stomper::Frame frames} to the supplied IO object (typically
14
+ # a TCP or SSL socket.)
15
+ # @param [IO] io IO stream to read from and write to
16
+ def initialize(io)
17
+ @io = io
18
+ @write_mutex = ::Mutex.new
19
+ @read_mutex = ::Mutex.new
20
+ end
21
+
22
+ # Extends the serializer based on the version of the protocol being used.
23
+ # @param [String] version protocol version being used
24
+ # @return [self]
25
+ def extend_for_protocol(version)
26
+ if EXTEND_BY_VERSION[version]
27
+ EXTEND_BY_VERSION[version].each { |m| extend m }
28
+ end
29
+ self
30
+ end
31
+
32
+ # Serializes and writes a {Stomper::Frame} to the underlying IO stream. This
33
+ # includes setting the appropriate values for 'content-length' and
34
+ # 'content-type' headers, if applicable.
35
+ # @param [Stomper::Frame] frame
36
+ # @return [Stomper::Frame] the frame that was passed to the method
37
+ def write_frame(frame)
38
+ @write_mutex.synchronize { __write_frame__(frame) }
39
+ end
40
+
41
+ # Deserializes and returns a {Stomper::Frame} read from the underlying IO
42
+ # stream. If the IO stream produces data that violates the Stomp protocol
43
+ # specification, an instance of {Stomper::Errors::FatalProtocolError}, or
44
+ # one of its subclasses, will be raised.
45
+ # @return [Stomper::Frame]
46
+ # @raise [Stomper::Errors::MalformedFrameError] if the frame is not properly terminated
47
+ # @raise [Stomper::Errors::MalformedHeaderError] if a header is malformed (eg: contains no ':' separator)
48
+ def read_frame
49
+ @read_mutex.synchronize { __read_frame__ }
50
+ end
51
+
52
+ # Converts the headers of the supplied frame into a single "\n" delimited
53
+ # string that's suitable for writing to io.
54
+ # @param [Stomper::Frame] frame the frame whose headers should be serialized
55
+ # @return [String]
56
+ def serialize_headers(frame)
57
+ serialized = frame.headers.inject('') do |head_str, (k, v)|
58
+ k = escape_header_name(k)
59
+ next head_str if k.empty? || ['content-type', 'content-length'].include?(k)
60
+ head_str << "#{k}:#{escape_header_value(v)}\n"
61
+ end
62
+ if frame.body
63
+ if ct = determine_content_type(frame)
64
+ serialized << "content-type:#{ct}\n"
65
+ end
66
+ if clen = determine_content_length(frame)
67
+ serialized << "content-length:#{clen}\n"
68
+ end
69
+ end
70
+ serialized
71
+ end
72
+
73
+ # Escape a header name to comply with Stomp Protocol 1.0 specifications.
74
+ # All LF ("\n") and ":" characters are replaced with empty strings.
75
+ # @note If the connection is using the 1.1 protocol, this method will
76
+ # be overridden by {FrameSerializer::V1_1#escape_header_name}
77
+ # @param [String] str
78
+ # @return [String] escaped header name
79
+ def escape_header_name(str)
80
+ str.gsub(/[\n:]/, '')
81
+ end
82
+
83
+ # Escape a header value to comply with Stomp Protocol 1.0 specifications.
84
+ # All LF ("\n") characters are replaced with empty strings.
85
+ # @note If the connection is using the 1.1 protocol, this method will
86
+ # be overridden by {FrameSerializer::V1_1#escape_header_value}
87
+ # @param [String] str
88
+ # @return [String] escaped header value
89
+ def escape_header_value(str)
90
+ str.gsub(/\n/, '')
91
+ end
92
+
93
+ # Return the header name as it was passed. Stomp 1.0 does not provide
94
+ # any means for escaping special characters such as ":" and "\n"
95
+ # @note If the connection is using the 1.1 protocol, this method will
96
+ # be overridden by {FrameSerializer::V1_1#unescape_header_name}
97
+ # @param [String] str
98
+ # @return [String]
99
+ def unescape_header_name(str); str; end
100
+ alias :unescape_header_value :unescape_header_name
101
+
102
+ private
103
+ # These are the un-synchronized methods that do the real reading/writing
104
+ # of frames. This approach facilitates easier testing of Thread safety
105
+ def __write_frame__(frame)
106
+ if frame.command
107
+ @io.write [frame.command, "\n", serialize_headers(frame),
108
+ "\n", frame.body, FRAME_TERMINATOR].compact.join
109
+ else
110
+ @io.write "\n"
111
+ end
112
+ frame
113
+ end
114
+ def __read_frame__
115
+ command = @io.gets
116
+ return nil if command.nil?
117
+ command.chomp!
118
+ frame = Stomper::Frame.new
119
+ unless command.nil? || command.empty?
120
+ frame.command = command
121
+
122
+ while (header_line = get_header_line.chomp).length > 0
123
+ raise ::Stomper::Errors::MalformedHeaderError,
124
+ "unterminated header: '#{header_line}'" unless header_line.include? ':'
125
+ k, v = header_line.split(':', 2)
126
+ frame.headers.append(unescape_header_name(k), unescape_header_value(v))
127
+ end
128
+
129
+ body = nil
130
+ if frame[:'content-length'] && (len = frame[:'content-length'].to_i) > 0
131
+ body = @io.read len
132
+ raise ::Stomper::Errors::MalformedFrameError, "frame was not properly terminated" if get_body_byte
133
+ else
134
+ while (c = get_body_byte)
135
+ body ||= ""
136
+ body << c
137
+ end
138
+ end
139
+ frame.body = body && encode_body(body, frame[:'content-type'])
140
+ end
141
+ frame
142
+ end
143
+
144
+ # Stomp Protocol 1.1 specific frame writing / reading methods.
145
+ module V1_1
146
+ # Mapping of characters to their appropriate escape sequences. This
147
+ # is used when escaping headers for frames being written to the stream.
148
+ CHARACTER_ESCAPES = {
149
+ ':' => "\\c",
150
+ "\n" => "\\n",
151
+ "\\" => "\\\\"
152
+ }
153
+
154
+ # Mapping of escape sequences to their appropriate characters. This
155
+ # is used when unescaping headers being read from the stream.
156
+ ESCAPE_SEQUENCES = {
157
+ 'c' => ':',
158
+ '\\' => "\\",
159
+ 'n' => "\n"
160
+ }
161
+
162
+ # Escape a header name to comply with Stomp Protocol 1.1 specifications.
163
+ # All special characters (the keys of {CHARACTER_ESCAPES}) are replaced
164
+ # by their corresponding escape sequences.
165
+ # @param [String] str
166
+ # @return [String] escaped header name
167
+ def escape_header_name(hdr)
168
+ hdr.each_char.inject('') do |esc, ch|
169
+ esc << (CHARACTER_ESCAPES[ch] || ch)
170
+ end
171
+ end
172
+ alias :escape_header_value :escape_header_name
173
+
174
+ # Return the header name after known escape sequences have been
175
+ # translated to their respective values. The keys of {ESCAPE_SEQUENCES},
176
+ # prefixed with a '\' character, denote the allowed escape sequences.
177
+ # If an unknown escape sequence is encountered, an error is raised.
178
+ # @param [String] str header string to unescape
179
+ # @return [String] unescaped header string
180
+ # @raise [Stomper::Errors::InvalidHeaderEscapeSequenceError] if an
181
+ # unknown escape sequence is encountered within the string or if
182
+ # an escape sequence is not properly completed (the last character of
183
+ # +str+ is '\')
184
+ def unescape_header_name(str)
185
+ state = :read
186
+ str.each_char.inject('') do |unesc, ch|
187
+ case state
188
+ when :read
189
+ if ch == '\\'
190
+ state = :unescape
191
+ else
192
+ unesc << ch
193
+ end
194
+ when :unescape
195
+ state = :read
196
+ if ESCAPE_SEQUENCES[ch]
197
+ unesc << ESCAPE_SEQUENCES[ch]
198
+ else
199
+ raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError,
200
+ "invalid header escape sequence encountered '\\#{ch}'"
201
+ end
202
+ end
203
+ unesc
204
+ end.tap do
205
+ raise ::Stomper::Errors::InvalidHeaderEscapeSequenceError,
206
+ "incomplete escape sequence encountered in '#{str}'" if state != :read
207
+ end
208
+ end
209
+ alias :unescape_header_value :unescape_header_name
210
+ end
211
+
212
+ # The modules to mix-in to {FrameSerializer} instances depending upon which
213
+ # protocol version is being used.
214
+ EXTEND_BY_VERSION = {
215
+ '1.1' => [ ::Stomper::FrameSerializer::V1_1 ]
216
+ }
217
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # A specialized container for storing header name / value pairs for a Stomp
4
+ # {Stomper::Frame Frame}. This container behaves much like a +Hash+, but
5
+ # is specialized for the Stomp protocol. Header names are always converted
6
+ # into +String+s through the use of +to_s+ and may have more than one value
7
+ # associated with them.
8
+ #
9
+ # @note Header names are case sensitive, therefore the names 'header'
10
+ # and 'Header' will not refer to the same values.
11
+ # @see Stomper::Support::Ruby1_8::Headers Implementation for Ruby 1.8.7
12
+ # @see Stomper::Support::Ruby1_9::Headers Implementation for Ruby 1.9
13
+ class Stomper::Headers
14
+ include ::Enumerable
15
+ end