onstomp 1.0.0pre1

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