omf_common 6.0.0.pre.6 → 6.0.0.pre.7
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.
- data/lib/omf_common.rb +4 -0
- data/lib/omf_common/core_ext/object.rb +21 -0
- data/lib/omf_common/dsl/xmpp.rb +72 -22
- data/lib/omf_common/exec_app.rb +163 -0
- data/lib/omf_common/message.rb +110 -26
- data/lib/omf_common/protocol/6.0.rnc +61 -0
- data/lib/omf_common/protocol/6.0.rng +157 -0
- data/lib/omf_common/topic.rb +34 -0
- data/lib/omf_common/topic_message.rb +20 -0
- data/lib/omf_common/version.rb +1 -1
- data/omf_common.gemspec +4 -1
- data/test/fixture/pubsub.rb +252 -0
- data/test/omf_common/command_spec.rb +8 -2
- data/test/omf_common/dsl/xmpp_spec.rb +309 -0
- data/test/omf_common/message_spec.rb +53 -18
- data/test/omf_common/topic_message_spec.rb +114 -0
- data/test/omf_common/topic_spec.rb +75 -0
- data/test/test_helper.rb +3 -0
- metadata +63 -6
- data/lib/omf_common/protocol.rnc +0 -42
- data/lib/omf_common/protocol.rng +0 -141
data/lib/omf_common.rb
CHANGED
@@ -5,8 +5,12 @@ require "omf_common/version"
|
|
5
5
|
require "omf_common/message"
|
6
6
|
require "omf_common/comm"
|
7
7
|
require "omf_common/command"
|
8
|
+
require "omf_common/topic"
|
9
|
+
require "omf_common/topic_message"
|
8
10
|
require "omf_common/core_ext/string"
|
11
|
+
require "omf_common/core_ext/object"
|
9
12
|
|
13
|
+
# Use global default logger from logging gem
|
10
14
|
include Logging.globally
|
11
15
|
|
12
16
|
Logging.appenders.stdout('stdout',
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class Object
|
2
|
+
def stub name, val_or_callable, &block
|
3
|
+
new_name = "__minitest_stub__#{name}"
|
4
|
+
metaclass = class << self; self; end
|
5
|
+
metaclass.send :alias_method, new_name, name
|
6
|
+
metaclass.send :define_method, name do |*args, &stub_block|
|
7
|
+
if val_or_callable.respond_to? :call then
|
8
|
+
val_or_callable.call(*args, &stub_block)
|
9
|
+
else
|
10
|
+
val_or_callable
|
11
|
+
end
|
12
|
+
end
|
13
|
+
yield self
|
14
|
+
ensure
|
15
|
+
metaclass.send :undef_method, name
|
16
|
+
metaclass.send :alias_method, name, new_name
|
17
|
+
metaclass.send :undef_method, new_name
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
|
data/lib/omf_common/dsl/xmpp.rb
CHANGED
@@ -33,55 +33,105 @@ module OmfCommon
|
|
33
33
|
# Create a new pubsub topic with additional configuration
|
34
34
|
#
|
35
35
|
# @param [String] topic Pubsub topic name
|
36
|
-
|
37
|
-
|
38
|
-
pubsub.create(topic, prefix_host(host), PUBSUB_CONFIGURE, &callback_logging(__method__, topic, &block))
|
36
|
+
def create_topic(topic, &block)
|
37
|
+
pubsub.create(topic, default_host, PUBSUB_CONFIGURE, &callback_logging(__method__, topic, &block))
|
39
38
|
end
|
40
39
|
|
41
40
|
# Delete a pubsub topic
|
42
41
|
#
|
43
42
|
# @param [String] topic Pubsub topic name
|
44
|
-
|
45
|
-
|
46
|
-
pubsub.delete(topic, prefix_host(host), &callback_logging(__method__, topic, &block))
|
43
|
+
def delete_topic(topic, &block)
|
44
|
+
pubsub.delete(topic, default_host, &callback_logging(__method__, topic, &block))
|
47
45
|
end
|
48
46
|
|
49
47
|
# Subscribe to a pubsub topic
|
50
48
|
#
|
51
49
|
# @param [String] topic Pubsub topic name
|
52
|
-
|
53
|
-
|
54
|
-
pubsub.subscribe(topic, nil, prefix_host(host), &callback_logging(__method__, topic, &block))
|
50
|
+
def subscribe(topic, &block)
|
51
|
+
pubsub.subscribe(topic, nil, default_host, &callback_logging(__method__, topic, &block))
|
55
52
|
end
|
56
53
|
|
57
54
|
# Un-subscribe all existing subscriptions from all pubsub topics.
|
58
|
-
|
59
|
-
|
60
|
-
def unsubscribe(host)
|
61
|
-
pubsub.subscriptions(prefix_host(host)) do |m|
|
55
|
+
def unsubscribe
|
56
|
+
pubsub.subscriptions(default_host) do |m|
|
62
57
|
m[:subscribed] && m[:subscribed].each do |s|
|
63
|
-
pubsub.unsubscribe(s[:node], nil, s[:subid],
|
58
|
+
pubsub.unsubscribe(s[:node], nil, s[:subid], default_host, &callback_logging(__method__, s[:node], s[:subid]))
|
64
59
|
end
|
65
60
|
end
|
66
61
|
end
|
67
62
|
|
68
|
-
def affiliations(
|
69
|
-
pubsub.affiliations(
|
63
|
+
def affiliations(&block)
|
64
|
+
pubsub.affiliations(default_host, &callback_logging(__method__, &block))
|
70
65
|
end
|
71
66
|
|
72
67
|
# Publish to a pubsub topic
|
73
68
|
#
|
74
69
|
# @param [String] topic Pubsub topic name
|
75
70
|
# @param [String] message Any XML fragment to be sent as payload
|
76
|
-
|
77
|
-
|
78
|
-
pubsub.publish(topic, message,
|
71
|
+
def publish(topic, message, &block)
|
72
|
+
raise StandardError, "Invalid message" unless message.valid?
|
73
|
+
pubsub.publish(topic, message, default_host, &callback_logging(__method__, topic, message.operation, &block))
|
74
|
+
end
|
75
|
+
|
76
|
+
# Generate OMF related message
|
77
|
+
%w(create configure request inform release).each do |m_name|
|
78
|
+
define_method("#{m_name}_message") do |*args, &block|
|
79
|
+
message =
|
80
|
+
if block
|
81
|
+
Message.send(m_name, *args, &block)
|
82
|
+
elsif args[0].kind_of? Array
|
83
|
+
Message.send(m_name) do |v|
|
84
|
+
args[0].each do |opt|
|
85
|
+
if opt.kind_of? Hash
|
86
|
+
opt.each_pair do |key, value|
|
87
|
+
v.property(key, value)
|
88
|
+
end
|
89
|
+
else
|
90
|
+
v.property(opt)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
OmfCommon::TopicMessage.new(message, self)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Event machine related method delegation
|
101
|
+
%w(add_timer add_periodic_timer).each do |m_name|
|
102
|
+
define_method(m_name) do |*args, &block|
|
103
|
+
EM.send(m_name, *args, &block)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
%w(created status released failed).each do |inform_type|
|
108
|
+
define_method("on_#{inform_type}_message") do |*args, &message_block|
|
109
|
+
context_id = args[0].context_id if args[0]
|
110
|
+
event_block = proc do |event|
|
111
|
+
message_block.call(Message.parse(event.items.first.payload))
|
112
|
+
end
|
113
|
+
guard_block = proc do |event|
|
114
|
+
(event.items?) && (!event.delayed?) &&
|
115
|
+
event.items.first.payload &&
|
116
|
+
(omf_message = Message.parse(event.items.first.payload)) &&
|
117
|
+
omf_message.operation == :inform &&
|
118
|
+
omf_message.read_content(:inform_type) == inform_type.upcase &&
|
119
|
+
(context_id ? (omf_message.context_id == context_id) : true)
|
120
|
+
end
|
121
|
+
pubsub_event(guard_block, &callback_logging(__method__, &event_block))
|
122
|
+
end
|
79
123
|
end
|
80
124
|
|
81
125
|
# Event callback for pubsub topic event(item published)
|
82
126
|
#
|
83
127
|
def topic_event(*args, &block)
|
84
|
-
pubsub_event(
|
128
|
+
pubsub_event(*args, &callback_logging(__method__, &block))
|
129
|
+
end
|
130
|
+
|
131
|
+
# Return a topic object represents pubsub topic
|
132
|
+
#
|
133
|
+
def get_topic(topic_id)
|
134
|
+
OmfCommon::Topic.new(topic_id, self)
|
85
135
|
end
|
86
136
|
|
87
137
|
private
|
@@ -96,8 +146,8 @@ module OmfCommon
|
|
96
146
|
end
|
97
147
|
end
|
98
148
|
|
99
|
-
def
|
100
|
-
"#{HOST_PREFIX}.#{
|
149
|
+
def default_host
|
150
|
+
"#{HOST_PREFIX}.#{client.jid.domain}"
|
101
151
|
end
|
102
152
|
end
|
103
153
|
end
|
@@ -0,0 +1,163 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (c) 2006-2012 National ICT Australia (NICTA), Australia
|
3
|
+
#
|
4
|
+
# Copyright (c) 2004-2009 WINLAB, Rutgers University, USA
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
7
|
+
# of this software and associated documentation files (the "Software"), to deal
|
8
|
+
# in the Software without restriction, including without limitation the rights
|
9
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
10
|
+
# copies of the Software, and to permit persons to whom the Software is
|
11
|
+
# furnished to do so, subject to the following conditions:
|
12
|
+
#
|
13
|
+
# The above copyright notice and this permission notice shall be included in
|
14
|
+
# all copies or substantial portions of the Software.
|
15
|
+
#
|
16
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
17
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
18
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
19
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
20
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
21
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
22
|
+
# THE SOFTWARE.
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# Library of client side helpers
|
26
|
+
#
|
27
|
+
require 'fcntl'
|
28
|
+
|
29
|
+
#
|
30
|
+
# Run an application on the client.
|
31
|
+
#
|
32
|
+
# Borrows from Open3
|
33
|
+
#
|
34
|
+
class ExecApp
|
35
|
+
|
36
|
+
# Holds the pids for all active apps
|
37
|
+
@@all_apps = Hash.new
|
38
|
+
|
39
|
+
# True if this active app is being killed by a proper
|
40
|
+
# call to ExecApp.signal_all() or signal()
|
41
|
+
# (i.e. when the caller of ExecApp decided to stop the application,
|
42
|
+
# as far as we are concerned, this is a 'clean' exit)
|
43
|
+
@clean_exit = false
|
44
|
+
|
45
|
+
# Return an application instance based on its ID
|
46
|
+
#
|
47
|
+
# @param [String] id of the application to return
|
48
|
+
def ExecApp.[](id)
|
49
|
+
app = @@all_apps[id]
|
50
|
+
logger.info "Unknown application '#{id}/#{id.class}'" if app.nil?
|
51
|
+
return app
|
52
|
+
end
|
53
|
+
|
54
|
+
def ExecApp.signal_all(signal = 'KILL')
|
55
|
+
@@all_apps.each_value { |app| app.signal(signal) }
|
56
|
+
end
|
57
|
+
|
58
|
+
def stdin(line)
|
59
|
+
logger.debug "Writing '#{line}' to app '#{@id}'"
|
60
|
+
@stdin.write("#{line}\n")
|
61
|
+
@stdin.flush
|
62
|
+
end
|
63
|
+
|
64
|
+
def signal(signal = 'KILL')
|
65
|
+
@clean_exit = true
|
66
|
+
Process.kill(signal, @pid)
|
67
|
+
end
|
68
|
+
|
69
|
+
#
|
70
|
+
# Run an application 'cmd' in a separate thread and monitor
|
71
|
+
# its stdout. Also send status reports to the 'observer' by
|
72
|
+
# calling its "on_app_event(eventType, appId, message")"
|
73
|
+
#
|
74
|
+
# @param id ID of application (used for reporting)
|
75
|
+
# @param observer Observer of application's progress
|
76
|
+
# @param cmd Command path and args
|
77
|
+
# @param map_std_err_to_out If true report stderr as stdin [false]
|
78
|
+
#
|
79
|
+
def initialize(id, observer, cmd, map_std_err_to_out = false)
|
80
|
+
|
81
|
+
@id = id
|
82
|
+
@observer = observer
|
83
|
+
@@all_apps[id] = self
|
84
|
+
|
85
|
+
pw = IO::pipe # pipe[0] for read, pipe[1] for write
|
86
|
+
pr = IO::pipe
|
87
|
+
pe = IO::pipe
|
88
|
+
|
89
|
+
logger.debug "Starting application '#{id}' - cmd: '#{cmd}'"
|
90
|
+
@observer.on_app_event(:STARTED, id, cmd)
|
91
|
+
@pid = fork {
|
92
|
+
# child will remap pipes to std and exec cmd
|
93
|
+
pw[1].close
|
94
|
+
STDIN.reopen(pw[0])
|
95
|
+
pw[0].close
|
96
|
+
|
97
|
+
pr[0].close
|
98
|
+
STDOUT.reopen(pr[1])
|
99
|
+
pr[1].close
|
100
|
+
|
101
|
+
pe[0].close
|
102
|
+
STDERR.reopen(pe[1])
|
103
|
+
pe[1].close
|
104
|
+
|
105
|
+
begin
|
106
|
+
exec(cmd)
|
107
|
+
rescue => ex
|
108
|
+
cmd = cmd.join(' ') if cmd.kind_of?(Array)
|
109
|
+
STDERR.puts "exec failed for '#{cmd}' (#{$!}): #{ex}"
|
110
|
+
end
|
111
|
+
# Should never get here
|
112
|
+
exit!
|
113
|
+
}
|
114
|
+
|
115
|
+
pw[0].close
|
116
|
+
pr[1].close
|
117
|
+
pe[1].close
|
118
|
+
monitor_pipe(:stdout, pr[0])
|
119
|
+
monitor_pipe(map_std_err_to_out ? :stdout : :stderr, pe[0])
|
120
|
+
# Create thread which waits for application to exit
|
121
|
+
Thread.new(id, @pid) do |id, pid|
|
122
|
+
ret = Process.waitpid(pid)
|
123
|
+
status = $?
|
124
|
+
@@all_apps.delete(@id)
|
125
|
+
# app finished
|
126
|
+
if (status == 0) || @clean_exit
|
127
|
+
s = "OK"
|
128
|
+
logger.debug "Application '#{id}' finished"
|
129
|
+
else
|
130
|
+
s = "ERROR"
|
131
|
+
logger.debug "Application '#{id}' failed (code=#{status})"
|
132
|
+
end
|
133
|
+
@observer.on_app_event("DONE.#{s}", @id, "status: #{status}")
|
134
|
+
end
|
135
|
+
@stdin = pw[1]
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
#
|
141
|
+
# Create a thread to monitor the process and its output
|
142
|
+
# and report that back to the server
|
143
|
+
#
|
144
|
+
# @param name Name of app stream to monitor (should be :stdout, :stderr)
|
145
|
+
# @param pipe Pipe to read from
|
146
|
+
#
|
147
|
+
def monitor_pipe(name, pipe)
|
148
|
+
Thread.new() do
|
149
|
+
begin
|
150
|
+
while true do
|
151
|
+
s = pipe.readline.chomp
|
152
|
+
@observer.on_app_event(name.to_s.upcase, @id, s)
|
153
|
+
end
|
154
|
+
rescue EOFError
|
155
|
+
# do nothing
|
156
|
+
rescue Exception => err
|
157
|
+
logger.error "monitorApp(#{@id}): #{err}"
|
158
|
+
ensure
|
159
|
+
pipe.close
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
data/lib/omf_common/message.rb
CHANGED
@@ -10,23 +10,25 @@ module OmfCommon
|
|
10
10
|
#
|
11
11
|
# Message.request do |message|
|
12
12
|
# message.property('os', 'debian')
|
13
|
-
# message.property('memory', 2
|
14
|
-
# p.element('unit', 'gb')
|
15
|
-
# end
|
13
|
+
# message.property('memory', { value: 2, unit: 'gb' })
|
16
14
|
# end
|
17
15
|
#
|
18
16
|
class Message < Niceogiri::XML::Node
|
19
17
|
OMF_NAMESPACE = "http://schema.mytestbed.net/#{OmfCommon::PROTOCOL_VERSION}/protocol"
|
20
|
-
SCHEMA_FILE = "#{File.dirname(__FILE__)}/protocol.rng"
|
18
|
+
SCHEMA_FILE = "#{File.dirname(__FILE__)}/protocol/#{OmfCommon::PROTOCOL_VERSION}.rng"
|
21
19
|
OPERATION = %w(create configure request release inform)
|
22
20
|
|
23
21
|
class << self
|
24
22
|
OPERATION.each do |operation|
|
25
23
|
define_method(operation) do |*args, &block|
|
26
24
|
xml = new(operation, nil, OMF_NAMESPACE)
|
27
|
-
|
25
|
+
if operation == 'inform'
|
26
|
+
xml.element('context_id', args[1] || SecureRandom.uuid)
|
27
|
+
xml.element('inform_type', args[0])
|
28
|
+
else
|
29
|
+
xml.element('context_id', SecureRandom.uuid)
|
30
|
+
end
|
28
31
|
xml.element('publish_to', args[0]) if operation == 'request'
|
29
|
-
xml.element('inform_type', args[1]) if operation == 'inform'
|
30
32
|
block.call(xml) if block
|
31
33
|
xml.sign
|
32
34
|
end
|
@@ -40,17 +42,62 @@ module OmfCommon
|
|
40
42
|
|
41
43
|
# Construct a property xml node
|
42
44
|
#
|
43
|
-
def property(key, value = nil
|
45
|
+
def property(key, value = nil)
|
44
46
|
key_node = Message.new('property')
|
45
47
|
key_node.write_attr('key', key)
|
48
|
+
|
49
|
+
unless value.nil?
|
50
|
+
key_node.write_attr('type', value.class.to_s.downcase)
|
51
|
+
c_node = value_node_set(value)
|
52
|
+
|
53
|
+
if c_node.class == Array
|
54
|
+
c_node.each { |c_n| key_node.add_child(c_n) }
|
55
|
+
else
|
56
|
+
key_node.add_child(c_node)
|
57
|
+
end
|
58
|
+
end
|
46
59
|
add_child(key_node)
|
47
|
-
|
48
|
-
|
49
|
-
|
60
|
+
key_node
|
61
|
+
end
|
62
|
+
|
63
|
+
def value_node_set(value, key = nil)
|
64
|
+
case value
|
65
|
+
when Hash
|
66
|
+
[].tap do |array|
|
67
|
+
value.each_pair do |k, v|
|
68
|
+
n = Message.new(k)
|
69
|
+
n.write_attr('type', v.class.to_s.downcase)
|
70
|
+
|
71
|
+
c_node = value_node_set(v, k)
|
72
|
+
if c_node.class == Array
|
73
|
+
c_node.each { |c_n| n.add_child(c_n) }
|
74
|
+
else
|
75
|
+
n.add_child(c_node)
|
76
|
+
end
|
77
|
+
array << n
|
78
|
+
end
|
79
|
+
end
|
80
|
+
when Array
|
81
|
+
value.map do |v|
|
82
|
+
n = Message.new('item')
|
83
|
+
n.write_attr('type', v.class.to_s.downcase)
|
84
|
+
|
85
|
+
c_node = value_node_set(v, 'item')
|
86
|
+
if c_node.class == Array
|
87
|
+
c_node.each { |c_n| n.add_child(c_n) }
|
88
|
+
else
|
89
|
+
n.add_child(c_node)
|
90
|
+
end
|
91
|
+
n
|
92
|
+
end
|
50
93
|
else
|
51
|
-
|
94
|
+
if key.nil?
|
95
|
+
value.to_s
|
96
|
+
else
|
97
|
+
n = Message.new(key)
|
98
|
+
n.add_child(value.to_s)
|
99
|
+
end
|
52
100
|
end
|
53
|
-
key_node
|
54
101
|
end
|
55
102
|
|
56
103
|
# Generate SHA1 of canonicalised xml and write into the ID attribute of the message
|
@@ -63,21 +110,26 @@ module OmfCommon
|
|
63
110
|
# Validate against relaxng schema
|
64
111
|
#
|
65
112
|
def valid?
|
66
|
-
validation = Nokogiri::XML::RelaxNG(File.open(SCHEMA_FILE)).validate(document)
|
113
|
+
validation = Nokogiri::XML::RelaxNG(File.open(SCHEMA_FILE)).validate(self.document)
|
67
114
|
if validation.empty?
|
68
115
|
true
|
69
116
|
else
|
70
117
|
logger.error validation.map(&:message).join("\n")
|
118
|
+
logger.debug self.to_s
|
71
119
|
false
|
72
120
|
end
|
73
121
|
end
|
74
122
|
|
75
123
|
# Short cut for adding xml node
|
76
124
|
#
|
77
|
-
def element(key, value)
|
78
|
-
key_node =
|
79
|
-
key_node.content = value
|
125
|
+
def element(key, value = nil, &block)
|
126
|
+
key_node = Message.new(key)
|
80
127
|
add_child(key_node)
|
128
|
+
if block
|
129
|
+
block.call(key_node)
|
130
|
+
else
|
131
|
+
key_node.content = value if value
|
132
|
+
end
|
81
133
|
end
|
82
134
|
|
83
135
|
# The root element_name represents operation
|
@@ -98,7 +150,18 @@ module OmfCommon
|
|
98
150
|
# We just want to know the content of an non-repeatable element
|
99
151
|
#
|
100
152
|
def read_content(element_name)
|
101
|
-
read_element("//#{element_name}").first.content rescue nil
|
153
|
+
element_content = read_element("//#{element_name}").first.content rescue nil
|
154
|
+
element_content.empty? ? nil : element_content
|
155
|
+
end
|
156
|
+
|
157
|
+
# Context ID will be requested quite often
|
158
|
+
def context_id
|
159
|
+
read_property(:context_id) || read_content(:context_id)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Resource ID is another frequent requested property
|
163
|
+
def resource_id
|
164
|
+
read_property(:resource_id) || read_content(:resource_id)
|
102
165
|
end
|
103
166
|
|
104
167
|
# Get a property by key
|
@@ -109,17 +172,38 @@ module OmfCommon
|
|
109
172
|
def read_property(key)
|
110
173
|
key = key.to_s
|
111
174
|
e = read_element("//property[@key='#{key}']").first
|
112
|
-
if e
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
175
|
+
reconstruct_data(e) if e
|
176
|
+
end
|
177
|
+
|
178
|
+
def reconstruct_data(node)
|
179
|
+
case node.attr('type')
|
180
|
+
when 'array'
|
181
|
+
mash ||= Hashie::Mash.new
|
182
|
+
mash[:items] = node.element_children.map do |child|
|
183
|
+
reconstruct_data(child)
|
121
184
|
end
|
185
|
+
mash
|
186
|
+
when /hash/
|
187
|
+
mash ||= Hashie::Mash.new
|
188
|
+
node.element_children.each do |child|
|
189
|
+
mash[child.attr('key') || child.element_name] ||= reconstruct_data(child)
|
190
|
+
end
|
191
|
+
mash
|
192
|
+
else
|
193
|
+
node.content.empty? ? nil : node.content.ducktype
|
122
194
|
end
|
123
195
|
end
|
196
|
+
|
197
|
+
# Iterate each property element
|
198
|
+
#
|
199
|
+
def each_property(&block)
|
200
|
+
read_element("//property").each { |v| block.call(v) }
|
201
|
+
end
|
202
|
+
|
203
|
+
# Pretty print for application event message
|
204
|
+
#
|
205
|
+
def print_app_event
|
206
|
+
"APP_EVENT (#{read_property(:app)}, ##{read_property(:seq)}, #{read_property(:event)}): #{read_property(:msg)}"
|
207
|
+
end
|
124
208
|
end
|
125
209
|
end
|