rmthemegen 0.0.23 → 0.0.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1030 @@
1
+ # = XmlSimple
2
+ #
3
+ # Author:: Maik Schmidt <contact@maik-schmidt.de>
4
+ # Copyright:: Copyright (c) 2003-2011 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.15'
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 intializes 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).dup
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.read)
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) || string.kind_of?(Zlib::GzipReader)
189
+ @doc = parse(string.read)
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 attrprefix
271
+ ),
272
+ 'out' => %w(
273
+ keyattr keeproot contentkey noattr rootname
274
+ xmldeclaration outputfile noescape suppressempty
275
+ anonymoustag indent grouptags noindent attrprefix
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.to_s.downcase.gsub(/_/, '')
303
+ if !known_options.member?(lkey)
304
+ raise ArgumentError, "Unrecognised option: #{lkey}."
305
+ end
306
+ result[lkey] = value
307
+ }
308
+ result
309
+ end
310
+
311
+ # Merges a set of options with the default options.
312
+ #
313
+ # direction::
314
+ # 'in': If options should be handled for xml_in.
315
+ # 'out': If options should be handled for xml_out.
316
+ # options::
317
+ # Options to be merged with the default options.
318
+ def handle_options(direction, options)
319
+ @options = options || Hash.new
320
+
321
+ raise ArgumentError, "Options must be a Hash!" unless @options.instance_of?(Hash)
322
+
323
+ unless KNOWN_OPTIONS.has_key?(direction)
324
+ raise ArgumentError, "Unknown direction: <#{direction}>."
325
+ end
326
+
327
+ known_options = KNOWN_OPTIONS[direction]
328
+ @options = normalize_option_names(@options, known_options)
329
+
330
+ unless @default_options.nil?
331
+ known_options.each { |option|
332
+ unless @options.has_key?(option)
333
+ if @default_options.has_key?(option)
334
+ @options[option] = @default_options[option]
335
+ end
336
+ end
337
+ }
338
+ end
339
+
340
+ unless @options.has_key?('noattr')
341
+ @options['noattr'] = false
342
+ end
343
+
344
+ if @options.has_key?('rootname')
345
+ @options['rootname'] = '' if @options['rootname'].nil?
346
+ else
347
+ @options['rootname'] = DEF_ROOT_NAME
348
+ end
349
+
350
+ if @options.has_key?('xmldeclaration') && @options['xmldeclaration'] == true
351
+ @options['xmldeclaration'] = DEF_XML_DECLARATION
352
+ end
353
+
354
+ @options['keytosymbol'] = DEF_KEY_TO_SYMBOL unless @options.has_key?('keytosymbol')
355
+
356
+ if @options.has_key?('contentkey')
357
+ if @options['contentkey'] =~ /^-(.*)$/
358
+ @options['contentkey'] = $1
359
+ @options['collapseagain'] = true
360
+ end
361
+ else
362
+ @options['contentkey'] = DEF_CONTENT_KEY
363
+ end
364
+
365
+ unless @options.has_key?('normalisespace')
366
+ @options['normalisespace'] = @options['normalizespace']
367
+ end
368
+ @options['normalisespace'] = 0 if @options['normalisespace'].nil?
369
+
370
+ if @options.has_key?('searchpath')
371
+ unless @options['searchpath'].instance_of?(Array)
372
+ @options['searchpath'] = [ @options['searchpath'] ]
373
+ end
374
+ else
375
+ @options['searchpath'] = []
376
+ end
377
+
378
+ if @options.has_key?('cache') && scalar(@options['cache'])
379
+ @options['cache'] = [ @options['cache'] ]
380
+ end
381
+
382
+ @options['anonymoustag'] = DEF_ANONYMOUS_TAG unless @options.has_key?('anonymoustag')
383
+
384
+ if !@options.has_key?('indent') || @options['indent'].nil?
385
+ @options['indent'] = DEF_INDENTATION
386
+ end
387
+
388
+ @options['indent'] = '' if @options.has_key?('noindent')
389
+
390
+ # Special cleanup for 'keyattr' which could be an array or
391
+ # a hash or left to default to array.
392
+ if @options.has_key?('keyattr')
393
+ if !scalar(@options['keyattr'])
394
+ # Convert keyattr => { elem => '+attr' }
395
+ # to keyattr => { elem => ['attr', '+'] }
396
+ if @options['keyattr'].instance_of?(Hash)
397
+ @options['keyattr'].each { |key, value|
398
+ if value =~ /^([-+])?(.*)$/
399
+ @options['keyattr'][key] = [$2, $1 ? $1 : '']
400
+ end
401
+ }
402
+ elsif !@options['keyattr'].instance_of?(Array)
403
+ raise ArgumentError, "'keyattr' must be String, Hash, or Array!"
404
+ end
405
+ else
406
+ @options['keyattr'] = [ @options['keyattr'] ]
407
+ end
408
+ else
409
+ @options['keyattr'] = DEF_KEY_ATTRIBUTES
410
+ end
411
+
412
+ if @options.has_key?('forcearray')
413
+ if @options['forcearray'].instance_of?(Regexp)
414
+ @options['forcearray'] = [ @options['forcearray'] ]
415
+ end
416
+
417
+ if @options['forcearray'].instance_of?(Array)
418
+ force_list = @options['forcearray']
419
+ unless force_list.empty?
420
+ @options['forcearray'] = {}
421
+ force_list.each { |tag|
422
+ if tag.instance_of?(Regexp)
423
+ unless @options['forcearray']['_regex'].instance_of?(Array)
424
+ @options['forcearray']['_regex'] = []
425
+ end
426
+ @options['forcearray']['_regex'] << tag
427
+ else
428
+ @options['forcearray'][tag] = true
429
+ end
430
+ }
431
+ else
432
+ @options['forcearray'] = false
433
+ end
434
+ else
435
+ @options['forcearray'] = @options['forcearray'] ? true : false
436
+ end
437
+ else
438
+ @options['forcearray'] = DEF_FORCE_ARRAY
439
+ end
440
+
441
+ if @options.has_key?('grouptags') && !@options['grouptags'].instance_of?(Hash)
442
+ raise ArgumentError, "Illegal value for 'GroupTags' option - expected a Hash."
443
+ end
444
+
445
+ if @options.has_key?('variables') && !@options['variables'].instance_of?(Hash)
446
+ raise ArgumentError, "Illegal value for 'Variables' option - expected a Hash."
447
+ end
448
+
449
+ if @options.has_key?('variables')
450
+ @_var_values = @options['variables']
451
+ elsif @options.has_key?('varattr')
452
+ @_var_values = {}
453
+ end
454
+ end
455
+
456
+ # Actually converts an XML document element into a data structure.
457
+ #
458
+ # element::
459
+ # The document element to be collapsed.
460
+ def collapse(element)
461
+ result = @options['noattr'] ? {} : get_attributes(element)
462
+
463
+ if @options['normalisespace'] == 2
464
+ result.each { |k, v| result[k] = normalise_space(v) }
465
+ end
466
+
467
+ if element.has_elements?
468
+ element.each_element { |child|
469
+ value = collapse(child)
470
+ if empty(value) && (element.attributes.empty? || @options['noattr'])
471
+ next if @options.has_key?('suppressempty') && @options['suppressempty'] == true
472
+ end
473
+ result = merge(result, child.name, value)
474
+ }
475
+ if has_mixed_content?(element)
476
+ # normalisespace?
477
+ content = element.texts.map { |x| x.to_s }
478
+ content = content[0] if content.size == 1
479
+ result[@options['contentkey']] = content
480
+ end
481
+ elsif element.has_text? # i.e. it has only text.
482
+ return collapse_text_node(result, element)
483
+ end
484
+
485
+ # Turn Arrays into Hashes if key fields present.
486
+ count = fold_arrays(result)
487
+
488
+ # Disintermediate grouped tags.
489
+ if @options.has_key?('grouptags')
490
+ result.each { |key, value|
491
+ next unless (value.instance_of?(Hash) && (value.size == 1))
492
+ child_key, child_value = value.to_a[0]
493
+ if @options['grouptags'][key] == child_key
494
+ result[key] = child_value
495
+ end
496
+ }
497
+ end
498
+
499
+ # Fold Hashes containing a single anonymous Array up into just the Array.
500
+ if count == 1
501
+ anonymoustag = @options['anonymoustag']
502
+ if result.has_key?(anonymoustag) && result[anonymoustag].instance_of?(Array)
503
+ return result[anonymoustag]
504
+ end
505
+ end
506
+
507
+ if result.empty? && @options.has_key?('suppressempty')
508
+ return @options['suppressempty'] == '' ? '' : nil
509
+ end
510
+
511
+ result
512
+ end
513
+
514
+ # Collapses a text node and merges it with an existing Hash, if
515
+ # possible.
516
+ # Thanks to Curtis Schofield for reporting a subtle bug.
517
+ #
518
+ # hash::
519
+ # Hash to merge text node value with, if possible.
520
+ # element::
521
+ # Text node to be collapsed.
522
+ def collapse_text_node(hash, element)
523
+ value = node_to_text(element)
524
+ if empty(value) && !element.has_attributes?
525
+ return {}
526
+ end
527
+
528
+ if element.has_attributes? && !@options['noattr']
529
+ return merge(hash, @options['contentkey'], value)
530
+ else
531
+ if @options['forcecontent']
532
+ return merge(hash, @options['contentkey'], value)
533
+ else
534
+ return value
535
+ end
536
+ end
537
+ end
538
+
539
+ # Folds all arrays in a Hash.
540
+ #
541
+ # hash::
542
+ # Hash to be folded.
543
+ def fold_arrays(hash)
544
+ fold_amount = 0
545
+ keyattr = @options['keyattr']
546
+ if (keyattr.instance_of?(Array) || keyattr.instance_of?(Hash))
547
+ hash.each { |key, value|
548
+ if value.instance_of?(Array)
549
+ if keyattr.instance_of?(Array)
550
+ hash[key] = fold_array(value)
551
+ else
552
+ hash[key] = fold_array_by_name(key, value)
553
+ end
554
+ fold_amount += 1
555
+ end
556
+ }
557
+ end
558
+ fold_amount
559
+ end
560
+
561
+ # Folds an Array to a Hash, if possible. Folding happens
562
+ # according to the content of keyattr, which has to be
563
+ # an array.
564
+ #
565
+ # array::
566
+ # Array to be folded.
567
+ def fold_array(array)
568
+ hash = Hash.new
569
+ array.each { |x|
570
+ return array unless x.instance_of?(Hash)
571
+ key_matched = false
572
+ @options['keyattr'].each { |key|
573
+ if x.has_key?(key)
574
+ key_matched = true
575
+ value = x[key]
576
+ return array if value.instance_of?(Hash) || value.instance_of?(Array)
577
+ value = normalise_space(value) if @options['normalisespace'] == 1
578
+ x.delete(key)
579
+ hash[value] = x
580
+ break
581
+ end
582
+ }
583
+ return array unless key_matched
584
+ }
585
+ hash = collapse_content(hash) if @options['collapseagain']
586
+ hash
587
+ end
588
+
589
+ # Folds an Array to a Hash, if possible. Folding happens
590
+ # according to the content of keyattr, which has to be
591
+ # a Hash.
592
+ #
593
+ # name::
594
+ # Name of the attribute to be folded upon.
595
+ # array::
596
+ # Array to be folded.
597
+ def fold_array_by_name(name, array)
598
+ return array unless @options['keyattr'].has_key?(name)
599
+ key, flag = @options['keyattr'][name]
600
+
601
+ hash = Hash.new
602
+ array.each { |x|
603
+ if x.instance_of?(Hash) && x.has_key?(key)
604
+ value = x[key]
605
+ return array if value.instance_of?(Hash) || value.instance_of?(Array)
606
+ value = normalise_space(value) if @options['normalisespace'] == 1
607
+ hash[value] = x
608
+ hash[value]["-#{key}"] = hash[value][key] if flag == '-'
609
+ hash[value].delete(key) unless flag == '+'
610
+ else
611
+ $stderr.puts("Warning: <#{name}> element has no '#{key}' attribute.")
612
+ return array
613
+ end
614
+ }
615
+ hash = collapse_content(hash) if @options['collapseagain']
616
+ hash
617
+ end
618
+
619
+ # Tries to collapse a Hash even more ;-)
620
+ #
621
+ # hash::
622
+ # Hash to be collapsed again.
623
+ def collapse_content(hash)
624
+ content_key = @options['contentkey']
625
+ hash.each_value { |value|
626
+ return hash unless value.instance_of?(Hash) && value.size == 1 && value.has_key?(content_key)
627
+ hash.each_key { |key| hash[key] = hash[key][content_key] }
628
+ }
629
+ hash
630
+ end
631
+
632
+ # Adds a new key/value pair to an existing Hash. If the key to be added
633
+ # does already exist and the existing value associated with key is not
634
+ # an Array, it will be converted into an Array. Then the new value is
635
+ # appended to that Array.
636
+ #
637
+ # hash::
638
+ # Hash to add key/value pair to.
639
+ # key::
640
+ # Key to be added.
641
+ # value::
642
+ # Value to be associated with key.
643
+ def merge(hash, key, value)
644
+ if value.instance_of?(String)
645
+ value = normalise_space(value) if @options['normalisespace'] == 2
646
+
647
+ # do variable substitutions
648
+ unless @_var_values.nil? || @_var_values.empty?
649
+ value.gsub!(/\$\{(\w+)\}/) { |x| get_var($1) }
650
+ end
651
+
652
+ # look for variable definitions
653
+ if @options.has_key?('varattr')
654
+ varattr = @options['varattr']
655
+ if hash.has_key?(varattr)
656
+ set_var(hash[varattr], value)
657
+ end
658
+ end
659
+ end
660
+
661
+ #patch for converting keys to symbols
662
+ if @options.has_key?('keytosymbol')
663
+ if @options['keytosymbol'] == true
664
+ key = key.to_s.downcase.to_sym
665
+ end
666
+ end
667
+
668
+ if hash.has_key?(key)
669
+ if hash[key].instance_of?(Array)
670
+ hash[key] << value
671
+ else
672
+ hash[key] = [ hash[key], value ]
673
+ end
674
+ elsif value.instance_of?(Array) # Handle anonymous arrays.
675
+ hash[key] = [ value ]
676
+ else
677
+ if force_array?(key)
678
+ hash[key] = [ value ]
679
+ else
680
+ hash[key] = value
681
+ end
682
+ end
683
+ hash
684
+ end
685
+
686
+ # Checks, if the 'forcearray' option has to be used for
687
+ # a certain key.
688
+ def force_array?(key)
689
+ return false if key == @options['contentkey']
690
+ return true if @options['forcearray'] == true
691
+ forcearray = @options['forcearray']
692
+ if forcearray.instance_of?(Hash)
693
+ return true if forcearray.has_key?(key)
694
+ return false unless forcearray.has_key?('_regex')
695
+ forcearray['_regex'].each { |x| return true if key =~ x }
696
+ end
697
+ return false
698
+ end
699
+
700
+ # Converts the attributes array of a document node into a Hash.
701
+ # Returns an empty Hash, if node has no attributes.
702
+ #
703
+ # node::
704
+ # Document node to extract attributes from.
705
+ def get_attributes(node)
706
+ attributes = {}
707
+ if @options['attrprefix']
708
+ node.attributes.each { |n,v| attributes["@" + n] = v }
709
+ else
710
+ node.attributes.each { |n,v| attributes[n] = v }
711
+ end
712
+ attributes
713
+ end
714
+
715
+ # Determines, if a document element has mixed content.
716
+ #
717
+ # element::
718
+ # Document element to be checked.
719
+ def has_mixed_content?(element)
720
+ if element.has_text? && element.has_elements?
721
+ return true if element.texts.join('') !~ /^\s*$/s
722
+ end
723
+ false
724
+ end
725
+
726
+ # Called when a variable definition is encountered in the XML.
727
+ # A variable definition looks like
728
+ # <element attrname="name">value</element>
729
+ # where attrname matches the varattr setting.
730
+ def set_var(name, value)
731
+ @_var_values[name] = value
732
+ end
733
+
734
+ # Called during variable substitution to get the value for the
735
+ # named variable.
736
+ def get_var(name)
737
+ if @_var_values.has_key?(name)
738
+ return @_var_values[name]
739
+ else
740
+ return "${#{name}}"
741
+ end
742
+ end
743
+
744
+ # Recurses through a data structure building up and returning an
745
+ # XML representation of that structure as a string.
746
+ #
747
+ # ref::
748
+ # Reference to the data structure to be encoded.
749
+ # name::
750
+ # The XML tag name to be used for this item.
751
+ # indent::
752
+ # A string of spaces for use as the current indent level.
753
+ def value_to_xml(ref, name, indent)
754
+ named = !name.nil? && name != ''
755
+ nl = @options.has_key?('noindent') ? '' : "\n"
756
+
757
+ if !scalar(ref)
758
+ if @ancestors.member?(ref)
759
+ raise ArgumentError, "Circular data structures not supported!"
760
+ end
761
+ @ancestors << ref
762
+ else
763
+ if named
764
+ return [indent, '<', name, '>', @options['noescape'] ? ref.to_s : escape_value(ref.to_s), '</', name, '>', nl].join('')
765
+ else
766
+ return ref.to_s + nl
767
+ end
768
+ end
769
+
770
+ # Unfold hash to array if possible.
771
+ if ref.instance_of?(Hash) && !ref.empty? && !@options['keyattr'].empty? && indent != ''
772
+ ref = hash_to_array(name, ref)
773
+ end
774
+
775
+ result = []
776
+ if ref.instance_of?(Hash)
777
+ # Reintermediate grouped values if applicable.
778
+ if @options.has_key?('grouptags')
779
+ ref.each { |key, value|
780
+ if @options['grouptags'].has_key?(key)
781
+ ref[key] = { @options['grouptags'][key] => value }
782
+ end
783
+ }
784
+ end
785
+
786
+ nested = []
787
+ text_content = nil
788
+ if named
789
+ result << indent << '<' << name
790
+ end
791
+
792
+ if !ref.empty?
793
+ ref.each { |key, value|
794
+ next if !key.nil? && key.to_s[0, 1] == '-'
795
+ if value.nil?
796
+ unless @options.has_key?('suppressempty') && @options['suppressempty'].nil?
797
+ raise ArgumentError, "Use of uninitialized value!"
798
+ end
799
+ value = {}
800
+ end
801
+
802
+ # Check for the '@' attribute prefix to allow separation of attributes and elements
803
+
804
+ if (@options['noattr'] ||
805
+ (@options['attrprefix'] && !(key =~ /^@(.*)/)) ||
806
+ !scalar(value)
807
+ ) &&
808
+ key != @options['contentkey']
809
+ nested << value_to_xml(value, key, indent + @options['indent'])
810
+ else
811
+ value = value.to_s
812
+ value = escape_value(value) unless @options['noescape']
813
+ if key == @options['contentkey']
814
+ text_content = value
815
+ else
816
+ result << ' ' << ($1||key) << '="' << value << '"'
817
+ end
818
+ end
819
+ }
820
+ else
821
+ text_content = ''
822
+ end
823
+
824
+ if !nested.empty? || !text_content.nil?
825
+ if named
826
+ result << '>'
827
+ if !text_content.nil?
828
+ result << text_content
829
+ nested[0].sub!(/^\s+/, '') if !nested.empty?
830
+ else
831
+ result << nl
832
+ end
833
+ if !nested.empty?
834
+ result << nested << indent
835
+ end
836
+ result << '</' << name << '>' << nl
837
+ else
838
+ result << nested
839
+ end
840
+ else
841
+ result << ' />' << nl
842
+ end
843
+ elsif ref.instance_of?(Array)
844
+ ref.each { |value|
845
+ if scalar(value)
846
+ result << indent << '<' << name << '>'
847
+ result << (@options['noescape'] ? value.to_s : escape_value(value.to_s))
848
+ result << '</' << name << '>' << nl
849
+ elsif value.instance_of?(Hash)
850
+ result << value_to_xml(value, name, indent)
851
+ else
852
+ result << indent << '<' << name << '>' << nl
853
+ result << value_to_xml(value, @options['anonymoustag'], indent + @options['indent'])
854
+ result << indent << '</' << name << '>' << nl
855
+ end
856
+ }
857
+ else
858
+ # Probably, this is obsolete.
859
+ raise ArgumentError, "Can't encode a value of type: #{ref.type}."
860
+ end
861
+ @ancestors.pop if !scalar(ref)
862
+ result.join('')
863
+ end
864
+
865
+ # Checks, if a certain value is a "scalar" value. Whatever
866
+ # that will be in Ruby ... ;-)
867
+ #
868
+ # value::
869
+ # Value to be checked.
870
+ def scalar(value)
871
+ return false if value.instance_of?(Hash) || value.instance_of?(Array)
872
+ return true
873
+ end
874
+
875
+ # Attempts to unfold a hash of hashes into an array of hashes. Returns
876
+ # a reference to th array on success or the original hash, if unfolding
877
+ # is not possible.
878
+ #
879
+ # parent::
880
+ #
881
+ # hashref::
882
+ # Reference to the hash to be unfolded.
883
+ def hash_to_array(parent, hashref)
884
+ arrayref = []
885
+ hashref.each { |key, value|
886
+ return hashref unless value.instance_of?(Hash)
887
+
888
+ if @options['keyattr'].instance_of?(Hash)
889
+ return hashref unless @options['keyattr'].has_key?(parent)
890
+ arrayref << { @options['keyattr'][parent][0] => key }.update(value)
891
+ else
892
+ arrayref << { @options['keyattr'][0] => key }.update(value)
893
+ end
894
+ }
895
+ arrayref
896
+ end
897
+
898
+ # Replaces XML markup characters by their external entities.
899
+ #
900
+ # data::
901
+ # The string to be escaped.
902
+ def escape_value(data)
903
+ Text::normalize(data)
904
+ end
905
+
906
+ # Removes leading and trailing whitespace and sequences of
907
+ # whitespaces from a string.
908
+ #
909
+ # text::
910
+ # String to be normalised.
911
+ def normalise_space(text)
912
+ text.strip.gsub(/\s\s+/, ' ')
913
+ end
914
+
915
+ # Checks, if an object is nil, an empty String or an empty Hash.
916
+ # Thanks to Norbert Gawor for a bugfix.
917
+ #
918
+ # value::
919
+ # Value to be checked for emptyness.
920
+ def empty(value)
921
+ case value
922
+ when Hash
923
+ return value.empty?
924
+ when String
925
+ return value !~ /\S/m
926
+ else
927
+ return value.nil?
928
+ end
929
+ end
930
+
931
+ # Converts a document node into a String.
932
+ # If the node could not be converted into a String
933
+ # for any reason, default will be returned.
934
+ #
935
+ # node::
936
+ # Document node to be converted.
937
+ # default::
938
+ # Value to be returned, if node could not be converted.
939
+ def node_to_text(node, default = nil)
940
+ if node.instance_of?(REXML::Element)
941
+ node.texts.map { |t| t.value }.join('')
942
+ elsif node.instance_of?(REXML::Attribute)
943
+ node.value.nil? ? default : node.value.strip
944
+ elsif node.instance_of?(REXML::Text)
945
+ node.value.strip
946
+ else
947
+ default
948
+ end
949
+ end
950
+
951
+ # Parses an XML string and returns the according document.
952
+ #
953
+ # xml_string::
954
+ # XML string to be parsed.
955
+ #
956
+ # The following exception may be raised:
957
+ #
958
+ # REXML::ParseException::
959
+ # If the specified file is not wellformed.
960
+ def parse(xml_string)
961
+ Document.new(xml_string)
962
+ end
963
+
964
+ # Searches in a list of paths for a certain file. Returns
965
+ # the full path to the file, if it could be found. Otherwise,
966
+ # an exception will be raised.
967
+ #
968
+ # filename::
969
+ # Name of the file to search for.
970
+ # searchpath::
971
+ # List of paths to search in.
972
+ def find_xml_file(file, searchpath)
973
+ filename = File::basename(file)
974
+
975
+ if filename != file
976
+ return file if File::file?(file)
977
+ else
978
+ searchpath.each { |path|
979
+ full_path = File::join(path, filename)
980
+ return full_path if File::file?(full_path)
981
+ }
982
+ end
983
+
984
+ if searchpath.empty?
985
+ return file if File::file?(file)
986
+ raise ArgumentError, "File does not exist: #{file}."
987
+ end
988
+ raise ArgumentError, "Could not find <#{filename}> in <#{searchpath.join(':')}>"
989
+ end
990
+
991
+ # Loads and parses an XML configuration file.
992
+ #
993
+ # filename::
994
+ # Name of the configuration file to be loaded.
995
+ #
996
+ # The following exceptions may be raised:
997
+ #
998
+ # Errno::ENOENT::
999
+ # If the specified file does not exist.
1000
+ # REXML::ParseException::
1001
+ # If the specified file is not wellformed.
1002
+ def load_xml_file(filename)
1003
+ parse(IO::read(filename))
1004
+ end
1005
+
1006
+ # Caches the data belonging to a certain file.
1007
+ #
1008
+ # data::
1009
+ # Data to be cached.
1010
+ # filename::
1011
+ # Name of file the data was read from.
1012
+ def put_into_cache(data, filename)
1013
+ if @options.has_key?('cache')
1014
+ @options['cache'].each { |scheme|
1015
+ case(scheme)
1016
+ when 'storable'
1017
+ @@cache.save_storable(data, filename)
1018
+ when 'mem_share'
1019
+ @@cache.save_mem_share(data, filename)
1020
+ when 'mem_copy'
1021
+ @@cache.save_mem_copy(data, filename)
1022
+ else
1023
+ raise ArgumentError, "Unsupported caching scheme: <#{scheme}>."
1024
+ end
1025
+ }
1026
+ end
1027
+ end
1028
+ end
1029
+
1030
+ # vim:sw=2