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.

Files changed (101) hide show
  1. data/CHANGELOG +309 -16
  2. data/README +1 -1
  3. data/lib/action_controller.rb +5 -0
  4. data/lib/action_controller/assertions.rb +57 -12
  5. data/lib/action_controller/auto_complete.rb +47 -0
  6. data/lib/action_controller/base.rb +288 -258
  7. data/lib/action_controller/benchmarking.rb +8 -3
  8. data/lib/action_controller/caching.rb +88 -42
  9. data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
  10. data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
  11. data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
  12. data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
  13. data/lib/action_controller/cgi_process.rb +39 -11
  14. data/lib/action_controller/code_generation.rb +235 -0
  15. data/lib/action_controller/cookies.rb +14 -8
  16. data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
  17. data/lib/action_controller/filters.rb +8 -7
  18. data/lib/action_controller/helpers.rb +41 -6
  19. data/lib/action_controller/layout.rb +45 -16
  20. data/lib/action_controller/request.rb +86 -23
  21. data/lib/action_controller/rescue.rb +1 -0
  22. data/lib/action_controller/response.rb +1 -1
  23. data/lib/action_controller/routing.rb +536 -272
  24. data/lib/action_controller/scaffolding.rb +30 -25
  25. data/lib/action_controller/session/active_record_store.rb +251 -50
  26. data/lib/action_controller/streaming.rb +133 -0
  27. data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
  28. data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
  29. data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
  30. data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
  31. data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
  32. data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
  33. data/lib/action_controller/test_process.rb +68 -47
  34. data/lib/action_controller/upload_progress.rb +421 -0
  35. data/lib/action_controller/url_rewriter.rb +8 -11
  36. data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
  37. data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
  38. data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
  39. data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
  40. data/lib/action_controller/vendor/xml_simple.rb +1019 -0
  41. data/lib/action_controller/verification.rb +36 -30
  42. data/lib/action_view/base.rb +21 -14
  43. data/lib/action_view/helpers/active_record_helper.rb +15 -13
  44. data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
  45. data/lib/action_view/helpers/benchmark_helper.rb +24 -0
  46. data/lib/action_view/helpers/capture_helper.rb +7 -5
  47. data/lib/action_view/helpers/date_helper.rb +63 -46
  48. data/lib/action_view/helpers/form_helper.rb +7 -1
  49. data/lib/action_view/helpers/form_options_helper.rb +19 -11
  50. data/lib/action_view/helpers/form_tag_helper.rb +5 -1
  51. data/lib/action_view/helpers/javascript_helper.rb +403 -35
  52. data/lib/action_view/helpers/javascripts/controls.js +261 -0
  53. data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
  54. data/lib/action_view/helpers/javascripts/effects.js +570 -0
  55. data/lib/action_view/helpers/javascripts/prototype.js +633 -371
  56. data/lib/action_view/helpers/number_helper.rb +11 -13
  57. data/lib/action_view/helpers/tag_helper.rb +1 -2
  58. data/lib/action_view/helpers/text_helper.rb +69 -6
  59. data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
  60. data/lib/action_view/helpers/url_helper.rb +98 -3
  61. data/lib/action_view/partials.rb +14 -8
  62. data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
  63. data/rakefile +13 -5
  64. data/test/abstract_unit.rb +1 -1
  65. data/test/controller/action_pack_assertions_test.rb +52 -9
  66. data/test/controller/active_record_assertions_test.rb +119 -120
  67. data/test/controller/active_record_store_test.rb +111 -0
  68. data/test/controller/addresses_render_test.rb +45 -0
  69. data/test/controller/caching_filestore.rb +92 -0
  70. data/test/controller/capture_test.rb +39 -0
  71. data/test/controller/cgi_test.rb +40 -3
  72. data/test/controller/helper_test.rb +65 -13
  73. data/test/controller/multipart_progress_testx.rb +365 -0
  74. data/test/controller/new_render_test.rb +263 -0
  75. data/test/controller/redirect_test.rb +64 -0
  76. data/test/controller/render_test.rb +20 -21
  77. data/test/controller/request_test.rb +83 -3
  78. data/test/controller/routing_test.rb +702 -0
  79. data/test/controller/send_file_test.rb +2 -0
  80. data/test/controller/test_test.rb +44 -8
  81. data/test/controller/upload_progress_testx.rb +89 -0
  82. data/test/controller/verification_test.rb +94 -29
  83. data/test/fixtures/addresses/list.rhtml +1 -0
  84. data/test/fixtures/test/capturing.rhtml +4 -0
  85. data/test/fixtures/test/list.rhtml +1 -1
  86. data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
  87. data/test/template/active_record_helper_test.rb +30 -15
  88. data/test/template/asset_tag_helper_test.rb +12 -5
  89. data/test/template/benchmark_helper_test.rb +72 -0
  90. data/test/template/date_helper_test.rb +69 -0
  91. data/test/template/form_helper_test.rb +18 -10
  92. data/test/template/form_options_helper_test.rb +40 -5
  93. data/test/template/javascript_helper.rb +149 -2
  94. data/test/template/number_helper_test.rb +2 -0
  95. data/test/template/tag_helper_test.rb +4 -0
  96. data/test/template/text_helper_test.rb +36 -0
  97. data/test/template/upload_progress_helper_testx.rb +272 -0
  98. data/test/template/url_helper_test.rb +30 -0
  99. metadata +30 -6
  100. data/test/controller/layout_test.rb +0 -49
  101. 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? && !node.childless?
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
- warn <<EOF.strip
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
- scanner.skip(/</) or raise "expected <"
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*>/) or raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
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
- @name =~ /^(img|br|hr|link|meta|area|base|basefont|col|frame|input|isindex|param)$/o
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 { |k,v| s << " #{k}='#{v.gsub(/'/,"\\\\'")}'" }
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
- # only Text nodes have content
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
- @current_line += text.scan(/\r\n|\r|\n/).length
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(/['">]/) or break
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 (delim = @scanner.matched) == ">"
84
- # consume the conqued region
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
@@ -1,9 +1,9 @@
1
- module HTML#:nodoc:
2
- module Version#:nodoc:
1
+ module HTML #:nodoc:
2
+ module Version #:nodoc:
3
3
 
4
4
  MAJOR = 0
5
5
  MINOR = 5
6
- TINY = 0
6
+ TINY = 2
7
7
 
8
8
  STRING = [ MAJOR, MINOR, TINY ].join(".")
9
9
 
@@ -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!('&', '&amp;')
885
+ result.gsub!('<', '&lt;')
886
+ result.gsub!('>', '&gt;')
887
+ result.gsub!('"', '&quot;')
888
+ result.gsub!("'", '&apos;')
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