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