rmthemegen 0.0.23 → 0.0.25

Sign up to get free protection for your applications and to get access to all the features.
@@ -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