onstomp 1.0.0pre1

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.
Files changed (80) hide show
  1. data/.autotest +2 -0
  2. data/.gitignore +6 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +4 -0
  6. data/DeveloperNarrative.md +15 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.md +221 -0
  9. data/README.md +73 -0
  10. data/Rakefile +6 -0
  11. data/UserNarrative.md +8 -0
  12. data/examples/basic.rb +40 -0
  13. data/examples/events.rb +72 -0
  14. data/lib/onstomp/client.rb +152 -0
  15. data/lib/onstomp/components/frame.rb +108 -0
  16. data/lib/onstomp/components/frame_headers.rb +212 -0
  17. data/lib/onstomp/components/nil_processor.rb +20 -0
  18. data/lib/onstomp/components/scopes/header_scope.rb +25 -0
  19. data/lib/onstomp/components/scopes/receipt_scope.rb +25 -0
  20. data/lib/onstomp/components/scopes/transaction_scope.rb +191 -0
  21. data/lib/onstomp/components/scopes.rb +45 -0
  22. data/lib/onstomp/components/subscription.rb +30 -0
  23. data/lib/onstomp/components/threaded_processor.rb +62 -0
  24. data/lib/onstomp/components/uri.rb +30 -0
  25. data/lib/onstomp/components.rb +13 -0
  26. data/lib/onstomp/connections/base.rb +208 -0
  27. data/lib/onstomp/connections/heartbeating.rb +82 -0
  28. data/lib/onstomp/connections/serializers/stomp_1.rb +166 -0
  29. data/lib/onstomp/connections/serializers/stomp_1_0.rb +41 -0
  30. data/lib/onstomp/connections/serializers/stomp_1_1.rb +134 -0
  31. data/lib/onstomp/connections/serializers.rb +9 -0
  32. data/lib/onstomp/connections/stomp_1.rb +69 -0
  33. data/lib/onstomp/connections/stomp_1_0.rb +28 -0
  34. data/lib/onstomp/connections/stomp_1_1.rb +65 -0
  35. data/lib/onstomp/connections.rb +119 -0
  36. data/lib/onstomp/interfaces/client_configurable.rb +55 -0
  37. data/lib/onstomp/interfaces/client_events.rb +168 -0
  38. data/lib/onstomp/interfaces/connection_events.rb +62 -0
  39. data/lib/onstomp/interfaces/event_manager.rb +69 -0
  40. data/lib/onstomp/interfaces/frame_methods.rb +190 -0
  41. data/lib/onstomp/interfaces/receipt_manager.rb +33 -0
  42. data/lib/onstomp/interfaces/subscription_manager.rb +48 -0
  43. data/lib/onstomp/interfaces/uri_configurable.rb +106 -0
  44. data/lib/onstomp/interfaces.rb +14 -0
  45. data/lib/onstomp/version.rb +13 -0
  46. data/lib/onstomp.rb +147 -0
  47. data/onstomp.gemspec +29 -0
  48. data/spec/onstomp/client_spec.rb +265 -0
  49. data/spec/onstomp/components/frame_headers_spec.rb +163 -0
  50. data/spec/onstomp/components/frame_spec.rb +144 -0
  51. data/spec/onstomp/components/nil_processor_spec.rb +32 -0
  52. data/spec/onstomp/components/scopes/header_scope_spec.rb +27 -0
  53. data/spec/onstomp/components/scopes/receipt_scope_spec.rb +33 -0
  54. data/spec/onstomp/components/scopes/transaction_scope_spec.rb +227 -0
  55. data/spec/onstomp/components/scopes_spec.rb +63 -0
  56. data/spec/onstomp/components/subscription_spec.rb +58 -0
  57. data/spec/onstomp/components/threaded_processor_spec.rb +92 -0
  58. data/spec/onstomp/components/uri_spec.rb +33 -0
  59. data/spec/onstomp/connections/base_spec.rb +349 -0
  60. data/spec/onstomp/connections/heartbeating_spec.rb +132 -0
  61. data/spec/onstomp/connections/serializers/stomp_1_0_spec.rb +50 -0
  62. data/spec/onstomp/connections/serializers/stomp_1_1_spec.rb +99 -0
  63. data/spec/onstomp/connections/serializers/stomp_1_spec.rb +104 -0
  64. data/spec/onstomp/connections/stomp_1_0_spec.rb +54 -0
  65. data/spec/onstomp/connections/stomp_1_1_spec.rb +137 -0
  66. data/spec/onstomp/connections/stomp_1_spec.rb +113 -0
  67. data/spec/onstomp/connections_spec.rb +135 -0
  68. data/spec/onstomp/interfaces/client_events_spec.rb +108 -0
  69. data/spec/onstomp/interfaces/connection_events_spec.rb +55 -0
  70. data/spec/onstomp/interfaces/event_manager_spec.rb +72 -0
  71. data/spec/onstomp/interfaces/frame_methods_spec.rb +109 -0
  72. data/spec/onstomp/interfaces/receipt_manager_spec.rb +53 -0
  73. data/spec/onstomp/interfaces/subscription_manager_spec.rb +64 -0
  74. data/spec/onstomp_spec.rb +15 -0
  75. data/spec/spec_helper.rb +12 -0
  76. data/spec/support/custom_argument_matchers.rb +51 -0
  77. data/spec/support/frame_matchers.rb +88 -0
  78. data/spec/support/shared_frame_method_examples.rb +116 -0
  79. data/yard_extensions.rb +32 -0
  80. metadata +219 -0
