actionpack 1.8.1 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- data/CHANGELOG +309 -16
- data/README +1 -1
- data/lib/action_controller.rb +5 -0
- data/lib/action_controller/assertions.rb +57 -12
- data/lib/action_controller/auto_complete.rb +47 -0
- data/lib/action_controller/base.rb +288 -258
- data/lib/action_controller/benchmarking.rb +8 -3
- data/lib/action_controller/caching.rb +88 -42
- data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
- data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
- data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
- data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
- data/lib/action_controller/cgi_process.rb +39 -11
- data/lib/action_controller/code_generation.rb +235 -0
- data/lib/action_controller/cookies.rb +14 -8
- data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
- data/lib/action_controller/filters.rb +8 -7
- data/lib/action_controller/helpers.rb +41 -6
- data/lib/action_controller/layout.rb +45 -16
- data/lib/action_controller/request.rb +86 -23
- data/lib/action_controller/rescue.rb +1 -0
- data/lib/action_controller/response.rb +1 -1
- data/lib/action_controller/routing.rb +536 -272
- data/lib/action_controller/scaffolding.rb +30 -25
- data/lib/action_controller/session/active_record_store.rb +251 -50
- data/lib/action_controller/streaming.rb +133 -0
- data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
- data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
- data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
- data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
- data/lib/action_controller/test_process.rb +68 -47
- data/lib/action_controller/upload_progress.rb +421 -0
- data/lib/action_controller/url_rewriter.rb +8 -11
- data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
- data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
- data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
- data/lib/action_controller/vendor/xml_simple.rb +1019 -0
- data/lib/action_controller/verification.rb +36 -30
- data/lib/action_view/base.rb +21 -14
- data/lib/action_view/helpers/active_record_helper.rb +15 -13
- data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
- data/lib/action_view/helpers/benchmark_helper.rb +24 -0
- data/lib/action_view/helpers/capture_helper.rb +7 -5
- data/lib/action_view/helpers/date_helper.rb +63 -46
- data/lib/action_view/helpers/form_helper.rb +7 -1
- data/lib/action_view/helpers/form_options_helper.rb +19 -11
- data/lib/action_view/helpers/form_tag_helper.rb +5 -1
- data/lib/action_view/helpers/javascript_helper.rb +403 -35
- data/lib/action_view/helpers/javascripts/controls.js +261 -0
- data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
- data/lib/action_view/helpers/javascripts/effects.js +570 -0
- data/lib/action_view/helpers/javascripts/prototype.js +633 -371
- data/lib/action_view/helpers/number_helper.rb +11 -13
- data/lib/action_view/helpers/tag_helper.rb +1 -2
- data/lib/action_view/helpers/text_helper.rb +69 -6
- data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
- data/lib/action_view/helpers/url_helper.rb +98 -3
- data/lib/action_view/partials.rb +14 -8
- data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
- data/rakefile +13 -5
- data/test/abstract_unit.rb +1 -1
- data/test/controller/action_pack_assertions_test.rb +52 -9
- data/test/controller/active_record_assertions_test.rb +119 -120
- data/test/controller/active_record_store_test.rb +111 -0
- data/test/controller/addresses_render_test.rb +45 -0
- data/test/controller/caching_filestore.rb +92 -0
- data/test/controller/capture_test.rb +39 -0
- data/test/controller/cgi_test.rb +40 -3
- data/test/controller/helper_test.rb +65 -13
- data/test/controller/multipart_progress_testx.rb +365 -0
- data/test/controller/new_render_test.rb +263 -0
- data/test/controller/redirect_test.rb +64 -0
- data/test/controller/render_test.rb +20 -21
- data/test/controller/request_test.rb +83 -3
- data/test/controller/routing_test.rb +702 -0
- data/test/controller/send_file_test.rb +2 -0
- data/test/controller/test_test.rb +44 -8
- data/test/controller/upload_progress_testx.rb +89 -0
- data/test/controller/verification_test.rb +94 -29
- data/test/fixtures/addresses/list.rhtml +1 -0
- data/test/fixtures/test/capturing.rhtml +4 -0
- data/test/fixtures/test/list.rhtml +1 -1
- data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
- data/test/template/active_record_helper_test.rb +30 -15
- data/test/template/asset_tag_helper_test.rb +12 -5
- data/test/template/benchmark_helper_test.rb +72 -0
- data/test/template/date_helper_test.rb +69 -0
- data/test/template/form_helper_test.rb +18 -10
- data/test/template/form_options_helper_test.rb +40 -5
- data/test/template/javascript_helper.rb +149 -2
- data/test/template/number_helper_test.rb +2 -0
- data/test/template/tag_helper_test.rb +4 -0
- data/test/template/text_helper_test.rb +36 -0
- data/test/template/upload_progress_helper_testx.rb +272 -0
- data/test/template/url_helper_test.rb +30 -0
- metadata +30 -6
- data/test/controller/layout_test.rb +0 -49
- data/test/controller/routing_tests.rb +0 -543
@@ -1,7 +1,7 @@
|
|
1
1
|
require 'html/tokenizer'
|
2
2
|
require 'html/node'
|
3
3
|
|
4
|
-
module HTML#:nodoc:
|
4
|
+
module HTML #:nodoc:
|
5
5
|
|
6
6
|
# A top-level HTMl document. You give it a body of text, and it will parse that
|
7
7
|
# text into a tree of nodes.
|
@@ -11,7 +11,7 @@ module HTML#:nodoc:
|
|
11
11
|
attr_reader :root
|
12
12
|
|
13
13
|
# Create a new Document from the given text.
|
14
|
-
def initialize(text)
|
14
|
+
def initialize(text, strict=false)
|
15
15
|
tokenizer = Tokenizer.new(text)
|
16
16
|
@root = Node.new(nil)
|
17
17
|
node_stack = [ @root ]
|
@@ -19,7 +19,7 @@ module HTML#:nodoc:
|
|
19
19
|
node = Node.parse(node_stack.last, tokenizer.line, tokenizer.position, token)
|
20
20
|
|
21
21
|
node_stack.last.children << node unless node.tag? && node.closing == :close
|
22
|
-
if node.tag?
|
22
|
+
if node.tag?
|
23
23
|
if node_stack.length > 1 && node.closing == :close
|
24
24
|
if node_stack.last.name == node.name
|
25
25
|
node_stack.pop
|
@@ -28,7 +28,7 @@ module HTML#:nodoc:
|
|
28
28
|
open_start = 0 if open_start < 0
|
29
29
|
close_start = node.position - 20
|
30
30
|
close_start = 0 if close_start < 0
|
31
|
-
|
31
|
+
msg = <<EOF.strip
|
32
32
|
ignoring attempt to close #{node_stack.last.name} with #{node.name}
|
33
33
|
opened at byte #{node_stack.last.position}, line #{node_stack.last.line}
|
34
34
|
closed at byte #{node.position}, line #{node.line}
|
@@ -36,8 +36,9 @@ ignoring attempt to close #{node_stack.last.name} with #{node.name}
|
|
36
36
|
text around open: #{text[open_start,40].inspect}
|
37
37
|
text around close: #{text[close_start,40].inspect}
|
38
38
|
EOF
|
39
|
+
strict ? raise(msg) : warn(msg)
|
39
40
|
end
|
40
|
-
elsif node.closing != :close
|
41
|
+
elsif !node.childless? && node.closing != :close
|
41
42
|
node_stack.push node
|
42
43
|
end
|
43
44
|
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'strscan'
|
2
2
|
|
3
|
-
module HTML#:nodoc:
|
3
|
+
module HTML #:nodoc:
|
4
4
|
|
5
|
-
class Conditions < Hash#:nodoc:
|
5
|
+
class Conditions < Hash #:nodoc:
|
6
6
|
def initialize(hash)
|
7
7
|
super()
|
8
8
|
hash = { :content => hash } unless Hash === hash
|
@@ -13,7 +13,8 @@ module HTML#:nodoc:
|
|
13
13
|
# keys are valid, and require no further processing
|
14
14
|
when :attributes then
|
15
15
|
hash[k] = keys_to_strings(v)
|
16
|
-
when :parent, :child, :ancestor, :descendant
|
16
|
+
when :parent, :child, :ancestor, :descendant, :sibling, :before,
|
17
|
+
:after
|
17
18
|
hash[k] = Conditions.new(v)
|
18
19
|
when :children
|
19
20
|
hash[k] = v = keys_to_symbols(v)
|
@@ -53,7 +54,7 @@ module HTML#:nodoc:
|
|
53
54
|
end
|
54
55
|
|
55
56
|
# The base class of all nodes, textual and otherwise, in an HTML document.
|
56
|
-
class Node#:nodoc:
|
57
|
+
class Node #:nodoc:
|
57
58
|
# The array of children of this node. Not all nodes have children.
|
58
59
|
attr_reader :children
|
59
60
|
|
@@ -90,6 +91,8 @@ module HTML#:nodoc:
|
|
90
91
|
# Search the children of this node for the first node for which #find
|
91
92
|
# returns non +nil+. Returns the result of the #find call that succeeded.
|
92
93
|
def find(conditions)
|
94
|
+
conditions = validate_conditions(conditions)
|
95
|
+
|
93
96
|
@children.each do |child|
|
94
97
|
node = child.find(conditions)
|
95
98
|
return node if node
|
@@ -100,6 +103,8 @@ module HTML#:nodoc:
|
|
100
103
|
# Search for all nodes that match the given conditions, and return them
|
101
104
|
# as an array.
|
102
105
|
def find_all(conditions)
|
106
|
+
conditions = validate_conditions(conditions)
|
107
|
+
|
103
108
|
matches = []
|
104
109
|
matches << self if match(conditions)
|
105
110
|
@children.each do |child|
|
@@ -119,12 +124,20 @@ module HTML#:nodoc:
|
|
119
124
|
end
|
120
125
|
|
121
126
|
class <<self
|
122
|
-
def parse(parent, line, pos, content)
|
127
|
+
def parse(parent, line, pos, content, strict=true)
|
123
128
|
if content !~ /^<\S/
|
124
129
|
Text.new(parent, line, pos, content)
|
125
130
|
else
|
126
131
|
scanner = StringScanner.new(content)
|
127
|
-
|
132
|
+
|
133
|
+
unless scanner.skip(/</)
|
134
|
+
if strict
|
135
|
+
raise "expected <"
|
136
|
+
else
|
137
|
+
return Text.new(parent, line, pos, content)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
128
141
|
closing = ( scanner.scan(/\//) ? :close : nil )
|
129
142
|
return Text.new(parent, line, pos, content) unless name = scanner.scan(/[\w:]+/)
|
130
143
|
name.downcase!
|
@@ -158,7 +171,14 @@ module HTML#:nodoc:
|
|
158
171
|
closing = ( scanner.scan(/\//) ? :self : nil )
|
159
172
|
end
|
160
173
|
|
161
|
-
scanner.scan(/\s*>/)
|
174
|
+
unless scanner.scan(/\s*>/)
|
175
|
+
if strict
|
176
|
+
raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
|
177
|
+
else
|
178
|
+
# throw away all text until we find what we're looking for
|
179
|
+
scanner.skip_until(/>/) or scanner.terminate
|
180
|
+
end
|
181
|
+
end
|
162
182
|
|
163
183
|
Tag.new(parent, line, pos, name, attributes, closing)
|
164
184
|
end
|
@@ -167,7 +187,7 @@ module HTML#:nodoc:
|
|
167
187
|
end
|
168
188
|
|
169
189
|
# A node that represents text, rather than markup.
|
170
|
-
class Text < Node#:nodoc:
|
190
|
+
class Text < Node #:nodoc:
|
171
191
|
|
172
192
|
attr_reader :content
|
173
193
|
|
@@ -223,7 +243,7 @@ module HTML#:nodoc:
|
|
223
243
|
# A Tag is any node that represents markup. It may be an opening tag, a
|
224
244
|
# closing tag, or a self-closing tag. It has a name, and may have a hash of
|
225
245
|
# attributes.
|
226
|
-
class Tag < Node#:nodoc:
|
246
|
+
class Tag < Node #:nodoc:
|
227
247
|
|
228
248
|
# Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
|
229
249
|
attr_reader :closing
|
@@ -252,7 +272,9 @@ module HTML#:nodoc:
|
|
252
272
|
|
253
273
|
# Returns non-+nil+ if this tag can contain child nodes.
|
254
274
|
def childless?
|
255
|
-
|
275
|
+
!@closing.nil? ||
|
276
|
+
@name =~ /^(img|br|hr|link|meta|area|base|basefont|
|
277
|
+
col|frame|input|isindex|param)$/ox
|
256
278
|
end
|
257
279
|
|
258
280
|
# Returns a textual representation of the node
|
@@ -261,10 +283,14 @@ module HTML#:nodoc:
|
|
261
283
|
"</#{@name}>"
|
262
284
|
else
|
263
285
|
s = "<#{@name}"
|
264
|
-
@attributes.each
|
286
|
+
@attributes.each do |k,v|
|
287
|
+
s << " #{k}"
|
288
|
+
s << "='#{v.gsub(/'/,"\\\\'")}'" if String === v
|
289
|
+
end
|
265
290
|
s << " /" if @closing == :self
|
266
291
|
s << ">"
|
267
292
|
@children.each { |child| s << child.to_s }
|
293
|
+
s << "</#{@name}>" if @closing != :self && !@children.empty?
|
268
294
|
s
|
269
295
|
end
|
270
296
|
end
|
@@ -296,6 +322,12 @@ module HTML#:nodoc:
|
|
296
322
|
# meet the criteria described by the hash.
|
297
323
|
# * <tt>:descendant</tt>: a hash. At least one of the node's descendants
|
298
324
|
# must meet the criteria described by the hash.
|
325
|
+
# * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
|
326
|
+
# meet the criteria described by the hash.
|
327
|
+
# * <tt>:after</tt>: a hash. The node must be after any sibling meeting
|
328
|
+
# the criteria described by the hash, and at least one sibling must match.
|
329
|
+
# * <tt>:before</tt>: a hash. The node must be before any sibling meeting
|
330
|
+
# the criteria described by the hash, and at least one sibling must match.
|
299
331
|
# * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
|
300
332
|
# keys:
|
301
333
|
# ** <tt>:count</tt>: either a number or a range which must equal (or
|
@@ -347,9 +379,9 @@ module HTML#:nodoc:
|
|
347
379
|
# :child => /hello world/ }
|
348
380
|
def match(conditions)
|
349
381
|
conditions = validate_conditions(conditions)
|
350
|
-
|
351
|
-
#
|
352
|
-
return false if conditions[:content]
|
382
|
+
|
383
|
+
# check content of child nodes
|
384
|
+
return false unless children.find { |child| child.match(conditions[:content]) } if conditions[:content]
|
353
385
|
|
354
386
|
# test the name
|
355
387
|
return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
|
@@ -404,6 +436,30 @@ module HTML#:nodoc:
|
|
404
436
|
end
|
405
437
|
end
|
406
438
|
end
|
439
|
+
|
440
|
+
# test siblings
|
441
|
+
if conditions[:sibling] || conditions[:before] || conditions[:after]
|
442
|
+
siblings = parent ? parent.children : []
|
443
|
+
self_index = siblings.index(self)
|
444
|
+
|
445
|
+
if conditions[:sibling]
|
446
|
+
return false unless siblings.detect do |s|
|
447
|
+
s != self && s.match(conditions[:sibling])
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
if conditions[:before]
|
452
|
+
return false unless siblings[self_index+1..-1].detect do |s|
|
453
|
+
s != self && s.match(conditions[:before])
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
if conditions[:after]
|
458
|
+
return false unless siblings[0,self_index].detect do |s|
|
459
|
+
s != self && s.match(conditions[:after])
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
407
463
|
|
408
464
|
true
|
409
465
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'strscan'
|
2
2
|
|
3
|
-
module HTML#:nodoc:
|
3
|
+
module HTML #:nodoc:
|
4
4
|
|
5
5
|
# A simple HTML tokenizer. It simply breaks a stream of text into tokens, where each
|
6
6
|
# token is a string. Each string represents either "text", or an HTML element.
|
@@ -13,7 +13,7 @@ module HTML#:nodoc:
|
|
13
13
|
# while token = tokenizer.next
|
14
14
|
# p token
|
15
15
|
# end
|
16
|
-
class Tokenizer#:nodoc:
|
16
|
+
class Tokenizer #:nodoc:
|
17
17
|
|
18
18
|
# The current (byte) position in the text
|
19
19
|
attr_reader :position
|
@@ -51,7 +51,7 @@ module HTML#:nodoc:
|
|
51
51
|
tag = @scanner.getch
|
52
52
|
if @scanner.scan(/!--/) # comment
|
53
53
|
tag << @scanner.matched
|
54
|
-
tag << @scanner.scan_until(/--\s*>/)
|
54
|
+
tag << (@scanner.scan_until(/--\s*>/) || @scanner.scan_until(/\Z/))
|
55
55
|
elsif @scanner.scan(/!/) # doctype
|
56
56
|
tag << @scanner.matched
|
57
57
|
tag << consume_quoted_regions
|
@@ -63,14 +63,13 @@ module HTML#:nodoc:
|
|
63
63
|
|
64
64
|
# Scan all text up to the next < character and return it.
|
65
65
|
def scan_text
|
66
|
-
@scanner.scan(/[^<]*/)
|
66
|
+
"#{@scanner.getch}#{@scanner.scan(/[^<]*/)}"
|
67
67
|
end
|
68
68
|
|
69
69
|
# Counts the number of newlines in the text and updates the current line
|
70
70
|
# accordingly.
|
71
71
|
def update_current_line(text)
|
72
|
-
|
73
|
-
text
|
72
|
+
text.scan(/\r?\n/) { @current_line += 1 }
|
74
73
|
end
|
75
74
|
|
76
75
|
# Skips over quoted strings, so that less-than and greater-than characters
|
@@ -78,10 +77,18 @@ module HTML#:nodoc:
|
|
78
77
|
def consume_quoted_regions
|
79
78
|
text = ""
|
80
79
|
loop do
|
81
|
-
match = @scanner.scan_until(/['"
|
80
|
+
match = @scanner.scan_until(/['"<>]/) or break
|
81
|
+
|
82
|
+
delim = @scanner.matched
|
83
|
+
if delim == "<"
|
84
|
+
match = match.chop
|
85
|
+
@scanner.pos -= 1
|
86
|
+
end
|
87
|
+
|
82
88
|
text << match
|
83
|
-
break if
|
84
|
-
|
89
|
+
break if delim == "<" || delim == ">"
|
90
|
+
|
91
|
+
# consume the quoted region
|
85
92
|
while match = @scanner.scan_until(/[\\#{delim}]/)
|
86
93
|
text << match
|
87
94
|
break if @scanner.matched == delim
|
@@ -92,4 +99,4 @@ module HTML#:nodoc:
|
|
92
99
|
end
|
93
100
|
end
|
94
101
|
|
95
|
-
end
|
102
|
+
end
|
@@ -0,0 +1,1019 @@
|
|
1
|
+
# = XmlSimple
|
2
|
+
#
|
3
|
+
# Author:: Maik Schmidt <contact@maik-schmidt.de>
|
4
|
+
# Copyright:: Copyright (c) 2003 Maik Schmidt
|
5
|
+
# License:: Distributes under the same terms as Ruby.
|
6
|
+
#
|
7
|
+
require 'rexml/document'
|
8
|
+
|
9
|
+
# Easy API to maintain XML (especially configuration files).
|
10
|
+
class XmlSimple #:nodoc:
|
11
|
+
include REXML
|
12
|
+
|
13
|
+
@@VERSION = '1.0.2'
|
14
|
+
|
15
|
+
# A simple cache for XML documents that were already transformed
|
16
|
+
# by xml_in.
|
17
|
+
class Cache #:nodoc:
|
18
|
+
# Creates and initializes a new Cache object.
|
19
|
+
def initialize
|
20
|
+
@mem_share_cache = {}
|
21
|
+
@mem_copy_cache = {}
|
22
|
+
end
|
23
|
+
|
24
|
+
# Saves a data structure into a file.
|
25
|
+
#
|
26
|
+
# data::
|
27
|
+
# Data structure to be saved.
|
28
|
+
# filename::
|
29
|
+
# Name of the file belonging to the data structure.
|
30
|
+
def save_storable(data, filename)
|
31
|
+
cache_file = get_cache_filename(filename)
|
32
|
+
File.open(cache_file, "w+") { |f| Marshal.dump(data, f) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Restores a data structure from a file. If restoring the data
|
36
|
+
# structure failed for any reason, nil will be returned.
|
37
|
+
#
|
38
|
+
# filename::
|
39
|
+
# Name of the file belonging to the data structure.
|
40
|
+
def restore_storable(filename)
|
41
|
+
cache_file = get_cache_filename(filename)
|
42
|
+
return nil unless File::exist?(cache_file)
|
43
|
+
return nil unless File::mtime(cache_file).to_i > File::mtime(filename).to_i
|
44
|
+
data = nil
|
45
|
+
File.open(cache_file) { |f| data = Marshal.load(f) }
|
46
|
+
data
|
47
|
+
end
|
48
|
+
|
49
|
+
# Saves a data structure in a shared memory cache.
|
50
|
+
#
|
51
|
+
# data::
|
52
|
+
# Data structure to be saved.
|
53
|
+
# filename::
|
54
|
+
# Name of the file belonging to the data structure.
|
55
|
+
def save_mem_share(data, filename)
|
56
|
+
@mem_share_cache[filename] = [Time::now.to_i, data]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Restores a data structure from a shared memory cache. You
|
60
|
+
# should consider these elements as "read only". If restoring
|
61
|
+
# the data structure failed for any reason, nil will be
|
62
|
+
# returned.
|
63
|
+
#
|
64
|
+
# filename::
|
65
|
+
# Name of the file belonging to the data structure.
|
66
|
+
def restore_mem_share(filename)
|
67
|
+
get_from_memory_cache(filename, @mem_share_cache)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Copies a data structure to a memory cache.
|
71
|
+
#
|
72
|
+
# data::
|
73
|
+
# Data structure to be copied.
|
74
|
+
# filename::
|
75
|
+
# Name of the file belonging to the data structure.
|
76
|
+
def save_mem_copy(data, filename)
|
77
|
+
@mem_share_cache[filename] = [Time::now.to_i, Marshal.dump(data)]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Restores a data structure from a memory cache. If restoring
|
81
|
+
# the data structure failed for any reason, nil will be
|
82
|
+
# returned.
|
83
|
+
#
|
84
|
+
# filename::
|
85
|
+
# Name of the file belonging to the data structure.
|
86
|
+
def restore_mem_copy(filename)
|
87
|
+
data = get_from_memory_cache(filename, @mem_share_cache)
|
88
|
+
data = Marshal.load(data) unless data.nil?
|
89
|
+
data
|
90
|
+
end
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Returns the "cache filename" belonging to a filename, i.e.
|
95
|
+
# the extension '.xml' in the original filename will be replaced
|
96
|
+
# by '.stor'. If filename does not have this extension, '.stor'
|
97
|
+
# will be appended.
|
98
|
+
#
|
99
|
+
# filename::
|
100
|
+
# Filename to get "cache filename" for.
|
101
|
+
def get_cache_filename(filename)
|
102
|
+
filename.sub(/(\.xml)?$/, '.stor')
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns a cache entry from a memory cache belonging to a
|
106
|
+
# certain filename. If no entry could be found for any reason,
|
107
|
+
# nil will be returned.
|
108
|
+
#
|
109
|
+
# filename::
|
110
|
+
# Name of the file the cache entry belongs to.
|
111
|
+
# cache::
|
112
|
+
# Memory cache to get entry from.
|
113
|
+
def get_from_memory_cache(filename, cache)
|
114
|
+
return nil unless cache[filename]
|
115
|
+
return nil unless cache[filename][0] > File::mtime(filename).to_i
|
116
|
+
return cache[filename][1]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Create a "global" cache.
|
121
|
+
@@cache = Cache.new
|
122
|
+
|
123
|
+
# Creates and intializes a new XmlSimple object.
|
124
|
+
#
|
125
|
+
# defaults::
|
126
|
+
# Default values for options.
|
127
|
+
def initialize(defaults = nil)
|
128
|
+
unless defaults.nil? || defaults.instance_of?(Hash)
|
129
|
+
raise ArgumentError, "Options have to be a Hash."
|
130
|
+
end
|
131
|
+
@default_options = normalize_option_names(defaults, KNOWN_OPTIONS['in'] & KNOWN_OPTIONS['out'])
|
132
|
+
@options = Hash.new
|
133
|
+
@_var_values = nil
|
134
|
+
end
|
135
|
+
|
136
|
+
# Converts an XML document in the same way as the Perl module XML::Simple.
|
137
|
+
#
|
138
|
+
# string::
|
139
|
+
# XML source. Could be one of the following:
|
140
|
+
#
|
141
|
+
# - nil: Tries to load and parse '<scriptname>.xml'.
|
142
|
+
# - filename: Tries to load and parse filename.
|
143
|
+
# - IO object: Reads from object until EOF is detected and parses result.
|
144
|
+
# - XML string: Parses string.
|
145
|
+
#
|
146
|
+
# options::
|
147
|
+
# Options to be used.
|
148
|
+
def xml_in(string = nil, options = nil)
|
149
|
+
handle_options('in', options)
|
150
|
+
|
151
|
+
# If no XML string or filename was supplied look for scriptname.xml.
|
152
|
+
if string.nil?
|
153
|
+
string = File::basename($0)
|
154
|
+
string.sub!(/\.[^.]+$/, '')
|
155
|
+
string += '.xml'
|
156
|
+
|
157
|
+
directory = File::dirname($0)
|
158
|
+
@options['searchpath'].unshift(directory) unless directory.nil?
|
159
|
+
end
|
160
|
+
|
161
|
+
if string.instance_of?(String)
|
162
|
+
if string =~ /<.*?>/m
|
163
|
+
@doc = parse(string)
|
164
|
+
elsif string == '-'
|
165
|
+
@doc = parse($stdin.readlines.to_s)
|
166
|
+
else
|
167
|
+
filename = find_xml_file(string, @options['searchpath'])
|
168
|
+
|
169
|
+
if @options.has_key?('cache')
|
170
|
+
@options['cache'].each { |scheme|
|
171
|
+
case(scheme)
|
172
|
+
when 'storable'
|
173
|
+
content = @@cache.restore_storable(filename)
|
174
|
+
when 'mem_share'
|
175
|
+
content = @@cache.restore_mem_share(filename)
|
176
|
+
when 'mem_copy'
|
177
|
+
content = @@cache.restore_mem_copy(filename)
|
178
|
+
else
|
179
|
+
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
|
180
|
+
end
|
181
|
+
return content if content
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
185
|
+
@doc = load_xml_file(filename)
|
186
|
+
end
|
187
|
+
elsif string.kind_of?(IO)
|
188
|
+
@doc = parse(string.readlines.to_s)
|
189
|
+
else
|
190
|
+
raise ArgumentError, "Could not parse object of type: <#{string.type}>."
|
191
|
+
end
|
192
|
+
|
193
|
+
result = collapse(@doc.root)
|
194
|
+
result = @options['keeproot'] ? merge({}, @doc.root.name, result) : result
|
195
|
+
put_into_cache(result, filename)
|
196
|
+
result
|
197
|
+
end
|
198
|
+
|
199
|
+
# This is the functional version of the instance method xml_in.
|
200
|
+
def XmlSimple.xml_in(string = nil, options = nil)
|
201
|
+
xml_simple = XmlSimple.new
|
202
|
+
xml_simple.xml_in(string, options)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Converts a data structure into an XML document.
|
206
|
+
#
|
207
|
+
# ref::
|
208
|
+
# Reference to data structure to be converted into XML.
|
209
|
+
# options::
|
210
|
+
# Options to be used.
|
211
|
+
def xml_out(ref, options = nil)
|
212
|
+
handle_options('out', options)
|
213
|
+
if ref.instance_of?(Array)
|
214
|
+
ref = { @options['anonymoustag'] => ref }
|
215
|
+
end
|
216
|
+
|
217
|
+
if @options['keeproot']
|
218
|
+
keys = ref.keys
|
219
|
+
if keys.size == 1
|
220
|
+
ref = ref[keys[0]]
|
221
|
+
@options['rootname'] = keys[0]
|
222
|
+
end
|
223
|
+
elsif @options['rootname'] == ''
|
224
|
+
if ref.instance_of?(Hash)
|
225
|
+
refsave = ref
|
226
|
+
ref = {}
|
227
|
+
refsave.each { |key, value|
|
228
|
+
if !scalar(value)
|
229
|
+
ref[key] = value
|
230
|
+
else
|
231
|
+
ref[key] = [ value.to_s ]
|
232
|
+
end
|
233
|
+
}
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
@ancestors = []
|
238
|
+
xml = value_to_xml(ref, @options['rootname'], '')
|
239
|
+
@ancestors = nil
|
240
|
+
|
241
|
+
if @options['xmldeclaration']
|
242
|
+
xml = @options['xmldeclaration'] + "\n" + xml
|
243
|
+
end
|
244
|
+
|
245
|
+
if @options.has_key?('outputfile')
|
246
|
+
if @options['outputfile'].kind_of?(IO)
|
247
|
+
return @options['outputfile'].write(xml)
|
248
|
+
else
|
249
|
+
File.open(@options['outputfile'], "w") { |file| file.write(xml) }
|
250
|
+
end
|
251
|
+
end
|
252
|
+
xml
|
253
|
+
end
|
254
|
+
|
255
|
+
# This is the functional version of the instance method xml_out.
|
256
|
+
def XmlSimple.xml_out(hash, options = nil)
|
257
|
+
xml_simple = XmlSimple.new
|
258
|
+
xml_simple.xml_out(hash, options)
|
259
|
+
end
|
260
|
+
|
261
|
+
private
|
262
|
+
|
263
|
+
# Declare options that are valid for xml_in and xml_out.
|
264
|
+
KNOWN_OPTIONS = {
|
265
|
+
'in' => %w(
|
266
|
+
keyattr keeproot forcecontent contentkey noattr
|
267
|
+
searchpath forcearray suppressempty anonymoustag
|
268
|
+
cache grouptags normalisespace normalizespace
|
269
|
+
variables varattr
|
270
|
+
),
|
271
|
+
'out' => %w(
|
272
|
+
keyattr keeproot contentkey noattr rootname
|
273
|
+
xmldeclaration outputfile noescape suppressempty
|
274
|
+
anonymoustag indent grouptags noindent
|
275
|
+
)
|
276
|
+
}
|
277
|
+
|
278
|
+
# Define some reasonable defaults.
|
279
|
+
DEF_KEY_ATTRIBUTES = []
|
280
|
+
DEF_ROOT_NAME = 'opt'
|
281
|
+
DEF_CONTENT_KEY = 'content'
|
282
|
+
DEF_XML_DECLARATION = "<?xml version='1.0' standalone='yes'?>"
|
283
|
+
DEF_ANONYMOUS_TAG = 'anon'
|
284
|
+
DEF_FORCE_ARRAY = true
|
285
|
+
DEF_INDENTATION = ' '
|
286
|
+
|
287
|
+
# Normalizes option names in a hash, i.e., turns all
|
288
|
+
# characters to lower case and removes all underscores.
|
289
|
+
# Additionally, this method checks, if an unknown option
|
290
|
+
# was used and raises an according exception.
|
291
|
+
#
|
292
|
+
# options::
|
293
|
+
# Hash to be normalized.
|
294
|
+
# known_options::
|
295
|
+
# List of known options.
|
296
|
+
def normalize_option_names(options, known_options)
|
297
|
+
return nil if options.nil?
|
298
|
+
result = Hash.new
|
299
|
+
options.each { |key, value|
|
300
|
+
lkey = key.downcase
|
301
|
+
lkey.gsub!(/_/, '')
|
302
|
+
if !known_options.member?(lkey)
|
303
|
+
raise ArgumentError, "Unrecognised option: #{lkey}."
|
304
|
+
end
|
305
|
+
result[lkey] = value
|
306
|
+
}
|
307
|
+
result
|
308
|
+
end
|
309
|
+
|
310
|
+
# Merges a set of options with the default options.
|
311
|
+
#
|
312
|
+
# direction::
|
313
|
+
# 'in': If options should be handled for xml_in.
|
314
|
+
# 'out': If options should be handled for xml_out.
|
315
|
+
# options::
|
316
|
+
# Options to be merged with the default options.
|
317
|
+
def handle_options(direction, options)
|
318
|
+
@options = options || Hash.new
|
319
|
+
|
320
|
+
raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash)
|
321
|
+
|
322
|
+
unless KNOWN_OPTIONS.has_key?(direction)
|
323
|
+
raise ArgumentError, "Unknown direction: <#{direction}>."
|
324
|
+
end
|
325
|
+
|
326
|
+
known_options = KNOWN_OPTIONS[direction]
|
327
|
+
@options = normalize_option_names(@options, known_options)
|
328
|
+
|
329
|
+
unless @default_options.nil?
|
330
|
+
known_options.each { |option|
|
331
|
+
unless @options.has_key?(option)
|
332
|
+
if @default_options.has_key?(option)
|
333
|
+
@options[option] = @default_options[option]
|
334
|
+
end
|
335
|
+
end
|
336
|
+
}
|
337
|
+
end
|
338
|
+
|
339
|
+
unless @options.has_key?('noattr')
|
340
|
+
@options['noattr'] = false
|
341
|
+
end
|
342
|
+
|
343
|
+
if @options.has_key?('rootname')
|
344
|
+
@options['rootname'] = '' if @options['rootname'].nil?
|
345
|
+
else
|
346
|
+
@options['rootname'] = DEF_ROOT_NAME
|
347
|
+
end
|
348
|
+
|
349
|
+
if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true
|
350
|
+
@options['xmldeclaration'] = DEF_XML_DECLARATION
|
351
|
+
end
|
352
|
+
|
353
|
+
if @options.has_key?('contentkey')
|
354
|
+
if @options['contentkey'] =~ /^-(.*)$/
|
355
|
+
@options['contentkey'] = $1
|
356
|
+
@options['collapseagain'] = true
|
357
|
+
end
|
358
|
+
else
|
359
|
+
@options['contentkey'] = DEF_CONTENT_KEY
|
360
|
+
end
|
361
|
+
|
362
|
+
unless @options.has_key?('normalisespace')
|
363
|
+
@options['normalisespace'] = @options['normalizespace']
|
364
|
+
end
|
365
|
+
@options['normalisespace'] = 0 if @options['normalisespace'].nil?
|
366
|
+
|
367
|
+
if @options.has_key?('searchpath')
|
368
|
+
unless @options['searchpath'].instance_of?(Array)
|
369
|
+
@options['searchpath'] = [ @options['searchpath'] ]
|
370
|
+
end
|
371
|
+
else
|
372
|
+
@options['searchpath'] = []
|
373
|
+
end
|
374
|
+
|
375
|
+
if @options.has_key?('cache') && scalar(@options['cache'])
|
376
|
+
@options['cache'] = [ @options['cache'] ]
|
377
|
+
end
|
378
|
+
|
379
|
+
@options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag')
|
380
|
+
|
381
|
+
if !@options.has_key?('indent') || @options['indent'].nil?
|
382
|
+
@options['indent'] = DEF_INDENTATION
|
383
|
+
end
|
384
|
+
|
385
|
+
@options['indent'] = '' if @options.has_key?('noindent')
|
386
|
+
|
387
|
+
# Special cleanup for 'keyattr' which could be an array or
|
388
|
+
# a hash or left to default to array.
|
389
|
+
if @options.has_key?('keyattr')
|
390
|
+
if !scalar(@options['keyattr'])
|
391
|
+
# Convert keyattr => { elem => '+attr' }
|
392
|
+
# to keyattr => { elem => ['attr', '+'] }
|
393
|
+
if @options['keyattr'].instance_of?(Hash)
|
394
|
+
@options['keyattr'].each { |key, value|
|
395
|
+
if value =~ /^([-+])?(.*)$/
|
396
|
+
@options['keyattr'][key] = [$2, $1 ? $1 : '']
|
397
|
+
end
|
398
|
+
}
|
399
|
+
elsif !@options['keyattr'].instance_of?(Array)
|
400
|
+
raise ArgumentError, "'keyattr' must be String, Hash, or Array!"
|
401
|
+
end
|
402
|
+
else
|
403
|
+
@options['keyattr'] = [ @options['keyattr'] ]
|
404
|
+
end
|
405
|
+
else
|
406
|
+
@options['keyattr'] = DEF_KEY_ATTRIBUTES
|
407
|
+
end
|
408
|
+
|
409
|
+
if @options.has_key?('forcearray')
|
410
|
+
if @options['forcearray'].instance_of?(Regexp)
|
411
|
+
@options['forcearray'] = [ @options['forcearray'] ]
|
412
|
+
end
|
413
|
+
|
414
|
+
if @options['forcearray'].instance_of?(Array)
|
415
|
+
force_list = @options['forcearray']
|
416
|
+
unless force_list.empty?
|
417
|
+
@options['forcearray'] = {}
|
418
|
+
force_list.each { |tag|
|
419
|
+
if tag.instance_of?(Regexp)
|
420
|
+
unless @options['forcearray']['_regex'].instance_of?(Array)
|
421
|
+
@options['forcearray']['_regex'] = []
|
422
|
+
end
|
423
|
+
@options['forcearray']['_regex'] << tag
|
424
|
+
else
|
425
|
+
@options['forcearray'][tag] = true
|
426
|
+
end
|
427
|
+
}
|
428
|
+
else
|
429
|
+
@options['forcearray'] = false
|
430
|
+
end
|
431
|
+
else
|
432
|
+
@options['forcearray'] = @options['forcearray'] ? true : false
|
433
|
+
end
|
434
|
+
else
|
435
|
+
@options['forcearray'] = DEF_FORCE_ARRAY
|
436
|
+
end
|
437
|
+
|
438
|
+
if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash)
|
439
|
+
raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash."
|
440
|
+
end
|
441
|
+
|
442
|
+
if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash)
|
443
|
+
raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash."
|
444
|
+
end
|
445
|
+
|
446
|
+
if @options.has_key?('variables')
|
447
|
+
@_var_values = @options['variables']
|
448
|
+
elsif @options.has_key?('varattr')
|
449
|
+
@_var_values = {}
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
453
|
+
# Actually converts an XML document element into a data structure.
|
454
|
+
#
|
455
|
+
# element::
|
456
|
+
# The document element to be collapsed.
|
457
|
+
def collapse(element)
|
458
|
+
result = @options['noattr'] ? {} : get_attributes(element)
|
459
|
+
|
460
|
+
if @options['normalisespace'] == 2
|
461
|
+
result.each { |k, v| result[k] = normalise_space(v) }
|
462
|
+
end
|
463
|
+
|
464
|
+
if element.has_elements?
|
465
|
+
element.each_element { |child|
|
466
|
+
value = collapse(child)
|
467
|
+
if empty(value) && (element.attributes.empty? || @options['noattr'])
|
468
|
+
next if @options.has_key?('suppressempty') && @options['suppressempty'] == true
|
469
|
+
end
|
470
|
+
result = merge(result, child.name, value)
|
471
|
+
}
|
472
|
+
if has_mixed_content?(element)
|
473
|
+
# normalisespace?
|
474
|
+
content = element.texts.map { |x| x.to_s }
|
475
|
+
content = content[0] if content.size == 1
|
476
|
+
result[@options['contentkey']] = content
|
477
|
+
end
|
478
|
+
elsif element.has_text? # i.e. it has only text.
|
479
|
+
return collapse_text_node(result, element)
|
480
|
+
end
|
481
|
+
|
482
|
+
# Turn Arrays into Hashes if key fields present.
|
483
|
+
count = fold_arrays(result)
|
484
|
+
|
485
|
+
# Disintermediate grouped tags.
|
486
|
+
if @options.has_key?('grouptags')
|
487
|
+
result.each { |key, value|
|
488
|
+
next unless (value.instance_of?(Hash) && (value.size == 1))
|
489
|
+
child_key, child_value = value.to_a[0]
|
490
|
+
if @options['grouptags'][key] == child_key
|
491
|
+
result[key] = child_value
|
492
|
+
end
|
493
|
+
}
|
494
|
+
end
|
495
|
+
|
496
|
+
# Fold Hases containing a single anonymous Array up into just the Array.
|
497
|
+
if count == 1
|
498
|
+
anonymoustag = @options['anonymoustag']
|
499
|
+
if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
|
500
|
+
return result[anonymoustag]
|
501
|
+
end
|
502
|
+
end
|
503
|
+
|
504
|
+
if result.empty? && @options.has_key?('suppressempty')
|
505
|
+
return @options['suppressempty'] == '' ? '' : nil
|
506
|
+
end
|
507
|
+
|
508
|
+
result
|
509
|
+
end
|
510
|
+
|
511
|
+
# Collapses a text node and merges it with an existing Hash, if
|
512
|
+
# possible.
|
513
|
+
# Thanks to Curtis Schofield for reporting a subtle bug.
|
514
|
+
#
|
515
|
+
# hash::
|
516
|
+
# Hash to merge text node value with, if possible.
|
517
|
+
# element::
|
518
|
+
# Text node to be collapsed.
|
519
|
+
def collapse_text_node(hash, element)
|
520
|
+
value = node_to_text(element)
|
521
|
+
if empty(value) && !element.has_attributes?
|
522
|
+
return {}
|
523
|
+
end
|
524
|
+
|
525
|
+
if element.has_attributes? && !@options['noattr']
|
526
|
+
return merge(hash, @options['contentkey'], value)
|
527
|
+
else
|
528
|
+
if @options['forcecontent']
|
529
|
+
return merge(hash, @options['contentkey'], value)
|
530
|
+
else
|
531
|
+
return value
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|
535
|
+
|
536
|
+
# Folds all arrays in a Hash.
|
537
|
+
#
|
538
|
+
# hash::
|
539
|
+
# Hash to be folded.
|
540
|
+
def fold_arrays(hash)
|
541
|
+
fold_amount = 0
|
542
|
+
keyattr = @options['keyattr']
|
543
|
+
if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash))
|
544
|
+
hash.each { |key, value|
|
545
|
+
if value.instance_of?(Array)
|
546
|
+
if keyattr.instance_of?(Array)
|
547
|
+
hash[key] = fold_array(value)
|
548
|
+
else
|
549
|
+
hash[key] = fold_array_by_name(key, value)
|
550
|
+
end
|
551
|
+
fold_amount += 1
|
552
|
+
end
|
553
|
+
}
|
554
|
+
end
|
555
|
+
fold_amount
|
556
|
+
end
|
557
|
+
|
558
|
+
# Folds an Array to a Hash, if possible. Folding happens
|
559
|
+
# according to the content of keyattr, which has to be
|
560
|
+
# an array.
|
561
|
+
#
|
562
|
+
# array::
|
563
|
+
# Array to be folded.
|
564
|
+
def fold_array(array)
|
565
|
+
hash = Hash.new
|
566
|
+
array.each { |x|
|
567
|
+
return array unless x.instance_of?(Hash)
|
568
|
+
key_matched = false
|
569
|
+
@options['keyattr'].each { |key|
|
570
|
+
if x.has_key?(key)
|
571
|
+
key_matched = true
|
572
|
+
value = x[key]
|
573
|
+
return array if value.instance_of?(Hash) || value.instance_of?(Array)
|
574
|
+
value = normalise_space(value) if @options['normalisespace'] == 1
|
575
|
+
x.delete(key)
|
576
|
+
hash[value] = x
|
577
|
+
break
|
578
|
+
end
|
579
|
+
}
|
580
|
+
return array unless key_matched
|
581
|
+
}
|
582
|
+
hash = collapse_content(hash) if @options['collapseagain']
|
583
|
+
hash
|
584
|
+
end
|
585
|
+
|
586
|
+
# Folds an Array to a Hash, if possible. Folding happens
|
587
|
+
# according to the content of keyattr, which has to be
|
588
|
+
# a Hash.
|
589
|
+
#
|
590
|
+
# name::
|
591
|
+
# Name of the attribute to be folded upon.
|
592
|
+
# array::
|
593
|
+
# Array to be folded.
|
594
|
+
def fold_array_by_name(name, array)
|
595
|
+
return array unless @options['keyattr'].has_key?(name)
|
596
|
+
key, flag = @options['keyattr'][name]
|
597
|
+
|
598
|
+
hash = Hash.new
|
599
|
+
array.each { |x|
|
600
|
+
if x.instance_of?(Hash) && x.has_key?(key)
|
601
|
+
value = x[key]
|
602
|
+
return array if value.instance_of?(Hash) || value.instance_of?(Array)
|
603
|
+
value = normalise_space(value) if @options['normalisespace'] == 1
|
604
|
+
hash[value] = x
|
605
|
+
hash[value]["-#{key}"] = hash[value][key] if flag == '-'
|
606
|
+
hash[value].delete(key) unless flag == '+'
|
607
|
+
else
|
608
|
+
$stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.")
|
609
|
+
return array
|
610
|
+
end
|
611
|
+
}
|
612
|
+
hash = collapse_content(hash) if @options['collapseagain']
|
613
|
+
hash
|
614
|
+
end
|
615
|
+
|
616
|
+
# Tries to collapse a Hash even more ;-)
|
617
|
+
#
|
618
|
+
# hash::
|
619
|
+
# Hash to be collapsed again.
|
620
|
+
def collapse_content(hash)
|
621
|
+
content_key = @options['contentkey']
|
622
|
+
hash.each_value { |value|
|
623
|
+
return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key)
|
624
|
+
hash.each_key { |key| hash[key] = hash[key][content_key] }
|
625
|
+
}
|
626
|
+
hash
|
627
|
+
end
|
628
|
+
|
629
|
+
# Adds a new key/value pair to an existing Hash. If the key to be added
|
630
|
+
# does already exist and the existing value associated with key is not
|
631
|
+
# an Array, it will be converted into an Array. Then the new value is
|
632
|
+
# appended to that Array.
|
633
|
+
#
|
634
|
+
# hash::
|
635
|
+
# Hash to add key/value pair to.
|
636
|
+
# key::
|
637
|
+
# Key to be added.
|
638
|
+
# value::
|
639
|
+
# Value to be associated with key.
|
640
|
+
def merge(hash, key, value)
|
641
|
+
if value.instance_of?(String)
|
642
|
+
value = normalise_space(value) if @options['normalisespace'] == 2
|
643
|
+
|
644
|
+
# do variable substitutions
|
645
|
+
unless @_var_values.nil? || @_var_values.empty?
|
646
|
+
value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
|
647
|
+
end
|
648
|
+
|
649
|
+
# look for variable definitions
|
650
|
+
if @options.has_key?('varattr')
|
651
|
+
varattr = @options['varattr']
|
652
|
+
if hash.has_key?(varattr)
|
653
|
+
set_var(hash[varattr], value)
|
654
|
+
end
|
655
|
+
end
|
656
|
+
end
|
657
|
+
if hash.has_key?(key)
|
658
|
+
if hash[key].instance_of?(Array)
|
659
|
+
hash[key] << value
|
660
|
+
else
|
661
|
+
hash[key] = [ hash[key], value ]
|
662
|
+
end
|
663
|
+
elsif value.instance_of?(Array) # Handle anonymous arrays.
|
664
|
+
hash[key] = [ value ]
|
665
|
+
else
|
666
|
+
if force_array?(key)
|
667
|
+
hash[key] = [ value ]
|
668
|
+
else
|
669
|
+
hash[key] = value
|
670
|
+
end
|
671
|
+
end
|
672
|
+
hash
|
673
|
+
end
|
674
|
+
|
675
|
+
# Checks, if the 'forcearray' option has to be used for
|
676
|
+
# a certain key.
|
677
|
+
def force_array?(key)
|
678
|
+
return false if key == @options['contentkey']
|
679
|
+
return true if @options['forcearray'] == true
|
680
|
+
forcearray = @options['forcearray']
|
681
|
+
if forcearray.instance_of?(Hash)
|
682
|
+
return true if forcearray.has_key?(key)
|
683
|
+
return false unless forcearray.has_key?('_regex')
|
684
|
+
forcearray['_regex'].each { |x| return true if key =~ x }
|
685
|
+
end
|
686
|
+
return false
|
687
|
+
end
|
688
|
+
|
689
|
+
# Converts the attributes array of a document node into a Hash.
|
690
|
+
# Returns an empty Hash, if node has no attributes.
|
691
|
+
#
|
692
|
+
# node::
|
693
|
+
# Document node to extract attributes from.
|
694
|
+
def get_attributes(node)
|
695
|
+
attributes = {}
|
696
|
+
node.attributes.each { |n,v| attributes[n] = v }
|
697
|
+
attributes
|
698
|
+
end
|
699
|
+
|
700
|
+
# Determines, if a document element has mixed content.
|
701
|
+
#
|
702
|
+
# element::
|
703
|
+
# Document element to be checked.
|
704
|
+
def has_mixed_content?(element)
|
705
|
+
if element.has_text? && element.has_elements?
|
706
|
+
return true if element.texts.join('') !~ /^\s*$/s
|
707
|
+
end
|
708
|
+
false
|
709
|
+
end
|
710
|
+
|
711
|
+
# Called when a variable definition is encountered in the XML.
|
712
|
+
# A variable definition looks like
|
713
|
+
# <element attrname="name">value</element>
|
714
|
+
# where attrname matches the varattr setting.
|
715
|
+
def set_var(name, value)
|
716
|
+
@_var_values[name] = value
|
717
|
+
end
|
718
|
+
|
719
|
+
# Called during variable substitution to get the value for the
|
720
|
+
# named variable.
|
721
|
+
def get_var(name)
|
722
|
+
if @_var_values.has_key?(name)
|
723
|
+
return @_var_values[name]
|
724
|
+
else
|
725
|
+
return "${#{name}}"
|
726
|
+
end
|
727
|
+
end
|
728
|
+
|
729
|
+
# Recurses through a data structure building up and returning an
|
730
|
+
# XML representation of that structure as a string.
|
731
|
+
#
|
732
|
+
# ref::
|
733
|
+
# Reference to the data structure to be encoded.
|
734
|
+
# name::
|
735
|
+
# The XML tag name to be used for this item.
|
736
|
+
# indent::
|
737
|
+
# A string of spaces for use as the current indent level.
|
738
|
+
def value_to_xml(ref, name, indent)
|
739
|
+
named = !name.nil? && name != ''
|
740
|
+
nl = @options.has_key?('noindent') ? '' : "\n"
|
741
|
+
|
742
|
+
if !scalar(ref)
|
743
|
+
if @ancestors.member?(ref)
|
744
|
+
raise ArgumentError, "Circular data structures not supported!"
|
745
|
+
end
|
746
|
+
@ancestors << ref
|
747
|
+
else
|
748
|
+
if named
|
749
|
+
return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '</', name, '>', nl].join('')
|
750
|
+
else
|
751
|
+
return ref.to_s + nl
|
752
|
+
end
|
753
|
+
end
|
754
|
+
|
755
|
+
# Unfold hash to array if possible.
|
756
|
+
if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != ''
|
757
|
+
ref = hash_to_array(name, ref)
|
758
|
+
end
|
759
|
+
|
760
|
+
result = []
|
761
|
+
if ref.instance_of?(Hash)
|
762
|
+
# Reintermediate grouped values if applicable.
|
763
|
+
if @options.has_key?('grouptags')
|
764
|
+
ref.each { |key, value|
|
765
|
+
if @options['grouptags'].has_key?(key)
|
766
|
+
ref[key] = { @options['grouptags'][key] => value }
|
767
|
+
end
|
768
|
+
}
|
769
|
+
end
|
770
|
+
|
771
|
+
nested = []
|
772
|
+
text_content = nil
|
773
|
+
if named
|
774
|
+
result << indent << '<' << name
|
775
|
+
end
|
776
|
+
|
777
|
+
if !ref.empty?
|
778
|
+
ref.each { |key, value|
|
779
|
+
next if !key.nil? && key[0, 1] == '-'
|
780
|
+
if value.nil?
|
781
|
+
unless @options.has_key?('suppressempty') && @options['suppressempty'].nil?
|
782
|
+
raise ArgumentError, "Use of uninitialized value!"
|
783
|
+
end
|
784
|
+
value = {}
|
785
|
+
end
|
786
|
+
|
787
|
+
if !scalar(value) || @options['noattr']
|
788
|
+
nested << value_to_xml(value, key, indent + @options['indent'])
|
789
|
+
else
|
790
|
+
value = value.to_s
|
791
|
+
value = escape_value(value) unless @options['noescape']
|
792
|
+
if key == @options['contentkey']
|
793
|
+
text_content = value
|
794
|
+
else
|
795
|
+
result << ' ' << key << '="' << value << '"'
|
796
|
+
end
|
797
|
+
end
|
798
|
+
}
|
799
|
+
else
|
800
|
+
text_content = ''
|
801
|
+
end
|
802
|
+
|
803
|
+
if !nested.empty? || !text_content.nil?
|
804
|
+
if named
|
805
|
+
result << '>'
|
806
|
+
if !text_content.nil?
|
807
|
+
result << text_content
|
808
|
+
nested[0].sub!(/^\s+/, '') if !nested.empty?
|
809
|
+
else
|
810
|
+
result << nl
|
811
|
+
end
|
812
|
+
if !nested.empty?
|
813
|
+
result << nested << indent
|
814
|
+
end
|
815
|
+
result << '</' << name << '>' << nl
|
816
|
+
else
|
817
|
+
result << nested
|
818
|
+
end
|
819
|
+
else
|
820
|
+
result << ' />' << nl
|
821
|
+
end
|
822
|
+
elsif ref.instance_of?(Array)
|
823
|
+
ref.each { |value|
|
824
|
+
if scalar(value)
|
825
|
+
result << indent << '<' << name << '>'
|
826
|
+
result << (@options['noescape'] ? value.to_s : escape_value(value.to_s))
|
827
|
+
result << '</' << name << '>' << nl
|
828
|
+
elsif value.instance_of?(Hash)
|
829
|
+
result << value_to_xml(value, name, indent)
|
830
|
+
else
|
831
|
+
result << indent << '<' << name << '>' << nl
|
832
|
+
result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent'])
|
833
|
+
result << indent << '</' << name << '>' << nl
|
834
|
+
end
|
835
|
+
}
|
836
|
+
else
|
837
|
+
# Probably, this is obsolete.
|
838
|
+
raise ArgumentError, "Can't encode a value of type: #{ref.type}."
|
839
|
+
end
|
840
|
+
@ancestors.pop if !scalar(ref)
|
841
|
+
result.join('')
|
842
|
+
end
|
843
|
+
|
844
|
+
# Checks, if a certain value is a "scalar" value. Whatever
|
845
|
+
# that will be in Ruby ... ;-)
|
846
|
+
#
|
847
|
+
# value::
|
848
|
+
# Value to be checked.
|
849
|
+
def scalar(value)
|
850
|
+
return false if value.instance_of?(Hash) || value.instance_of?(Array)
|
851
|
+
return true
|
852
|
+
end
|
853
|
+
|
854
|
+
# Attempts to unfold a hash of hashes into an array of hashes. Returns
|
855
|
+
# a reference to th array on success or the original hash, if unfolding
|
856
|
+
# is not possible.
|
857
|
+
#
|
858
|
+
# parent::
|
859
|
+
#
|
860
|
+
# hashref::
|
861
|
+
# Reference to the hash to be unfolded.
|
862
|
+
def hash_to_array(parent, hashref)
|
863
|
+
arrayref = []
|
864
|
+
hashref.each { |key, value|
|
865
|
+
return hashref unless value.instance_of?(Hash)
|
866
|
+
|
867
|
+
if @options['keyattr'].instance_of?(Hash)
|
868
|
+
return hashref unless @options['keyattr'].has_key?(parent)
|
869
|
+
arrayref << { @options['keyattr'][parent][0] => key }.update(value)
|
870
|
+
else
|
871
|
+
arrayref << { @options['keyattr'][0] => key }.update(value)
|
872
|
+
end
|
873
|
+
}
|
874
|
+
arrayref
|
875
|
+
end
|
876
|
+
|
877
|
+
# Replaces XML markup characters by their external entities.
|
878
|
+
#
|
879
|
+
# data::
|
880
|
+
# The string to be escaped.
|
881
|
+
def escape_value(data)
|
882
|
+
return data if data.nil? || data == ''
|
883
|
+
result = data.dup
|
884
|
+
result.gsub!('&', '&')
|
885
|
+
result.gsub!('<', '<')
|
886
|
+
result.gsub!('>', '>')
|
887
|
+
result.gsub!('"', '"')
|
888
|
+
result.gsub!("'", ''')
|
889
|
+
result
|
890
|
+
end
|
891
|
+
|
892
|
+
# Removes leading and trailing whitespace and sequences of
|
893
|
+
# whitespaces from a string.
|
894
|
+
#
|
895
|
+
# text::
|
896
|
+
# String to be normalised.
|
897
|
+
def normalise_space(text)
|
898
|
+
text.sub!(/^\s+/, '')
|
899
|
+
text.sub!(/\s+$/, '')
|
900
|
+
text.gsub!(/\s\s+/, ' ')
|
901
|
+
text
|
902
|
+
end
|
903
|
+
|
904
|
+
# Checks, if an object is nil, an empty String or an empty Hash.
|
905
|
+
# Thanks to Norbert Gawor for a bugfix.
|
906
|
+
#
|
907
|
+
# value::
|
908
|
+
# Value to be checked for emptyness.
|
909
|
+
def empty(value)
|
910
|
+
case value
|
911
|
+
when Hash
|
912
|
+
return value.empty?
|
913
|
+
when String
|
914
|
+
return value !~ /\S/m
|
915
|
+
else
|
916
|
+
return value.nil?
|
917
|
+
end
|
918
|
+
end
|
919
|
+
|
920
|
+
# Converts a document node into a String.
|
921
|
+
# If the node could not be converted into a String
|
922
|
+
# for any reason, default will be returned.
|
923
|
+
#
|
924
|
+
# node::
|
925
|
+
# Document node to be converted.
|
926
|
+
# default::
|
927
|
+
# Value to be returned, if node could not be converted.
|
928
|
+
def node_to_text(node, default = nil)
|
929
|
+
if node.instance_of?(Element)
|
930
|
+
return node.texts.join('')
|
931
|
+
elsif node.instance_of?(Attribute)
|
932
|
+
return node.value.nil? ? default : node.value.strip
|
933
|
+
elsif node.instance_of?(Text)
|
934
|
+
return node.to_s.strip
|
935
|
+
else
|
936
|
+
return default
|
937
|
+
end
|
938
|
+
end
|
939
|
+
|
940
|
+
# Parses an XML string and returns the according document.
|
941
|
+
#
|
942
|
+
# xml_string::
|
943
|
+
# XML string to be parsed.
|
944
|
+
#
|
945
|
+
# The following exception may be raised:
|
946
|
+
#
|
947
|
+
# REXML::ParseException::
|
948
|
+
# If the specified file is not wellformed.
|
949
|
+
def parse(xml_string)
|
950
|
+
Document.new(xml_string)
|
951
|
+
end
|
952
|
+
|
953
|
+
# Searches in a list of paths for a certain file. Returns
|
954
|
+
# the full path to the file, if it could be found. Otherwise,
|
955
|
+
# an exception will be raised.
|
956
|
+
#
|
957
|
+
# filename::
|
958
|
+
# Name of the file to search for.
|
959
|
+
# searchpath::
|
960
|
+
# List of paths to search in.
|
961
|
+
def find_xml_file(file, searchpath)
|
962
|
+
filename = File::basename(file)
|
963
|
+
|
964
|
+
if filename != file
|
965
|
+
return file if File::file?(file)
|
966
|
+
else
|
967
|
+
searchpath.each { |path|
|
968
|
+
full_path = File::join(path, filename)
|
969
|
+
return full_path if File::file?(full_path)
|
970
|
+
}
|
971
|
+
end
|
972
|
+
|
973
|
+
if searchpath.empty?
|
974
|
+
return file if File::file?(file)
|
975
|
+
raise ArgumentError, "File does not exist: #{file}."
|
976
|
+
end
|
977
|
+
raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
|
978
|
+
end
|
979
|
+
|
980
|
+
# Loads and parses an XML configuration file.
|
981
|
+
#
|
982
|
+
# filename::
|
983
|
+
# Name of the configuration file to be loaded.
|
984
|
+
#
|
985
|
+
# The following exceptions may be raised:
|
986
|
+
#
|
987
|
+
# Errno::ENOENT::
|
988
|
+
# If the specified file does not exist.
|
989
|
+
# REXML::ParseException::
|
990
|
+
# If the specified file is not wellformed.
|
991
|
+
def load_xml_file(filename)
|
992
|
+
parse(File.readlines(filename).to_s)
|
993
|
+
end
|
994
|
+
|
995
|
+
# Caches the data belonging to a certain file.
|
996
|
+
#
|
997
|
+
# data::
|
998
|
+
# Data to be cached.
|
999
|
+
# filename::
|
1000
|
+
# Name of file the data was read from.
|
1001
|
+
def put_into_cache(data, filename)
|
1002
|
+
if @options.has_key?('cache')
|
1003
|
+
@options['cache'].each { |scheme|
|
1004
|
+
case(scheme)
|
1005
|
+
when 'storable'
|
1006
|
+
@@cache.save_storable(data, filename)
|
1007
|
+
when 'mem_share'
|
1008
|
+
@@cache.save_mem_share(data, filename)
|
1009
|
+
when 'mem_copy'
|
1010
|
+
@@cache.save_mem_copy(data, filename)
|
1011
|
+
else
|
1012
|
+
raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
|
1013
|
+
end
|
1014
|
+
}
|
1015
|
+
end
|
1016
|
+
end
|
1017
|
+
end
|
1018
|
+
|
1019
|
+
# vim:sw=2
|