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