omf_common 6.0.0.pre.10 → 6.0.0.pre.11
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/monitor_topic.rb +80 -0
- data/bin/send_create.rb +94 -0
- data/bin/send_request.rb +58 -0
- data/example/engine_alt.rb +136 -0
- data/example/vm_alt.rb +65 -0
- data/lib/omf_common.rb +224 -3
- data/lib/omf_common/comm.rb +113 -46
- data/lib/omf_common/comm/amqp/amqp_communicator.rb +76 -0
- data/lib/omf_common/comm/amqp/amqp_topic.rb +91 -0
- data/lib/omf_common/comm/local/local_communicator.rb +64 -0
- data/lib/omf_common/comm/local/local_topic.rb +42 -0
- data/lib/omf_common/comm/topic.rb +190 -0
- data/lib/omf_common/{dsl/xmpp.rb → comm/xmpp/communicator.rb} +93 -53
- data/lib/omf_common/comm/xmpp/topic.rb +147 -0
- data/lib/omf_common/{dsl → comm/xmpp}/xmpp_mp.rb +2 -0
- data/lib/omf_common/eventloop.rb +94 -0
- data/lib/omf_common/eventloop/em.rb +57 -0
- data/lib/omf_common/eventloop/local_evl.rb +78 -0
- data/lib/omf_common/message.rb +112 -229
- data/lib/omf_common/message/json/json_message.rb +129 -0
- data/lib/omf_common/message/xml/message.rb +410 -0
- data/lib/omf_common/message/xml/relaxng_schema.rb +17 -0
- data/lib/omf_common/message/xml/topic_message.rb +20 -0
- data/lib/omf_common/protocol/6.0.rnc +11 -21
- data/lib/omf_common/protocol/6.0.rng +52 -119
- data/lib/omf_common/version.rb +1 -1
- data/omf_common.gemspec +4 -2
- data/test/fixture/pubsub.rb +19 -19
- data/test/omf_common/{dsl/xmpp_spec.rb → comm/xmpp/communicator_spec.rb} +47 -111
- data/test/omf_common/comm/xmpp/topic_spec.rb +113 -0
- data/test/omf_common/comm_spec.rb +1 -0
- data/test/omf_common/message/xml/message_spec.rb +136 -0
- data/test/omf_common/message_spec.rb +37 -131
- data/test/test_helper.rb +4 -1
- metadata +38 -28
- data/lib/omf_common/core_ext/object.rb +0 -21
- data/lib/omf_common/relaxng_schema.rb +0 -17
- data/lib/omf_common/topic.rb +0 -34
- data/lib/omf_common/topic_message.rb +0 -20
- data/test/omf_common/topic_message_spec.rb +0 -114
- data/test/omf_common/topic_spec.rb +0 -75
@@ -0,0 +1,129 @@
|
|
1
|
+
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module OmfCommon
|
5
|
+
class Message
|
6
|
+
class Json
|
7
|
+
class Message < OmfCommon::Message
|
8
|
+
|
9
|
+
|
10
|
+
def self.create(type, properties, body = {})
|
11
|
+
if type == :request
|
12
|
+
unless properties.kind_of?(Array)
|
13
|
+
raise "Expected array, but got #{properties.class} for request message"
|
14
|
+
end
|
15
|
+
properties = {select: properties}
|
16
|
+
elsif not properties.kind_of?(Hash)
|
17
|
+
raise "Expected hash, but got #{properties.class}"
|
18
|
+
end
|
19
|
+
content = body.merge({
|
20
|
+
operation: type,
|
21
|
+
mid: SecureRandom.uuid,
|
22
|
+
properties: properties
|
23
|
+
})
|
24
|
+
self.new(content)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.create_inform_message(itype = nil, properties = {}, body = {})
|
28
|
+
body[:itype] = itype if itype
|
29
|
+
create(:inform, properties, body)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Create and return a message by parsing 'str'
|
33
|
+
#
|
34
|
+
def self.parse(str)
|
35
|
+
content = JSON.parse(str, :symbolize_names => true)
|
36
|
+
#puts content
|
37
|
+
new(content)
|
38
|
+
end
|
39
|
+
|
40
|
+
def each_property(&block)
|
41
|
+
@properties.each do |k, v|
|
42
|
+
#unless INTERNAL_PROPS.include?(k.to_sym)
|
43
|
+
block.call(k, v)
|
44
|
+
#end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def has_properties?
|
49
|
+
not @properties.empty?
|
50
|
+
end
|
51
|
+
|
52
|
+
def valid?
|
53
|
+
true # don't do schema verification , yet
|
54
|
+
end
|
55
|
+
|
56
|
+
# Loop over all the unbound (sent without a value) properties
|
57
|
+
# of a request message.
|
58
|
+
#
|
59
|
+
def each_unbound_request_property(&block)
|
60
|
+
unless type == :request
|
61
|
+
raise "Can only be used for request messages"
|
62
|
+
end
|
63
|
+
self[:select].each do |el|
|
64
|
+
#puts "UUU: #{el}::#{el.class}"
|
65
|
+
if el.is_a? Symbol
|
66
|
+
block.call(el)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Loop over all the bound (sent with a value) properties
|
72
|
+
# of a request message.
|
73
|
+
#
|
74
|
+
def each_bound_request_property(&block)
|
75
|
+
unless type == :request
|
76
|
+
raise "Can only be used for request messages"
|
77
|
+
end
|
78
|
+
self[:select].each do |el|
|
79
|
+
#puts "BBB #{el}::#{el.class}"
|
80
|
+
if el.is_a? Hash
|
81
|
+
el.each do |key, value|
|
82
|
+
block.call(key, value)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def to_s
|
90
|
+
"JsonMessage: #{@content.inspect}"
|
91
|
+
end
|
92
|
+
|
93
|
+
def marshall
|
94
|
+
@content.to_json
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
def initialize(content)
|
99
|
+
debug "Create message: #{content.inspect}"
|
100
|
+
@content = content
|
101
|
+
unless op = content[:operation]
|
102
|
+
raise "Missing message type (:operation)"
|
103
|
+
end
|
104
|
+
content[:operation] = op.to_sym # needs to be symbol
|
105
|
+
@properties = content[:properties] || []
|
106
|
+
#@properties = Hashie::Mash.new(content[:properties])
|
107
|
+
end
|
108
|
+
|
109
|
+
def _set_core(key, value)
|
110
|
+
@content[key] = value
|
111
|
+
end
|
112
|
+
|
113
|
+
def _get_core(key)
|
114
|
+
@content[key]
|
115
|
+
end
|
116
|
+
|
117
|
+
def _set_property(key, value)
|
118
|
+
@properties[key] = value
|
119
|
+
end
|
120
|
+
|
121
|
+
def _get_property(key)
|
122
|
+
#puts key
|
123
|
+
@properties[key]
|
124
|
+
end
|
125
|
+
|
126
|
+
end # class
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,410 @@
|
|
1
|
+
require 'niceogiri'
|
2
|
+
require 'hashie'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'openssl'
|
5
|
+
require 'cgi'
|
6
|
+
|
7
|
+
require 'omf_common/message/xml/relaxng_schema'
|
8
|
+
|
9
|
+
module OmfCommon
|
10
|
+
class Message
|
11
|
+
class XML
|
12
|
+
|
13
|
+
# @example To create a valid omf message, e.g. a 'create' message:
|
14
|
+
#
|
15
|
+
# Message.create(:create,
|
16
|
+
# { p1: 'p1_value', p2: { unit: 'u', precision: 2 } },
|
17
|
+
# { guard: { p1: 'p1_value' } })
|
18
|
+
class Message < OmfCommon::Message
|
19
|
+
include Comparable
|
20
|
+
|
21
|
+
attr_accessor :xml
|
22
|
+
attr_accessor :content
|
23
|
+
|
24
|
+
class << self
|
25
|
+
# Create a OMF message
|
26
|
+
def create(operation_type, properties = {}, core_elements= {})
|
27
|
+
# For request messages, properties will be an array
|
28
|
+
if properties.kind_of? Array
|
29
|
+
properties = Hashie::Mash.new.tap do |mash|
|
30
|
+
properties.each { |p| mash[p] = nil }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
properties = Hashie::Mash.new(properties)
|
35
|
+
core_elements = Hashie::Mash.new(core_elements)
|
36
|
+
|
37
|
+
if operation_type.to_sym == :create
|
38
|
+
core_elements[:rtype] ||= properties[:type]
|
39
|
+
end
|
40
|
+
|
41
|
+
content = core_elements.merge({
|
42
|
+
operation: operation_type,
|
43
|
+
type: operation_type,
|
44
|
+
properties: properties
|
45
|
+
})
|
46
|
+
|
47
|
+
new(content)
|
48
|
+
end
|
49
|
+
|
50
|
+
def parse(xml)
|
51
|
+
raise ArgumentError, 'Can not parse an empty XML into OMF message' if xml.nil? || xml.empty?
|
52
|
+
|
53
|
+
xml_node = Nokogiri::XML(xml).root
|
54
|
+
|
55
|
+
self.create(xml_node.name.to_sym).tap do |message|
|
56
|
+
message.xml = xml_node
|
57
|
+
|
58
|
+
message.send(:_set_core, :mid, message.xml.attr('mid'))
|
59
|
+
|
60
|
+
message.xml.elements.each do |el|
|
61
|
+
unless %w(digest props guard).include? el.name
|
62
|
+
message.send(:_set_core, el.name, message.read_content(el.name))
|
63
|
+
end
|
64
|
+
|
65
|
+
if el.name == 'props'
|
66
|
+
message.read_element('props').first.element_children.each do |prop_node|
|
67
|
+
message.send(:_set_property,
|
68
|
+
prop_node.element_name,
|
69
|
+
message.reconstruct_data(prop_node))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if el.name == 'guard'
|
74
|
+
message.read_element('guard').first.element_children.each do |guard_node|
|
75
|
+
message.guard ||= Hashie::Mash.new
|
76
|
+
message.guard[guard_node.element_name] = message.reconstruct_data(guard_node)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
if OmfCommon::Measure.enabled? && !@@mid_list.include?(message.mid)
|
82
|
+
MPMessage.inject(Time.now.to_f, message.operation.to_s, message.mid, message.cid, message.to_s.gsub("\n",''))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def resource
|
89
|
+
r_id = _get_property(:res_id)
|
90
|
+
OmfCommon::Comm::XMPP::Topic.create(r_id)
|
91
|
+
end
|
92
|
+
|
93
|
+
def itype
|
94
|
+
@content.itype.to_s.upcase.gsub(/_/, '.') unless @content.itype.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
def marshall
|
98
|
+
build_xml
|
99
|
+
@xml.to_xml
|
100
|
+
end
|
101
|
+
|
102
|
+
alias_method :to_xml, :marshall
|
103
|
+
alias_method :to_s, :marshall
|
104
|
+
|
105
|
+
def build_xml
|
106
|
+
@xml = Niceogiri::XML::Node.new(self.operation.to_s, nil, OMF_NAMESPACE)
|
107
|
+
|
108
|
+
@xml.write_attr(:mid, mid)
|
109
|
+
|
110
|
+
props_node = Niceogiri::XML::Node.new(:props)
|
111
|
+
guard_node = Niceogiri::XML::Node.new(:guard)
|
112
|
+
|
113
|
+
props_ns.each do |k, v|
|
114
|
+
props_node.add_namespace_definition(k.to_s, v.to_s)
|
115
|
+
end
|
116
|
+
|
117
|
+
@xml.add_child(props_node)
|
118
|
+
@xml.add_child(guard_node) if _get_core(:guard)
|
119
|
+
|
120
|
+
(OMF_CORE_READ - [:mid, :guard, :operation]).each do |attr|
|
121
|
+
attr_value = self.send(attr)
|
122
|
+
|
123
|
+
next unless attr_value
|
124
|
+
|
125
|
+
add_element(attr, attr_value) unless (self.operation != :release && attr == :res_id)
|
126
|
+
end
|
127
|
+
|
128
|
+
self.properties.each { |k, v| add_property(k, v) }
|
129
|
+
self.guard.each { |k, v| add_property(k, v, :guard) } if _get_core(:guard)
|
130
|
+
|
131
|
+
#digest = OpenSSL::Digest::SHA512.new(@xml.canonicalize)
|
132
|
+
|
133
|
+
#add_element(:digest, digest)
|
134
|
+
@xml
|
135
|
+
end
|
136
|
+
|
137
|
+
# Construct a property xml node
|
138
|
+
#
|
139
|
+
def add_property(key, value = nil, add_to = :props)
|
140
|
+
if !default_props_ns.empty? && add_to == :props
|
141
|
+
key_node = Niceogiri::XML::Node.new(key, nil, default_props_ns)
|
142
|
+
else
|
143
|
+
key_node = Niceogiri::XML::Node.new(key)
|
144
|
+
end
|
145
|
+
|
146
|
+
unless value.nil?
|
147
|
+
key_node.write_attr('type', ruby_type_2_prop_type(value.class))
|
148
|
+
c_node = value_node_set(value)
|
149
|
+
|
150
|
+
if c_node.class == Array
|
151
|
+
c_node.each { |c_n| key_node.add_child(c_n) }
|
152
|
+
else
|
153
|
+
key_node.add_child(c_node)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
read_element(add_to).first.add_child(key_node)
|
157
|
+
key_node
|
158
|
+
end
|
159
|
+
|
160
|
+
def value_node_set(value, key = nil)
|
161
|
+
case value
|
162
|
+
when Hash
|
163
|
+
[].tap do |array|
|
164
|
+
value.each_pair do |k, v|
|
165
|
+
n = Niceogiri::XML::Node.new(k, nil, OMF_NAMESPACE)
|
166
|
+
n.write_attr('type', ruby_type_2_prop_type(v.class))
|
167
|
+
|
168
|
+
c_node = value_node_set(v, k)
|
169
|
+
if c_node.class == Array
|
170
|
+
c_node.each { |c_n| n.add_child(c_n) }
|
171
|
+
else
|
172
|
+
n.add_child(c_node)
|
173
|
+
end
|
174
|
+
array << n
|
175
|
+
end
|
176
|
+
end
|
177
|
+
when Array
|
178
|
+
value.map do |v|
|
179
|
+
n = Niceogiri::XML::Node.new('it', nil, OMF_NAMESPACE)
|
180
|
+
n.write_attr('type', ruby_type_2_prop_type(v.class))
|
181
|
+
|
182
|
+
c_node = value_node_set(v, 'it')
|
183
|
+
if c_node.class == Array
|
184
|
+
c_node.each { |c_n| n.add_child(c_n) }
|
185
|
+
else
|
186
|
+
n.add_child(c_node)
|
187
|
+
end
|
188
|
+
n
|
189
|
+
end
|
190
|
+
else
|
191
|
+
if key.nil?
|
192
|
+
string_value(value)
|
193
|
+
else
|
194
|
+
n = Niceogiri::XML::Node.new(key, nil, OMF_NAMESPACE)
|
195
|
+
n.add_child(string_value(value))
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Generate SHA1 of canonicalised xml and write into the ID attribute of the message
|
201
|
+
#
|
202
|
+
def sign
|
203
|
+
write_attr('mid', SecureRandom.uuid)
|
204
|
+
write_attr('ts', Time.now.utc.to_i)
|
205
|
+
canonical_msg = self.canonicalize
|
206
|
+
|
207
|
+
#priv_key = OmfCommon::Key.instance.private_key
|
208
|
+
#digest = OpenSSL::Digest::SHA512.new(canonical_msg)
|
209
|
+
|
210
|
+
#signature = Base64.encode64(priv_key.sign(digest, canonical_msg)).encode('utf-8') if priv_key
|
211
|
+
#write_attr('digest', digest)
|
212
|
+
#write_attr('signature', signature) if signature
|
213
|
+
|
214
|
+
if OmfCommon::Measure.enabled?
|
215
|
+
MPMessage.inject(Time.now.to_f, operation.to_s, mid, cid, self.to_s.gsub("\n",''))
|
216
|
+
@@mid_list << mid
|
217
|
+
end
|
218
|
+
self
|
219
|
+
end
|
220
|
+
|
221
|
+
# Validate against relaxng schema
|
222
|
+
#
|
223
|
+
def valid?
|
224
|
+
build_xml
|
225
|
+
|
226
|
+
validation = RelaxNGSchema.instance.validate(@xml.document)
|
227
|
+
if validation.empty?
|
228
|
+
true
|
229
|
+
else
|
230
|
+
logger.error validation.map(&:message).join("\n")
|
231
|
+
logger.debug @xml.to_s
|
232
|
+
false
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Short cut for adding xml node
|
237
|
+
#
|
238
|
+
def add_element(key, value = nil, &block)
|
239
|
+
key_node = Niceogiri::XML::Node.new(key)
|
240
|
+
@xml.add_child(key_node)
|
241
|
+
if block
|
242
|
+
block.call(key_node)
|
243
|
+
else
|
244
|
+
key_node.content = value if value
|
245
|
+
end
|
246
|
+
key_node
|
247
|
+
end
|
248
|
+
|
249
|
+
# Short cut for grabbing a group of nodes using xpath, but with default namespace
|
250
|
+
def element_by_xpath_with_default_namespace(xpath_without_ns)
|
251
|
+
xpath_without_ns = xpath_without_ns.to_s
|
252
|
+
if !default_props_ns.empty? && xpath_without_ns !~ /props|guard|ts|src|mid|rtype|res_id|cid|itype/
|
253
|
+
@xml.xpath(xpath_without_ns.gsub(/(^|\/{1,2})(\w+)/, "\\1#{rtype.to_s}:\\2"), default_props_ns)
|
254
|
+
else
|
255
|
+
@xml.xpath(xpath_without_ns.gsub(/(^|\/{1,2})(\w+)/, '\1xmlns:\2'), :xmlns => OMF_NAMESPACE)
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
# In case you think method :element_by_xpath_with_default_namespace is too long
|
260
|
+
#
|
261
|
+
alias_method :read_element, :element_by_xpath_with_default_namespace
|
262
|
+
|
263
|
+
# We just want to know the content of an non-repeatable element
|
264
|
+
#
|
265
|
+
def read_content(element_name)
|
266
|
+
element_content = read_element("#{element_name}").first.content rescue nil
|
267
|
+
unless element_content.nil?
|
268
|
+
element_content.empty? ? nil : element_content
|
269
|
+
else
|
270
|
+
nil
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# Reconstruct xml node into Ruby object
|
275
|
+
#
|
276
|
+
# @param [Niceogiri::XML::Node] property xml node
|
277
|
+
# @return [Object] the content of the property, as string, integer, float, or mash(hash with indifferent access)
|
278
|
+
def reconstruct_data(node, data_binding = nil)
|
279
|
+
node_type = node.attr('type')
|
280
|
+
case node_type
|
281
|
+
when 'array'
|
282
|
+
node.element_children.map do |child|
|
283
|
+
reconstruct_data(child, data_binding)
|
284
|
+
end
|
285
|
+
when /hash/
|
286
|
+
mash ||= Hashie::Mash.new
|
287
|
+
node.element_children.each do |child|
|
288
|
+
mash[child.attr('key') || child.element_name] ||= reconstruct_data(child, data_binding)
|
289
|
+
end
|
290
|
+
mash
|
291
|
+
when /boolean/
|
292
|
+
node.content == "true"
|
293
|
+
else
|
294
|
+
if node.content.empty?
|
295
|
+
nil
|
296
|
+
elsif data_binding && node_type == 'string'
|
297
|
+
ERB.new(node.content).result(data_binding)
|
298
|
+
else
|
299
|
+
node.content.ducktype
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def <=>(another)
|
305
|
+
@content <=> another.content
|
306
|
+
end
|
307
|
+
|
308
|
+
def properties
|
309
|
+
@content.properties
|
310
|
+
end
|
311
|
+
|
312
|
+
def has_properties?
|
313
|
+
@content.properties.empty?
|
314
|
+
end
|
315
|
+
|
316
|
+
def guard?
|
317
|
+
@content.guard.empty?
|
318
|
+
end
|
319
|
+
|
320
|
+
# Pretty print for application event message
|
321
|
+
#
|
322
|
+
def print_app_event
|
323
|
+
"APP_EVENT (#{read_property(:app)}, ##{read_property(:seq)}, #{read_property(:event)}): #{read_property(:msg)}"
|
324
|
+
end
|
325
|
+
|
326
|
+
# Iterate each property key value pair
|
327
|
+
#
|
328
|
+
def each_property(&block)
|
329
|
+
properties.each { |k, v| block.call(k, v) }
|
330
|
+
end
|
331
|
+
|
332
|
+
|
333
|
+
def each_unbound_request_property(&block)
|
334
|
+
raise "Can only be used for request messages. Got #{type}." if type != :request
|
335
|
+
properties.each { |k, v| block.call(k, v) if v.nil? }
|
336
|
+
end
|
337
|
+
|
338
|
+
def each_bound_request_property(&block)
|
339
|
+
raise "Can only be used for request messages. Got #{type}." if type != :request
|
340
|
+
properties.each { |k, v| block.call(k, v) unless v.nil? }
|
341
|
+
end
|
342
|
+
|
343
|
+
#def [](name, evaluate = false)
|
344
|
+
# value = properties[name]
|
345
|
+
|
346
|
+
# if evaluate && value.kind_of?(String)
|
347
|
+
# ERB.new(value).result(evaluate)
|
348
|
+
# else
|
349
|
+
# value
|
350
|
+
# end
|
351
|
+
#end
|
352
|
+
|
353
|
+
alias_method :read_property, :[]
|
354
|
+
|
355
|
+
alias_method :write_property, :[]=
|
356
|
+
|
357
|
+
private
|
358
|
+
|
359
|
+
def initialize(content = {})
|
360
|
+
@content = content
|
361
|
+
@content.mid = SecureRandom.uuid
|
362
|
+
@content.ts = Time.now.utc.to_i
|
363
|
+
end
|
364
|
+
|
365
|
+
def _set_core(key, value)
|
366
|
+
@content[key] = value
|
367
|
+
end
|
368
|
+
|
369
|
+
def _get_core(key)
|
370
|
+
@content[key]
|
371
|
+
end
|
372
|
+
|
373
|
+
def _set_property(key, value, ns = nil)
|
374
|
+
# TODO what to do here
|
375
|
+
@content.properties[key] = value
|
376
|
+
end
|
377
|
+
|
378
|
+
def _get_property(key, ns = nil)
|
379
|
+
# TODO what to do here
|
380
|
+
@content.properties[key]
|
381
|
+
end
|
382
|
+
|
383
|
+
def ruby_type_2_prop_type(ruby_class_type)
|
384
|
+
v_type = ruby_class_type.to_s.downcase
|
385
|
+
case v_type
|
386
|
+
when *%w(trueclass falseclass)
|
387
|
+
'boolean'
|
388
|
+
when *%w(fixnum bignum)
|
389
|
+
'integer'
|
390
|
+
when /hash|mash/
|
391
|
+
'hash'
|
392
|
+
else
|
393
|
+
v_type
|
394
|
+
end
|
395
|
+
end
|
396
|
+
|
397
|
+
# Get string of a value object, escape if object is string
|
398
|
+
def string_value(value)
|
399
|
+
if value.kind_of? String
|
400
|
+
value = CGI::escape_html(value)
|
401
|
+
else
|
402
|
+
value = value.to_s
|
403
|
+
end
|
404
|
+
value
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
end
|
409
|
+
end
|
410
|
+
|