@@ -0,0 +1,62 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # An IO processor that does its work on its own thread.
4
+ class OnStomp::Components::ThreadedProcessor
5
+ # Creates a new processor for the {OnStomp::Client client}
6
+ # @param [OnStomp::Client] client
7
+ def initialize client
8
+ @client = client
9
+ @run_thread = nil
10
+ end
11
+
12
+ # Returns true if its IO thread has been created and is alive, otherwise
13
+ # false.
14
+ # @return [true,false]
15
+ def running?
16
+ @run_thread && @run_thread.alive?
17
+ end
18
+
19
+ # Starts the processor by creating a new thread that continually invokes
20
+ # {OnStomp::Connections::Base#io_process} while the client is
21
+ # {OnStomp::Client#connected? connected}.
22
+ # @return [self]
23
+ def start
24
+ @run_thread = Thread.new do
25
+ begin
26
+ while @client.connected?
27
+ @client.connection.io_process
28
+ end
29
+ rescue OnStomp::StopReceiver
30
+ rescue Exception
31
+ raise
32
+ end
33
+ end
34
+ self
35
+ end
36
+
37
+ # Causes the thread this method was invoked in to +pass+ until the
38
+ # processor is no longer {#running? running}.
39
+ # @return [self]
40
+ def join
41
+ Thread.pass while running?
42
+ @run_thread && @run_thread.join
43
+ self
44
+ end
45
+
46
+ # Forcefully stops the processor and joins its IO thread to the
47
+ # callee's thread.
48
+ # @return [self]
49
+ # @raise [IOError, SystemCallError] if either were raised in the IO thread
50
+ # and the {OnStomp::Client client} is still
51
+ # {OnStomp::Client#connected? connected} after the thread is joined.
52
+ def stop
53
+ begin
54
+ @run_thread.raise OnStomp::StopReceiver if @run_thread.alive?
55
+ @run_thread.join
56
+ rescue IOError, SystemCallError
57
+ raise if @client.connected?
58
+ end
59
+ @run_thread = nil
60
+ self
61
+ end
62
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Subclasses of URI::Generic to ease working with Stomp URIs.
4
+ module OnStomp::Components::URI
5
+ # A URI class for representing URIs with a 'stomp' scheme.
6
+ class STOMP < ::URI::Generic
7
+ # The default port to use for these kinds of URI objects when none has
8
+ # been specified.
9
+ DEFAULT_PORT = 61613
10
+ # The type of socket to use with these kinds of URI objects.
11
+ # @return [:tcp]
12
+ def onstomp_socket_type; :tcp; end
13
+ end
14
+
15
+ # A URI class for representing URIs with a 'stomp+ssl' scheme.
16
+ class STOMP_SSL < STOMP
17
+ # The default port to use for these kinds of URI objects when none has
18
+ # been specified.
19
+ DEFAULT_PORT = 61612
20
+ # The type of socket to use with these kinds of URI objects.
21
+ # @return [:ssl]
22
+ def onstomp_socket_type; :ssl; end
23
+ end
24
+ end
25
+
26
+ # Add the new URI classes to +URI+'s set of known schemes.
27
+ module ::URI
28
+ @@schemes['STOMP'] = OnStomp::Components::URI::STOMP
29
+ @@schemes['STOMP+SSL'] = OnStomp::Components::URI::STOMP_SSL
30
+ end
@@ -0,0 +1,13 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Namespace for various components used by OnStomp library.
4
+ module OnStomp::Components
5
+ end
6
+
7
+ require 'onstomp/components/uri'
8
+ require 'onstomp/components/frame_headers'
9
+ require 'onstomp/components/frame'
10
+ require 'onstomp/components/subscription'
11
+ require 'onstomp/components/nil_processor'
12
+ require 'onstomp/components/threaded_processor'
13
+ require 'onstomp/components/scopes'
@@ -0,0 +1,208 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Common behavior for all connections.
4
+ class OnStomp::Connections::Base
5
+ include OnStomp::Interfaces::ConnectionEvents
6
+ attr_reader :version, :socket, :client
7
+ attr_reader :last_transmitted_at, :last_received_at
8
+
9
+ # The approximate maximum number of bytes to write per call to
10
+ # {#io_process_write}.
11
+ MAX_BYTES_PER_WRITE = 1024 * 8
12
+ # The maximum number of bytes to read per call to {#io_process_read}
13
+ MAX_BYTES_PER_READ = 1024 * 4
14
+
15
+ # Creates a new connection using the given {#socket} object and
16
+ # {OnStomp::Client client}. The {#socket} object will generally be a +TCPSocket+
17
+ # or an +OpenSSL::SSL::SSLSocket+ and must support the methods +read_nonblock+
18
+ # +write_nonblock+, and +close+.
19
+ # @param [TCPSocket,OpenSSL::SSL::SSLSocket] socket
20
+ # @param [OnStomp::Client] client
21
+ def initialize socket, client
22
+ @socket = socket
23
+ @write_mutex = Mutex.new
24
+ @closing = false
25
+ @write_buffer = []
26
+ @read_buffer = []
27
+ @client = client
28
+ @connection_up = false
29
+ end
30
+
31
+ # Performs any necessary configuration of the connection from the CONNECTED
32
+ # frame sent by the broker and a +Hash+ of pending callbacks. This method
33
+ # is called after the protocol negotiation has taken place between client
34
+ # and broker, and the connection that receives it will be the connection
35
+ # used by the client for the duration of the session.
36
+ # @param [OnStomp::Components::Frame] connected
37
+ # @param [{Symbol => Proc}] con_cbs
38
+ def configure connected, con_cbs
39
+ @version = connected.header?(:version) ? connected[:version] : '1.0'
40
+ install_bindings_from_client con_cbs
41
+ end
42
+
43
+ # Returns true if the socket has not been closed, false otherwise.
44
+ # @return [true,false]
45
+ def connected?
46
+ !socket.closed?
47
+ end
48
+
49
+ # Closes the {#socket}. If +blocking+ is true, the socket will be closed
50
+ # immediately, otherwies the socket will remain open until {#io_process_write}
51
+ # has finished writing all of its buffered data. Once this method has been
52
+ # invoked, {#write_frame_nonblock} will not enqueue any additional frames
53
+ # for writing.
54
+ # @param [true,false] blocking
55
+ def close blocking=false
56
+ @write_mutex.synchronize { @closing = true }
57
+ if blocking
58
+ io_process_write until @write_buffer.empty?
59
+ socket.close
60
+ end
61
+ end
62
+
63
+ # Exchanges the CONNECT/CONNECTED frame handshake with the broker and returns
64
+ # the version detected along with the received CONNECTED frame. The supplied
65
+ # list of headers will be merged into the CONNECT frame sent to the broker.
66
+ # @param [OnStomp::Client] client
67
+ # @param [Array<Hash>] headers
68
+ def connect client, *headers
69
+ write_frame_nonblock connect_frame(*headers)
70
+ client_con = nil
71
+ until client_con
72
+ io_process_write { |f| client_con ||= f }
73
+ end
74
+ broker_con = nil
75
+ until broker_con
76
+ io_process_read { |f| broker_con ||= f }
77
+ end
78
+ raise OnStomp::ConnectFailedError if broker_con.command != 'CONNECTED'
79
+ vers = broker_con.header?(:version) ? broker_con[:version] : '1.0'
80
+ raise OnStomp::UnsupportedProtocolVersionError, vers unless client.versions.include?(vers)
81
+ @connection_up = true
82
+ [ vers, broker_con ]
83
+ end
84
+
85
+ # Checks if the missing method ends with '_frame', and if so raises a
86
+ # {OnStomp::UnsupportedCommandError} exception.
87
+ # @raise [OnStomp::UnsupportedCommandError]
88
+ def method_missing meth, *args, &block
89
+ if meth.to_s =~ /^(.*)_frame$/
90
+ raise OnStomp::UnsupportedCommandError, $1.upcase
91
+ else
92
+ super
93
+ end
94
+ end
95
+
96
+ # Makes a single call to {#io_process_write} and a single call to
97
+ # {#io_process_read}
98
+ def io_process &cb
99
+ io_process_write &cb
100
+ io_process_read &cb
101
+ if @connection_up && !connected?
102
+ triggered_close :died
103
+ end
104
+ end
105
+
106
+ # Serializes the given frame and adds the data to the connections internal
107
+ # write buffer
108
+ # @param [OnStomp::Components::Frame] frame
109
+ def write_frame_nonblock frame
110
+ ser = serializer.frame_to_bytes frame
111
+ push_write_buffer ser, frame
112
+ end
113
+
114
+ # Adds data and frame pair to the end of the write buffer
115
+ # @param [String] data
116
+ # @param [OnStomp::Components::Frame]
117
+ def push_write_buffer data, frame
118
+ @write_mutex.synchronize {
119
+ @write_buffer << [data, frame] unless @closing
120
+ }
121
+ end
122
+ # Removes the first data and frame pair from the write buffer
123
+ # @param [String] data
124
+ # @param [OnStomp::Components::Frame]
125
+ def shift_write_buffer
126
+ @write_mutex.synchronize { @write_buffer.shift }
127
+ end
128
+ # Adds the remains of data and frame pair to the head of the write buffer
129
+ # @param [String] data
130
+ # @param [OnStomp::Components::Frame]
131
+ def unshift_write_buffer data, frame
132
+ @write_mutex.synchronize { @write_buffer.unshift [data, frame] }
133
+ end
134
+
135
+ # Writes serialized frame data to the socket if the write buffer is not
136
+ # empty and socket is ready for writing. Once a complete frame has
137
+ # been written, this method will call {OnStomp::Client#dispatch_transmitted}
138
+ # to notify the client that the frame has been sent to the broker. If a
139
+ # complete frame cannot be written without blocking, the unsent data is
140
+ # sent to the head of the write buffer to be processed first the next time
141
+ # this method is invoked.
142
+ def io_process_write
143
+ if @write_buffer.length > 0 && IO.select(nil, [socket], nil, 0.1)
144
+ to_shift = @write_buffer.length / 3
145
+ written = 0
146
+ while written < MAX_BYTES_PER_WRITE
147
+ data, frame = shift_write_buffer
148
+ break unless data && connected?
149
+ begin
150
+ w = socket.write_nonblock(data)
151
+ written += w
152
+ @last_transmitted_at = Time.now
153
+ if w < data.length
154
+ unshift_write_buffer data[w..-1], frame
155
+ else
156
+ yield frame if block_given?
157
+ client.dispatch_transmitted frame
158
+ end
159
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
160
+ # writing will either block, or cannot otherwise be completed,
161
+ # put data back and try again some other day
162
+ unshift_write_buffer data, frame
163
+ break
164
+ rescue Exception
165
+ triggered_close :terminated
166
+ raise
167
+ end
168
+ end
169
+ end
170
+ if @write_buffer.empty? && @closing
171
+ triggered_close
172
+ end
173
+ end
174
+
175
+ # Reads serialized frame data from the socket if we're connected and
176
+ # and the socket is ready for reading. The received data will be pushed
177
+ # to the end of a read buffer, which is then sent to the connection's
178
+ # {OnStomp::Connections::Serializers serializer} for processing.
179
+ def io_process_read
180
+ if connected? && IO.select([socket], nil, nil, 0.1)
181
+ begin
182
+ data = socket.read_nonblock(MAX_BYTES_PER_READ)
183
+ @read_buffer << data
184
+ @last_received_at = Time.now
185
+ serializer.bytes_to_frame(@read_buffer) do |frame|
186
+ yield frame if block_given?
187
+ client.dispatch_received frame
188
+ end
189
+ rescue Errno::EINTR, Errno::EAGAIN, Errno::EWOULDBLOCK
190
+ # do not
191
+ rescue EOFError
192
+ triggered_close
193
+ rescue Exception
194
+ triggered_close :terminated
195
+ raise
196
+ end
197
+ end
198
+ end
199
+
200
+ private
201
+ def triggered_close *evs
202
+ @connection_up = false
203
+ @closing = false
204
+ socket.close
205
+ evs.each { |ev| trigger_connection_event ev }
206
+ trigger_connection_event :closed
207
+ end
208
+ end
@@ -0,0 +1,82 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Mixin for connections to include heartbeating functionality.
4
+ module OnStomp::Connections::Heartbeating
5
+ # A pair of integers indicating the maximum number of milliseconds the
6
+ # client and broker can go without transmitting data respectively. If
7
+ # either value is 0, the respective heartbeating is not enabled.
8
+ # @return [[Fixnum, Fixnum]]
9
+ attr_reader :heartbeating
10
+
11
+ # Configures heartbeating strategy by taking the maximum timeout
12
+ # for clients and brokers. If either pair contains a zero, the respective
13
+ # beating is disabled.
14
+ # @param [[Fixnum, Fixnum]] client_beats
15
+ # @param [[Fixnum, Fixnum]] broker_beats
16
+ def configure_heartbeating client_beats, broker_beats
17
+ c_x, c_y = client_beats
18
+ s_x, s_y = broker_beats
19
+ @heartbeating = [ (c_x == 0||s_y == 0 ? 0 : [c_x,s_y].max),
20
+ (c_y == 0||s_x == 0 ? 0 : [c_y,s_x].max) ]
21
+ end
22
+
23
+ # Returns true if both the client and broker are transmitting data in
24
+ # accordance with the heartbeating strategy. If this method returns false,
25
+ # the connection is effectively dead and should be {OnStomp::Connections::Base#close closed}
26
+ # @return [true, false]
27
+ def pulse?
28
+ client_pulse? && broker_pulse?
29
+ end
30
+
31
+ # Maximum number of milliseconds allowed between bytes being sent by
32
+ # the client, or 0 if there is no limit. This method will add a 10% margin
33
+ # of error to the timeout determined from heartbeat negotiation to allow a
34
+ # little slack before a connection is deemed dead.
35
+ # @return [Fixnum]
36
+ def heartbeat_client_limit
37
+ unless defined?(@heartbeat_client_limit)
38
+ @heartbeat_client_limit = heartbeating[0] > 0 ? (1.1 * heartbeating[0]) : 0
39
+ end
40
+ @heartbeat_client_limit
41
+ end
42
+
43
+ # Maximum number of milliseconds allowed between bytes being sent from
44
+ # the broker, or 0 if there is no limit. This method will add a 10% margin
45
+ # of error to the timeout determined from heartbeat negotiation to allow a
46
+ # little slack before a connection is deemed dead.
47
+ # @return [Fixnum]
48
+ def heartbeat_broker_limit
49
+ unless defined?(@heartbeat_broker_limit)
50
+ @heartbeat_broker_limit = heartbeating[1] > 0 ? (1.1 * heartbeating[1]) : 0
51
+ end
52
+ @heartbeat_broker_limit
53
+ end
54
+
55
+ # Number of milliseconds since data was last transmitted to the broker or
56
+ # +nil+ if no data has been transmitted when the method is called.
57
+ # @return [Fixnum, nil]
58
+ def duration_since_transmitted
59
+ last_transmitted_at && ((Time.now - last_transmitted_at)*1000).to_i
60
+ end
61
+
62
+ # Number of milliseconds since data was last received from the broker or
63
+ # +nil+ if no data has been received when the method is called.
64
+ # @return [Fixnum, nil]
65
+ def duration_since_received
66
+ last_received_at && ((Time.now - last_received_at)*1000).to_i
67
+ end
68
+
69
+ # Returns true if client-side heartbeating is disabled, or
70
+ # {#duration_since_transmitted} has not exceeded {#heartbeat_client_limit}
71
+ # @return [true, false]
72
+ def client_pulse?
73
+ heartbeat_client_limit == 0 || duration_since_transmitted <= heartbeat_client_limit
74
+ end
75
+
76
+ # Returns true if broker-side heartbeating is disabled, or
77
+ # {#duration_since_received} has not exceeded {#heartbeat_broker_limit}
78
+ # @return [true, false]
79
+ def broker_pulse?
80
+ heartbeat_broker_limit == 0 || duration_since_received <= heartbeat_broker_limit
81
+ end
82
+ end
@@ -0,0 +1,166 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Classes that mix this in must define +split_header+ and +prepare_parsed_frame+
4
+ # The method +frame_to_string_base+ is provided as a factoring out of the
5
+ # common tasks of serializing a frame for STOMP 1.0 and STOMP 1.1.
6
+ module OnStomp::Connections::Serializers::Stomp_1
7
+ # Resets the parser that converts byte strings to {OnStomp::Components::Frame frames}
8
+ def reset_parser
9
+ @parse_accum = ''
10
+ @cur_frame = nil
11
+ @parse_state = :command
12
+ end
13
+
14
+ # The common elements of serializing a {OnStomp::Components::Frame frame}
15
+ # as a string in STOMP 1.0 and STOMP 1.1 protocols.
16
+ def frame_to_string_base frame
17
+ if frame.command
18
+ frame.force_content_length
19
+ str = "#{frame.command}\n"
20
+ frame.headers.inject(str) do |acc, (k,v)|
21
+ acc << yield(k,v)
22
+ end
23
+ str << "\n"
24
+ str << "#{frame.body}" if frame.body
25
+ str << "\000"
26
+ str
27
+ else
28
+ "\n"
29
+ end
30
+ end
31
+
32
+ # Parses a {OnStomp::Components::Frame frame} command from the buffer
33
+ # @param [Array<String>] buffer
34
+ def parse_command buffer
35
+ data = buffer.shift
36
+ eol = data.index("\n")
37
+ if eol
38
+ parser_flush(buffer, data, eol, :finish_command)
39
+ else
40
+ @parse_accum << data
41
+ end
42
+ end
43
+
44
+ # Parses a {OnStomp::Components::Frame frame} header line from the buffer
45
+ # @param [Array<String>] buffer
46
+ def parse_header_line buffer
47
+ data = buffer.shift
48
+ eol = data.index("\n")
49
+ if eol
50
+ parser_flush(buffer, data, eol, :finish_header_line)
51
+ else
52
+ @parse_accum << data
53
+ end
54
+ end
55
+
56
+ # Parses a {OnStomp::Components::Frame frame} body from the buffer
57
+ # @param [Array<String>] buffer
58
+ def parse_body buffer
59
+ data = buffer.shift
60
+ if rlen = @cur_frame.content_length
61
+ rlen -= @parse_accum.length
62
+ end
63
+ body_upto = rlen ? (rlen < data.length && rlen) : data.index("\000")
64
+ if body_upto
65
+ if data[body_upto, 1] != "\000"
66
+ raise OnStomp::MalformedFrameError, 'missing terminator'
67
+ end
68
+ parser_flush(buffer, data, body_upto, :finish_body)
69
+ else
70
+ @parse_accum << data
71
+ end
72
+ end
73
+
74
+ # Adds the substring +data[0...idx]+ to the parser's accumulator,
75
+ # unshifts the remaining data back onto the buffer, and calls +meth+
76
+ # with the parser's accumulated string.
77
+ # @param [Array<String>] buffer
78
+ # @param [String] data
79
+ # @param [Fixnum] idx
80
+ # @param [Symbol] meth
81
+ def parser_flush buffer, data, idx, meth
82
+ remain = data[(idx+1)..-1]
83
+ buffer.unshift(remain) unless remain.empty?
84
+ __send__ meth, (@parse_accum + data[0...idx])
85
+ @parse_accum = ''
86
+ end
87
+
88
+ # Called when a frame's command has been fully read from the buffer. This
89
+ # method will create a new "current frame", set its
90
+ # {OnStomp::Components::Frame#command command} attribute, and tell the parser
91
+ # to move on to the next state.
92
+ # @param [String] command
93
+ def finish_command command
94
+ @cur_frame = OnStomp::Components::Frame.new
95
+ if command.empty?
96
+ @parse_state = :completed
97
+ else
98
+ @cur_frame.command = command
99
+ @parse_state = :header_line
100
+ end
101
+ end
102
+
103
+ # Called when a header line has been fully read from the buffer. This
104
+ # method will split the header line into a name/value pair,
105
+ # {OnStomp::Components::FrameHeaders#append append} the
106
+ # header to the "current frame" and tell the parser to move on to the next
107
+ # state
108
+ # @param [String] headline
109
+ def finish_header_line headline
110
+ if headline.empty?
111
+ @parse_state = :body
112
+ else
113
+ k,v = split_header(headline)
114
+ @cur_frame.headers.append(k, v)
115
+ end
116
+ end
117
+
118
+ # Called when a frame's body has been fully read from the buffer. This
119
+ # method will set the frame's {OnStomp::Components::Frame#body body}
120
+ # attribute, call +prepare_parsed_frame+ with the "current frame",
121
+ # and tell the parser to move on to the next state.
122
+ # @param [String] body
123
+ def finish_body body
124
+ @cur_frame.body = body
125
+ prepare_parsed_frame @cur_frame
126
+ @parse_state = :completed
127
+ end
128
+
129
+ # Takes a buffer of strings and constructs all the
130
+ # {OnStomp::Components::Frame frames} it can from the data. The parser
131
+ # builds a "current frame" and updates it with attributes as they are
132
+ # parsed from the buffer. It is only safe to invoke this method from a
133
+ # single thread as no synchronization is being performed. This will work
134
+ # fine with the {OnStomp::Components::ThreadedProcessor threaded} processor
135
+ # as it performs its calls all within a single thread, but if you wish
136
+ # to develop your own processor that can call
137
+ # {OnStomp::Connections::Base#io_process_read} across separate threads, you
138
+ # will have to implement your own synchronization strategy.
139
+ # @note It is NOT safe to invoke this method from multiple threads as-is.
140
+ # @param [Array<String>] buffer
141
+ def bytes_to_frame buffer
142
+ until buffer.first.nil? && @parse_state != :completed
143
+ if @parse_state == :completed
144
+ yield @cur_frame
145
+ reset_parser
146
+ else
147
+ __send__(:"parse_#{@parse_state}", buffer)
148
+ end
149
+ end
150
+ end
151
+
152
+ if RUBY_VERSION >= "1.9"
153
+ # Takes the result of +frame_to_string+ and forces it to use a binary
154
+ # encoding
155
+ # @return [String] string serialization of frame with 'ASCII-8BIT' encoding
156
+ def frame_to_bytes frame
157
+ frame_to_string(frame).tap { |s| s.force_encoding('ASCII-8BIT') }
158
+ end
159
+ else
160
+ # Takes the result of +frame_to_string+ and passes it along. Ruby 1.8.7
161
+ # treats strings as a collection of bytes, so we don't need to do any
162
+ # further work.
163
+ # @return [String]
164
+ def frame_to_bytes(frame); frame_to_string(frame); end
165
+ end
166
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ # Frame serializer / parser for STOMP 1.0 connections.
4
+ class OnStomp::Connections::Serializers::Stomp_1_0
5
+ include OnStomp::Connections::Serializers::Stomp_1
6
+
7
+ # Creates a new serializer and calls {#reset_parser}
8
+ def initialize
9
+ reset_parser
10
+ end
11
+
12
+ # Converts a {OnStomp::Components::Frame frame} to a string
13
+ # @param [OnStomp::Components::Frame] frame
14
+ # @return [String]
15
+ def frame_to_string frame
16
+ frame_to_string_base(frame) do |k,v|
17
+ "#{k.gsub(/[\n:]/, '')}:#{v.gsub(/\n/, '')}\n"
18
+ end
19
+ end
20
+
21
+ # Splits a header line into a header name / header value pair at the first
22
+ # ':' character and returns the pair.
23
+ # @param [String] str header line to split
24
+ # @return [[String, String]]
25
+ # @raise [OnStomp::MalformedHeaderError] if the header line
26
+ # lacks a ':' character
27
+ def split_header(str)
28
+ col = str.index(':')
29
+ unless col
30
+ raise OnStomp::MalformedHeaderError, "unterminated header: '#{str}'"
31
+ end
32
+ [ str[0...col], str[(col+1)..-1] ]
33
+ end
34
+
35
+ # Nothing special needs to be done with frames parsed from a STOMP 1.0
36
+ # connection, so this is a no-op.
37
+ # @param [OnStomp::Components::Frame] frame
38
+ # @return [nil]
39
+ def prepare_parsed_frame frame
40
+ end
41
+ end