rjr 0.18.2 → 0.19.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +2 -0
- data/bin/rjr-client +16 -9
- data/bin/rjr-server +2 -1
- data/examples/client.rb +21 -19
- data/examples/server.rb +1 -1
- data/examples/structured_server.rb +1 -0
- data/examples/tcp.rb +1 -0
- data/lib/rjr/common.rb +1 -226
- data/lib/rjr/core_ext.rb +63 -0
- data/lib/rjr/dispatcher.rb +75 -219
- data/lib/rjr/messages.rb +8 -0
- data/lib/rjr/messages/compressed.rb +264 -0
- data/lib/rjr/messages/notification.rb +95 -0
- data/lib/rjr/messages/request.rb +99 -0
- data/lib/rjr/messages/response.rb +128 -0
- data/lib/rjr/node.rb +100 -97
- data/lib/rjr/node_callback.rb +43 -0
- data/lib/rjr/nodes/amqp.rb +12 -11
- data/lib/rjr/nodes/easy.rb +4 -4
- data/lib/rjr/nodes/local.rb +13 -12
- data/lib/rjr/nodes/multi.rb +1 -1
- data/lib/rjr/nodes/tcp.rb +15 -13
- data/lib/rjr/nodes/template.rb +4 -4
- data/lib/rjr/nodes/unix.rb +15 -13
- data/lib/rjr/nodes/web.rb +15 -14
- data/lib/rjr/nodes/ws.rb +12 -11
- data/lib/rjr/request.rb +128 -0
- data/lib/rjr/result.rb +75 -0
- data/lib/rjr/util/args.rb +145 -0
- data/lib/rjr/{em_adapter.rb → util/em_adapter.rb} +0 -0
- data/lib/rjr/util/handles_methods.rb +115 -0
- data/lib/rjr/util/has_messages.rb +50 -0
- data/lib/rjr/{inspect.rb → util/inspect.rb} +1 -1
- data/lib/rjr/util/json_parser.rb +101 -0
- data/lib/rjr/util/logger.rb +128 -0
- data/lib/rjr/{thread_pool.rb → util/thread_pool.rb} +2 -0
- data/lib/rjr/version.rb +1 -1
- data/site/jrw.js +1 -1
- data/specs/args_spec.rb +144 -0
- data/specs/dispatcher_spec.rb +399 -211
- data/specs/em_adapter_spec.rb +31 -18
- data/specs/handles_methods_spec.rb +154 -0
- data/specs/has_messages_spec.rb +54 -0
- data/specs/inspect_spec.rb +1 -1
- data/specs/json_parser_spec.rb +169 -0
- data/specs/messages/notification_spec.rb +59 -0
- data/specs/messages/request_spec.rb +66 -0
- data/specs/messages/response_spec.rb +94 -0
- data/specs/node_callbacks_spec.rb +47 -0
- data/specs/node_spec.rb +465 -56
- data/specs/request_spec.rb +147 -0
- data/specs/result_spec.rb +144 -0
- data/specs/thread_pool_spec.rb +1 -1
- metadata +41 -11
- data/lib/rjr/errors.rb +0 -23
- data/lib/rjr/message.rb +0 -351
- data/lib/rjr/semaphore.rb +0 -58
- data/specs/message_spec.rb +0 -229
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 517f79e571aae798dcc24be8feacd2ef5cbc3606
|
4
|
+
data.tar.gz: 340a496e1f47092a7b01a4b1363bac1db87b9e81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b5e056c47862523e346cf2a47923146e0b15a3fb856071f20a4fa226d55fdb6046e5ec463c8aea4e356d96d49e707279d5b477549df2537041943aaa04513f7
|
7
|
+
data.tar.gz: 325453d870c227bd35826a3bab5933fc6a62e644f1efbb2d5de3f762e917b24e38908aa15cfad2d7d3aae2d6d2f8af8da57a7bf527f9558570a78412747c5b60
|
data/Rakefile
CHANGED
data/bin/rjr-client
CHANGED
@@ -5,8 +5,8 @@
|
|
5
5
|
# Licensed under the Apache License, Version 2.0
|
6
6
|
|
7
7
|
require 'optparse'
|
8
|
-
require 'rjr/
|
9
|
-
require 'rjr/
|
8
|
+
require 'rjr/core_ext'
|
9
|
+
require 'rjr/util/logger'
|
10
10
|
require 'rjr/nodes/easy'
|
11
11
|
|
12
12
|
RJR::Logger.log_level = ::Logger::DEBUG
|
@@ -27,7 +27,8 @@ config = { :mode => :msg,
|
|
27
27
|
:msg_id => :rand,
|
28
28
|
:block => false,
|
29
29
|
:interval => 0,
|
30
|
-
:disconnect => false
|
30
|
+
:disconnect => false,
|
31
|
+
:class => 'Object'}
|
31
32
|
|
32
33
|
optparse = OptionParser.new do |opts|
|
33
34
|
opts.on('-h', '--help', 'Display this help screen') do
|
@@ -59,6 +60,10 @@ optparse = OptionParser.new do |opts|
|
|
59
60
|
config[:block] = !ret
|
60
61
|
end
|
61
62
|
|
63
|
+
opts.on('-c', '--class [value]', 'Ruby class to extract messages from (default Class)') do |cls|
|
64
|
+
config[:class] = cls
|
65
|
+
end
|
66
|
+
|
62
67
|
opts.on('-n', '--num number_of_messages', 'Number of messages to send to server (may be a number, :rand, or :indefinite)') do |n|
|
63
68
|
config[:num_msg] = case n.to_s.intern
|
64
69
|
when :rand then rand(MAX_MESSAGES)
|
@@ -68,7 +73,7 @@ optparse = OptionParser.new do |opts|
|
|
68
73
|
end
|
69
74
|
|
70
75
|
opts.on('--message ID', 'Message to send to server (rand to select random)') do |mid|
|
71
|
-
config[:msg_id] = mid
|
76
|
+
config[:msg_id] = (mid == 'rand' ? :rand : mid)
|
72
77
|
end
|
73
78
|
|
74
79
|
opts.on('--interval seconds', 'Number of seconds after which to wait between requests (or rand)') do |s|
|
@@ -94,10 +99,13 @@ NODES = {config[:transport] => {:node_id => config[:node_id],
|
|
94
99
|
:keep_alive => true} } # conditionally set keep alive?
|
95
100
|
|
96
101
|
cdir = File.dirname(__FILE__)
|
97
|
-
client_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
102
|
+
client_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
103
|
+
File.join(cdir, '..', 'examples', 'client'))
|
98
104
|
|
99
105
|
##########################################################
|
100
106
|
|
107
|
+
msg_class = config[:class].to_class
|
108
|
+
|
101
109
|
node = RJR::Nodes::Easy.new(NODES)
|
102
110
|
client_path.split(':').each { |cp|
|
103
111
|
node.dispatcher.add_modules(cp)
|
@@ -112,13 +120,14 @@ if config[:disconnect]
|
|
112
120
|
}
|
113
121
|
end
|
114
122
|
|
123
|
+
|
115
124
|
# invoke request(s)
|
116
125
|
0.upto(config[:num_msg]-1) { |i|
|
117
126
|
# TODO implement mode == :rand
|
118
127
|
|
119
128
|
# grab message (or rand message)
|
120
|
-
msg = (config[:msg_id] == :rand ?
|
121
|
-
|
129
|
+
msg = (config[:msg_id] == :rand ? msg_class.rand_message(config[:transport]) :
|
130
|
+
msg_class.message(config[:msg_id]))
|
122
131
|
|
123
132
|
if msg.nil?
|
124
133
|
puts "Invalid message id"
|
@@ -141,8 +150,6 @@ end
|
|
141
150
|
res = node.invoke(config[:dst], msg[:method], *params)
|
142
151
|
|
143
152
|
# verify and output result
|
144
|
-
puts res
|
145
|
-
puts msg[:result]
|
146
153
|
ress = (msg[:result].nil? ? "" : (msg[:result].call(res) ? "passed" : "failed"))
|
147
154
|
RJR::Logger.info "#{msg[:method]} result #{res} #{ress}"
|
148
155
|
|
data/bin/rjr-server
CHANGED
@@ -62,7 +62,8 @@ NODES = {:amqp => {:node_id => config[:node_id], :broker => config[:broker]},
|
|
62
62
|
:tcp => {:node_id => config[:node_id], :host => config[:host], :port => config[:tcp_port]}}
|
63
63
|
|
64
64
|
cdir = File.dirname(__FILE__)
|
65
|
-
server_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
65
|
+
server_path = File.join(ENV['RJR_LOAD_PATH'] ||
|
66
|
+
File.join(cdir, '..', 'examples', 'server'))
|
66
67
|
|
67
68
|
##########################################################
|
68
69
|
|
data/examples/client.rb
CHANGED
@@ -3,30 +3,32 @@
|
|
3
3
|
# Copyright (C) 2013 Mohammed Morsi <mo@morsi.org>
|
4
4
|
# Licensed under the Apache License, Version 2.0
|
5
5
|
|
6
|
-
|
6
|
+
require 'rjr/util/has_messages'
|
7
7
|
|
8
|
-
|
8
|
+
include RJR::HasMessages
|
9
|
+
|
10
|
+
define_message "stress" do
|
11
|
+
{ :method => 'stress',
|
12
|
+
:params => ["<CLIENT_ID>"],
|
13
|
+
:result => lambda { |r| r =~ /foobar.*/ } }
|
14
|
+
end
|
15
|
+
|
16
|
+
define_message "stress_callback" do
|
17
|
+
{ :method => 'stress_callback',
|
18
|
+
:params => ["<CLIENT_ID>"],
|
19
|
+
:transports => [:tcp, :ws, :amqp],
|
20
|
+
:result => lambda { |r| r =~ /barfoo.*/ } }
|
21
|
+
end
|
22
|
+
|
23
|
+
define_message "messages" do
|
24
|
+
{ :method => 'messages'}
|
25
|
+
end
|
26
|
+
|
27
|
+
def dispatch_examples_client(dispatcher)
|
9
28
|
dispatcher.handle "client_callback" do |p|
|
10
29
|
RJR::Logger.info "invoked client_callback method #{p}"
|
11
30
|
#amqp_node.invoke_request('stress_test-queue', 'stress', "foozmoney#{client_id}")
|
12
31
|
#amqp_node.stop
|
13
32
|
nil
|
14
33
|
end
|
15
|
-
|
16
|
-
define_message "stress" do
|
17
|
-
{ :method => 'stress',
|
18
|
-
:params => ["<CLIENT_ID>"],
|
19
|
-
:result => lambda { |r| r =~ /foobar.*/ } }
|
20
|
-
end
|
21
|
-
|
22
|
-
define_message "stress_callback" do
|
23
|
-
{ :method => 'stress_callback',
|
24
|
-
:params => ["<CLIENT_ID>"],
|
25
|
-
:transports => [:tcp, :ws, :amqp],
|
26
|
-
:result => lambda { |r| r =~ /barfoo.*/ } }
|
27
|
-
end
|
28
|
-
|
29
|
-
define_message "messages" do
|
30
|
-
{ :method => 'messages'}
|
31
|
-
end
|
32
34
|
end
|
data/examples/server.rb
CHANGED
@@ -0,0 +1 @@
|
|
1
|
+
# TODO server using HandlesMethods mixin
|
data/examples/tcp.rb
CHANGED
data/lib/rjr/common.rb
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
#
|
5
5
|
# Copyright (C) 2011-2013 Mohammed Morsi <mo@morsi.org>
|
6
6
|
# Licensed under the Apache License, Version 2.0
|
7
|
-
|
7
|
+
|
8
8
|
require 'json'
|
9
9
|
|
10
10
|
# Return a random uuid
|
@@ -25,229 +25,4 @@ def self.persistent_nodes
|
|
25
25
|
}.compact
|
26
26
|
end
|
27
27
|
|
28
|
-
# Logger helper class.
|
29
|
-
#
|
30
|
-
# Encapsulates the standard ruby logger in a thread safe manner. Dispatches
|
31
|
-
# class methods to an internally tracked logger to provide global access.
|
32
|
-
#
|
33
|
-
# TODO handle logging errors (log size too big, logrotate, etc)
|
34
|
-
#
|
35
|
-
# @example
|
36
|
-
# RJR::Logger.info 'my message'
|
37
|
-
# RJR::Logger.warn 'my warning'
|
38
|
-
class Logger
|
39
|
-
private
|
40
|
-
def self._instantiate_logger
|
41
|
-
if @logger.nil?
|
42
|
-
#STDOUT.sync = true
|
43
|
-
output = @log_to || ENV['RJR_LOG'] || STDOUT
|
44
|
-
@logger = ::Logger.new(output)
|
45
|
-
@logger.level = @log_level || ::Logger::FATAL
|
46
|
-
@logger_mutex = Mutex.new
|
47
|
-
@filters = []
|
48
|
-
@highlights = []
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
public
|
53
|
-
|
54
|
-
# Add method which to call on every log message to determine
|
55
|
-
# if messages should be included/excluded
|
56
|
-
def self.add_filter(filter)
|
57
|
-
@logger_mutex.synchronize{
|
58
|
-
@filters << filter
|
59
|
-
}
|
60
|
-
end
|
61
|
-
|
62
|
-
# Add a method which to call on every log message to determine
|
63
|
-
# if message should be highlighted
|
64
|
-
def self.highlight(hlight)
|
65
|
-
@logger_mutex.synchronize{
|
66
|
-
@highlights << hlight
|
67
|
-
}
|
68
|
-
end
|
69
|
-
|
70
|
-
def self.method_missing(method_id, *args)
|
71
|
-
_instantiate_logger
|
72
|
-
@logger_mutex.synchronize {
|
73
|
-
args = args.first if args.first.is_a?(Array)
|
74
|
-
args.each { |a|
|
75
|
-
# run highlights / filters against output before
|
76
|
-
# sending formatted output to logger
|
77
|
-
# TODO allow user to customize highlight mechanism/text
|
78
|
-
na = @highlights.any? { |h| h.call a } ?
|
79
|
-
"\e[1m\e[31m#{a}\e[0m\e[0m" : a
|
80
|
-
@logger.send(method_id, na) if @filters.all? { |f| f.call a }
|
81
|
-
}
|
82
|
-
}
|
83
|
-
end
|
84
|
-
|
85
|
-
def self.safe_exec(*args, &bl)
|
86
|
-
_instantiate_logger
|
87
|
-
@logger_mutex.synchronize {
|
88
|
-
bl.call *args
|
89
|
-
}
|
90
|
-
end
|
91
|
-
|
92
|
-
def self.logger
|
93
|
-
_instantiate_logger
|
94
|
-
@logger
|
95
|
-
end
|
96
|
-
|
97
|
-
# Set log destination
|
98
|
-
# @param dst destination which to log to (file name, STDOUT, etc)
|
99
|
-
def self.log_to(dst)
|
100
|
-
@log_to = dst
|
101
|
-
@logger = nil
|
102
|
-
_instantiate_logger
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set log level.
|
106
|
-
# @param level one of the standard rails log levels (default fatal)
|
107
|
-
def self.log_level=(level)
|
108
|
-
_instantiate_logger
|
109
|
-
if level.is_a?(String)
|
110
|
-
level = case level
|
111
|
-
when 'debug' then
|
112
|
-
::Logger::DEBUG
|
113
|
-
when 'info' then
|
114
|
-
::Logger::INFO
|
115
|
-
when 'warn' then
|
116
|
-
::Logger::WARN
|
117
|
-
when 'error' then
|
118
|
-
::Logger::ERROR
|
119
|
-
when 'fatal' then
|
120
|
-
::Logger::FATAL
|
121
|
-
end
|
122
|
-
end
|
123
|
-
@log_level = level
|
124
|
-
@logger.level = level
|
125
|
-
end
|
126
|
-
|
127
|
-
# Return true if log level is set to debug, else false
|
128
|
-
def self.debug?
|
129
|
-
@log_level == ::Logger::DEBUG
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
end # module RJR
|
134
|
-
|
135
|
-
# Serialized puts, uses logger lock to serialize puts output
|
136
|
-
def sputs(*args)
|
137
|
-
::RJR::Logger.safe_exec {
|
138
|
-
puts *args
|
139
|
-
}
|
140
|
-
end
|
141
|
-
|
142
|
-
class Object
|
143
|
-
def eigenclass
|
144
|
-
class << self
|
145
|
-
self
|
146
|
-
end
|
147
|
-
end
|
148
|
-
end
|
149
|
-
|
150
|
-
if RUBY_VERSION < "1.9"
|
151
|
-
# We extend object in ruby 1.9 to define 'instance_exec'
|
152
|
-
#
|
153
|
-
# {http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html Further reference}
|
154
|
-
class Object
|
155
|
-
module InstanceExecHelper; end
|
156
|
-
include InstanceExecHelper
|
157
|
-
# Execute the specified block in the scope of the local object
|
158
|
-
# @param [Array] args array of args to be passed to block
|
159
|
-
# @param [Callable] block callable object to bind and invoke in the local namespace
|
160
|
-
def instance_exec(*args, &block)
|
161
|
-
begin
|
162
|
-
old_critical, Thread.critical = Thread.critical, true
|
163
|
-
n = 0
|
164
|
-
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
165
|
-
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
166
|
-
ensure
|
167
|
-
Thread.critical = old_critical
|
168
|
-
end
|
169
|
-
begin
|
170
|
-
ret = send(mname, *args)
|
171
|
-
ensure
|
172
|
-
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
173
|
-
end
|
174
|
-
ret
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
|
-
# Two stage json parsing required, for more details
|
180
|
-
# see json issue https://github.com/flori/json/issues/179
|
181
|
-
|
182
|
-
# FIXME this will only work for json >= 1.7.6 where
|
183
|
-
# create_additions is defined
|
184
|
-
|
185
|
-
class Class
|
186
|
-
class << self
|
187
|
-
attr_accessor :whitelist_json_classes
|
188
|
-
attr_accessor :permitted_json_classes
|
189
|
-
end
|
190
|
-
|
191
|
-
def permit_json_create
|
192
|
-
Class.whitelist_json_classes = true
|
193
|
-
Class.permitted_json_classes ||= []
|
194
|
-
unless Class.permitted_json_classes.include?(self.name)
|
195
|
-
Class.permitted_json_classes << self.name
|
196
|
-
end
|
197
|
-
end
|
198
|
-
end
|
199
|
-
|
200
|
-
module RJR
|
201
|
-
def self.invalid_json_class?(jc)
|
202
|
-
Class.whitelist_json_classes ||= false
|
203
|
-
|
204
|
-
Class.whitelist_json_classes ?
|
205
|
-
# only permit classes user explicitly authorizes
|
206
|
-
!Class.permitted_json_classes.include?(jc) :
|
207
|
-
|
208
|
-
# allow any class
|
209
|
-
jc.to_s.split(/::/).inject(Object) do |p,c|
|
210
|
-
case
|
211
|
-
when c.empty? then p
|
212
|
-
when p.constants.collect { |c| c.to_s }.include?(c)
|
213
|
-
then p.const_get(c)
|
214
|
-
else
|
215
|
-
nil
|
216
|
-
end
|
217
|
-
end.nil?
|
218
|
-
end
|
219
|
-
|
220
|
-
def self.validate_json_hash(jh)
|
221
|
-
jh.each { |k,v|
|
222
|
-
if k == ::JSON.create_id && invalid_json_class?(v)
|
223
|
-
raise ArgumentError, "can't create json class #{v}"
|
224
|
-
elsif v.is_a?(Array)
|
225
|
-
validate_json_array(v)
|
226
|
-
elsif v.is_a?(Hash)
|
227
|
-
validate_json_hash(v)
|
228
|
-
end
|
229
|
-
}
|
230
|
-
end
|
231
|
-
|
232
|
-
def self.validate_json_array(ja)
|
233
|
-
ja.each { |jai|
|
234
|
-
if jai.is_a?(Array)
|
235
|
-
validate_json_array(jai)
|
236
|
-
elsif jai.is_a?(Hash)
|
237
|
-
validate_json_hash(jai)
|
238
|
-
end
|
239
|
-
}
|
240
|
-
end
|
241
|
-
|
242
|
-
def self.parse_json(js)
|
243
|
-
jp = ::JSON.parse js, :create_additions => false
|
244
|
-
if jp.is_a?(Array)
|
245
|
-
validate_json_array(jp)
|
246
|
-
elsif jp.is_a?(Hash)
|
247
|
-
validate_json_hash(jp)
|
248
|
-
else
|
249
|
-
return jp
|
250
|
-
end
|
251
|
-
::JSON.parse js, :create_additions => true
|
252
|
-
end
|
253
28
|
end
|
data/lib/rjr/core_ext.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# RJR Ruby Core Extensions
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2014 Mohammed Morsi <mo@morsi.org>
|
4
|
+
# Licensed under the Apache License, Version 2.0
|
5
|
+
|
6
|
+
class String
|
7
|
+
# Safely convert string to ruby class it represents
|
8
|
+
def to_class
|
9
|
+
split(/::/).inject(Object) do |p,c|
|
10
|
+
case
|
11
|
+
when c.empty? then p
|
12
|
+
when p.constants.collect { |c| c.to_s }.include?(c)
|
13
|
+
then p.const_get(c)
|
14
|
+
else
|
15
|
+
nil
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if RUBY_VERSION < "1.9"
|
22
|
+
# We extend object in ruby 1.9 to define 'instance_exec'
|
23
|
+
#
|
24
|
+
# {http://blog.jayfields.com/2006/09/ruby-instanceexec-aka-instanceeval.html Further reference}
|
25
|
+
class Object
|
26
|
+
module InstanceExecHelper; end
|
27
|
+
include InstanceExecHelper
|
28
|
+
# Execute the specified block in the scope of the local object
|
29
|
+
# @param [Array] args array of args to be passed to block
|
30
|
+
# @param [Callable] block callable object to bind and invoke in the local namespace
|
31
|
+
def instance_exec(*args, &block)
|
32
|
+
begin
|
33
|
+
old_critical, Thread.critical = Thread.critical, true
|
34
|
+
n = 0
|
35
|
+
n += 1 while respond_to?(mname="__instance_exec#{n}")
|
36
|
+
InstanceExecHelper.module_eval{ define_method(mname, &block) }
|
37
|
+
ensure
|
38
|
+
Thread.critical = old_critical
|
39
|
+
end
|
40
|
+
begin
|
41
|
+
ret = send(mname, *args)
|
42
|
+
ensure
|
43
|
+
InstanceExecHelper.module_eval{ remove_method(mname) } rescue nil
|
44
|
+
end
|
45
|
+
ret
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class Class
|
51
|
+
class << self
|
52
|
+
attr_accessor :whitelist_json_classes
|
53
|
+
attr_accessor :permitted_json_classes
|
54
|
+
end
|
55
|
+
|
56
|
+
def permit_json_create
|
57
|
+
Class.whitelist_json_classes = true
|
58
|
+
Class.permitted_json_classes ||= []
|
59
|
+
unless Class.permitted_json_classes.include?(self.name)
|
60
|
+
Class.permitted_json_classes << self.name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|